@sovereignfs/ui 0.41.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.
Files changed (256) hide show
  1. package/dist/Accordion.module.css +3 -0
  2. package/dist/Alert.module.css +62 -0
  3. package/dist/AspectRatio.module.css +15 -0
  4. package/dist/Badge.module.css +75 -9
  5. package/dist/Breadcrumb.module.css +49 -0
  6. package/dist/Button.module.css +12 -3
  7. package/dist/ButtonGroup.module.css +29 -0
  8. package/dist/Card.module.css +6 -0
  9. package/dist/Checkbox.module.css +3 -1
  10. package/dist/Collapsible.module.css +70 -0
  11. package/dist/Combobox.module.css +122 -0
  12. package/dist/Command.module.css +80 -0
  13. package/dist/ConfirmDialog.module.css +5 -1
  14. package/dist/DataTable.module.css +28 -0
  15. package/dist/DatePicker.module.css +6 -2
  16. package/dist/Dialog.module.css +3 -0
  17. package/dist/HoverCard.module.css +3 -0
  18. package/dist/Input.module.css +6 -1
  19. package/dist/Item.module.css +70 -0
  20. package/dist/Kbd.module.css +14 -0
  21. package/dist/Label.module.css +11 -0
  22. package/dist/LegalLinks.module.css +24 -0
  23. package/dist/Markdown.module.css +49 -0
  24. package/dist/Marker.module.css +33 -0
  25. package/dist/Menu.module.css +8 -1
  26. package/dist/Message.module.css +99 -0
  27. package/dist/MessageScroller.module.css +54 -0
  28. package/dist/MobileAppsDrawer.module.css +81 -0
  29. package/dist/MobileFooter.module.css +73 -0
  30. package/dist/MobileHeader.module.css +63 -0
  31. package/dist/NavigationMenu.module.css +53 -0
  32. package/dist/PageContainer.module.css +20 -0
  33. package/dist/Pagination.module.css +98 -0
  34. package/dist/Progress.module.css +20 -0
  35. package/dist/RadioGroup.module.css +88 -0
  36. package/dist/Resizable.module.css +70 -0
  37. package/dist/ScrollArea.module.css +20 -0
  38. package/dist/Select.module.css +7 -3
  39. package/dist/Slider.module.css +95 -0
  40. package/dist/SwipableMobileCarousel.module.css +55 -0
  41. package/dist/SwipableMobileCarouselDots.module.css +49 -0
  42. package/dist/SwipableMobileCarouselSlideParts.module.css +29 -0
  43. package/dist/Table.module.css +45 -0
  44. package/dist/Tabs.module.css +6 -1
  45. package/dist/Toggle.module.css +6 -1
  46. package/dist/Typography.module.css +47 -0
  47. package/dist/index.d.ts +1268 -8
  48. package/dist/index.js +3046 -975
  49. package/dist/tokens/primitives.css +47 -8
  50. package/package.json +13 -9
  51. package/src/components/Accordion/Accordion.module.css +3 -0
  52. package/src/components/Accordion/Accordion.stories.tsx +110 -0
  53. package/src/components/Accordion/Accordion.tsx +53 -0
  54. package/src/components/Accordion/__tests__/Accordion.test.tsx +61 -0
  55. package/src/components/Alert/Alert.module.css +62 -0
  56. package/src/components/Alert/Alert.stories.tsx +96 -0
  57. package/src/components/Alert/Alert.tsx +58 -0
  58. package/src/components/Alert/__tests__/Alert.test.tsx +75 -0
  59. package/src/components/AspectRatio/AspectRatio.module.css +15 -0
  60. package/src/components/AspectRatio/AspectRatio.stories.tsx +78 -0
  61. package/src/components/AspectRatio/AspectRatio.tsx +21 -0
  62. package/src/components/AspectRatio/__tests__/AspectRatio.test.tsx +27 -0
  63. package/src/components/Badge/Badge.module.css +75 -9
  64. package/src/components/Badge/Badge.stories.tsx +109 -1
  65. package/src/components/Badge/Badge.tsx +31 -9
  66. package/src/components/Badge/__tests__/Badge.test.tsx +26 -2
  67. package/src/components/Breadcrumb/Breadcrumb.module.css +49 -0
  68. package/src/components/Breadcrumb/Breadcrumb.stories.tsx +47 -0
  69. package/src/components/Breadcrumb/Breadcrumb.tsx +57 -0
  70. package/src/components/Breadcrumb/__tests__/Breadcrumb.test.tsx +52 -0
  71. package/src/components/Button/Button.module.css +12 -3
  72. package/src/components/Button/Button.stories.tsx +3 -2
  73. package/src/components/Button/Button.tsx +1 -1
  74. package/src/components/Button/__tests__/Button.test.tsx +10 -0
  75. package/src/components/ButtonGroup/ButtonGroup.module.css +29 -0
  76. package/src/components/ButtonGroup/ButtonGroup.stories.tsx +57 -0
  77. package/src/components/ButtonGroup/ButtonGroup.tsx +27 -0
  78. package/src/components/ButtonGroup/__tests__/ButtonGroup.test.tsx +29 -0
  79. package/src/components/Card/Card.module.css +6 -0
  80. package/src/components/Checkbox/Checkbox.module.css +3 -1
  81. package/src/components/Collapsible/Collapsible.module.css +70 -0
  82. package/src/components/Collapsible/Collapsible.stories.tsx +62 -0
  83. package/src/components/Collapsible/Collapsible.tsx +66 -0
  84. package/src/components/Collapsible/__tests__/Collapsible.test.tsx +71 -0
  85. package/src/components/Combobox/Combobox.module.css +122 -0
  86. package/src/components/Combobox/Combobox.stories.tsx +79 -0
  87. package/src/components/Combobox/Combobox.tsx +186 -0
  88. package/src/components/Combobox/__tests__/Combobox.test.tsx +158 -0
  89. package/src/components/Command/Command.module.css +80 -0
  90. package/src/components/Command/Command.stories.tsx +104 -0
  91. package/src/components/Command/Command.tsx +168 -0
  92. package/src/components/Command/__tests__/Command.test.tsx +162 -0
  93. package/src/components/ConfirmDialog/ConfirmDialog.module.css +5 -1
  94. package/src/components/ContextMenu/ContextMenu.stories.tsx +81 -0
  95. package/src/components/ContextMenu/ContextMenu.tsx +100 -0
  96. package/src/components/ContextMenu/__tests__/ContextMenu.test.tsx +87 -0
  97. package/src/components/DataTable/DataTable.module.css +28 -0
  98. package/src/components/DataTable/DataTable.stories.tsx +74 -0
  99. package/src/components/DataTable/DataTable.tsx +154 -0
  100. package/src/components/DataTable/__tests__/DataTable.test.tsx +123 -0
  101. package/src/components/DatePicker/DatePicker.module.css +6 -2
  102. package/src/components/DeviceOnlyGate/DeviceOnlyGate.tsx +58 -0
  103. package/src/components/DeviceOnlyGate/__tests__/DeviceOnlyGate.test.tsx +45 -0
  104. package/src/components/DeviceStorageKeyGate/DeviceStorageKeyGate.tsx +100 -0
  105. package/src/components/DeviceStorageKeyGate/__tests__/DeviceStorageKeyGate.test.tsx +119 -0
  106. package/src/components/Dialog/Dialog.module.css +3 -0
  107. package/src/components/Dialog/Dialog.tsx +8 -59
  108. package/src/components/Drawer/Drawer.stories.tsx +44 -0
  109. package/src/components/Drawer/Drawer.tsx +18 -56
  110. package/src/components/Drawer/__tests__/Drawer.test.tsx +22 -0
  111. package/src/components/HoverCard/HoverCard.module.css +3 -0
  112. package/src/components/HoverCard/HoverCard.stories.tsx +66 -0
  113. package/src/components/HoverCard/HoverCard.tsx +111 -0
  114. package/src/components/HoverCard/__tests__/HoverCard.test.tsx +99 -0
  115. package/src/components/Icon/icons/circle-check.tsx +21 -0
  116. package/src/components/Icon/icons/circle-x.tsx +22 -0
  117. package/src/components/Icon/icons/index.ts +16 -0
  118. package/src/components/Icon/icons/menu.tsx +22 -0
  119. package/src/components/Icon/icons/smartphone.tsx +21 -0
  120. package/src/components/Input/Input.module.css +6 -1
  121. package/src/components/Item/Item.module.css +70 -0
  122. package/src/components/Item/Item.stories.tsx +62 -0
  123. package/src/components/Item/Item.tsx +54 -0
  124. package/src/components/Item/__tests__/Item.test.tsx +39 -0
  125. package/src/components/Kbd/Kbd.module.css +14 -0
  126. package/src/components/Kbd/Kbd.stories.tsx +32 -0
  127. package/src/components/Kbd/Kbd.tsx +12 -0
  128. package/src/components/Kbd/__tests__/Kbd.test.tsx +14 -0
  129. package/src/components/Label/Label.module.css +11 -0
  130. package/src/components/Label/Label.stories.tsx +40 -0
  131. package/src/components/Label/Label.tsx +24 -0
  132. package/src/components/Label/__tests__/Label.test.tsx +32 -0
  133. package/src/components/LegalLinks/LegalLinks.module.css +24 -0
  134. package/src/components/LegalLinks/LegalLinks.stories.tsx +22 -0
  135. package/src/components/LegalLinks/LegalLinks.tsx +40 -0
  136. package/src/components/Markdown/Markdown.module.css +49 -0
  137. package/src/components/Markdown/Markdown.stories.tsx +30 -0
  138. package/src/components/Markdown/Markdown.tsx +168 -0
  139. package/src/components/Marker/Marker.module.css +33 -0
  140. package/src/components/Marker/Marker.stories.tsx +39 -0
  141. package/src/components/Marker/Marker.tsx +34 -0
  142. package/src/components/Marker/__tests__/Marker.test.tsx +23 -0
  143. package/src/components/Menu/Menu.module.css +8 -1
  144. package/src/components/Menu/Menu.tsx +30 -8
  145. package/src/components/Menubar/Menubar.stories.tsx +52 -0
  146. package/src/components/Menubar/Menubar.tsx +42 -0
  147. package/src/components/Menubar/__tests__/Menubar.test.tsx +45 -0
  148. package/src/components/Message/Message.module.css +99 -0
  149. package/src/components/Message/Message.stories.tsx +79 -0
  150. package/src/components/Message/Message.tsx +41 -0
  151. package/src/components/Message/__tests__/Message.test.tsx +46 -0
  152. package/src/components/MessageScroller/MessageScroller.module.css +54 -0
  153. package/src/components/MessageScroller/MessageScroller.stories.tsx +84 -0
  154. package/src/components/MessageScroller/MessageScroller.tsx +72 -0
  155. package/src/components/MessageScroller/__tests__/MessageScroller.test.tsx +105 -0
  156. package/src/components/MobileAppsDrawer/MobileAppsDrawer.module.css +81 -0
  157. package/src/components/MobileAppsDrawer/MobileAppsDrawer.tsx +78 -0
  158. package/src/components/MobileAppsDrawer/__tests__/MobileAppsDrawer.test.tsx +80 -0
  159. package/src/components/MobileFooter/MobileFooter.module.css +73 -0
  160. package/src/components/MobileFooter/MobileFooter.stories.tsx +47 -0
  161. package/src/components/MobileFooter/MobileFooter.tsx +109 -0
  162. package/src/components/MobileFooter/__tests__/MobileFooter.test.tsx +71 -0
  163. package/src/components/MobileHeader/MobileHeader.module.css +63 -0
  164. package/src/components/MobileHeader/MobileHeader.stories.tsx +57 -0
  165. package/src/components/MobileHeader/MobileHeader.tsx +77 -0
  166. package/src/components/MobileHeader/__tests__/MobileHeader.test.tsx +58 -0
  167. package/src/components/NavTabs/NavTabs.tsx +39 -11
  168. package/src/components/NavigationMenu/NavigationMenu.module.css +53 -0
  169. package/src/components/NavigationMenu/NavigationMenu.stories.tsx +58 -0
  170. package/src/components/NavigationMenu/NavigationMenu.tsx +125 -0
  171. package/src/components/NavigationMenu/__tests__/NavigationMenu.test.tsx +93 -0
  172. package/src/components/OfflineGate/OfflineGate.tsx +44 -0
  173. package/src/components/OfflineGate/__tests__/OfflineGate.test.tsx +66 -0
  174. package/src/components/PageContainer/PageContainer.module.css +20 -0
  175. package/src/components/PageContainer/PageContainer.tsx +31 -0
  176. package/src/components/PageContainer/__tests__/PageContainer.test.tsx +37 -0
  177. package/src/components/PageHeader/PageHeader.tsx +16 -2
  178. package/src/components/Pagination/Pagination.module.css +98 -0
  179. package/src/components/Pagination/Pagination.stories.tsx +49 -0
  180. package/src/components/Pagination/Pagination.tsx +87 -0
  181. package/src/components/Pagination/__tests__/Pagination.test.tsx +56 -0
  182. package/src/components/Progress/Progress.module.css +20 -0
  183. package/src/components/Progress/Progress.stories.tsx +40 -0
  184. package/src/components/Progress/Progress.tsx +35 -0
  185. package/src/components/Progress/__tests__/Progress.test.tsx +31 -0
  186. package/src/components/RadioGroup/RadioGroup.module.css +88 -0
  187. package/src/components/RadioGroup/RadioGroup.stories.tsx +69 -0
  188. package/src/components/RadioGroup/RadioGroup.tsx +99 -0
  189. package/src/components/RadioGroup/__tests__/RadioGroup.test.tsx +59 -0
  190. package/src/components/Resizable/Resizable.module.css +70 -0
  191. package/src/components/Resizable/Resizable.stories.tsx +82 -0
  192. package/src/components/Resizable/Resizable.tsx +313 -0
  193. package/src/components/Resizable/__tests__/Resizable.test.tsx +128 -0
  194. package/src/components/ResponsiveSurface/ResponsiveSurface.tsx +24 -0
  195. package/src/components/ResponsiveSurface/__tests__/ResponsiveSurface.test.tsx +73 -0
  196. package/src/components/ScrollArea/ScrollArea.module.css +20 -0
  197. package/src/components/ScrollArea/ScrollArea.stories.tsx +57 -0
  198. package/src/components/ScrollArea/ScrollArea.tsx +27 -0
  199. package/src/components/ScrollArea/__tests__/ScrollArea.test.tsx +39 -0
  200. package/src/components/Select/Select.module.css +7 -3
  201. package/src/components/Sheet/Sheet.tsx +9 -51
  202. package/src/components/Slider/Slider.module.css +95 -0
  203. package/src/components/Slider/Slider.stories.tsx +64 -0
  204. package/src/components/Slider/Slider.tsx +77 -0
  205. package/src/components/Slider/__tests__/Slider.test.tsx +45 -0
  206. package/src/components/SwipableMobileCarousel/SwipableMobileCarousel.module.css +55 -0
  207. package/src/components/SwipableMobileCarousel/SwipableMobileCarousel.tsx +227 -0
  208. package/src/components/SwipableMobileCarousel/SwipableMobileCarouselSlide.tsx +35 -0
  209. package/src/components/SwipableMobileCarousel/SwipableMobileCarouselSlideBody.tsx +42 -0
  210. package/src/components/SwipableMobileCarousel/SwipableMobileCarouselSlideFooter.tsx +18 -0
  211. package/src/components/SwipableMobileCarousel/SwipableMobileCarouselSlideHeader.tsx +21 -0
  212. package/src/components/SwipableMobileCarousel/SwipableMobileCarouselSlideParts.module.css +29 -0
  213. package/src/components/SwipableMobileCarousel/__tests__/SwipableMobileCarousel.test.tsx +133 -0
  214. package/src/components/SwipableMobileCarousel/__tests__/SwipableMobileCarouselDevGuard.test.tsx +61 -0
  215. package/src/components/SwipableMobileCarousel/context.ts +13 -0
  216. package/src/components/SwipableMobileCarouselDots/SwipableMobileCarouselDots.module.css +49 -0
  217. package/src/components/SwipableMobileCarouselDots/SwipableMobileCarouselDots.tsx +62 -0
  218. package/src/components/SwipableMobileCarouselDots/__tests__/SwipableMobileCarouselDots.test.tsx +63 -0
  219. package/src/components/Table/Table.module.css +45 -0
  220. package/src/components/Table/Table.stories.tsx +125 -0
  221. package/src/components/Table/Table.tsx +51 -0
  222. package/src/components/Table/__tests__/Table.test.tsx +50 -0
  223. package/src/components/Tabs/Tabs.module.css +6 -1
  224. package/src/components/Toggle/Toggle.module.css +6 -1
  225. package/src/components/Typography/Typography.module.css +47 -0
  226. package/src/components/Typography/Typography.stories.tsx +62 -0
  227. package/src/components/Typography/Typography.tsx +30 -0
  228. package/src/components/Typography/__tests__/Typography.test.tsx +34 -0
  229. package/src/hooks/__tests__/useCarouselRouteSync.test.tsx +126 -0
  230. package/src/hooks/__tests__/useIsOffline.test.tsx +53 -0
  231. package/src/hooks/__tests__/useOfflineTileState.test.tsx +69 -0
  232. package/src/hooks/__tests__/useResponsiveLayout.test.tsx +87 -0
  233. package/src/hooks/__tests__/useSnapCarousel.test.tsx +93 -0
  234. package/src/hooks/__tests__/useSwipeReveal.test.tsx +110 -0
  235. package/src/hooks/index.ts +18 -0
  236. package/src/hooks/useCarouselRouteSync.ts +92 -0
  237. package/src/hooks/useIsOffline.ts +41 -0
  238. package/src/hooks/useOfflineTileState.ts +45 -0
  239. package/src/hooks/useResponsiveLayout.ts +36 -0
  240. package/src/hooks/useSnapCarousel.ts +79 -0
  241. package/src/hooks/useSwipeReveal.ts +107 -0
  242. package/src/index.ts +141 -3
  243. package/src/overlay-shell.ts +94 -0
  244. package/src/stories/DesignSystemOverview.stories.tsx +936 -3
  245. package/src/stories/DeviceOnlyGate.stories.tsx +35 -0
  246. package/src/stories/DeviceStorageKeyGate.stories.tsx +57 -0
  247. package/src/stories/InteractionHooks.stories.tsx +483 -0
  248. package/src/stories/MobilePatterns.stories.tsx +255 -4
  249. package/src/stories/NavTabs.stories.tsx +18 -0
  250. package/src/stories/OfflineGate.stories.tsx +42 -0
  251. package/src/stories/PageContainer.stories.tsx +37 -0
  252. package/src/stories/PageHeader.stories.tsx +11 -0
  253. package/src/stories/ResponsiveSurface.stories.tsx +47 -0
  254. package/src/stories/SwipableMobileCarousel.stories.tsx +304 -0
  255. package/src/stories/TokenGallery.stories.tsx +177 -16
  256. package/src/tokens/primitives.css +47 -8
