@sovereignfs/ui 0.23.0 → 0.33.1
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 +14 -2
- 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 +77 -50
- package/dist/DragHandleRow.module.css +9 -3
- package/dist/Drawer.module.css +61 -0
- package/dist/Icon.module.css +5 -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/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/Toast.module.css +4 -2
- package/dist/Tooltip.module.css +6 -1
- package/dist/index.d.ts +493 -17
- package/dist/index.js +1390 -209
- package/dist/tokens/primitives.css +19 -0
- package/dist/tokens/semantic.css +13 -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 +14 -2
- package/src/components/Checkbox/Checkbox.tsx +22 -12
- 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 +77 -50
- 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 +9 -3
- 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/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/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/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/Toast/Toast.module.css +4 -2
- 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 +30 -1
- package/src/motion.ts +78 -0
- package/src/stories/DesignSystemOverview.stories.tsx +292 -4
- package/src/stories/InteractionHooks.stories.tsx +388 -0
- package/src/stories/TokenGallery.stories.tsx +85 -1
- package/src/tokens/primitives.css +19 -0
- package/src/tokens/semantic.css +13 -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
|
+
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
1
2
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
3
|
|
|
3
4
|
// ---------------------------------------------------------------------------
|
|
@@ -24,12 +25,16 @@ const SEMANTIC_COLORS = [
|
|
|
24
25
|
'--sv-color-error-surface',
|
|
25
26
|
'--sv-color-error-text',
|
|
26
27
|
'--sv-color-error-border',
|
|
28
|
+
'--sv-color-error-solid',
|
|
27
29
|
'--sv-color-warning-surface',
|
|
28
30
|
'--sv-color-warning-text',
|
|
29
31
|
'--sv-color-warning-border',
|
|
30
32
|
'--sv-color-success-surface',
|
|
31
33
|
'--sv-color-success-text',
|
|
32
34
|
'--sv-color-success-border',
|
|
35
|
+
'--sv-color-success-solid',
|
|
36
|
+
'--sv-color-text-on-error',
|
|
37
|
+
'--sv-color-text-on-success',
|
|
33
38
|
'--sv-color-info-surface',
|
|
34
39
|
'--sv-color-info-text',
|
|
35
40
|
'--sv-color-info-border',
|
|
@@ -58,7 +63,12 @@ const RADIUS_TOKENS = [
|
|
|
58
63
|
'--sv-radius-3xl',
|
|
59
64
|
'--sv-radius-full',
|
|
60
65
|
];
|
|
61
|
-
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
|
+
];
|
|
62
72
|
|
|
63
73
|
const FONT_SIZE_TOKENS = [
|
|
64
74
|
'--sv-font-size-label',
|
|
@@ -86,6 +96,31 @@ const SHADOW_TOKENS = [
|
|
|
86
96
|
'--sv-shadow-control',
|
|
87
97
|
];
|
|
88
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
|
+
},
|
|
122
|
+
];
|
|
123
|
+
|
|
89
124
|
// ---------------------------------------------------------------------------
|
|
90
125
|
|
|
91
126
|
const row: React.CSSProperties = {
|
|
@@ -154,6 +189,49 @@ function ScaleRow({
|
|
|
154
189
|
);
|
|
155
190
|
}
|
|
156
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
|
+
|
|
157
235
|
function Section({ title, children }: { title: string; children: React.ReactNode }) {
|
|
158
236
|
return (
|
|
159
237
|
<section style={{ marginBottom: 40 }}>
|
|
@@ -331,6 +409,12 @@ function TokenGalleryComponent() {
|
|
|
331
409
|
/>
|
|
332
410
|
))}
|
|
333
411
|
</Section>
|
|
412
|
+
|
|
413
|
+
<Section title="Motion">
|
|
414
|
+
{MOTION_PAIRS.map((pair) => (
|
|
415
|
+
<MotionRow key={`${pair.durationToken}-${pair.easeToken}`} {...pair} />
|
|
416
|
+
))}
|
|
417
|
+
</Section>
|
|
334
418
|
</div>
|
|
335
419
|
);
|
|
336
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
|
@@ -45,6 +45,19 @@
|
|
|
45
45
|
--sv-color-success-text: var(--sv-green-800);
|
|
46
46
|
--sv-color-success-border: var(--sv-green-200);
|
|
47
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
|
+
|
|
48
61
|
/* Info surfaces — blue palette. Use for informational banners and badges.
|
|
49
62
|
Never hardcode colour hex in components — reference these tokens. */
|
|
50
63
|
--sv-color-info-surface: var(--sv-blue-100);
|
|
@@ -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
|
-
}
|