@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.
Files changed (93) hide show
  1. package/dist/LegalLinks.module.css +24 -0
  2. package/dist/Markdown.module.css +49 -0
  3. package/dist/MobileAppsDrawer.module.css +81 -0
  4. package/dist/MobileFooter.module.css +73 -0
  5. package/dist/MobileHeader.module.css +63 -0
  6. package/dist/PageContainer.module.css +20 -0
  7. package/dist/SwipableMobileCarousel.module.css +55 -0
  8. package/dist/SwipableMobileCarouselDots.module.css +49 -0
  9. package/dist/SwipableMobileCarouselSlideParts.module.css +29 -0
  10. package/dist/index.d.ts +654 -6
  11. package/dist/index.js +1856 -1133
  12. package/dist/tokens/primitives.css +25 -0
  13. package/package.json +13 -9
  14. package/src/components/Badge/Badge.tsx +1 -7
  15. package/src/components/DeviceOnlyGate/DeviceOnlyGate.tsx +58 -0
  16. package/src/components/DeviceOnlyGate/__tests__/DeviceOnlyGate.test.tsx +45 -0
  17. package/src/components/DeviceStorageKeyGate/DeviceStorageKeyGate.tsx +100 -0
  18. package/src/components/DeviceStorageKeyGate/__tests__/DeviceStorageKeyGate.test.tsx +119 -0
  19. package/src/components/Dialog/Dialog.tsx +8 -59
  20. package/src/components/Drawer/Drawer.stories.tsx +44 -0
  21. package/src/components/Drawer/Drawer.tsx +18 -56
  22. package/src/components/Drawer/__tests__/Drawer.test.tsx +22 -0
  23. package/src/components/Icon/icons/index.ts +8 -0
  24. package/src/components/Icon/icons/menu.tsx +22 -0
  25. package/src/components/Icon/icons/smartphone.tsx +21 -0
  26. package/src/components/LegalLinks/LegalLinks.module.css +24 -0
  27. package/src/components/LegalLinks/LegalLinks.stories.tsx +22 -0
  28. package/src/components/LegalLinks/LegalLinks.tsx +40 -0
  29. package/src/components/Markdown/Markdown.module.css +49 -0
  30. package/src/components/Markdown/Markdown.stories.tsx +30 -0
  31. package/src/components/Markdown/Markdown.tsx +168 -0
  32. package/src/components/MobileAppsDrawer/MobileAppsDrawer.module.css +81 -0
  33. package/src/components/MobileAppsDrawer/MobileAppsDrawer.tsx +78 -0
  34. package/src/components/MobileAppsDrawer/__tests__/MobileAppsDrawer.test.tsx +80 -0
  35. package/src/components/MobileFooter/MobileFooter.module.css +73 -0
  36. package/src/components/MobileFooter/MobileFooter.stories.tsx +47 -0
  37. package/src/components/MobileFooter/MobileFooter.tsx +109 -0
  38. package/src/components/MobileFooter/__tests__/MobileFooter.test.tsx +71 -0
  39. package/src/components/MobileHeader/MobileHeader.module.css +63 -0
  40. package/src/components/MobileHeader/MobileHeader.stories.tsx +57 -0
  41. package/src/components/MobileHeader/MobileHeader.tsx +77 -0
  42. package/src/components/MobileHeader/__tests__/MobileHeader.test.tsx +58 -0
  43. package/src/components/NavTabs/NavTabs.tsx +39 -11
  44. package/src/components/OfflineGate/OfflineGate.tsx +44 -0
  45. package/src/components/OfflineGate/__tests__/OfflineGate.test.tsx +66 -0
  46. package/src/components/PageContainer/PageContainer.module.css +20 -0
  47. package/src/components/PageContainer/PageContainer.tsx +31 -0
  48. package/src/components/PageContainer/__tests__/PageContainer.test.tsx +37 -0
  49. package/src/components/PageHeader/PageHeader.tsx +16 -2
  50. package/src/components/ResponsiveSurface/ResponsiveSurface.tsx +24 -0
  51. package/src/components/ResponsiveSurface/__tests__/ResponsiveSurface.test.tsx +73 -0
  52. package/src/components/Sheet/Sheet.tsx +9 -51
  53. package/src/components/SwipableMobileCarousel/SwipableMobileCarousel.module.css +55 -0
  54. package/src/components/SwipableMobileCarousel/SwipableMobileCarousel.tsx +227 -0
  55. package/src/components/SwipableMobileCarousel/SwipableMobileCarouselSlide.tsx +35 -0
  56. package/src/components/SwipableMobileCarousel/SwipableMobileCarouselSlideBody.tsx +42 -0
  57. package/src/components/SwipableMobileCarousel/SwipableMobileCarouselSlideFooter.tsx +18 -0
  58. package/src/components/SwipableMobileCarousel/SwipableMobileCarouselSlideHeader.tsx +21 -0
  59. package/src/components/SwipableMobileCarousel/SwipableMobileCarouselSlideParts.module.css +29 -0
  60. package/src/components/SwipableMobileCarousel/__tests__/SwipableMobileCarousel.test.tsx +133 -0
  61. package/src/components/SwipableMobileCarousel/__tests__/SwipableMobileCarouselDevGuard.test.tsx +61 -0
  62. package/src/components/SwipableMobileCarousel/context.ts +13 -0
  63. package/src/components/SwipableMobileCarouselDots/SwipableMobileCarouselDots.module.css +49 -0
  64. package/src/components/SwipableMobileCarouselDots/SwipableMobileCarouselDots.tsx +62 -0
  65. package/src/components/SwipableMobileCarouselDots/__tests__/SwipableMobileCarouselDots.test.tsx +63 -0
  66. package/src/hooks/__tests__/useCarouselRouteSync.test.tsx +126 -0
  67. package/src/hooks/__tests__/useIsOffline.test.tsx +53 -0
  68. package/src/hooks/__tests__/useOfflineTileState.test.tsx +69 -0
  69. package/src/hooks/__tests__/useResponsiveLayout.test.tsx +87 -0
  70. package/src/hooks/__tests__/useSnapCarousel.test.tsx +93 -0
  71. package/src/hooks/__tests__/useSwipeReveal.test.tsx +110 -0
  72. package/src/hooks/index.ts +18 -0
  73. package/src/hooks/useCarouselRouteSync.ts +92 -0
  74. package/src/hooks/useIsOffline.ts +41 -0
  75. package/src/hooks/useOfflineTileState.ts +45 -0
  76. package/src/hooks/useResponsiveLayout.ts +36 -0
  77. package/src/hooks/useSnapCarousel.ts +79 -0
  78. package/src/hooks/useSwipeReveal.ts +107 -0
  79. package/src/index.ts +63 -2
  80. package/src/overlay-shell.ts +94 -0
  81. package/src/stories/DesignSystemOverview.stories.tsx +322 -1
  82. package/src/stories/DeviceOnlyGate.stories.tsx +35 -0
  83. package/src/stories/DeviceStorageKeyGate.stories.tsx +57 -0
  84. package/src/stories/InteractionHooks.stories.tsx +483 -0
  85. package/src/stories/MobilePatterns.stories.tsx +247 -2
  86. package/src/stories/NavTabs.stories.tsx +18 -0
  87. package/src/stories/OfflineGate.stories.tsx +42 -0
  88. package/src/stories/PageContainer.stories.tsx +37 -0
  89. package/src/stories/PageHeader.stories.tsx +11 -0
  90. package/src/stories/ResponsiveSurface.stories.tsx +47 -0
  91. package/src/stories/SwipableMobileCarousel.stories.tsx +304 -0
  92. package/src/stories/TokenGallery.stories.tsx +65 -0
  93. package/src/tokens/primitives.css +25 -0