@@ -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
+ };
@@ -1,4 +1,4 @@
1
- import { useEffect, useState } from 'react';
1
+ import { useEffect, useRef, useState } from 'react';
2
2
  import type { Meta, StoryObj } from '@storybook/react-vite';
3
3
 
4
4
  // ---------------------------------------------------------------------------
@@ -63,6 +63,34 @@ const RADIUS_TOKENS = [
63
63
  '--sv-radius-3xl',
64
64
  '--sv-radius-full',
65
65
  ];
66
+ // RFC 0077 — instance corner-radius presets. Base px values mirror
67
+ // primitives.css's own comments (sm 6px, md 8px, xl 12px at scale 1).
68
+ //
69
+ // Each swatch's radius is computed here in JS rather than by overriding
70
+ // --sv-radius-scale on a local wrapper and letting --sv-radius-md etc.
71
+ // recompute from it. That would be the more elegant approach, but CSS custom
72
+ // properties don't support it: a custom property that references another
73
+ // custom property inside calc() (--sv-radius-md: calc(var(--sv-radius-scale)
74
+ // * 0.5rem)) has that inner reference resolved once, at the element where
75
+ // --sv-radius-md itself is *declared* (:root in primitives.css) — not at
76
+ // wherever it's later consumed via var(). A descendant overriding
77
+ // --sv-radius-scale doesn't reach back and change --sv-radius-md's
78
+ // already-fixed value (confirmed empirically — this isn't a guess). The
79
+ // production mechanism (instance-provider.tsx) still works fine: its
80
+ // override is injected as another :root { } rule, the *same* cascade node
81
+ // primitives.css declares --sv-radius-md at, not a descendant — but a
82
+ // side-by-side, multi-preset comparison on one page has no such single
83
+ // :root to share, so the swatches below compute the resulting px directly.
84
+ const RADIUS_PRESETS = [
85
+ { preset: 'none', scale: 0 },
86
+ { preset: 'xs', scale: 0.35 },
87
+ { preset: 's', scale: 0.65 },
88
+ { preset: 'm', scale: 1 },
89
+ { preset: 'l', scale: 2.75 },
90
+ ] as const;
91
+
92
+ const RADIUS_BASE_PX = { sm: 6, md: 8, xl: 12 };
93
+
66
94
  const ICON_SIZE_TOKENS = [
67
95
  '--sv-icon-size-xs',
68
96
  '--sv-icon-size-sm',
@@ -81,6 +109,20 @@ const FONT_SIZE_TOKENS = [
81
109
  '--sv-font-size-2xl',
82
110
  ];
83
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
+
84
126
  const FONT_WEIGHT_TOKENS = [
85
127
  '--sv-font-weight-normal',
86
128
  '--sv-font-weight-medium',
@@ -189,6 +231,113 @@ function ScaleRow({
189
231
  );
190
232
  }
191
233
 
234
+ // Radius tokens are calc() expressions since RFC 0077 (proportional to
235
+ // --sv-radius-scale) — reading the raw custom property text now shows the
236
+ // unresolved formula (e.g. "calc(1 * 0.375rem)"), not a pixel value. Render
237
+ // the swatch, then read the browser's own computed border-radius off it
238
+ // (a real CSS property, so calc() is fully resolved) instead of ScaleRow's
239
+ // getPropertyValue text read.
240
+ function RadiusScaleRow({ token }: { token: string }) {
241
+ const ref = useRef<HTMLDivElement>(null);
242
+ const [resolved, setResolved] = useState('');
243
+ useEffect(() => {
244
+ if (ref.current) setResolved(getComputedStyle(ref.current).borderRadius);
245
+ }, []);
246
+ return (
247
+ <div style={row}>
248
+ <div
249
+ ref={ref}
250
+ style={{
251
+ width: 40,
252
+ height: 24,
253
+ background: 'var(--sv-color-accent)',
254
+ borderRadius: `var(${token})`,
255
+ flexShrink: 0,
256
+ }}
257
+ />
258
+ <span style={label}>{token}</span>
259
+ <span style={value}>{resolved}</span>
260
+ </div>
261
+ );
262
+ }
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
+
302
+ function RadiusPresetRow({ preset, scale }: { preset: string; scale: number }) {
303
+ return (
304
+ <div style={row}>
305
+ <div style={{ display: 'flex', gap: 12, alignItems: 'center', flexShrink: 0 }}>
306
+ <div
307
+ style={{
308
+ width: 56,
309
+ height: 40,
310
+ background: 'var(--sv-color-surface-sunken)',
311
+ border: '1px solid var(--sv-color-border)',
312
+ borderRadius: `${scale * RADIUS_BASE_PX.xl}px`,
313
+ }}
314
+ title="card (--sv-radius-xl)"
315
+ />
316
+ <div
317
+ style={{
318
+ width: 64,
319
+ height: 36,
320
+ background: 'var(--sv-color-accent)',
321
+ borderRadius: `${scale * RADIUS_BASE_PX.md}px`,
322
+ }}
323
+ title="button, 36px tall (--sv-radius-md)"
324
+ />
325
+ <div
326
+ style={{
327
+ width: 48,
328
+ height: 18,
329
+ background: 'var(--sv-color-accent-subtle)',
330
+ borderRadius: `${scale * RADIUS_BASE_PX.sm}px`,
331
+ }}
332
+ title="badge, 18px tall (--sv-radius-sm)"
333
+ />
334
+ </div>
335
+ <span style={label}>{preset}</span>
336
+ <span style={value}>scale {scale}</span>
337
+ </div>
338
+ );
339
+ }
340
+
192
341
  // Auto-loops so the duration/easing is visible without needing to interact —
193
342
  // same technique Dialog/Drawer use for their own open/close transition
194
343
  // (see motion.ts), just driven by a timer instead of an `open` prop.
@@ -349,23 +498,35 @@ function TokenGalleryComponent() {
349
498
  ))}
