@sovereignfs/ui 0.45.0 → 0.54.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/LegalLinks.module.css +24 -0
- package/dist/Markdown.module.css +49 -0
- package/dist/MobileAppsDrawer.module.css +81 -0
- package/dist/MobileFooter.module.css +73 -0
- package/dist/MobileHeader.module.css +63 -0
- package/dist/PageContainer.module.css +20 -0
- package/dist/SwipableMobileCarousel.module.css +55 -0
- package/dist/SwipableMobileCarouselDots.module.css +49 -0
- package/dist/SwipableMobileCarouselSlideParts.module.css +29 -0
- package/dist/index.d.ts +654 -6
- package/dist/index.js +1856 -1133
- package/dist/tokens/primitives.css +25 -0
- package/package.json +13 -9
- package/src/components/Badge/Badge.tsx +1 -7
- package/src/components/DeviceOnlyGate/DeviceOnlyGate.tsx +58 -0
- package/src/components/DeviceOnlyGate/__tests__/DeviceOnlyGate.test.tsx +45 -0
- package/src/components/DeviceStorageKeyGate/DeviceStorageKeyGate.tsx +100 -0
- package/src/components/DeviceStorageKeyGate/__tests__/DeviceStorageKeyGate.test.tsx +119 -0
- package/src/components/Dialog/Dialog.tsx +8 -59
- package/src/components/Drawer/Drawer.stories.tsx +44 -0
- package/src/components/Drawer/Drawer.tsx +18 -56
- package/src/components/Drawer/__tests__/Drawer.test.tsx +22 -0
- package/src/components/Icon/icons/index.ts +8 -0
- package/src/components/Icon/icons/menu.tsx +22 -0
- package/src/components/Icon/icons/smartphone.tsx +21 -0
- package/src/components/LegalLinks/LegalLinks.module.css +24 -0
- package/src/components/LegalLinks/LegalLinks.stories.tsx +22 -0
- package/src/components/LegalLinks/LegalLinks.tsx +40 -0
- package/src/components/Markdown/Markdown.module.css +49 -0
- package/src/components/Markdown/Markdown.stories.tsx +30 -0
- package/src/components/Markdown/Markdown.tsx +168 -0
- package/src/components/MobileAppsDrawer/MobileAppsDrawer.module.css +81 -0
- package/src/components/MobileAppsDrawer/MobileAppsDrawer.tsx +78 -0
- package/src/components/MobileAppsDrawer/__tests__/MobileAppsDrawer.test.tsx +80 -0
- package/src/components/MobileFooter/MobileFooter.module.css +73 -0
- package/src/components/MobileFooter/MobileFooter.stories.tsx +47 -0
- package/src/components/MobileFooter/MobileFooter.tsx +109 -0
- package/src/components/MobileFooter/__tests__/MobileFooter.test.tsx +71 -0
- package/src/components/MobileHeader/MobileHeader.module.css +63 -0
- package/src/components/MobileHeader/MobileHeader.stories.tsx +57 -0
- package/src/components/MobileHeader/MobileHeader.tsx +77 -0
- package/src/components/MobileHeader/__tests__/MobileHeader.test.tsx +58 -0
- package/src/components/NavTabs/NavTabs.tsx +39 -11
- package/src/components/OfflineGate/OfflineGate.tsx +44 -0
- package/src/components/OfflineGate/__tests__/OfflineGate.test.tsx +66 -0
- package/src/components/PageContainer/PageContainer.module.css +20 -0
- package/src/components/PageContainer/PageContainer.tsx +31 -0
- package/src/components/PageContainer/__tests__/PageContainer.test.tsx +37 -0
- package/src/components/PageHeader/PageHeader.tsx +16 -2
- package/src/components/ResponsiveSurface/ResponsiveSurface.tsx +24 -0
- package/src/components/ResponsiveSurface/__tests__/ResponsiveSurface.test.tsx +73 -0
- package/src/components/Sheet/Sheet.tsx +9 -51
- package/src/components/SwipableMobileCarousel/SwipableMobileCarousel.module.css +55 -0
- package/src/components/SwipableMobileCarousel/SwipableMobileCarousel.tsx +227 -0
- package/src/components/SwipableMobileCarousel/SwipableMobileCarouselSlide.tsx +35 -0
- package/src/components/SwipableMobileCarousel/SwipableMobileCarouselSlideBody.tsx +42 -0
- package/src/components/SwipableMobileCarousel/SwipableMobileCarouselSlideFooter.tsx +18 -0
- package/src/components/SwipableMobileCarousel/SwipableMobileCarouselSlideHeader.tsx +21 -0
- package/src/components/SwipableMobileCarousel/SwipableMobileCarouselSlideParts.module.css +29 -0
- package/src/components/SwipableMobileCarousel/__tests__/SwipableMobileCarousel.test.tsx +133 -0
- package/src/components/SwipableMobileCarousel/__tests__/SwipableMobileCarouselDevGuard.test.tsx +61 -0
- package/src/components/SwipableMobileCarousel/context.ts +13 -0
- package/src/components/SwipableMobileCarouselDots/SwipableMobileCarouselDots.module.css +49 -0
- package/src/components/SwipableMobileCarouselDots/SwipableMobileCarouselDots.tsx +62 -0
- package/src/components/SwipableMobileCarouselDots/__tests__/SwipableMobileCarouselDots.test.tsx +63 -0
- package/src/hooks/__tests__/useCarouselRouteSync.test.tsx +126 -0
- package/src/hooks/__tests__/useIsOffline.test.tsx +53 -0
- package/src/hooks/__tests__/useOfflineTileState.test.tsx +69 -0
- package/src/hooks/__tests__/useResponsiveLayout.test.tsx +87 -0
- package/src/hooks/__tests__/useSnapCarousel.test.tsx +93 -0
- package/src/hooks/__tests__/useSwipeReveal.test.tsx +110 -0
- package/src/hooks/index.ts +18 -0
- package/src/hooks/useCarouselRouteSync.ts +92 -0
- package/src/hooks/useIsOffline.ts +41 -0
- package/src/hooks/useOfflineTileState.ts +45 -0
- package/src/hooks/useResponsiveLayout.ts +36 -0
- package/src/hooks/useSnapCarousel.ts +79 -0
- package/src/hooks/useSwipeReveal.ts +107 -0
- package/src/index.ts +63 -2
- package/src/overlay-shell.ts +94 -0
- package/src/stories/DesignSystemOverview.stories.tsx +322 -1
- package/src/stories/DeviceOnlyGate.stories.tsx +35 -0
- package/src/stories/DeviceStorageKeyGate.stories.tsx +57 -0
- package/src/stories/InteractionHooks.stories.tsx +483 -0
- package/src/stories/MobilePatterns.stories.tsx +247 -2
- package/src/stories/NavTabs.stories.tsx +18 -0
- package/src/stories/OfflineGate.stories.tsx +42 -0
- package/src/stories/PageContainer.stories.tsx +37 -0
- package/src/stories/PageHeader.stories.tsx +11 -0
- package/src/stories/ResponsiveSurface.stories.tsx +47 -0
- package/src/stories/SwipableMobileCarousel.stories.tsx +304 -0
- package/src/stories/TokenGallery.stories.tsx +65 -0
- package/src/tokens/primitives.css +25 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
3
|
+
import { SwipableMobileCarousel } from '../components/SwipableMobileCarousel/SwipableMobileCarousel';
|
|
4
|
+
import { SwipableMobileCarouselSlide } from '../components/SwipableMobileCarousel/SwipableMobileCarouselSlide';
|
|
5
|
+
import { SwipableMobileCarouselSlideHeader } from '../components/SwipableMobileCarousel/SwipableMobileCarouselSlideHeader';
|
|
6
|
+
import { SwipableMobileCarouselSlideBody } from '../components/SwipableMobileCarousel/SwipableMobileCarouselSlideBody';
|
|
7
|
+
import { SwipableMobileCarouselSlideFooter } from '../components/SwipableMobileCarousel/SwipableMobileCarouselSlideFooter';
|
|
8
|
+
import { SwipableMobileCarouselDots } from '../components/SwipableMobileCarouselDots/SwipableMobileCarouselDots';
|
|
9
|
+
|
|
10
|
+
const SLIDES = [
|
|
11
|
+
{ key: 'lists', label: 'Lists' },
|
|
12
|
+
{ key: 'groceries', label: 'Groceries' },
|
|
13
|
+
{ key: 'errands', label: 'Errands' },
|
|
14
|
+
{ key: 'work', label: 'Work' },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
function slideBodyStyle(): React.CSSProperties {
|
|
18
|
+
return {
|
|
19
|
+
display: 'flex',
|
|
20
|
+
alignItems: 'center',
|
|
21
|
+
justifyContent: 'center',
|
|
22
|
+
height: '100%',
|
|
23
|
+
fontFamily: 'var(--sv-font-family)',
|
|
24
|
+
color: 'var(--sv-color-text-primary)',
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function headerStyle(): React.CSSProperties {
|
|
29
|
+
return {
|
|
30
|
+
padding: 'var(--sv-space-4)',
|
|
31
|
+
fontFamily: 'var(--sv-font-family)',
|
|
32
|
+
fontWeight: 'var(--sv-font-weight-semibold)',
|
|
33
|
+
color: 'var(--sv-color-text-primary)',
|
|
34
|
+
borderBottom: '1px solid var(--sv-color-border)',
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function CarouselFrame({ children }: { children: React.ReactNode }) {
|
|
39
|
+
return (
|
|
40
|
+
<div
|
|
41
|
+
style={{
|
|
42
|
+
height: 480,
|
|
43
|
+
maxWidth: 375,
|
|
44
|
+
margin: '0 auto',
|
|
45
|
+
border: '1px solid var(--sv-color-border)',
|
|
46
|
+
borderRadius: 'var(--sv-radius-md)',
|
|
47
|
+
overflow: 'hidden',
|
|
48
|
+
}}
|
|
49
|
+
>
|
|
50
|
+
{children}
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function DefaultDemo() {
|
|
56
|
+
const [activeIndex, setActiveIndex] = useState(0);
|
|
57
|
+
return (
|
|
58
|
+
<CarouselFrame>
|
|
59
|
+
<SwipableMobileCarousel
|
|
60
|
+
activeIndex={activeIndex}
|
|
61
|
+
onSettle={setActiveIndex}
|
|
62
|
+
aria-label="Task lists"
|
|
63
|
+
>
|
|
64
|
+
{SLIDES.map((slide) => (
|
|
65
|
+
<SwipableMobileCarouselSlide key={slide.key} slideKey={slide.key} label={slide.label}>
|
|
66
|
+
<SwipableMobileCarouselSlideHeader>
|
|
67
|
+
<div style={headerStyle()}>{slide.label}</div>
|
|
68
|
+
</SwipableMobileCarouselSlideHeader>
|
|
69
|
+
<SwipableMobileCarouselSlideBody>
|
|
70
|
+
<div style={slideBodyStyle()}>{slide.label} content</div>
|
|
71
|
+
</SwipableMobileCarouselSlideBody>
|
|
72
|
+
</SwipableMobileCarouselSlide>
|
|
73
|
+
))}
|
|
74
|
+
</SwipableMobileCarousel>
|
|
75
|
+
</CarouselFrame>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function LazyLoadingBodyDemo() {
|
|
80
|
+
const [activeIndex, setActiveIndex] = useState(0);
|
|
81
|
+
const [loadedKeys, setLoadedKeys] = useState<Set<string>>(new Set(['lists']));
|
|
82
|
+
|
|
83
|
+
function simulateLoad(key: string) {
|
|
84
|
+
setTimeout(() => setLoadedKeys((s) => new Set(s).add(key)), 1200);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<CarouselFrame>
|
|
89
|
+
<SwipableMobileCarousel
|
|
90
|
+
activeIndex={activeIndex}
|
|
91
|
+
onSettle={(i) => {
|
|
92
|
+
setActiveIndex(i);
|
|
93
|
+
const key = SLIDES[i]?.key;
|
|
94
|
+
if (key && !loadedKeys.has(key)) simulateLoad(key);
|
|
95
|
+
}}
|
|
96
|
+
aria-label="Task lists (simulated fetch)"
|
|
97
|
+
>
|
|
98
|
+
{SLIDES.map((slide) => (
|
|
99
|
+
<SwipableMobileCarouselSlide key={slide.key} slideKey={slide.key} label={slide.label}>
|
|
100
|
+
{/* The title renders immediately from already-known metadata —
|
|
101
|
+
only the Body below waits on its own simulated 1.2s fetch. */}
|
|
102
|
+
<SwipableMobileCarouselSlideHeader>
|
|
103
|
+
<div style={headerStyle()}>{slide.label}</div>
|
|
104
|
+
</SwipableMobileCarouselSlideHeader>
|
|
105
|
+
<SwipableMobileCarouselSlideBody loading={!loadedKeys.has(slide.key)}>
|
|
106
|
+
<div style={slideBodyStyle()}>{slide.label} content (loaded)</div>
|
|
107
|
+
</SwipableMobileCarouselSlideBody>
|
|
108
|
+
</SwipableMobileCarouselSlide>
|
|
109
|
+
))}
|
|
110
|
+
</SwipableMobileCarousel>
|
|
111
|
+
</CarouselFrame>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function CustomIndicatorDemo() {
|
|
116
|
+
const [activeIndex, setActiveIndex] = useState(0);
|
|
117
|
+
return (
|
|
118
|
+
<CarouselFrame>
|
|
119
|
+
<SwipableMobileCarousel
|
|
120
|
+
activeIndex={activeIndex}
|
|
121
|
+
onSettle={setActiveIndex}
|
|
122
|
+
aria-label="Task lists"
|
|
123
|
+
renderIndicator={({ count, activeIndex: i, onJump }) => (
|
|
124
|
+
<div
|
|
125
|
+
style={{
|
|
126
|
+
position: 'absolute',
|
|
127
|
+
top: 8,
|
|
128
|
+
right: 8,
|
|
129
|
+
fontFamily: 'var(--sv-font-family)',
|
|
130
|
+
fontSize: 'var(--sv-font-size-sm)',
|
|
131
|
+
color: 'var(--sv-color-text-primary)',
|
|
132
|
+
background: 'var(--sv-color-surface-raised)',
|
|
133
|
+
borderRadius: 'var(--sv-radius-md)',
|
|
134
|
+
padding: 'var(--sv-space-1) var(--sv-space-2)',
|
|
135
|
+
}}
|
|
136
|
+
>
|
|
137
|
+
<button type="button" onClick={() => onJump(Math.max(0, i - 1))}>
|
|
138
|
+
‹
|
|
139
|
+
</button>{' '}
|
|
140
|
+
{i + 1} / {count}{' '}
|
|
141
|
+
<button type="button" onClick={() => onJump(Math.min(count - 1, i + 1))}>
|
|
142
|
+
›
|
|
143
|
+
</button>
|
|
144
|
+
</div>
|
|
145
|
+
)}
|
|
146
|
+
>
|
|
147
|
+
{SLIDES.map((slide) => (
|
|
148
|
+
<SwipableMobileCarouselSlide key={slide.key} slideKey={slide.key} label={slide.label}>
|
|
149
|
+
<SwipableMobileCarouselSlideHeader>
|
|
150
|
+
<div style={headerStyle()}>{slide.label}</div>
|
|
151
|
+
</SwipableMobileCarouselSlideHeader>
|
|
152
|
+
<SwipableMobileCarouselSlideBody>
|
|
153
|
+
<div style={slideBodyStyle()}>{slide.label} content</div>
|
|
154
|
+
</SwipableMobileCarouselSlideBody>
|
|
155
|
+
</SwipableMobileCarouselSlide>
|
|
156
|
+
))}
|
|
157
|
+
</SwipableMobileCarousel>
|
|
158
|
+
</CarouselFrame>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function NoIndicatorDemo() {
|
|
163
|
+
const [activeIndex, setActiveIndex] = useState(0);
|
|
164
|
+
return (
|
|
165
|
+
<CarouselFrame>
|
|
166
|
+
<SwipableMobileCarousel
|
|
167
|
+
activeIndex={activeIndex}
|
|
168
|
+
onSettle={setActiveIndex}
|
|
169
|
+
aria-label="Task lists"
|
|
170
|
+
renderIndicator={null}
|
|
171
|
+
>
|
|
172
|
+
{SLIDES.map((slide) => (
|
|
173
|
+
<SwipableMobileCarouselSlide key={slide.key} slideKey={slide.key} label={slide.label}>
|
|
174
|
+
<SwipableMobileCarouselSlideHeader>
|
|
175
|
+
<div style={headerStyle()}>{slide.label}</div>
|
|
176
|
+
</SwipableMobileCarouselSlideHeader>
|
|
177
|
+
<SwipableMobileCarouselSlideBody>
|
|
178
|
+
<div style={slideBodyStyle()}>{slide.label} content</div>
|
|
179
|
+
</SwipableMobileCarouselSlideBody>
|
|
180
|
+
</SwipableMobileCarouselSlide>
|
|
181
|
+
))}
|
|
182
|
+
</SwipableMobileCarousel>
|
|
183
|
+
</CarouselFrame>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function FooterDemo() {
|
|
188
|
+
const [activeIndex, setActiveIndex] = useState(0);
|
|
189
|
+
return (
|
|
190
|
+
<CarouselFrame>
|
|
191
|
+
<SwipableMobileCarousel
|
|
192
|
+
activeIndex={activeIndex}
|
|
193
|
+
onSettle={setActiveIndex}
|
|
194
|
+
aria-label="Task lists"
|
|
195
|
+
>
|
|
196
|
+
{SLIDES.map((slide) => (
|
|
197
|
+
<SwipableMobileCarouselSlide key={slide.key} slideKey={slide.key} label={slide.label}>
|
|
198
|
+
<SwipableMobileCarouselSlideHeader>
|
|
199
|
+
<div style={headerStyle()}>{slide.label}</div>
|
|
200
|
+
</SwipableMobileCarouselSlideHeader>
|
|
201
|
+
<SwipableMobileCarouselSlideBody>
|
|
202
|
+
<div style={slideBodyStyle()}>{slide.label} content</div>
|
|
203
|
+
</SwipableMobileCarouselSlideBody>
|
|
204
|
+
<SwipableMobileCarouselSlideFooter>
|
|
205
|
+
<div
|
|
206
|
+
style={{
|
|
207
|
+
padding: 'var(--sv-space-3) var(--sv-space-4)',
|
|
208
|
+
borderTop: '1px solid var(--sv-color-border)',
|
|
209
|
+
fontFamily: 'var(--sv-font-family)',
|
|
210
|
+
fontSize: 'var(--sv-font-size-sm)',
|
|
211
|
+
color: 'var(--sv-color-text-muted)',
|
|
212
|
+
}}
|
|
213
|
+
>
|
|
214
|
+
+ Add to {slide.label}
|
|
215
|
+
</div>
|
|
216
|
+
</SwipableMobileCarouselSlideFooter>
|
|
217
|
+
</SwipableMobileCarouselSlide>
|
|
218
|
+
))}
|
|
219
|
+
</SwipableMobileCarousel>
|
|
220
|
+
</CarouselFrame>
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function DotsStandaloneDemo() {
|
|
225
|
+
const [activeIndex, setActiveIndex] = useState(0);
|
|
226
|
+
return (
|
|
227
|
+
<SwipableMobileCarouselDots
|
|
228
|
+
count={4}
|
|
229
|
+
activeIndex={activeIndex}
|
|
230
|
+
onJump={setActiveIndex}
|
|
231
|
+
labels={['Lists', 'Groceries', 'Errands', 'Work']}
|
|
232
|
+
aria-label="Task lists"
|
|
233
|
+
/>
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const meta = {
|
|
238
|
+
title: 'Components/SwipableMobileCarousel',
|
|
239
|
+
component: SwipableMobileCarousel,
|
|
240
|
+
parameters: {
|
|
241
|
+
layout: 'padded',
|
|
242
|
+
docs: {
|
|
243
|
+
description: {
|
|
244
|
+
component:
|
|
245
|
+
"A compound component for swiping between full-width slides (each an independent route/view on mobile), wrapping useSnapCarousel. Owns rendering and mount-window mechanics only — it has no opinion on where slide data lives. Do NOT aggregate cross-slide data or mount a detail overlay (Sheet, Dialog) inside a Slide's children; see docs/design-system.md's carousel section.",
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
// Every story below supplies its own render(), which is stateful (activeIndex
|
|
250
|
+
// lives in useState) — these args only satisfy the required-prop typing.
|
|
251
|
+
args: {
|
|
252
|
+
activeIndex: 0,
|
|
253
|
+
onSettle: () => {},
|
|
254
|
+
'aria-label': 'Task lists',
|
|
255
|
+
children: null,
|
|
256
|
+
},
|
|
257
|
+
} satisfies Meta<typeof SwipableMobileCarousel>;
|
|
258
|
+
|
|
259
|
+
export default meta;
|
|
260
|
+
type Story = StoryObj<typeof meta>;
|
|
261
|
+
|
|
262
|
+
export const Default: Story = { render: () => <DefaultDemo /> };
|
|
263
|
+
|
|
264
|
+
export const LazyLoadingBody: Story = {
|
|
265
|
+
render: () => <LazyLoadingBodyDemo />,
|
|
266
|
+
parameters: {
|
|
267
|
+
docs: {
|
|
268
|
+
description: {
|
|
269
|
+
story:
|
|
270
|
+
'Swipe to a new slide — its title renders immediately from already-known metadata, while only the Body region shows a brief loading placeholder for its simulated 1.2s fetch. This is the fix for the "whole slide blanks out, title included, until its own fetch resolves" bug.',
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
export const CustomIndicator: Story = { render: () => <CustomIndicatorDemo /> };
|
|
277
|
+
|
|
278
|
+
export const NoIndicator: Story = { render: () => <NoIndicatorDemo /> };
|
|
279
|
+
|
|
280
|
+
export const WithFooter: Story = { render: () => <FooterDemo /> };
|
|
281
|
+
|
|
282
|
+
export const MobileViewport: Story = {
|
|
283
|
+
render: () => <DefaultDemo />,
|
|
284
|
+
parameters: { viewport: { defaultViewport: 'mobile1' } },
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
// SwipableMobileCarouselDots — standalone, also this carousel's default
|
|
289
|
+
// indicator. Grouped in this file rather than its own (Table's sub-parts
|
|
290
|
+
// share one story file too), though it has its own component subfolder and
|
|
291
|
+
// barrel export for independent reuse.
|
|
292
|
+
// ---------------------------------------------------------------------------
|
|
293
|
+
|
|
294
|
+
export const DotsStandalone: Story = {
|
|
295
|
+
render: () => <DotsStandaloneDemo />,
|
|
296
|
+
parameters: {
|
|
297
|
+
docs: {
|
|
298
|
+
description: {
|
|
299
|
+
story:
|
|
300
|
+
'SwipableMobileCarouselDots used on its own, outside a carousel — a real role="tablist"/role="tab" component (tappable, labeled, focusable), unlike the aria-hidden decorative dots both sovereign-tasks and sovereign-shopper currently hand-roll.',
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
};
|
|
@@ -109,6 +109,20 @@ const FONT_SIZE_TOKENS = [
|
|
|
109
109
|
'--sv-font-size-2xl',
|
|
110
110
|
];
|
|
111
111
|
|
|
112
|
+
// Task 10.2 — in-app text-size control. --sv-text-size-scale is theme-stable
|
|
113
|
+
// like --sv-radius-scale, but unlike radius (a calc() chain resolved once at
|
|
114
|
+
// :root's declaration site — see the RADIUS_PRESETS comment below), rem units
|
|
115
|
+
// genuinely re-resolve against the root element's live computed font-size, so
|
|
116
|
+
// these swatches compute the result directly in JS rather than needing a real
|
|
117
|
+
// [data-text-size] toggle on the document root.
|
|
118
|
+
const TEXT_SIZE_PRESETS = [
|
|
119
|
+
{ preset: 'default', scale: 1 },
|
|
120
|
+
{ preset: 'large', scale: 1.125 },
|
|
121
|
+
{ preset: 'larger', scale: 1.25 },
|
|
122
|
+
] as const;
|
|
123
|
+
|
|
124
|
+
const TEXT_SIZE_BASE_PX = { md: 16, lg: 18, '2xl': 24 };
|
|
125
|
+
|
|
112
126
|
const FONT_WEIGHT_TOKENS = [
|
|
113
127
|
'--sv-font-weight-normal',
|
|
114
128
|
'--sv-font-weight-medium',
|
|
@@ -247,6 +261,44 @@ function RadiusScaleRow({ token }: { token: string }) {
|
|
|
247
261
|
);
|
|
248
262
|
}
|
|
249
263
|
|
|
264
|
+
function TextSizePresetRow({ preset, scale }: { preset: string; scale: number }) {
|
|
265
|
+
return (
|
|
266
|
+
<div style={row}>
|
|
267
|
+
<div style={{ display: 'flex', gap: 12, alignItems: 'baseline', flexShrink: 0, width: 180 }}>
|
|
268
|
+
<span
|
|
269
|
+
style={{
|
|
270
|
+
fontSize: `${scale * TEXT_SIZE_BASE_PX.md}px`,
|
|
271
|
+
color: 'var(--sv-color-text-primary)',
|
|
272
|
+
}}
|
|
273
|
+
title="body (--sv-font-size-md)"
|
|
274
|
+
>
|
|
275
|
+
Aa
|
|
276
|
+
</span>
|
|
277
|
+
<span
|
|
278
|
+
style={{
|
|
279
|
+
fontSize: `${scale * TEXT_SIZE_BASE_PX.lg}px`,
|
|
280
|
+
color: 'var(--sv-color-text-primary)',
|
|
281
|
+
}}
|
|
282
|
+
title="large (--sv-font-size-lg)"
|
|
283
|
+
>
|
|
284
|
+
Aa
|
|
285
|
+
</span>
|
|
286
|
+
<span
|
|
287
|
+
style={{
|
|
288
|
+
fontSize: `${scale * TEXT_SIZE_BASE_PX['2xl']}px`,
|
|
289
|
+
color: 'var(--sv-color-text-primary)',
|
|
290
|
+
}}
|
|
291
|
+
title="heading (--sv-font-size-2xl)"
|
|
292
|
+
>
|
|
293
|
+
Aa
|
|
294
|
+
</span>
|
|
295
|
+
</div>
|
|
296
|
+
<span style={label}>{preset}</span>
|
|
297
|
+
<span style={value}>scale {scale}</span>
|
|
298
|
+
</div>
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
250
302
|
function RadiusPresetRow({ preset, scale }: { preset: string; scale: number }) {
|
|
251
303
|
return (
|
|
252
304
|
<div style={row}>
|
|
@@ -446,6 +498,19 @@ function TokenGalleryComponent() {
|
|
|
446
498
|
))}
|
|
447
499
|
</Section>
|
|
448
500
|
|
|
501
|
+
<Section title="Text-size presets (task 10.2)">
|
|
502
|
+
<p style={{ color: 'var(--sv-color-text-muted)', fontSize: 13, marginBottom: 12 }}>
|
|
503
|
+
Account → Preferences → Appearance. Overrides <code>--sv-text-size-scale</code> via{' '}
|
|
504
|
+
<code>[data-text-size]</code> on <code><html></code> — every rem-based token above
|
|
505
|
+
(font sizes, but also spacing and radii) scales with it, since the root element's
|
|
506
|
+
font-size is what rem resolves against. Discharges the accessibility debt from disabling
|
|
507
|
+
pinch-zoom (<code>runtime/app/layout.tsx</code>).
|
|
508
|
+
</p>
|
|
509
|
+
{TEXT_SIZE_PRESETS.map((p) => (
|
|
510
|
+
<TextSizePresetRow key={p.preset} preset={p.preset} scale={p.scale} />
|
|
511
|
+
))}
|
|
512
|
+
</Section>
|
|
513
|
+
|
|
449
514
|
<Section title="Radius scale">
|
|
450
515
|
{RADIUS_TOKENS.map((t) => (
|
|
451
516
|
<RadiusScaleRow key={t} token={t} />
|
|
@@ -77,6 +77,16 @@
|
|
|
77
77
|
--sv-font-weight-semibold: 600;
|
|
78
78
|
--sv-font-weight-bold: 700;
|
|
79
79
|
|
|
80
|
+
/* In-app text-size control (task 10.2), discharging the pinch-zoom-disabled
|
|
81
|
+
accessibility debt (runtime/app/layout.tsx sets userScalable: false).
|
|
82
|
+
Every --sv-font-size-* above is rem-based, so scaling the root font size
|
|
83
|
+
scales the whole type system with no per-component changes. Driven by
|
|
84
|
+
[data-text-size] on <html>, set pre-paint by runtime/src/theme-script.ts
|
|
85
|
+
from the `sv-text-size` cookie — same mechanism as [data-theme]. Like
|
|
86
|
+
--sv-radius-scale, this is a theme-stable scale token with no separate
|
|
87
|
+
semantic tier. */
|
|
88
|
+
--sv-text-size-scale: 1;
|
|
89
|
+
|
|
80
90
|
/* Icon sizes — scale tokens used directly by the Icon component */
|
|
81
91
|
--sv-icon-size-xs: 0.75rem; /* 12px — inline metadata (e.g. a due-date badge) */
|
|
82
92
|
--sv-icon-size-sm: 1rem; /* 16px — inline with body text */
|
|
@@ -130,3 +140,18 @@
|
|
|
130
140
|
1
|
|
131
141
|
); /* slight overshoot — playful emphasis, use sparingly */
|
|
132
142
|
}
|
|
143
|
+
|
|
144
|
+
/* Root font size scales with --sv-text-size-scale — see the token comment
|
|
145
|
+
above. calc(... * 100%) keeps the browser/OS text-zoom default intact at
|
|
146
|
+
scale 1 rather than hardcoding a px value. */
|
|
147
|
+
html {
|
|
148
|
+
font-size: calc(var(--sv-text-size-scale) * 100%);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
:root[data-text-size='large'] {
|
|
152
|
+
--sv-text-size-scale: 1.125; /* +12.5% */
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
:root[data-text-size='larger'] {
|
|
156
|
+
--sv-text-size-scale: 1.25; /* +25% */
|
|
157
|
+
}
|