Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,282 @@
/**
* Storybook stories for `<Label>`.
*
* `@storybook/react` is NOT a dep of `@suaveplan/react-ui`; story-meta
* shapes are inlined.
*/
import {
createI18n as createI18nNarrow,
type I18nInstance,
} from "@suaveplan/i18n/plugin";
import { TranslationProvider } from "@suaveplan/react-i18n";
// Widen to `I18nInstance<string>` so the literal-locale narrowing from
// the storybook fixture does not collide with `<TranslationProvider>`'s
// expected `I18nInstance<string>` parameterisation.
function createI18n(
options: Parameters<typeof createI18nNarrow>[0]
): I18nInstance {
return createI18nNarrow(options) as unknown as I18nInstance;
}
import type { ReactElement, ReactNode } from "react";
import type { SupportedLocale } from "../../locales/locales.types.js";
import type { ThemePlugin } from "../../theme/theme.types.js";
import { ThemeProvider } from "../../theme/theme-provider/theme-provider.js";
import { darkTheme } from "../../theme/themes/dark.js";
import { highContrastTheme } from "../../theme/themes/high-contrast.js";
import { lightTheme } from "../../theme/themes/light.js";
import { Input } from "../input/input.js";
import { Label } from "./label.js";
import type { LabelProps } from "./label.types.js";
interface Meta<T> {
readonly component: T;
readonly title: string;
readonly tags?: readonly string[];
readonly parameters?: Record<string, unknown>;
readonly argTypes?: Record<string, unknown>;
}
interface StoryObj<TArgs> {
readonly args?: Partial<TArgs>;
readonly render?: (args: Partial<TArgs>) => ReactNode;
readonly name?: string;
readonly parameters?: Record<string, unknown>;
}
const meta: Meta<typeof Label> = {
component: Label,
title: "react-ui/Foundation/Label",
tags: ["autodocs"],
argTypes: {
disabled: { control: "boolean" },
},
};
export default meta;
type Story = StoryObj<LabelProps>;
interface ProviderShellProps {
readonly locale: SupportedLocale;
readonly themes: ReadonlyArray<ThemePlugin>;
readonly defaultTheme: string;
readonly children: ReactNode;
}
function ProviderShellSync(props: ProviderShellProps): ReactElement {
const i18n = createI18n({
defaultLocale: props.locale,
fallbackLocale: "en-GB",
});
return (
<TranslationProvider i18n={i18n}>
<ThemeProvider
themes={props.themes}
defaultTheme={props.defaultTheme}
>
{props.children}
</ThemeProvider>
</TranslationProvider>
);
}
export const Default: Story = {
render: (args) => (
<ProviderShellSync
locale="en-GB"
themes={[lightTheme]}
defaultTheme="light"
>
<Label {...args} htmlFor="story-default">
Email
</Label>
</ProviderShellSync>
),
};
export const Playground: Story = {
render: (args) => (
<ProviderShellSync
locale="en-GB"
themes={[lightTheme]}
defaultTheme="light"
>
<Label {...args} htmlFor="story-playground">
{(args.children as ReactNode) ?? "Playground"}
</Label>
</ProviderShellSync>
),
args: {
disabled: false,
},
};
export const Variants: Story = {
render: () => (
<ProviderShellSync
locale="en-GB"
themes={[lightTheme]}
defaultTheme="light"
>
<div className="flex flex-col gap-2">
<Label htmlFor="v1">Default</Label>
<Label htmlFor="v2" disabled>
Disabled
</Label>
</div>
</ProviderShellSync>
),
};
export const States: Story = {
render: () => (
<ProviderShellSync
locale="en-GB"
themes={[lightTheme]}
defaultTheme="light"
>
<div className="flex flex-col gap-2">
<Label htmlFor="s1">Active</Label>
<Label htmlFor="s2" disabled>
Disabled
</Label>
</div>
</ProviderShellSync>
),
};
const LOCALES: ReadonlyArray<SupportedLocale> = [
"en-GB",
"zh-CN",
"ar-AE",
"cy-GB",
"ru-RU",
];
const LOCALE_TEXT: Record<SupportedLocale, string> = {
"en-GB": "Email",
"zh-CN": "电子邮件",
"ar-AE": "البريد الإلكتروني",
"cy-GB": "E-bost",
"ru-RU": "Электронная почта",
};
export const WithI18n: Story = {
render: () => (
<div className="flex flex-col gap-2">
{LOCALES.map((locale) => (
<div
key={locale}
dir={locale === "ar-AE" ? "rtl" : "ltr"}
className="flex items-center gap-2"
>
<span style={{ width: 80 }}>{locale}</span>
<ProviderShellSync
locale={locale}
themes={[lightTheme]}
defaultTheme="light"
>
<Label htmlFor={`i18n-${locale}`}>
{LOCALE_TEXT[locale]}
</Label>
</ProviderShellSync>
</div>
))}
</div>
),
};
export const WithRTL: Story = {
render: () => (
<div dir="rtl">
<ProviderShellSync
locale="ar-AE"
themes={[lightTheme]}
defaultTheme="light"
>
<Label htmlFor="rtl-x">{LOCALE_TEXT["ar-AE"]}</Label>
</ProviderShellSync>
</div>
),
};
const THEMES: ReadonlyArray<{
readonly label: string;
readonly theme: ThemePlugin;
}> = [
{ label: "light", theme: lightTheme },
{ label: "dark", theme: darkTheme },
{ label: "highContrast", theme: highContrastTheme },
];
export const WithTheme: Story = {
render: () => (
<div className="flex flex-col gap-2">
{THEMES.map(({ label, theme }) => (
<div key={label} className="flex items-center gap-2 p-3 border">
<span style={{ width: 120 }}>{label}</span>
<ProviderShellSync
locale="en-GB"
themes={[theme]}
defaultTheme={theme.name}
>
<Label htmlFor={`t-${label}`}>Email</Label>
</ProviderShellSync>
</div>
))}
</div>
),
};
export const A11y: Story = {
parameters: {
a11y: {
element: "#storybook-root",
config: {
rules: [
{ id: "label", enabled: true },
{ id: "color-contrast", enabled: true },
],
},
},
},
render: () => (
<ProviderShellSync
locale="en-GB"
themes={[lightTheme]}
defaultTheme="light"
>
<div className="flex flex-col gap-1">
<Label htmlFor="a11y-input">Email</Label>
<Input id="a11y-input" type="email" />
</div>
</ProviderShellSync>
),
};
export const Composition: Story = {
name: "Composition (with Input)",
render: () => (
<ProviderShellSync
locale="en-GB"
themes={[lightTheme]}
defaultTheme="light"
>
<div className="flex flex-col gap-1">
<Label htmlFor="comp-input">
Email{" "}
<span aria-hidden="true" className="text-destructive">
*
</span>
</Label>
<Input
id="comp-input"
type="email"
placeholder={"react-ui.empty.noResults" as never}
/>
</div>
</ProviderShellSync>
),
};