@sovereignfs/ui 0.21.0

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.
Files changed (141) hide show
  1. package/LICENSE +661 -0
  2. package/NOTICE +24 -0
  3. package/dist/Avatar.module.css +38 -0
  4. package/dist/Badge.module.css +88 -0
  5. package/dist/Button.module.css +77 -0
  6. package/dist/Card.module.css +34 -0
  7. package/dist/Dialog.module.css +188 -0
  8. package/dist/Drawer.module.css +30 -0
  9. package/dist/EmptyState.module.css +27 -0
  10. package/dist/FormField.module.css +28 -0
  11. package/dist/Icon.module.css +22 -0
  12. package/dist/Input.module.css +37 -0
  13. package/dist/NavTabs.module.css +48 -0
  14. package/dist/PageHeader.module.css +32 -0
  15. package/dist/Popover.module.css +44 -0
  16. package/dist/SegmentedControl.module.css +53 -0
  17. package/dist/Select.module.css +69 -0
  18. package/dist/Spinner.module.css +39 -0
  19. package/dist/SystemBanner.module.css +67 -0
  20. package/dist/Tabs.module.css +49 -0
  21. package/dist/Toast.module.css +130 -0
  22. package/dist/Toggle.module.css +52 -0
  23. package/dist/Tooltip.module.css +51 -0
  24. package/dist/index.d.ts +327 -0
  25. package/dist/index.js +1370 -0
  26. package/dist/tokens/primitives.css +99 -0
  27. package/dist/tokens/semantic.css +111 -0
  28. package/dist/tokens.css +5 -0
  29. package/package.json +50 -0
  30. package/src/components/Avatar/Avatar.module.css +38 -0
  31. package/src/components/Avatar/Avatar.tsx +42 -0
  32. package/src/components/Badge/Badge.module.css +88 -0
  33. package/src/components/Badge/Badge.stories.tsx +118 -0
  34. package/src/components/Badge/Badge.tsx +59 -0
  35. package/src/components/Badge/__tests__/Badge.test.tsx +64 -0
  36. package/src/components/Button/Button.module.css +77 -0
  37. package/src/components/Button/Button.stories.tsx +76 -0
  38. package/src/components/Button/Button.tsx +36 -0
  39. package/src/components/Button/__tests__/Button.test.tsx +37 -0
  40. package/src/components/Card/Card.module.css +34 -0
  41. package/src/components/Card/Card.tsx +34 -0
  42. package/src/components/Dialog/Dialog.module.css +188 -0
  43. package/src/components/Dialog/Dialog.stories.tsx +97 -0
  44. package/src/components/Dialog/Dialog.tsx +137 -0
  45. package/src/components/Dialog/__tests__/Dialog.test.tsx +73 -0
  46. package/src/components/Drawer/Drawer.module.css +30 -0
  47. package/src/components/Drawer/Drawer.stories.tsx +88 -0
  48. package/src/components/Drawer/Drawer.tsx +103 -0
  49. package/src/components/Drawer/__tests__/Drawer.test.tsx +56 -0
  50. package/src/components/EmptyState/EmptyState.module.css +27 -0
  51. package/src/components/EmptyState/EmptyState.tsx +27 -0
  52. package/src/components/FormField/FormField.module.css +28 -0
  53. package/src/components/FormField/FormField.tsx +53 -0
  54. package/src/components/Icon/Icon.module.css +22 -0
  55. package/src/components/Icon/Icon.stories.tsx +94 -0
  56. package/src/components/Icon/Icon.tsx +43 -0
  57. package/src/components/Icon/__tests__/Icon.test.tsx +40 -0
  58. package/src/components/Icon/icons/activity.tsx +20 -0
  59. package/src/components/Icon/icons/alert-triangle.tsx +22 -0
  60. package/src/components/Icon/icons/bell.tsx +21 -0
  61. package/src/components/Icon/icons/check.tsx +20 -0
  62. package/src/components/Icon/icons/chevron-down.tsx +20 -0
  63. package/src/components/Icon/icons/chevron-left.tsx +20 -0
  64. package/src/components/Icon/icons/chevron-right.tsx +20 -0
  65. package/src/components/Icon/icons/chevron-up.tsx +20 -0
  66. package/src/components/Icon/icons/eye-off.tsx +23 -0
  67. package/src/components/Icon/icons/eye.tsx +21 -0
  68. package/src/components/Icon/icons/grid-2x2.tsx +22 -0
  69. package/src/components/Icon/icons/house.tsx +21 -0
  70. package/src/components/Icon/icons/index.ts +122 -0
  71. package/src/components/Icon/icons/info.tsx +22 -0
  72. package/src/components/Icon/icons/lock.tsx +21 -0
  73. package/src/components/Icon/icons/log-out.tsx +22 -0
  74. package/src/components/Icon/icons/mail.tsx +21 -0
  75. package/src/components/Icon/icons/package.tsx +23 -0
  76. package/src/components/Icon/icons/pencil.tsx +21 -0
  77. package/src/components/Icon/icons/plus.tsx +21 -0
  78. package/src/components/Icon/icons/rotate-ccw.tsx +21 -0
  79. package/src/components/Icon/icons/search.tsx +21 -0
  80. package/src/components/Icon/icons/settings.tsx +21 -0
  81. package/src/components/Icon/icons/shield.tsx +20 -0
  82. package/src/components/Icon/icons/sliders-horizontal.tsx +28 -0
  83. package/src/components/Icon/icons/terminal.tsx +19 -0
  84. package/src/components/Icon/icons/trash-2.tsx +24 -0
  85. package/src/components/Icon/icons/user.tsx +21 -0
  86. package/src/components/Icon/icons/x.tsx +21 -0
  87. package/src/components/Input/Input.module.css +37 -0
  88. package/src/components/Input/Input.stories.tsx +86 -0
  89. package/src/components/Input/Input.tsx +14 -0
  90. package/src/components/Input/__tests__/Input.test.tsx +22 -0
  91. package/src/components/NavTabs/NavTabs.module.css +48 -0
  92. package/src/components/NavTabs/NavTabs.tsx +33 -0
  93. package/src/components/PageHeader/PageHeader.module.css +32 -0
  94. package/src/components/PageHeader/PageHeader.tsx +21 -0
  95. package/src/components/Popover/Popover.module.css +44 -0
  96. package/src/components/Popover/Popover.stories.tsx +232 -0
  97. package/src/components/Popover/Popover.tsx +75 -0
  98. package/src/components/Popover/__tests__/Popover.test.tsx +98 -0
  99. package/src/components/SegmentedControl/SegmentedControl.module.css +53 -0
  100. package/src/components/SegmentedControl/SegmentedControl.stories.tsx +118 -0
  101. package/src/components/SegmentedControl/SegmentedControl.tsx +56 -0
  102. package/src/components/SegmentedControl/__tests__/SegmentedControl.test.tsx +63 -0
  103. package/src/components/Select/Select.module.css +69 -0
  104. package/src/components/Select/Select.stories.tsx +115 -0
  105. package/src/components/Select/Select.tsx +40 -0
  106. package/src/components/Select/__tests__/Select.test.tsx +59 -0
  107. package/src/components/Spinner/Spinner.module.css +39 -0
  108. package/src/components/Spinner/Spinner.tsx +23 -0
  109. package/src/components/SystemBanner/SystemBanner.module.css +67 -0
  110. package/src/components/SystemBanner/SystemBanner.stories.tsx +86 -0
  111. package/src/components/SystemBanner/SystemBanner.tsx +50 -0
  112. package/src/components/SystemBanner/__tests__/SystemBanner.test.tsx +50 -0
  113. package/src/components/Tabs/Tabs.module.css +49 -0
  114. package/src/components/Tabs/Tabs.stories.tsx +100 -0
  115. package/src/components/Tabs/Tabs.tsx +46 -0
  116. package/src/components/Tabs/__tests__/Tabs.test.tsx +45 -0
  117. package/src/components/Toast/Toast.module.css +130 -0
  118. package/src/components/Toast/Toast.stories.tsx +191 -0
  119. package/src/components/Toast/Toast.tsx +139 -0
  120. package/src/components/Toggle/Toggle.module.css +52 -0
  121. package/src/components/Toggle/Toggle.stories.tsx +95 -0
  122. package/src/components/Toggle/Toggle.tsx +39 -0
  123. package/src/components/Toggle/__tests__/Toggle.test.tsx +47 -0
  124. package/src/components/Tooltip/Tooltip.module.css +51 -0
  125. package/src/components/Tooltip/Tooltip.tsx +30 -0
  126. package/src/css-modules.d.ts +4 -0
  127. package/src/index.ts +45 -0
  128. package/src/stories/Avatar.stories.tsx +44 -0
  129. package/src/stories/Card.stories.tsx +35 -0
  130. package/src/stories/DesignSystemOverview.stories.tsx +1683 -0
  131. package/src/stories/EmptyState.stories.tsx +33 -0
  132. package/src/stories/FormField.stories.tsx +47 -0
  133. package/src/stories/MobilePatterns.stories.tsx +1285 -0
  134. package/src/stories/NavTabs.stories.tsx +35 -0
  135. package/src/stories/PageHeader.stories.tsx +31 -0
  136. package/src/stories/Spinner.stories.tsx +25 -0
  137. package/src/stories/TokenGallery.stories.tsx +355 -0
  138. package/src/stories/Tooltip.stories.tsx +59 -0
  139. package/src/tokens/primitives.css +99 -0
  140. package/src/tokens/semantic.css +111 -0
  141. package/src/tokens.css +5 -0