@@ -8,6 +8,9 @@ import { Card } from '../components/Card/Card';
8
8
  import { Dialog } from '../components/Dialog/Dialog';
9
9
  import { Drawer } from '../components/Drawer/Drawer';
10
10
  import { EmptyState } from '../components/EmptyState/EmptyState';
11
+ import { OfflineGate } from '../components/OfflineGate/OfflineGate';
12
+ import { DeviceOnlyGate } from '../components/DeviceOnlyGate/DeviceOnlyGate';
13
+ import { DeviceStorageKeyGate } from '../components/DeviceStorageKeyGate/DeviceStorageKeyGate';
11
14
  import { FormField } from '../components/FormField/FormField';
12
15
  import { Icon } from '../components/Icon/Icon';
13
16
  import type { IconName } from '../components/Icon/Icon';
@@ -21,9 +24,19 @@ import { StatusBadge } from '../components/StatusBadge/StatusBadge';
21
24
  import { SplitPane } from '../components/SplitPane/SplitPane';
22
25
  import { TagInput } from '../components/TagInput/TagInput';
23
26
  import { SuggestionInput } from '../components/SuggestionInput/SuggestionInput';
27
+ import { MobileAppsDrawer } from '../components/MobileAppsDrawer/MobileAppsDrawer';
28
+ import { MobileFooter } from '../components/MobileFooter/MobileFooter';
29
+ import { MobileHeader } from '../components/MobileHeader/MobileHeader';
24
30
  import { NavTabs } from '../components/NavTabs/NavTabs';
