@sovereignfs/ui 0.21.3 → 0.33.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/dist/Button.module.css +75 -8
- package/dist/Calendar.module.css +126 -0
- package/dist/Card.module.css +4 -2
- package/dist/Checkbox.module.css +109 -0
- package/dist/CodeTextarea.module.css +53 -0
- package/dist/ConfirmDialog.module.css +109 -0
- package/dist/DatePicker.module.css +57 -0
- package/dist/Dialog.module.css +73 -46
- package/dist/DragHandleRow.module.css +57 -0
- package/dist/Drawer.module.css +61 -0
- package/dist/FormField.module.css +5 -0
- package/dist/Icon.module.css +5 -0
- package/dist/Input.module.css +10 -0
- package/dist/Menu.module.css +82 -0
- package/dist/NavTabs.module.css +4 -2
- package/dist/OverlayHeader.module.css +98 -0
- package/dist/Popover.module.css +31 -1
- package/dist/SegmentedControl.module.css +4 -2
- package/dist/Select.module.css +9 -0
- package/dist/Sheet.module.css +55 -0
- package/dist/SplitPane.module.css +85 -0
- package/dist/StatusBadge.module.css +59 -0
- package/dist/SystemBanner.module.css +4 -2
- package/dist/Tabs.module.css +4 -2
- package/dist/TagInput.module.css +141 -0
- package/dist/Textarea.module.css +39 -0
- package/dist/Toast.module.css +7 -5
- package/dist/Toggle.module.css +1 -1
- package/dist/Tooltip.module.css +6 -1
- package/dist/index.d.ts +549 -20
- package/dist/index.js +1496 -193
- package/dist/tokens/primitives.css +19 -0
- package/dist/tokens/semantic.css +19 -0
- package/package.json +1 -1
- package/src/__tests__/motion.test.tsx +105 -0
- package/src/components/Button/Button.module.css +75 -8
- package/src/components/Button/Button.stories.tsx +6 -0
- package/src/components/Button/Button.tsx +17 -1
- package/src/components/Button/__tests__/Button.test.tsx +19 -0
- package/src/components/Calendar/Calendar.module.css +126 -0
- package/src/components/Calendar/Calendar.tsx +213 -0
- package/src/components/Calendar/__tests__/Calendar.test.tsx +98 -0
- package/src/components/Calendar/dateUtils.ts +97 -0
- package/src/components/Card/Card.module.css +4 -2
- package/src/components/Checkbox/Checkbox.module.css +109 -0
- package/src/components/Checkbox/Checkbox.tsx +78 -0
- package/src/components/CodeTextarea/CodeTextarea.module.css +53 -0
- package/src/components/CodeTextarea/CodeTextarea.stories.tsx +77 -0
- package/src/components/CodeTextarea/CodeTextarea.tsx +36 -0
- package/src/components/ConfirmDialog/ConfirmDialog.module.css +109 -0
- package/src/components/ConfirmDialog/ConfirmDialog.tsx +124 -0
- package/src/components/ConfirmDialog/__tests__/ConfirmDialog.test.tsx +146 -0
- package/src/components/DatePicker/DatePicker.module.css +57 -0
- package/src/components/DatePicker/DatePicker.tsx +103 -0
- package/src/components/DatePicker/__tests__/DatePicker.test.tsx +89 -0
- package/src/components/Dialog/Dialog.module.css +73 -46
- package/src/components/Dialog/Dialog.stories.tsx +51 -1
- package/src/components/Dialog/Dialog.tsx +86 -22
- package/src/components/Dialog/__tests__/Dialog.test.tsx +67 -3
- package/src/components/DragHandleRow/DragHandleRow.module.css +57 -0
- package/src/components/DragHandleRow/DragHandleRow.tsx +61 -0
- package/src/components/Drawer/Drawer.module.css +61 -0
- package/src/components/Drawer/Drawer.tsx +117 -13
- package/src/components/Drawer/__tests__/Drawer.test.tsx +123 -2
- package/src/components/FormField/FormField.module.css +5 -0
- package/src/components/FormField/FormField.tsx +36 -11
- package/src/components/FormField/__tests__/FormField.test.tsx +83 -0
- package/src/components/Icon/Icon.module.css +5 -0
- package/src/components/Icon/Icon.stories.tsx +4 -3
- package/src/components/Icon/Icon.tsx +1 -1
- package/src/components/Icon/icons/calendar.tsx +23 -0
- package/src/components/Icon/icons/ellipsis-vertical.tsx +22 -0
- package/src/components/Icon/icons/index.ts +8 -4
- package/src/components/Icon/icons/sliders-horizontal.tsx +9 -9
- package/src/components/Input/Input.module.css +10 -0
- package/src/components/Menu/Menu.module.css +82 -0
- package/src/components/Menu/Menu.tsx +137 -0
- package/src/components/Menu/__tests__/Menu.test.tsx +197 -0
- package/src/components/NavTabs/NavTabs.module.css +4 -2
- package/src/components/OverlayHeader/OverlayHeader.module.css +98 -0
- package/src/components/OverlayHeader/OverlayHeader.tsx +62 -0
- package/src/components/OverlayHeader/__tests__/OverlayHeader.test.tsx +51 -0
- package/src/components/Popover/Popover.module.css +31 -1
- package/src/components/Popover/Popover.stories.tsx +126 -1
- package/src/components/Popover/Popover.tsx +140 -7
- package/src/components/SegmentedControl/SegmentedControl.module.css +4 -2
- package/src/components/Select/Select.module.css +9 -0
- package/src/components/Select/Select.tsx +10 -2
- package/src/components/Sheet/Sheet.module.css +55 -0
- package/src/components/Sheet/Sheet.tsx +151 -0
- package/src/components/Sheet/__tests__/Sheet.test.tsx +99 -0
- package/src/components/SplitPane/SplitPane.module.css +85 -0
- package/src/components/SplitPane/SplitPane.stories.tsx +160 -0
- package/src/components/SplitPane/SplitPane.tsx +149 -0
- package/src/components/SplitPane/__tests__/SplitPane.test.tsx +51 -0
- package/src/components/StatusBadge/StatusBadge.module.css +59 -0
- package/src/components/StatusBadge/StatusBadge.stories.tsx +74 -0
- package/src/components/StatusBadge/StatusBadge.tsx +65 -0
- package/src/components/SystemBanner/SystemBanner.module.css +4 -2
- package/src/components/Tabs/Tabs.module.css +4 -2
- package/src/components/TagInput/TagInput.module.css +141 -0
- package/src/components/TagInput/TagInput.stories.tsx +109 -0
- package/src/components/TagInput/TagInput.tsx +174 -0
- package/src/components/TagInput/__tests__/TagInput.test.tsx +69 -0
- package/src/components/Textarea/Textarea.module.css +39 -0
- package/src/components/Textarea/Textarea.tsx +15 -0
- package/src/components/Textarea/__tests__/Textarea.test.tsx +29 -0
- package/src/components/Toast/Toast.module.css +7 -5
- package/src/components/Toggle/Toggle.module.css +1 -1
- package/src/components/Tooltip/Tooltip.module.css +6 -1
- package/src/hooks/__tests__/useDoubleTap.test.tsx +117 -0
- package/src/hooks/__tests__/useIsMobile.test.tsx +80 -0
- package/src/hooks/__tests__/useLongPress.test.tsx +158 -0
- package/src/hooks/index.ts +4 -0
- package/src/hooks/useDoubleTap.ts +91 -0
- package/src/hooks/useIsMobile.ts +33 -0
- package/src/hooks/useLongPress.ts +185 -0
- package/src/index.ts +37 -2
- package/src/motion.ts +78 -0
- package/src/stories/Checkbox.stories.tsx +45 -0
- package/src/stories/DesignSystemOverview.stories.tsx +354 -8
- package/src/stories/DragHandleRow.stories.tsx +41 -0
- package/src/stories/FormField.stories.tsx +8 -8
- package/src/stories/InteractionHooks.stories.tsx +388 -0
- package/src/stories/MobilePatterns.stories.tsx +4 -4
- package/src/stories/Textarea.stories.tsx +51 -0
- package/src/stories/TokenGallery.stories.tsx +87 -1
- package/src/tokens/primitives.css +19 -0
- package/src/tokens/semantic.css +19 -0
- package/src/components/Icon/icons/terminal.tsx +0 -19
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
3
|
+
import { Card } from '../components/Card/Card';
|
|
4
|
+
import { useDoubleTapHandler, useIsMobile, useLongPress, useSingleOrDoubleTap } from '../hooks';
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Shared primitives (mirrors MobilePatterns.stories.tsx's doc styling)
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
const ff = 'var(--sv-font-family)';
|
|
11
|
+
|
|
12
|
+
function Heading({ level = 2, children }: { level?: 1 | 2 | 3; children: React.ReactNode }) {
|
|
13
|
+
const sizes: Record<number, string> = { 1: '1.75rem', 2: '1.125rem', 3: '0.9375rem' };
|
|
14
|
+
const weights: Record<number, number> = { 1: 700, 2: 600, 3: 600 };
|
|
15
|
+
const Tag = `h${level}` as 'h1' | 'h2' | 'h3';
|
|
16
|
+
return (
|
|
17
|
+
<Tag
|
|
18
|
+
style={{
|
|
19
|
+
fontFamily: ff,
|
|
20
|
+
fontSize: sizes[level],
|
|
21
|
+
fontWeight: weights[level],
|
|
22
|
+
color: 'var(--sv-color-text-primary)',
|
|
23
|
+
margin: 0,
|
|
24
|
+
lineHeight: 1.3,
|
|
25
|
+
}}
|
|
26
|
+
>
|
|
27
|
+
{children}
|
|
28
|
+
</Tag>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function Body({ children }: { children: React.ReactNode }) {
|
|
33
|
+
return (
|
|
34
|
+
<p
|
|
35
|
+
style={{
|
|
36
|
+
fontFamily: ff,
|
|
37
|
+
fontSize: 'var(--sv-font-size-sm)',
|
|
38
|
+
color: 'var(--sv-color-text-muted)',
|
|
39
|
+
margin: '0.5rem 0 0',
|
|
40
|
+
lineHeight: 1.6,
|
|
41
|
+
}}
|
|
42
|
+
>
|
|
43
|
+
{children}
|
|
44
|
+
</p>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function SectionHeader({ title, subtitle }: { title: string; subtitle?: string }) {
|
|
49
|
+
return (
|
|
50
|
+
<div
|
|
51
|
+
style={{
|
|
52
|
+
paddingBottom: 'var(--sv-space-3)',
|
|
53
|
+
borderBottom: '2px solid var(--sv-color-accent)',
|
|
54
|
+
marginBottom: 'var(--sv-space-5)',
|
|
55
|
+
}}
|
|
56
|
+
>
|
|
57
|
+
<Heading level={2}>{title}</Heading>
|
|
58
|
+
{subtitle && <Body>{subtitle}</Body>}
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function Callout({
|
|
64
|
+
type = 'info',
|
|
65
|
+
children,
|
|
66
|
+
}: {
|
|
67
|
+
type?: 'info' | 'warn' | 'tip';
|
|
68
|
+
children: React.ReactNode;
|
|
69
|
+
}) {
|
|
70
|
+
const map = {
|
|
71
|
+
info: {
|
|
72
|
+
bg: 'var(--sv-color-info-surface)',
|
|
73
|
+
border: 'var(--sv-color-info-border)',
|
|
74
|
+
text: 'var(--sv-color-info-text)',
|
|
75
|
+
label: 'ℹ',
|
|
76
|
+
},
|
|
77
|
+
warn: {
|
|
78
|
+
bg: 'var(--sv-color-warning-surface)',
|
|
79
|
+
border: 'var(--sv-color-warning-border)',
|
|
80
|
+
text: 'var(--sv-color-warning-text)',
|
|
81
|
+
label: '⚠',
|
|
82
|
+
},
|
|
83
|
+
tip: {
|
|
84
|
+
bg: 'var(--sv-color-success-surface)',
|
|
85
|
+
border: 'var(--sv-color-success-border)',
|
|
86
|
+
text: 'var(--sv-color-success-text)',
|
|
87
|
+
label: '✓',
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
const c = map[type];
|
|
91
|
+
return (
|
|
92
|
+
<div
|
|
93
|
+
style={{
|
|
94
|
+
background: c.bg,
|
|
95
|
+
border: `1px solid ${c.border}`,
|
|
96
|
+
borderRadius: 'var(--sv-radius-md)',
|
|
97
|
+
padding: 'var(--sv-space-3) var(--sv-space-4)',
|
|
98
|
+
display: 'flex',
|
|
99
|
+
gap: 'var(--sv-space-2)',
|
|
100
|
+
alignItems: 'flex-start',
|
|
101
|
+
marginBottom: 'var(--sv-space-4)',
|
|
102
|
+
}}
|
|
103
|
+
>
|
|
104
|
+
<span style={{ color: c.text, fontWeight: 700, flexShrink: 0, fontSize: '0.875rem' }}>
|
|
105
|
+
{c.label}
|
|
106
|
+
</span>
|
|
107
|
+
<p
|
|
108
|
+
style={{
|
|
109
|
+
fontFamily: ff,
|
|
110
|
+
fontSize: 'var(--sv-font-size-sm)',
|
|
111
|
+
color: c.text,
|
|
112
|
+
margin: 0,
|
|
113
|
+
lineHeight: 1.55,
|
|
114
|
+
}}
|
|
115
|
+
>
|
|
116
|
+
{children}
|
|
117
|
+
</p>
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function CodeBlock({ children }: { children: string }) {
|
|
123
|
+
return (
|
|
124
|
+
<pre
|
|
125
|
+
style={{
|
|
126
|
+
fontFamily: 'var(--sv-font-family-mono)',
|
|
127
|
+
fontSize: '0.75rem',
|
|
128
|
+
background: 'var(--sv-color-surface-sunken)',
|
|
129
|
+
border: '1px solid var(--sv-color-border)',
|
|
130
|
+
borderRadius: 'var(--sv-radius-md)',
|
|
131
|
+
padding: 'var(--sv-space-3) var(--sv-space-4)',
|
|
132
|
+
marginTop: 'var(--sv-space-3)',
|
|
133
|
+
overflowX: 'auto',
|
|
134
|
+
color: 'var(--sv-color-text-primary)',
|
|
135
|
+
lineHeight: 1.6,
|
|
136
|
+
}}
|
|
137
|
+
>
|
|
138
|
+
{children}
|
|
139
|
+
</pre>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function EventLog({ events }: { events: string[] }) {
|
|
144
|
+
return (
|
|
145
|
+
<div
|
|
146
|
+
style={{
|
|
147
|
+
marginTop: 'var(--sv-space-3)',
|
|
148
|
+
fontFamily: 'var(--sv-font-family-mono)',
|
|
149
|
+
fontSize: '0.75rem',
|
|
150
|
+
color: 'var(--sv-color-text-muted)',
|
|
151
|
+
background: 'var(--sv-color-surface-sunken)',
|
|
152
|
+
border: '1px solid var(--sv-color-border)',
|
|
153
|
+
borderRadius: 'var(--sv-radius-md)',
|
|
154
|
+
padding: 'var(--sv-space-2) var(--sv-space-3)',
|
|
155
|
+
minHeight: '2.5em',
|
|
156
|
+
}}
|
|
157
|
+
>
|
|
158
|
+
{events.length === 0 ? '(no events yet)' : events.slice(-5).join('\n')}
|
|
159
|
+
</div>
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
// Interactive demos
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
function LongPressDemo() {
|
|
168
|
+
const [events, setEvents] = useState<string[]>([]);
|
|
169
|
+
const log = (msg: string) =>
|
|
170
|
+
setEvents((e) => [...e, `${new Date().toLocaleTimeString()} — ${msg}`]);
|
|
171
|
+
const longPress = useLongPress({ onLongPress: () => log('onLongPress fired') });
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
<div>
|
|
175
|
+
<div
|
|
176
|
+
{...longPress}
|
|
177
|
+
style={{
|
|
178
|
+
...longPress.style,
|
|
179
|
+
padding: 'var(--sv-space-5)',
|
|
180
|
+
borderRadius: 'var(--sv-radius-md)',
|
|
181
|
+
border: '1px dashed var(--sv-color-border-strong)',
|
|
182
|
+
background: 'var(--sv-color-surface)',
|
|
183
|
+
textAlign: 'center',
|
|
184
|
+
fontFamily: ff,
|
|
185
|
+
fontSize: 'var(--sv-font-size-sm)',
|
|
186
|
+
color: 'var(--sv-color-text-muted)',
|
|
187
|
+
cursor: 'default',
|
|
188
|
+
}}
|
|
189
|
+
>
|
|
190
|
+
Touch and hold (real touch input only — see note below)
|
|
191
|
+
</div>
|
|
192
|
+
<EventLog events={events} />
|
|
193
|
+
<Callout type="info">
|
|
194
|
+
The hook only arms for <code>pointerType === 'touch'</code> — a desktop mouse
|
|
195
|
+
click never starts the timer, so mouse text-selection is never disabled. Test this on a real
|
|
196
|
+
touch device or with devtools' device toolbar (which synthesizes real touch pointer
|
|
197
|
+
events), not a plain mouse click in this preview.
|
|
198
|
+
</Callout>
|
|
199
|
+
</div>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function DoubleTapDemo() {
|
|
204
|
+
const [events, setEvents] = useState<string[]>([]);
|
|
205
|
+
const log = (msg: string) =>
|
|
206
|
+
setEvents((e) => [...e, `${new Date().toLocaleTimeString()} — ${msg}`]);
|
|
207
|
+
|
|
208
|
+
const handleSwatchTap = useDoubleTapHandler<React.MouseEvent>(() => log('double-tap: swatch'));
|
|
209
|
+
const handleTitleTap = useSingleOrDoubleTap<React.MouseEvent>(
|
|
210
|
+
() => log('single tap: would navigate'),
|
|
211
|
+
() => log('double-tap: would rename'),
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
return (
|
|
215
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--sv-space-3)' }}>
|
|
216
|
+
<div style={{ display: 'flex', gap: 'var(--sv-space-3)', alignItems: 'center' }}>
|
|
217
|
+
<button
|
|
218
|
+
type="button"
|
|
219
|
+
onClick={handleSwatchTap}
|
|
220
|
+
style={{
|
|
221
|
+
width: 40,
|
|
222
|
+
height: 40,
|
|
223
|
+
borderRadius: 'var(--sv-radius-full)',
|
|
224
|
+
background: 'var(--sv-color-accent)',
|
|
225
|
+
border: 'none',
|
|
226
|
+
cursor: 'pointer',
|
|
227
|
+
}}
|
|
228
|
+
aria-label="Double-tap demo swatch"
|
|
229
|
+
/>
|
|
230
|
+
<span
|
|
231
|
+
style={{
|
|
232
|
+
fontFamily: ff,
|
|
233
|
+
fontSize: 'var(--sv-font-size-sm)',
|
|
234
|
+
color: 'var(--sv-color-text-muted)',
|
|
235
|
+
}}
|
|
236
|
+
>
|
|
237
|
+
useDoubleTapHandler — nothing to preempt, double-click/double-tap fires immediately
|
|
238
|
+
</span>
|
|
239
|
+
</div>
|
|
240
|
+
<div style={{ display: 'flex', gap: 'var(--sv-space-3)', alignItems: 'center' }}>
|
|
241
|
+
<button
|
|
242
|
+
type="button"
|
|
243
|
+
onClick={handleTitleTap}
|
|
244
|
+
style={{
|
|
245
|
+
padding: 'var(--sv-space-2) var(--sv-space-4)',
|
|
246
|
+
borderRadius: 'var(--sv-radius-md)',
|
|
247
|
+
background: 'var(--sv-color-surface-sunken)',
|
|
248
|
+
border: '1px solid var(--sv-color-border)',
|
|
249
|
+
fontFamily: ff,
|
|
250
|
+
fontSize: 'var(--sv-font-size-sm)',
|
|
251
|
+
cursor: 'pointer',
|
|
252
|
+
}}
|
|
253
|
+
>
|
|
254
|
+
List title
|
|
255
|
+
</button>
|
|
256
|
+
<span
|
|
257
|
+
style={{
|
|
258
|
+
fontFamily: ff,
|
|
259
|
+
fontSize: 'var(--sv-font-size-sm)',
|
|
260
|
+
color: 'var(--sv-color-text-muted)',
|
|
261
|
+
}}
|
|
262
|
+
>
|
|
263
|
+
useSingleOrDoubleTap — single tap deferred ~350ms so a double can preempt it
|
|
264
|
+
</span>
|
|
265
|
+
</div>
|
|
266
|
+
<EventLog events={events} />
|
|
267
|
+
</div>
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function IsMobileDemo() {
|
|
272
|
+
const isMobile = useIsMobile();
|
|
273
|
+
return (
|
|
274
|
+
<div
|
|
275
|
+
style={{
|
|
276
|
+
padding: 'var(--sv-space-4)',
|
|
277
|
+
borderRadius: 'var(--sv-radius-md)',
|
|
278
|
+
background: isMobile ? 'var(--sv-color-success-surface)' : 'var(--sv-color-info-surface)',
|
|
279
|
+
color: isMobile ? 'var(--sv-color-success-text)' : 'var(--sv-color-info-text)',
|
|
280
|
+
fontFamily: ff,
|
|
281
|
+
fontSize: 'var(--sv-font-size-sm)',
|
|
282
|
+
fontWeight: 600,
|
|
283
|
+
textAlign: 'center',
|
|
284
|
+
}}
|
|
285
|
+
>
|
|
286
|
+
useIsMobile() → {String(isMobile)} — resize the preview or switch the viewport toolbar to see
|
|
287
|
+
this flip at 768px.
|
|
288
|
+
</div>
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ---------------------------------------------------------------------------
|
|
293
|
+
// Main story component
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
|
|
296
|
+
function InteractionHooksDoc() {
|
|
297
|
+
return (
|
|
298
|
+
<div
|
|
299
|
+
style={{
|
|
300
|
+
padding: 'var(--sv-space-6)',
|
|
301
|
+
background: 'var(--sv-color-surface)',
|
|
302
|
+
minHeight: '100vh',
|
|
303
|
+
fontFamily: ff,
|
|
304
|
+
maxWidth: 760,
|
|
305
|
+
}}
|
|
306
|
+
>
|
|
307
|
+
<div style={{ marginBottom: 'var(--sv-space-8)' }}>
|
|
308
|
+
<Heading level={1}>Interaction Hooks</Heading>
|
|
309
|
+
<Body>
|
|
310
|
+
Touch gesture handling, carried once in the design system so no plugin has to rediscover
|
|
311
|
+
the failure modes. Per the DS-first principle, build gesture handling here — not in a
|
|
312
|
+
plugin. See{' '}
|
|
313
|
+
<code style={{ fontFamily: 'var(--sv-font-family-mono)', fontSize: '0.8em' }}>
|
|
314
|
+
docs/design-system.md
|
|
315
|
+
</code>{' '}
|
|
316
|
+
for the full written reference.
|
|
317
|
+
</Body>
|
|
318
|
+
</div>
|
|
319
|
+
|
|
320
|
+
<section style={{ marginBottom: 'var(--sv-space-10)' }}>
|
|
321
|
+
<SectionHeader
|
|
322
|
+
title="useLongPress"
|
|
323
|
+
subtitle="Touch-and-hold with movement tolerance, pointercancel handling, OS-callout suppression, and time-boxed click suppression."
|
|
324
|
+
/>
|
|
325
|
+
<Card padding="md">
|
|
326
|
+
<LongPressDemo />
|
|
327
|
+
</Card>
|
|
328
|
+
<CodeBlock>{`const longPress = useLongPress({ onLongPress: () => setBulkMode(true) });
|
|
329
|
+
<div {...longPress}>Hold to select</div>`}</CodeBlock>
|
|
330
|
+
</section>
|
|
331
|
+
|
|
332
|
+
<section style={{ marginBottom: 'var(--sv-space-10)' }}>
|
|
333
|
+
<SectionHeader
|
|
334
|
+
title="useDoubleTapHandler / useSingleOrDoubleTap"
|
|
335
|
+
subtitle="Desktop double-click reports e.detail === 2 natively; touch needs timing-based detection instead."
|
|
336
|
+
/>
|
|
337
|
+
<Card padding="md">
|
|
338
|
+
<DoubleTapDemo />
|
|
339
|
+
</Card>
|
|
340
|
+
<Callout type="tip">
|
|
341
|
+
Use <code style={{ fontFamily: 'var(--sv-font-family-mono)' }}>useDoubleTapHandler</code>{' '}
|
|
342
|
+
when the single tap has nothing to cancel. Use{' '}
|
|
343
|
+
<code style={{ fontFamily: 'var(--sv-font-family-mono)' }}>useSingleOrDoubleTap</code>{' '}
|
|
344
|
+
only when the single tap has a default action (e.g. navigation) a following double-tap
|
|
345
|
+
must be able to preempt — every single tap through it pays the ~350ms wait.
|
|
346
|
+
</Callout>
|
|
347
|
+
</section>
|
|
348
|
+
|
|
349
|
+
<section style={{ marginBottom: 'var(--sv-space-10)' }}>
|
|
350
|
+
<SectionHeader
|
|
351
|
+
title="useIsMobile"
|
|
352
|
+
subtitle="SSR-safe viewport check against the platform's single documented breakpoint (768px)."
|
|
353
|
+
/>
|
|
354
|
+
<Card padding="md">
|
|
355
|
+
<IsMobileDemo />
|
|
356
|
+
</Card>
|
|
357
|
+
<CodeBlock>{`import { useIsMobile, MOBILE_BREAKPOINT_PX } from '@sovereignfs/ui';
|
|
358
|
+
|
|
359
|
+
const isMobile = useIsMobile(); // defaults to MOBILE_BREAKPOINT_PX (768)`}</CodeBlock>
|
|
360
|
+
</section>
|
|
361
|
+
</div>
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// ---------------------------------------------------------------------------
|
|
366
|
+
|
|
367
|
+
const meta = {
|
|
368
|
+
title: 'Overview/Interaction Hooks',
|
|
369
|
+
component: InteractionHooksDoc,
|
|
370
|
+
parameters: {
|
|
371
|
+
layout: 'fullscreen',
|
|
372
|
+
docs: {
|
|
373
|
+
description: {
|
|
374
|
+
component:
|
|
375
|
+
'Reference and live demos for the touch-gesture hooks exported from @sovereignfs/ui. Long-press requires real touch input to trigger — use the devtools device toolbar or a physical device.',
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
} satisfies Meta<typeof InteractionHooksDoc>;
|
|
380
|
+
|
|
381
|
+
export default meta;
|
|
382
|
+
type Story = StoryObj<typeof meta>;
|
|
383
|
+
|
|
384
|
+
export const Default: Story = {};
|
|
385
|
+
|
|
386
|
+
export const MobileView: Story = {
|
|
387
|
+
parameters: { viewport: { defaultViewport: 'mobile' } },
|
|
388
|
+
};
|
|
@@ -405,11 +405,11 @@ function MobilePatternsDoc() {
|
|
|
405
405
|
gap: 'var(--sv-space-3)',
|
|
406
406
|
}}
|
|
407
407
|
>
|
|
408
|
-
<FormField label="Display name"
|
|
409
|
-
<Input
|
|
408
|
+
<FormField label="Display name" id="mp-name">
|
|
409
|
+
{(field) => <Input {...field} placeholder="Your name" />}
|
|
410
410
|
</FormField>
|
|
411
|
-
<FormField label="Email"
|
|
412
|
-
<Input
|
|
411
|
+
<FormField label="Email" id="mp-email">
|
|
412
|
+
{(field) => <Input {...field} type="email" placeholder="you@example.com" />}
|
|
413
413
|
</FormField>
|
|
414
414
|
<Button type="submit">Save changes</Button>
|
|
415
415
|
</div>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { Textarea } from '../components/Textarea/Textarea';
|
|
3
|
+
|
|
4
|
+
// Wrap in a label so a11y checks pass (Textarea is a bare <textarea> — no
|
|
5
|
+
// built-in label association).
|
|
6
|
+
function Labeled({ label, ...props }: React.ComponentProps<typeof Textarea> & { label: string }) {
|
|
7
|
+
return (
|
|
8
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, width: 280 }}>
|
|
9
|
+
<label
|
|
10
|
+
htmlFor="sb-textarea"
|
|
11
|
+
style={{ fontSize: 14, color: 'var(--sv-color-text-primary)', fontFamily: 'system-ui' }}
|
|
12
|
+
>
|
|
13
|
+
{label}
|
|
14
|
+
</label>
|
|
15
|
+
<Textarea id="sb-textarea" {...props} />
|
|
16
|
+
</div>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const meta = {
|
|
21
|
+
title: 'Components/Textarea',
|
|
22
|
+
component: Textarea,
|
|
23
|
+
parameters: {
|
|
24
|
+
layout: 'centered',
|
|
25
|
+
docs: {
|
|
26
|
+
description: {
|
|
27
|
+
component:
|
|
28
|
+
'The primitive multi-line text field. RSC-safe; forwards all native `<textarea>` props. No label is built-in — always pair with a `<label>` (or `FormField`) for accessibility.',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
} satisfies Meta<typeof Textarea>;
|
|
33
|
+
|
|
34
|
+
export default meta;
|
|
35
|
+
type Story = StoryObj<typeof meta>;
|
|
36
|
+
|
|
37
|
+
export const Default: Story = {
|
|
38
|
+
render: (_args) => <Labeled label="Description" placeholder="Add a description…" />,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const WithValue: Story = {
|
|
42
|
+
render: (_args) => <Labeled label="Notes" defaultValue="Follow up next week." />,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const CustomRows: Story = {
|
|
46
|
+
render: (_args) => <Labeled label="Message" rows={8} placeholder="Write your message…" />,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const Disabled: Story = {
|
|
50
|
+
render: (_args) => <Labeled label="Read-only field" defaultValue="Cannot be changed" disabled />,
|
|
51
|
+
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
1
2
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
3
|
|
|
3
4
|
// ---------------------------------------------------------------------------
|
|
@@ -19,16 +20,21 @@ const SEMANTIC_COLORS = [
|
|
|
19
20
|
'--sv-color-border-strong',
|
|
20
21
|
'--sv-color-accent',
|
|
21
22
|
'--sv-color-accent-hover',
|
|
23
|
+
'--sv-color-accent-subtle',
|
|
22
24
|
'--sv-color-focus-ring',
|
|
23
25
|
'--sv-color-error-surface',
|
|
24
26
|
'--sv-color-error-text',
|
|
25
27
|
'--sv-color-error-border',
|
|
28
|
+
'--sv-color-error-solid',
|
|
26
29
|
'--sv-color-warning-surface',
|
|
27
30
|
'--sv-color-warning-text',
|
|
28
31
|
'--sv-color-warning-border',
|
|
29
32
|
'--sv-color-success-surface',
|
|
30
33
|
'--sv-color-success-text',
|
|
31
34
|
'--sv-color-success-border',
|
|
35
|
+
'--sv-color-success-solid',
|
|
36
|
+
'--sv-color-text-on-error',
|
|
37
|
+
'--sv-color-text-on-success',
|
|
32
38
|
'--sv-color-info-surface',
|
|
33
39
|
'--sv-color-info-text',
|
|
34
40
|
'--sv-color-info-border',
|
|
@@ -57,7 +63,12 @@ const RADIUS_TOKENS = [
|
|
|
57
63
|
'--sv-radius-3xl',
|
|
58
64
|
'--sv-radius-full',
|
|
59
65
|
];
|
|
60
|
-
const ICON_SIZE_TOKENS = [
|
|
66
|
+
const ICON_SIZE_TOKENS = [
|
|
67
|
+
'--sv-icon-size-xs',
|
|
68
|
+
'--sv-icon-size-sm',
|
|
69
|
+
'--sv-icon-size-md',
|
|
70
|
+
'--sv-icon-size-lg',
|
|
71
|
+
];
|
|
61
72
|
|
|
62
73
|
const FONT_SIZE_TOKENS = [
|
|
63
74
|
'--sv-font-size-label',
|
|
@@ -82,6 +93,32 @@ const SHADOW_TOKENS = [
|
|
|
82
93
|
'--sv-shadow-hover',
|
|
83
94
|
'--sv-shadow-popover',
|
|
84
95
|
'--sv-shadow-overlay',
|
|
96
|
+
'--sv-shadow-control',
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
// Realistic duration+easing pairings, not every combination — matches how
|
|
100
|
+
// Dialog/Drawer actually use these tokens (see motion.ts).
|
|
101
|
+
const MOTION_PAIRS: { durationToken: string; easeToken: string; use: string }[] = [
|
|
102
|
+
{
|
|
103
|
+
durationToken: '--sv-motion-duration-fast',
|
|
104
|
+
easeToken: '--sv-motion-ease-out',
|
|
105
|
+
use: 'micro-interaction',
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
durationToken: '--sv-motion-duration-base',
|
|
109
|
+
easeToken: '--sv-motion-ease-out',
|
|
110
|
+
use: 'Dialog / Drawer entrance',
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
durationToken: '--sv-motion-duration-slow',
|
|
114
|
+
easeToken: '--sv-motion-ease-in-out',
|
|
115
|
+
use: 'larger surface / longer travel',
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
durationToken: '--sv-motion-duration-base',
|
|
119
|
+
easeToken: '--sv-motion-ease-spring',
|
|
120
|
+
use: 'emphasis (use sparingly)',
|
|
121
|
+
},
|
|
85
122
|
];
|
|
86
123
|
|
|
87
124
|
// ---------------------------------------------------------------------------
|
|
@@ -152,6 +189,49 @@ function ScaleRow({
|
|
|
152
189
|
);
|
|
153
190
|
}
|
|
154
191
|
|
|
192
|
+
// Auto-loops so the duration/easing is visible without needing to interact —
|
|
193
|
+
// same technique Dialog/Drawer use for their own open/close transition
|
|
194
|
+
// (see motion.ts), just driven by a timer instead of an `open` prop.
|
|
195
|
+
function MotionRow({ durationToken, easeToken, use }: (typeof MOTION_PAIRS)[number]) {
|
|
196
|
+
const [toggled, setToggled] = useState(false);
|
|
197
|
+
useEffect(() => {
|
|
198
|
+
const id = setInterval(() => setToggled((v) => !v), 900);
|
|
199
|
+
return () => clearInterval(id);
|
|
200
|
+
}, []);
|
|
201
|
+
return (
|
|
202
|
+
<div style={row}>
|
|
203
|
+
<div
|
|
204
|
+
style={{
|
|
205
|
+
position: 'relative',
|
|
206
|
+
width: 120,
|
|
207
|
+
height: 24,
|
|
208
|
+
background: 'var(--sv-color-surface-sunken)',
|
|
209
|
+
borderRadius: 4,
|
|
210
|
+
flexShrink: 0,
|
|
211
|
+
}}
|
|
212
|
+
>
|
|
213
|
+
<div
|
|
214
|
+
style={{
|
|
215
|
+
position: 'absolute',
|
|
216
|
+
top: 2,
|
|
217
|
+
left: 2,
|
|
218
|
+
width: 20,
|
|
219
|
+
height: 20,
|
|
220
|
+
borderRadius: '50%',
|
|
221
|
+
background: 'var(--sv-color-accent)',
|
|
222
|
+
transform: `translateX(${toggled ? 96 : 0}px)`,
|
|
223
|
+
transition: `transform var(${durationToken}) var(${easeToken})`,
|
|
224
|
+
}}
|
|
225
|
+
/>
|
|
226
|
+
</div>
|
|
227
|
+
<span style={label}>
|
|
228
|
+
{durationToken} · {easeToken}
|
|
229
|
+
</span>
|
|
230
|
+
<span style={value}>{use}</span>
|
|
231
|
+
</div>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
155
235
|
function Section({ title, children }: { title: string; children: React.ReactNode }) {
|
|
156
236
|
return (
|
|
157
237
|
<section style={{ marginBottom: 40 }}>
|
|
@@ -329,6 +409,12 @@ function TokenGalleryComponent() {
|
|
|
329
409
|
/>
|
|
330
410
|
))}
|
|
331
411
|
</Section>
|
|
412
|
+
|
|
413
|
+
<Section title="Motion">
|
|
414
|
+
{MOTION_PAIRS.map((pair) => (
|
|
415
|
+
<MotionRow key={`${pair.durationToken}-${pair.easeToken}`} {...pair} />
|
|
416
|
+
))}
|
|
417
|
+
</Section>
|
|
332
418
|
</div>
|
|
333
419
|
);
|
|
334
420
|
}
|
|
@@ -78,6 +78,7 @@
|
|
|
78
78
|
--sv-font-weight-bold: 700;
|
|
79
79
|
|
|
80
80
|
/* Icon sizes — scale tokens used directly by the Icon component */
|
|
81
|
+
--sv-icon-size-xs: 0.75rem; /* 12px — inline metadata (e.g. a due-date badge) */
|
|
81
82
|
--sv-icon-size-sm: 1rem; /* 16px — inline with body text */
|
|
82
83
|
--sv-icon-size-md: 1.25rem; /* 20px — standard affordance */
|
|
83
84
|
--sv-icon-size-lg: 1.5rem; /* 24px — prominent / standalone */
|
|
@@ -96,4 +97,22 @@
|
|
|
96
97
|
touch without a pointing device. Applied to icon-only controls on mobile
|
|
97
98
|
(avatar, footer button, Drawer nav items). */
|
|
98
99
|
--sv-touch-target-min: 44px;
|
|
100
|
+
|
|
101
|
+
/* Motion scale — durations and easings for overlay enter/exit transitions
|
|
102
|
+
(Dialog, Drawer, and their Phase-B successors: Sheet, ConfirmDialog,
|
|
103
|
+
Menu). Theme-stable like the other scale tokens above — dark mode does
|
|
104
|
+
not change motion, so there is no semantic tier. Components must pair
|
|
105
|
+
every transition with `@media (prefers-reduced-motion: reduce)` collapsing
|
|
106
|
+
it to near-instant; the durations here are not a substitute for that. */
|
|
107
|
+
--sv-motion-duration-fast: 150ms; /* micro-interactions — hover/press feedback */
|
|
108
|
+
--sv-motion-duration-base: 250ms; /* default overlay enter/exit (Dialog, Drawer) */
|
|
109
|
+
--sv-motion-duration-slow: 350ms; /* larger surfaces / bigger travel distance */
|
|
110
|
+
--sv-motion-ease-out: cubic-bezier(0, 0, 0.2, 1); /* entrances — fast start, gentle settle */
|
|
111
|
+
--sv-motion-ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); /* state changes with no fixed direction */
|
|
112
|
+
--sv-motion-ease-spring: cubic-bezier(
|
|
113
|
+
0.34,
|
|
114
|
+
1.56,
|
|
115
|
+
0.64,
|
|
116
|
+
1
|
|
117
|
+
); /* slight overshoot — playful emphasis, use sparingly */
|
|
99
118
|
}
|
package/src/tokens/semantic.css
CHANGED
|
@@ -25,6 +25,10 @@
|
|
|
25
25
|
|
|
26
26
|
--sv-color-accent: var(--sv-grey-900);
|
|
27
27
|
--sv-color-accent-hover: var(--sv-grey-700);
|
|
28
|
+
/* Derived from --sv-color-accent via color-mix, so it re-themes automatically —
|
|
29
|
+
no separate dark-mode override needed. Tinted background for badges/chips
|
|
30
|
+
paired with --sv-color-accent text. */
|
|
31
|
+
--sv-color-accent-subtle: color-mix(in srgb, var(--sv-color-accent) 12%, transparent);
|
|
28
32
|
|
|
29
33
|
--sv-color-focus-ring: var(--sv-grey-900);
|
|
30
34
|
|
|
@@ -41,6 +45,19 @@
|
|
|
41
45
|
--sv-color-success-text: var(--sv-green-800);
|
|
42
46
|
--sv-color-success-border: var(--sv-green-200);
|
|
43
47
|
|
|
48
|
+
/* Status solids — a saturated fill for status affordances that need to
|
|
49
|
+
read as a filled action button rather than a tinted surface (e.g. a
|
|
50
|
+
mobile swipe-to-reveal Delete/Done action). Paired with
|
|
51
|
+
--sv-color-text-on-error/-success (white) for the icon/label on top.
|
|
52
|
+
Intentionally NOT overridden under [data-theme='dark'] below — like
|
|
53
|
+
--sv-color-accent-subtle, a solid action fill reads the same regardless
|
|
54
|
+
of overall page theme; it's the surrounding surface that inverts, not
|
|
55
|
+
this fixed-contrast pairing. */
|
|
56
|
+
--sv-color-error-solid: var(--sv-red-700);
|
|
57
|
+
--sv-color-success-solid: var(--sv-green-800);
|
|
58
|
+
--sv-color-text-on-error: var(--sv-white);
|
|
59
|
+
--sv-color-text-on-success: var(--sv-white);
|
|
60
|
+
|
|
44
61
|
/* Info surfaces — blue palette. Use for informational banners and badges.
|
|
45
62
|
Never hardcode colour hex in components — reference these tokens. */
|
|
46
63
|
--sv-color-info-surface: var(--sv-blue-100);
|
|
@@ -56,6 +73,7 @@
|
|
|
56
73
|
--sv-shadow-popover:
|
|
57
74
|
0 4px 16px rgba(0, 0, 0, 0.14), 0 2px 6px rgba(0, 0, 0, 0.08); /* e2 — floating panels */
|
|
58
75
|
--sv-shadow-overlay: 0 10px 38px rgba(0, 0, 0, 0.18), 0 6px 12px rgba(0, 0, 0, 0.12);
|
|
76
|
+
--sv-shadow-control: 0 1px 3px rgba(0, 0, 0, 0.2); /* small interactive-control shadows, e.g. Toggle thumb */
|
|
59
77
|
|
|
60
78
|
/*
|
|
61
79
|
* Instance identity tokens (RFC 0027 / RFC 0032 rename). Distinct namespace from --sv-color-*:
|
|
@@ -108,4 +126,5 @@
|
|
|
108
126
|
--sv-shadow-hover: 0 2px 6px rgba(0, 0, 0, 0.3);
|
|
109
127
|
--sv-shadow-popover: 0 4px 16px rgba(0, 0, 0, 0.5), 0 2px 6px rgba(0, 0, 0, 0.3);
|
|
110
128
|
--sv-shadow-overlay: 0 10px 38px rgba(0, 0, 0, 0.6), 0 6px 12px rgba(0, 0, 0, 0.5);
|
|
129
|
+
--sv-shadow-control: 0 1px 3px rgba(0, 0, 0, 0.5);
|
|
111
130
|
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import type { SVGProps } from 'react';
|
|
2
|
-
|
|
3
|
-
export function TerminalIcon(props: SVGProps<SVGSVGElement>) {
|
|
4
|
-
return (
|
|
5
|
-
<svg
|
|
6
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
7
|
-
viewBox="0 0 24 24"
|
|
8
|
-
fill="none"
|
|
9
|
-
stroke="currentColor"
|
|
10
|
-
strokeWidth="2"
|
|
11
|
-
strokeLinecap="round"
|
|
12
|
-
strokeLinejoin="round"
|
|
13
|
-
{...props}
|
|
14
|
-
>
|
|
15
|
-
<polyline points="4 17 10 11 4 5" />
|
|
16
|
-
<line x1="12" x2="20" y1="19" y2="19" />
|
|
17
|
-
</svg>
|
|
18
|
-
);
|
|
19
|
-
}
|