@@ -0,0 +1,1683 @@
1
+ import { useState } from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react-vite';
3
+
4
+ import { Avatar } from '../components/Avatar/Avatar';
5
+ import { Badge } from '../components/Badge/Badge';
6
+ import { Button } from '../components/Button/Button';
7
+ import { Card } from '../components/Card/Card';
8
+ import { Dialog } from '../components/Dialog/Dialog';
9
+ import { Drawer } from '../components/Drawer/Drawer';
10
+ import { EmptyState } from '../components/EmptyState/EmptyState';
11
+ import { FormField } from '../components/FormField/FormField';
12
+ import { Icon } from '../components/Icon/Icon';
13
+ import { Input } from '../components/Input/Input';
14
+ import { NavTabs } from '../components/NavTabs/NavTabs';
15
+ import { PageHeader } from '../components/PageHeader/PageHeader';
16
+ import { Popover } from '../components/Popover/Popover';
17
+ import { SegmentedControl } from '../components/SegmentedControl/SegmentedControl';
18
+ import { Select } from '../components/Select/Select';
19
+ import { Spinner } from '../components/Spinner/Spinner';
20
+ import { SystemBanner } from '../components/SystemBanner/SystemBanner';
21
+ import { Tabs } from '../components/Tabs/Tabs';
22
+ import { ToastProvider, useToast } from '../components/Toast/Toast';
23
+ import { Toggle } from '../components/Toggle/Toggle';
24
+ import { Tooltip } from '../components/Tooltip/Tooltip';
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Shared primitives
28
+ // ---------------------------------------------------------------------------
29
+
30
+ const ff = 'var(--sv-font-family)';
31
+ const ffm = 'var(--sv-font-family-mono)';
32
+
33
+ function Heading({ level = 2, children }: { level?: 1 | 2 | 3; children: React.ReactNode }) {
34
+ const sizes: Record<number, string> = { 1: '2rem', 2: '1.25rem', 3: '1rem' };
35
+ const weights: Record<number, number> = { 1: 700, 2: 600, 3: 600 };
36
+ const Tag = `h${level}` as 'h1' | 'h2' | 'h3';
37
+ return (
38
+ <Tag
39
+ style={{
40
+ fontFamily: ff,
41
+ fontSize: sizes[level],
42
+ fontWeight: weights[level],
43
+ color: 'var(--sv-color-text-primary)',
44
+ margin: 0,
45
+ lineHeight: 1.2,
46
+ }}
47
+ >
48
+ {children}
49
+ </Tag>
50
+ );
51
+ }
52
+
53
+ function SectionHeader({ title, subtitle }: { title: string; subtitle?: string }) {
54
+ return (
55
+ <div
56
+ style={{
57
+ paddingBottom: 'var(--sv-space-3)',
58
+ borderBottom: '2px solid var(--sv-color-accent)',
59
+ marginBottom: 'var(--sv-space-5)',
60
+ }}
61
+ >
62
+ <Heading level={2}>{title}</Heading>
63
+ {subtitle && (
64
+ <p
65
+ style={{
66
+ fontFamily: ff,
67
+ fontSize: 'var(--sv-font-size-sm)',
68
+ color: 'var(--sv-color-text-muted)',
69
+ marginTop: 'var(--sv-space-1)',
70
+ }}
71
+ >
72
+ {subtitle}
73
+ </p>
74
+ )}
75
+ </div>
76
+ );
77
+ }
78
+
79
+ function Code({ children }: { children: string }) {
80
+ return (
81
+ <pre
82
+ style={{
83
+ fontFamily: ffm,
84
+ fontSize: 'var(--sv-font-size-xs)',
85
+ background: 'var(--sv-color-surface-sunken)',
86
+ border: '1px solid var(--sv-color-border)',
87
+ borderRadius: 'var(--sv-radius-md)',
88
+ padding: 'var(--sv-space-3) var(--sv-space-4)',
89
+ color: 'var(--sv-color-text-primary)',
90
+ overflowX: 'auto',
91
+ margin: 0,
92
+ lineHeight: 1.6,
93
+ whiteSpace: 'pre',
94
+ }}
95
+ >
96
+ {children}
97
+ </pre>
98
+ );
99
+ }
100
+
101
+ function DemoBox({ children, style }: { children: React.ReactNode; style?: React.CSSProperties }) {
102
+ return (
103
+ <div
104
+ style={{
105
+ background: 'var(--sv-color-surface-sunken)',
106
+ border: '1px solid var(--sv-color-border)',
107
+ borderRadius: 'var(--sv-radius-lg)',
108
+ padding: 'var(--sv-space-4)',
109
+ display: 'flex',
110
+ alignItems: 'center',
111
+ justifyContent: 'center',
112
+ gap: 'var(--sv-space-3)',
113
+ flexWrap: 'wrap',
114
+ minHeight: 80,
115
+ ...style,
116
+ }}
117
+ >
118
+ {children}
119
+ </div>
120
+ );
121
+ }
122
+
123
+ function Pill({ children }: { children: React.ReactNode }) {
124
+ return (
125
+ <span
126
+ style={{
127
+ display: 'inline-block',
128
+ fontFamily: ffm,
129
+ fontSize: '0.6875rem',
130
+ fontWeight: 600,
131
+ letterSpacing: '0.03em',
132
+ background: 'var(--sv-color-surface-sunken)',
133
+ border: '1px solid var(--sv-color-border)',
134
+ color: 'var(--sv-color-text-muted)',
135
+ borderRadius: 'var(--sv-radius-full)',
136
+ padding: '2px 10px',
137
+ }}
138
+ >
139
+ {children}
140
+ </span>
141
+ );
142
+ }
143
+
144
+ // ---------------------------------------------------------------------------
145
+ // Color groups
146
+ // ---------------------------------------------------------------------------
147
+
148
+ const COLOR_GROUPS: Array<{
149
+ label: string;
150
+ usage: string;
151
+ tokens: Array<{ token: string; name: string }>;
152
+ }> = [
153
+ {
154
+ label: 'Surface',
155
+ usage: 'Page backgrounds and card elevations.',
156
+ tokens: [
157
+ { token: '--sv-color-surface', name: 'surface' },
158
+ { token: '--sv-color-surface-sunken', name: 'surface-sunken' },
159
+ { token: '--sv-color-surface-raised', name: 'surface-raised' },
160
+ ],
161
+ },
162
+ {
163
+ label: 'Text',
164
+ usage: 'Copy hierarchy from primary body to disabled hints.',
165
+ tokens: [
166
+ { token: '--sv-color-text-primary', name: 'text-primary' },
167
+ { token: '--sv-color-text-muted', name: 'text-muted' },
168
+ { token: '--sv-color-text-subtle', name: 'text-subtle' },
169
+ { token: '--sv-color-text-on-accent', name: 'text-on-accent' },
170
+ ],
171
+ },
172
+ {
173
+ label: 'Border',
174
+ usage: 'Dividers, input outlines, and card edges.',
175
+ tokens: [
176
+ { token: '--sv-color-border', name: 'border' },
177
+ { token: '--sv-color-border-strong', name: 'border-strong' },
178
+ ],
179
+ },
180
+ {
181
+ label: 'Accent',
182
+ usage:
183
+ 'Primary interactive color. Monochrome by default; instance admins override with their brand color.',
184
+ tokens: [
185
+ { token: '--sv-color-accent', name: 'accent' },
186
+ { token: '--sv-color-accent-hover', name: 'accent-hover' },
187
+ { token: '--sv-color-focus-ring', name: 'focus-ring' },
188
+ ],
189
+ },
190
+ {
191
+ label: 'Error',
192
+ usage: 'Destructive states, form validation errors, critical banners.',
193
+ tokens: [
194
+ { token: '--sv-color-error-surface', name: 'error-surface' },
195
+ { token: '--sv-color-error-text', name: 'error-text' },
196
+ { token: '--sv-color-error-border', name: 'error-border' },
197
+ ],
198
+ },
199
+ {
200
+ label: 'Warning',
201
+ usage: 'Caution states, expiring licenses, near-limit notices.',
202
+ tokens: [
203
+ { token: '--sv-color-warning-surface', name: 'warning-surface' },
204
+ { token: '--sv-color-warning-text', name: 'warning-text' },
205
+ { token: '--sv-color-warning-border', name: 'warning-border' },
206
+ ],
207
+ },
208
+ {
209
+ label: 'Success',
210
+ usage: 'Positive confirmations, completed actions, healthy status.',
211
+ tokens: [
212
+ { token: '--sv-color-success-surface', name: 'success-surface' },
213
+ { token: '--sv-color-success-text', name: 'success-text' },
214
+ { token: '--sv-color-success-border', name: 'success-border' },
215
+ ],
216
+ },
217
+ {
218
+ label: 'Info',
219
+ usage: 'Informational notices and neutral callouts.',
220
+ tokens: [
221
+ { token: '--sv-color-info-surface', name: 'info-surface' },
222
+ { token: '--sv-color-info-text', name: 'info-text' },
223
+ { token: '--sv-color-info-border', name: 'info-border' },
224
+ ],
225
+ },
226
+ ];
227
+
228
+ function ColorSwatch({ token, name }: { token: string; name: string }) {
229
+ const value =
230
+ typeof window !== 'undefined'
231
+ ? getComputedStyle(document.documentElement).getPropertyValue(token).trim()
232
+ : '';
233
+ return (
234
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6, flex: '1 1 160px' }}>
235
+ <div
236
+ style={{
237
+ height: 44,
238
+ borderRadius: 'var(--sv-radius-md)',
239
+ background: `var(${token})`,
240
+ border: '1px solid var(--sv-color-border)',
241
+ }}
242
+ />
243
+ <div>
244
+ <p
245
+ style={{
246
+ fontFamily: ffm,
247
+ fontSize: '0.6875rem',
248
+ color: 'var(--sv-color-text-primary)',
249
+ margin: 0,
250
+ }}
251
+ >
252
+ --sv-color-{name}
253
+ </p>
254
+ <p
255
+ style={{
256
+ fontFamily: ffm,
257
+ fontSize: '0.6875rem',
258
+ color: 'var(--sv-color-text-muted)',
259
+ margin: 0,
260
+ }}
261
+ >
262
+ {value}
263
+ </p>
264
+ </div>
265
+ </div>
266
+ );
267
+ }
268
+
269
+ function ColorGroup({
270
+ label,
271
+ usage,
272
+ tokens,
273
+ }: {
274
+ label: string;
275
+ usage: string;
276
+ tokens: Array<{ token: string; name: string }>;
277
+ }) {
278
+ return (
279
+ <div style={{ marginBottom: 'var(--sv-space-8)' }}>
280
+ <div style={{ marginBottom: 'var(--sv-space-3)' }}>
281
+ <Heading level={3}>{label}</Heading>
282
+ <p
283
+ style={{
284
+ fontFamily: ff,
285
+ fontSize: 'var(--sv-font-size-caption)',
286
+ color: 'var(--sv-color-text-muted)',
287
+ margin: '4px 0 0',
288
+ }}
289
+ >
290
+ {usage}
291
+ </p>
292
+ </div>
293
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: 'var(--sv-space-4)' }}>
294
+ {tokens.map((t) => (
295
+ <ColorSwatch key={t.token} token={t.token} name={t.name} />
296
+ ))}
297
+ </div>
298
+ </div>
299
+ );
300
+ }
301
+
302
+ // ---------------------------------------------------------------------------
303
+ // Typography
304
+ // ---------------------------------------------------------------------------
305
+
306
+ const TYPE_SCALE: Array<{ token: string; label: string; note: string }> = [
307
+ { token: '--sv-font-size-2xl', label: '2xl — 24px', note: 'Page titles' },
308
+ { token: '--sv-font-size-xl', label: 'xl — 20px', note: 'Section headings' },
309
+ { token: '--sv-font-size-lg', label: 'lg — 18px', note: 'Sub-headings' },
310
+ { token: '--sv-font-size-md', label: 'md — 16px', note: 'Body (base)' },
311
+ { token: '--sv-font-size-sm', label: 'sm — 14px', note: 'Body copy, labels' },
312
+ { token: '--sv-font-size-caption', label: 'caption — 13px', note: 'Secondary / supporting copy' },
313
+ { token: '--sv-font-size-xs', label: 'xs — 12px', note: 'Mono identifiers, badges' },
314
+ { token: '--sv-font-size-label', label: 'label — 11px', note: 'All-caps section labels' },
315
+ ];
316
+
317
+ // ---------------------------------------------------------------------------
318
+ // Component cards
319
+ // ---------------------------------------------------------------------------
320
+
321
+ function ComponentCard({
322
+ name,
323
+ importLine,
324
+ usage,
325
+ children,
326
+ }: {
327
+ name: string;
328
+ importLine: string;
329
+ usage: string;
330
+ children: React.ReactNode;
331
+ }) {
332
+ return (
333
+ <div
334
+ style={{
335
+ border: '1px solid var(--sv-color-border)',
336
+ borderRadius: 'var(--sv-radius-xl)',
337
+ overflow: 'hidden',
338
+ background: 'var(--sv-color-surface)',
339
+ boxShadow: 'var(--sv-shadow-card)',
340
+ display: 'flex',
341
+ flexDirection: 'column',
342
+ }}
343
+ >
344
+ {/* Header */}
345
+ <div
346
+ style={{
347
+ padding: 'var(--sv-space-4)',
348
+ borderBottom: '1px solid var(--sv-color-border)',
349
+ background: 'var(--sv-color-surface-sunken)',
350
+ }}
351
+ >
352
+ <Heading level={3}>{name}</Heading>
353
+ <p
354
+ style={{
355
+ fontFamily: ff,
356
+ fontSize: 'var(--sv-font-size-caption)',
357
+ color: 'var(--sv-color-text-muted)',
358
+ margin: '4px 0 0',
359
+ }}
360
+ >
361
+ {usage}
362
+ </p>
363
+ </div>
364
+ {/* Demo */}
365
+ <div
366
+ style={{
367
+ padding: 'var(--sv-space-5)',
368
+ display: 'flex',
369
+ alignItems: 'center',
370
+ justifyContent: 'center',
371
+ minHeight: 96,
372
+ gap: 'var(--sv-space-3)',
373
+ flexWrap: 'wrap',
374
+ flexGrow: 1,
375
+ }}
376
+ >
377
+ {children}
378
+ </div>
379
+ {/* Import */}
380
+ <div style={{ padding: 'var(--sv-space-3) var(--sv-space-4)' }}>
381
+ <code
382
+ style={{
383
+ fontFamily: ffm,
384
+ fontSize: '0.6875rem',
385
+ color: 'var(--sv-color-text-muted)',
386
+ display: 'block',
387
+ }}
388
+ >
389
+ {importLine}
390
+ </code>
391
+ </div>
392
+ </div>
393
+ );
394
+ }
395
+
396
+ // Interactive wrappers
397
+
398
+ function ToggleDemo() {
399
+ const [on, setOn] = useState(false);
400
+ return <Toggle checked={on} onChange={setOn} aria-label="Enable feature" />;
401
+ }
402
+
403
+ function SegmentedDemo() {
404
+ const [v, setV] = useState<'user' | 'admin'>('user');
405
+ return (
406
+ <SegmentedControl
407
+ value={v}
408
+ onChange={setV}
409
+ options={[
410
+ { label: 'User', value: 'user' },
411
+ { label: 'Admin', value: 'admin' },
412
+ ]}
413
+ aria-label="Role"
414
+ />
415
+ );
416
+ }
417
+
418
+ function TabsDemo() {
419
+ const [tab, setTab] = useState('profile');
420
+ return (
421
+ <div style={{ width: '100%', fontFamily: ff }}>
422
+ <Tabs
423
+ items={[
424
+ { label: 'Profile', value: 'profile' },
425
+ { label: 'Security', value: 'security' },
426
+ { label: 'Data', value: 'data' },
427
+ ]}
428
+ value={tab}
429
+ onChange={setTab}
430
+ aria-label="Account"
431
+ />
432
+ </div>
433
+ );
434
+ }
435
+
436
+ function DialogDemo() {
437
+ const [open, setOpen] = useState(false);
438
+ return (
439
+ <>
440
+ <Button size="sm" onClick={() => setOpen(true)}>
441
+ Open dialog
442
+ </Button>
443
+ <Dialog open={open} onClose={() => setOpen(false)} size="sm" aria-label="Example dialog">
444
+ <div style={{ padding: 24, fontFamily: ff }}>
445
+ <Heading level={3}>Confirm action</Heading>
446
+ <p
447
+ style={{
448
+ color: 'var(--sv-color-text-muted)',
449
+ fontSize: 'var(--sv-font-size-sm)',
450
+ margin: '12px 0 20px',
451
+ }}
452
+ >
453
+ This will permanently delete the item. Are you sure?
454
+ </p>
455
+ <div style={{ display: 'flex', gap: 8 }}>
456
+ <Button variant="destructive" size="sm" onClick={() => setOpen(false)}>
457
+ Delete
458
+ </Button>
459
+ <Button variant="secondary" size="sm" onClick={() => setOpen(false)}>
460
+ Cancel
461
+ </Button>
462
+ </div>
463
+ </div>
464
+ </Dialog>
465
+ </>
466
+ );
467
+ }
468
+
469
+ function DrawerDemo() {
470
+ const [open, setOpen] = useState(false);
471
+ return (
472
+ <>
473
+ <Button size="sm" variant="secondary" onClick={() => setOpen(true)}>
474
+ Open drawer
475
+ </Button>
476
+ <Drawer open={open} onClose={() => setOpen(false)} aria-label="Navigation">
477
+ <ul style={{ listStyle: 'none', margin: 0, padding: '8px 0' }}>
478
+ {(['Home', 'Settings', 'Account'] as const).map((item) => (
479
+ <li key={item}>
480
+ <button
481
+ onClick={() => setOpen(false)}
482
+ style={{
483
+ display: 'block',
484
+ width: '100%',
485
+ padding: '12px 20px',
486
+ background: 'none',
487
+ border: 'none',
488
+ cursor: 'pointer',
489
+ fontSize: 15,
490
+ textAlign: 'left',
491
+ color: 'var(--sv-color-text-primary)',
492
+ fontFamily: ff,
493
+ }}
494
+ >
495
+ {item}
496
+ </button>
497
+ </li>
498
+ ))}
499
+ </ul>
500
+ </Drawer>
501
+ </>
502
+ );
503
+ }
504
+
505
+ function PopoverDemo() {
506
+ const [open, setOpen] = useState(false);
507
+ return (
508
+ <Popover
509
+ open={open}
510
+ onClose={() => setOpen(false)}
511
+ align="left"
512
+ aria-label="Options menu"
513
+ trigger={
514
+ <Button size="sm" variant="ghost" onClick={() => setOpen((o) => !o)}>
515
+ <Icon name="settings" size="sm" aria-hidden /> Options
516
+ </Button>
517
+ }
518
+ >
519
+ <div style={{ padding: 'var(--sv-space-3)', fontFamily: ff }}>
520
+ {['Edit', 'Duplicate', 'Delete'].map((item) => (
521
+ <button
522
+ key={item}
523
+ type="button"
524
+ onClick={() => setOpen(false)}
525
+ style={{
526
+ display: 'block',
527
+ width: '100%',
528
+ padding: '8px 12px',
529
+ background: 'none',
530
+ border: 'none',
531
+ cursor: 'pointer',
532
+ fontSize: 'var(--sv-font-size-sm)',
533
+ textAlign: 'left',
534
+ color:
535
+ item === 'Delete' ? 'var(--sv-color-error-text)' : 'var(--sv-color-text-primary)',
536
+ fontFamily: ff,
537
+ borderRadius: 'var(--sv-radius-sm)',
538
+ }}
539
+ >
540
+ {item}
541
+ </button>
542
+ ))}
543
+ </div>
544
+ </Popover>
545
+ );
546
+ }
547
+
548
+ function ToastDemo() {
549
+ const { show } = useToast();
550
+ return (
551
+ <Button
552
+ size="sm"
553
+ variant="secondary"
554
+ onClick={() =>
555
+ show({
556
+ title: 'Plugin installed',
557
+ message: 'Tasks v1.2.0 is now active.',
558
+ category: 'success',
559
+ })
560
+ }
561
+ >
562
+ Fire toast
563
+ </Button>
564
+ );
565
+ }
566
+
567
+ // ---------------------------------------------------------------------------
568
+ // Main component
569
+ // ---------------------------------------------------------------------------
570
+
571
+ function OverviewPage() {
572
+ return (
573
+ <div
574
+ style={{
575
+ fontFamily: ff,
576
+ background: 'var(--sv-color-surface)',
577
+ minHeight: '100vh',
578
+ color: 'var(--sv-color-text-primary)',
579
+ }}
580
+ >
581
+ {/* ── Hero ─────────────────────────────────────────────────────────── */}
582
+ <div
583
+ style={{
584
+ padding: 'var(--sv-space-10) var(--sv-space-8)',
585
+ borderBottom: '1px solid var(--sv-color-border)',
586
+ background: 'var(--sv-color-surface-sunken)',
587
+ }}
588
+ >
589
+ <div style={{ maxWidth: 800, margin: '0 auto' }}>
590
+ <div
591
+ style={{
592
+ display: 'flex',
593
+ alignItems: 'center',
594
+ gap: 'var(--sv-space-3)',
595
+ marginBottom: 'var(--sv-space-4)',
596
+ }}
597
+ >
598
+ <Pill>@sovereignfs/ui</Pill>
599
+ <Pill>v1.x</Pill>
600
+ </div>
601
+ <Heading level={1}>Sovereign Design System</Heading>
602
+ <p
603
+ style={{
604
+ fontSize: 'var(--sv-font-size-lg)',
605
+ color: 'var(--sv-color-text-muted)',
606
+ marginTop: 'var(--sv-space-3)',
607
+ maxWidth: 600,
608
+ lineHeight: 1.5,
609
+ }}
610
+ >
611
+ The component library and token system for building Sovereign plugins. Everything here
612
+ is the public contract — available to every plugin developer, stable across minor
613
+ versions.
614
+ </p>
615
+ <div
616
+ style={{
617
+ marginTop: 'var(--sv-space-5)',
618
+ padding: 'var(--sv-space-4)',
619
+ background: 'var(--sv-color-surface)',
620
+ border: '1px solid var(--sv-color-border)',
621
+ borderRadius: 'var(--sv-radius-lg)',
622
+ display: 'flex',
623
+ flexDirection: 'column',
624
+ gap: 'var(--sv-space-2)',
625
+ }}
626
+ >
627
+ <p
628
+ style={{
629
+ fontFamily: ffm,
630
+ fontSize: 'var(--sv-font-size-xs)',
631
+ color: 'var(--sv-color-text-muted)',
632
+ margin: 0,
633
+ }}
634
+ >
635
+ Three things to remember:
636
+ </p>
637
+ <ul
638
+ style={{
639
+ margin: 0,
640
+ paddingLeft: 'var(--sv-space-5)',
641
+ color: 'var(--sv-color-text-primary)',
642
+ fontSize: 'var(--sv-font-size-sm)',
643
+ lineHeight: 1.8,
644
+ }}
645
+ >
646
+ <li>
647
+ Import components from <code style={{ fontFamily: ffm }}>@sovereignfs/ui</code>
648
+ </li>
649
+ <li>
650
+ Use <code style={{ fontFamily: ffm }}>--sv-*</code> semantic tokens in your CSS —
651
+ they are injected globally by the runtime shell, no import needed
652
+ </li>
653
+ <li>Never hardcode hex values or reference primitive tokens directly</li>
654
+ </ul>
655
+ </div>
656
+ </div>
657
+ </div>
658
+
659
+ <div style={{ maxWidth: 800, margin: '0 auto', padding: 'var(--sv-space-8)' }}>
660
+ {/* ── Quick Start ───────────────────────────────────────────────── */}
661
+ <section style={{ marginBottom: 'var(--sv-space-12)' }}>
662
+ <SectionHeader
663
+ title="Quick start"
664
+ subtitle="Everything you need to build a Sovereign plugin UI."
665
+ />
666
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--sv-space-4)' }}>
667
+ <div>
668
+ <p
669
+ style={{
670
+ fontSize: 'var(--sv-font-size-caption)',
671
+ color: 'var(--sv-color-text-muted)',
672
+ marginBottom: 'var(--sv-space-2)',
673
+ }}
674
+ >
675
+ Import typed React components:
676
+ </p>
677
+ <Code>{`import { Button, Badge, Input, Icon, Toggle, Tabs } from '@sovereignfs/ui';`}</Code>
678
+ </div>
679
+ <div>
680
+ <p
681
+ style={{
682
+ fontSize: 'var(--sv-font-size-caption)',
683
+ color: 'var(--sv-color-text-muted)',
684
+ marginBottom: 'var(--sv-space-2)',
685
+ }}
686
+ >
687
+ Reference semantic tokens directly in plugin CSS — no import required:
688
+ </p>
689
+ <Code>{`.my-card {
690
+ background: var(--sv-color-surface);
691
+ border: 1px solid var(--sv-color-border);
692
+ border-radius: var(--sv-radius-lg);
693
+ padding: var(--sv-space-4);
694
+ box-shadow: var(--sv-shadow-card);
695
+ font-family: var(--sv-font-family);
696
+ color: var(--sv-color-text-primary);
697
+ }`}</Code>
698
+ </div>
699
+ </div>
700
+ </section>
701
+
702
+ {/* ── Token Architecture ────────────────────────────────────────── */}
703
+ <section style={{ marginBottom: 'var(--sv-space-12)' }}>
704
+ <SectionHeader
705
+ title="Token architecture"
706
+ subtitle="Two layers — only the semantic layer is a public API."
707
+ />
708
+ <div
709
+ style={{
710
+ display: 'grid',
711
+ gridTemplateColumns: '1fr auto 1fr auto 1fr',
712
+ gap: 'var(--sv-space-3)',
713
+ alignItems: 'center',
714
+ marginBottom: 'var(--sv-space-6)',
715
+ }}
716
+ >
717
+ {[
718
+ {
719
+ label: 'Primitives',
720
+ sub: '--sv-grey-900\n--sv-red-100\n--sv-space-4',
721
+ note: 'Internal only. Raw scale values.',
722
+ muted: true,
723
+ },
724
+ ].map((b) => (
725
+ <div
726
+ key={b.label}
727
+ style={{
728
+ border: `1px solid ${b.muted ? 'var(--sv-color-border)' : 'var(--sv-color-accent)'}`,
729
+ borderRadius: 'var(--sv-radius-lg)',
730
+ padding: 'var(--sv-space-4)',
731
+ opacity: b.muted ? 0.6 : 1,
732
+ }}
733
+ >
734
+ <p
735
+ style={{
736
+ fontSize: 'var(--sv-font-size-sm)',
737
+ fontWeight: 600,
738
+ margin: '0 0 6px',
739
+ color: 'var(--sv-color-text-primary)',
740
+ }}
741
+ >
742
+ {b.label}
743
+ </p>
744
+ <pre
745
+ style={{
746
+ fontFamily: ffm,
747
+ fontSize: '0.6875rem',
748
+ color: 'var(--sv-color-text-muted)',
749
+ margin: 0,
750
+ whiteSpace: 'pre',
751
+ }}
752
+ >
753
+ {b.sub}
754
+ </pre>
755
+ <p
756
+ style={{
757
+ fontSize: 'var(--sv-font-size-caption)',
758
+ color: 'var(--sv-color-text-muted)',
759
+ margin: '8px 0 0',
760
+ }}
761
+ >
762
+ {b.note}
763
+ </p>
764
+ </div>
765
+ ))}
766
+ <div style={{ textAlign: 'center', color: 'var(--sv-color-text-muted)', fontSize: 24 }}>
767
+
768
+ </div>
769
+ <div
770
+ style={{
771
+ border: '2px solid var(--sv-color-accent)',
772
+ borderRadius: 'var(--sv-radius-lg)',
773
+ padding: 'var(--sv-space-4)',
774
+ }}
775
+ >
776
+ <p
777
+ style={{
778
+ fontSize: 'var(--sv-font-size-sm)',
779
+ fontWeight: 600,
780
+ margin: '0 0 6px',
781
+ color: 'var(--sv-color-text-primary)',
782
+ }}
783
+ >
784
+ Semantic tokens ✓
785
+ </p>
786
+ <pre
787
+ style={{
788
+ fontFamily: ffm,
789
+ fontSize: '0.6875rem',
790
+ color: 'var(--sv-color-text-muted)',
791
+ margin: 0,
792
+ whiteSpace: 'pre',
793
+ }}
794
+ >
795
+ {'--sv-color-surface\n--sv-color-error-text\n--sv-radius-lg'}
796
+ </pre>
797
+ <p
798
+ style={{
799
+ fontSize: 'var(--sv-font-size-caption)',
800
+ color: 'var(--sv-color-text-muted)',
801
+ margin: '8px 0 0',
802
+ }}
803
+ >
804
+ Plugin public API. Theme-aware.
805
+ </p>
806
+ </div>
807
+ <div style={{ textAlign: 'center', color: 'var(--sv-color-text-muted)', fontSize: 24 }}>
808
+
809
+ </div>
810
+ <div
811
+ style={{
812
+ border: '2px solid var(--sv-color-accent)',
813
+ borderRadius: 'var(--sv-radius-lg)',
814
+ padding: 'var(--sv-space-4)',
815
+ }}
816
+ >
817
+ <p
818
+ style={{
819
+ fontSize: 'var(--sv-font-size-sm)',
820
+ fontWeight: 600,
821
+ margin: '0 0 6px',
822
+ color: 'var(--sv-color-text-primary)',
823
+ }}
824
+ >
825
+ Components ✓
826
+ </p>
827
+ <pre
828
+ style={{
829
+ fontFamily: ffm,
830
+ fontSize: '0.6875rem',
831
+ color: 'var(--sv-color-text-muted)',
832
+ margin: 0,
833
+ whiteSpace: 'pre',
834
+ }}
835
+ >
836
+ {'<Button />\n<Badge />\n<Input />'}
837
+ </pre>
838
+ <p
839
+ style={{
840
+ fontSize: 'var(--sv-font-size-caption)',
841
+ color: 'var(--sv-color-text-muted)',
842
+ margin: '8px 0 0',
843
+ }}
844
+ >
845
+ React, typed, RSC-safe.
846
+ </p>
847
+ </div>
848
+ </div>
849
+ <div
850
+ style={{
851
+ background: 'var(--sv-color-warning-surface)',
852
+ border: '1px solid var(--sv-color-warning-border)',
853
+ borderRadius: 'var(--sv-radius-md)',
854
+ padding: 'var(--sv-space-3) var(--sv-space-4)',
855
+ fontSize: 'var(--sv-font-size-sm)',
856
+ color: 'var(--sv-color-warning-text)',
857
+ }}
858
+ >
859
+ <strong>Never use primitive tokens in plugin code.</strong> Primitives like{' '}
860
+ <code style={{ fontFamily: ffm }}>--sv-grey-900</code> or{' '}
861
+ <code style={{ fontFamily: ffm }}>--sv-red-100</code> are fixed values — they do not
862
+ swap with dark mode or instance theming. Only semantic tokens do.
863
+ </div>
864
+ </section>
865
+
866
+ {/* ── Color System ─────────────────────────────────────────────── */}
867
+ <section style={{ marginBottom: 'var(--sv-space-12)' }}>
868
+ <SectionHeader
869
+ title="Color system"
870
+ subtitle="Semantic groups — use the Themes toolbar above to compare light and dark."
871
+ />
872
+ {COLOR_GROUPS.map((g) => (
873
+ <ColorGroup key={g.label} label={g.label} usage={g.usage} tokens={g.tokens} />
874
+ ))}
875
+ </section>
876
+
877
+ {/* ── Typography ───────────────────────────────────────────────── */}
878
+ <section style={{ marginBottom: 'var(--sv-space-12)' }}>
879
+ <SectionHeader
880
+ title="Typography"
881
+ subtitle="Hanken Grotesk (body) · JetBrains Mono (code) — fallback stacks apply when web fonts are not loaded."
882
+ />
883
+
884
+ <div
885
+ style={{
886
+ display: 'grid',
887
+ gridTemplateColumns: '1fr 1fr',
888
+ gap: 'var(--sv-space-6)',
889
+ marginBottom: 'var(--sv-space-6)',
890
+ }}
891
+ >
892
+ <div>
893
+ <p
894
+ style={{
895
+ fontSize: 'var(--sv-font-size-caption)',
896
+ color: 'var(--sv-color-text-muted)',
897
+ marginBottom: 'var(--sv-space-2)',
898
+ }}
899
+ >
900
+ Body — var(--sv-font-family)
901
+ </p>
902
+ <p
903
+ style={{
904
+ fontFamily: ff,
905
+ fontSize: 'var(--sv-font-size-lg)',
906
+ color: 'var(--sv-color-text-primary)',
907
+ margin: 0,
908
+ }}
909
+ >
910
+ The quick brown fox
911
+ </p>
912
+ <p
913
+ style={{
914
+ fontFamily: ff,
915
+ fontSize: 'var(--sv-font-size-sm)',
916
+ color: 'var(--sv-color-text-muted)',
917
+ margin: '4px 0 0',
918
+ }}
919
+ >
920
+ ABCDEFGHIJKLMNOPQRSTUVWXYZ
921
+ </p>
922
+ </div>
923
+ <div>
924
+ <p
925
+ style={{
926
+ fontSize: 'var(--sv-font-size-caption)',
927
+ color: 'var(--sv-color-text-muted)',
928
+ marginBottom: 'var(--sv-space-2)',
929
+ }}
930
+ >
931
+ Mono — var(--sv-font-family-mono)
932
+ </p>
933
+ <p
934
+ style={{
935
+ fontFamily: ffm,
936
+ fontSize: 'var(--sv-font-size-lg)',
937
+ color: 'var(--sv-color-text-primary)',
938
+ margin: 0,
939
+ }}
940
+ >
941
+ const x = 42;
942
+ </p>
943
+ <p
944
+ style={{
945
+ fontFamily: ffm,
946
+ fontSize: 'var(--sv-font-size-sm)',
947
+ color: 'var(--sv-color-text-muted)',
948
+ margin: '4px 0 0',
949
+ }}
950
+ >
951
+ --sv-font-family-mono
952
+ </p>
953
+ </div>
954
+ </div>
955
+
956
+ <div
957
+ style={{
958
+ border: '1px solid var(--sv-color-border)',
959
+ borderRadius: 'var(--sv-radius-lg)',
960
+ overflow: 'hidden',
961
+ }}
962
+ >
963
+ {TYPE_SCALE.map((t, i) => (
964
+ <div
965
+ key={t.token}
966
+ style={{
967
+ display: 'flex',
968
+ alignItems: 'baseline',
969
+ gap: 'var(--sv-space-4)',
970
+ padding: 'var(--sv-space-3) var(--sv-space-4)',
971
+ borderBottom:
972
+ i < TYPE_SCALE.length - 1 ? '1px solid var(--sv-color-border)' : 'none',
973
+ background:
974
+ i % 2 === 0 ? 'var(--sv-color-surface)' : 'var(--sv-color-surface-sunken)',
975
+ }}
976
+ >
977
+ <span
978
+ style={{
979
+ fontSize: `var(${t.token})`,
980
+ color: 'var(--sv-color-text-primary)',
981
+ lineHeight: 1.2,
982
+ minWidth: 160,
983
+ }}
984
+ >
985
+ {t.label}
986
+ </span>
987
+ <code
988
+ style={{
989
+ fontFamily: ffm,
990
+ fontSize: '0.6875rem',
991
+ color: 'var(--sv-color-text-muted)',
992
+ flexShrink: 0,
993
+ }}
994
+ >
995
+ {t.token}
996
+ </code>
997
+ <span
998
+ style={{
999
+ fontSize: 'var(--sv-font-size-caption)',
1000
+ color: 'var(--sv-color-text-muted)',
1001
+ marginLeft: 'auto',
1002
+ }}
1003
+ >
1004
+ {t.note}
1005
+ </span>
1006
+ </div>
1007
+ ))}
1008
+ </div>
1009
+
1010
+ <div style={{ marginTop: 'var(--sv-space-4)' }}>
1011
+ <p
1012
+ style={{
1013
+ fontSize: 'var(--sv-font-size-caption)',
1014
+ color: 'var(--sv-color-text-muted)',
1015
+ marginBottom: 'var(--sv-space-2)',
1016
+ }}
1017
+ >
1018
+ Font weight tokens:
1019
+ </p>
1020
+ <Code>{`font-weight: var(--sv-font-weight-regular); /* 400 */
1021
+ font-weight: var(--sv-font-weight-medium); /* 500 */
1022
+ font-weight: var(--sv-font-weight-semibold); /* 600 */
1023
+ font-weight: var(--sv-font-weight-bold); /* 700 */`}</Code>
1024
+ </div>
1025
+ </section>
1026
+
1027
+ {/* ── Spacing & Radius ─────────────────────────────────────────── */}
1028
+ <section style={{ marginBottom: 'var(--sv-space-12)' }}>
1029
+ <SectionHeader title="Spacing & radius" subtitle="4px base grid." />
1030
+ <div
1031
+ style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 'var(--sv-space-8)' }}
1032
+ >
1033
+ {/* Spacing */}
1034
+ <div>
1035
+ <p
1036
+ style={{
1037
+ fontSize: 'var(--sv-font-size-caption)',
1038
+ color: 'var(--sv-color-text-muted)',
1039
+ marginBottom: 'var(--sv-space-3)',
1040
+ }}
1041
+ >
1042
+ Spacing (--sv-space-*)
1043
+ </p>
1044
+ {(
1045
+ [
1046
+ '--sv-space-1',
1047
+ '--sv-space-2',
1048
+ '--sv-space-3',
1049
+ '--sv-space-4',
1050
+ '--sv-space-5',
1051
+ '--sv-space-6',
1052
+ '--sv-space-8',
1053
+ '--sv-space-10',
1054
+ '--sv-space-12',
1055
+ '--sv-space-16',
1056
+ ] as const
1057
+ ).map((t) => (
1058
+ <div
1059
+ key={t}
1060
+ style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 8 }}
1061
+ >
1062
+ <div
1063
+ style={{
1064
+ width: `var(${t})`,
1065
+ minWidth: 4,
1066
+ height: 10,
1067
+ background: 'var(--sv-color-accent)',
1068
+ borderRadius: 2,
1069
+ flexShrink: 0,
1070
+ }}
1071
+ />
1072
+ <code
1073
+ style={{
1074
+ fontFamily: ffm,
1075
+ fontSize: '0.6875rem',
1076
+ color: 'var(--sv-color-text-muted)',
1077
+ }}
1078
+ >
1079
+ {t}
1080
+ </code>
1081
+ </div>
1082
+ ))}
1083
+ </div>
1084
+ {/* Radius */}
1085
+ <div>
1086
+ <p
1087
+ style={{
1088
+ fontSize: 'var(--sv-font-size-caption)',
1089
+ color: 'var(--sv-color-text-muted)',
1090
+ marginBottom: 'var(--sv-space-3)',
1091
+ }}
1092
+ >
1093
+ Border radius (--sv-radius-*)
1094
+ </p>
1095
+ {(
1096
+ [
1097
+ '--sv-radius-sm',
1098
+ '--sv-radius-md',
1099
+ '--sv-radius-lg',
1100
+ '--sv-radius-xl',
1101
+ '--sv-radius-2xl',
1102
+ '--sv-radius-3xl',
1103
+ '--sv-radius-full',
1104
+ ] as const
1105
+ ).map((t) => (
1106
+ <div
1107
+ key={t}
1108
+ style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 12 }}
1109
+ >
1110
+ <div
1111
+ style={{
1112
+ width: 48,
1113
+ height: 24,
1114
+ background: 'var(--sv-color-accent)',
1115
+ borderRadius: `var(${t})`,
1116
+ flexShrink: 0,
1117
+ }}
1118
+ />
1119
+ <code
1120
+ style={{
1121
+ fontFamily: ffm,
1122
+ fontSize: '0.6875rem',
1123
+ color: 'var(--sv-color-text-muted)',
1124
+ }}
1125
+ >
1126
+ {t}
1127
+ </code>
1128
+ </div>
1129
+ ))}
1130
+ </div>
1131
+ </div>
1132
+ </section>
1133
+
1134
+ {/* ── Shadows ──────────────────────────────────────────────────── */}
1135
+ <section style={{ marginBottom: 'var(--sv-space-12)' }}>
1136
+ <SectionHeader title="Elevation & shadows" subtitle="Four levels, dark-mode adjusted." />
1137
+ <div
1138
+ style={{
1139
+ display: 'grid',
1140
+ gridTemplateColumns: 'repeat(4, 1fr)',
1141
+ gap: 'var(--sv-space-5)',
1142
+ }}
1143
+ >
1144
+ {(
1145
+ [
1146
+ '--sv-shadow-card',
1147
+ '--sv-shadow-hover',
1148
+ '--sv-shadow-popover',
1149
+ '--sv-shadow-overlay',
1150
+ ] as const
1151
+ ).map((t) => (
1152
+ <div
1153
+ key={t}
1154
+ style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 12 }}
1155
+ >
1156
+ <div
1157
+ style={{
1158
+ width: 64,
1159
+ height: 40,
1160
+ background: 'var(--sv-color-surface-raised)',
1161
+ boxShadow: `var(${t})`,
1162
+ borderRadius: 'var(--sv-radius-md)',
1163
+ }}
1164
+ />
1165
+ <code
1166
+ style={{
1167
+ fontFamily: ffm,
1168
+ fontSize: '0.6875rem',
1169
+ color: 'var(--sv-color-text-muted)',
1170
+ textAlign: 'center',
1171
+ }}
1172
+ >
1173
+ {t.replace('--sv-shadow-', '')}
1174
+ </code>
1175
+ </div>
1176
+ ))}
1177
+ </div>
1178
+ </section>
1179
+
1180
+ {/* ── Component Gallery ─────────────────────────────────────────── */}
1181
+ <section style={{ marginBottom: 'var(--sv-space-12)' }}>
1182
+ <SectionHeader
1183
+ title="Component gallery"
1184
+ subtitle="All 13 components — click each story in the sidebar for the full API, variants, and controls."
1185
+ />
1186
+
1187
+ <div style={{ marginBottom: 'var(--sv-space-6)' }}>
1188
+ <SystemBanner variant="info">
1189
+ All components reference <code style={{ fontFamily: ffm }}>--sv-*</code> tokens
1190
+ internally — they automatically adapt to dark mode and instance theming without any
1191
+ extra configuration.
1192
+ </SystemBanner>
1193
+ </div>
1194
+
1195
+ <div
1196
+ style={{
1197
+ display: 'grid',
1198
+ gridTemplateColumns: 'repeat(auto-fill, minmax(340px, 1fr))',
1199
+ gap: 'var(--sv-space-5)',
1200
+ }}
1201
+ >
1202
+ {/* Button */}
1203
+ <ComponentCard
1204
+ name="Button"
1205
+ importLine="import { Button } from '@sovereignfs/ui';"
1206
+ usage="Primary interactive control. Four variants: primary, secondary, ghost, destructive. Two sizes: md (default), sm."
1207
+ >
1208
+ <Button>Primary</Button>
1209
+ <Button variant="secondary">Secondary</Button>
1210
+ <Button variant="ghost">Ghost</Button>
1211
+ <Button variant="destructive" size="sm">
1212
+ Delete
1213
+ </Button>
1214
+ </ComponentCard>
1215
+
1216
+ {/* Badge */}
1217
+ <ComponentCard
1218
+ name="Badge"
1219
+ importLine="import { Badge } from '@sovereignfs/ui';"
1220
+ usage="Compact label for roles (role), lifecycle states (status), and type/version tags (mono). RSC-safe."
1221
+ >
1222
+ <Badge variant="role">Admin</Badge>
1223
+ <Badge variant="status" status="active">
1224
+ Active
1225
+ </Badge>
1226
+ <Badge variant="status" status="invited">
1227
+ Invited
1228
+ </Badge>
1229
+ <Badge variant="mono">v1.2.0</Badge>
1230
+ </ComponentCard>
1231
+
1232
+ {/* Input */}
1233
+ <ComponentCard
1234
+ name="Input"
1235
+ importLine="import { Input } from '@sovereignfs/ui';"
1236
+ usage="Primitive text field. No label built-in — always pair with <label> for accessibility. Forwards all native input props."
1237
+ >
1238
+ <div style={{ width: '100%', display: 'flex', flexDirection: 'column', gap: 6 }}>
1239
+ <Input placeholder="Email address" type="email" style={{ width: '100%' }} />
1240
+ <Input placeholder="Disabled" disabled style={{ width: '100%' }} />
1241
+ </div>
1242
+ </ComponentCard>
1243
+
1244
+ {/* Select */}
1245
+ <ComponentCard
1246
+ name="Select"
1247
+ importLine="import { Select } from '@sovereignfs/ui';"
1248
+ usage="Styled native <select> — same visual language as Input. Preserves native picker on mobile. RSC-safe."
1249
+ >
1250
+ <div style={{ width: '100%' }}>
1251
+ <Select defaultValue="admin" style={{ width: '100%' }}>
1252
+ <option value="user">User</option>
1253
+ <option value="admin">Admin</option>
1254
+ <option value="owner">Owner</option>
1255
+ </Select>
1256
+ </div>
1257
+ </ComponentCard>
1258
+
1259
+ {/* Toggle */}
1260
+ <ComponentCard
1261
+ name="Toggle"
1262
+ importLine="import { Toggle } from '@sovereignfs/ui';"
1263
+ usage="38×22px binary switch. Renders as role='switch' for screen reader support. aria-label is required."
1264
+ >
1265
+ <ToggleDemo />
1266
+ <span
1267
+ style={{
1268
+ fontFamily: ff,
1269
+ fontSize: 'var(--sv-font-size-sm)',
1270
+ color: 'var(--sv-color-text-muted)',
1271
+ }}
1272
+ >
1273
+ Click to toggle
1274
+ </span>
1275
+ </ComponentCard>
1276
+
1277
+ {/* SegmentedControl */}
1278
+ <ComponentCard
1279
+ name="SegmentedControl"
1280
+ importLine="import { SegmentedControl } from '@sovereignfs/ui';"
1281
+ usage="Pill-based 2–3 option picker for inline use (role selector, theme switcher). Renders as role='radiogroup'."
1282
+ >
1283
+ <SegmentedDemo />
1284
+ </ComponentCard>
1285
+
1286
+ {/* Tabs */}
1287
+ <ComponentCard
1288
+ name="Tabs"
1289
+ importLine="import { Tabs } from '@sovereignfs/ui';"
1290
+ usage="Underline tab nav. Stateless — caller owns value + onChange. Scrolls horizontally on mobile."
1291
+ >
1292
+ <TabsDemo />
1293
+ </ComponentCard>
1294
+
1295
+ {/* Icon */}
1296
+ <ComponentCard
1297
+ name="Icon"
1298
+ importLine="import { Icon } from '@sovereignfs/ui';"
1299
+ usage="SVG icon primitive. 29 bundled icons. Decorative: aria-hidden. Meaningful: aria-label. Three sizes: sm, md, lg."
1300
+ >
1301
+ {(
1302
+ [
1303
+ 'house',
1304
+ 'settings',
1305
+ 'bell',
1306
+ 'user',
1307
+ 'shield',
1308
+ 'mail',
1309
+ 'search',
1310
+ 'plus',
1311
+ 'trash-2',
1312
+ ] as const
1313
+ ).map((n) => (
1314
+ <Icon key={n} name={n} size="md" aria-hidden />
1315
+ ))}
1316
+ </ComponentCard>
1317
+
1318
+ {/* Dialog */}
1319
+ <ComponentCard
1320
+ name="Dialog"
1321
+ importLine="import { Dialog } from '@sovereignfs/ui';"
1322
+ usage="Modal surface (scrim + panel). Esc, scrim-click, focus trap. Sizes: sm, md, lg, full. Mobile renders fullscreen."
1323
+ >
1324
+ <DialogDemo />
1325
+ </ComponentCard>
1326
+
1327
+ {/* Drawer */}
1328
+ <ComponentCard
1329
+ name="Drawer"
1330
+ importLine="import { Drawer } from '@sovereignfs/ui';"
1331
+ usage="Bottom-sheet panel for mobile navigation. Esc, scrim-click, focus trap. Respects safe-area-inset-bottom."
1332
+ >
1333
+ <DrawerDemo />
1334
+ </ComponentCard>
1335
+
1336
+ {/* Popover */}
1337
+ <ComponentCard
1338
+ name="Popover"
1339
+ importLine="import { Popover } from '@sovereignfs/ui';"
1340
+ usage="Floating panel anchored below a trigger. Non-modal. Closes on outside click or Escape. Left or right aligned."
1341
+ >
1342
+ <PopoverDemo />
1343
+ </ComponentCard>
1344
+
1345
+ {/* SystemBanner */}
1346
+ <ComponentCard
1347
+ name="SystemBanner"
1348
+ importLine="import { SystemBanner } from '@sovereignfs/ui';"
1349
+ usage="Full-width sticky strip for platform-level notices. Three variants: info, warning, error. Dismissible optional."
1350
+ >
1351
+ <div style={{ width: '100%', display: 'flex', flexDirection: 'column', gap: 4 }}>
1352
+ <SystemBanner variant="info">Read-only mode during migration.</SystemBanner>
1353
+ <SystemBanner variant="warning">License expires in 7 days.</SystemBanner>
1354
+ <SystemBanner variant="error">Maintenance mode active.</SystemBanner>
1355
+ </div>
1356
+ </ComponentCard>
1357
+
1358
+ {/* Toast */}
1359
+ <ComponentCard
1360
+ name="Toast"
1361
+ importLine="import { ToastProvider, useToast } from '@sovereignfs/ui';"
1362
+ usage="Fixed top-right notification stack. Wrap app in ToastProvider; call useToast().show() imperatively. Six categories."
1363
+ >
1364
+ <ToastProvider>
1365
+ <ToastDemo />
1366
+ </ToastProvider>
1367
+ </ComponentCard>
1368
+
1369
+ {/* Card */}
1370
+ <ComponentCard
1371
+ name="Card"
1372
+ importLine="import { Card } from '@sovereignfs/ui';"
1373
+ usage="Surface container with border, shadow, and padding. Use as='article' or 'li' for semantics. Add interactive for hover/focus styles on clickable cards."
1374
+ >
1375
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 8, width: '100%' }}>
1376
+ <Card padding="sm">Small padding card</Card>
1377
+ <Card padding="md" interactive>
1378
+ Interactive card — hover me
1379
+ </Card>
1380
+ </div>
1381
+ </ComponentCard>
1382
+
1383
+ {/* FormField */}
1384
+ <ComponentCard
1385
+ name="FormField"
1386
+ importLine="import { FormField } from '@sovereignfs/ui';"
1387
+ usage="Accessible label + input wrapper. Wires hint and error text to the child via aria-describedby. Always pair with an Input and matching htmlFor/id."
1388
+ >
1389
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 12, width: '100%' }}>
1390
+ <FormField label="Email" hint="We'll never share this." htmlFor="ov-email">
1391
+ <Input id="ov-email" type="email" placeholder="you@example.com" />
1392
+ </FormField>
1393
+ <FormField label="Password" error="Must be 8+ characters." htmlFor="ov-pw">
1394
+ <Input id="ov-pw" type="password" />
1395
+ </FormField>
1396
+ </div>
1397
+ </ComponentCard>
1398
+
1399
+ {/* PageHeader */}
1400
+ <ComponentCard
1401
+ name="PageHeader"
1402
+ importLine="import { PageHeader } from '@sovereignfs/ui';"
1403
+ usage="Plugin page top section. Title + optional description + right-side action slot. Replaces the hand-rolled .pageHeader pattern in every plugin."
1404
+ >
1405
+ <div style={{ width: '100%' }}>
1406
+ <PageHeader
1407
+ title="Users"
1408
+ description="Manage who has access to this instance."
1409
+ action={<Button size="sm">Invite user</Button>}
1410
+ />
1411
+ </div>
1412
+ </ComponentCard>
1413
+
1414
+ {/* EmptyState */}
1415
+ <ComponentCard
1416
+ name="EmptyState"
1417
+ importLine="import { EmptyState } from '@sovereignfs/ui';"
1418
+ usage="Zero-data placeholder. Icon slot, heading, description, and optional CTA. Use whenever a list or table has no rows."
1419
+ >
1420
+ <EmptyState
1421
+ icon="search"
1422
+ heading="No results found"
1423
+ description="Try adjusting your search."
1424
+ action={
1425
+ <Button variant="secondary" size="sm">
1426
+ Clear filters
1427
+ </Button>
1428
+ }
1429
+ />
1430
+ </ComponentCard>
1431
+
1432
+ {/* Spinner */}
1433
+ <ComponentCard
1434
+ name="Spinner"
1435
+ importLine="import { Spinner } from '@sovereignfs/ui';"
1436
+ usage="CSS-animated loading ring in sm/md/lg sizes matching icon-size tokens. Pauses under prefers-reduced-motion. Sets role='status' with aria-label."
1437
+ >
1438
+ <div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
1439
+ <Spinner size="sm" />
1440
+ <Spinner size="md" />
1441
+ <Spinner size="lg" />
1442
+ </div>
1443
+ </ComponentCard>
1444
+
1445
+ {/* Avatar */}
1446
+ <ComponentCard
1447
+ name="Avatar"
1448
+ importLine="import { Avatar } from '@sovereignfs/ui';"
1449
+ usage="User representation. Shows image when src loads; falls back to initials derived from name. Three sizes. Always sets alt text for accessibility."
1450
+ >
1451
+ <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
1452
+ <Avatar name="Jane Smith" size="sm" />
1453
+ <Avatar name="Jane Smith" size="md" />
1454
+ <Avatar name="Jane Smith" size="lg" />
1455
+ <Avatar name="Admin" src="https://i.pravatar.cc/150?u=sb" size="lg" />
1456
+ </div>
1457
+ </ComponentCard>
1458
+
1459
+ {/* NavTabs */}
1460
+ <ComponentCard
1461
+ name="NavTabs"
1462
+ importLine="import { NavTabs } from '@sovereignfs/ui';"
1463
+ usage="Underline-style navigation tabs for plugin-level page routing. Distinct from the contained Tabs component. Scrolls horizontally on mobile with no visible scrollbar."
1464
+ >
1465
+ <div style={{ width: '100%' }}>
1466
+ <NavTabs
1467
+ items={[
1468
+ { label: 'Profile', href: '#', active: true },
1469
+ { label: 'Security', href: '#' },
1470
+ { label: 'Preferences', href: '#' },
1471
+ { label: 'Data', href: '#' },
1472
+ ]}
1473
+ />
1474
+ </div>
1475
+ </ComponentCard>
1476
+
1477
+ {/* Tooltip */}
1478
+ <ComponentCard
1479
+ name="Tooltip"
1480
+ importLine="import { Tooltip } from '@sovereignfs/ui';"
1481
+ usage="CSS-only hover/focus hint. Four placements. Wired to its trigger via aria-describedby. No JS positioning — RSC-safe."
1482
+ >
1483
+ <div style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
1484
+ <Tooltip content="Saved to your account" side="top">
1485
+ <Button variant="secondary" size="sm">
1486
+ Save
1487
+ </Button>
1488
+ </Tooltip>
1489
+ <Tooltip content="Cannot be undone" side="right">
1490
+ <Button variant="destructive" size="sm">
1491
+ Delete
1492
+ </Button>
1493
+ </Tooltip>
1494
+ </div>
1495
+ </ComponentCard>
1496
+ </div>
1497
+ </section>
1498
+
1499
+ {/* ── Theming ──────────────────────────────────────────────────── */}
1500
+ <section style={{ marginBottom: 'var(--sv-space-12)' }}>
1501
+ <SectionHeader
1502
+ title="Tenant theming"
1503
+ subtitle="The identity is monochrome by default. Tenants add brand color by overriding --sv-color-accent."
1504
+ />
1505
+ <p
1506
+ style={{
1507
+ fontSize: 'var(--sv-font-size-sm)',
1508
+ color: 'var(--sv-color-text-muted)',
1509
+ marginBottom: 'var(--sv-space-4)',
1510
+ }}
1511
+ >
1512
+ Set via the Console → Instance identity section (or the{' '}
1513
+ <code style={{ fontFamily: ffm }}>/api/instance/</code> API). Plugin CSS does not need
1514
+ to change — all components read{' '}
1515
+ <code style={{ fontFamily: ffm }}>--sv-color-accent</code> from the cascade.
1516
+ </p>
1517
+ <Code>{`/* Operator CSS override — injected by InstanceProvider */
1518
+ :root {
1519
+ --sv-color-accent: #5c6bc0; /* brand blue */
1520
+ --sv-color-accent-hover: #3949ab;
1521
+ --sv-color-focus-ring: #5c6bc0;
1522
+ --sv-color-text-on-accent: #ffffff; /* must contrast with accent */
1523
+ }`}</Code>
1524
+
1525
+ <DemoBox style={{ marginTop: 'var(--sv-space-4)' }}>
1526
+ <Button style={{ background: '#5c6bc0', borderColor: '#5c6bc0', color: '#fff' }}>
1527
+ Themed button
1528
+ </Button>
1529
+ <span
1530
+ style={{
1531
+ display: 'inline-block',
1532
+ fontFamily: ff,
1533
+ fontSize: 'var(--sv-font-size-caption)',
1534
+ fontWeight: 600,
1535
+ padding: '2px 8px',
1536
+ borderRadius: 'var(--sv-radius-sm)',
1537
+ background: '#e8eaf6',
1538
+ color: '#3949ab',
1539
+ border: '1px solid #c5cae9',
1540
+ }}
1541
+ >
1542
+ Owner
1543
+ </span>
1544
+ <span
1545
+ style={{
1546
+ fontFamily: ff,
1547
+ fontSize: 'var(--sv-font-size-caption)',
1548
+ color: 'var(--sv-color-text-muted)',
1549
+ }}
1550
+ >
1551
+ Components read the accent token — brand applied everywhere.
1552
+ </span>
1553
+ </DemoBox>
1554
+ </section>
1555
+
1556
+ {/* ── Rules ────────────────────────────────────────────────────── */}
1557
+ <section style={{ marginBottom: 'var(--sv-space-8)' }}>
1558
+ <SectionHeader
1559
+ title="Design rules"
1560
+ subtitle="The short list of things that break dark mode or instance theming if ignored."
1561
+ />
1562
+ <div
1563
+ style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 'var(--sv-space-5)' }}
1564
+ >
1565
+ <div
1566
+ style={{
1567
+ background: 'var(--sv-color-success-surface)',
1568
+ border: '1px solid var(--sv-color-success-border)',
1569
+ borderRadius: 'var(--sv-radius-lg)',
1570
+ padding: 'var(--sv-space-4)',
1571
+ }}
1572
+ >
1573
+ <p
1574
+ style={{
1575
+ fontWeight: 600,
1576
+ color: 'var(--sv-color-success-text)',
1577
+ marginBottom: 'var(--sv-space-3)',
1578
+ fontSize: 'var(--sv-font-size-sm)',
1579
+ }}
1580
+ >
1581
+ ✓ Do
1582
+ </p>
1583
+ <ul
1584
+ style={{
1585
+ margin: 0,
1586
+ padding: '0 0 0 var(--sv-space-4)',
1587
+ fontSize: 'var(--sv-font-size-sm)',
1588
+ color: 'var(--sv-color-text-primary)',
1589
+ lineHeight: 1.8,
1590
+ }}
1591
+ >
1592
+ <li>
1593
+ Use <code style={{ fontFamily: ffm }}>--sv-color-*</code> semantic tokens
1594
+ </li>
1595
+ <li>
1596
+ Use <code style={{ fontFamily: ffm }}>--sv-space-*</code>,{' '}
1597
+ <code style={{ fontFamily: ffm }}>--sv-radius-*</code> scale tokens
1598
+ </li>
1599
+ <li>
1600
+ Use <code style={{ fontFamily: ffm }}>--sv-font-family</code> and{' '}
1601
+ <code style={{ fontFamily: ffm }}>--sv-font-family-mono</code>
1602
+ </li>
1603
+ <li>
1604
+ Import components from <code style={{ fontFamily: ffm }}>@sovereignfs/ui</code>
1605
+ </li>
1606
+ <li>
1607
+ Use <code style={{ fontFamily: ffm }}>--sv-shadow-*</code> for elevation
1608
+ </li>
1609
+ </ul>
1610
+ </div>
1611
+ <div
1612
+ style={{
1613
+ background: 'var(--sv-color-error-surface)',
1614
+ border: '1px solid var(--sv-color-error-border)',
1615
+ borderRadius: 'var(--sv-radius-lg)',
1616
+ padding: 'var(--sv-space-4)',
1617
+ }}
1618
+ >
1619
+ <p
1620
+ style={{
1621
+ fontWeight: 600,
1622
+ color: 'var(--sv-color-error-text)',
1623
+ marginBottom: 'var(--sv-space-3)',
1624
+ fontSize: 'var(--sv-font-size-sm)',
1625
+ }}
1626
+ >
1627
+ ✗ Don't
1628
+ </p>
1629
+ <ul
1630
+ style={{
1631
+ margin: 0,
1632
+ padding: '0 0 0 var(--sv-space-4)',
1633
+ fontSize: 'var(--sv-font-size-sm)',
1634
+ color: 'var(--sv-color-text-primary)',
1635
+ lineHeight: 1.8,
1636
+ }}
1637
+ >
1638
+ <li>
1639
+ Hardcode hex values like <code style={{ fontFamily: ffm }}>#333</code>
1640
+ </li>
1641
+ <li>
1642
+ Use primitive tokens like <code style={{ fontFamily: ffm }}>--sv-grey-900</code>{' '}
1643
+ directly
1644
+ </li>
1645
+ <li>Use Tailwind classes or runtime CSS-in-JS</li>
1646
+ <li>
1647
+ Import from <code style={{ fontFamily: ffm }}>runtime/src</code> (SDK boundary)
1648
+ </li>
1649
+ <li>
1650
+ Override <code style={{ fontFamily: ffm }}>--sv-color-accent</code> from plugin
1651
+ CSS
1652
+ </li>
1653
+ </ul>
1654
+ </div>
1655
+ </div>
1656
+ </section>
1657
+ </div>
1658
+ </div>
1659
+ );
1660
+ }
1661
+
1662
+ // ---------------------------------------------------------------------------
1663
+ // Story
1664
+ // ---------------------------------------------------------------------------
1665
+
1666
+ const meta = {
1667
+ title: 'Overview',
1668
+ component: OverviewPage,
1669
+ parameters: {
1670
+ layout: 'fullscreen',
1671
+ docs: {
1672
+ description: {
1673
+ component:
1674
+ 'Complete reference for plugin developers — components, tokens, theming, and design rules in one page.',
1675
+ },
1676
+ },
1677
+ },
1678
+ } satisfies Meta<typeof OverviewPage>;
1679
+
1680
+ export default meta;
1681
+ type Story = StoryObj<typeof meta>;
1682
+
1683
+ export const DesignSystem: Story = {};