@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,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
|
+
};
|