@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,1285 @@
1
+ import { useState } from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react-vite';
3
+ import { Button } from '../components/Button/Button';
4
+ import { Card } from '../components/Card/Card';
5
+ import { Dialog } from '../components/Dialog/Dialog';
6
+ import { Drawer } from '../components/Drawer/Drawer';
7
+ import { EmptyState } from '../components/EmptyState/EmptyState';
8
+ import { FormField } from '../components/FormField/FormField';
9
+ import { Icon } from '../components/Icon/Icon';
10
+ import { Input } from '../components/Input/Input';
11
+ import { NavTabs } from '../components/NavTabs/NavTabs';
12
+ import { PageHeader } from '../components/PageHeader/PageHeader';
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Shared primitives
16
+ // ---------------------------------------------------------------------------
17
+
18
+ const ff = 'var(--sv-font-family)';
19
+
20
+ function Heading({ level = 2, children }: { level?: 1 | 2 | 3; children: React.ReactNode }) {
21
+ const sizes: Record<number, string> = { 1: '1.75rem', 2: '1.125rem', 3: '0.9375rem' };
22
+ const weights: Record<number, number> = { 1: 700, 2: 600, 3: 600 };
23
+ const Tag = `h${level}` as 'h1' | 'h2' | 'h3';
24
+ return (
25
+ <Tag
26
+ style={{
27
+ fontFamily: ff,
28
+ fontSize: sizes[level],
29
+ fontWeight: weights[level],
30
+ color: 'var(--sv-color-text-primary)',
31
+ margin: 0,
32
+ lineHeight: 1.3,
33
+ }}
34
+ >
35
+ {children}
36
+ </Tag>
37
+ );
38
+ }
39
+
40
+ function Body({ children }: { children: React.ReactNode }) {
41
+ return (
42
+ <p
43
+ style={{
44
+ fontFamily: ff,
45
+ fontSize: 'var(--sv-font-size-sm)',
46
+ color: 'var(--sv-color-text-muted)',
47
+ margin: '0.5rem 0 0',
48
+ lineHeight: 1.6,
49
+ }}
50
+ >
51
+ {children}
52
+ </p>
53
+ );
54
+ }
55
+
56
+ function SectionHeader({ title, subtitle }: { title: string; subtitle?: string }) {
57
+ return (
58
+ <div
59
+ style={{
60
+ paddingBottom: 'var(--sv-space-3)',
61
+ borderBottom: '2px solid var(--sv-color-accent)',
62
+ marginBottom: 'var(--sv-space-5)',
63
+ }}
64
+ >
65
+ <Heading level={2}>{title}</Heading>
66
+ {subtitle && <Body>{subtitle}</Body>}
67
+ </div>
68
+ );
69
+ }
70
+
71
+ function Callout({
72
+ type = 'info',
73
+ children,
74
+ }: {
75
+ type?: 'info' | 'warn' | 'tip';
76
+ children: React.ReactNode;
77
+ }) {
78
+ const map = {
79
+ info: {
80
+ bg: 'var(--sv-color-info-surface)',
81
+ border: 'var(--sv-color-info-border)',
82
+ text: 'var(--sv-color-info-text)',
83
+ label: 'ℹ',
84
+ },
85
+ warn: {
86
+ bg: 'var(--sv-color-warning-surface)',
87
+ border: 'var(--sv-color-warning-border)',
88
+ text: 'var(--sv-color-warning-text)',
89
+ label: '⚠',
90
+ },
91
+ tip: {
92
+ bg: 'var(--sv-color-success-surface)',
93
+ border: 'var(--sv-color-success-border)',
94
+ text: 'var(--sv-color-success-text)',
95
+ label: '✓',
96
+ },
97
+ };
98
+ const c = map[type];
99
+ return (
100
+ <div
101
+ style={{
102
+ background: c.bg,
103
+ border: `1px solid ${c.border}`,
104
+ borderRadius: 'var(--sv-radius-md)',
105
+ padding: 'var(--sv-space-3) var(--sv-space-4)',
106
+ display: 'flex',
107
+ gap: 'var(--sv-space-2)',
108
+ alignItems: 'flex-start',
109
+ marginBottom: 'var(--sv-space-4)',
110
+ }}
111
+ >
112
+ <span style={{ color: c.text, fontWeight: 700, flexShrink: 0, fontSize: '0.875rem' }}>
113
+ {c.label}
114
+ </span>
115
+ <p
116
+ style={{
117
+ fontFamily: ff,
118
+ fontSize: 'var(--sv-font-size-sm)',
119
+ color: c.text,
120
+ margin: 0,
121
+ lineHeight: 1.55,
122
+ }}
123
+ >
124
+ {children}
125
+ </p>
126
+ </div>
127
+ );
128
+ }
129
+
130
+ function BreakpointRow({ px, name, behavior }: { px: number; name: string; behavior: string }) {
131
+ return (
132
+ <div
133
+ style={{
134
+ display: 'grid',
135
+ gridTemplateColumns: '80px 140px 1fr',
136
+ alignItems: 'center',
137
+ gap: 'var(--sv-space-3)',
138
+ padding: 'var(--sv-space-2) 0',
139
+ borderBottom: '1px solid var(--sv-color-border)',
140
+ fontFamily: ff,
141
+ fontSize: 'var(--sv-font-size-sm)',
142
+ }}
143
+ >
144
+ <code
145
+ style={{
146
+ fontFamily: 'var(--sv-font-family-mono)',
147
+ fontSize: '0.75rem',
148
+ background: 'var(--sv-color-surface-sunken)',
149
+ padding: '2px 6px',
150
+ borderRadius: 'var(--sv-radius-sm)',
151
+ color: 'var(--sv-color-text-primary)',
152
+ }}
153
+ >
154
+ {px}px
155
+ </code>
156
+ <span style={{ color: 'var(--sv-color-text-primary)', fontWeight: 600 }}>{name}</span>
157
+ <span style={{ color: 'var(--sv-color-text-muted)' }}>{behavior}</span>
158
+ </div>
159
+ );
160
+ }
161
+
162
+ function TouchTargetDemo({ label, size }: { label: string; size: number }) {
163
+ return (
164
+ <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--sv-space-3)' }}>
165
+ <div
166
+ style={{
167
+ width: size,
168
+ height: size,
169
+ background:
170
+ size >= 44 ? 'var(--sv-color-success-surface)' : 'var(--sv-color-warning-surface)',
171
+ border: `1px solid ${size >= 44 ? 'var(--sv-color-success-border)' : 'var(--sv-color-warning-border)'}`,
172
+ borderRadius: 'var(--sv-radius-sm)',
173
+ display: 'flex',
174
+ alignItems: 'center',
175
+ justifyContent: 'center',
176
+ flexShrink: 0,
177
+ }}
178
+ >
179
+ <span
180
+ style={{
181
+ fontFamily: 'var(--sv-font-family-mono)',
182
+ fontSize: '0.6875rem',
183
+ color: size >= 44 ? 'var(--sv-color-success-text)' : 'var(--sv-color-warning-text)',
184
+ fontWeight: 600,
185
+ }}
186
+ >
187
+ {size}px
188
+ </span>
189
+ </div>
190
+ <span
191
+ style={{
192
+ fontFamily: ff,
193
+ fontSize: 'var(--sv-font-size-sm)',
194
+ color: 'var(--sv-color-text-muted)',
195
+ }}
196
+ >
197
+ {label}
198
+ </span>
199
+ </div>
200
+ );
201
+ }
202
+
203
+ // ---------------------------------------------------------------------------
204
+ // Interactive demos
205
+ // ---------------------------------------------------------------------------
206
+
207
+ function DialogDemo() {
208
+ const [open, setOpen] = useState(false);
209
+ return (
210
+ <>
211
+ <Button size="sm" onClick={() => setOpen(true)}>
212
+ Open dialog
213
+ </Button>
214
+ <Dialog open={open} onClose={() => setOpen(false)} title="Mobile dialog" size="md">
215
+ <p
216
+ style={{
217
+ fontFamily: ff,
218
+ fontSize: 'var(--sv-font-size-sm)',
219
+ color: 'var(--sv-color-text-muted)',
220
+ margin: 0,
221
+ }}
222
+ >
223
+ On screens ≤ 768 px this panel slides up from the bottom as a full-width sheet. On larger
224
+ screens it appears centered. Resize the viewport to see the difference.
225
+ </p>
226
+ <div
227
+ style={{ marginTop: 'var(--sv-space-4)', display: 'flex', justifyContent: 'flex-end' }}
228
+ >
229
+ <Button size="sm" onClick={() => setOpen(false)}>
230
+ Close
231
+ </Button>
232
+ </div>
233
+ </Dialog>
234
+ </>
235
+ );
236
+ }
237
+
238
+ function DrawerDemo() {
239
+ const [open, setOpen] = useState(false);
240
+ return (
241
+ <>
242
+ <Button size="sm" variant="secondary" onClick={() => setOpen(true)}>
243
+ Open drawer
244
+ </Button>
245
+ <Drawer open={open} onClose={() => setOpen(false)} aria-label="Mobile navigation">
246
+ <p
247
+ style={{
248
+ fontFamily: ff,
249
+ fontSize: 'var(--sv-font-size-sm)',
250
+ color: 'var(--sv-color-text-muted)',
251
+ margin: 0,
252
+ }}
253
+ >
254
+ Drawer is always a bottom sheet — it never becomes a side panel. Use it for plugin grids,
255
+ navigation menus, or contextual actions on mobile. Respects{' '}
256
+ <code style={{ fontFamily: 'var(--sv-font-family-mono)', fontSize: '0.75rem' }}>
257
+ env(safe-area-inset-bottom)
258
+ </code>{' '}
259
+ for devices with a home indicator.
260
+ </p>
261
+ </Drawer>
262
+ </>
263
+ );
264
+ }
265
+
266
+ // ---------------------------------------------------------------------------
267
+ // Main story component
268
+ // ---------------------------------------------------------------------------
269
+
270
+ function MobilePatternsDoc() {
271
+ return (
272
+ <div
273
+ style={{
274
+ padding: 'var(--sv-space-6)',
275
+ background: 'var(--sv-color-surface)',
276
+ minHeight: '100vh',
277
+ fontFamily: ff,
278
+ maxWidth: 760,
279
+ }}
280
+ >
281
+ <div style={{ marginBottom: 'var(--sv-space-8)' }}>
282
+ <Heading level={1}>Mobile Patterns</Heading>
283
+ <Body>
284
+ Plugin developer reference for building layouts that work at 375 px (mobile), 768 px
285
+ (tablet), and 1280 px (desktop). Use the viewport toolbar above to preview each size. The
286
+ Sovereign shell handles the chrome (sidebar ↔ bottom nav, drawer) — plugins only need to
287
+ own their content area.
288
+ </Body>
289
+ </div>
290
+
291
+ {/* ── Breakpoints ── */}
292
+ <section style={{ marginBottom: 'var(--sv-space-10)' }}>
293
+ <SectionHeader
294
+ title="Breakpoints"
295
+ subtitle="Two breakpoints in use across the design system. No token variables — use the pixel values directly in your CSS."
296
+ />
297
+ <div>
298
+ <div
299
+ style={{
300
+ display: 'grid',
301
+ gridTemplateColumns: '80px 140px 1fr',
302
+ gap: 'var(--sv-space-3)',
303
+ padding: 'var(--sv-space-2) 0',
304
+ fontFamily: ff,
305
+ fontSize: 'var(--sv-font-size-xs)',
306
+ fontWeight: 700,
307
+ color: 'var(--sv-color-text-muted)',
308
+ letterSpacing: '0.05em',
309
+ textTransform: 'uppercase',
310
+ borderBottom: '2px solid var(--sv-color-border)',
311
+ }}
312
+ >
313
+ <span>Width</span>
314
+ <span>Name</span>
315
+ <span>What changes</span>
316
+ </div>
317
+ <BreakpointRow
318
+ px={640}
319
+ name="sm"
320
+ behavior="NavTabs switches to horizontal scroll (no visible scrollbar)."
321
+ />
322
+ <BreakpointRow
323
+ px={768}
324
+ name="md"
325
+ behavior="Shell sidebar becomes a bottom nav + drawer. Dialog becomes a full-screen bottom sheet."
326
+ />
327
+ </div>
328
+ <Callout type="info">
329
+ Use{' '}
330
+ <code style={{ fontFamily: 'var(--sv-font-family-mono)', fontSize: '0.8em' }}>
331
+ max-width: 768px
332
+ </code>{' '}
333
+ for the primary mobile breakpoint in your plugin CSS. Components below this width are
334
+ already adapted by the design system.
335
+ </Callout>
336
+ </section>
337
+
338
+ {/* ── Layout conventions ── */}
339
+ <section style={{ marginBottom: 'var(--sv-space-10)' }}>
340
+ <SectionHeader
341
+ title="Layout conventions"
342
+ subtitle="Patterns used across all built-in plugins."
343
+ />
344
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--sv-space-5)' }}>
345
+ <Card padding="md">
346
+ <Heading level={3}>Constrained content column</Heading>
347
+ <Body>
348
+ Wrap your page content in a centered column with a readable max-width. All built-in
349
+ plugins use this pattern. Content fills full width below 640 px automatically.
350
+ </Body>
351
+ <pre
352
+ style={{
353
+ fontFamily: 'var(--sv-font-family-mono)',
354
+ fontSize: '0.75rem',
355
+ background: 'var(--sv-color-surface-sunken)',
356
+ border: '1px solid var(--sv-color-border)',
357
+ borderRadius: 'var(--sv-radius-md)',
358
+ padding: 'var(--sv-space-3) var(--sv-space-4)',
359
+ marginTop: 'var(--sv-space-3)',
360
+ overflowX: 'auto',
361
+ color: 'var(--sv-color-text-primary)',
362
+ lineHeight: 1.6,
363
+ }}
364
+ >
365
+ {`.page {
366
+ max-width: 680px; /* or 720px for wider layouts */
367
+ margin: 0 auto;
368
+ padding: var(--sv-space-6) var(--sv-space-4);
369
+ }
370
+
371
+ @media (max-width: 768px) {
372
+ .page {
373
+ padding: var(--sv-space-4);
374
+ }
375
+ }`}
376
+ </pre>
377
+ </Card>
378
+
379
+ <Card padding="md">
380
+ <Heading level={3}>PageHeader on mobile</Heading>
381
+ <Body>
382
+ PageHeader stacks its action below the title on narrow viewports. Pass a compact
383
+ action (a single small Button) — avoid toolbars with multiple controls.
384
+ </Body>
385
+ <div style={{ marginTop: 'var(--sv-space-4)' }}>
386
+ <PageHeader
387
+ title="Settings"
388
+ description="Manage your plugin preferences."
389
+ action={<Button size="sm">Save</Button>}
390
+ />
391
+ </div>
392
+ </Card>
393
+
394
+ <Card padding="md">
395
+ <Heading level={3}>Form layout</Heading>
396
+ <Body>
397
+ Forms are always single-column on mobile. Never use multi-column grid layouts for form
398
+ fields — they're too narrow below 640 px.
399
+ </Body>
400
+ <div
401
+ style={{
402
+ marginTop: 'var(--sv-space-4)',
403
+ display: 'flex',
404
+ flexDirection: 'column',
405
+ gap: 'var(--sv-space-3)',
406
+ }}
407
+ >
408
+ <FormField label="Display name" htmlFor="mp-name">
409
+ <Input id="mp-name" placeholder="Your name" />
410
+ </FormField>
411
+ <FormField label="Email" htmlFor="mp-email">
412
+ <Input id="mp-email" type="email" placeholder="you@example.com" />
413
+ </FormField>
414
+ <Button type="submit">Save changes</Button>
415
+ </div>
416
+ </Card>
417
+ </div>
418
+ </section>
419
+
420
+ {/* ── Auto-adapting components ── */}
421
+ <section style={{ marginBottom: 'var(--sv-space-10)' }}>
422
+ <SectionHeader
423
+ title="Auto-adapting components"
424
+ subtitle="These components change behaviour at the 640 px or 768 px breakpoint automatically — no extra work needed."
425
+ />
426
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--sv-space-5)' }}>
427
+ <Card padding="md">
428
+ <Heading level={3}>NavTabs — horizontal scroll below 640 px</Heading>
429
+ <Body>
430
+ Tabs overflow horizontally with no visible scrollbar below 640 px. Use the{' '}
431
+ <em>Mobile</em> viewport to test overflow.
432
+ </Body>
433
+ <div style={{ marginTop: 'var(--sv-space-4)' }}>
434
+ <NavTabs
435
+ items={[
436
+ { label: 'Profile', href: '#', active: true },
437
+ { label: 'Security', href: '#' },
438
+ { label: 'Preferences', href: '#' },
439
+ { label: 'Notifications', href: '#' },
440
+ { label: 'Billing', href: '#' },
441
+ { label: 'Data', href: '#' },
442
+ ]}
443
+ />
444
+ </div>
445
+ </Card>
446
+
447
+ <Card padding="md">
448
+ <Heading level={3}>Dialog — full-screen sheet below 768 px</Heading>
449
+ <Body>
450
+ Dialog slides up as a bottom sheet on mobile — identical to native OS sheets. No size
451
+ prop differences needed; all sizes become full-screen below 768 px.
452
+ </Body>
453
+ <div style={{ marginTop: 'var(--sv-space-4)' }}>
454
+ <DialogDemo />
455
+ </div>
456
+ </Card>
457
+
458
+ <Card padding="md">
459
+ <Heading level={3}>Drawer — always a bottom sheet</Heading>
460
+ <Body>
461
+ Drawer is the component the shell uses for the mobile plugin navigation grid. Your
462
+ plugin can use it for contextual navigation menus or action sheets. It always occupies
463
+ the bottom of the screen regardless of viewport width.
464
+ </Body>
465
+ <div style={{ marginTop: 'var(--sv-space-4)' }}>
466
+ <DrawerDemo />
467
+ </div>
468
+ </Card>
469
+
470
+ <Card padding="md">
471
+ <Heading level={3}>EmptyState — centered, narrow</Heading>
472
+ <Body>
473
+ EmptyState constrains its content to{' '}
474
+ <code style={{ fontFamily: 'var(--sv-font-family-mono)', fontSize: '0.8em' }}>
475
+ 36ch
476
+ </code>{' '}
477
+ and works well at all widths. No extra handling needed.
478
+ </Body>
479
+ <div style={{ marginTop: 'var(--sv-space-4)' }}>
480
+ <EmptyState
481
+ icon="search"
482
+ heading="Nothing here yet"
483
+ description="Add your first item to get started."
484
+ action={<Button size="sm">Add item</Button>}
485
+ />
486
+ </div>
487
+ </Card>
488
+ </div>
489
+ </section>
490
+
491
+ {/* ── Shell chrome on mobile ── */}
492
+ <section style={{ marginBottom: 'var(--sv-space-10)' }}>
493
+ <SectionHeader
494
+ title="Shell chrome on mobile"
495
+ subtitle="What the runtime shell renders on your behalf at ≤ 768 px. Plugins only own the content area — everything else is provided."
496
+ />
497
+
498
+ {/* Anatomy diagram */}
499
+ <div
500
+ style={{
501
+ border: '1px solid var(--sv-color-border)',
502
+ borderRadius: 'var(--sv-radius-lg)',
503
+ overflow: 'hidden',
504
+ marginBottom: 'var(--sv-space-5)',
505
+ fontFamily: ff,
506
+ fontSize: 'var(--sv-font-size-sm)',
507
+ }}
508
+ >
509
+ {/* Header */}
510
+ <div
511
+ style={{
512
+ background: 'var(--sv-color-surface)',
513
+ borderBottom: '1px solid var(--sv-color-border)',
514
+ padding: 'var(--sv-space-3) var(--sv-space-4)',
515
+ display: 'flex',
516
+ alignItems: 'center',
517
+ justifyContent: 'space-between',
518
+ }}
519
+ >
520
+ <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--sv-space-2)' }}>
521
+ <div
522
+ style={{
523
+ width: 28,
524
+ height: 28,
525
+ borderRadius: 'var(--sv-radius-sm)',
526
+ background: 'var(--sv-color-accent)',
527
+ display: 'flex',
528
+ alignItems: 'center',
529
+ justifyContent: 'center',
530
+ color: 'var(--sv-color-text-on-accent)',
531
+ fontWeight: 700,
532
+ fontSize: 13,
533
+ }}
534
+ >
535
+ S
536
+ </div>
537
+ <span style={{ fontWeight: 600, color: 'var(--sv-color-text-primary)' }}>
538
+ My Instance
539
+ </span>
540
+ </div>
541
+ <div style={{ display: 'flex', gap: 'var(--sv-space-2)', alignItems: 'center' }}>
542
+ <div
543
+ style={{
544
+ width: 28,
545
+ height: 28,
546
+ borderRadius: 'var(--sv-radius-full)',
547
+ background: 'var(--sv-color-surface-sunken)',
548
+ border: '1px solid var(--sv-color-border)',
549
+ }}
550
+ />
551
+ <div
552
+ style={{
553
+ width: 28,
554
+ height: 28,
555
+ borderRadius: 'var(--sv-radius-full)',
556
+ background: 'var(--sv-color-accent)',
557
+ display: 'flex',
558
+ alignItems: 'center',
559
+ justifyContent: 'center',
560
+ color: 'var(--sv-color-text-on-accent)',
561
+ fontSize: 11,
562
+ fontWeight: 700,
563
+ }}
564
+ >
565
+ JD
566
+ </div>
567
+ </div>
568
+ </div>
569
+ <div
570
+ style={{
571
+ background: 'var(--sv-color-info-surface)',
572
+ padding: 'var(--sv-space-2) var(--sv-space-4)',
573
+ fontSize: 'var(--sv-font-size-xs)',
574
+ color: 'var(--sv-color-info-text)',
575
+ fontFamily: 'var(--sv-font-family-mono)',
576
+ }}
577
+ >
578
+ ↑ Mobile header — 60 px, sticky · Logo · Instance name · Notification bell · Account
579
+ menu
580
+ </div>
581
+
582
+ {/* Content area */}
583
+ <div
584
+ style={{
585
+ background: 'var(--sv-color-surface-sunken)',
586
+ padding: 'var(--sv-space-6) var(--sv-space-4)',
587
+ textAlign: 'center',
588
+ color: 'var(--sv-color-text-muted)',
589
+ minHeight: 100,
590
+ display: 'flex',
591
+ alignItems: 'center',
592
+ justifyContent: 'center',
593
+ flexDirection: 'column',
594
+ gap: 'var(--sv-space-1)',
595
+ }}
596
+ >
597
+ <span style={{ fontWeight: 600, color: 'var(--sv-color-text-primary)' }}>
598
+ Plugin content area
599
+ </span>
600
+ <span style={{ fontSize: 'var(--sv-font-size-xs)' }}>
601
+ Shell sets padding-bottom automatically — no overlap with footer
602
+ </span>
603
+ </div>
604
+ <div
605
+ style={{
606
+ background: 'var(--sv-color-success-surface)',
607
+ padding: 'var(--sv-space-2) var(--sv-space-4)',
608
+ fontSize: 'var(--sv-font-size-xs)',
609
+ color: 'var(--sv-color-success-text)',
610
+ fontFamily: 'var(--sv-font-family-mono)',
611
+ }}
612
+ >
613
+ ↓ Your plugin renders here — full width, auto scroll
614
+ </div>
615
+
616
+ {/* Footer */}
617
+ <div
618
+ style={{
619
+ background: 'var(--sv-color-surface)',
620
+ borderTop: '1px solid var(--sv-color-border)',
621
+ padding: 'var(--sv-space-3) var(--sv-space-6)',
622
+ display: 'flex',
623
+ alignItems: 'center',
624
+ justifyContent: 'space-around',
625
+ }}
626
+ >
627
+ {(
628
+ [
629
+ { iconName: 'house', label: 'Home', note: '→ /' },
630
+ { iconName: 'grid-2x2', label: 'Apps', note: 'opens Drawer' },
631
+ { iconName: 'search', label: 'Search', note: 'opens overlay' },
632
+ ] as { iconName: Parameters<typeof Icon>[0]['name']; label: string; note: string }[]
633
+ ).map(({ iconName, label, note }) => (
634
+ <div
635
+ key={label}
636
+ style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 2 }}
637
+ >
638
+ <Icon name={iconName} size="md" aria-hidden />
639
+ <span
640
+ style={{
641
+ fontSize: 'var(--sv-font-size-xs)',
642
+ color: 'var(--sv-color-text-primary)',
643
+ fontWeight: 600,
644
+ }}
645
+ >
646
+ {label}
647
+ </span>
648
+ <span
649
+ style={{
650
+ fontSize: '0.625rem',
651
+ color: 'var(--sv-color-text-muted)',
652
+ fontFamily: 'var(--sv-font-family-mono)',
653
+ }}
654
+ >
655
+ {note}
656
+ </span>
657
+ </div>
658
+ ))}
659
+ </div>
660
+ <div
661
+ style={{
662
+ background: 'var(--sv-color-warning-surface)',
663
+ padding: 'var(--sv-space-2) var(--sv-space-4)',
664
+ fontSize: 'var(--sv-font-size-xs)',
665
+ color: 'var(--sv-color-warning-text)',
666
+ fontFamily: 'var(--sv-font-family-mono)',
667
+ }}
668
+ >
669
+ ↑ Mobile footer (MobileNav) — 60 px · Home · Apps · Search
670
+ </div>
671
+ </div>
672
+
673
+ {/* Header details */}
674
+ <Card padding="md" style={{ marginBottom: 'var(--sv-space-4)' }}>
675
+ <Heading level={3}>Mobile header</Heading>
676
+ <Body>
677
+ The header is 60 px tall and sticks to the top during scroll. It contains the instance
678
+ logo + name (left), notification bell, and the account/console menu (right). Dialogs
679
+ automatically slide up from below it — the shell sets{' '}
680
+ <code style={{ fontFamily: 'var(--sv-font-family-mono)', fontSize: '0.8em' }}>
681
+ --sv-dialog-inset-top
682
+ </code>{' '}
683
+ so your Dialog calls need no special offset prop.
684
+ </Body>
685
+ <pre
686
+ style={{
687
+ fontFamily: 'var(--sv-font-family-mono)',
688
+ fontSize: '0.75rem',
689
+ background: 'var(--sv-color-surface-sunken)',
690
+ border: '1px solid var(--sv-color-border)',
691
+ borderRadius: 'var(--sv-radius-md)',
692
+ padding: 'var(--sv-space-3) var(--sv-space-4)',
693
+ marginTop: 'var(--sv-space-3)',
694
+ overflowX: 'auto',
695
+ color: 'var(--sv-color-text-primary)',
696
+ lineHeight: 1.6,
697
+ }}
698
+ >
699
+ {`/* Shell-exposed CSS variables (read-only for plugins) */
700
+ --sv-shell-header-height: 60px /* height of the sticky mobile header */
701
+ --sv-shell-footer-height: 60px /* height of the sticky mobile footer */
702
+ --sv-dialog-inset-top: 60px /* auto-set — Dialog slides below header */`}
703
+ </pre>
704
+ </Card>
705
+
706
+ {/* Footer details */}
707
+ <Card padding="md" style={{ marginBottom: 'var(--sv-space-4)' }}>
708
+ <Heading level={3}>Mobile footer (MobileNav)</Heading>
709
+ <Body>
710
+ A three-button bar fixed to the bottom. Plugins don't render here — the shell adds it
711
+ automatically. The plugin content area already has{' '}
712
+ <code style={{ fontFamily: 'var(--sv-font-family-mono)', fontSize: '0.8em' }}>
713
+ padding-bottom
714
+ </code>{' '}
715
+ so content never scrolls behind the footer.
716
+ </Body>
717
+ <div
718
+ style={{
719
+ marginTop: 'var(--sv-space-3)',
720
+ display: 'flex',
721
+ flexDirection: 'column',
722
+ gap: 'var(--sv-space-2)',
723
+ }}
724
+ >
725
+ {(
726
+ [
727
+ {
728
+ slot: 'Home',
729
+ iconName: 'house',
730
+ desc: 'Returns to "/" (launcher). Active when pathname is "/" or "/launcher".',
731
+ },
732
+ {
733
+ slot: 'Apps',
734
+ iconName: 'grid-2x2',
735
+ desc: 'Opens the Launcher Drawer showing all installed plugins as a grid.',
736
+ },
737
+ {
738
+ slot: 'Search',
739
+ iconName: 'search',
740
+ desc: 'Opens the Search overlay — a full-screen input with a live-filtered plugin list. Closes on back button, Esc, or after navigating to a result.',
741
+ },
742
+ ] as {
743
+ slot: string;
744
+ iconName: Parameters<typeof Icon>[0]['name'];
745
+ desc: string;
746
+ }[]
747
+ ).map(({ slot, iconName, desc }) => (
748
+ <div
749
+ key={slot}
750
+ style={{
751
+ display: 'flex',
752
+ gap: 'var(--sv-space-3)',
753
+ alignItems: 'flex-start',
754
+ padding: 'var(--sv-space-2) 0',
755
+ borderBottom: '1px solid var(--sv-color-border)',
756
+ }}
757
+ >
758
+ <span style={{ flexShrink: 0, display: 'flex' }}>
759
+ <Icon name={iconName} size="md" aria-hidden />
760
+ </span>
761
+ <div>
762
+ <span
763
+ style={{
764
+ fontFamily: ff,
765
+ fontWeight: 600,
766
+ fontSize: 'var(--sv-font-size-sm)',
767
+ color: 'var(--sv-color-text-primary)',
768
+ }}
769
+ >
770
+ {slot}
771
+ </span>
772
+ <span
773
+ style={{
774
+ fontFamily: ff,
775
+ fontSize: 'var(--sv-font-size-sm)',
776
+ color: 'var(--sv-color-text-muted)',
777
+ display: 'block',
778
+ }}
779
+ >
780
+ {desc}
781
+ </span>
782
+ </div>
783
+ </div>
784
+ ))}
785
+ </div>
786
+ </Card>
787
+
788
+ {/* Launcher Drawer */}
789
+ <Card padding="md">
790
+ <Heading level={3}>Launcher Drawer</Heading>
791
+ <Body>
792
+ Opened by the Apps button in the mobile footer. Renders all installed plugins as a
793
+ scrollable 2-column icon grid using the{' '}
794
+ <code style={{ fontFamily: 'var(--sv-font-family-mono)', fontSize: '0.8em' }}>
795
+ Drawer
796
+ </code>{' '}
797
+ component from{' '}
798
+ <code style={{ fontFamily: 'var(--sv-font-family-mono)', fontSize: '0.8em' }}>
799
+ @sovereignfs/ui
800
+ </code>
801
+ . It includes a drag handle, "Apps" title, and close button. Tapping any plugin tile
802
+ navigates to its route and closes the drawer. Respects{' '}
803
+ <code style={{ fontFamily: 'var(--sv-font-family-mono)', fontSize: '0.8em' }}>
804
+ env(safe-area-inset-bottom)
805
+ </code>{' '}
806
+ via the Drawer component.
807
+ </Body>
808
+ <div
809
+ style={{
810
+ marginTop: 'var(--sv-space-4)',
811
+ background: 'var(--sv-color-surface)',
812
+ border: '1px solid var(--sv-color-border)',
813
+ borderRadius: 'var(--sv-radius-lg)',
814
+ overflow: 'hidden',
815
+ }}
816
+ >
817
+ <div
818
+ style={{
819
+ display: 'flex',
820
+ justifyContent: 'center',
821
+ padding: 'var(--sv-space-2) 0 var(--sv-space-1)',
822
+ }}
823
+ >
824
+ <div
825
+ style={{
826
+ width: 36,
827
+ height: 4,
828
+ background: 'var(--sv-color-border-strong)',
829
+ borderRadius: 'var(--sv-radius-full)',
830
+ }}
831
+ />
832
+ </div>
833
+ <div
834
+ style={{
835
+ display: 'flex',
836
+ alignItems: 'center',
837
+ justifyContent: 'space-between',
838
+ padding: 'var(--sv-space-3) var(--sv-space-4)',
839
+ borderBottom: '1px solid var(--sv-color-border)',
840
+ }}
841
+ >
842
+ <span
843
+ style={{
844
+ fontFamily: ff,
845
+ fontWeight: 600,
846
+ fontSize: 'var(--sv-font-size-md)',
847
+ color: 'var(--sv-color-text-primary)',
848
+ }}
849
+ >
850
+ Apps
851
+ </span>
852
+ <div
853
+ style={{
854
+ width: 28,
855
+ height: 28,
856
+ borderRadius: 'var(--sv-radius-sm)',
857
+ background: 'var(--sv-color-surface-sunken)',
858
+ border: '1px solid var(--sv-color-border)',
859
+ display: 'flex',
860
+ alignItems: 'center',
861
+ justifyContent: 'center',
862
+ fontSize: 13,
863
+ color: 'var(--sv-color-text-muted)',
864
+ }}
865
+ >
866
+
867
+ </div>
868
+ </div>
869
+ <div
870
+ style={{
871
+ display: 'grid',
872
+ gridTemplateColumns: 'repeat(4, 1fr)',
873
+ gap: 'var(--sv-space-2)',
874
+ padding: 'var(--sv-space-4)',
875
+ }}
876
+ >
877
+ {['Console', 'Account', 'Tasks', 'Files'].map((name) => (
878
+ <div
879
+ key={name}
880
+ style={{
881
+ display: 'flex',
882
+ flexDirection: 'column',
883
+ alignItems: 'center',
884
+ gap: 'var(--sv-space-2)',
885
+ }}
886
+ >
887
+ <div
888
+ style={{
889
+ width: 52,
890
+ height: 52,
891
+ borderRadius: 'var(--sv-radius-lg)',
892
+ background: 'var(--sv-color-surface-sunken)',
893
+ border: '1px solid var(--sv-color-border)',
894
+ display: 'flex',
895
+ alignItems: 'center',
896
+ justifyContent: 'center',
897
+ fontWeight: 700,
898
+ fontSize: 16,
899
+ color: 'var(--sv-color-text-muted)',
900
+ }}
901
+ >
902
+ {name.slice(0, 2).toUpperCase()}
903
+ </div>
904
+ <span
905
+ style={{
906
+ fontFamily: ff,
907
+ fontSize: 'var(--sv-font-size-xs)',
908
+ color: 'var(--sv-color-text-muted)',
909
+ textAlign: 'center',
910
+ }}
911
+ >
912
+ {name}
913
+ </span>
914
+ </div>
915
+ ))}
916
+ </div>
917
+ </div>
918
+ <Callout type="info">
919
+ Plugin developers cannot customise the Launcher Drawer. Use the Drawer component from{' '}
920
+ <code style={{ fontFamily: 'var(--sv-font-family-mono)', fontSize: '0.8em' }}>
921
+ @sovereignfs/ui
922
+ </code>{' '}
923
+ if you need a similar bottom-sheet pattern inside your own plugin.
924
+ </Callout>
925
+ </Card>
926
+
927
+ {/* Search overlay */}
928
+ <Card padding="md" style={{ marginTop: 'var(--sv-space-4)' }}>
929
+ <Heading level={3}>Search overlay</Heading>
930
+ <Body>
931
+ Opened by the Search button in the mobile footer. Renders a full-screen overlay (z-index
932
+ 150, above the footer and Drawer scrim) with an auto-focused search input and a
933
+ live-filtered list of installed plugins. Tapping a result navigates to its route and
934
+ closes the overlay. Scroll lock is applied to{' '}
935
+ <code style={{ fontFamily: 'var(--sv-font-family-mono)', fontSize: '0.8em' }}>
936
+ document.body
937
+ </code>{' '}
938
+ while open.
939
+ </Body>
940
+ {/* Anatomy mock */}
941
+ <div
942
+ style={{
943
+ marginTop: 'var(--sv-space-4)',
944
+ background: 'var(--sv-color-surface)',
945
+ border: '1px solid var(--sv-color-border)',
946
+ borderRadius: 'var(--sv-radius-lg)',
947
+ overflow: 'hidden',
948
+ fontFamily: ff,
949
+ }}
950
+ >
951
+ {/* Header row */}
952
+ <div
953
+ style={{
954
+ display: 'flex',
955
+ alignItems: 'center',
956
+ gap: 'var(--sv-space-2)',
957
+ padding: 'var(--sv-space-3) var(--sv-space-4)',
958
+ borderBottom: '1px solid var(--sv-color-border)',
959
+ }}
960
+ >
961
+ <span
962
+ style={{
963
+ display: 'flex',
964
+ alignItems: 'center',
965
+ justifyContent: 'center',
966
+ width: 36,
967
+ height: 36,
968
+ borderRadius: 'var(--sv-radius-md)',
969
+ background: 'var(--sv-color-surface-sunken)',
970
+ flexShrink: 0,
971
+ }}
972
+ >
973
+ <Icon name="chevron-left" size="md" aria-hidden />
974
+ </span>
975
+ <span
976
+ style={{
977
+ fontWeight: 600,
978
+ fontSize: 'var(--sv-font-size-md)',
979
+ color: 'var(--sv-color-text-primary)',
980
+ }}
981
+ >
982
+ Search
983
+ </span>
984
+ </div>
985
+ {/* Input row */}
986
+ <div style={{ padding: 'var(--sv-space-3) var(--sv-space-4)' }}>
987
+ <div
988
+ style={{
989
+ display: 'flex',
990
+ alignItems: 'center',
991
+ gap: 'var(--sv-space-2)',
992
+ padding: 'var(--sv-space-2) var(--sv-space-3)',
993
+ borderRadius: 'var(--sv-radius-md)',
994
+ border: '1px solid var(--sv-color-accent)',
995
+ background: 'var(--sv-color-surface-sunken)',
996
+ }}
997
+ >
998
+ <Icon name="search" size="sm" aria-hidden />
999
+ <span
1000
+ style={{
1001
+ color: 'var(--sv-color-text-muted)',
1002
+ fontSize: 'var(--sv-font-size-sm)',
1003
+ }}
1004
+ >
1005
+ Search apps…
1006
+ </span>
1007
+ </div>
1008
+ </div>
1009
+ {/* Result rows */}
1010
+ {['Console', 'Account', 'Launcher'].map((name) => (
1011
+ <div
1012
+ key={name}
1013
+ style={{
1014
+ display: 'flex',
1015
+ alignItems: 'center',
1016
+ gap: 'var(--sv-space-3)',
1017
+ padding: 'var(--sv-space-2) var(--sv-space-4)',
1018
+ borderBottom: '1px solid var(--sv-color-border)',
1019
+ minHeight: 44,
1020
+ }}
1021
+ >
1022
+ <div
1023
+ style={{
1024
+ width: 40,
1025
+ height: 40,
1026
+ borderRadius: 'var(--sv-radius-lg)',
1027
+ background: 'var(--sv-color-surface-sunken)',
1028
+ display: 'flex',
1029
+ alignItems: 'center',
1030
+ justifyContent: 'center',
1031
+ fontWeight: 700,
1032
+ fontSize: 12,
1033
+ color: 'var(--sv-color-text-muted)',
1034
+ flexShrink: 0,
1035
+ }}
1036
+ >
1037
+ {name.slice(0, 2).toUpperCase()}
1038
+ </div>
1039
+ <span
1040
+ style={{
1041
+ flex: 1,
1042
+ fontSize: 'var(--sv-font-size-sm)',
1043
+ fontWeight: 500,
1044
+ color: 'var(--sv-color-text-primary)',
1045
+ }}
1046
+ >
1047
+ {name}
1048
+ </span>
1049
+ <Icon name="chevron-right" size="sm" aria-hidden />
1050
+ </div>
1051
+ ))}
1052
+ </div>
1053
+ <Callout type="info">
1054
+ The search overlay is shell chrome — plugin developers do not render it. The result set
1055
+ is the platform's installed plugin list. If you need in-plugin search, build your own
1056
+ input using{' '}
1057
+ <code style={{ fontFamily: 'var(--sv-font-family-mono)', fontSize: '0.8em' }}>
1058
+ Input
1059
+ </code>{' '}
1060
+ from{' '}
1061
+ <code style={{ fontFamily: 'var(--sv-font-family-mono)', fontSize: '0.8em' }}>
1062
+ @sovereignfs/ui
1063
+ </code>
1064
+ .
1065
+ </Callout>
1066
+ </Card>
1067
+ </section>
1068
+
1069
+ {/* ── Touch targets ── */}
1070
+ <section style={{ marginBottom: 'var(--sv-space-10)' }}>
1071
+ <SectionHeader
1072
+ title="Touch targets"
1073
+ subtitle="WCAG 2.5.5 (AAA) recommends 44 × 44 px minimum for interactive elements. All design system buttons meet this by default."
1074
+ />
1075
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--sv-space-3)' }}>
1076
+ <TouchTargetDemo
1077
+ label="Button (sm) — 32 px visual, but padded to 44 px tap area via inline-flex"
1078
+ size={44}
1079
+ />
1080
+ <TouchTargetDemo label="Button (md) — 44 px height" size={44} />
1081
+ <TouchTargetDemo label="Button (lg) — 52 px height" size={52} />
1082
+ <TouchTargetDemo
1083
+ label="Avoid — 28 px tap area is too small for confident tapping"
1084
+ size={28}
1085
+ />
1086
+ </div>
1087
+ <Callout type="tip">
1088
+ When building custom interactive elements (icon-only buttons, list rows, toggle rows), add
1089
+ explicit{' '}
1090
+ <code style={{ fontFamily: 'var(--sv-font-family-mono)', fontSize: '0.8em' }}>
1091
+ min-height: 44px
1092
+ </code>{' '}
1093
+ and enough horizontal padding to make the tap area comfortable.
1094
+ </Callout>
1095
+ </section>
1096
+
1097
+ {/* ── Safe area insets ── */}
1098
+ <section style={{ marginBottom: 'var(--sv-space-10)' }}>
1099
+ <SectionHeader
1100
+ title="Safe area insets"
1101
+ subtitle="iOS devices with a home indicator and Android devices with gesture navigation require bottom clearance. The shell handles this for its own chrome; plugins only need to account for it on fixed-bottom elements."
1102
+ />
1103
+ <Callout type="warn">
1104
+ If your plugin has a sticky/fixed footer bar (e.g., a compose toolbar), add{' '}
1105
+ <code style={{ fontFamily: 'var(--sv-font-family-mono)', fontSize: '0.8em' }}>
1106
+ padding-bottom: env(safe-area-inset-bottom)
1107
+ </code>{' '}
1108
+ to it. The Drawer already does this.
1109
+ </Callout>
1110
+ <pre
1111
+ style={{
1112
+ fontFamily: 'var(--sv-font-family-mono)',
1113
+ fontSize: '0.75rem',
1114
+ background: 'var(--sv-color-surface-sunken)',
1115
+ border: '1px solid var(--sv-color-border)',
1116
+ borderRadius: 'var(--sv-radius-md)',
1117
+ padding: 'var(--sv-space-3) var(--sv-space-4)',
1118
+ overflowX: 'auto',
1119
+ color: 'var(--sv-color-text-primary)',
1120
+ lineHeight: 1.6,
1121
+ }}
1122
+ >
1123
+ {`.stickyFooter {
1124
+ position: sticky;
1125
+ bottom: 0;
1126
+ /* clears the home indicator bar on iOS/Android */
1127
+ padding-bottom: max(var(--sv-space-3), env(safe-area-inset-bottom));
1128
+ background: var(--sv-color-surface);
1129
+ border-top: 1px solid var(--sv-color-border);
1130
+ }`}
1131
+ </pre>
1132
+ </section>
1133
+
1134
+ {/* ── Typography ── */}
1135
+ <section style={{ marginBottom: 'var(--sv-space-10)' }}>
1136
+ <SectionHeader
1137
+ title="Typography on mobile"
1138
+ subtitle="All --sv-font-size-* tokens are fixed values — they do not scale with viewport. Use them as-is; they were calibrated for legibility at 375 px."
1139
+ />
1140
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--sv-space-3)' }}>
1141
+ {[
1142
+ { token: '--sv-font-size-xs', hint: 'Labels, captions. Minimum for body text.' },
1143
+ { token: '--sv-font-size-sm', hint: 'Default body text. Use for most content.' },
1144
+ { token: '--sv-font-size-md', hint: 'Slightly larger body. Section intros.' },
1145
+ { token: '--sv-font-size-lg', hint: 'Sub-headings, card titles.' },
1146
+ { token: '--sv-font-size-xl', hint: 'Page headings (h2 equivalent).' },
1147
+ { token: '--sv-font-size-2xl', hint: 'Hero headings. Use sparingly on mobile.' },
1148
+ ].map(({ token, hint }) => (
1149
+ <div
1150
+ key={token}
1151
+ style={{
1152
+ display: 'flex',
1153
+ alignItems: 'baseline',
1154
+ gap: 'var(--sv-space-4)',
1155
+ borderBottom: '1px solid var(--sv-color-border)',
1156
+ padding: 'var(--sv-space-2) 0',
1157
+ }}
1158
+ >
1159
+ <span
1160
+ style={{
1161
+ fontFamily: ff,
1162
+ fontSize: `var(${token})`,
1163
+ color: 'var(--sv-color-text-primary)',
1164
+ lineHeight: 1.2,
1165
+ flexShrink: 0,
1166
+ }}
1167
+ >
1168
+ Aa
1169
+ </span>
1170
+ <code
1171
+ style={{
1172
+ fontFamily: 'var(--sv-font-family-mono)',
1173
+ fontSize: '0.6875rem',
1174
+ color: 'var(--sv-color-text-muted)',
1175
+ minWidth: 200,
1176
+ }}
1177
+ >
1178
+ {token}
1179
+ </code>
1180
+ <span
1181
+ style={{
1182
+ fontFamily: ff,
1183
+ fontSize: 'var(--sv-font-size-xs)',
1184
+ color: 'var(--sv-color-text-subtle)',
1185
+ }}
1186
+ >
1187
+ {hint}
1188
+ </span>
1189
+ </div>
1190
+ ))}
1191
+ </div>
1192
+ <Callout type="info">
1193
+ Line lengths should stay between 45–75 characters per line for comfortable reading. The{' '}
1194
+ <code style={{ fontFamily: 'var(--sv-font-family-mono)', fontSize: '0.8em' }}>
1195
+ max-width: 680px
1196
+ </code>{' '}
1197
+ container enforces this on wide viewports; on mobile the full width is fine because the
1198
+ viewport itself constrains line length.
1199
+ </Callout>
1200
+ </section>
1201
+
1202
+ {/* ── Checklist ── */}
1203
+ <section style={{ marginBottom: 'var(--sv-space-10)' }}>
1204
+ <SectionHeader title="Mobile readiness checklist" />
1205
+ {[
1206
+ {
1207
+ done: true,
1208
+ text: 'Content column has a max-width (680–720 px) and horizontal padding (--sv-space-4).',
1209
+ },
1210
+ { done: true, text: 'Forms are single-column — never two-column grids below 640 px.' },
1211
+ { done: true, text: 'All tap targets are ≥ 44 × 44 px.' },
1212
+ { done: true, text: 'NavTabs used for page-level navigation (not a custom tab row).' },
1213
+ { done: true, text: 'Dialogs use Dialog component (auto full-screen on mobile).' },
1214
+ { done: true, text: 'Fixed/sticky footers have env(safe-area-inset-bottom) padding.' },
1215
+ { done: true, text: 'No hardcoded pixel widths on content — use max-width instead.' },
1216
+ { done: true, text: 'Dark mode tested — use semantic --sv-color-* tokens only.' },
1217
+ ].map(({ done, text }, i) => (
1218
+ <div
1219
+ key={i}
1220
+ style={{
1221
+ display: 'flex',
1222
+ alignItems: 'flex-start',
1223
+ gap: 'var(--sv-space-3)',
1224
+ padding: 'var(--sv-space-2) 0',
1225
+ borderBottom: '1px solid var(--sv-color-border)',
1226
+ }}
1227
+ >
1228
+ <span
1229
+ style={{
1230
+ color: done ? 'var(--sv-color-success-text)' : 'var(--sv-color-text-subtle)',
1231
+ fontWeight: 700,
1232
+ flexShrink: 0,
1233
+ fontSize: 'var(--sv-font-size-sm)',
1234
+ }}
1235
+ >
1236
+ {done ? '✓' : '○'}
1237
+ </span>
1238
+ <span
1239
+ style={{
1240
+ fontFamily: ff,
1241
+ fontSize: 'var(--sv-font-size-sm)',
1242
+ color: 'var(--sv-color-text-muted)',
1243
+ lineHeight: 1.5,
1244
+ }}
1245
+ >
1246
+ {text}
1247
+ </span>
1248
+ </div>
1249
+ ))}
1250
+ </section>
1251
+ </div>
1252
+ );
1253
+ }
1254
+
1255
+ // ---------------------------------------------------------------------------
1256
+
1257
+ const meta = {
1258
+ title: 'Overview/Mobile Patterns',
1259
+ component: MobilePatternsDoc,
1260
+ parameters: {
1261
+ layout: 'fullscreen',
1262
+ viewport: { defaultViewport: 'mobile' },
1263
+ docs: {
1264
+ description: {
1265
+ component:
1266
+ 'Plugin developer reference for mobile-responsive layouts. Switch to the Mobile viewport (375 px) in the toolbar to preview at target size.',
1267
+ },
1268
+ },
1269
+ },
1270
+ } satisfies Meta<typeof MobilePatternsDoc>;
1271
+
1272
+ export default meta;
1273
+ type Story = StoryObj<typeof meta>;
1274
+
1275
+ export const MobileView: Story = {
1276
+ parameters: { viewport: { defaultViewport: 'mobile' } },
1277
+ };
1278
+
1279
+ export const TabletView: Story = {
1280
+ parameters: { viewport: { defaultViewport: 'tablet' } },
1281
+ };
1282
+
1283
+ export const DesktopView: Story = {
1284
+ parameters: { viewport: { defaultViewport: 'desktop' } },
1285
+ };