@ponti-studios/ui 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -0
- package/dist/index.js +2118 -0
- package/dist/index.js.map +1 -0
- package/package.json +130 -0
- package/src/animations.css +437 -0
- package/src/components/data-display/accordion.stories.tsx +57 -0
- package/src/components/data-display/accordion.tsx +65 -0
- package/src/components/data-display/filter-chip.tsx +41 -0
- package/src/components/data-display/index.ts +13 -0
- package/src/components/data-display/table.stories.tsx +75 -0
- package/src/components/data-display/table.tsx +84 -0
- package/src/components/feedback/alert.stories.tsx +51 -0
- package/src/components/feedback/alert.tsx +60 -0
- package/src/components/feedback/empty-state.tsx +66 -0
- package/src/components/feedback/index.ts +6 -0
- package/src/components/feedback/progress.stories.tsx +37 -0
- package/src/components/feedback/progress.tsx +30 -0
- package/src/components/feedback/spinner.stories.tsx +52 -0
- package/src/components/feedback/spinner.tsx +48 -0
- package/src/components/forms/calendar.stories.tsx +30 -0
- package/src/components/forms/calendar.tsx +67 -0
- package/src/components/forms/date-picker.stories.tsx +48 -0
- package/src/components/forms/date-picker.tsx +60 -0
- package/src/components/forms/field.stories.tsx +62 -0
- package/src/components/forms/field.tsx +65 -0
- package/src/components/forms/field.types.ts +7 -0
- package/src/components/forms/index.ts +24 -0
- package/src/components/forms/input.stories.tsx +47 -0
- package/src/components/forms/input.tsx +20 -0
- package/src/components/forms/radio-group.stories.tsx +48 -0
- package/src/components/forms/radio-group.tsx +38 -0
- package/src/components/forms/select.stories.tsx +88 -0
- package/src/components/forms/select.tsx +147 -0
- package/src/components/forms/slider.stories.tsx +67 -0
- package/src/components/forms/slider.tsx +25 -0
- package/src/components/forms/stepper.stories.tsx +111 -0
- package/src/components/forms/stepper.tsx +66 -0
- package/src/components/forms/switch.stories.tsx +144 -0
- package/src/components/forms/switch.tsx +36 -0
- package/src/components/forms/text-field.stories.tsx +87 -0
- package/src/components/forms/text-field.tsx +40 -0
- package/src/components/forms/text-field.types.ts +9 -0
- package/src/components/forms/textarea.stories.tsx +150 -0
- package/src/components/forms/textarea.tsx +18 -0
- package/src/components/layout/index.ts +2 -0
- package/src/components/layout/scroll-area.stories.tsx +83 -0
- package/src/components/layout/scroll-area.tsx +40 -0
- package/src/components/navigation/app-navigation.stories.tsx +81 -0
- package/src/components/navigation/app-navigation.tsx +155 -0
- package/src/components/navigation/index.ts +3 -0
- package/src/components/navigation/tabs.stories.tsx +69 -0
- package/src/components/navigation/tabs.tsx +49 -0
- package/src/components/overlays/alert-dialog.stories.tsx +45 -0
- package/src/components/overlays/alert-dialog.tsx +119 -0
- package/src/components/overlays/command.stories.tsx +65 -0
- package/src/components/overlays/command.tsx +130 -0
- package/src/components/overlays/dialog.stories.tsx +111 -0
- package/src/components/overlays/dialog.tsx +162 -0
- package/src/components/overlays/dropdown-menu.stories.tsx +79 -0
- package/src/components/overlays/dropdown-menu.tsx +214 -0
- package/src/components/overlays/index.ts +65 -0
- package/src/components/overlays/popover.stories.tsx +39 -0
- package/src/components/overlays/popover.tsx +40 -0
- package/src/components/overlays/sheet.stories.tsx +68 -0
- package/src/components/overlays/sheet.tsx +98 -0
- package/src/components/primitives/avatar.stories.tsx +71 -0
- package/src/components/primitives/avatar.tsx +132 -0
- package/src/components/primitives/badge.stories.tsx +33 -0
- package/src/components/primitives/badge.tsx +44 -0
- package/src/components/primitives/button.stories.tsx +113 -0
- package/src/components/primitives/button.tsx +95 -0
- package/src/components/primitives/button.types.ts +10 -0
- package/src/components/primitives/card.stories.tsx +59 -0
- package/src/components/primitives/card.tsx +69 -0
- package/src/components/primitives/index.ts +13 -0
- package/src/components/primitives/label.stories.tsx +47 -0
- package/src/components/primitives/label.tsx +21 -0
- package/src/constants/chart-colors.ts +39 -0
- package/src/env.d.ts +9 -0
- package/src/hooks/index.ts +6 -0
- package/src/hooks/sort.types.ts +12 -0
- package/src/hooks/use-api-client.ts +122 -0
- package/src/hooks/use-debounced-value.ts +12 -0
- package/src/hooks/use-filter-state.ts +68 -0
- package/src/hooks/use-media-query.ts +65 -0
- package/src/hooks/use-mobile.tsx +19 -0
- package/src/index.ts +25 -0
- package/src/lib/auth.ts +5 -0
- package/src/lib/clipboard.ts +8 -0
- package/src/lib/create-memory-storage.ts +24 -0
- package/src/lib/index.ts +4 -0
- package/src/lib/utils.ts +6 -0
- package/src/storybook/controls.ts +117 -0
- package/src/storybook/options.ts +52 -0
- package/src/styles.css +1052 -0
- package/src/tokens/color-systems.stories.tsx +133 -0
- package/src/tokens/colors.ts +156 -0
- package/src/tokens/foundations.stories.tsx +151 -0
- package/src/tokens/index.ts +16 -0
- package/src/tokens/layout.stories.tsx +118 -0
- package/src/tokens/motion.ts +51 -0
- package/src/tokens/radii.ts +10 -0
- package/src/tokens/shadows.ts +34 -0
- package/src/tokens/spacing.ts +30 -0
- package/src/tokens/typography.ts +42 -0
- package/src/translations/en.ts +34 -0
- package/src/translations/index.ts +32 -0
- package/src/types/upload.ts +42 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
|
|
3
|
+
import { Button } from "../components/primitives/button";
|
|
4
|
+
import { colorSystems, colorTokenNames, type ColorMode } from "./colors";
|
|
5
|
+
|
|
6
|
+
const meta: Meta = {
|
|
7
|
+
title: "Foundations/Colors",
|
|
8
|
+
parameters: { layout: "fullscreen" },
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default meta;
|
|
12
|
+
type Story = StoryObj<typeof meta>;
|
|
13
|
+
|
|
14
|
+
const groups = {
|
|
15
|
+
surfaces: colorTokenNames.filter((token) => token.startsWith("surface-")),
|
|
16
|
+
content: colorTokenNames.filter((token) => token.startsWith("text-")),
|
|
17
|
+
structure: colorTokenNames.filter((token) => token.startsWith("border-")),
|
|
18
|
+
intent: colorTokenNames.filter(
|
|
19
|
+
(token) =>
|
|
20
|
+
token.startsWith("accent") ||
|
|
21
|
+
token.startsWith("destructive") ||
|
|
22
|
+
token.startsWith("success") ||
|
|
23
|
+
token.startsWith("warning") ||
|
|
24
|
+
token === "overlay-scrim" ||
|
|
25
|
+
token === "ring-focus",
|
|
26
|
+
),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function Swatch({ mode, token }: { mode: ColorMode; token: string }) {
|
|
30
|
+
const value = colorSystems.ponti[mode][token as keyof (typeof colorSystems.ponti)[typeof mode]];
|
|
31
|
+
const foreground = token.startsWith("text-") ? value : colorSystems.ponti[mode]["text-primary"];
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div className="grid min-w-0 gap-2">
|
|
35
|
+
<div
|
|
36
|
+
className="flex min-h-16 items-end overflow-hidden rounded-md border p-2"
|
|
37
|
+
style={{
|
|
38
|
+
background: value,
|
|
39
|
+
color: foreground,
|
|
40
|
+
borderColor: colorSystems.ponti[mode]["border-default"],
|
|
41
|
+
}}
|
|
42
|
+
>
|
|
43
|
+
<span className="font-mono text-[11px] break-all">Aa</span>
|
|
44
|
+
</div>
|
|
45
|
+
<span
|
|
46
|
+
className="font-mono text-[11px] break-all"
|
|
47
|
+
style={{ color: colorSystems.ponti[mode]["text-secondary"] }}
|
|
48
|
+
>
|
|
49
|
+
{token}
|
|
50
|
+
</span>
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function ThemePanel({ mode }: { mode: ColorMode }) {
|
|
56
|
+
const theme = colorSystems.ponti[mode];
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<section
|
|
60
|
+
className="grid min-w-0 gap-6 rounded-xl border p-5"
|
|
61
|
+
style={{
|
|
62
|
+
background: theme["surface-canvas"],
|
|
63
|
+
color: theme["text-primary"],
|
|
64
|
+
borderColor: theme["border-default"],
|
|
65
|
+
}}
|
|
66
|
+
aria-label={`Ponti ${mode} color system`}
|
|
67
|
+
>
|
|
68
|
+
<header className="grid gap-2">
|
|
69
|
+
<p className="font-mono text-xs uppercase" style={{ color: theme["text-secondary"] }}>
|
|
70
|
+
Ponti / {mode}
|
|
71
|
+
</p>
|
|
72
|
+
<h2 className="text-xl font-semibold">Semantic roles</h2>
|
|
73
|
+
<p className="max-w-prose text-sm" style={{ color: theme["text-secondary"] }}>
|
|
74
|
+
Components consume meaning, while this palette supplies the light or dark value.
|
|
75
|
+
</p>
|
|
76
|
+
</header>
|
|
77
|
+
<div className="flex flex-wrap gap-2">
|
|
78
|
+
<Button>Primary action</Button>
|
|
79
|
+
<Button variant="secondary">Secondary action</Button>
|
|
80
|
+
<Button variant="outline">Outline action</Button>
|
|
81
|
+
<Button variant="ghost">Quiet action</Button>
|
|
82
|
+
<Button variant="destructive">Delete item</Button>
|
|
83
|
+
</div>
|
|
84
|
+
<div className="grid gap-6">
|
|
85
|
+
{Object.entries(groups).map(([title, tokens]) => (
|
|
86
|
+
<section key={title} className="grid gap-3">
|
|
87
|
+
<h3 className="text-sm font-semibold capitalize">{title}</h3>
|
|
88
|
+
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4">
|
|
89
|
+
{tokens.map((token) => (
|
|
90
|
+
<Swatch key={token} mode={mode} token={token} />
|
|
91
|
+
))}
|
|
92
|
+
</div>
|
|
93
|
+
</section>
|
|
94
|
+
))}
|
|
95
|
+
</div>
|
|
96
|
+
</section>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export const LightAndDark: Story = {
|
|
101
|
+
render: () => (
|
|
102
|
+
<main className="grid min-h-screen gap-4 bg-slate-100 p-4 md:p-6 dark:bg-slate-950">
|
|
103
|
+
<ThemePanel mode="light" />
|
|
104
|
+
<ThemePanel mode="dark" />
|
|
105
|
+
</main>
|
|
106
|
+
),
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export const ButtonStates: Story = {
|
|
110
|
+
render: () => (
|
|
111
|
+
<main className="bg-canvas text-primary grid min-h-screen gap-6 p-6">
|
|
112
|
+
<header className="grid gap-2">
|
|
113
|
+
<h1 className="text-2xl font-semibold">Button state matrix</h1>
|
|
114
|
+
<p className="text-secondary max-w-prose text-sm">
|
|
115
|
+
Inspect hover, focus, press, disabled, destructive, and loading behavior in the toolbar’s
|
|
116
|
+
light and dark modes.
|
|
117
|
+
</p>
|
|
118
|
+
</header>
|
|
119
|
+
<div className="grid max-w-3xl gap-3 sm:grid-cols-2">
|
|
120
|
+
<Button>Primary action</Button>
|
|
121
|
+
<Button variant="secondary">Secondary action</Button>
|
|
122
|
+
<Button variant="outline">Outline action</Button>
|
|
123
|
+
<Button variant="ghost">Quiet action</Button>
|
|
124
|
+
<Button variant="destructive">Delete item</Button>
|
|
125
|
+
<Button variant="link">Read more</Button>
|
|
126
|
+
<Button disabled>Disabled action</Button>
|
|
127
|
+
<Button isLoading loadingLabel="Saving">
|
|
128
|
+
Save changes
|
|
129
|
+
</Button>
|
|
130
|
+
</div>
|
|
131
|
+
</main>
|
|
132
|
+
),
|
|
133
|
+
};
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical semantic color contract for the Ponti Studios system.
|
|
3
|
+
*
|
|
4
|
+
* Components consume these roles. The runtime CSS provides the light and dark
|
|
5
|
+
* values; this module exists for inspection, tooling, and typed consumers.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const colorTokenNames = [
|
|
9
|
+
"surface-canvas",
|
|
10
|
+
"surface-panel",
|
|
11
|
+
"surface-raised",
|
|
12
|
+
"surface-inset",
|
|
13
|
+
"surface-hover",
|
|
14
|
+
"surface-pressed",
|
|
15
|
+
"surface-selected",
|
|
16
|
+
"surface-disabled",
|
|
17
|
+
"overlay-scrim",
|
|
18
|
+
"text-primary",
|
|
19
|
+
"text-secondary",
|
|
20
|
+
"text-tertiary",
|
|
21
|
+
"text-disabled",
|
|
22
|
+
"text-accent",
|
|
23
|
+
"text-destructive",
|
|
24
|
+
"text-success",
|
|
25
|
+
"text-warning",
|
|
26
|
+
"text-on-accent",
|
|
27
|
+
"text-on-destructive",
|
|
28
|
+
"text-on-success",
|
|
29
|
+
"text-on-warning",
|
|
30
|
+
"border-subtle",
|
|
31
|
+
"border-default",
|
|
32
|
+
"border-strong",
|
|
33
|
+
"border-focus",
|
|
34
|
+
"border-accent",
|
|
35
|
+
"border-destructive",
|
|
36
|
+
"accent",
|
|
37
|
+
"accent-hover",
|
|
38
|
+
"accent-pressed",
|
|
39
|
+
"accent-subtle",
|
|
40
|
+
"destructive",
|
|
41
|
+
"destructive-hover",
|
|
42
|
+
"destructive-pressed",
|
|
43
|
+
"destructive-subtle",
|
|
44
|
+
"success",
|
|
45
|
+
"success-subtle",
|
|
46
|
+
"warning",
|
|
47
|
+
"warning-subtle",
|
|
48
|
+
"ring-focus",
|
|
49
|
+
] as const;
|
|
50
|
+
|
|
51
|
+
export type ColorToken = (typeof colorTokenNames)[number];
|
|
52
|
+
export type ColorMode = "light" | "dark";
|
|
53
|
+
export type ColorSystem = "ponti";
|
|
54
|
+
export type ColorTheme = Record<ColorToken, string>;
|
|
55
|
+
export type ColorSystemThemes = Record<ColorMode, ColorTheme>;
|
|
56
|
+
|
|
57
|
+
const pontiLight: ColorTheme = {
|
|
58
|
+
"surface-canvas": "#ffffff",
|
|
59
|
+
"surface-panel": "#f6f8fa",
|
|
60
|
+
"surface-raised": "#ffffff",
|
|
61
|
+
"surface-inset": "#eff2f5",
|
|
62
|
+
"surface-hover": "#e8ebef",
|
|
63
|
+
"surface-pressed": "#dfe3e8",
|
|
64
|
+
"surface-selected": "rgba(9, 105, 218, 0.1)",
|
|
65
|
+
"surface-disabled": "#eff2f5",
|
|
66
|
+
"overlay-scrim": "rgba(31, 35, 40, 0.42)",
|
|
67
|
+
"text-primary": "#25292e",
|
|
68
|
+
"text-secondary": "#59636e",
|
|
69
|
+
"text-tertiary": "#818b98",
|
|
70
|
+
"text-disabled": "#9ea7b3",
|
|
71
|
+
"text-accent": "#0969da",
|
|
72
|
+
"text-destructive": "#cf222e",
|
|
73
|
+
"text-success": "#1f883d",
|
|
74
|
+
"text-warning": "#9a6700",
|
|
75
|
+
"text-on-accent": "#ffffff",
|
|
76
|
+
"text-on-destructive": "#ffffff",
|
|
77
|
+
"text-on-success": "#ffffff",
|
|
78
|
+
"text-on-warning": "#1c1c1e",
|
|
79
|
+
"border-subtle": "rgba(129, 139, 152, 0.2)",
|
|
80
|
+
"border-default": "#d1d9e0",
|
|
81
|
+
"border-strong": "#8c959f",
|
|
82
|
+
"border-focus": "#0969da",
|
|
83
|
+
"border-accent": "#0969da",
|
|
84
|
+
"border-destructive": "#cf222e",
|
|
85
|
+
accent: "#0969da",
|
|
86
|
+
"accent-hover": "#0550ae",
|
|
87
|
+
"accent-pressed": "#033d8b",
|
|
88
|
+
"accent-subtle": "rgba(9, 105, 218, 0.1)",
|
|
89
|
+
destructive: "#cf222e",
|
|
90
|
+
"destructive-hover": "#a40e26",
|
|
91
|
+
"destructive-pressed": "#82071e",
|
|
92
|
+
"destructive-subtle": "rgba(207, 34, 46, 0.12)",
|
|
93
|
+
success: "#1f883d",
|
|
94
|
+
"success-subtle": "rgba(31, 136, 61, 0.12)",
|
|
95
|
+
warning: "#9a6700",
|
|
96
|
+
"warning-subtle": "rgba(154, 103, 0, 0.12)",
|
|
97
|
+
"ring-focus": "#0969da",
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const pontiDark: ColorTheme = {
|
|
101
|
+
"surface-canvas": "#0d1117",
|
|
102
|
+
"surface-panel": "#161b22",
|
|
103
|
+
"surface-raised": "#21262d",
|
|
104
|
+
"surface-inset": "#1a2029",
|
|
105
|
+
"surface-hover": "#242a33",
|
|
106
|
+
"surface-pressed": "#30363d",
|
|
107
|
+
"surface-selected": "rgba(47, 129, 247, 0.18)",
|
|
108
|
+
"surface-disabled": "#1a2029",
|
|
109
|
+
"overlay-scrim": "rgba(1, 4, 9, 0.78)",
|
|
110
|
+
"text-primary": "#f0f6fc",
|
|
111
|
+
"text-secondary": "#8b949e",
|
|
112
|
+
"text-tertiary": "#6e7681",
|
|
113
|
+
"text-disabled": "#484f58",
|
|
114
|
+
"text-accent": "#58a6ff",
|
|
115
|
+
"text-destructive": "#ff7b72",
|
|
116
|
+
"text-success": "#3fb950",
|
|
117
|
+
"text-warning": "#d29922",
|
|
118
|
+
"text-on-accent": "#ffffff",
|
|
119
|
+
"text-on-destructive": "#ffffff",
|
|
120
|
+
"text-on-success": "#ffffff",
|
|
121
|
+
"text-on-warning": "#0d1117",
|
|
122
|
+
"border-subtle": "rgba(240, 246, 252, 0.14)",
|
|
123
|
+
"border-default": "#30363d",
|
|
124
|
+
"border-strong": "#6e7681",
|
|
125
|
+
"border-focus": "#2f81f7",
|
|
126
|
+
"border-accent": "#2f81f7",
|
|
127
|
+
"border-destructive": "#da3633",
|
|
128
|
+
accent: "#2f81f7",
|
|
129
|
+
"accent-hover": "#58a6ff",
|
|
130
|
+
"accent-pressed": "#1f6feb",
|
|
131
|
+
"accent-subtle": "rgba(47, 129, 247, 0.18)",
|
|
132
|
+
destructive: "#da3633",
|
|
133
|
+
"destructive-hover": "#f85149",
|
|
134
|
+
"destructive-pressed": "#b62324",
|
|
135
|
+
"destructive-subtle": "rgba(218, 54, 51, 0.18)",
|
|
136
|
+
success: "#238636",
|
|
137
|
+
"success-subtle": "rgba(35, 134, 54, 0.18)",
|
|
138
|
+
warning: "#9e6a03",
|
|
139
|
+
"warning-subtle": "rgba(158, 106, 3, 0.18)",
|
|
140
|
+
"ring-focus": "#2f81f7",
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
export const colorSystems = {
|
|
144
|
+
ponti: { light: pontiLight, dark: pontiDark },
|
|
145
|
+
} as const satisfies Record<ColorSystem, ColorSystemThemes>;
|
|
146
|
+
|
|
147
|
+
export const colors = colorTokenNames.reduce(
|
|
148
|
+
(accumulator, token) => {
|
|
149
|
+
accumulator[token] = `var(--color-${token})`;
|
|
150
|
+
return accumulator;
|
|
151
|
+
},
|
|
152
|
+
{} as Record<ColorToken, `var(--color-${string})`>,
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
export const COLOR_SYSTEM_ATTRIBUTE = "data-color-system" as const;
|
|
156
|
+
export const COLOR_MODE_ATTRIBUTE = "data-color-mode" as const;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
|
|
3
|
+
const meta: Meta = {
|
|
4
|
+
title: "Foundations/System Lab",
|
|
5
|
+
parameters: { layout: "fullscreen" },
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export default meta;
|
|
9
|
+
type Story = StoryObj<typeof meta>;
|
|
10
|
+
|
|
11
|
+
export const SpacingAndType: Story = {
|
|
12
|
+
render: () => (
|
|
13
|
+
<main className="bg-canvas text-primary grid min-h-screen gap-10 p-6 md:p-10">
|
|
14
|
+
<section className="grid gap-6">
|
|
15
|
+
<header className="grid gap-2">
|
|
16
|
+
<p className="body-4 text-secondary">Foundations / rhythm</p>
|
|
17
|
+
<h1 className="display-1">Space gives the interface its pace.</h1>
|
|
18
|
+
<p className="body-1 text-secondary max-w-2xl">
|
|
19
|
+
The specimen uses the shared interval scale and the four local typography roles.
|
|
20
|
+
</p>
|
|
21
|
+
</header>
|
|
22
|
+
<div className="grid gap-2">
|
|
23
|
+
{[4, 8, 12, 16, 24, 32, 48, 64].map((size) => (
|
|
24
|
+
<div key={size} className="flex items-center gap-3">
|
|
25
|
+
<span className="text-secondary w-12 font-mono text-xs tabular-nums">{size}px</span>
|
|
26
|
+
<div
|
|
27
|
+
className="bg-accent h-3 rounded-sm"
|
|
28
|
+
style={{ width: size }}
|
|
29
|
+
aria-hidden="true"
|
|
30
|
+
/>
|
|
31
|
+
</div>
|
|
32
|
+
))}
|
|
33
|
+
</div>
|
|
34
|
+
</section>
|
|
35
|
+
<section className="border-subtle grid gap-5 rounded-xl border p-6">
|
|
36
|
+
<h2 className="heading-2">Typography roles</h2>
|
|
37
|
+
<div className="grid gap-4">
|
|
38
|
+
<div>
|
|
39
|
+
<p className="text-secondary text-xs uppercase">Display</p>
|
|
40
|
+
<p className="display-2">A clear beginning</p>
|
|
41
|
+
</div>
|
|
42
|
+
<div>
|
|
43
|
+
<p className="text-secondary text-xs uppercase">Body</p>
|
|
44
|
+
<p className="body-1 max-w-2xl">
|
|
45
|
+
Readable content should have room to breathe and should not require the user to decode
|
|
46
|
+
the container.
|
|
47
|
+
</p>
|
|
48
|
+
</div>
|
|
49
|
+
<div>
|
|
50
|
+
<p className="text-secondary text-xs uppercase">Label</p>
|
|
51
|
+
<p className="body-3 font-medium">Current collection</p>
|
|
52
|
+
</div>
|
|
53
|
+
<div>
|
|
54
|
+
<p className="text-secondary text-xs uppercase">Caption</p>
|
|
55
|
+
<p className="body-4 text-secondary">Updated just now</p>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</section>
|
|
59
|
+
</main>
|
|
60
|
+
),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const Surfaces: Story = {
|
|
64
|
+
render: () => (
|
|
65
|
+
<main className="bg-canvas text-primary grid min-h-screen gap-6 p-6 md:p-10">
|
|
66
|
+
<header className="grid gap-2">
|
|
67
|
+
<p className="body-4 text-secondary">Foundations / surfaces</p>
|
|
68
|
+
<h1 className="display-1">Quiet layers, clear ownership.</h1>
|
|
69
|
+
</header>
|
|
70
|
+
<div className="grid gap-4 md:grid-cols-2">
|
|
71
|
+
<div className="bg-canvas border-subtle grid min-h-40 content-start gap-2 rounded-xl border p-5">
|
|
72
|
+
<h2 className="heading-3">Canvas</h2>
|
|
73
|
+
<p className="body-3 text-secondary">The page background.</p>
|
|
74
|
+
</div>
|
|
75
|
+
<div className="bg-panel border-subtle grid min-h-40 content-start gap-2 rounded-xl border p-5">
|
|
76
|
+
<h2 className="heading-3">Panel</h2>
|
|
77
|
+
<p className="body-3 text-secondary">A contained region.</p>
|
|
78
|
+
</div>
|
|
79
|
+
<div className="bg-raised border-subtle grid min-h-40 content-start gap-2 rounded-xl border p-5">
|
|
80
|
+
<h2 className="heading-3">Raised</h2>
|
|
81
|
+
<p className="body-3 text-secondary">An overlay or elevated layer.</p>
|
|
82
|
+
</div>
|
|
83
|
+
<div className="bg-inset border-subtle grid min-h-40 content-start gap-2 rounded-xl border p-5">
|
|
84
|
+
<h2 className="heading-3">Inset</h2>
|
|
85
|
+
<p className="body-3 text-secondary">A recessed working region.</p>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</main>
|
|
89
|
+
),
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export const Motion: Story = {
|
|
93
|
+
render: () => (
|
|
94
|
+
<main className="bg-canvas text-primary grid min-h-screen gap-8 p-6 md:p-10">
|
|
95
|
+
<header className="grid gap-2">
|
|
96
|
+
<p className="body-4 text-secondary">Foundations / motion</p>
|
|
97
|
+
<h1 className="display-1">Motion should explain, not perform.</h1>
|
|
98
|
+
<p className="body-1 text-secondary max-w-2xl">
|
|
99
|
+
Use the Storybook motion-reduction setting to confirm that nonessential movement
|
|
100
|
+
disappears.
|
|
101
|
+
</p>
|
|
102
|
+
</header>
|
|
103
|
+
<div className="grid gap-4 sm:grid-cols-3">
|
|
104
|
+
<div className="bg-panel border-subtle motion-safe:animate-in motion-safe:fade-in-0 motion-safe:slide-in-from-bottom-2 grid min-h-32 place-content-center gap-2 rounded-xl border p-5 duration-150 motion-reduce:animate-none">
|
|
105
|
+
<span className="heading-3">Enter</span>
|
|
106
|
+
<span className="body-4 text-secondary">150ms / 6px</span>
|
|
107
|
+
</div>
|
|
108
|
+
<button className="bg-panel border-subtle text-primary hover:bg-surface-hover active:bg-surface-pressed focus-visible:ring-focus grid min-h-32 place-content-center gap-2 rounded-xl border p-5 text-left focus-visible:ring-2 focus-visible:outline-none motion-safe:transition-colors">
|
|
109
|
+
<span className="heading-3">Press</span>
|
|
110
|
+
<span className="body-4 text-secondary">Immediate feedback</span>
|
|
111
|
+
</button>
|
|
112
|
+
<div className="bg-panel border-subtle grid min-h-32 place-content-center gap-2 rounded-xl border p-5 motion-safe:animate-pulse motion-reduce:animate-none">
|
|
113
|
+
<span className="heading-3">Loading</span>
|
|
114
|
+
<span className="body-4 text-secondary">Geometry remains stable</span>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</main>
|
|
118
|
+
),
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export const Accessibility: Story = {
|
|
122
|
+
render: () => (
|
|
123
|
+
<main className="bg-canvas text-primary grid min-h-screen gap-8 p-6 md:p-10">
|
|
124
|
+
<header className="grid gap-2">
|
|
125
|
+
<p className="body-4 text-secondary">Foundations / accessibility</p>
|
|
126
|
+
<h1 className="display-1">The system should be calm for everyone.</h1>
|
|
127
|
+
</header>
|
|
128
|
+
<section className="border-subtle grid max-w-2xl gap-5 rounded-xl border p-6">
|
|
129
|
+
<label className="grid gap-2">
|
|
130
|
+
<span className="body-3 font-medium">Email address</span>
|
|
131
|
+
<input
|
|
132
|
+
className="bg-canvas border-default text-primary focus-visible:ring-focus h-11 rounded-md border px-3 outline-none focus-visible:ring-2"
|
|
133
|
+
placeholder="you@example.com"
|
|
134
|
+
/>
|
|
135
|
+
<span className="body-4 text-secondary">Use a reachable address for notifications.</span>
|
|
136
|
+
</label>
|
|
137
|
+
<div className="flex flex-wrap gap-3">
|
|
138
|
+
<button className="bg-accent text-on-accent focus-visible:ring-focus min-h-11 rounded-md px-4 focus-visible:ring-2 focus-visible:outline-none">
|
|
139
|
+
Continue
|
|
140
|
+
</button>
|
|
141
|
+
<button disabled className="bg-panel text-disabled min-h-11 rounded-md px-4">
|
|
142
|
+
Unavailable
|
|
143
|
+
</button>
|
|
144
|
+
</div>
|
|
145
|
+
<p className="body-4 text-destructive" role="alert">
|
|
146
|
+
Example error: enter a valid email address.
|
|
147
|
+
</p>
|
|
148
|
+
</section>
|
|
149
|
+
</main>
|
|
150
|
+
),
|
|
151
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { colors } from "./colors";
|
|
2
|
+
export {
|
|
3
|
+
COLOR_MODE_ATTRIBUTE,
|
|
4
|
+
COLOR_SYSTEM_ATTRIBUTE,
|
|
5
|
+
colorSystems,
|
|
6
|
+
colorTokenNames,
|
|
7
|
+
} from "./colors";
|
|
8
|
+
export type { ColorMode, ColorSystem, ColorToken } from "./colors";
|
|
9
|
+
export { durations, easingWeb, translateDistances } from "./motion";
|
|
10
|
+
export { radii } from "./radii";
|
|
11
|
+
export { shadowsNative, shadowsWeb } from "./shadows";
|
|
12
|
+
export { contentWidths, spacing } from "./spacing";
|
|
13
|
+
export { fontFamilies, fontSizes, fontWeights, letterSpacing, lineHeights } from "./typography";
|
|
14
|
+
export type { RadiusToken } from "./radii";
|
|
15
|
+
export type { ContentWidthToken, SpacingToken } from "./spacing";
|
|
16
|
+
export type { FontSizeToken, FontWeightToken } from "./typography";
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
|
|
3
|
+
const meta = {
|
|
4
|
+
title: "Foundations/Layout Rhythm",
|
|
5
|
+
parameters: {
|
|
6
|
+
layout: "fullscreen",
|
|
7
|
+
},
|
|
8
|
+
} satisfies Meta;
|
|
9
|
+
|
|
10
|
+
export default meta;
|
|
11
|
+
type Story = StoryObj<typeof meta>;
|
|
12
|
+
|
|
13
|
+
export const PageShell: Story = {
|
|
14
|
+
render: () => (
|
|
15
|
+
<main className="page-shell bg-canvas text-primary">
|
|
16
|
+
<header className="layout-stack">
|
|
17
|
+
<p className="body-4 text-secondary">Page shell</p>
|
|
18
|
+
<h1 className="display-1">A consistent starting point for content pages</h1>
|
|
19
|
+
</header>
|
|
20
|
+
<section className="layout-stack">
|
|
21
|
+
<h2 className="heading-2">Section rhythm</h2>
|
|
22
|
+
<p className="body-1 text-secondary max-w-2xl">
|
|
23
|
+
Page-level breathing room and internal section spacing remain consistent across indexes,
|
|
24
|
+
feeds, and editorial surfaces.
|
|
25
|
+
</p>
|
|
26
|
+
</section>
|
|
27
|
+
</main>
|
|
28
|
+
),
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const ListRows: Story = {
|
|
32
|
+
render: () => (
|
|
33
|
+
<main className="page-shell bg-canvas text-primary">
|
|
34
|
+
<div className="layout-stack">
|
|
35
|
+
<h1 className="display-1">List row rhythm</h1>
|
|
36
|
+
<div className="border-subtle divide-border-subtle divide-y border-b">
|
|
37
|
+
{[
|
|
38
|
+
["01", "A compact row", "For projects, search results, or activity."],
|
|
39
|
+
["02", "A readable row", "Consistent padding keeps scanning calm."],
|
|
40
|
+
["03", "A responsive row", "The same rhythm holds on mobile and desktop."],
|
|
41
|
+
].map(([index, title, description]) => (
|
|
42
|
+
<div key={index} className="list-row items-start justify-between">
|
|
43
|
+
<span className="body-3 text-secondary tabular-nums">{index}</span>
|
|
44
|
+
<div className="layout-stack min-w-0 flex-1 gap-1">
|
|
45
|
+
<h2 className="heading-3">{title}</h2>
|
|
46
|
+
<p className="body-3 text-secondary">{description}</p>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
))}
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
</main>
|
|
53
|
+
),
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const ListRowsWithMedia: Story = {
|
|
57
|
+
render: () => (
|
|
58
|
+
<main className="page-shell bg-canvas text-primary">
|
|
59
|
+
<div className="layout-stack">
|
|
60
|
+
<h1 className="display-1">List rows with media</h1>
|
|
61
|
+
<div className="border-subtle divide-border-subtle divide-y border-b">
|
|
62
|
+
<div className="list-row items-center">
|
|
63
|
+
<div className="list-media list-media-wide bg-panel" aria-hidden="true">
|
|
64
|
+
<span className="heading-3 text-secondary">WO</span>
|
|
65
|
+
</div>
|
|
66
|
+
<div className="layout-stack min-w-0 gap-1">
|
|
67
|
+
<h2 className="heading-3">Wide media row</h2>
|
|
68
|
+
<p className="body-3 text-secondary">For client or partner marks.</p>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
<div className="list-row items-center">
|
|
72
|
+
<div className="list-media list-media-square bg-panel" aria-hidden="true">
|
|
73
|
+
<span className="heading-3 text-secondary">PR</span>
|
|
74
|
+
</div>
|
|
75
|
+
<div className="layout-stack min-w-0 gap-1">
|
|
76
|
+
<h2 className="heading-3">Square media row</h2>
|
|
77
|
+
<p className="body-3 text-secondary">For product or project marks.</p>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</main>
|
|
83
|
+
),
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const DetailSections: Story = {
|
|
87
|
+
render: () => (
|
|
88
|
+
<main className="bg-canvas text-primary">
|
|
89
|
+
<section className="section section-hero">
|
|
90
|
+
<p className="body-4 text-secondary">Detail hero</p>
|
|
91
|
+
<h1 className="display-1">A detail page with a clear vertical rhythm</h1>
|
|
92
|
+
</section>
|
|
93
|
+
<section className="section">
|
|
94
|
+
<h2 className="heading-2">Standard section</h2>
|
|
95
|
+
<p className="body-1 text-secondary max-w-2xl">
|
|
96
|
+
Sections share a border, gutter, and vertical breathing room without requiring a React
|
|
97
|
+
component.
|
|
98
|
+
</p>
|
|
99
|
+
</section>
|
|
100
|
+
<section className="section gap-8">
|
|
101
|
+
<h2 className="heading-2">Dense section</h2>
|
|
102
|
+
<ol className="layout-stack gap-8">
|
|
103
|
+
<li className="body-1">01 — Start with the problem.</li>
|
|
104
|
+
<li className="body-1">02 — Explain the approach.</li>
|
|
105
|
+
<li className="body-1">03 — Close with the outcome.</li>
|
|
106
|
+
</ol>
|
|
107
|
+
</section>
|
|
108
|
+
<div className="detail-navigation px-4 md:px-6">
|
|
109
|
+
<a className="body-2 text-secondary hover:text-primary" href="#previous">
|
|
110
|
+
← Previous
|
|
111
|
+
</a>
|
|
112
|
+
<a className="body-2 text-secondary hover:text-primary" href="#next">
|
|
113
|
+
Next →
|
|
114
|
+
</a>
|
|
115
|
+
</div>
|
|
116
|
+
</main>
|
|
117
|
+
),
|
|
118
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Motion tokens — durations and translate distances.
|
|
3
|
+
*
|
|
4
|
+
* Durations are in milliseconds. Must match animations.css and
|
|
5
|
+
* apps/omiro/theme/motion.ts (which imports from here).
|
|
6
|
+
*
|
|
7
|
+
* CSS easing strings are web-only. Mobile uses react-native-reanimated
|
|
8
|
+
* Easing.* functions which cannot be expressed as strings — the mobile
|
|
9
|
+
* motion.ts keeps those local and imports only the numeric constants here.
|
|
10
|
+
*
|
|
11
|
+
* ## Animation mandate
|
|
12
|
+
*
|
|
13
|
+
* Web interactive animations MUST use the shared GSAP helpers exposed by the
|
|
14
|
+
* consuming app.
|
|
15
|
+
* The canonical sequences (playFocusExpand, playFocusCollapse,
|
|
16
|
+
* playContextSwitch, playSubmitPulse, playEnterRow, playExitRow,
|
|
17
|
+
* playShimmer) read the numeric constants from this file so all motion
|
|
18
|
+
* stays in sync.
|
|
19
|
+
*
|
|
20
|
+
* CSS `motion-anim-*` classes (animations.css) are reserved for Radix UI
|
|
21
|
+
* component enter/exit only — do not add new CSS keyframe animations for
|
|
22
|
+
* interactive product surfaces.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export const durations = {
|
|
26
|
+
/** Element arrives and settles (decelerate). */
|
|
27
|
+
enter: 150,
|
|
28
|
+
/** Element leaves without lingering (accelerate). */
|
|
29
|
+
exit: 120,
|
|
30
|
+
/** Content animations: loading states, thinking indicators. */
|
|
31
|
+
standard: 120,
|
|
32
|
+
/** Loop / breezy animations. */
|
|
33
|
+
breezy: 1800,
|
|
34
|
+
spin: 1200,
|
|
35
|
+
} as const;
|
|
36
|
+
|
|
37
|
+
export const translateDistances = {
|
|
38
|
+
/** Enter lift (px). */
|
|
39
|
+
enterY: 6,
|
|
40
|
+
/** Exit settle (px). */
|
|
41
|
+
exitY: 4,
|
|
42
|
+
enterX: 6,
|
|
43
|
+
exitX: 4,
|
|
44
|
+
} as const;
|
|
45
|
+
|
|
46
|
+
/** CSS cubic-bezier strings — web only. */
|
|
47
|
+
export const easingWeb = {
|
|
48
|
+
enter: "cubic-bezier(0.0, 0.0, 0.2, 1)",
|
|
49
|
+
exit: "cubic-bezier(0.4, 0.0, 1, 1)",
|
|
50
|
+
standard: "cubic-bezier(0.4, 0, 0.2, 1)",
|
|
51
|
+
} as const;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shadow tokens.
|
|
3
|
+
*
|
|
4
|
+
* `shadowsWeb` values must match --shadow-* in globals.css.
|
|
5
|
+
* `shadowsNative` are React Native shadow object equivalents.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const shadowsWeb = {
|
|
9
|
+
low: "0 2px 8px rgba(0, 0, 0, 0.35)",
|
|
10
|
+
medium: "0 8px 24px rgba(0, 0, 0, 0.45)",
|
|
11
|
+
high: "0 20px 60px rgba(0, 0, 0, 0.55)",
|
|
12
|
+
} as const;
|
|
13
|
+
|
|
14
|
+
/** React Native shadow objects. Pass these into StyleSheet styles. */
|
|
15
|
+
export const shadowsNative = {
|
|
16
|
+
low: {
|
|
17
|
+
shadowColor: "#0f1113",
|
|
18
|
+
shadowOffset: { width: 0, height: 2 },
|
|
19
|
+
shadowOpacity: 0.35,
|
|
20
|
+
shadowRadius: 8,
|
|
21
|
+
},
|
|
22
|
+
medium: {
|
|
23
|
+
shadowColor: "#0f1113",
|
|
24
|
+
shadowOffset: { width: 0, height: 8 },
|
|
25
|
+
shadowOpacity: 0.45,
|
|
26
|
+
shadowRadius: 24,
|
|
27
|
+
},
|
|
28
|
+
high: {
|
|
29
|
+
shadowColor: "#0f1113",
|
|
30
|
+
shadowOffset: { width: 0, height: 20 },
|
|
31
|
+
shadowOpacity: 0.55,
|
|
32
|
+
shadowRadius: 60,
|
|
33
|
+
},
|
|
34
|
+
} as const;
|