25
31
  import { PageHeader } from '../components/PageHeader/PageHeader';
32
+ import { PageContainer } from '../components/PageContainer/PageContainer';
26
33
  import { Popover } from '../components/Popover/Popover';
34
+ import { ResponsiveSurface } from '../components/ResponsiveSurface/ResponsiveSurface';
35
+ import { SwipableMobileCarousel } from '../components/SwipableMobileCarousel/SwipableMobileCarousel';
36
+ import { SwipableMobileCarouselSlide } from '../components/SwipableMobileCarousel/SwipableMobileCarouselSlide';
37
+ import { SwipableMobileCarouselSlideHeader } from '../components/SwipableMobileCarousel/SwipableMobileCarouselSlideHeader';
38
+ import { SwipableMobileCarouselSlideBody } from '../components/SwipableMobileCarousel/SwipableMobileCarouselSlideBody';
39
+ import { SwipableMobileCarouselDots } from '../components/SwipableMobileCarouselDots/SwipableMobileCarouselDots';
27
40
  import { SegmentedControl } from '../components/SegmentedControl/SegmentedControl';
28
41
  import { Select } from '../components/Select/Select';
29
42
  import { Spinner } from '../components/Spinner/Spinner';
@@ -46,6 +59,8 @@ import {
46
59
  } from '../components/Table/Table';
47
60
  import { Alert } from '../components/Alert/Alert';
48
61
  import { Breadcrumb } from '../components/Breadcrumb/Breadcrumb';
62
+ import { LegalLinks } from '../components/LegalLinks/LegalLinks';
63
+ import { Markdown } from '../components/Markdown/Markdown';
49
64
  import { Pagination } from '../components/Pagination/Pagination';
50
65
  import { Kbd } from '../components/Kbd/Kbd';
51
66
  import { Accordion } from '../components/Accordion/Accordion';
@@ -631,6 +646,28 @@ function DrawerDemo() {
631
646
  );
632
647
  }
633
648
 
