@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.
- package/LICENSE +661 -0
- package/NOTICE +24 -0
- package/dist/Avatar.module.css +38 -0
- package/dist/Badge.module.css +88 -0
- package/dist/Button.module.css +77 -0
- package/dist/Card.module.css +34 -0
- package/dist/Dialog.module.css +188 -0
- package/dist/Drawer.module.css +30 -0
- package/dist/EmptyState.module.css +27 -0
- package/dist/FormField.module.css +28 -0
- package/dist/Icon.module.css +22 -0
- package/dist/Input.module.css +37 -0
- package/dist/NavTabs.module.css +48 -0
- package/dist/PageHeader.module.css +32 -0
- package/dist/Popover.module.css +44 -0
- package/dist/SegmentedControl.module.css +53 -0
- package/dist/Select.module.css +69 -0
- package/dist/Spinner.module.css +39 -0
- package/dist/SystemBanner.module.css +67 -0
- package/dist/Tabs.module.css +49 -0
- package/dist/Toast.module.css +130 -0
- package/dist/Toggle.module.css +52 -0
- package/dist/Tooltip.module.css +51 -0
- package/dist/index.d.ts +327 -0
- package/dist/index.js +1370 -0
- package/dist/tokens/primitives.css +99 -0
- package/dist/tokens/semantic.css +111 -0
- package/dist/tokens.css +5 -0
- package/package.json +50 -0
- package/src/components/Avatar/Avatar.module.css +38 -0
- package/src/components/Avatar/Avatar.tsx +42 -0
- package/src/components/Badge/Badge.module.css +88 -0
- package/src/components/Badge/Badge.stories.tsx +118 -0
- package/src/components/Badge/Badge.tsx +59 -0
- package/src/components/Badge/__tests__/Badge.test.tsx +64 -0
- package/src/components/Button/Button.module.css +77 -0
- package/src/components/Button/Button.stories.tsx +76 -0
- package/src/components/Button/Button.tsx +36 -0
- package/src/components/Button/__tests__/Button.test.tsx +37 -0
- package/src/components/Card/Card.module.css +34 -0
- package/src/components/Card/Card.tsx +34 -0
- package/src/components/Dialog/Dialog.module.css +188 -0
- package/src/components/Dialog/Dialog.stories.tsx +97 -0
- package/src/components/Dialog/Dialog.tsx +137 -0
- package/src/components/Dialog/__tests__/Dialog.test.tsx +73 -0
- package/src/components/Drawer/Drawer.module.css +30 -0
- package/src/components/Drawer/Drawer.stories.tsx +88 -0
- package/src/components/Drawer/Drawer.tsx +103 -0
- package/src/components/Drawer/__tests__/Drawer.test.tsx +56 -0
- package/src/components/EmptyState/EmptyState.module.css +27 -0
- package/src/components/EmptyState/EmptyState.tsx +27 -0
- package/src/components/FormField/FormField.module.css +28 -0
- package/src/components/FormField/FormField.tsx +53 -0
- package/src/components/Icon/Icon.module.css +22 -0
- package/src/components/Icon/Icon.stories.tsx +94 -0
- package/src/components/Icon/Icon.tsx +43 -0
- package/src/components/Icon/__tests__/Icon.test.tsx +40 -0
- package/src/components/Icon/icons/activity.tsx +20 -0
- package/src/components/Icon/icons/alert-triangle.tsx +22 -0
- package/src/components/Icon/icons/bell.tsx +21 -0
- package/src/components/Icon/icons/check.tsx +20 -0
- package/src/components/Icon/icons/chevron-down.tsx +20 -0
- package/src/components/Icon/icons/chevron-left.tsx +20 -0
- package/src/components/Icon/icons/chevron-right.tsx +20 -0
- package/src/components/Icon/icons/chevron-up.tsx +20 -0
- package/src/components/Icon/icons/eye-off.tsx +23 -0
- package/src/components/Icon/icons/eye.tsx +21 -0
- package/src/components/Icon/icons/grid-2x2.tsx +22 -0
- package/src/components/Icon/icons/house.tsx +21 -0
- package/src/components/Icon/icons/index.ts +122 -0
- package/src/components/Icon/icons/info.tsx +22 -0
- package/src/components/Icon/icons/lock.tsx +21 -0
- package/src/components/Icon/icons/log-out.tsx +22 -0
- package/src/components/Icon/icons/mail.tsx +21 -0
- package/src/components/Icon/icons/package.tsx +23 -0
- package/src/components/Icon/icons/pencil.tsx +21 -0
- package/src/components/Icon/icons/plus.tsx +21 -0
- package/src/components/Icon/icons/rotate-ccw.tsx +21 -0
- package/src/components/Icon/icons/search.tsx +21 -0
- package/src/components/Icon/icons/settings.tsx +21 -0
- package/src/components/Icon/icons/shield.tsx +20 -0
- package/src/components/Icon/icons/sliders-horizontal.tsx +28 -0
- package/src/components/Icon/icons/terminal.tsx +19 -0
- package/src/components/Icon/icons/trash-2.tsx +24 -0
- package/src/components/Icon/icons/user.tsx +21 -0
- package/src/components/Icon/icons/x.tsx +21 -0
- package/src/components/Input/Input.module.css +37 -0
- package/src/components/Input/Input.stories.tsx +86 -0
- package/src/components/Input/Input.tsx +14 -0
- package/src/components/Input/__tests__/Input.test.tsx +22 -0
- package/src/components/NavTabs/NavTabs.module.css +48 -0
- package/src/components/NavTabs/NavTabs.tsx +33 -0
- package/src/components/PageHeader/PageHeader.module.css +32 -0
- package/src/components/PageHeader/PageHeader.tsx +21 -0
- package/src/components/Popover/Popover.module.css +44 -0
- package/src/components/Popover/Popover.stories.tsx +232 -0
- package/src/components/Popover/Popover.tsx +75 -0
- package/src/components/Popover/__tests__/Popover.test.tsx +98 -0
- package/src/components/SegmentedControl/SegmentedControl.module.css +53 -0
- package/src/components/SegmentedControl/SegmentedControl.stories.tsx +118 -0
- package/src/components/SegmentedControl/SegmentedControl.tsx +56 -0
- package/src/components/SegmentedControl/__tests__/SegmentedControl.test.tsx +63 -0
- package/src/components/Select/Select.module.css +69 -0
- package/src/components/Select/Select.stories.tsx +115 -0
- package/src/components/Select/Select.tsx +40 -0
- package/src/components/Select/__tests__/Select.test.tsx +59 -0
- package/src/components/Spinner/Spinner.module.css +39 -0
- package/src/components/Spinner/Spinner.tsx +23 -0
- package/src/components/SystemBanner/SystemBanner.module.css +67 -0
- package/src/components/SystemBanner/SystemBanner.stories.tsx +86 -0
- package/src/components/SystemBanner/SystemBanner.tsx +50 -0
- package/src/components/SystemBanner/__tests__/SystemBanner.test.tsx +50 -0
- package/src/components/Tabs/Tabs.module.css +49 -0
- package/src/components/Tabs/Tabs.stories.tsx +100 -0
- package/src/components/Tabs/Tabs.tsx +46 -0
- package/src/components/Tabs/__tests__/Tabs.test.tsx +45 -0
- package/src/components/Toast/Toast.module.css +130 -0
- package/src/components/Toast/Toast.stories.tsx +191 -0
- package/src/components/Toast/Toast.tsx +139 -0
- package/src/components/Toggle/Toggle.module.css +52 -0
- package/src/components/Toggle/Toggle.stories.tsx +95 -0
- package/src/components/Toggle/Toggle.tsx +39 -0
- package/src/components/Toggle/__tests__/Toggle.test.tsx +47 -0
- package/src/components/Tooltip/Tooltip.module.css +51 -0
- package/src/components/Tooltip/Tooltip.tsx +30 -0
- package/src/css-modules.d.ts +4 -0
- package/src/index.ts +45 -0
- package/src/stories/Avatar.stories.tsx +44 -0
- package/src/stories/Card.stories.tsx +35 -0
- package/src/stories/DesignSystemOverview.stories.tsx +1683 -0
- package/src/stories/EmptyState.stories.tsx +33 -0
- package/src/stories/FormField.stories.tsx +47 -0
- package/src/stories/MobilePatterns.stories.tsx +1285 -0
- package/src/stories/NavTabs.stories.tsx +35 -0
- package/src/stories/PageHeader.stories.tsx +31 -0
- package/src/stories/Spinner.stories.tsx +25 -0
- package/src/stories/TokenGallery.stories.tsx +355 -0
- package/src/stories/Tooltip.stories.tsx +59 -0
- package/src/tokens/primitives.css +99 -0
- package/src/tokens/semantic.css +111 -0
- 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 = {};
|