This commit is contained in:
51
frontend/storybook/.storybook/ThemeDecorator.tsx
Normal file
51
frontend/storybook/.storybook/ThemeDecorator.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import type { ThemeName } from "@monkeytype/schemas/configs";
|
||||
import type { JSXElement } from "solid-js";
|
||||
|
||||
import { ThemesList, ThemeWithName } from "../../src/ts/constants/themes";
|
||||
|
||||
type StoryContext = {
|
||||
globals: { theme?: string };
|
||||
};
|
||||
|
||||
const themeMap = new Map(ThemesList.map((t) => [t.name, t]));
|
||||
|
||||
let currentThemeLink: HTMLLinkElement | null = null;
|
||||
|
||||
export function ThemeDecorator(
|
||||
Story: () => JSXElement,
|
||||
context: StoryContext,
|
||||
): JSXElement {
|
||||
const themeName = (context.globals.theme ?? "serika_dark") as ThemeName;
|
||||
const theme =
|
||||
themeMap.get(themeName) ?? (themeMap.get("serika_dark") as ThemeWithName);
|
||||
|
||||
const root = document.documentElement;
|
||||
root.style.setProperty("--bg-color", theme.bg);
|
||||
root.style.setProperty("--main-color", theme.main);
|
||||
root.style.setProperty("--caret-color", theme.caret);
|
||||
root.style.setProperty("--sub-color", theme.sub);
|
||||
root.style.setProperty("--sub-alt-color", theme.subAlt);
|
||||
root.style.setProperty("--text-color", theme.text);
|
||||
root.style.setProperty("--error-color", theme.error);
|
||||
root.style.setProperty("--error-extra-color", theme.errorExtra);
|
||||
root.style.setProperty("--colorful-error-color", theme.colorfulError);
|
||||
root.style.setProperty(
|
||||
"--colorful-error-extra-color",
|
||||
theme.colorfulErrorExtra,
|
||||
);
|
||||
|
||||
// Load/unload theme CSS file
|
||||
if (currentThemeLink) {
|
||||
currentThemeLink.remove();
|
||||
currentThemeLink = null;
|
||||
}
|
||||
if (theme.hasCss) {
|
||||
const link = document.createElement("link");
|
||||
link.rel = "stylesheet";
|
||||
link.href = `/themes/${themeName}.css`;
|
||||
document.head.appendChild(link);
|
||||
currentThemeLink = link;
|
||||
}
|
||||
|
||||
return Story();
|
||||
}
|
||||
153
frontend/storybook/.storybook/main.ts
Normal file
153
frontend/storybook/.storybook/main.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import { defineMain } from "storybook-solidjs-vite";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import type { Plugin } from "vite";
|
||||
|
||||
function stubVirtualEnvConfig(): Plugin {
|
||||
const id = "virtual:env-config";
|
||||
const resolved = "\0" + id;
|
||||
return {
|
||||
name: "stub-virtual-env-config",
|
||||
resolveId(source) {
|
||||
if (source === id) return resolved;
|
||||
},
|
||||
load(loadId) {
|
||||
if (loadId === resolved) {
|
||||
return `export const envConfig = ${JSON.stringify({
|
||||
isDevelopment: true,
|
||||
backendUrl: "http://localhost:5005",
|
||||
clientVersion: "storybook",
|
||||
recaptchaSiteKey: "",
|
||||
quickLoginEmail: undefined,
|
||||
quickLoginPassword: undefined,
|
||||
})};`;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function patchQsrToNotThrow(): Plugin {
|
||||
return {
|
||||
name: "patch-qsr-not-throw",
|
||||
enforce: "pre",
|
||||
transform(code, id) {
|
||||
if (!id.includes("utils/dom")) return;
|
||||
// Replace the throw in qsr with creating a dummy element
|
||||
return code.replaceAll(
|
||||
`throw new Error(\`Required element not found: \${selector}\`);`,
|
||||
`console.warn(\`[storybook] qsr: element not found: \${selector}, returning dummy\`);
|
||||
return new ElementWithUtils(document.createElement("div") as T);`,
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function patchAnimatedModalToNotThrow(): Plugin {
|
||||
return {
|
||||
name: "patch-animated-modal-not-throw",
|
||||
enforce: "pre",
|
||||
transform(code, id) {
|
||||
if (!id.includes("utils/animated-modal")) return;
|
||||
return code
|
||||
.replaceAll(
|
||||
`throw new Error(
|
||||
\`Dialog element with id \${constructorParams.dialogId} not found\`,
|
||||
);`,
|
||||
`console.warn(\`[storybook] AnimatedModal: dialog #\${constructorParams.dialogId} not found\`); return;`,
|
||||
)
|
||||
.replace(
|
||||
`throw new Error("Animated dialog must be an HTMLDialogElement");`,
|
||||
`console.warn("[storybook] AnimatedModal: element is not a dialog"); return;`,
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function stubChartController(): Plugin {
|
||||
const stubId = "\0stub-chart-controller";
|
||||
const stubCode = `
|
||||
const noop = () => {};
|
||||
const fakeScale = new Proxy({}, { get: () => "" , set: () => true });
|
||||
const fakeDataset = new Proxy({}, { get: () => [], set: () => true });
|
||||
const fakeChart = {
|
||||
data: { labels: [] },
|
||||
options: { plugins: {} },
|
||||
getDataset: () => fakeDataset,
|
||||
getScale: () => fakeScale,
|
||||
update: noop,
|
||||
resize: noop,
|
||||
};
|
||||
export class ChartWithUpdateColors {}
|
||||
export const result = fakeChart;
|
||||
export const accountHistory = fakeChart;
|
||||
export const accountActivity = fakeChart;
|
||||
export const accountHistogram = fakeChart;
|
||||
export const miniResult = fakeChart;
|
||||
export let accountHistoryActiveIndex = 0;
|
||||
export function updateAccountChartButtons() {}
|
||||
`;
|
||||
return {
|
||||
name: "stub-chart-controller",
|
||||
enforce: "pre",
|
||||
resolveId(source, _importer) {
|
||||
if (
|
||||
source.endsWith("controllers/chart-controller") ||
|
||||
source.endsWith("controllers/chart-controller.ts")
|
||||
) {
|
||||
return stubId;
|
||||
}
|
||||
},
|
||||
load(id) {
|
||||
if (id === stubId) return stubCode;
|
||||
if (id.includes("controllers/chart-controller")) return stubCode;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function stubVirtualLanguageHashes(): Plugin {
|
||||
const id = "virtual:language-hashes";
|
||||
const resolved = "\0" + id;
|
||||
return {
|
||||
name: "stub-virtual-language-hashes",
|
||||
resolveId(source) {
|
||||
if (source === id) return resolved;
|
||||
},
|
||||
load(loadId) {
|
||||
if (loadId === resolved) {
|
||||
return `export const languageHashes = {};`;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default defineMain({
|
||||
staticDirs: ["../../static"],
|
||||
framework: {
|
||||
name: "storybook-solidjs-vite",
|
||||
options: {
|
||||
// docgen: {
|
||||
// Enabled by default, but you can configure or disable it:
|
||||
// see https://github.com/styleguidist/react-docgen-typescript#options
|
||||
// },
|
||||
},
|
||||
},
|
||||
addons: [
|
||||
"@storybook/addon-docs",
|
||||
"@storybook/addon-a11y",
|
||||
"@storybook/addon-links",
|
||||
"@storybook/addon-vitest",
|
||||
],
|
||||
stories: [
|
||||
"../stories/**/*.mdx",
|
||||
"../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)",
|
||||
],
|
||||
viteFinal(config) {
|
||||
config.plugins ??= [];
|
||||
config.plugins.push(tailwindcss());
|
||||
config.plugins.push(stubVirtualEnvConfig());
|
||||
config.plugins.push(stubVirtualLanguageHashes());
|
||||
config.plugins.push(patchQsrToNotThrow());
|
||||
config.plugins.push(patchAnimatedModalToNotThrow());
|
||||
config.plugins.push(stubChartController());
|
||||
return config;
|
||||
},
|
||||
});
|
||||
6
frontend/storybook/.storybook/preview-body.html
Normal file
6
frontend/storybook/.storybook/preview-body.html
Normal file
@@ -0,0 +1,6 @@
|
||||
<!-- Stub elements needed by components with module-level DOM query calls -->
|
||||
<body>
|
||||
<div style="display: none">
|
||||
<input id="wordsInput" />
|
||||
</div>
|
||||
</body>
|
||||
74
frontend/storybook/.storybook/preview.ts
Normal file
74
frontend/storybook/.storybook/preview.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import addonA11y from "@storybook/addon-a11y";
|
||||
import addonDocs from "@storybook/addon-docs";
|
||||
import { definePreview } from "storybook-solidjs-vite";
|
||||
|
||||
import "../stories/tailwind.css";
|
||||
import "../stories/storybook-theme.css";
|
||||
import "@fortawesome/fontawesome-free/css/all.min.css";
|
||||
import "balloon-css/balloon.min.css";
|
||||
//@ts-expect-error this works i think
|
||||
import "slim-select/styles";
|
||||
|
||||
import { ThemesList } from "../../src/ts/constants/themes";
|
||||
import { ThemeDecorator } from "./ThemeDecorator";
|
||||
|
||||
const tailwindViewports = {
|
||||
xxs: { name: "xxs (331px)", styles: { width: "331px", height: "900px" } },
|
||||
xs: { name: "xs (426px)", styles: { width: "426px", height: "900px" } },
|
||||
sm: { name: "sm (640px)", styles: { width: "640px", height: "900px" } },
|
||||
md: { name: "md (768px)", styles: { width: "768px", height: "900px" } },
|
||||
lg: { name: "lg (1024px)", styles: { width: "1024px", height: "900px" } },
|
||||
xl: { name: "xl (1280px)", styles: { width: "1280px", height: "900px" } },
|
||||
"2xl": {
|
||||
name: "2xl (1536px)",
|
||||
styles: { width: "1536px", height: "900px" },
|
||||
},
|
||||
};
|
||||
|
||||
export default definePreview({
|
||||
addons: [addonDocs(), addonA11y()],
|
||||
globalTypes: {
|
||||
theme: {
|
||||
description: "Global theme for components",
|
||||
toolbar: {
|
||||
title: "Theme",
|
||||
icon: "paintbrush",
|
||||
items: ThemesList.sort((a, b) => a.name.localeCompare(b.name)).map(
|
||||
(t) => ({
|
||||
value: t.name,
|
||||
title: t.name.replace(/_/g, " "),
|
||||
}),
|
||||
),
|
||||
dynamicTitle: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
initialGlobals: {
|
||||
theme: "serika_dark",
|
||||
},
|
||||
decorators: [ThemeDecorator],
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
// automatically create action args for all props that start with 'on'
|
||||
actions: {
|
||||
argTypesRegex: "^on.*",
|
||||
},
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i,
|
||||
},
|
||||
},
|
||||
viewport: {
|
||||
options: tailwindViewports,
|
||||
},
|
||||
a11y: {
|
||||
// 'todo' - show a11y violations in the test UI only
|
||||
// 'error' - fail CI on a11y violations
|
||||
// 'off' - skip a11y checks entirely
|
||||
test: "todo",
|
||||
},
|
||||
},
|
||||
// All components will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
|
||||
// tags: ['autodocs'],
|
||||
});
|
||||
Reference in New Issue
Block a user