@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
|
@@ -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><html></code> to override{' '}
|
|
1568
|
+
<code>--sv-text-size-scale</code> — see the "Text-size presets" 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
|
|
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="sm"
|
|
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
|
+
};
|