350
499
  </Section>
351
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>&lt;html&gt;</code> — every rem-based token above
505
+ (font sizes, but also spacing and radii) scales with it, since the root element&apos;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
+
352
514
  <Section title="Radius scale">
353
515
  {RADIUS_TOKENS.map((t) => (
354
- <ScaleRow
355
- key={t}
356
- token={t}
357
- renderPreview={(_v) => (
358
- <div
359
- style={{
360
- width: 40,
361
- height: 24,
362
- background: 'var(--sv-color-accent)',
363
- borderRadius: `var(${t})`,
364
- flexShrink: 0,
365
- }}
366
- />
367
- )}
368
- />
516
+ <RadiusScaleRow key={t} token={t} />
517
+ ))}
518
+ </Section>
519
+
520
+ <Section title="Radius presets (RFC 0077)">
521
+ <p style={{ color: 'var(--sv-color-text-muted)', fontSize: 13, marginBottom: 12 }}>
522
+ Instance-level corner-radius intensity — Console → Instance identity, or{' '}
523
+ <code>INSTANCE_RADIUS</code>. Overrides <code>--sv-radius-scale</code>; every{' '}
524
+ <code>--sv-radius-*</code> token above stays proportional to it. Note how the button
525
+ swatch clips to a full pill by L — <code>border-radius</code>&apos;s own half-height
526
+ clamp, not a separate mechanism.
527
+ </p>
528
+ {RADIUS_PRESETS.map((p) => (
529
+ <RadiusPresetRow key={p.preset} preset={p.preset} scale={p.scale} />
369
530
  ))}