649
+ function MobileAppsDrawerDemo() {
650
+ const [open, setOpen] = useState(false);
651
+ const items = [
652
+ { key: 'home', icon: <Icon name="house" size="lg" aria-hidden />, label: 'Home' },
653
+ { key: 'notes', icon: <Icon name="file" size="lg" aria-hidden />, label: 'Notes' },
654
+ { key: 'gallery', icon: <Icon name="grid-2x2" size="lg" aria-hidden />, label: 'Gallery' },
655
+ ];
656
+ return (
657
+ <>
658
+ <Button size="sm" variant="secondary" onClick={() => setOpen(true)}>
659
+ Open drawer
660
+ </Button>
661
+ <MobileAppsDrawer
662
+ open={open}
663
+ onClose={() => setOpen(false)}
664
+ aria-label="Sections"
665
+ items={items.map((item) => ({ ...item, onClick: () => setOpen(false) }))}
666
+ />
667
+ </>
668
+ );
669
+ }
670
+
634
671
  function PopoverDemo() {
635
672
  const [open, setOpen] = useState(false);
636
673
  return (
@@ -921,6 +958,100 @@ function FileDropzoneDemo() {
921
958
  );
922
959
  }
923
960
 
961
+ function ResponsiveSurfaceDemo() {
962
+ return (
963
+ <div
964
+ style={{
965
+ border: '1px dashed var(--sv-color-border)',
966
+ borderRadius: 'var(--sv-radius-md)',
967
+ padding: 'var(--sv-space-4)',
968
+ fontFamily: ff,
969
+ fontSize: 'var(--sv-font-size-sm)',
970
+ color: 'var(--sv-color-text-muted)',
971
+ }}
972
+ >
973
+ <ResponsiveSurface
974
+ web={
975
+ <span style={{ color: 'var(--sv-color-text-primary)' }}>
976
+ Web tree (resize below 768px)
977
+ </span>
978
+ }
979
+ mobile={<span style={{ color: 'var(--sv-color-text-primary)' }}>Mobile tree</span>}
980
+ />
981
+ </div>
982
+ );
983
+ }
984
+
985
+ const SWIPABLE_CAROUSEL_DEMO_SLIDES = [
986
+ { key: 'lists', label: 'Lists' },
987
+ { key: 'groceries', label: 'Groceries' },
988
+ { key: 'errands', label: 'Errands' },
989
+ ];
990
+
991
+ function SwipableMobileCarouselDemo() {
992
+ const [activeIndex, setActiveIndex] = useState(0);
993
+ return (
994
+ <div
995
+ style={{
996
+ height: 220,
997
+ border: '1px solid var(--sv-color-border)',
998
+ borderRadius: 'var(--sv-radius-md)',
999
+ overflow: 'hidden',
1000
+ }}
1001
+ >
1002
+ <SwipableMobileCarousel
1003
+ activeIndex={activeIndex}
1004
+ onSettle={setActiveIndex}
1005
+ aria-label="Task lists"
1006
+ >
1007
+ {SWIPABLE_CAROUSEL_DEMO_SLIDES.map((slide) => (
1008
+ <SwipableMobileCarouselSlide key={slide.key} slideKey={slide.key} label={slide.label}>
1009
+ <SwipableMobileCarouselSlideHeader>
1010
+ <div
1011
+ style={{
1012
+ padding: 'var(--sv-space-3)',
1013
+ fontFamily: ff,
1014
+ fontWeight: 'var(--sv-font-weight-semibold)',
1015
+ color: 'var(--sv-color-text-primary)',
1016
+ }}
1017
+ >
1018
+ {slide.label}
1019
+ </div>
1020
+ </SwipableMobileCarouselSlideHeader>
1021
+ <SwipableMobileCarouselSlideBody>
1022
+ <div
1023
+ style={{
1024
+ display: 'flex',
1025
+ alignItems: 'center',
1026
+ justifyContent: 'center',
1027
+ height: '100%',
1028
+ fontFamily: ff,
1029
+ color: 'var(--sv-color-text-muted)',
1030
+ }}
1031
+ >
1032
+ {slide.label} content
1033
+ </div>
1034
+ </SwipableMobileCarouselSlideBody>
1035
+ </SwipableMobileCarouselSlide>
1036
+ ))}
1037
+ </SwipableMobileCarousel>
1038
+ </div>
1039
+ );
1040
+ }
1041
+
1042
+ function SwipableMobileCarouselDotsDemo() {
1043
+ const [activeIndex, setActiveIndex] = useState(0);
1044
+ return (
1045
+ <SwipableMobileCarouselDots
1046
+ count={3}
1047
+ activeIndex={activeIndex}
1048
+ onJump={setActiveIndex}
1049
+ labels={['Lists', 'Groceries', 'Errands']}
1050
+ aria-label="Task lists"
1051
+ />
1052
+ );
1053
+ }
1054
+
924
1055
  function IconPickerDemo() {
925
1056
  const [value, setValue] = useState<IconName | null>('banana');
926
1057
  const options: IconName[] = [
@@ -1423,6 +1554,21 @@ font-weight: var(--sv-font-weight-medium); /* 500 */
1423
1554
  font-weight: var(--sv-font-weight-semibold); /* 600 */
1424
1555
  font-weight: var(--sv-font-weight-bold); /* 700 */`}</Code>
1425
1556
  </div>
1557
+
1558
+ <div style={{ marginTop: 'var(--sv-space-4)' }}>
1559
+ <p
1560
+ style={{
1561
+ fontSize: 'var(--sv-font-size-caption)',
1562
+ color: 'var(--sv-color-text-muted)',
1563
+ }}
1564
+ >
1565
+ Every size above is rem-based, so the whole scale responds to the in-app text-size
1566
+ control (Account → Preferences → Appearance, task 10.2), which sets{' '}
1567
+ <code>[data-text-size]</code> on <code>&lt;html&gt;</code> to override{' '}
1568
+ <code>--sv-text-size-scale</code> — see the &quot;Text-size presets&quot; section of
1569
+ the Token Gallery.
1570
+ </p>
1571
+ </div>
1426
1572
  </section>
1427
1573
 
1428
1574
  {/* ── Spacing & Radius ─────────────────────────────────────────── */}
@@ -1582,7 +1728,7 @@ font-weight: var(--sv-font-weight-bold); /* 700 */`}</Code>
1582
1728
  <section style={{ marginBottom: 'var(--sv-space-12)' }}>
1583
1729
  <SectionHeader
1584
1730
  title="Component gallery"
1585
- subtitle="All 22 components — click each story in the sidebar for the full API, variants, and controls."
1731
+ subtitle="All 74 components — click each story in the sidebar for the full API, variants, and controls."
1586
1732
  />
1587
1733
 
1588
1734
  <div style={{ marginBottom: 'var(--sv-space-6)' }}>
@@ -2013,6 +2159,85 @@ font-weight: var(--sv-font-weight-bold); /* 700 */`}</Code>
2013
2159
  </div>
2014
2160
  </ComponentCard>
2015
2161
 
2162
+ {/* PageContainer */}
2163
+ <ComponentCard
2164
+ name="PageContainer"
2165
+ importLine="import { PageContainer } from '@sovereignfs/ui';"
2166
+ usage="Constrains and centers a plugin's main content width (sm/md/lg/full). Adds no padding of its own — the runtime shell already pads plugin content. Use instead of local container CSS in a plugin's layout."
2167
+ >
2168
+ <div style={{ width: '100%', background: 'var(--sv-color-surface-sunken)' }}>
2169
+ <PageContainer maxWidth="sm">
2170
+ <div
2171
+ style={{
2172
+ background: 'var(--sv-color-surface-raised)',
2173
+ border: '1px dashed var(--sv-color-border)',
2174
+ borderRadius: 'var(--sv-radius-md)',
2175
+ padding: 'var(--sv-space-4)',
2176
+ }}
2177
+ >
2178
+ maxWidth=&quot;sm&quot;
2179
+ </div>
2180
+ </PageContainer>
2181
+ </div>
2182
+ </ComponentCard>
2183
+
2184
+ {/* MobileHeader */}
2185
+ <ComponentCard
2186
+ name="MobileHeader"
2187
+ importLine="import { MobileHeader } from '@sovereignfs/ui';"
2188
+ usage="Mobile shell header content row (RFC 0088). Bell and avatarMenu are always rendered by the consumer; logo is optional — omit it for a default 'S' badge (matching the shell's own fallback brand icon), or supply your own. Title is the only content-level override. Presentational only — see Overview/Mobile Patterns for the full immutable/overridable boundary."
2189
+ >
2190
+ <div
2191
+ style={{
2192
+ width: '100%',
2193
+ background: 'var(--sv-color-surface)',
2194
+ border: '1px solid var(--sv-color-border)',
2195
+ borderRadius: 'var(--sv-radius-md)',
2196
+ padding: '0 var(--sv-space-3)',
2197
+ }}
2198
+ >
2199
+ <MobileHeader
2200
+ title="Tasks"
2201
+ bell={<Icon name="bell" size="lg" aria-label="Notifications" />}
2202
+ avatarMenu={<Avatar name="Jamie Doe" size="sm" />}
2203
+ />
2204
+ </div>
2205
+ </ComponentCard>
2206
+
2207
+ {/* MobileFooter */}
2208
+ <ComponentCard
2209
+ name="MobileFooter"
2210
+ importLine="import { MobileFooter } from '@sovereignfs/ui';"
2211
+ usage="Mobile shell footer nav row (RFC 0088). The centered launcher (onOpenApps) is always rendered; leftIcons/rightIcons take 1 or 2 items each, symmetric so the launcher stays centered."
2212
+ >
2213
+ <div
2214
+ style={{
2215
+ width: '100%',
2216
+ background: 'var(--sv-color-surface)',
2217
+ border: '1px solid var(--sv-color-border)',
2218
+ borderRadius: 'var(--sv-radius-md)',
2219
+ padding: '0 var(--sv-space-3)',
2220
+ }}
2221
+ >
2222
+ <MobileFooter
2223
+ onOpenApps={() => {}}
2224
+ leftIcons={[{ icon: <Icon name="house" size="md" aria-hidden />, label: 'Home' }]}
2225
+ rightIcons={[
2226
+ { icon: <Icon name="search" size="md" aria-hidden />, label: 'Search' },
2227
+ ]}
2228
+ />
2229
+ </div>
2230
+ </ComponentCard>
2231
+
2232
+ {/* MobileAppsDrawer */}
2233
+ <ComponentCard
2234
+ name="MobileAppsDrawer"
2235
+ importLine="import { MobileAppsDrawer } from '@sovereignfs/ui';"
2236
+ usage="The mobile 'Apps' bottom sheet: a 3-column tile grid matching the runtime shell's own Apps drawer exactly (56px icon tiles, radius 14px). No header row — Drawer's own grab handle plus swipe-down/scrim-tap dismissal are enough. The tile list is supplied by the consumer via `items` — e.g. installed plugins for the shell, or a plugin's own sections for its own launcher button."
2237
+ >
2238
+ <MobileAppsDrawerDemo />
2239
+ </ComponentCard>
2240
+
2016
2241
  {/* EmptyState */}
2017
2242
  <ComponentCard
2018
2243
  name="EmptyState"
@@ -2031,6 +2256,53 @@ font-weight: var(--sv-font-weight-bold); /* 700 */`}</Code>
2031
2256
  />
2032
2257
  </ComponentCard>
2033
2258
 
2259
+ {/* OfflineGate */}
2260
+ <ComponentCard
2261
+ name="OfflineGate"
2262
+ importLine="import { OfflineGate } from '@sovereignfs/ui';"
2263
+ usage="Blocks an administrative/settings surface from rendering possibly-stale cached content while offline, showing an EmptyState instead. Reads the live useIsOffline() hook — toggle DevTools > Network > Offline to see it swap. Wrap the body of a surface like Console or Account where operating against stale data is actively wrong, not every page."
2264
+ >
2265
+ <OfflineGate surfaceName="Console">
2266
+ <p style={{ margin: 0, fontFamily: 'var(--sv-font-family)', fontSize: 14 }}>
2267
+ Renders normally while online.
2268
+ </p>
2269
+ </OfflineGate>
2270
+ </ComponentCard>
2271
+
2272
+ {/* DeviceOnlyGate */}
2273
+ <ComponentCard
2274
+ name="DeviceOnlyGate"
2275
+ importLine="import { DeviceOnlyGate } from '@sovereignfs/ui';"
2276
+ usage="Blocks a device-only-tier plugin's content on a surface without the durable, encrypted, device-auth-gated store that tier needs. `available` is the caller's own isDeviceOnlyTierAvailable() result (from @sovereignfs/sdk/device-client) — a capability signal, never a security boundary; the real protection is that the data is encrypted regardless."
2277
+ >
2278
+ <DeviceOnlyGate available={false} surfaceName="Wallet">
2279
+ <p style={{ margin: 0, fontFamily: 'var(--sv-font-family)', fontSize: 14 }}>
2280
+ Renders normally when available.
2281
+ </p>
2282
+ </DeviceOnlyGate>
2283
+ </ComponentCard>
2284
+
2285
+ {/* DeviceStorageKeyGate */}
2286
+ <ComponentCard
2287
+ name="DeviceStorageKeyGate"
2288
+ importLine="import { DeviceStorageKeyGate } from '@sovereignfs/ui';"
2289
+ usage="Blocks a device-only-tier plugin's content until the user has set up their Device Storage Key. `status` is the caller's own getDeviceStorageKeyStatus() result (from @sovereignfs/sdk/device-only-storage). Enrollment is centralized in Account > Security, not per-plugin — `setupAction` is where the caller supplies a link there."
2290
+ >
2291
+ <DeviceStorageKeyGate
2292
+ status="not-set-up"
2293
+ surfaceName="Notes"
2294
+ setupAction={
2295
+ <a href="/account/security" style={{ color: 'var(--sv-color-accent)' }}>
2296
+ Go to Account → Security
2297
+ </a>
2298
+ }
2299
+ >
2300
+ <p style={{ margin: 0, fontFamily: 'var(--sv-font-family)', fontSize: 14 }}>
2301
+ Renders normally once set up.
2302
+ </p>
2303
+ </DeviceStorageKeyGate>
2304
+ </ComponentCard>
2305
+
2034
2306
  {/* Spinner */}
2035
2307
  <ComponentCard
2036
2308
  name="Spinner"
@@ -2238,6 +2510,28 @@ font-weight: var(--sv-font-weight-bold); /* 700 */`}</Code>
2238
2510
  />
2239
2511
  </ComponentCard>
2240
2512
 
2513
+ {/* LegalLinks */}
2514
+ <ComponentCard
2515
+ name="LegalLinks"
2516
+ importLine="import { LegalLinks } from '@sovereignfs/ui';"
2517
+ usage="Privacy Policy / Terms of Service link row for standalone auth pages (login, register, forgot/reset password). Pass renderLink to keep navigation client-side if ever mounted inside an SPA-like shell."
2518
+ >
2519
+ <LegalLinks />
2520
+ </ComponentCard>
2521
+
2522
+ {/* Markdown */}
2523
+ <ComponentCard
2524
+ name="Markdown"
2525
+ importLine="import { Markdown } from '@sovereignfs/ui';"
2526
+ usage="Renders a constrained markdown subset (headings, paragraphs, bold/italic/code, links, lists, blockquotes) as Typography-based React elements — no HTML string injection. Built for first-party long-form content pages (privacy policy, terms of service), not arbitrary or user-supplied markdown."
2527
+ >
2528
+ <Markdown
2529
+ content={
2530
+ '## Heading\n\nA paragraph with **bold** and a [link](/privacy).\n\n- One\n- Two'
2531
+ }
2532
+ />
2533
+ </ComponentCard>
2534
+
2241
2535
  {/* Pagination */}
2242
2536
  <ComponentCard
2243
2537
  name="Pagination"
@@ -2676,6 +2970,33 @@ font-weight: var(--sv-font-weight-bold); /* 700 */`}</Code>
2676
2970
  >
