@sovereignfs/ui 0.45.0 → 0.83.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Accordion.module.css +1 -1
- package/dist/Alert.module.css +1 -1
- package/dist/AppsLauncher.module.css +96 -0
- package/dist/Avatar.module.css +1 -1
- package/dist/Badge.module.css +30 -7
- package/dist/BalanceChip.module.css +1 -1
- package/dist/Button.module.css +18 -2
- package/dist/Card.module.css +1 -1
- package/dist/CardTile.module.css +181 -0
- package/dist/CodeTextarea.module.css +1 -1
- package/dist/Collapsible.module.css +1 -1
- package/dist/ColorPicker.module.css +132 -0
- package/dist/Combobox.module.css +2 -2
- package/dist/Command.module.css +1 -1
- package/dist/ConfirmDialog.module.css +2 -2
- package/dist/ConsentPrompt.module.css +21 -0
- package/dist/DatePicker.module.css +1 -1
- package/dist/Dialog.module.css +130 -21
- package/dist/Drawer.module.css +36 -7
- package/dist/Header.module.css +62 -0
- package/dist/HeaderFooterLayout.module.css +21 -0
- package/dist/IconPicker.module.css +1 -1
- package/dist/Input.module.css +1 -1
- package/dist/Kbd.module.css +1 -1
- package/dist/LegalLinks.module.css +24 -0
- package/dist/Markdown.module.css +73 -0
- package/dist/Menu.module.css +1 -0
- package/dist/Message.module.css +1 -1
- package/dist/MessageScroller.module.css +12 -1
- package/dist/MobileAppsDrawer.module.css +82 -0
- package/dist/MobileFooter.module.css +73 -0
- package/dist/MobileHeader.module.css +76 -0
- package/dist/NavList.module.css +130 -0
- package/dist/NavTabs.module.css +1 -1
- package/dist/NotificationsPanel.module.css +258 -0
- package/dist/OverlayHeader.module.css +1 -1
- package/dist/PageContainer.module.css +82 -0
- package/dist/PageHeader.module.css +54 -0
- package/dist/PageLayout.module.css +51 -0
- package/dist/Popover.module.css +1 -1
- package/dist/QuantityStepper.module.css +1 -1
- package/dist/Resizable.module.css +3 -3
- package/dist/RootLayout.module.css +12 -0
- package/dist/SegmentedControl.module.css +1 -1
- package/dist/Select.module.css +1 -1
- package/dist/Sheet.module.css +4 -1
- package/dist/SplitPane.module.css +4 -4
- package/dist/StatusBadge.module.css +1 -1
- package/dist/StepStrip.module.css +53 -0
- package/dist/SuggestionInput.module.css +2 -2
- package/dist/SwipableMobileCarousel.module.css +64 -0
- package/dist/SwipableMobileCarouselDots.module.css +64 -0
- package/dist/SwipableMobileCarouselSlideParts.module.css +29 -0
- package/dist/SwipeStack.module.css +101 -0
- package/dist/SystemBanner.module.css +10 -0
- package/dist/Table.module.css +2 -2
- package/dist/Tabs.module.css +1 -1
- package/dist/TagInput.module.css +2 -2
- package/dist/Textarea.module.css +9 -1
- package/dist/ThreeColumnLayout.module.css +35 -0
- package/dist/Toast.module.css +62 -3
- package/dist/Tooltip.module.css +1 -1
- package/dist/UserMenu.module.css +90 -0
- package/dist/index.d.ts +1680 -30
- package/dist/index.js +4618 -1443
- package/dist/tokens/primitives.css +36 -0
- package/dist/tokens/semantic.css +32 -0
- package/dist/tokens/theme-presets.css +33 -0
- package/package.json +32 -12
- package/src/__tests__/package-json.test.ts +8 -0
- package/src/components/Accordion/Accordion.module.css +1 -1
- package/src/components/Alert/Alert.module.css +1 -1
- package/src/components/AppsLauncher/AppsLauncher.module.css +96 -0
- package/src/components/AppsLauncher/AppsLauncher.stories.tsx +58 -0
- package/src/components/AppsLauncher/AppsLauncher.tsx +117 -0
- package/src/components/AppsLauncher/__tests__/AppsLauncher.test.tsx +52 -0
- package/src/components/Avatar/Avatar.module.css +1 -1
- package/src/components/Badge/Badge.module.css +30 -7
- package/src/components/Badge/Badge.stories.tsx +16 -4
- package/src/components/Badge/Badge.tsx +8 -11
- package/src/components/Badge/__tests__/Badge.test.tsx +4 -2
- package/src/components/BalanceChip/BalanceChip.module.css +1 -1
- package/src/components/Button/Button.module.css +18 -2
- package/src/components/Card/Card.module.css +1 -1
- package/src/components/CardTile/CardTile.module.css +181 -0
- package/src/components/CardTile/CardTile.stories.tsx +86 -0
- package/src/components/CardTile/CardTile.tsx +128 -0
- package/src/components/CardTile/__tests__/CardTile.test.tsx +56 -0
- package/src/components/CodeTextarea/CodeTextarea.module.css +1 -1
- package/src/components/Collapsible/Collapsible.module.css +1 -1
- package/src/components/ColorPicker/ColorPicker.module.css +132 -0
- package/src/components/ColorPicker/ColorPicker.stories.tsx +80 -0
- package/src/components/ColorPicker/ColorPicker.tsx +157 -0
- package/src/components/ColorPicker/__tests__/ColorPicker.test.tsx +117 -0
- package/src/components/Combobox/Combobox.module.css +2 -2
- package/src/components/Command/Command.module.css +1 -1
- package/src/components/ConfirmDialog/ConfirmDialog.module.css +2 -2
- package/src/components/ConfirmDialog/ConfirmDialog.tsx +28 -2
- package/src/components/ConfirmDialog/__tests__/ConfirmDialog.test.tsx +71 -3
- package/src/components/ConsentPrompt/ConsentPrompt.module.css +21 -0
- package/src/components/ConsentPrompt/ConsentPrompt.stories.tsx +31 -0
- package/src/components/ConsentPrompt/ConsentPrompt.tsx +66 -0
- package/src/components/ContextMenu/ContextMenu.tsx +1 -1
- package/src/components/DatePicker/DatePicker.module.css +1 -1
- 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.module.css +130 -21
- package/src/components/Dialog/Dialog.stories.tsx +179 -2
- package/src/components/Dialog/Dialog.tsx +137 -94
- package/src/components/Dialog/__tests__/Dialog.test.tsx +190 -9
- package/src/components/DragHandleRow/DragHandleRow.tsx +2 -21
- package/src/components/Drawer/Drawer.module.css +36 -7
- package/src/components/Drawer/Drawer.stories.tsx +44 -0
- package/src/components/Drawer/Drawer.tsx +27 -66
- package/src/components/Drawer/__tests__/Drawer.test.tsx +55 -6
- package/src/components/GripIcon/GripIcon.tsx +27 -0
- package/src/components/Header/Header.module.css +62 -0
- package/src/components/Header/Header.stories.tsx +452 -0
- package/src/components/Header/Header.tsx +102 -0
- package/src/components/Header/__tests__/Header.test.tsx +104 -0
- package/src/components/HeaderFooterLayout/HeaderFooterLayout.module.css +21 -0
- package/src/components/HeaderFooterLayout/HeaderFooterLayout.stories.tsx +111 -0
- package/src/components/HeaderFooterLayout/HeaderFooterLayout.tsx +54 -0
- package/src/components/HeaderFooterLayout/__tests__/HeaderFooterLayout.test.tsx +58 -0
- package/src/components/Icon/icons/arrow-left-right.tsx +23 -0
- package/src/components/Icon/icons/bold.tsx +20 -0
- package/src/components/Icon/icons/calendar.tsx +4 -4
- package/src/components/Icon/icons/carrot.tsx +3 -3
- package/src/components/Icon/icons/circle-chevron-left.tsx +21 -0
- package/src/components/Icon/icons/code.tsx +21 -0
- package/src/components/Icon/icons/copy.tsx +21 -0
- package/src/components/Icon/icons/credit-card.tsx +21 -0
- package/src/components/Icon/icons/database.tsx +22 -0
- package/src/components/Icon/icons/download.tsx +22 -0
- package/src/components/Icon/icons/file-text.tsx +24 -0
- package/src/components/Icon/icons/folder-closed.tsx +21 -0
- package/src/components/Icon/icons/folder-open.tsx +20 -0
- package/src/components/Icon/icons/folder.tsx +20 -0
- package/src/components/Icon/icons/folders.tsx +21 -0
- package/src/components/Icon/icons/hat-glasses.tsx +24 -0
- package/src/components/Icon/icons/history.tsx +22 -0
- package/src/components/Icon/icons/inbox.tsx +21 -0
- package/src/components/Icon/icons/index.ts +169 -1
- package/src/components/Icon/icons/italic.tsx +22 -0
- package/src/components/Icon/icons/layers.tsx +22 -0
- package/src/components/Icon/icons/layout-dashboard.tsx +23 -0
- package/src/components/Icon/icons/layout-grid.tsx +23 -0
- package/src/components/Icon/icons/link.tsx +21 -0
- package/src/components/Icon/icons/list-ordered.tsx +25 -0
- package/src/components/Icon/icons/list.tsx +25 -0
- package/src/components/Icon/icons/luggage.tsx +24 -0
- package/src/components/Icon/icons/map-pin.tsx +21 -0
- package/src/components/Icon/icons/menu.tsx +22 -0
- package/src/components/Icon/icons/minus.tsx +20 -0
- package/src/components/Icon/icons/paint-bucket.tsx +23 -0
- package/src/components/Icon/icons/panel-left.tsx +21 -0
- package/src/components/Icon/icons/pin.tsx +21 -0
- package/src/components/Icon/icons/rectangle-ellipsis.tsx +23 -0
- package/src/components/Icon/icons/refresh-cw.tsx +23 -0
- package/src/components/Icon/icons/rotate-cw.tsx +21 -0
- package/src/components/Icon/icons/route.tsx +22 -0
- package/src/components/Icon/icons/send.tsx +21 -0
- package/src/components/Icon/icons/share-2.tsx +24 -0
- package/src/components/Icon/icons/sheet.tsx +24 -0
- package/src/components/Icon/icons/smartphone.tsx +21 -0
- package/src/components/Icon/icons/square-kanban.tsx +23 -0
- package/src/components/Icon/icons/table.tsx +23 -0
- package/src/components/Icon/icons/user-round-plus.tsx +23 -0
- package/src/components/Icon/icons/users.tsx +23 -0
- package/src/components/IconPicker/IconPicker.module.css +1 -1
- package/src/components/Input/Input.module.css +1 -1
- package/src/components/Input/Input.tsx +8 -2
- package/src/components/Kbd/Kbd.module.css +1 -1
- 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 +73 -0
- package/src/components/Markdown/Markdown.stories.tsx +40 -0
- package/src/components/Markdown/Markdown.tsx +305 -0
- package/src/components/Markdown/__tests__/Markdown.test.tsx +144 -0
- package/src/components/Menu/Menu.module.css +1 -0
- package/src/components/Menu/Menu.tsx +72 -22
- package/src/components/Menu/__tests__/Menu.test.tsx +30 -0
- package/src/components/Menubar/Menubar.tsx +1 -1
- package/src/components/Message/Message.module.css +1 -1
- package/src/components/MessageScroller/MessageScroller.module.css +12 -1
- package/src/components/MessageScroller/MessageScroller.tsx +12 -0
- package/src/components/MobileAppsDrawer/MobileAppsDrawer.module.css +82 -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 +122 -0
- package/src/components/MobileFooter/__tests__/MobileFooter.test.tsx +129 -0
- package/src/components/MobileHeader/MobileHeader.module.css +76 -0
- package/src/components/MobileHeader/MobileHeader.stories.tsx +57 -0
- package/src/components/MobileHeader/MobileHeader.tsx +90 -0
- package/src/components/MobileHeader/__tests__/MobileHeader.test.tsx +113 -0
- package/src/components/NavList/NavList.module.css +130 -0
- package/src/components/NavList/NavList.tsx +136 -0
- package/src/components/NavList/__tests__/NavList.test.tsx +109 -0
- package/src/components/NavTabs/NavTabs.module.css +1 -1
- package/src/components/NavTabs/NavTabs.tsx +39 -11
- package/src/components/NotificationsPanel/NotificationsPanel.module.css +258 -0
- package/src/components/NotificationsPanel/NotificationsPanel.stories.tsx +61 -0
- package/src/components/NotificationsPanel/NotificationsPanel.tsx +187 -0
- package/src/components/NotificationsPanel/__tests__/NotificationsPanel.test.tsx +78 -0
- package/src/components/OfflineGate/OfflineGate.tsx +44 -0
- package/src/components/OfflineGate/__tests__/OfflineGate.test.tsx +66 -0
- package/src/components/OverlayHeader/OverlayHeader.module.css +1 -1
- package/src/components/OverlayHeader/OverlayHeader.tsx +19 -4
- package/src/components/OverlayHeader/__tests__/OverlayHeader.test.tsx +18 -0
- package/src/components/PageContainer/PageContainer.module.css +82 -0
- package/src/components/PageContainer/PageContainer.tsx +64 -0
- package/src/components/PageContainer/__tests__/PageContainer.test.tsx +70 -0
- package/src/components/PageHeader/PageHeader.module.css +54 -0
- package/src/components/PageHeader/PageHeader.tsx +157 -2
- package/src/components/PageHeader/__tests__/PageHeader.test.tsx +103 -0
- package/src/components/PageLayout/PageLayout.module.css +51 -0
- package/src/components/PageLayout/PageLayout.stories.tsx +73 -0
- package/src/components/PageLayout/PageLayout.tsx +51 -0
- package/src/components/PageLayout/__tests__/PageLayout.test.tsx +34 -0
- package/src/components/Popover/Popover.module.css +1 -1
- package/src/components/Popover/Popover.tsx +86 -4
- package/src/components/Popover/__tests__/Popover.test.tsx +165 -0
- package/src/components/QuantityStepper/QuantityStepper.module.css +1 -1
- package/src/components/Resizable/Resizable.module.css +3 -3
- package/src/components/ResponsiveSurface/ResponsiveSurface.tsx +24 -0
- package/src/components/ResponsiveSurface/__tests__/ResponsiveSurface.test.tsx +73 -0
- package/src/components/RootLayout/RootLayout.module.css +12 -0
- package/src/components/RootLayout/RootLayout.stories.tsx +125 -0
- package/src/components/RootLayout/RootLayout.tsx +185 -0
- package/src/components/RootLayout/__tests__/RootLayout.test.tsx +161 -0
- package/src/components/SegmentedControl/SegmentedControl.module.css +1 -1
- package/src/components/Select/Select.module.css +1 -1
- package/src/components/Sheet/Sheet.module.css +4 -1
- package/src/components/Sheet/Sheet.tsx +14 -56
- package/src/components/SplitPane/SplitPane.module.css +4 -4
- package/src/components/StatusBadge/StatusBadge.module.css +1 -1
- package/src/components/StepStrip/StepStrip.module.css +53 -0
- package/src/components/StepStrip/StepStrip.tsx +74 -0
- package/src/components/SuggestionInput/SuggestionInput.module.css +2 -2
- package/src/components/SwipableMobileCarousel/SwipableMobileCarousel.module.css +64 -0
- package/src/components/SwipableMobileCarousel/SwipableMobileCarousel.tsx +243 -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 +214 -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 +64 -0
- package/src/components/SwipableMobileCarouselDots/SwipableMobileCarouselDots.tsx +73 -0
- package/src/components/SwipableMobileCarouselDots/__tests__/SwipableMobileCarouselDots.test.tsx +63 -0
- package/src/components/SwipeStack/SwipeStack.module.css +101 -0
- package/src/components/SwipeStack/SwipeStack.tsx +199 -0
- package/src/components/SwipeStack/SwipeStackCard.tsx +22 -0
- package/src/components/SwipeStack/__tests__/SwipeStack.test.tsx +169 -0
- package/src/components/SystemBanner/SystemBanner.module.css +10 -0
- package/src/components/Table/Table.module.css +2 -2
- package/src/components/Tabs/Tabs.module.css +1 -1
- package/src/components/TagInput/TagInput.module.css +2 -2
- package/src/components/Textarea/Textarea.module.css +9 -1
- package/src/components/Textarea/Textarea.tsx +28 -5
- package/src/components/Textarea/__tests__/Textarea.test.tsx +23 -1
- package/src/components/ThreeColumnLayout/ThreeColumnLayout.module.css +35 -0
- package/src/components/ThreeColumnLayout/ThreeColumnLayout.stories.tsx +152 -0
- package/src/components/ThreeColumnLayout/ThreeColumnLayout.tsx +87 -0
- package/src/components/ThreeColumnLayout/__tests__/ThreeColumnLayout.test.tsx +78 -0
- package/src/components/Toast/Toast.module.css +62 -3
- package/src/components/Toast/Toast.stories.tsx +1 -1
- package/src/components/Tooltip/Tooltip.module.css +1 -1
- package/src/components/UserMenu/UserMenu.module.css +90 -0
- package/src/components/UserMenu/UserMenu.stories.tsx +49 -0
- package/src/components/UserMenu/UserMenu.tsx +132 -0
- package/src/components/UserMenu/__tests__/UserMenu.test.tsx +77 -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__/usePublishShellChromeHeight.test.tsx +118 -0
- package/src/hooks/__tests__/useReorderSensors.test.ts +26 -0
- package/src/hooks/__tests__/useResponsiveLayout.test.tsx +87 -0
- package/src/hooks/__tests__/useSnapCarousel.test.tsx +175 -0
- package/src/hooks/__tests__/useSwipeReveal.test.tsx +110 -0
- package/src/hooks/__tests__/useSwipeStack.test.tsx +155 -0
- package/src/hooks/index.ts +27 -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/usePublishShellChromeHeight.ts +96 -0
- package/src/hooks/useReorderSensors.ts +88 -0
- package/src/hooks/useResponsiveLayout.ts +36 -0
- package/src/hooks/useSnapCarousel.ts +284 -0
- package/src/hooks/useSwipeReveal.ts +107 -0
- package/src/hooks/useSwipeStack.ts +235 -0
- package/src/index.ts +124 -3
- package/src/overlay-shell.ts +159 -0
- package/src/stories/DesignSystemOverview.stories.tsx +933 -6
- package/src/stories/DeviceOnlyGate.stories.tsx +35 -0
- package/src/stories/DeviceStorageKeyGate.stories.tsx +57 -0
- package/src/stories/GripIcon.stories.tsx +29 -0
- package/src/stories/InteractionHooks.stories.tsx +483 -0
- package/src/stories/LayoutSystem.stories.tsx +639 -0
- package/src/stories/MenuEntries.stories.tsx +86 -0
- package/src/stories/MobilePatterns.stories.tsx +247 -2
- package/src/stories/NavList.stories.tsx +147 -0
- package/src/stories/NavTabs.stories.tsx +18 -0
- package/src/stories/OfflineGate.stories.tsx +42 -0
- package/src/stories/PageContainer.stories.tsx +82 -0
- package/src/stories/PageHeader.stories.tsx +65 -1
- package/src/stories/ResponsiveSurface.stories.tsx +47 -0
- package/src/stories/StepStrip.stories.tsx +76 -0
- package/src/stories/SwipableMobileCarousel.stories.tsx +362 -0
- package/src/stories/SwipeStack.stories.tsx +200 -0
- package/src/stories/Textarea.stories.tsx +12 -0
- package/src/stories/TokenGallery.stories.tsx +115 -0
- package/src/tokens/primitives.css +36 -0
- package/src/tokens/semantic.css +32 -0
- package/src/tokens/theme-presets.css +33 -0
- package/src/tokens/theme-presets.ts +72 -0
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { GripIcon } from '../components/GripIcon/GripIcon';
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof GripIcon> = {
|
|
5
|
+
title: 'Components/GripIcon',
|
|
6
|
+
component: GripIcon,
|
|
7
|
+
parameters: { layout: 'padded' },
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default meta;
|
|
12
|
+
type Story = StoryObj<typeof GripIcon>;
|
|
13
|
+
|
|
14
|
+
export const Default: Story = {
|
|
15
|
+
render: () => (
|
|
16
|
+
<div style={{ color: 'var(--sv-color-text-muted)' }}>
|
|
17
|
+
<GripIcon />
|
|
18
|
+
</div>
|
|
19
|
+
),
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const LargeSize: Story = {
|
|
23
|
+
render: () => (
|
|
24
|
+
<div style={{ color: 'var(--sv-color-text-muted)' }}>
|
|
25
|
+
<GripIcon size={28} />
|
|
26
|
+
</div>
|
|
27
|
+
),
|
|
28
|
+
name: 'Size variant (28px)',
|
|
29
|
+
};
|
|
@@ -2,11 +2,17 @@ import { useState } from 'react';
|
|
|
2
2
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
3
3
|
import { Card } from '../components/Card/Card';
|
|
4
4
|
import {
|
|
5
|
+
useCarouselRouteSync,
|
|
5
6
|
useCommitOnEnterOrBlur,
|
|
6
7
|
useDoubleTapHandler,
|
|
7
8
|
useIsMobile,
|
|
9
|
+
useIsOffline,
|
|
8
10
|
useLongPress,
|
|
11
|
+
useOfflineTileState,
|
|
12
|
+
useResponsiveLayout,
|
|
9
13
|
useSingleOrDoubleTap,
|
|
14
|
+
useSnapCarousel,
|
|
15
|
+
useSwipeReveal,
|
|
10
16
|
} from '../hooks';
|
|
11
17
|
|
|
12
18
|
// ---------------------------------------------------------------------------
|
|
@@ -331,6 +337,385 @@ function IsMobileDemo() {
|
|
|
331
337
|
);
|
|
332
338
|
}
|
|
333
339
|
|
|
340
|
+
function IsOfflineDemo() {
|
|
341
|
+
const isOffline = useIsOffline();
|
|
342
|
+
return (
|
|
343
|
+
<div
|
|
344
|
+
style={{
|
|
345
|
+
padding: 'var(--sv-space-4)',
|
|
346
|
+
borderRadius: 'var(--sv-radius-md)',
|
|
347
|
+
background: isOffline
|
|
348
|
+
? 'var(--sv-color-warning-surface)'
|
|
349
|
+
: 'var(--sv-color-success-surface)',
|
|
350
|
+
color: isOffline ? 'var(--sv-color-warning-text)' : 'var(--sv-color-success-text)',
|
|
351
|
+
fontFamily: ff,
|
|
352
|
+
fontSize: 'var(--sv-font-size-sm)',
|
|
353
|
+
fontWeight: 600,
|
|
354
|
+
textAlign: 'center',
|
|
355
|
+
}}
|
|
356
|
+
>
|
|
357
|
+
useIsOffline() → {String(isOffline)} — toggle DevTools > Network > Offline (or your OS's
|
|
358
|
+
airplane mode) to see this flip.
|
|
359
|
+
</div>
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function OfflineTileStateDemo() {
|
|
364
|
+
const [tier, setTier] = useState<'none' | 'offline-first' | 'device-only'>('device-only');
|
|
365
|
+
const [deviceOnlyAvailable, setDeviceOnlyAvailable] = useState(false);
|
|
366
|
+
const state = useOfflineTileState(tier === 'none' ? undefined : tier, deviceOnlyAvailable);
|
|
367
|
+
|
|
368
|
+
return (
|
|
369
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--sv-space-3)' }}>
|
|
370
|
+
<div
|
|
371
|
+
style={{ display: 'flex', gap: 'var(--sv-space-4)', alignItems: 'center', fontFamily: ff }}
|
|
372
|
+
>
|
|
373
|
+
<label style={{ fontSize: 'var(--sv-font-size-sm)' }}>
|
|
374
|
+
offline tier:{' '}
|
|
375
|
+
<select value={tier} onChange={(e) => setTier(e.target.value as typeof tier)}>
|
|
376
|
+
<option value="none">(none)</option>
|
|
377
|
+
<option value="offline-first">offline-first</option>
|
|
378
|
+
<option value="device-only">device-only</option>
|
|
379
|
+
</select>
|
|
380
|
+
</label>
|
|
381
|
+
<label style={{ fontSize: 'var(--sv-font-size-sm)' }}>
|
|
382
|
+
<input
|
|
383
|
+
type="checkbox"
|
|
384
|
+
checked={deviceOnlyAvailable}
|
|
385
|
+
onChange={(e) => setDeviceOnlyAvailable(e.target.checked)}
|
|
386
|
+
/>{' '}
|
|
387
|
+
deviceOnlyAvailable
|
|
388
|
+
</label>
|
|
389
|
+
</div>
|
|
390
|
+
<div
|
|
391
|
+
style={{
|
|
392
|
+
padding: 'var(--sv-space-4)',
|
|
393
|
+
borderRadius: 'var(--sv-radius-md)',
|
|
394
|
+
background:
|
|
395
|
+
state === 'connectivity-dimmed'
|
|
396
|
+
? 'var(--sv-color-warning-surface)'
|
|
397
|
+
: state === 'capability-restricted'
|
|
398
|
+
? 'var(--sv-color-info-surface)'
|
|
399
|
+
: 'var(--sv-color-success-surface)',
|
|
400
|
+
color:
|
|
401
|
+
state === 'connectivity-dimmed'
|
|
402
|
+
? 'var(--sv-color-warning-text)'
|
|
403
|
+
: state === 'capability-restricted'
|
|
404
|
+
? 'var(--sv-color-info-text)'
|
|
405
|
+
: 'var(--sv-color-success-text)',
|
|
406
|
+
fontFamily: ff,
|
|
407
|
+
fontSize: 'var(--sv-font-size-sm)',
|
|
408
|
+
fontWeight: 600,
|
|
409
|
+
textAlign: 'center',
|
|
410
|
+
}}
|
|
411
|
+
>
|
|
412
|
+
useOfflineTileState() → {String(state)}
|
|
413
|
+
</div>
|
|
414
|
+
<p
|
|
415
|
+
style={{
|
|
416
|
+
margin: 0,
|
|
417
|
+
fontFamily: ff,
|
|
418
|
+
fontSize: 'var(--sv-font-size-xs)',
|
|
419
|
+
color: 'var(--sv-color-text-muted)',
|
|
420
|
+
}}
|
|
421
|
+
>
|
|
422
|
+
connectivity-dimmed also reacts live to DevTools > Network > Offline — try it with
|
|
423
|
+
tier set to (none).
|
|
424
|
+
</p>
|
|
425
|
+
</div>
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function SwipeRevealDemo() {
|
|
430
|
+
const [open, setOpen] = useState(false);
|
|
431
|
+
const [events, setEvents] = useState<string[]>([]);
|
|
432
|
+
const log = (msg: string) =>
|
|
433
|
+
setEvents((e) => [...e, `${new Date().toLocaleTimeString()} — ${msg}`]);
|
|
434
|
+
const { rowRef, handlers } = useSwipeReveal({
|
|
435
|
+
revealWidth: 88,
|
|
436
|
+
open,
|
|
437
|
+
onOpen: () => {
|
|
438
|
+
setOpen(true);
|
|
439
|
+
log('opened');
|
|
440
|
+
},
|
|
441
|
+
onClose: () => {
|
|
442
|
+
setOpen(false);
|
|
443
|
+
log('closed');
|
|
444
|
+
},
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
return (
|
|
448
|
+
<div>
|
|
449
|
+
<div
|
|
450
|
+
style={{
|
|
451
|
+
position: 'relative',
|
|
452
|
+
overflow: 'hidden',
|
|
453
|
+
borderRadius: 'var(--sv-radius-md)',
|
|
454
|
+
border: '1px solid var(--sv-color-border)',
|
|
455
|
+
}}
|
|
456
|
+
>
|
|
457
|
+
<div
|
|
458
|
+
style={{
|
|
459
|
+
position: 'absolute',
|
|
460
|
+
inset: 0,
|
|
461
|
+
display: 'flex',
|
|
462
|
+
justifyContent: 'flex-end',
|
|
463
|
+
}}
|
|
464
|
+
>
|
|
465
|
+
<button
|
|
466
|
+
type="button"
|
|
467
|
+
onClick={() => {
|
|
468
|
+
setOpen(false);
|
|
469
|
+
log('Delete tapped');
|
|
470
|
+
}}
|
|
471
|
+
style={{
|
|
472
|
+
width: 88,
|
|
473
|
+
border: 'none',
|
|
474
|
+
background: 'var(--sv-color-error-surface)',
|
|
475
|
+
color: 'var(--sv-color-error-text)',
|
|
476
|
+
fontFamily: ff,
|
|
477
|
+
fontSize: 'var(--sv-font-size-sm)',
|
|
478
|
+
fontWeight: 600,
|
|
479
|
+
cursor: 'pointer',
|
|
480
|
+
}}
|
|
481
|
+
>
|
|
482
|
+
Delete
|
|
483
|
+
</button>
|
|
484
|
+
</div>
|
|
485
|
+
<div
|
|
486
|
+
ref={rowRef}
|
|
487
|
+
{...handlers}
|
|
488
|
+
style={{
|
|
489
|
+
position: 'relative',
|
|
490
|
+
padding: 'var(--sv-space-4)',
|
|
491
|
+
background: 'var(--sv-color-surface)',
|
|
492
|
+
fontFamily: ff,
|
|
493
|
+
fontSize: 'var(--sv-font-size-sm)',
|
|
494
|
+
color: 'var(--sv-color-text-primary)',
|
|
495
|
+
touchAction: 'pan-y',
|
|
496
|
+
transform: open ? 'translateX(-88px)' : undefined,
|
|
497
|
+
transition: 'transform 150ms ease-out',
|
|
498
|
+
}}
|
|
499
|
+
>
|
|
500
|
+
Drag me left to reveal Delete
|
|
501
|
+
</div>
|
|
502
|
+
</div>
|
|
503
|
+
<EventLog events={events} />
|
|
504
|
+
<Callout type="info">
|
|
505
|
+
Drag with a mouse in this preview, or touch on a real device/device toolbar. The hook only
|
|
506
|
+
writes an imperative transform during the drag itself — the CSS transition above only takes
|
|
507
|
+
over once the gesture resolves to open or closed on release.
|
|
508
|
+
</Callout>
|
|
509
|
+
</div>
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function SnapCarouselDemo() {
|
|
514
|
+
const slides = ['Lists', 'Groceries', 'Errands'];
|
|
515
|
+
const [index, setIndex] = useState(0);
|
|
516
|
+
const [events, setEvents] = useState<string[]>([]);
|
|
517
|
+
const log = (msg: string) =>
|
|
518
|
+
setEvents((e) => [...e, `${new Date().toLocaleTimeString()} — ${msg}`]);
|
|
519
|
+
const { scrollRef, scrollToIndex } = useSnapCarousel({
|
|
520
|
+
itemCount: slides.length,
|
|
521
|
+
onSettle: (i) => {
|
|
522
|
+
setIndex(i);
|
|
523
|
+
log(`settled on slide ${i}`);
|
|
524
|
+
},
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
return (
|
|
528
|
+
<div>
|
|
529
|
+
<div
|
|
530
|
+
ref={scrollRef}
|
|
531
|
+
style={{
|
|
532
|
+
display: 'flex',
|
|
533
|
+
overflowX: 'auto',
|
|
534
|
+
scrollSnapType: 'x mandatory',
|
|
535
|
+
borderRadius: 'var(--sv-radius-md)',
|
|
536
|
+
border: '1px solid var(--sv-color-border)',
|
|
537
|
+
}}
|
|
538
|
+
>
|
|
539
|
+
{slides.map((label, i) => (
|
|
540
|
+
<div
|
|
541
|
+
key={label}
|
|
542
|
+
style={{
|
|
543
|
+
flex: '0 0 100%',
|
|
544
|
+
scrollSnapAlign: 'start',
|
|
545
|
+
padding: 'var(--sv-space-6)',
|
|
546
|
+
textAlign: 'center',
|
|
547
|
+
fontFamily: ff,
|
|
548
|
+
fontSize: 'var(--sv-font-size-sm)',
|
|
549
|
+
color: 'var(--sv-color-text-primary)',
|
|
550
|
+
background:
|
|
551
|
+
i === index ? 'var(--sv-color-surface-sunken)' : 'var(--sv-color-surface)',
|
|
552
|
+
}}
|
|
553
|
+
>
|
|
554
|
+
{label}
|
|
555
|
+
</div>
|
|
556
|
+
))}
|
|
557
|
+
</div>
|
|
558
|
+
<div style={{ display: 'flex', gap: 'var(--sv-space-2)', marginTop: 'var(--sv-space-3)' }}>
|
|
559
|
+
{slides.map((label, i) => (
|
|
560
|
+
<button
|
|
561
|
+
key={label}
|
|
562
|
+
type="button"
|
|
563
|
+
onClick={() => scrollToIndex(i)}
|
|
564
|
+
style={{
|
|
565
|
+
padding: 'var(--sv-space-1) var(--sv-space-3)',
|
|
566
|
+
borderRadius: 'var(--sv-radius-md)',
|
|
567
|
+
border: '1px solid var(--sv-color-border)',
|
|
568
|
+
background: i === index ? 'var(--sv-color-accent)' : 'var(--sv-color-surface)',
|
|
569
|
+
color:
|
|
570
|
+
i === index ? 'var(--sv-color-text-on-accent)' : 'var(--sv-color-text-primary)',
|
|
571
|
+
fontFamily: ff,
|
|
572
|
+
fontSize: '0.75rem',
|
|
573
|
+
cursor: 'pointer',
|
|
574
|
+
}}
|
|
575
|
+
>
|
|
576
|
+
Slide {i}
|
|
577
|
+
</button>
|
|
578
|
+
))}
|
|
579
|
+
</div>
|
|
580
|
+
<EventLog events={events} />
|
|
581
|
+
<Callout type="info">
|
|
582
|
+
Swipe or scroll the strip above horizontally, or use the buttons. Settle detection is
|
|
583
|
+
debounced (120ms default) rather than tied to the <code>scrollend</code> event, which
|
|
584
|
+
pre-17.4 iOS Safari/WKWebView doesn't support.
|
|
585
|
+
</Callout>
|
|
586
|
+
</div>
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function ResponsiveLayoutDemo() {
|
|
591
|
+
const { isMobile, value } = useResponsiveLayout({
|
|
592
|
+
web: (
|
|
593
|
+
<div
|
|
594
|
+
style={{
|
|
595
|
+
padding: 'var(--sv-space-6)',
|
|
596
|
+
textAlign: 'center',
|
|
597
|
+
fontFamily: ff,
|
|
598
|
+
fontSize: 'var(--sv-font-size-sm)',
|
|
599
|
+
color: 'var(--sv-color-text-primary)',
|
|
600
|
+
}}
|
|
601
|
+
>
|
|
602
|
+
Web tree — a three-column layout, for example.
|
|
603
|
+
</div>
|
|
604
|
+
),
|
|
605
|
+
mobile: (
|
|
606
|
+
<div
|
|
607
|
+
style={{
|
|
608
|
+
padding: 'var(--sv-space-6)',
|
|
609
|
+
textAlign: 'center',
|
|
610
|
+
fontFamily: ff,
|
|
611
|
+
fontSize: 'var(--sv-font-size-sm)',
|
|
612
|
+
color: 'var(--sv-color-text-primary)',
|
|
613
|
+
}}
|
|
614
|
+
>
|
|
615
|
+
Mobile tree — an entirely different component, not a CSS squeeze.
|
|
616
|
+
</div>
|
|
617
|
+
),
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
return (
|
|
621
|
+
<div>
|
|
622
|
+
<div
|
|
623
|
+
style={{
|
|
624
|
+
borderRadius: 'var(--sv-radius-md)',
|
|
625
|
+
border: '1px solid var(--sv-color-border)',
|
|
626
|
+
background: 'var(--sv-color-surface)',
|
|
627
|
+
}}
|
|
628
|
+
>
|
|
629
|
+
{value}
|
|
630
|
+
</div>
|
|
631
|
+
<Callout type="info">
|
|
632
|
+
Currently showing the <strong>{isMobile ? 'mobile' : 'web'}</strong> tree. Resize the
|
|
633
|
+
Storybook viewport (or the toolbar's viewport picker) below 768px to see it swap — only
|
|
634
|
+
the active side is ever mounted, the other tree doesn't exist in the DOM at all.
|
|
635
|
+
</Callout>
|
|
636
|
+
</div>
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function CarouselRouteSyncDemo() {
|
|
641
|
+
const PATHS = ['/tasks', '/tasks/starred', '/tasks/groceries'];
|
|
642
|
+
const LABELS = ['Lists', 'Starred', 'Groceries'];
|
|
643
|
+
const [pathname, setPathname] = useState(PATHS[0] as string);
|
|
644
|
+
const [events, setEvents] = useState<string[]>([]);
|
|
645
|
+
const log = (msg: string) =>
|
|
646
|
+
setEvents((e) => [...e, `${new Date().toLocaleTimeString()} — ${msg}`]);
|
|
647
|
+
|
|
648
|
+
const { activeIndex, onSettle } = useCarouselRouteSync({
|
|
649
|
+
indexForPathname: (p) => Math.max(0, PATHS.indexOf(p)),
|
|
650
|
+
pathForIndex: (i) => PATHS[i] ?? '/tasks',
|
|
651
|
+
pathname,
|
|
652
|
+
onNavigate: (path) => {
|
|
653
|
+
log(`onNavigate("${path}") — simulating router.replace`);
|
|
654
|
+
setPathname(path);
|
|
655
|
+
},
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
return (
|
|
659
|
+
<div>
|
|
660
|
+
<div style={{ display: 'flex', gap: 'var(--sv-space-2)', marginBottom: 'var(--sv-space-3)' }}>
|
|
661
|
+
{LABELS.map((label, i) => (
|
|
662
|
+
<button
|
|
663
|
+
key={label}
|
|
664
|
+
type="button"
|
|
665
|
+
onClick={() => {
|
|
666
|
+
log(`swipe settled on "${label}" (index ${i})`);
|
|
667
|
+
onSettle(i);
|
|
668
|
+
}}
|
|
669
|
+
style={{
|
|
670
|
+
padding: 'var(--sv-space-2) var(--sv-space-3)',
|
|
671
|
+
borderRadius: 'var(--sv-radius-md)',
|
|
672
|
+
border: '1px solid var(--sv-color-border)',
|
|
673
|
+
background: i === activeIndex ? 'var(--sv-color-accent)' : 'var(--sv-color-surface)',
|
|
674
|
+
color:
|
|
675
|
+
i === activeIndex
|
|
676
|
+
? 'var(--sv-color-text-on-accent)'
|
|
677
|
+
: 'var(--sv-color-text-primary)',
|
|
678
|
+
fontFamily: ff,
|
|
679
|
+
fontSize: '0.8rem',
|
|
680
|
+
cursor: 'pointer',
|
|
681
|
+
}}
|
|
682
|
+
>
|
|
683
|
+
{label}
|
|
684
|
+
</button>
|
|
685
|
+
))}
|
|
686
|
+
<button
|
|
687
|
+
type="button"
|
|
688
|
+
onClick={() => {
|
|
689
|
+
const other = PATHS[(activeIndex + 1) % PATHS.length] as string;
|
|
690
|
+
log(`external nav to "${other}" (e.g. a tapped <Link>, browser back/forward)`);
|
|
691
|
+
setPathname(other);
|
|
692
|
+
}}
|
|
693
|
+
style={{
|
|
694
|
+
padding: 'var(--sv-space-2) var(--sv-space-3)',
|
|
695
|
+
borderRadius: 'var(--sv-radius-md)',
|
|
696
|
+
border: '1px dashed var(--sv-color-border)',
|
|
697
|
+
background: 'var(--sv-color-surface)',
|
|
698
|
+
color: 'var(--sv-color-text-primary)',
|
|
699
|
+
fontFamily: ff,
|
|
700
|
+
fontSize: '0.8rem',
|
|
701
|
+
cursor: 'pointer',
|
|
702
|
+
}}
|
|
703
|
+
>
|
|
704
|
+
Simulate external nav
|
|
705
|
+
</button>
|
|
706
|
+
</div>
|
|
707
|
+
<EventLog events={events} />
|
|
708
|
+
<Callout type="info">
|
|
709
|
+
This demo fakes a router with local <code>useState</code> — in a real app, pass{' '}
|
|
710
|
+
<code>next/navigation</code>'s <code>usePathname()</code> as <code>pathname</code> and{' '}
|
|
711
|
+
<code>router.replace</code> as <code>onNavigate</code>. Note the "swipe settled"
|
|
712
|
+
buttons never log a redundant external-nav resync afterwards — that's the isInternalNav
|
|
713
|
+
dance being handled internally.
|
|
714
|
+
</Callout>
|
|
715
|
+
</div>
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
|
|
334
719
|
// ---------------------------------------------------------------------------
|
|
335
720
|
// Main story component
|
|
336
721
|
// ---------------------------------------------------------------------------
|
|
@@ -412,6 +797,104 @@ function InteractionHooksDoc() {
|
|
|
412
797
|
|
|
413
798
|
const isMobile = useIsMobile(); // defaults to MOBILE_BREAKPOINT_PX (768)`}</CodeBlock>
|
|
414
799
|
</section>
|
|
800
|
+
|
|
801
|
+
<section style={{ marginBottom: 'var(--sv-space-10)' }}>
|
|
802
|
+
<SectionHeader
|
|
803
|
+
title="useIsOffline"
|
|
804
|
+
subtitle="SSR-safe navigator.onLine tracking, shared by OfflineBanner, the login page, and Console/Account's offline gate (research 0012)."
|
|
805
|
+
/>
|
|
806
|
+
<Card padding="md">
|
|
807
|
+
<IsOfflineDemo />
|
|
808
|
+
</Card>
|
|
809
|
+
<CodeBlock>{`import { useIsOffline } from '@sovereignfs/ui';
|
|
810
|
+
|
|
811
|
+
const isOffline = useIsOffline();`}</CodeBlock>
|
|
812
|
+
</section>
|
|
813
|
+
|
|
814
|
+
<section style={{ marginBottom: 'var(--sv-space-10)' }}>
|
|
815
|
+
<SectionHeader
|
|
816
|
+
title="useOfflineTileState"
|
|
817
|
+
subtitle="Which of the two app-tile states, if any, applies given a plugin's declared offline tier — connectivity-dimmed (no tier, offline right now) vs. capability-restricted (device-only, unavailable here), never conflated (research 0012)."
|
|
818
|
+
/>
|
|
819
|
+
<Card padding="md">
|
|
820
|
+
<OfflineTileStateDemo />
|
|
821
|
+
</Card>
|
|
822
|
+
<CodeBlock>{`import { useOfflineTileState } from '@sovereignfs/ui';
|
|
823
|
+
|
|
824
|
+
// deviceOnlyAvailable comes from isDeviceOnlyTierAvailable() in
|
|
825
|
+
// @sovereignfs/sdk/device-client — passed in, not computed here, so
|
|
826
|
+
// @sovereignfs/ui never depends on @sovereignfs/sdk.
|
|
827
|
+
const state = useOfflineTileState(plugin.offline, deviceOnlyAvailable);
|
|
828
|
+
// 'connectivity-dimmed' | 'capability-restricted' | null`}</CodeBlock>
|
|
829
|
+
</section>
|
|
830
|
+
|
|
831
|
+
<section style={{ marginBottom: 'var(--sv-space-10)' }}>
|
|
832
|
+
<SectionHeader
|
|
833
|
+
title="useSwipeReveal"
|
|
834
|
+
subtitle="Horizontal swipe-to-reveal for a row's Done/Delete actions — extracted from sovereign-tasks' two independent hand-rolled implementations (RFC 0079)."
|
|
835
|
+
/>
|
|
836
|
+
<Card padding="md">
|
|
837
|
+
<SwipeRevealDemo />
|
|
838
|
+
</Card>
|
|
839
|
+
<CodeBlock>{`const { rowRef, handlers } = useSwipeReveal({
|
|
840
|
+
revealWidth: 88,
|
|
841
|
+
open,
|
|
842
|
+
onOpen: () => setOpen(true),
|
|
843
|
+
onClose: () => setOpen(false),
|
|
844
|
+
});
|
|
845
|
+
<div ref={rowRef} {...handlers} style={{ transform: open ? 'translateX(-88px)' : undefined }}>
|
|
846
|
+
Row content
|
|
847
|
+
</div>`}</CodeBlock>
|
|
848
|
+
</section>
|
|
849
|
+
|
|
850
|
+
<section style={{ marginBottom: 'var(--sv-space-10)' }}>
|
|
851
|
+
<SectionHeader
|
|
852
|
+
title="useSnapCarousel"
|
|
853
|
+
subtitle="Debounced settled-slide detection over a native scroll-snap container — extracted from sovereign-tasks and sovereign-shopper's independently duplicated carousels (RFC 0079)."
|
|
854
|
+
/>
|
|
855
|
+
<Card padding="md">
|
|
856
|
+
<SnapCarouselDemo />
|
|
857
|
+
</Card>
|
|
858
|
+
<CodeBlock>{`const { scrollRef, scrollToIndex } = useSnapCarousel({
|
|
859
|
+
itemCount: slides.length,
|
|
860
|
+
onSettle: (index) => setActiveIndex(index),
|
|
861
|
+
});
|
|
862
|
+
<div ref={scrollRef} style={{ display: 'flex', overflowX: 'auto', scrollSnapType: 'x mandatory' }}>
|
|
863
|
+
{slides.map((slide) => <div style={{ scrollSnapAlign: 'start' }}>{slide}</div>)}
|
|
864
|
+
</div>`}</CodeBlock>
|
|
865
|
+
</section>
|
|
866
|
+
|
|
867
|
+
<section style={{ marginBottom: 'var(--sv-space-10)' }}>
|
|
868
|
+
<SectionHeader
|
|
869
|
+
title="useResponsiveLayout"
|
|
870
|
+
subtitle="Picks between a web and mobile value for the current viewport — formalizes the 'render an entirely different tree below a breakpoint' fork plugins have hand-rolled per plugin. See also the ResponsiveSurface component for the equivalent as plain JSX."
|
|
871
|
+
/>
|
|
872
|
+
<Card padding="md">
|
|
873
|
+
<ResponsiveLayoutDemo />
|
|
874
|
+
</Card>
|
|
875
|
+
<CodeBlock>{`const { isMobile, value } = useResponsiveLayout({ web: <WebShell />, mobile: <MobileShell /> });
|
|
876
|
+
// or, as a component:
|
|
877
|
+
<ResponsiveSurface web={<WebShell />} mobile={<MobileShell />} />`}</CodeBlock>
|
|
878
|
+
</section>
|
|
879
|
+
|
|
880
|
+
<section style={{ marginBottom: 'var(--sv-space-10)' }}>
|
|
881
|
+
<SectionHeader
|
|
882
|
+
title="useCarouselRouteSync"
|
|
883
|
+
subtitle="Router-agnostic pathname↔slide-index sync for SwipableMobileCarousel — centralizes the isInternalNav dance both sovereign-tasks and sovereign-shopper hand-roll identically today."
|
|
884
|
+
/>
|
|
885
|
+
<Card padding="md">
|
|
886
|
+
<CarouselRouteSyncDemo />
|
|
887
|
+
</Card>
|
|
888
|
+
<CodeBlock>{`const { activeIndex, onSettle } = useCarouselRouteSync({
|
|
889
|
+
indexForPathname: (p) => indexForPathname(p, lists), // your plugin's own logic, unchanged
|
|
890
|
+
pathForIndex: (i) => pathForIndex(i, lists),
|
|
891
|
+
pathname: usePathname(),
|
|
892
|
+
onNavigate: (path) => router.replace(path, { scroll: false }),
|
|
893
|
+
});
|
|
894
|
+
<SwipableMobileCarousel activeIndex={activeIndex} onSettle={onSettle} aria-label="Task lists">
|
|
895
|
+
...
|
|
896
|
+
</SwipableMobileCarousel>`}</CodeBlock>
|
|
897
|
+
</section>
|
|
415
898
|
</div>
|
|
416
899
|
);
|
|
417
900
|
}
|