370
531
  </Section>
371
532
 
@@ -77,20 +77,44 @@
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 */
83
93
  --sv-icon-size-md: 1.25rem; /* 20px — standard affordance */
84
94
  --sv-icon-size-lg: 1.5rem; /* 24px — prominent / standalone */
85
95
 
86
- /* Border radii (v3.1 spec values) */
87
- --sv-radius-sm: 0.375rem; /* 6px — badge, tag */
88
- --sv-radius-md: 0.5rem; /* 8px — button, input */
89
- --sv-radius-lg: 0.6875rem; /* 11px — sidebar icon, card */
90
- --sv-radius-xl: 0.75rem; /* 12px — card, panel */
91
- --sv-radius-2xl: 0.875rem; /* 14px — popover */
92
- --sv-radius-3xl: 1.25rem; /* 20px — bottom sheet (Drawer top corners) */
93
- --sv-radius-full: 9999px; /* pill / avatar */
96
+ /* Border radii (v3.1 spec values). --sv-radius-scale is the one
97
+ instance-controllable exception to "scale tokens are theme-stable"
98
+ (RFC 0077) — an operator's corner-radius preset (None/XS/S/M/L)
99
+ overrides it at :root via the InstanceProvider style injection
100
+ (runtime/src/instance-provider.tsx), same pipeline as --sv-color-accent.
101
+ Every corner token below is proportional to it, so a plugin author never
102
+ references --sv-radius-scale directly — they keep using --sv-radius-sm
103
+ etc. exactly as before. --sv-radius-full is deliberately NOT derived
104
+ from the scale: it's a shape token (circle/pill), not a corner-rounding
105
+ one, and stays fixed regardless of the instance preset. */
106
+ --sv-radius-scale: 1;
107
+ --sv-radius-sm: calc(var(--sv-radius-scale) * 0.375rem); /* 6px @ scale 1 — badge, tag */
108
+ --sv-radius-md: calc(var(--sv-radius-scale) * 0.5rem); /* 8px @ scale 1 — button, input */
109
+ --sv-radius-lg: calc(
110
+ var(--sv-radius-scale) * 0.6875rem
111
+ ); /* 11px @ scale 1 — sidebar icon, card */
112
+ --sv-radius-xl: calc(var(--sv-radius-scale) * 0.75rem); /* 12px @ scale 1 — card, panel */
113
+ --sv-radius-2xl: calc(var(--sv-radius-scale) * 0.875rem); /* 14px @ scale 1 — popover */
114
+ --sv-radius-3xl: calc(
115
+ var(--sv-radius-scale) * 1.25rem
116
+ ); /* 20px @ scale 1 — bottom sheet (Drawer top corners) */
117
+ --sv-radius-full: 9999px; /* pill / avatar — unaffected by --sv-radius-scale */
94
118
 
95
119
  /* Touch targets — minimum hit-area dimension for interactive controls.
96
120
  44px follows the WCAG 2.5.5 / Apple HIG / Material guideline for reliable
@@ -116,3 +140,18 @@
116
140
  1
117
141
  ); /* slight overshoot — playful emphasis, use sparingly */
118
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
+ }