2677
2971
  <FileDropzoneDemo />
2678
2972
  </ComponentCard>
2973
+
2974
+ {/* ResponsiveSurface */}
2975
+ <ComponentCard
2976
+ name="ResponsiveSurface"
2977
+ importLine="import { ResponsiveSurface } from '@sovereignfs/ui';"
2978
+ usage="Renders an entirely different component tree below a breakpoint, not a CSS squeeze of the same one — only the active side is ever mounted. See also the useResponsiveLayout hook for non-JSX values."
2979
+ >
2980
+ <ResponsiveSurfaceDemo />
2981
+ </ComponentCard>
2982
+
2983
+ {/* SwipableMobileCarousel */}
2984
+ <ComponentCard
2985
+ name="SwipableMobileCarousel"
2986
+ importLine="import { SwipableMobileCarousel, SwipableMobileCarouselSlide, SwipableMobileCarouselSlideHeader, SwipableMobileCarouselSlideBody } from '@sovereignfs/ui';"
2987
+ usage="Compound component for swiping between full-width slides. A Slide's Header/Footer always render; only Body's own loading prop gates its content, so a title can show immediately while the body's own fetch is still in flight. Owns mount-window/reorder mechanics only — never put cross-slide aggregation or a detail overlay inside a Slide."
2988
+ >
2989
+ <SwipableMobileCarouselDemo />
2990
+ </ComponentCard>
2991
+
2992
+ {/* SwipableMobileCarouselDots */}
2993
+ <ComponentCard
2994
+ name="SwipableMobileCarouselDots"
2995
+ importLine="import { SwipableMobileCarouselDots } from '@sovereignfs/ui';"
2996
+ usage="A real, tappable, labeled slide indicator (role=tablist/tab) — standalone and reusable outside SwipableMobileCarousel too, and its default renderIndicator."
2997
+ >
2998
+ <SwipableMobileCarouselDotsDemo />
2999
+ </ComponentCard>
2679
3000
  </div>
