This commit is contained in:
37
frontend/storybook/stories/AccountMenu.stories.tsx
Normal file
37
frontend/storybook/stories/AccountMenu.stories.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import preview from "#.storybook/preview";
|
||||
|
||||
import { AccountMenu } from "../../src/ts/components/layout/header/AccountMenu";
|
||||
|
||||
const meta = preview.meta({
|
||||
title: "Layout/Header/AccountMenu",
|
||||
component: AccountMenu,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
showFriendsNotificationBubble: { control: "boolean" },
|
||||
},
|
||||
});
|
||||
|
||||
export const Default = meta.story({
|
||||
render: () => (
|
||||
<div
|
||||
style={{ position: "relative", "pointer-events": "auto", opacity: 1 }}
|
||||
class="**:data-[ui-element='accountMenu']:pointer-events-auto **:data-[ui-element='accountMenu']:opacity-100"
|
||||
>
|
||||
<AccountMenu showFriendsNotificationBubble={false} />
|
||||
</div>
|
||||
),
|
||||
});
|
||||
|
||||
export const WithNotification = meta.story({
|
||||
render: () => (
|
||||
<div
|
||||
style={{ position: "relative", "pointer-events": "auto", opacity: 1 }}
|
||||
class="**:data-[ui-element='accountMenu']:pointer-events-auto **:data-[ui-element='accountMenu']:opacity-100"
|
||||
>
|
||||
<AccountMenu showFriendsNotificationBubble />
|
||||
</div>
|
||||
),
|
||||
});
|
||||
62
frontend/storybook/stories/AccountXpBar.stories.tsx
Normal file
62
frontend/storybook/stories/AccountXpBar.stories.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import type { XpBreakdown } from "@monkeytype/schemas/results";
|
||||
|
||||
import preview from "#.storybook/preview";
|
||||
import { onMount } from "solid-js";
|
||||
|
||||
import { AccountXpBar } from "../../src/ts/components/layout/header/AccountXpBar";
|
||||
import { setXpBarData, setAnimatedLevel } from "../../src/ts/states/header";
|
||||
|
||||
const meta = preview.meta({
|
||||
title: "Layout/Header/AccountXpBar",
|
||||
component: AccountXpBar,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
});
|
||||
|
||||
function XpBarStory(props: {
|
||||
level: number;
|
||||
addedXp: number;
|
||||
resultingXp: number;
|
||||
breakdown?: XpBreakdown;
|
||||
}) {
|
||||
onMount(() => {
|
||||
setXpBarData(null);
|
||||
setAnimatedLevel(props.level);
|
||||
// Delay so the effect picks up the change
|
||||
setTimeout(() => {
|
||||
setXpBarData({
|
||||
addedXp: props.addedXp,
|
||||
resultingXp: props.resultingXp,
|
||||
breakdown: props.breakdown,
|
||||
});
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
return (
|
||||
<div class="relative">
|
||||
<div style="width: 150px;"></div>
|
||||
<AccountXpBar />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const WithBreakdown = meta.story(() => (
|
||||
<XpBarStory
|
||||
level={1}
|
||||
addedXp={150}
|
||||
resultingXp={150}
|
||||
breakdown={{
|
||||
base: 80,
|
||||
fullAccuracy: 20,
|
||||
punctuation: 10,
|
||||
streak: 25,
|
||||
daily: 15,
|
||||
}}
|
||||
/>
|
||||
));
|
||||
|
||||
export const NoBreakdown = meta.story(() => (
|
||||
<XpBarStory level={1} addedXp={200} resultingXp={200} />
|
||||
));
|
||||
93
frontend/storybook/stories/AnimatedModal.stories.tsx
Normal file
93
frontend/storybook/stories/AnimatedModal.stories.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import preview from "#.storybook/preview";
|
||||
|
||||
import { AnimatedModal } from "../../src/ts/components/common/AnimatedModal";
|
||||
import { ModalId, showModal } from "../../src/ts/states/modals";
|
||||
|
||||
function ModalTrigger(props: { modalId: ModalId; label: string }) {
|
||||
return (
|
||||
<button
|
||||
style={{
|
||||
padding: "8px 16px",
|
||||
cursor: "pointer",
|
||||
"background-color": "var(--sub-alt-color)",
|
||||
color: "var(--text-color)",
|
||||
border: "none",
|
||||
"border-radius": "8px",
|
||||
}}
|
||||
onClick={() => showModal(props.modalId)}
|
||||
>
|
||||
{props.label}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
const meta = preview.meta({
|
||||
title: "Common/AnimatedModal",
|
||||
component: AnimatedModal,
|
||||
parameters: {
|
||||
layout: "fullscreen",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
mode: { control: "select", options: ["modal", "dialog"] },
|
||||
animationMode: {
|
||||
control: "select",
|
||||
options: ["none", "both", "modalOnly"],
|
||||
},
|
||||
title: { control: "text" },
|
||||
modalClass: { control: "text" },
|
||||
wrapperClass: { control: "text" },
|
||||
},
|
||||
decorators: [
|
||||
(Story, context) => {
|
||||
// oxlint-disable-next-line typescript/no-unsafe-member-access -- storybook decorator context is untyped
|
||||
const modalId = context.args.id as ModalId;
|
||||
// oxlint-disable-next-line typescript/no-unsafe-member-access -- storybook decorator context is untyped
|
||||
const title = (context.args.title as string) ?? "Modal";
|
||||
return (
|
||||
<div>
|
||||
<ModalTrigger modalId={modalId} label={`Open ${title}`} />
|
||||
<Story />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export const Default = meta.story({
|
||||
args: {
|
||||
id: "Contact",
|
||||
title: "Example Modal",
|
||||
children: (
|
||||
<div>
|
||||
<p>This is modal content.</p>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
export const NoAnimation = meta.story({
|
||||
args: {
|
||||
id: "Support",
|
||||
title: "No Animation Modal",
|
||||
animationMode: "none",
|
||||
children: (
|
||||
<div>
|
||||
<p>This modal has no animation.</p>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
export const DialogMode = meta.story({
|
||||
args: {
|
||||
id: "DevOptions",
|
||||
mode: "dialog",
|
||||
title: "Dialog Mode",
|
||||
children: (
|
||||
<div>
|
||||
<p>This uses dialog mode instead of modal.</p>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
});
|
||||
98
frontend/storybook/stories/Anime.stories.tsx
Normal file
98
frontend/storybook/stories/Anime.stories.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import preview from "#.storybook/preview";
|
||||
import { AnimationParams } from "animejs";
|
||||
import { Component, createSignal } from "solid-js";
|
||||
|
||||
import { Anime } from "../../src/ts/components/common/anime/Anime";
|
||||
|
||||
const meta = preview.meta({
|
||||
title: "Common/Anime/Anime",
|
||||
component: Anime as Component,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
});
|
||||
|
||||
const box = {
|
||||
width: "100px",
|
||||
height: "100px",
|
||||
"background-color": "var(--main-color)",
|
||||
"border-radius": "8px",
|
||||
display: "flex",
|
||||
"align-items": "center",
|
||||
"justify-content": "center",
|
||||
color: "var(--bg-color)",
|
||||
"font-weight": "bold",
|
||||
};
|
||||
|
||||
export const FadeIn = meta.story({
|
||||
render: () => (
|
||||
<Anime
|
||||
initial={{ opacity: 0 } as Partial<AnimationParams>}
|
||||
animate={{ opacity: 1, duration: 600 } as AnimationParams}
|
||||
>
|
||||
<div style={box}>Fade In</div>
|
||||
</Anime>
|
||||
),
|
||||
});
|
||||
|
||||
export const SlideIn = meta.story({
|
||||
render: () => (
|
||||
<Anime
|
||||
initial={{ opacity: 0, translateY: -30 } as Partial<AnimationParams>}
|
||||
animate={{ opacity: 1, translateY: 0, duration: 500 } as AnimationParams}
|
||||
>
|
||||
<div style={box}>Slide In</div>
|
||||
</Anime>
|
||||
),
|
||||
});
|
||||
|
||||
export const ReactiveAnimation = meta.story({
|
||||
render: () => {
|
||||
const [expanded, setExpanded] = createSignal(false);
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
style={{
|
||||
padding: "8px 16px",
|
||||
cursor: "pointer",
|
||||
"background-color": "var(--sub-alt-color)",
|
||||
color: "var(--text-color)",
|
||||
border: "none",
|
||||
"border-radius": "8px",
|
||||
"margin-bottom": "16px",
|
||||
}}
|
||||
onClick={() => setExpanded((prev) => !prev)}
|
||||
>
|
||||
{expanded() ? "Shrink" : "Expand"}
|
||||
</button>
|
||||
<Anime
|
||||
animation={
|
||||
{
|
||||
scale: expanded() ? 1.5 : 1,
|
||||
rotate: expanded() ? 45 : 0,
|
||||
duration: 400,
|
||||
} as AnimationParams
|
||||
}
|
||||
>
|
||||
<div style={box}>Click</div>
|
||||
</Anime>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const CustomElement = meta.story({
|
||||
render: () => (
|
||||
<Anime
|
||||
as="span"
|
||||
initial={{ opacity: 0, translateX: -20 } as Partial<AnimationParams>}
|
||||
animate={{ opacity: 1, translateX: 0, duration: 400 } as AnimationParams}
|
||||
style={{ display: "inline-block" }}
|
||||
>
|
||||
<span style={{ color: "var(--text-color)" }}>
|
||||
Rendered as a {"<span>"}
|
||||
</span>
|
||||
</Anime>
|
||||
),
|
||||
});
|
||||
79
frontend/storybook/stories/AnimeConditional.stories.tsx
Normal file
79
frontend/storybook/stories/AnimeConditional.stories.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import preview from "#.storybook/preview";
|
||||
import { Accessor, Component, createSignal, JSXElement } from "solid-js";
|
||||
|
||||
import { AnimeConditional } from "../../src/ts/components/common/anime/AnimeConditional";
|
||||
|
||||
type AnimeConditionalProps = {
|
||||
if: boolean;
|
||||
then: JSXElement | ((value: Accessor<NonNullable<boolean>>) => JSXElement);
|
||||
else?: JSXElement;
|
||||
exitBeforeEnter?: boolean;
|
||||
};
|
||||
|
||||
const meta = preview.meta({
|
||||
title: "Common/Anime/AnimeConditional",
|
||||
component: AnimeConditional as Component<AnimeConditionalProps>,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
if: { control: "boolean" },
|
||||
exitBeforeEnter: { control: "boolean" },
|
||||
},
|
||||
});
|
||||
|
||||
export const Default = meta.story({
|
||||
args: {
|
||||
if: true,
|
||||
exitBeforeEnter: true,
|
||||
then: (
|
||||
<div style={{ color: "var(--text-color)", padding: "16px" }}>
|
||||
Condition is true
|
||||
</div>
|
||||
),
|
||||
else: (
|
||||
<div style={{ color: "var(--error-color)", padding: "16px" }}>
|
||||
Condition is false
|
||||
</div>
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
export const InteractiveToggle = meta.story({
|
||||
render: () => {
|
||||
const [show, setShow] = createSignal(true);
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
style={{
|
||||
padding: "8px 16px",
|
||||
cursor: "pointer",
|
||||
"background-color": "var(--sub-alt-color)",
|
||||
color: "var(--text-color)",
|
||||
border: "none",
|
||||
"border-radius": "8px",
|
||||
"margin-bottom": "16px",
|
||||
}}
|
||||
onClick={() => setShow((prev) => !prev)}
|
||||
>
|
||||
Toggle ({show() ? "showing true" : "showing false"})
|
||||
</button>
|
||||
<AnimeConditional
|
||||
if={show()}
|
||||
exitBeforeEnter
|
||||
then={
|
||||
<div style={{ color: "var(--text-color)", padding: "16px" }}>
|
||||
Condition is true
|
||||
</div>
|
||||
}
|
||||
else={
|
||||
<div style={{ color: "var(--error-color)", padding: "16px" }}>
|
||||
Condition is false
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
178
frontend/storybook/stories/AnimeGroup.stories.tsx
Normal file
178
frontend/storybook/stories/AnimeGroup.stories.tsx
Normal file
@@ -0,0 +1,178 @@
|
||||
import preview from "#.storybook/preview";
|
||||
import { AnimationParams } from "animejs";
|
||||
import { Component, createSignal, For } from "solid-js";
|
||||
|
||||
import { AnimeGroup } from "../../src/ts/components/common/anime/AnimeGroup";
|
||||
|
||||
const meta = preview.meta({
|
||||
title: "Common/Anime/AnimeGroup",
|
||||
component: AnimeGroup as Component,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
});
|
||||
|
||||
const itemStyle = {
|
||||
padding: "12px 24px",
|
||||
"background-color": "var(--sub-alt-color)",
|
||||
color: "var(--text-color)",
|
||||
"border-radius": "8px",
|
||||
"margin-bottom": "4px",
|
||||
};
|
||||
|
||||
export const StaggeredFadeIn = meta.story({
|
||||
render: () => (
|
||||
<AnimeGroup
|
||||
initial={{ opacity: 0, translateY: -10 } as Partial<AnimationParams>}
|
||||
animation={
|
||||
{ opacity: 1, translateY: 0, duration: 400 } as AnimationParams
|
||||
}
|
||||
stagger={80}
|
||||
>
|
||||
<div style={itemStyle}>First</div>
|
||||
<div style={itemStyle}>Second</div>
|
||||
<div style={itemStyle}>Third</div>
|
||||
<div style={itemStyle}>Fourth</div>
|
||||
<div style={itemStyle}>Fifth</div>
|
||||
</AnimeGroup>
|
||||
),
|
||||
});
|
||||
|
||||
export const ReverseDirection = meta.story({
|
||||
render: () => (
|
||||
<AnimeGroup
|
||||
initial={{ opacity: 0, translateX: -20 } as Partial<AnimationParams>}
|
||||
animation={
|
||||
{ opacity: 1, translateX: 0, duration: 300 } as AnimationParams
|
||||
}
|
||||
stagger={100}
|
||||
direction="reverse"
|
||||
>
|
||||
<div style={itemStyle}>Item 1</div>
|
||||
<div style={itemStyle}>Item 2</div>
|
||||
<div style={itemStyle}>Item 3</div>
|
||||
<div style={itemStyle}>Item 4</div>
|
||||
</AnimeGroup>
|
||||
),
|
||||
});
|
||||
|
||||
export const CenterDirection = meta.story({
|
||||
render: () => (
|
||||
<AnimeGroup
|
||||
initial={{ opacity: 0, scale: 0.5 } as Partial<AnimationParams>}
|
||||
animation={{ opacity: 1, scale: 1, duration: 400 } as AnimationParams}
|
||||
stagger={80}
|
||||
direction="center"
|
||||
class="flex gap-2"
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
...itemStyle,
|
||||
width: "60px",
|
||||
"text-align": "center",
|
||||
"margin-bottom": "0",
|
||||
}}
|
||||
>
|
||||
1
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
...itemStyle,
|
||||
width: "60px",
|
||||
"text-align": "center",
|
||||
"margin-bottom": "0",
|
||||
}}
|
||||
>
|
||||
2
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
...itemStyle,
|
||||
width: "60px",
|
||||
"text-align": "center",
|
||||
"margin-bottom": "0",
|
||||
}}
|
||||
>
|
||||
3
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
...itemStyle,
|
||||
width: "60px",
|
||||
"text-align": "center",
|
||||
"margin-bottom": "0",
|
||||
}}
|
||||
>
|
||||
4
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
...itemStyle,
|
||||
width: "60px",
|
||||
"text-align": "center",
|
||||
"margin-bottom": "0",
|
||||
}}
|
||||
>
|
||||
5
|
||||
</div>
|
||||
</AnimeGroup>
|
||||
),
|
||||
});
|
||||
|
||||
export const WithExitAnimation = meta.story({
|
||||
render: () => {
|
||||
const [items, setItems] = createSignal(["A", "B", "C", "D"]);
|
||||
return (
|
||||
<div>
|
||||
<div style={{ display: "flex", gap: "8px", "margin-bottom": "16px" }}>
|
||||
<button
|
||||
style={{
|
||||
padding: "8px 16px",
|
||||
cursor: "pointer",
|
||||
"background-color": "var(--sub-alt-color)",
|
||||
color: "var(--text-color)",
|
||||
border: "none",
|
||||
"border-radius": "8px",
|
||||
}}
|
||||
onClick={() =>
|
||||
setItems((prev) => [
|
||||
...prev,
|
||||
String.fromCodePoint(65 + prev.length),
|
||||
])
|
||||
}
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
<button
|
||||
style={{
|
||||
padding: "8px 16px",
|
||||
cursor: "pointer",
|
||||
"background-color": "var(--sub-alt-color)",
|
||||
color: "var(--text-color)",
|
||||
border: "none",
|
||||
"border-radius": "8px",
|
||||
}}
|
||||
onClick={() => setItems((prev) => prev.slice(0, -1))}
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
<AnimeGroup
|
||||
initial={{ opacity: 0, translateX: -15 } as Partial<AnimationParams>}
|
||||
animation={
|
||||
{ opacity: 1, translateX: 0, duration: 300 } as AnimationParams
|
||||
}
|
||||
exit={
|
||||
{ opacity: 0, translateX: 15, duration: 200 } as AnimationParams
|
||||
}
|
||||
stagger={60}
|
||||
>
|
||||
<For each={items()}>
|
||||
{(item) => <div style={itemStyle}>Item {item}</div>}
|
||||
</For>
|
||||
</AnimeGroup>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
194
frontend/storybook/stories/AnimePresence.stories.tsx
Normal file
194
frontend/storybook/stories/AnimePresence.stories.tsx
Normal file
@@ -0,0 +1,194 @@
|
||||
import preview from "#.storybook/preview";
|
||||
import { AnimationParams } from "animejs";
|
||||
import { Component, createSignal, For, Show } from "solid-js";
|
||||
|
||||
import { Anime } from "../../src/ts/components/common/anime/Anime";
|
||||
import { AnimePresence } from "../../src/ts/components/common/anime/AnimePresence";
|
||||
|
||||
const meta = preview.meta({
|
||||
title: "Common/Anime/AnimePresence",
|
||||
component: AnimePresence as Component,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
});
|
||||
|
||||
const box = {
|
||||
padding: "16px 24px",
|
||||
"background-color": "var(--sub-alt-color)",
|
||||
color: "var(--text-color)",
|
||||
"border-radius": "8px",
|
||||
"margin-bottom": "8px",
|
||||
};
|
||||
|
||||
export const SingleToggle = meta.story({
|
||||
render: () => {
|
||||
const [show, setShow] = createSignal(true);
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
style={{
|
||||
padding: "8px 16px",
|
||||
cursor: "pointer",
|
||||
"background-color": "var(--sub-alt-color)",
|
||||
color: "var(--text-color)",
|
||||
border: "none",
|
||||
"border-radius": "8px",
|
||||
"margin-bottom": "16px",
|
||||
}}
|
||||
onClick={() => setShow((prev) => !prev)}
|
||||
>
|
||||
Toggle ({show() ? "visible" : "hidden"})
|
||||
</button>
|
||||
<AnimePresence>
|
||||
<Show when={show()}>
|
||||
<Anime
|
||||
initial={{ opacity: 0, scale: 0.8 } as Partial<AnimationParams>}
|
||||
animate={
|
||||
{ opacity: 1, scale: 1, duration: 300 } as AnimationParams
|
||||
}
|
||||
exit={
|
||||
{ opacity: 0, scale: 0.8, duration: 300 } as AnimationParams
|
||||
}
|
||||
>
|
||||
<div style={box}>Content with enter and exit animations</div>
|
||||
</Anime>
|
||||
</Show>
|
||||
</AnimePresence>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const ExitBeforeEnter = meta.story({
|
||||
render: () => {
|
||||
const [view, setView] = createSignal<"a" | "b">("a");
|
||||
const buttonStyle = (active: boolean) => ({
|
||||
padding: "8px 16px",
|
||||
cursor: "pointer",
|
||||
"background-color": active ? "var(--main-color)" : "var(--sub-alt-color)",
|
||||
color: active ? "var(--bg-color)" : "var(--text-color)",
|
||||
border: "none",
|
||||
"border-radius": "8px",
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ display: "flex", gap: "8px", "margin-bottom": "16px" }}>
|
||||
<button
|
||||
style={buttonStyle(view() === "a")}
|
||||
onClick={() => setView("a")}
|
||||
>
|
||||
View A
|
||||
</button>
|
||||
<button
|
||||
style={buttonStyle(view() === "b")}
|
||||
onClick={() => setView("b")}
|
||||
>
|
||||
View B
|
||||
</button>
|
||||
</div>
|
||||
<AnimePresence exitBeforeEnter>
|
||||
<Show when={view() === "a"}>
|
||||
<Anime
|
||||
initial={
|
||||
{ opacity: 0, translateX: -20 } as Partial<AnimationParams>
|
||||
}
|
||||
animate={
|
||||
{ opacity: 1, translateX: 0, duration: 300 } as AnimationParams
|
||||
}
|
||||
exit={
|
||||
{ opacity: 0, translateX: 20, duration: 300 } as AnimationParams
|
||||
}
|
||||
>
|
||||
<div style={box}>View A - exits before B enters</div>
|
||||
</Anime>
|
||||
</Show>
|
||||
<Show when={view() === "b"}>
|
||||
<Anime
|
||||
initial={
|
||||
{ opacity: 0, translateX: -20 } as Partial<AnimationParams>
|
||||
}
|
||||
animate={
|
||||
{ opacity: 1, translateX: 0, duration: 300 } as AnimationParams
|
||||
}
|
||||
exit={
|
||||
{ opacity: 0, translateX: 20, duration: 300 } as AnimationParams
|
||||
}
|
||||
>
|
||||
<div style={box}>View B - exits before A enters</div>
|
||||
</Anime>
|
||||
</Show>
|
||||
</AnimePresence>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const ListMode = meta.story({
|
||||
render: () => {
|
||||
const [items, setItems] = createSignal([1, 2, 3]);
|
||||
let nextId = 4;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ display: "flex", gap: "8px", "margin-bottom": "16px" }}>
|
||||
<button
|
||||
style={{
|
||||
padding: "8px 16px",
|
||||
cursor: "pointer",
|
||||
"background-color": "var(--sub-alt-color)",
|
||||
color: "var(--text-color)",
|
||||
border: "none",
|
||||
"border-radius": "8px",
|
||||
}}
|
||||
onClick={() => setItems((prev) => [...prev, nextId++])}
|
||||
>
|
||||
Add item
|
||||
</button>
|
||||
<button
|
||||
style={{
|
||||
padding: "8px 16px",
|
||||
cursor: "pointer",
|
||||
"background-color": "var(--sub-alt-color)",
|
||||
color: "var(--text-color)",
|
||||
border: "none",
|
||||
"border-radius": "8px",
|
||||
}}
|
||||
onClick={() => setItems((prev) => prev.slice(0, -1))}
|
||||
>
|
||||
Remove last
|
||||
</button>
|
||||
</div>
|
||||
<AnimePresence mode="list">
|
||||
<For each={items()}>
|
||||
{(item) => (
|
||||
<Anime
|
||||
initial={
|
||||
{ opacity: 0, translateX: -10 } as Partial<AnimationParams>
|
||||
}
|
||||
animate={
|
||||
{
|
||||
opacity: 1,
|
||||
translateX: 0,
|
||||
duration: 200,
|
||||
} as AnimationParams
|
||||
}
|
||||
exit={
|
||||
{
|
||||
opacity: 0,
|
||||
translateX: 10,
|
||||
duration: 200,
|
||||
} as AnimationParams
|
||||
}
|
||||
>
|
||||
<div style={box}>Item {item}</div>
|
||||
</Anime>
|
||||
)}
|
||||
</For>
|
||||
</AnimePresence>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
93
frontend/storybook/stories/AnimeShow.stories.tsx
Normal file
93
frontend/storybook/stories/AnimeShow.stories.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import preview from "#.storybook/preview";
|
||||
import { Component, createSignal } from "solid-js";
|
||||
|
||||
import { AnimeShow } from "../../src/ts/components/common/anime/AnimeShow";
|
||||
|
||||
const meta = preview.meta({
|
||||
title: "Common/Anime/AnimeShow",
|
||||
component: AnimeShow as Component<{
|
||||
when: boolean;
|
||||
slide?: true;
|
||||
duration?: number;
|
||||
class?: string;
|
||||
}>,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
when: { control: "boolean" },
|
||||
slide: { control: "boolean" },
|
||||
duration: { control: "number" },
|
||||
},
|
||||
});
|
||||
|
||||
export const FadeToggle = meta.story({
|
||||
args: {
|
||||
when: true,
|
||||
children: (
|
||||
<div style={{ color: "var(--text-color)", padding: "16px" }}>
|
||||
This content fades in and out
|
||||
</div>
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
export const SlideToggle = meta.story({
|
||||
args: {
|
||||
when: true,
|
||||
slide: true,
|
||||
duration: 250,
|
||||
children: (
|
||||
<div style={{ color: "var(--text-color)", padding: "16px" }}>
|
||||
This content slides in and out
|
||||
</div>
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
export const InteractiveDemo = meta.story({
|
||||
render: () => {
|
||||
const [show, setShow] = createSignal(true);
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
style={{
|
||||
padding: "8px 16px",
|
||||
cursor: "pointer",
|
||||
"background-color": "var(--sub-alt-color)",
|
||||
color: "var(--text-color)",
|
||||
border: "none",
|
||||
"border-radius": "8px",
|
||||
"margin-bottom": "16px",
|
||||
}}
|
||||
onClick={() => setShow((prev) => !prev)}
|
||||
>
|
||||
Toggle ({show() ? "visible" : "hidden"})
|
||||
</button>
|
||||
<div style={{ display: "flex", gap: "24px" }}>
|
||||
<div>
|
||||
<div style={{ "font-size": "12px", color: "var(--sub-color)" }}>
|
||||
Fade
|
||||
</div>
|
||||
<AnimeShow when={show()}>
|
||||
<div style={{ color: "var(--text-color)", padding: "16px" }}>
|
||||
Fade content
|
||||
</div>
|
||||
</AnimeShow>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ "font-size": "12px", color: "var(--sub-color)" }}>
|
||||
Slide
|
||||
</div>
|
||||
<AnimeShow when={show()} slide duration={250}>
|
||||
<div style={{ color: "var(--text-color)", padding: "16px" }}>
|
||||
Slide content
|
||||
</div>
|
||||
</AnimeShow>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
77
frontend/storybook/stories/AnimeSwitch.stories.tsx
Normal file
77
frontend/storybook/stories/AnimeSwitch.stories.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import preview from "#.storybook/preview";
|
||||
import { Component, createSignal } from "solid-js";
|
||||
|
||||
import { AnimeMatch } from "../../src/ts/components/common/anime/AnimeMatch";
|
||||
import { AnimeSwitch } from "../../src/ts/components/common/anime/AnimeSwitch";
|
||||
|
||||
const meta = preview.meta({
|
||||
title: "Common/Anime/AnimeSwitch",
|
||||
component: AnimeSwitch as Component,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
});
|
||||
|
||||
export const Default = meta.story({
|
||||
render: () => {
|
||||
const [tab, setTab] = createSignal<"a" | "b" | "c">("a");
|
||||
const buttonStyle = (active: boolean) => ({
|
||||
padding: "8px 16px",
|
||||
cursor: "pointer",
|
||||
"background-color": active ? "var(--main-color)" : "var(--sub-alt-color)",
|
||||
color: active ? "var(--bg-color)" : "var(--text-color)",
|
||||
border: "none",
|
||||
"border-radius": "8px",
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ display: "flex", gap: "8px", "margin-bottom": "16px" }}>
|
||||
<button
|
||||
style={buttonStyle(tab() === "a")}
|
||||
onClick={() => setTab("a")}
|
||||
>
|
||||
Tab A
|
||||
</button>
|
||||
<button
|
||||
style={buttonStyle(tab() === "b")}
|
||||
onClick={() => setTab("b")}
|
||||
>
|
||||
Tab B
|
||||
</button>
|
||||
<button
|
||||
style={buttonStyle(tab() === "c")}
|
||||
onClick={() => setTab("c")}
|
||||
>
|
||||
Tab C
|
||||
</button>
|
||||
</div>
|
||||
<AnimeSwitch
|
||||
exitBeforeEnter
|
||||
animeProps={{
|
||||
initial: { opacity: 0 },
|
||||
animate: { opacity: 1, duration: 200 },
|
||||
exit: { opacity: 0, duration: 200 },
|
||||
}}
|
||||
>
|
||||
<AnimeMatch when={tab() === "a"}>
|
||||
<div style={{ color: "var(--text-color)", padding: "16px" }}>
|
||||
Content for Tab A
|
||||
</div>
|
||||
</AnimeMatch>
|
||||
<AnimeMatch when={tab() === "b"}>
|
||||
<div style={{ color: "var(--text-color)", padding: "16px" }}>
|
||||
Content for Tab B
|
||||
</div>
|
||||
</AnimeMatch>
|
||||
<AnimeMatch when={tab() === "c"}>
|
||||
<div style={{ color: "var(--text-color)", padding: "16px" }}>
|
||||
Content for Tab C
|
||||
</div>
|
||||
</AnimeMatch>
|
||||
</AnimeSwitch>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
80
frontend/storybook/stories/AsyncContent.stories.tsx
Normal file
80
frontend/storybook/stories/AsyncContent.stories.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import preview from "#.storybook/preview";
|
||||
import {
|
||||
QueryClient,
|
||||
QueryClientProvider,
|
||||
useQuery,
|
||||
} from "@tanstack/solid-query";
|
||||
|
||||
import AsyncContent from "../../src/ts/components/common/AsyncContent";
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
});
|
||||
|
||||
const meta = preview.meta({
|
||||
title: "Common/AsyncContent",
|
||||
// oxlint-disable-next-line typescript/no-unsafe-assignment -- generic component
|
||||
component: AsyncContent as unknown as () => ReturnType<typeof AsyncContent>,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Story />
|
||||
</QueryClientProvider>
|
||||
),
|
||||
],
|
||||
});
|
||||
|
||||
function LoadingExample(): ReturnType<typeof AsyncContent> {
|
||||
const query = useQuery(() => ({
|
||||
queryKey: ["storybook-loading"],
|
||||
queryFn: async () => new Promise<string>(() => undefined),
|
||||
}));
|
||||
|
||||
return (
|
||||
<AsyncContent query={query}>{(data) => <div>{data}</div>}</AsyncContent>
|
||||
);
|
||||
}
|
||||
|
||||
function SuccessExample(): ReturnType<typeof AsyncContent> {
|
||||
const query = useQuery(() => ({
|
||||
queryKey: ["storybook-success"],
|
||||
queryFn: async () => "Hello World",
|
||||
}));
|
||||
|
||||
return (
|
||||
<AsyncContent query={query}>
|
||||
{(data) => <div style={{ color: "var(--text-color)" }}>{data}</div>}
|
||||
</AsyncContent>
|
||||
);
|
||||
}
|
||||
|
||||
function ErrorExample(): ReturnType<typeof AsyncContent> {
|
||||
const query = useQuery(() => ({
|
||||
queryKey: ["storybook-error"],
|
||||
queryFn: async () => {
|
||||
throw new globalThis.Error("Failed to fetch");
|
||||
},
|
||||
}));
|
||||
|
||||
return (
|
||||
<AsyncContent query={query} errorMessage="Could not load data">
|
||||
{() => <div>This won't render</div>}
|
||||
</AsyncContent>
|
||||
);
|
||||
}
|
||||
|
||||
export const Loading = meta.story({
|
||||
render: () => <LoadingExample />,
|
||||
});
|
||||
|
||||
export const Success = meta.story({
|
||||
render: () => <SuccessExample />,
|
||||
});
|
||||
|
||||
export const WithError = meta.story({
|
||||
render: () => <ErrorExample />,
|
||||
});
|
||||
91
frontend/storybook/stories/AutoShrink.stories.tsx
Normal file
91
frontend/storybook/stories/AutoShrink.stories.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import preview from "#.storybook/preview";
|
||||
|
||||
import { AutoShrink } from "../../src/ts/components/common/AutoShrink";
|
||||
|
||||
const meta = preview.meta({
|
||||
title: "Common/AutoShrink",
|
||||
component: AutoShrink,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
upperLimitRem: { control: "number" },
|
||||
class: { control: "text" },
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div style={{ display: "flex", "flex-direction": "column", gap: "24px" }}>
|
||||
<div>
|
||||
<div style={{ "font-size": "12px", "margin-bottom": "4px" }}>
|
||||
400px container
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
width: "400px",
|
||||
border: "1px dashed gray",
|
||||
padding: "8px",
|
||||
resize: "horizontal",
|
||||
overflow: "auto",
|
||||
}}
|
||||
>
|
||||
<Story />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ "font-size": "12px", "margin-bottom": "4px" }}>
|
||||
200px container
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
width: "200px",
|
||||
border: "1px dashed gray",
|
||||
padding: "8px",
|
||||
resize: "horizontal",
|
||||
overflow: "auto",
|
||||
}}
|
||||
>
|
||||
<Story />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ "font-size": "12px", "margin-bottom": "4px" }}>
|
||||
100px container
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
width: "100px",
|
||||
border: "1px dashed gray",
|
||||
padding: "8px",
|
||||
resize: "horizontal",
|
||||
overflow: "auto",
|
||||
}}
|
||||
>
|
||||
<Story />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
],
|
||||
});
|
||||
|
||||
export const Default = meta.story({
|
||||
args: {
|
||||
upperLimitRem: 2,
|
||||
children: "Short",
|
||||
},
|
||||
});
|
||||
|
||||
export const LongText = meta.story({
|
||||
args: {
|
||||
upperLimitRem: 2,
|
||||
children: "This is a much longer piece of text that should shrink to fit",
|
||||
},
|
||||
});
|
||||
|
||||
export const LargeUpperLimit = meta.story({
|
||||
args: {
|
||||
upperLimitRem: 4,
|
||||
children: "Big text",
|
||||
},
|
||||
});
|
||||
123
frontend/storybook/stories/Balloon.stories.tsx
Normal file
123
frontend/storybook/stories/Balloon.stories.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import preview from "#.storybook/preview";
|
||||
import { Component } from "solid-js";
|
||||
|
||||
import { Balloon } from "../../src/ts/components/common/Balloon";
|
||||
|
||||
const meta = preview.meta({
|
||||
title: "Common/Balloon",
|
||||
component: Balloon as Component,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
text: { control: "text" },
|
||||
position: {
|
||||
control: "select",
|
||||
options: ["up", "down", "left", "right"],
|
||||
},
|
||||
break: { control: "boolean" },
|
||||
length: {
|
||||
control: "select",
|
||||
options: ["small", "medium", "large", "xlarge", "fit"],
|
||||
},
|
||||
inline: { control: "boolean" },
|
||||
},
|
||||
});
|
||||
|
||||
export const Default = meta.story(() => (
|
||||
<Balloon text="Tooltip text">
|
||||
<span style={{ color: "var(--text-color)", cursor: "pointer" }}>
|
||||
Hover me
|
||||
</span>
|
||||
</Balloon>
|
||||
));
|
||||
|
||||
export const AllPositions = meta.story(() => (
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
"grid-template-columns": "repeat(2, 1fr)",
|
||||
gap: "48px",
|
||||
padding: "64px",
|
||||
color: "var(--text-color)",
|
||||
}}
|
||||
>
|
||||
<Balloon text="Tooltip above" position="up">
|
||||
<span style={{ cursor: "pointer" }}>Up</span>
|
||||
</Balloon>
|
||||
<Balloon text="Tooltip below" position="down">
|
||||
<span style={{ cursor: "pointer" }}>Down</span>
|
||||
</Balloon>
|
||||
<Balloon text="Tooltip left" position="left">
|
||||
<span style={{ cursor: "pointer" }}>Left</span>
|
||||
</Balloon>
|
||||
<Balloon text="Tooltip right" position="right">
|
||||
<span style={{ cursor: "pointer" }}>Right</span>
|
||||
</Balloon>
|
||||
</div>
|
||||
));
|
||||
|
||||
export const Lengths = meta.story(() => (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
"flex-direction": "column",
|
||||
gap: "32px",
|
||||
padding: "64px",
|
||||
color: "var(--text-color)",
|
||||
}}
|
||||
>
|
||||
<Balloon text="Short" length="small">
|
||||
<span style={{ cursor: "pointer" }}>Small</span>
|
||||
</Balloon>
|
||||
<Balloon text="A medium length tooltip message" length="medium">
|
||||
<span style={{ cursor: "pointer" }}>Medium</span>
|
||||
</Balloon>
|
||||
<Balloon
|
||||
text="A longer tooltip message that takes up more space"
|
||||
length="large"
|
||||
>
|
||||
<span style={{ cursor: "pointer" }}>Large</span>
|
||||
</Balloon>
|
||||
<Balloon
|
||||
text="An extra large tooltip with a lot of text content inside it"
|
||||
length="xlarge"
|
||||
>
|
||||
<span style={{ cursor: "pointer" }}>XLarge</span>
|
||||
</Balloon>
|
||||
<Balloon text="Fits content" length="fit">
|
||||
<span style={{ cursor: "pointer" }}>Fit</span>
|
||||
</Balloon>
|
||||
</div>
|
||||
));
|
||||
|
||||
export const Inline = meta.story(() => (
|
||||
<p style={{ color: "var(--text-color)" }}>
|
||||
This is text with a{" "}
|
||||
<Balloon text="Inline tooltip" inline>
|
||||
<span
|
||||
style={{
|
||||
color: "var(--main-color)",
|
||||
cursor: "pointer",
|
||||
"text-decoration": "underline",
|
||||
}}
|
||||
>
|
||||
inline element
|
||||
</span>
|
||||
</Balloon>{" "}
|
||||
inside a sentence.
|
||||
</p>
|
||||
));
|
||||
|
||||
export const WithBreak = meta.story(() => (
|
||||
<Balloon
|
||||
text="This is a very long tooltip message that should break into multiple lines when displayed"
|
||||
break
|
||||
length="medium"
|
||||
>
|
||||
<span style={{ color: "var(--text-color)", cursor: "pointer" }}>
|
||||
Hover for multiline tooltip
|
||||
</span>
|
||||
</Balloon>
|
||||
));
|
||||
53
frontend/storybook/stories/Bar.stories.tsx
Normal file
53
frontend/storybook/stories/Bar.stories.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import preview from "#.storybook/preview";
|
||||
|
||||
import { Bar } from "../../src/ts/components/common/Bar";
|
||||
|
||||
const meta = preview.meta({
|
||||
title: "Common/Bar",
|
||||
component: Bar,
|
||||
parameters: {
|
||||
layout: "padded",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
percent: { control: { type: "range", min: 0, max: 100 } },
|
||||
fill: { control: "select", options: ["main", "text"] },
|
||||
bg: { control: "select", options: ["bg", "sub-alt"] },
|
||||
showPercentageOnHover: { control: "boolean" },
|
||||
animationDuration: { control: "number" },
|
||||
},
|
||||
});
|
||||
|
||||
export const Default = meta.story({
|
||||
args: {
|
||||
percent: 50,
|
||||
fill: "main",
|
||||
bg: "sub-alt",
|
||||
},
|
||||
});
|
||||
|
||||
export const Full = meta.story({
|
||||
args: {
|
||||
percent: 100,
|
||||
fill: "main",
|
||||
bg: "sub-alt",
|
||||
},
|
||||
});
|
||||
|
||||
export const HalfWithHover = meta.story({
|
||||
args: {
|
||||
percent: 50,
|
||||
fill: "main",
|
||||
bg: "sub-alt",
|
||||
|
||||
showPercentageOnHover: true,
|
||||
},
|
||||
});
|
||||
|
||||
export const TextFill = meta.story({
|
||||
args: {
|
||||
percent: 75,
|
||||
fill: "text",
|
||||
bg: "sub-alt",
|
||||
},
|
||||
});
|
||||
156
frontend/storybook/stories/Button.stories.tsx
Normal file
156
frontend/storybook/stories/Button.stories.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
import preview from "#.storybook/preview";
|
||||
import { fn } from "storybook/test";
|
||||
|
||||
import { Button } from "../../src/ts/components/common/Button";
|
||||
|
||||
const meta = preview.meta({
|
||||
title: "Common/Button",
|
||||
component: Button,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
active: {
|
||||
control: "boolean",
|
||||
},
|
||||
disabled: {
|
||||
control: "boolean",
|
||||
},
|
||||
text: {
|
||||
control: "text",
|
||||
},
|
||||
fa: {
|
||||
control: "object",
|
||||
},
|
||||
balloon: {
|
||||
control: "object",
|
||||
},
|
||||
class: {
|
||||
control: "text",
|
||||
},
|
||||
"router-link": {
|
||||
control: "boolean",
|
||||
},
|
||||
href: {
|
||||
control: "text",
|
||||
},
|
||||
sameTarget: {
|
||||
control: "boolean",
|
||||
},
|
||||
},
|
||||
args: {
|
||||
onClick: fn(),
|
||||
},
|
||||
});
|
||||
|
||||
export const Default = meta.story({
|
||||
args: {
|
||||
text: "Button",
|
||||
type: "button",
|
||||
},
|
||||
});
|
||||
|
||||
export const AllVariants = meta.story({
|
||||
render: () => (
|
||||
<div class="grid grid-cols-5 gap-4 text-text">
|
||||
<div class="self-center">Button</div>
|
||||
<Button text="Default" onClick={fn()} />
|
||||
<Button text="Active" active={true} onClick={fn()} />
|
||||
<Button text="Disabled" disabled={true} onClick={fn()} />
|
||||
<Button
|
||||
text="Active + Disabled"
|
||||
active={true}
|
||||
disabled={true}
|
||||
onClick={fn()}
|
||||
/>
|
||||
|
||||
<div class="self-center">Button + Icon</div>
|
||||
<Button
|
||||
text="Default"
|
||||
fa={{ icon: "fa-cog", variant: "solid" }}
|
||||
onClick={fn()}
|
||||
/>
|
||||
<Button
|
||||
text="Active"
|
||||
fa={{ icon: "fa-cog", variant: "solid" }}
|
||||
active={true}
|
||||
onClick={fn()}
|
||||
/>
|
||||
<Button
|
||||
text="Disabled"
|
||||
fa={{ icon: "fa-cog", variant: "solid" }}
|
||||
disabled={true}
|
||||
onClick={fn()}
|
||||
/>
|
||||
<Button
|
||||
text="Active + Disabled"
|
||||
fa={{ icon: "fa-cog", variant: "solid" }}
|
||||
active={true}
|
||||
disabled={true}
|
||||
onClick={fn()}
|
||||
/>
|
||||
|
||||
<div class="self-center">Icon Only</div>
|
||||
<Button fa={{ icon: "fa-cog", variant: "solid" }} onClick={fn()} />
|
||||
<Button
|
||||
fa={{ icon: "fa-cog", variant: "solid" }}
|
||||
active={true}
|
||||
onClick={fn()}
|
||||
/>
|
||||
<Button
|
||||
fa={{ icon: "fa-cog", variant: "solid" }}
|
||||
disabled={true}
|
||||
onClick={fn()}
|
||||
/>
|
||||
<Button
|
||||
fa={{ icon: "fa-cog", variant: "solid" }}
|
||||
active={true}
|
||||
disabled={true}
|
||||
onClick={fn()}
|
||||
/>
|
||||
|
||||
<div class="self-center">Text</div>
|
||||
<Button variant="text" text="Default" onClick={fn()} />
|
||||
<Button variant="text" text="Active" active={true} onClick={fn()} />
|
||||
<Button variant="text" text="Disabled" disabled={true} onClick={fn()} />
|
||||
<Button
|
||||
variant="text"
|
||||
text="Active + Disabled"
|
||||
active={true}
|
||||
disabled={true}
|
||||
onClick={fn()}
|
||||
/>
|
||||
|
||||
<div class="self-center">Text + Icon</div>
|
||||
<Button
|
||||
variant="text"
|
||||
text="Default"
|
||||
fa={{ icon: "fa-cog", variant: "solid" }}
|
||||
onClick={fn()}
|
||||
/>
|
||||
<Button
|
||||
variant="text"
|
||||
text="Active"
|
||||
fa={{ icon: "fa-cog", variant: "solid" }}
|
||||
active={true}
|
||||
onClick={fn()}
|
||||
/>
|
||||
<Button
|
||||
variant="text"
|
||||
text="Disabled"
|
||||
fa={{ icon: "fa-cog", variant: "solid" }}
|
||||
disabled={true}
|
||||
onClick={fn()}
|
||||
/>
|
||||
<Button
|
||||
variant="text"
|
||||
text="Active + Disabled"
|
||||
fa={{ icon: "fa-cog", variant: "solid" }}
|
||||
active={true}
|
||||
disabled={true}
|
||||
onClick={fn()}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
120
frontend/storybook/stories/ChartJs.stories.tsx
Normal file
120
frontend/storybook/stories/ChartJs.stories.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import preview from "#.storybook/preview";
|
||||
|
||||
import { ChartJs } from "../../src/ts/components/common/ChartJs";
|
||||
|
||||
const meta = preview.meta({
|
||||
title: "Common/ChartJs",
|
||||
component: ChartJs as any,
|
||||
parameters: {
|
||||
layout: "padded",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
});
|
||||
|
||||
export const BarChart = meta.story({
|
||||
args: {
|
||||
type: "bar",
|
||||
data: {
|
||||
labels: ["10", "20", "30", "40", "50", "60", "70", "80", "90", "100"],
|
||||
datasets: [
|
||||
{
|
||||
label: "Users",
|
||||
data: [12, 45, 78, 120, 250, 180, 95, 42, 18, 5],
|
||||
minBarLength: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
x: { display: true, title: { display: true, text: "WPM" } },
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
display: true,
|
||||
title: { display: true, text: "Users" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div style={{ height: "300px", width: "600px" }}>
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
});
|
||||
|
||||
export const LineChart = meta.story({
|
||||
args: {
|
||||
type: "line",
|
||||
data: {
|
||||
labels: Array.from({ length: 30 }, (_, i) => `${i + 1}`),
|
||||
datasets: [
|
||||
{
|
||||
label: "WPM",
|
||||
data: [
|
||||
65, 68, 72, 70, 75, 78, 80, 82, 79, 85, 88, 90, 87, 92, 95, 93, 98,
|
||||
100, 97, 102, 105, 103, 108, 110, 107, 112, 115, 113, 118, 120,
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
x: { display: true, title: { display: true, text: "Test #" } },
|
||||
y: { display: true, title: { display: true, text: "WPM" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div style={{ height: "300px", width: "600px" }}>
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
});
|
||||
|
||||
export const ScatterChart = meta.story({
|
||||
args: {
|
||||
type: "scatter",
|
||||
data: {
|
||||
datasets: [
|
||||
{
|
||||
label: "Results",
|
||||
data: [
|
||||
{ x: 90, y: 95 },
|
||||
{ x: 85, y: 92 },
|
||||
{ x: 100, y: 98 },
|
||||
{ x: 75, y: 88 },
|
||||
{ x: 110, y: 97 },
|
||||
{ x: 95, y: 94 },
|
||||
{ x: 80, y: 90 },
|
||||
{ x: 105, y: 96 },
|
||||
{ x: 70, y: 85 },
|
||||
{ x: 115, y: 99 },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
x: { display: true, title: { display: true, text: "WPM" } },
|
||||
y: { display: true, title: { display: true, text: "Accuracy %" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div style={{ height: "300px", width: "600px" }}>
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
});
|
||||
64
frontend/storybook/stories/Checkbox.stories.tsx
Normal file
64
frontend/storybook/stories/Checkbox.stories.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import preview from "#.storybook/preview";
|
||||
import { AnyFieldApi } from "@tanstack/solid-form";
|
||||
import { Accessor, Component, createSignal } from "solid-js";
|
||||
|
||||
import { Checkbox } from "../../src/ts/components/ui/form/Checkbox";
|
||||
|
||||
function createFieldMock(options: { name?: string; value?: boolean }) {
|
||||
const [value, setValue] = createSignal(options.value ?? false);
|
||||
return {
|
||||
name: options.name ?? "test",
|
||||
get state() {
|
||||
return { value: value() };
|
||||
},
|
||||
handleChange(v: boolean) {
|
||||
setValue(v);
|
||||
},
|
||||
handleBlur() {},
|
||||
} as unknown as AnyFieldApi;
|
||||
}
|
||||
|
||||
const meta = preview.meta({
|
||||
title: "UI/Form/Checkbox",
|
||||
component: Checkbox as Component<{
|
||||
field: Accessor<AnyFieldApi>;
|
||||
label?: string;
|
||||
disabled?: boolean;
|
||||
}>,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
label: { control: "text" },
|
||||
disabled: { control: "boolean" },
|
||||
},
|
||||
});
|
||||
|
||||
export const Default = meta.story({
|
||||
render: () => {
|
||||
const unchecked = createFieldMock({});
|
||||
const checked = createFieldMock({ value: true });
|
||||
const checked2xl = createFieldMock({ value: true });
|
||||
const disabledUnchecked = createFieldMock({});
|
||||
const disabledChecked = createFieldMock({ value: true });
|
||||
const withLabel = createFieldMock({ value: true });
|
||||
|
||||
return (
|
||||
<div class="grid grid-cols-[auto_auto] items-center gap-x-4 gap-y-3 text-text">
|
||||
<div class="text-xs text-sub">Unchecked</div>
|
||||
<Checkbox field={() => unchecked} />
|
||||
<div class="text-xs text-sub">Checked</div>
|
||||
<Checkbox field={() => checked} />
|
||||
<div class="text-xs text-sub">Checked 2xl</div>
|
||||
<Checkbox class="text-2xl" field={() => checked2xl} />
|
||||
<div class="text-xs text-sub">Disabled</div>
|
||||
<Checkbox field={() => disabledUnchecked} disabled={true} />
|
||||
<div class="text-xs text-sub">Disabled Checked</div>
|
||||
<Checkbox field={() => disabledChecked} disabled={true} />
|
||||
<div class="text-xs text-sub">With Label</div>
|
||||
<Checkbox field={() => withLabel} label="checkbox" />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
52
frontend/storybook/stories/Conditional.stories.tsx
Normal file
52
frontend/storybook/stories/Conditional.stories.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import preview from "#.storybook/preview";
|
||||
import { Component, JSXElement } from "solid-js";
|
||||
|
||||
import { Conditional } from "../../src/ts/components/common/Conditional";
|
||||
|
||||
type ConditionalProps = {
|
||||
if: boolean;
|
||||
then: JSXElement;
|
||||
else?: JSXElement;
|
||||
};
|
||||
|
||||
const meta = preview.meta({
|
||||
title: "Common/Conditional",
|
||||
component: Conditional as Component<ConditionalProps>,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
if: { control: "boolean" },
|
||||
},
|
||||
});
|
||||
|
||||
export const Truthy = meta.story({
|
||||
args: {
|
||||
if: true,
|
||||
then: <div style={{ color: "var(--text-color)" }}>Condition is true</div>,
|
||||
else: <div style={{ color: "var(--error-color)" }}>Condition is false</div>,
|
||||
},
|
||||
});
|
||||
|
||||
export const Falsy = meta.story({
|
||||
args: {
|
||||
if: false,
|
||||
then: <div style={{ color: "var(--text-color)" }}>Condition is true</div>,
|
||||
else: <div style={{ color: "var(--error-color)" }}>Condition is false</div>,
|
||||
},
|
||||
});
|
||||
|
||||
export const NoFallback = meta.story({
|
||||
args: {
|
||||
if: true,
|
||||
then: <div style={{ color: "var(--text-color)" }}>Visible content</div>,
|
||||
},
|
||||
});
|
||||
|
||||
export const FalsyNoFallback = meta.story({
|
||||
args: {
|
||||
if: false,
|
||||
then: <div style={{ color: "var(--text-color)" }}>Hidden content</div>,
|
||||
},
|
||||
});
|
||||
159
frontend/storybook/stories/DataTable.stories.tsx
Normal file
159
frontend/storybook/stories/DataTable.stories.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
import preview from "#.storybook/preview";
|
||||
import { Component, createSignal } from "solid-js";
|
||||
|
||||
import {
|
||||
DataTable,
|
||||
DataTableColumnDef,
|
||||
} from "../../src/ts/components/ui/table/DataTable";
|
||||
|
||||
type Person = {
|
||||
name: string;
|
||||
age: number;
|
||||
email: string;
|
||||
role: string;
|
||||
};
|
||||
|
||||
const sampleData: Person[] = [
|
||||
{ name: "Alice", age: 28, email: "alice@example.com", role: "Engineer" },
|
||||
{ name: "Bob", age: 34, email: "bob@example.com", role: "Designer" },
|
||||
{ name: "Charlie", age: 22, email: "charlie@example.com", role: "Manager" },
|
||||
{ name: "Diana", age: 31, email: "diana@example.com", role: "Engineer" },
|
||||
{ name: "Eve", age: 27, email: "eve@example.com", role: "Designer" },
|
||||
];
|
||||
|
||||
const columns: DataTableColumnDef<Person>[] = [
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: "Name",
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "age",
|
||||
header: "Age",
|
||||
enableSorting: true,
|
||||
meta: { align: "right" as const },
|
||||
},
|
||||
{
|
||||
accessorKey: "email",
|
||||
header: "Email",
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "role",
|
||||
header: "Role",
|
||||
enableSorting: true,
|
||||
},
|
||||
];
|
||||
|
||||
const meta = preview.meta({
|
||||
title: "UI/DataTable",
|
||||
component: DataTable as Component,
|
||||
parameters: {
|
||||
layout: "padded",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
});
|
||||
|
||||
export const Default = meta.story({
|
||||
render: () => (
|
||||
<DataTable id="story-default" columns={columns} data={sampleData} />
|
||||
),
|
||||
});
|
||||
|
||||
export const WithRowSelection = meta.story({
|
||||
render: () => {
|
||||
const [activeRow, setActiveRow] = createSignal<string | null>(null);
|
||||
return (
|
||||
<div>
|
||||
<p style={{ color: "var(--sub-color)", "margin-bottom": "8px" }}>
|
||||
Click a row to select it. Active: {activeRow() ?? "none"}
|
||||
</p>
|
||||
<DataTable
|
||||
id="story-selection"
|
||||
columns={columns.map((col) => ({
|
||||
...col,
|
||||
meta: {
|
||||
...col.meta,
|
||||
cellMeta: () => ({
|
||||
style: "cursor: pointer",
|
||||
onClick: () => {
|
||||
const key = (col as { accessorKey?: string }).accessorKey;
|
||||
if (key === "name") return;
|
||||
},
|
||||
}),
|
||||
},
|
||||
}))}
|
||||
data={sampleData}
|
||||
rowSelection={{
|
||||
getRowId: (row) => row.name,
|
||||
class: "bg-main/10",
|
||||
activeRow,
|
||||
}}
|
||||
/>
|
||||
<div style={{ display: "flex", gap: "4px", "margin-top": "8px" }}>
|
||||
{sampleData.map((p) => (
|
||||
<button
|
||||
style={{
|
||||
padding: "4px 8px",
|
||||
"background-color":
|
||||
activeRow() === p.name
|
||||
? "var(--main-color)"
|
||||
: "var(--sub-alt-color)",
|
||||
color:
|
||||
activeRow() === p.name
|
||||
? "var(--bg-color)"
|
||||
: "var(--text-color)",
|
||||
border: "none",
|
||||
"border-radius": "4px",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={() =>
|
||||
setActiveRow((prev) => (prev === p.name ? null : p.name))
|
||||
}
|
||||
>
|
||||
{p.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const Empty = meta.story({
|
||||
render: () => <DataTable id="story-empty" columns={columns} data={[]} />,
|
||||
});
|
||||
|
||||
export const EmptyWithNoDataRow = meta.story({
|
||||
render: () => (
|
||||
<DataTable id="story-nodata" columns={columns} data={[]} noDataRow />
|
||||
),
|
||||
});
|
||||
|
||||
export const EmptyWithCustomNoData = meta.story({
|
||||
render: () => (
|
||||
<DataTable
|
||||
id="story-custom-nodata"
|
||||
columns={columns}
|
||||
data={[]}
|
||||
noDataRow={{
|
||||
content: (
|
||||
<span style={{ color: "var(--error-color)" }}>
|
||||
No results found. Try a different search.
|
||||
</span>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
),
|
||||
});
|
||||
|
||||
export const HiddenHeader = meta.story({
|
||||
render: () => (
|
||||
<DataTable
|
||||
id="story-no-header"
|
||||
columns={columns}
|
||||
data={sampleData}
|
||||
hideHeader
|
||||
/>
|
||||
),
|
||||
});
|
||||
101
frontend/storybook/stories/DiscordAvatar.stories.tsx
Normal file
101
frontend/storybook/stories/DiscordAvatar.stories.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import preview from "#.storybook/preview";
|
||||
|
||||
import { DiscordAvatar } from "../../src/ts/components/common/DiscordAvatar";
|
||||
|
||||
const meta = preview.meta({
|
||||
title: "Common/DiscordAvatar",
|
||||
component: DiscordAvatar,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
discordId: { control: "text" },
|
||||
discordAvatar: { control: "text" },
|
||||
size: { control: "number" },
|
||||
class: { control: "text" },
|
||||
fallbackIcon: {
|
||||
control: "select",
|
||||
options: ["user-circle", "user"],
|
||||
},
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div class="text-2xl">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
});
|
||||
|
||||
export const Default = meta.story({
|
||||
args: {
|
||||
discordId: "102819690287489024",
|
||||
discordAvatar: "a_af6c0b8ad26fdd6bcb86ed7bb40ee6e5",
|
||||
},
|
||||
});
|
||||
|
||||
export const NoAvatar = meta.story({
|
||||
args: {
|
||||
discordId: "123456789",
|
||||
discordAvatar: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
export const UserFallback = meta.story({
|
||||
args: {
|
||||
discordId: undefined,
|
||||
discordAvatar: undefined,
|
||||
fallbackIcon: "user",
|
||||
},
|
||||
});
|
||||
|
||||
export const AllVariants = meta.story({
|
||||
render: () => (
|
||||
<div style={{ display: "flex", gap: "32px", "align-items": "center" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
"flex-direction": "column",
|
||||
"align-items": "center",
|
||||
gap: "8px",
|
||||
}}
|
||||
>
|
||||
<DiscordAvatar discordId={undefined} discordAvatar={undefined} />
|
||||
<div style={{ "font-size": "12px", color: "var(--sub-color)" }}>
|
||||
user-circle fallback
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
"flex-direction": "column",
|
||||
"align-items": "center",
|
||||
gap: "8px",
|
||||
}}
|
||||
>
|
||||
<DiscordAvatar
|
||||
discordId={undefined}
|
||||
discordAvatar={undefined}
|
||||
fallbackIcon="user"
|
||||
/>
|
||||
<div style={{ "font-size": "12px", color: "var(--sub-color)" }}>
|
||||
user fallback
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
"flex-direction": "column",
|
||||
"align-items": "center",
|
||||
gap: "8px",
|
||||
}}
|
||||
>
|
||||
<DiscordAvatar discordId="123456789" discordAvatar="fake" size={64} />
|
||||
<div style={{ "font-size": "12px", color: "var(--sub-color)" }}>
|
||||
invalid avatar (fallback)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
119
frontend/storybook/stories/Fa.stories.tsx
Normal file
119
frontend/storybook/stories/Fa.stories.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
import preview from "#.storybook/preview";
|
||||
|
||||
import { Fa } from "../../src/ts/components/common/Fa";
|
||||
|
||||
const meta = preview.meta({
|
||||
title: "Common/Fa",
|
||||
component: Fa,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
icon: { control: "text" },
|
||||
variant: {
|
||||
control: "select",
|
||||
options: ["solid", "regular", "brand"],
|
||||
},
|
||||
fixedWidth: { control: "boolean" },
|
||||
spin: { control: "boolean" },
|
||||
size: { control: "number" },
|
||||
class: { control: "text" },
|
||||
},
|
||||
});
|
||||
|
||||
export const Default = meta.story({
|
||||
args: {
|
||||
icon: "fa-cog",
|
||||
variant: "solid",
|
||||
},
|
||||
});
|
||||
|
||||
export const Spinning = meta.story({
|
||||
args: {
|
||||
icon: "fa-circle-notch",
|
||||
variant: "solid",
|
||||
spin: true,
|
||||
},
|
||||
});
|
||||
|
||||
export const FixedWidth = meta.story({
|
||||
args: {
|
||||
icon: "fa-home",
|
||||
variant: "solid",
|
||||
fixedWidth: true,
|
||||
},
|
||||
});
|
||||
|
||||
export const CustomSize = meta.story({
|
||||
args: {
|
||||
icon: "fa-star",
|
||||
variant: "solid",
|
||||
size: 3,
|
||||
},
|
||||
});
|
||||
|
||||
export const Brand = meta.story({
|
||||
args: {
|
||||
icon: "fa-discord",
|
||||
variant: "brand",
|
||||
size: 2,
|
||||
},
|
||||
});
|
||||
|
||||
export const AllVariants = meta.story({
|
||||
render: () => (
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
"grid-template-columns": "auto repeat(3, 1fr)",
|
||||
gap: "16px",
|
||||
"align-items": "center",
|
||||
"font-size": "1.5em",
|
||||
}}
|
||||
>
|
||||
<div style={{ "font-size": "10px", color: "var(--sub-color)" }} />
|
||||
<div style={{ "font-size": "10px", color: "var(--sub-color)" }}>
|
||||
Solid
|
||||
</div>
|
||||
<div style={{ "font-size": "10px", color: "var(--sub-color)" }}>
|
||||
Regular
|
||||
</div>
|
||||
<div style={{ "font-size": "10px", color: "var(--sub-color)" }}>
|
||||
Brand
|
||||
</div>
|
||||
|
||||
<div style={{ "font-size": "10px", color: "var(--sub-color)" }}>
|
||||
Default
|
||||
</div>
|
||||
<Fa icon="fa-cog" variant="solid" />
|
||||
<Fa icon="fa-circle" variant="regular" />
|
||||
<Fa icon="fa-discord" variant="brand" />
|
||||
|
||||
<div style={{ "font-size": "10px", color: "var(--sub-color)" }}>
|
||||
Fixed Width
|
||||
</div>
|
||||
<Fa icon="fa-cog" variant="solid" fixedWidth />
|
||||
<Fa icon="fa-circle" variant="regular" fixedWidth />
|
||||
<Fa icon="fa-discord" variant="brand" fixedWidth />
|
||||
|
||||
<div style={{ "font-size": "10px", color: "var(--sub-color)" }}>
|
||||
Spinning
|
||||
</div>
|
||||
<Fa icon="fa-circle-notch" variant="solid" spin />
|
||||
<Fa icon="fa-circle-notch" variant="solid" spin />
|
||||
<div />
|
||||
|
||||
<div style={{ "font-size": "10px", color: "var(--sub-color)" }}>
|
||||
Sizes
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: "8px", "align-items": "center" }}>
|
||||
<Fa icon="fa-star" variant="solid" size={1} />
|
||||
<Fa icon="fa-star" variant="solid" size={2} />
|
||||
<Fa icon="fa-star" variant="solid" size={3} />
|
||||
</div>
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
),
|
||||
});
|
||||
90
frontend/storybook/stories/FieldIndicator.stories.tsx
Normal file
90
frontend/storybook/stories/FieldIndicator.stories.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import preview from "#.storybook/preview";
|
||||
import { AnyFieldApi } from "@tanstack/solid-form";
|
||||
import { Component } from "solid-js";
|
||||
|
||||
import { FieldIndicator } from "../../src/ts/components/ui/form/FieldIndicator";
|
||||
|
||||
type MetaState = {
|
||||
isTouched?: boolean;
|
||||
isValid?: boolean;
|
||||
isValidating?: boolean;
|
||||
errors?: string[];
|
||||
hasWarning?: boolean;
|
||||
warnings?: string[];
|
||||
};
|
||||
|
||||
function createFieldMock(meta: MetaState) {
|
||||
const stateMeta = {
|
||||
isTouched: true,
|
||||
isValid: true,
|
||||
isValidating: false,
|
||||
errors: [],
|
||||
...meta,
|
||||
};
|
||||
|
||||
return {
|
||||
get state() {
|
||||
return {
|
||||
meta: stateMeta,
|
||||
};
|
||||
},
|
||||
|
||||
getMeta() {
|
||||
return {
|
||||
hasWarning: meta.hasWarning ?? false,
|
||||
warnings: meta.warnings ?? [],
|
||||
};
|
||||
},
|
||||
} as unknown as AnyFieldApi;
|
||||
}
|
||||
|
||||
const meta = preview.meta({
|
||||
title: "UI/Form/FieldIndicator",
|
||||
component: FieldIndicator as Component<{
|
||||
field?: AnyFieldApi;
|
||||
}>,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
argTypes: {},
|
||||
});
|
||||
|
||||
export const Validating = meta.story({
|
||||
args: {
|
||||
field: createFieldMock({
|
||||
isValidating: true,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
export const Warning = meta.story({
|
||||
args: {
|
||||
field: createFieldMock({
|
||||
isValid: true,
|
||||
hasWarning: true,
|
||||
warnings: ["are you sure?", "are you really sure?"],
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
export const Valid = meta.story({
|
||||
args: {
|
||||
field: createFieldMock({
|
||||
isValid: true,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
export const Error = meta.story({
|
||||
args: {
|
||||
field: createFieldMock({
|
||||
isValid: false,
|
||||
errors: [
|
||||
"Failed validation",
|
||||
"Extra error",
|
||||
"very very very very very very very very very long error",
|
||||
],
|
||||
}),
|
||||
},
|
||||
});
|
||||
21
frontend/storybook/stories/Footer.stories.tsx
Normal file
21
frontend/storybook/stories/Footer.stories.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import preview from "#.storybook/preview";
|
||||
|
||||
import { Footer } from "../../src/ts/components/layout/footer/Footer";
|
||||
|
||||
const meta = preview.meta({
|
||||
title: "Layout/Footer",
|
||||
component: Footer,
|
||||
parameters: {
|
||||
layout: "fullscreen",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div style={{ padding: "16px" }}>
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
});
|
||||
|
||||
export const Default = meta.story({});
|
||||
108
frontend/storybook/stories/Form.stories.tsx
Normal file
108
frontend/storybook/stories/Form.stories.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import preview from "#.storybook/preview";
|
||||
import { createForm } from "@tanstack/solid-form";
|
||||
import { createSignal } from "solid-js";
|
||||
import { z } from "zod";
|
||||
|
||||
import { Checkbox } from "../../src/ts/components/ui/form/Checkbox";
|
||||
import { InputField } from "../../src/ts/components/ui/form/InputField";
|
||||
import { SubmitButton } from "../../src/ts/components/ui/form/SubmitButton";
|
||||
import {
|
||||
fieldMandatory,
|
||||
fromSchema,
|
||||
} from "../../src/ts/components/ui/form/utils";
|
||||
import { showNoticeNotification } from "../../src/ts/states/notifications";
|
||||
import { sleep } from "../../src/ts/utils/misc";
|
||||
|
||||
const meta = preview.meta({
|
||||
title: "UI/Form",
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
argTypes: {},
|
||||
});
|
||||
|
||||
export const withValidation = meta.story({
|
||||
render: () => {
|
||||
const form = createForm(() => ({
|
||||
defaultValues: {
|
||||
username: "",
|
||||
password: "",
|
||||
rememberMe: true,
|
||||
},
|
||||
onSubmit: async () => {
|
||||
setEditable(false);
|
||||
await sleep(1000);
|
||||
setEditable(true);
|
||||
},
|
||||
onSubmitInvalid: () => {
|
||||
showNoticeNotification("Please fill in all fields");
|
||||
},
|
||||
}));
|
||||
|
||||
const [isEditable, setEditable] = createSignal(true);
|
||||
|
||||
return (
|
||||
<form
|
||||
class="grid w-full gap-2"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
void form.handleSubmit();
|
||||
}}
|
||||
>
|
||||
<form.Field
|
||||
name="username"
|
||||
validators={{
|
||||
onChange: fromSchema(z.string().min(3).max(5)),
|
||||
onChangeAsyncDebounceMs: 250,
|
||||
onChangeAsync: async ({ value }) => {
|
||||
await sleep(500);
|
||||
return value === "kevin" ? undefined : "you must be kevin";
|
||||
},
|
||||
}}
|
||||
children={(field) => (
|
||||
<InputField
|
||||
field={field}
|
||||
showIndicator
|
||||
autocomplete="current-user"
|
||||
disabled={!isEditable()}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<form.Field
|
||||
name="password"
|
||||
validators={{
|
||||
onChange: fieldMandatory(),
|
||||
}}
|
||||
children={(field) => (
|
||||
<InputField
|
||||
field={field}
|
||||
type="password"
|
||||
showIndicator
|
||||
autocomplete="current-password"
|
||||
disabled={!isEditable()}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<form.Field
|
||||
name="rememberMe"
|
||||
children={(field) => (
|
||||
<Checkbox
|
||||
field={field}
|
||||
disabled={!isEditable()}
|
||||
label="remember me"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<SubmitButton
|
||||
form={form}
|
||||
fa={{ icon: "fa-sign-in-alt" }}
|
||||
text="sign in"
|
||||
disabled={!isEditable()}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
},
|
||||
});
|
||||
25
frontend/storybook/stories/H2.stories.tsx
Normal file
25
frontend/storybook/stories/H2.stories.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import preview from "#.storybook/preview";
|
||||
|
||||
import { H2 } from "../../src/ts/components/common/Headers";
|
||||
|
||||
const metaH2 = preview.meta({
|
||||
title: "Common/H2",
|
||||
component: H2,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
});
|
||||
|
||||
export const Default = metaH2.story({
|
||||
args: {
|
||||
text: "Section Header",
|
||||
},
|
||||
});
|
||||
|
||||
export const WithIcon = metaH2.story({
|
||||
args: {
|
||||
text: "Settings",
|
||||
fa: { icon: "fa-cog", variant: "solid" },
|
||||
},
|
||||
});
|
||||
26
frontend/storybook/stories/H3.stories.tsx
Normal file
26
frontend/storybook/stories/H3.stories.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import preview from "#.storybook/preview";
|
||||
|
||||
import { H3 } from "../../src/ts/components/common/Headers";
|
||||
|
||||
const meta = preview.meta({
|
||||
title: "Common/H3",
|
||||
component: H3,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
});
|
||||
|
||||
export const Default = meta.story({
|
||||
args: {
|
||||
text: "Sub Section",
|
||||
fa: { icon: "fa-cog", variant: "solid" },
|
||||
},
|
||||
});
|
||||
|
||||
export const WithDifferentIcon = meta.story({
|
||||
args: {
|
||||
text: "Appearance",
|
||||
fa: { icon: "fa-paint-brush", variant: "solid" },
|
||||
},
|
||||
});
|
||||
28
frontend/storybook/stories/Header.stories.tsx
Normal file
28
frontend/storybook/stories/Header.stories.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import preview from "#.storybook/preview";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/solid-query";
|
||||
|
||||
import { Header } from "../../src/ts/components/layout/header/Header";
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
});
|
||||
|
||||
const meta = preview.meta({
|
||||
title: "Layout/Header",
|
||||
component: Header,
|
||||
parameters: {
|
||||
layout: "fullscreen",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<div style={{ padding: "16px" }}>
|
||||
<Story />
|
||||
</div>
|
||||
</QueryClientProvider>
|
||||
),
|
||||
],
|
||||
});
|
||||
|
||||
export const Default = meta.story({});
|
||||
109
frontend/storybook/stories/InputField.stories.tsx
Normal file
109
frontend/storybook/stories/InputField.stories.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import preview from "#.storybook/preview";
|
||||
import { AnyFieldApi } from "@tanstack/solid-form";
|
||||
import { Component, Accessor } from "solid-js";
|
||||
|
||||
import { InputField } from "../../src/ts/components/ui/form/InputField";
|
||||
|
||||
type MetaState = {
|
||||
isTouched?: boolean;
|
||||
isValid?: boolean;
|
||||
isValidating?: boolean;
|
||||
errors?: string[];
|
||||
hasWarning?: boolean;
|
||||
warnings?: string[];
|
||||
};
|
||||
|
||||
function createFieldMock(options: {
|
||||
name?: string;
|
||||
value?: string;
|
||||
meta?: MetaState;
|
||||
}) {
|
||||
const stateMeta = {
|
||||
isTouched: true,
|
||||
isValid: true,
|
||||
isValidating: false,
|
||||
errors: [],
|
||||
...(options.meta ?? {}),
|
||||
};
|
||||
return {
|
||||
name: options.name ?? "test",
|
||||
get state() {
|
||||
return {
|
||||
value: options.value ?? "",
|
||||
meta: stateMeta,
|
||||
};
|
||||
},
|
||||
getMeta() {
|
||||
return {
|
||||
hasWarning: options.meta?.hasWarning ?? false,
|
||||
warnings: options.meta?.warnings ?? [],
|
||||
};
|
||||
},
|
||||
} as unknown as AnyFieldApi;
|
||||
}
|
||||
|
||||
const meta = preview.meta({
|
||||
title: "UI/Form/InputField",
|
||||
component: InputField as Component<{
|
||||
field: Accessor<AnyFieldApi>;
|
||||
placeholder?: string;
|
||||
showIndicator?: true;
|
||||
autocomplete?: string;
|
||||
type?: string;
|
||||
disabled?: boolean;
|
||||
onFocus?: () => void;
|
||||
}>,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
placeholder: { control: "text" },
|
||||
showIndicator: { control: "boolean" },
|
||||
autocomplete: { control: "text" },
|
||||
type: { control: "text" },
|
||||
disabled: { control: "boolean" },
|
||||
},
|
||||
});
|
||||
|
||||
export const Default = meta.story({
|
||||
args: {
|
||||
field: () => createFieldMock({}),
|
||||
},
|
||||
});
|
||||
|
||||
export const withIndicator = meta.story({
|
||||
args: {
|
||||
showIndicator: true,
|
||||
field: () => createFieldMock({}),
|
||||
},
|
||||
});
|
||||
|
||||
export const withPlaceholder = meta.story({
|
||||
args: {
|
||||
placeholder: "placeholder",
|
||||
field: () => createFieldMock({}),
|
||||
},
|
||||
});
|
||||
|
||||
export const withAutocomplete = meta.story({
|
||||
args: {
|
||||
autocomplete: "autocomplete",
|
||||
field: () => createFieldMock({}),
|
||||
},
|
||||
});
|
||||
|
||||
export const withTypePassword = meta.story({
|
||||
args: {
|
||||
type: "password",
|
||||
placeholder: "password",
|
||||
field: () => createFieldMock({ value: "test" }),
|
||||
},
|
||||
});
|
||||
|
||||
export const disabled = meta.story({
|
||||
args: {
|
||||
disabled: true,
|
||||
field: () => createFieldMock({ value: "test", meta: { isValid: true } }),
|
||||
},
|
||||
});
|
||||
52
frontend/storybook/stories/LoadingCircle.stories.tsx
Normal file
52
frontend/storybook/stories/LoadingCircle.stories.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import preview from "#.storybook/preview";
|
||||
|
||||
import { LoadingCircle } from "../../src/ts/components/common/LoadingCircle";
|
||||
|
||||
const meta = preview.meta({
|
||||
title: "Common/LoadingCircle",
|
||||
component: LoadingCircle,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
mode: {
|
||||
control: "select",
|
||||
options: ["icon", "svg"],
|
||||
},
|
||||
class: { control: "text" },
|
||||
},
|
||||
});
|
||||
|
||||
export const AllVariants = meta.story({
|
||||
render: () => (
|
||||
<div style={{ display: "flex", gap: "32px", "align-items": "center" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
"flex-direction": "column",
|
||||
"align-items": "center",
|
||||
gap: "8px",
|
||||
}}
|
||||
>
|
||||
<LoadingCircle mode="icon" />
|
||||
<div style={{ "font-size": "12px", color: "var(--sub-color)" }}>
|
||||
Icon
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
"flex-direction": "column",
|
||||
"align-items": "center",
|
||||
gap: "8px",
|
||||
}}
|
||||
>
|
||||
<LoadingCircle mode="svg" class="h-8 w-8" />
|
||||
<div style={{ "font-size": "12px", color: "var(--sub-color)" }}>
|
||||
SVG
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
14
frontend/storybook/stories/Logo.stories.tsx
Normal file
14
frontend/storybook/stories/Logo.stories.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import preview from "#.storybook/preview";
|
||||
|
||||
import { Logo } from "../../src/ts/components/layout/header/Logo";
|
||||
|
||||
const meta = preview.meta({
|
||||
title: "Layout/Header/Logo",
|
||||
component: Logo,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
});
|
||||
|
||||
export const Default = meta.story({});
|
||||
28
frontend/storybook/stories/Nav.stories.tsx
Normal file
28
frontend/storybook/stories/Nav.stories.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import preview from "#.storybook/preview";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/solid-query";
|
||||
|
||||
import { Nav } from "../../src/ts/components/layout/header/Nav";
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
});
|
||||
|
||||
const meta = preview.meta({
|
||||
title: "Layout/Header/Nav",
|
||||
component: Nav,
|
||||
parameters: {
|
||||
layout: "fullscreen",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<div style={{ padding: "16px" }}>
|
||||
<Story />
|
||||
</div>
|
||||
</QueryClientProvider>
|
||||
),
|
||||
],
|
||||
});
|
||||
|
||||
export const Default = meta.story({});
|
||||
90
frontend/storybook/stories/NotificationBubble.stories.tsx
Normal file
90
frontend/storybook/stories/NotificationBubble.stories.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import preview from "#.storybook/preview";
|
||||
import { Component } from "solid-js";
|
||||
|
||||
import { NotificationBubble } from "../../src/ts/components/common/NotificationBubble";
|
||||
|
||||
const meta = preview.meta({
|
||||
title: "Common/NotificationBubble",
|
||||
component: NotificationBubble,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
variant: {
|
||||
control: "select",
|
||||
options: ["fromCorner", "atCorner", "center"],
|
||||
},
|
||||
show: { control: "boolean" },
|
||||
},
|
||||
decorators: [
|
||||
(Story: Component) => (
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
width: "48px",
|
||||
height: "48px",
|
||||
"background-color": "var(--sub-alt-color)",
|
||||
"border-radius": "8px",
|
||||
}}
|
||||
>
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
});
|
||||
|
||||
export const FromCorner = meta.story({
|
||||
args: {
|
||||
variant: "fromCorner",
|
||||
show: true,
|
||||
},
|
||||
});
|
||||
|
||||
export const AtCorner = meta.story({
|
||||
args: {
|
||||
variant: "atCorner",
|
||||
show: true,
|
||||
},
|
||||
});
|
||||
|
||||
export const Center = meta.story({
|
||||
args: {
|
||||
variant: "center",
|
||||
show: true,
|
||||
},
|
||||
});
|
||||
|
||||
export const AllVariants = meta.story({
|
||||
decorators: [
|
||||
() => (
|
||||
<div style={{ display: "flex", gap: "32px" }}>
|
||||
{(["fromCorner", "atCorner", "center"] as const).map((variant) => (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
"flex-direction": "column",
|
||||
"align-items": "center",
|
||||
gap: "8px",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
width: "48px",
|
||||
height: "48px",
|
||||
"background-color": "var(--sub-alt-color)",
|
||||
"border-radius": "8px",
|
||||
}}
|
||||
>
|
||||
<NotificationBubble variant={variant} show />
|
||||
</div>
|
||||
<div style={{ "font-size": "12px", color: "var(--sub-color)" }}>
|
||||
{variant}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
],
|
||||
});
|
||||
34
frontend/storybook/stories/Separator.stories.tsx
Normal file
34
frontend/storybook/stories/Separator.stories.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import preview from "#.storybook/preview";
|
||||
|
||||
import { Separator } from "../../src/ts/components/common/Separator";
|
||||
|
||||
const meta = preview.meta({
|
||||
title: "Common/Separator",
|
||||
component: Separator,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
vertical: { control: "boolean" },
|
||||
class: { control: "text" },
|
||||
},
|
||||
});
|
||||
|
||||
export const Horizontal = meta.story(() => (
|
||||
<div style={{ width: "300px" }}>
|
||||
<div style={{ color: "var(--text-color)", "margin-bottom": "8px" }}>
|
||||
Above
|
||||
</div>
|
||||
<Separator />
|
||||
<div style={{ color: "var(--text-color)", "margin-top": "8px" }}>Below</div>
|
||||
</div>
|
||||
));
|
||||
|
||||
export const Vertical = meta.story(() => (
|
||||
<div style={{ display: "flex", gap: "8px", height: "60px" }}>
|
||||
<span style={{ color: "var(--text-color)" }}>Left</span>
|
||||
<Separator vertical />
|
||||
<span style={{ color: "var(--text-color)" }}>Right</span>
|
||||
</div>
|
||||
));
|
||||
170
frontend/storybook/stories/SlimSelect.stories.tsx
Normal file
170
frontend/storybook/stories/SlimSelect.stories.tsx
Normal file
@@ -0,0 +1,170 @@
|
||||
import preview from "#.storybook/preview";
|
||||
import { Component, createSignal } from "solid-js";
|
||||
import { fn } from "storybook/test";
|
||||
|
||||
import SlimSelect from "../../src/ts/components/ui/SlimSelect";
|
||||
|
||||
const meta = preview.meta({
|
||||
title: "UI/SlimSelect",
|
||||
component: SlimSelect as Component,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
args: {
|
||||
onChange: fn(),
|
||||
},
|
||||
});
|
||||
|
||||
export const Default = meta.story({
|
||||
render: () => {
|
||||
const [selected, setSelected] = createSignal<string | undefined>("banana");
|
||||
return (
|
||||
<div style={{ width: "200px" }}>
|
||||
<SlimSelect
|
||||
options={[
|
||||
{ value: "apple", text: "Apple" },
|
||||
{ value: "banana", text: "Banana" },
|
||||
{ value: "cherry", text: "Cherry" },
|
||||
{ value: "date", text: "Date" },
|
||||
]}
|
||||
selected={selected()}
|
||||
onChange={(val) => setSelected(val)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const WithValues = meta.story({
|
||||
render: () => {
|
||||
const [selected, setSelected] = createSignal<string | undefined>("15");
|
||||
return (
|
||||
<div style={{ width: "200px" }}>
|
||||
<SlimSelect
|
||||
values={["15", "30", "60", "120"]}
|
||||
selected={selected()}
|
||||
onChange={(val) => setSelected(val)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const Multiple = meta.story({
|
||||
render: () => {
|
||||
const [selected, setSelected] = createSignal(["red", "blue"]);
|
||||
return (
|
||||
<div style={{ width: "300px" }}>
|
||||
<SlimSelect
|
||||
multiple
|
||||
options={[
|
||||
{ value: "red", text: "Red" },
|
||||
{ value: "green", text: "Green" },
|
||||
{ value: "blue", text: "Blue" },
|
||||
{ value: "yellow", text: "Yellow" },
|
||||
{ value: "purple", text: "Purple" },
|
||||
]}
|
||||
selected={selected()}
|
||||
onChange={(val) => setSelected(val)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const MultipleWithAll = meta.story({
|
||||
render: () => {
|
||||
const [selected, setSelected] = createSignal([
|
||||
"english",
|
||||
"spanish",
|
||||
"french",
|
||||
]);
|
||||
return (
|
||||
<div style={{ width: "300px" }}>
|
||||
<SlimSelect
|
||||
multiple
|
||||
options={[
|
||||
{ value: "english", text: "English" },
|
||||
{ value: "spanish", text: "Spanish" },
|
||||
{ value: "french", text: "French" },
|
||||
{ value: "german", text: "German" },
|
||||
]}
|
||||
selected={selected()}
|
||||
onChange={(val) => setSelected(val)}
|
||||
settings={{ addAllOption: true }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const TwoWayBinding = meta.story({
|
||||
render: () => {
|
||||
const options = [
|
||||
{ value: "apple", text: "Apple" },
|
||||
{ value: "banana", text: "Banana" },
|
||||
{ value: "cherry", text: "Cherry" },
|
||||
{ value: "date", text: "Date" },
|
||||
];
|
||||
const [selected, setSelected] = createSignal<string | undefined>("banana");
|
||||
|
||||
const buttonStyle = (active: boolean) => ({
|
||||
padding: "4px 12px",
|
||||
cursor: "pointer",
|
||||
"background-color": active ? "var(--main-color)" : "var(--sub-alt-color)",
|
||||
color: active ? "var(--bg-color)" : "var(--text-color)",
|
||||
border: "none",
|
||||
"border-radius": "4px",
|
||||
});
|
||||
|
||||
return (
|
||||
<div style={{ width: "250px" }}>
|
||||
<SlimSelect
|
||||
options={options}
|
||||
selected={selected()}
|
||||
onChange={(val) => setSelected(val)}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "4px",
|
||||
"margin-top": "12px",
|
||||
"flex-wrap": "wrap",
|
||||
}}
|
||||
>
|
||||
{options.map((opt) => (
|
||||
<button
|
||||
style={buttonStyle(selected() === opt.value)}
|
||||
onClick={() => setSelected(opt.value)}
|
||||
>
|
||||
{opt.text}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<p style={{ color: "var(--sub-color)", "margin-top": "8px" }}>
|
||||
Selected: {selected() ?? "none"}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const Searchable = meta.story({
|
||||
render: () => {
|
||||
const [selected, setSelected] = createSignal<string | undefined>(undefined);
|
||||
return (
|
||||
<div style={{ width: "250px" }}>
|
||||
<SlimSelect
|
||||
options={Array.from({ length: 50 }, (_, i) => ({
|
||||
value: `option-${i + 1}`,
|
||||
text: `Option ${i + 1}`,
|
||||
}))}
|
||||
selected={selected()}
|
||||
onChange={(val) => setSelected(val)}
|
||||
settings={{ placeholderText: "Search options..." }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
78
frontend/storybook/stories/Table.stories.tsx
Normal file
78
frontend/storybook/stories/Table.stories.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import preview from "#.storybook/preview";
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCaption,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "../../src/ts/components/ui/table/Table";
|
||||
|
||||
const meta = preview.meta({
|
||||
title: "UI/Table",
|
||||
component: Table,
|
||||
parameters: {
|
||||
layout: "padded",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
});
|
||||
|
||||
export const Default = meta.story({
|
||||
render: () => (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>WPM</TableHead>
|
||||
<TableHead>Accuracy</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>Alice</TableCell>
|
||||
<TableCell>120</TableCell>
|
||||
<TableCell>98%</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>Bob</TableCell>
|
||||
<TableCell>95</TableCell>
|
||||
<TableCell>96%</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>Charlie</TableCell>
|
||||
<TableCell>110</TableCell>
|
||||
<TableCell>97%</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
),
|
||||
});
|
||||
|
||||
export const WithCaption = meta.story({
|
||||
render: () => (
|
||||
<Table>
|
||||
<TableCaption>Recent typing test results</TableCaption>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>WPM</TableHead>
|
||||
<TableHead>Accuracy</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>Alice</TableCell>
|
||||
<TableCell>120</TableCell>
|
||||
<TableCell>98%</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>Bob</TableCell>
|
||||
<TableCell>95</TableCell>
|
||||
<TableCell>96%</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
),
|
||||
});
|
||||
161
frontend/storybook/stories/User.stories.tsx
Normal file
161
frontend/storybook/stories/User.stories.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
import preview from "#.storybook/preview";
|
||||
import { createSignal, onCleanup } from "solid-js";
|
||||
|
||||
import { User } from "../../src/ts/components/common/User";
|
||||
|
||||
const meta = preview.meta({
|
||||
title: "Common/User",
|
||||
component: User,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
showAvatar: { control: "boolean" },
|
||||
iconsOnly: { control: "boolean" },
|
||||
isFriend: { control: "boolean" },
|
||||
},
|
||||
});
|
||||
|
||||
// oxlint-disable-next-line no-unused-vars
|
||||
function SpinnerCycleUser(props: {
|
||||
data: Record<string, unknown>;
|
||||
}): ReturnType<typeof User> {
|
||||
const [showSpinner, setShowSpinner] = createSignal(false);
|
||||
const interval = setInterval(() => {
|
||||
setShowSpinner((prev) => !prev);
|
||||
}, 2000);
|
||||
onCleanup(() => clearInterval(interval));
|
||||
//@ts-expect-error - just for testing, ignore type issues
|
||||
return <User user={{ ...props.data }} showSpinner={showSpinner()} />;
|
||||
}
|
||||
|
||||
export const Default = meta.story({
|
||||
// args: {
|
||||
// user: {
|
||||
// uid: "user123",
|
||||
// name: "monkeytyper",
|
||||
// discordId: undefined,
|
||||
// discordAvatar: undefined,
|
||||
// },
|
||||
// },
|
||||
|
||||
render: () => {
|
||||
const data = {
|
||||
uid: "user123",
|
||||
name: "monkeytyper",
|
||||
discordId: "102819690287489024",
|
||||
discordAvatar: "a_af6c0b8ad26fdd6bcb86ed7bb40ee6e5",
|
||||
isPremium: true,
|
||||
banned: true,
|
||||
};
|
||||
return (
|
||||
<div class="grid grid-cols-[auto_1fr] place-items-start gap-4 [--themable-button-text:var(--sub-color)]">
|
||||
<div class="text-sub">With avatar:</div>
|
||||
<User user={{ ...data }} />
|
||||
<div class="text-sub">No avatar:</div>
|
||||
<User user={{ ...data }} showAvatar={false} />
|
||||
<div class="text-sub">Avatar fallback:</div>
|
||||
<User user={{ ...data, discordAvatar: "" }} />
|
||||
<div class="text-sub">Avatar fallback with color:</div>
|
||||
<User
|
||||
user={{ ...data, discordAvatar: "" }}
|
||||
avatarFallback="user-circle"
|
||||
avatarColor="sub"
|
||||
/>
|
||||
<div class="text-sub">Flag color:</div>
|
||||
<User user={{ ...data }} flagsColor="sub" />
|
||||
<div class="text-sub">Hide name on small screen:</div>
|
||||
<User user={{ ...data }} hideNameOnSmallScreens={true} />
|
||||
<div class="text-sub">Level:</div>
|
||||
<User user={{ ...data }} level={10} />
|
||||
<div class="text-sub">Level no flags:</div>
|
||||
<User
|
||||
user={{ ...data, isPremium: undefined, banned: undefined }}
|
||||
level={10}
|
||||
/>
|
||||
<div class="text-sub">Show spinner (cycling):</div>
|
||||
<SpinnerCycleUser data={data} />
|
||||
<div class="text-sub">Show notification bubble:</div>
|
||||
<User user={{ ...data }} showNotificationBubble={true} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const WithBadge = meta.story({
|
||||
args: {
|
||||
user: {
|
||||
uid: "user123",
|
||||
name: "monkeytyper",
|
||||
discordId: undefined,
|
||||
discordAvatar: undefined,
|
||||
badgeId: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const Premium = meta.story({
|
||||
args: {
|
||||
user: {
|
||||
uid: "user123",
|
||||
name: "monkeytyper",
|
||||
discordId: undefined,
|
||||
discordAvatar: undefined,
|
||||
badgeId: 6,
|
||||
isPremium: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const Friend = meta.story({
|
||||
args: {
|
||||
user: {
|
||||
uid: "user123",
|
||||
name: "monkeytyper",
|
||||
discordId: undefined,
|
||||
discordAvatar: undefined,
|
||||
},
|
||||
isFriend: true,
|
||||
},
|
||||
});
|
||||
|
||||
export const Banned = meta.story({
|
||||
args: {
|
||||
user: {
|
||||
uid: "user123",
|
||||
name: "monkeytyper",
|
||||
discordId: undefined,
|
||||
discordAvatar: undefined,
|
||||
banned: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const NoAvatar = meta.story({
|
||||
args: {
|
||||
user: {
|
||||
uid: "user123",
|
||||
name: "monkeytyper",
|
||||
discordId: undefined,
|
||||
discordAvatar: undefined,
|
||||
badgeId: 13,
|
||||
isPremium: true,
|
||||
},
|
||||
showAvatar: false,
|
||||
},
|
||||
});
|
||||
|
||||
export const FullyLoaded = meta.story({
|
||||
args: {
|
||||
user: {
|
||||
uid: "user123",
|
||||
name: "monkeytyper",
|
||||
discordId: undefined,
|
||||
discordAvatar: undefined,
|
||||
badgeId: 1,
|
||||
isPremium: true,
|
||||
},
|
||||
isFriend: true,
|
||||
},
|
||||
});
|
||||
71
frontend/storybook/stories/UserBadge.stories.tsx
Normal file
71
frontend/storybook/stories/UserBadge.stories.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import preview from "#.storybook/preview";
|
||||
|
||||
import { UserBadge } from "../../src/ts/components/common/UserBadge";
|
||||
|
||||
const meta = preview.meta({
|
||||
title: "Common/UserBadge",
|
||||
component: UserBadge,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
id: {
|
||||
control: { type: "select" },
|
||||
options: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17],
|
||||
},
|
||||
iconOnly: { control: "boolean" },
|
||||
},
|
||||
});
|
||||
|
||||
export const Developer = meta.story({
|
||||
args: { id: 1 },
|
||||
});
|
||||
|
||||
export const Collaborator = meta.story({
|
||||
args: { id: 2 },
|
||||
});
|
||||
|
||||
export const ServerMod = meta.story({
|
||||
args: { id: 3 },
|
||||
});
|
||||
|
||||
export const OGAccount = meta.story({
|
||||
args: { id: 4 },
|
||||
});
|
||||
|
||||
export const Supporter = meta.story({
|
||||
args: { id: 6 },
|
||||
});
|
||||
|
||||
export const SugarDaddy = meta.story({
|
||||
args: { id: 7 },
|
||||
});
|
||||
|
||||
export const WhiteHat = meta.story({
|
||||
args: { id: 9 },
|
||||
});
|
||||
|
||||
export const BugHunter = meta.story({
|
||||
args: { id: 10 },
|
||||
});
|
||||
|
||||
export const Contributor = meta.story({
|
||||
args: { id: 12 },
|
||||
});
|
||||
|
||||
export const Mythical = meta.story({
|
||||
args: { id: 13 },
|
||||
});
|
||||
|
||||
export const AllYearLong = meta.story({
|
||||
args: { id: 14 },
|
||||
});
|
||||
|
||||
export const Perfection = meta.story({
|
||||
args: { id: 16 },
|
||||
});
|
||||
|
||||
export const IconOnly = meta.story({
|
||||
args: { id: 1, iconOnly: true },
|
||||
});
|
||||
109
frontend/storybook/stories/UserFlags.stories.tsx
Normal file
109
frontend/storybook/stories/UserFlags.stories.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import preview from "#.storybook/preview";
|
||||
|
||||
import { UserFlags } from "../../src/ts/components/common/UserFlags";
|
||||
|
||||
const meta = preview.meta({
|
||||
title: "Common/UserFlags",
|
||||
component: UserFlags,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
isPremium: { control: "boolean" },
|
||||
banned: { control: "boolean" },
|
||||
lbOptOut: { control: "boolean" },
|
||||
isFriend: { control: "boolean" },
|
||||
iconsOnly: { control: "boolean" },
|
||||
},
|
||||
});
|
||||
|
||||
export const PrimeApe = meta.story({
|
||||
args: {
|
||||
isPremium: true,
|
||||
},
|
||||
});
|
||||
|
||||
export const Banned = meta.story({
|
||||
args: {
|
||||
banned: true,
|
||||
},
|
||||
});
|
||||
|
||||
export const LbOptOut = meta.story({
|
||||
args: {
|
||||
lbOptOut: true,
|
||||
},
|
||||
});
|
||||
|
||||
export const Friend = meta.story({
|
||||
args: {
|
||||
isFriend: true,
|
||||
},
|
||||
});
|
||||
|
||||
export const AllFlags = meta.story({
|
||||
args: {
|
||||
isPremium: true,
|
||||
banned: true,
|
||||
lbOptOut: true,
|
||||
isFriend: true,
|
||||
},
|
||||
});
|
||||
|
||||
export const AllFlagsIconsOnly = meta.story({
|
||||
args: {
|
||||
isPremium: true,
|
||||
banned: true,
|
||||
lbOptOut: true,
|
||||
isFriend: true,
|
||||
iconsOnly: true,
|
||||
},
|
||||
});
|
||||
|
||||
export const AllVariants = meta.story({
|
||||
render: () => (
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
"grid-template-columns": "auto 1fr 1fr",
|
||||
gap: "12px",
|
||||
"align-items": "center",
|
||||
}}
|
||||
>
|
||||
<div style={{ "font-size": "12px", color: "var(--sub-color)" }} />
|
||||
<div style={{ "font-size": "12px", color: "var(--sub-color)" }}>Full</div>
|
||||
<div style={{ "font-size": "12px", color: "var(--sub-color)" }}>
|
||||
Icon Only
|
||||
</div>
|
||||
|
||||
<div style={{ "font-size": "12px", color: "var(--sub-color)" }}>
|
||||
Premium
|
||||
</div>
|
||||
<UserFlags isPremium />
|
||||
<UserFlags isPremium iconsOnly />
|
||||
|
||||
<div style={{ "font-size": "12px", color: "var(--sub-color)" }}>
|
||||
Banned
|
||||
</div>
|
||||
<UserFlags banned />
|
||||
<UserFlags banned iconsOnly />
|
||||
|
||||
<div style={{ "font-size": "12px", color: "var(--sub-color)" }}>
|
||||
LB Opt Out
|
||||
</div>
|
||||
<UserFlags lbOptOut />
|
||||
<UserFlags lbOptOut iconsOnly />
|
||||
|
||||
<div style={{ "font-size": "12px", color: "var(--sub-color)" }}>
|
||||
Friend
|
||||
</div>
|
||||
<UserFlags isFriend />
|
||||
<UserFlags isFriend iconsOnly />
|
||||
|
||||
<div style={{ "font-size": "12px", color: "var(--sub-color)" }}>All</div>
|
||||
<UserFlags isPremium banned lbOptOut isFriend />
|
||||
<UserFlags isPremium banned lbOptOut isFriend iconsOnly />
|
||||
</div>
|
||||
),
|
||||
});
|
||||
240
frontend/storybook/stories/UserProfile.stories.tsx
Normal file
240
frontend/storybook/stories/UserProfile.stories.tsx
Normal file
@@ -0,0 +1,240 @@
|
||||
import preview from "#.storybook/preview";
|
||||
import { UserProfile as UserProfileType } from "@monkeytype/schemas/users";
|
||||
|
||||
import { UserProfile } from "../../src/ts/components/pages/profile/UserProfile";
|
||||
|
||||
const baseProfile: UserProfileType = {
|
||||
uid: "user123",
|
||||
name: "monkeytyper",
|
||||
addedAt: 1700000000000,
|
||||
xp: 42000,
|
||||
streak: 15,
|
||||
maxStreak: 30,
|
||||
isPremium: false,
|
||||
banned: false,
|
||||
lbOptOut: false,
|
||||
typingStats: {
|
||||
completedTests: 1234,
|
||||
startedTests: 1500,
|
||||
timeTyping: 360000,
|
||||
},
|
||||
personalBests: {
|
||||
time: {
|
||||
"15": [
|
||||
{
|
||||
acc: 97.5,
|
||||
consistency: 82.3,
|
||||
difficulty: "normal",
|
||||
language: "english",
|
||||
raw: 145,
|
||||
wpm: 138,
|
||||
timestamp: 1700000000000,
|
||||
},
|
||||
],
|
||||
"30": [
|
||||
{
|
||||
acc: 96.2,
|
||||
consistency: 80.1,
|
||||
difficulty: "normal",
|
||||
language: "english",
|
||||
raw: 140,
|
||||
wpm: 132,
|
||||
timestamp: 1699000000000,
|
||||
},
|
||||
],
|
||||
"60": [
|
||||
{
|
||||
acc: 95.8,
|
||||
consistency: 78.5,
|
||||
difficulty: "normal",
|
||||
language: "english",
|
||||
raw: 135,
|
||||
wpm: 125,
|
||||
timestamp: 1698000000000,
|
||||
},
|
||||
],
|
||||
"120": [],
|
||||
},
|
||||
words: {
|
||||
"10": [
|
||||
{
|
||||
acc: 100,
|
||||
consistency: 90.0,
|
||||
difficulty: "normal",
|
||||
language: "english",
|
||||
raw: 160,
|
||||
wpm: 155,
|
||||
timestamp: 1700000000000,
|
||||
},
|
||||
],
|
||||
"25": [
|
||||
{
|
||||
acc: 98.0,
|
||||
consistency: 85.0,
|
||||
difficulty: "normal",
|
||||
language: "english",
|
||||
raw: 150,
|
||||
wpm: 142,
|
||||
timestamp: 1699000000000,
|
||||
},
|
||||
],
|
||||
"50": [
|
||||
{
|
||||
acc: 96.5,
|
||||
consistency: 81.0,
|
||||
difficulty: "normal",
|
||||
language: "english",
|
||||
raw: 142,
|
||||
wpm: 130,
|
||||
timestamp: 1698000000000,
|
||||
},
|
||||
],
|
||||
"100": [
|
||||
{
|
||||
acc: 95.0,
|
||||
consistency: 79.0,
|
||||
difficulty: "normal",
|
||||
language: "english",
|
||||
raw: 138,
|
||||
wpm: 122,
|
||||
timestamp: 1697000000000,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
details: {
|
||||
bio: "Just a monkey typing away",
|
||||
keyboard: "Custom 65%",
|
||||
socialProfiles: {
|
||||
twitter: "monkeytyper",
|
||||
github: "monkeytyper",
|
||||
website: "https://example.com",
|
||||
},
|
||||
},
|
||||
// this is styled using global styles so it wont show up correctly in storybook
|
||||
// testActivity: {
|
||||
// testsByDays: [
|
||||
// null,
|
||||
// 2,
|
||||
// 5,
|
||||
// null,
|
||||
// 3,
|
||||
// 8,
|
||||
// 12,
|
||||
// null,
|
||||
// null,
|
||||
// 1,
|
||||
// 4,
|
||||
// 6,
|
||||
// null,
|
||||
// 7,
|
||||
// 3,
|
||||
// null,
|
||||
// null,
|
||||
// 5,
|
||||
// 9,
|
||||
// 2,
|
||||
// null,
|
||||
// null,
|
||||
// 4,
|
||||
// 6,
|
||||
// 11,
|
||||
// 3,
|
||||
// null,
|
||||
// 8,
|
||||
// 2,
|
||||
// 5,
|
||||
// ],
|
||||
// lastDay: 1700000000000,
|
||||
// },
|
||||
inventory: {
|
||||
badges: [{ id: 1, selected: true }],
|
||||
},
|
||||
};
|
||||
|
||||
const meta = preview.meta({
|
||||
title: "Pages/UserProfile",
|
||||
component: UserProfile,
|
||||
parameters: {
|
||||
layout: "padded",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
});
|
||||
|
||||
export const Default = meta.story({
|
||||
args: {
|
||||
profile: baseProfile,
|
||||
},
|
||||
});
|
||||
|
||||
export const AccountPage = meta.story({
|
||||
args: {
|
||||
profile: baseProfile,
|
||||
isAccountPage: true,
|
||||
},
|
||||
});
|
||||
|
||||
export const WithLeaderboard = meta.story({
|
||||
args: {
|
||||
profile: {
|
||||
...baseProfile,
|
||||
allTimeLbs: {
|
||||
time: {
|
||||
"15": {
|
||||
english: {
|
||||
rank: 42,
|
||||
count: 50000,
|
||||
},
|
||||
},
|
||||
"60": {
|
||||
english: {
|
||||
rank: 156,
|
||||
count: 50000,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const Banned = meta.story({
|
||||
args: {
|
||||
profile: {
|
||||
...baseProfile,
|
||||
banned: true,
|
||||
details: undefined,
|
||||
inventory: undefined,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const LbOptOut = meta.story({
|
||||
args: {
|
||||
profile: {
|
||||
...baseProfile,
|
||||
lbOptOut: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const NoPbs = meta.story({
|
||||
args: {
|
||||
profile: {
|
||||
...baseProfile,
|
||||
personalBests: {
|
||||
time: {},
|
||||
words: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const Premium = meta.story({
|
||||
args: {
|
||||
profile: {
|
||||
...baseProfile,
|
||||
isPremium: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
32
frontend/storybook/stories/storybook-theme.css
Normal file
32
frontend/storybook/stories/storybook-theme.css
Normal file
@@ -0,0 +1,32 @@
|
||||
/* Fallback theme variables for Storybook (serika_dark) */
|
||||
:root {
|
||||
--bg-color: #323437;
|
||||
--main-color: #e2b714;
|
||||
--caret-color: #e2b714;
|
||||
--sub-color: #646669;
|
||||
--sub-alt-color: #2c2e31;
|
||||
--text-color: #d1d0c5;
|
||||
--error-color: #ca4754;
|
||||
--error-extra-color: #7e2a33;
|
||||
--colorful-error-color: #ca4754;
|
||||
--colorful-error-extra-color: #7e2a33;
|
||||
--roundness: 0.5rem;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
font-family: "Roboto Mono", monospace;
|
||||
}
|
||||
|
||||
.docs-story {
|
||||
background: var(--bg-color) !important;
|
||||
}
|
||||
|
||||
.devIndicator {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.docs-story .docblock-code-toggle {
|
||||
display: none;
|
||||
}
|
||||
2
frontend/storybook/stories/tailwind.css
Normal file
2
frontend/storybook/stories/tailwind.css
Normal file
@@ -0,0 +1,2 @@
|
||||
@import "../../src/styles/tailwind.css";
|
||||
@source "../../src/ts/components/**/*.tsx";
|
||||
Reference in New Issue
Block a user