2680
3001
  </section>
2681
3002
 
@@ -0,0 +1,35 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { DeviceOnlyGate } from '../components/DeviceOnlyGate/DeviceOnlyGate';
3
+
4
+ const sampleContent = (
5
+ <div style={{ padding: 'var(--sv-space-4)' }}>
6
+ <p style={{ margin: 0, fontFamily: 'var(--sv-font-family)' }}>
7
+ The wrapped content — a wallet's card list, a health log, anything a `device-only` plugin
8
+ keeps entirely on-device.
9
+ </p>
10
+ </div>
11
+ );
12
+
13
+ const meta = {
14
+ title: 'Components/DeviceOnlyGate',
15
+ component: DeviceOnlyGate,
16
+ parameters: { layout: 'padded' },
17
+ } satisfies Meta<typeof DeviceOnlyGate>;
18
+
19
+ export default meta;
20
+ type Story = StoryObj<typeof meta>;
21
+
22
+ // `available` is the caller's own isDeviceOnlyTierAvailable() result — a
23
+ // real bridge-handshake check, not something this story can drive live, so
24
+ // it's passed directly as an arg here instead.
25
+ export const Available: Story = {
26
+ args: { children: sampleContent, available: true, surfaceName: 'Wallet' },
27
+ };
28
+
29
+ export const Restricted: Story = {
30
+ args: { children: sampleContent, available: false, surfaceName: 'Wallet' },
31
+ };
32
+
33
+ export const RestrictedGenericPhrasing: Story = {
34
+ args: { children: sampleContent, available: false },
35
+ };
@@ -0,0 +1,57 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { DeviceStorageKeyGate } from '../components/DeviceStorageKeyGate/DeviceStorageKeyGate';
3
+
4
+ const sampleContent = (
5
+ <div style={{ padding: 'var(--sv-space-4)' }}>
6
+ <p style={{ margin: 0, fontFamily: 'var(--sv-font-family)' }}>
7
+ The wrapped content — a notes list, a health log, anything a `device-only` plugin keeps
8
+ entirely on-device.
9
+ </p>
10
+ </div>
11
+ );
12
+
13
+ const setupAction = (
14
+ <a href="/account/security" style={{ color: 'var(--sv-color-accent)' }}>
15
+ Go to Account → Security
16
+ </a>
17
+ );
18
+
19
+ const meta = {
20
+ title: 'Components/DeviceStorageKeyGate',
21
+ component: DeviceStorageKeyGate,
22
+ parameters: { layout: 'padded' },
23
+ } satisfies Meta<typeof DeviceStorageKeyGate>;
24
+
25
+ export default meta;
26
+ type Story = StoryObj<typeof meta>;
27
+
28
+ // `status` is the caller's own getDeviceStorageKeyStatus() result — an OPFS
29
+ // read this story can't drive live, so it's passed directly as an arg here.
30
+ export const SetUp: Story = {
31
+ args: { children: sampleContent, status: 'set-up', surfaceName: 'Notes' },
32
+ };
33
+
34
+ export const NotSetUp: Story = {
35
+ args: {
36
+ children: sampleContent,
37
+ status: 'not-set-up',
38
+ surfaceName: 'Notes',
39
+ setupAction,
40
+ },
41
+ };
42
+
43
+ export const NotSetUpGenericPhrasing: Story = {
44
+ args: { children: sampleContent, status: 'not-set-up', setupAction },
45
+ };
46
+
47
+ export const Unsupported: Story = {
48
+ args: { children: sampleContent, status: 'unsupported', surfaceName: 'Notes' },
49
+ };
50
+
51
+ export const NoDeviceAuth: Story = {
52
+ args: { children: sampleContent, status: 'no-device-auth', surfaceName: 'Notes' },
53
+ };
54
+
55
+ export const Checking: Story = {
56
+ args: { children: sampleContent, status: 'checking', surfaceName: 'Notes' },
57
+ };