@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
|
@@ -75,6 +75,50 @@ export const Closed: Story = {
|
|
|
75
75
|
),
|
|
76
76
|
};
|
|
77
77
|
|
|
78
|
+
function DrawerWithTitleDemo() {
|
|
79
|
+
const [open, setOpen] = useState(false);
|
|
80
|
+
return (
|
|
81
|
+
<>
|
|
82
|
+
<Button onClick={() => setOpen(true)}>Open drawer</Button>
|
|
83
|
+
<Drawer open={open} onClose={() => setOpen(false)} title="Apps">
|
|
84
|
+
<ul style={{ listStyle: 'none', margin: 0, padding: '8px 0' }}>
|
|
85
|
+
{(['house', 'grid-2x2', 'settings', 'user'] as const).map((icon) => (
|
|
86
|
+
<li key={icon}>
|
|
87
|
+
<button
|
|
88
|
+
onClick={() => setOpen(false)}
|
|
89
|
+
style={{
|
|
90
|
+
display: 'flex',
|
|
91
|
+
alignItems: 'center',
|
|
92
|
+
gap: 12,
|
|
93
|
+
width: '100%',
|
|
94
|
+
padding: '12px 20px',
|
|
95
|
+
background: 'none',
|
|
96
|
+
border: 'none',
|
|
97
|
+
cursor: 'pointer',
|
|
98
|
+
fontSize: 15,
|
|
99
|
+
color: 'var(--sv-color-text-primary)',
|
|
100
|
+
fontFamily: 'system-ui',
|
|
101
|
+
}}
|
|
102
|
+
>
|
|
103
|
+
<Icon name={icon} size="md" aria-hidden />
|
|
104
|
+
{icon.replace(/-/g, ' ')}
|
|
105
|
+
</button>
|
|
106
|
+
</li>
|
|
107
|
+
))}
|
|
108
|
+
</ul>
|
|
109
|
+
</Drawer>
|
|
110
|
+
</>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Opt-in `title` prop renders a built-in `OverlayHeader` (title + close)
|
|
115
|
+
* below the grab handle — hidden by default (see the `Default` story), for
|
|
116
|
+
* consumers that want one instead of building their own header markup. */
|
|
117
|
+
export const WithTitle: Story = {
|
|
118
|
+
args: { open: false, onClose: () => {}, children: null },
|
|
119
|
+
render: (_args) => <DrawerWithTitleDemo />,
|
|
120
|
+
};
|
|
121
|
+
|
|
78
122
|
/** Play function opens the drawer and asserts its list items are visible. */
|
|
79
123
|
export const OpenViaInteraction: Story = {
|
|
80
124
|
args: { open: false, onClose: () => {}, children: null },
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { type PointerEvent as ReactPointerEvent, type ReactNode,
|
|
4
|
-
import { lockBodyScroll, unlockBodyScroll } from '../../scroll-lock';
|
|
3
|
+
import { type PointerEvent as ReactPointerEvent, type ReactNode, useRef } from 'react';
|
|
5
4
|
import { useMountTransition, usePrefersReducedMotion } from '../../motion';
|
|
5
|
+
import {
|
|
6
|
+
OVERLAY_MOTION_DURATION_MS,
|
|
7
|
+
useOverlayFocusCapture,
|
|
8
|
+
useOverlayKeyboardTrap,
|
|
9
|
+
useOverlayScrollLock,
|
|
10
|
+
} from '../../overlay-shell';
|
|
11
|
+
import { OverlayHeader } from '../OverlayHeader/OverlayHeader';
|
|
6
12
|
import styles from './Drawer.module.css';
|
|
7
13
|
|
|
8
|
-
// Matches --sv-motion-duration-base (Drawer.module.css) — see Dialog.tsx's
|
|
9
|
-
// identical constant for why this stays a plain JS number instead of being
|
|
10
|
-
// read from the CSS custom property.
|
|
11
|
-
const MOTION_DURATION_MS = 250;
|
|
12
|
-
|
|
13
14
|
export interface DrawerProps {
|
|
14
15
|
/** Whether the drawer is shown. When false, nothing renders. */
|
|
15
16
|
open: boolean;
|
|
@@ -22,12 +23,15 @@ export interface DrawerProps {
|
|
|
22
23
|
* "half screen or less" convention for Drawer, this is the only other
|
|
23
24
|
* supported size; taller content should use `Sheet` instead. */
|
|
24
25
|
snapHeight?: 'content' | 'half';
|
|
26
|
+
/** Renders a built-in `OverlayHeader` (title + close) below the grab
|
|
27
|
+
* handle. Omit when the content supplies its own header, or none is
|
|
28
|
+
* needed at all — e.g. the mobile shell's Apps drawer, whose grab handle
|
|
29
|
+
* and swipe-down/scrim-tap dismissal are enough on their own. Mirrors
|
|
30
|
+
* `Sheet`'s identical `title` prop. */
|
|
31
|
+
title?: string;
|
|
25
32
|
children: ReactNode;
|
|
26
33
|
}
|
|
27
34
|
|
|
28
|
-
const FOCUSABLE =
|
|
29
|
-
'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
30
|
-
|
|
31
35
|
// Downward drag distance (px) past which releasing dismisses the drawer
|
|
32
36
|
// instead of snapping back open. Distance-only, no velocity tracking — matches
|
|
33
37
|
// the swipe-to-reveal gesture already established in consuming plugins (see
|
|
@@ -61,67 +65,23 @@ export function Drawer({
|
|
|
61
65
|
onClose,
|
|
62
66
|
'aria-label': ariaLabel,
|
|
63
67
|
snapHeight = 'content',
|
|
68
|
+
title,
|
|
64
69
|
children,
|
|
65
70
|
}: DrawerProps) {
|
|
66
71
|
const panelRef = useRef<HTMLDivElement>(null);
|
|
67
|
-
const previouslyFocused = useRef<HTMLElement | null>(null);
|
|
68
72
|
const reducedMotion = usePrefersReducedMotion();
|
|
69
|
-
const { mounted, phase } = useMountTransition(
|
|
73
|
+
const { mounted, phase } = useMountTransition(
|
|
74
|
+
open,
|
|
75
|
+
reducedMotion ? 0 : OVERLAY_MOTION_DURATION_MS,
|
|
76
|
+
);
|
|
70
77
|
// Tracks an in-progress handle drag. Kept in a ref (not state) so pointermove
|
|
71
78
|
// updates the DOM directly at 60fps instead of re-rendering on every event —
|
|
72
79
|
// same technique as the tasks plugin's swipe-to-reveal rows.
|
|
73
80
|
const dragStartY = useRef<number | null>(null);
|
|
74
81
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (!mounted) return;
|
|
79
|
-
lockBodyScroll();
|
|
80
|
-
return unlockBodyScroll;
|
|
81
|
-
}, [mounted]);
|
|
82
|
-
|
|
83
|
-
useEffect(() => {
|
|
84
|
-
if (!open) return;
|
|
85
|
-
previouslyFocused.current = document.activeElement as HTMLElement | null;
|
|
86
|
-
const panel = panelRef.current;
|
|
87
|
-
const first = panel?.querySelector<HTMLElement>(FOCUSABLE);
|
|
88
|
-
(first ?? panel)?.focus();
|
|
89
|
-
return () => previouslyFocused.current?.focus();
|
|
90
|
-
}, [open]);
|
|
91
|
-
|
|
92
|
-
// Keyboard handling: Escape closes, Tab cycles within the panel (focus trap).
|
|
93
|
-
// Attached at document level so no keyboard listener is needed on the dialog
|
|
94
|
-
// div itself (which would trigger jsx-a11y/no-noninteractive-element-interactions).
|
|
95
|
-
useEffect(() => {
|
|
96
|
-
if (!open) return;
|
|
97
|
-
function handleKeyDown(e: KeyboardEvent) {
|
|
98
|
-
if (e.key === 'Escape') {
|
|
99
|
-
e.stopPropagation();
|
|
100
|
-
onClose();
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
if (e.key !== 'Tab') return;
|
|
104
|
-
const panel = panelRef.current;
|
|
105
|
-
if (!panel) return;
|
|
106
|
-
const focusable = [...panel.querySelectorAll<HTMLElement>(FOCUSABLE)];
|
|
107
|
-
const first = focusable[0];
|
|
108
|
-
const last = focusable.at(-1);
|
|
109
|
-
if (!first || !last) {
|
|
110
|
-
e.preventDefault();
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
const active = document.activeElement;
|
|
114
|
-
if (e.shiftKey && active === first) {
|
|
115
|
-
e.preventDefault();
|
|
116
|
-
last.focus();
|
|
117
|
-
} else if (!e.shiftKey && active === last) {
|
|
118
|
-
e.preventDefault();
|
|
119
|
-
first.focus();
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
document.addEventListener('keydown', handleKeyDown);
|
|
123
|
-
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
124
|
-
}, [open, onClose]);
|
|
82
|
+
useOverlayScrollLock(mounted);
|
|
83
|
+
useOverlayFocusCapture(panelRef, open);
|
|
84
|
+
useOverlayKeyboardTrap(panelRef, open, onClose);
|
|
125
85
|
|
|
126
86
|
// Swipe-down-to-dismiss, initiated only from the grab handle (see the
|
|
127
87
|
// component doc comment for why not the whole panel). Drags the panel
|
|
@@ -171,15 +131,15 @@ export function Drawer({
|
|
|
171
131
|
return (
|
|
172
132
|
// role="presentation" removes the scrim from the AT (it is purely visual).
|
|
173
133
|
// e.target check lets clicks inside the panel bubble without triggering dismiss.
|
|
134
|
+
// No onKeyDown here — useOverlayKeyboardTrap above already owns Escape via a
|
|
135
|
+
// document-level listener; a second handler here used to double-fire onClose
|
|
136
|
+
// per keypress (see Dialog.tsx's identical fix for the full explanation).
|
|
174
137
|
<div
|
|
175
138
|
className={[styles.scrim, isOpenPhase ? styles.scrimOpen : ''].filter(Boolean).join(' ')}
|
|
176
139
|
role="presentation"
|
|
177
140
|
onClick={(e) => {
|
|
178
141
|
if (e.target === e.currentTarget) onClose();
|
|
179
142
|
}}
|
|
180
|
-
onKeyDown={(e) => {
|
|
181
|
-
if (e.key === 'Escape') onClose();
|
|
182
|
-
}}
|
|
183
143
|
>
|
|
184
144
|
<div
|
|
185
145
|
ref={panelRef}
|
|
@@ -207,7 +167,8 @@ export function Drawer({
|
|
|
207
167
|
onPointerUp={(e) => releaseDrag(e, true)}
|
|
208
168
|
onPointerCancel={(e) => releaseDrag(e, false)}
|
|
209
169
|
/>
|
|
210
|
-
{
|
|
170
|
+
{title && <OverlayHeader title={title} onClose={onClose} />}
|
|
171
|
+
<div className={styles.body}>{children}</div>
|
|
211
172
|
</div>
|
|
212
173
|
</div>
|
|
213
174
|
);
|
|
@@ -63,16 +63,24 @@ describe('Drawer', () => {
|
|
|
63
63
|
expect(panel.getAttribute('aria-modal')).toBe('true');
|
|
64
64
|
});
|
|
65
65
|
|
|
66
|
-
it('calls onClose on Escape', () => {
|
|
66
|
+
it('calls onClose exactly once on Escape with focus inside the panel', () => {
|
|
67
67
|
const onClose = vi.fn();
|
|
68
68
|
render(
|
|
69
|
-
<Drawer open onClose={onClose}>
|
|
70
|
-
|
|
69
|
+
<Drawer open onClose={onClose} aria-label="Panel">
|
|
70
|
+
<button>Focus me</button>
|
|
71
71
|
</Drawer>,
|
|
72
72
|
);
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
|
|
73
|
+
// Dispatched from the actually-focused element (matching real usage,
|
|
74
|
+
// where useOverlayFocusCapture moves focus into the panel on open) so the
|
|
75
|
+
// keydown bubbles through the scrim on its way to document — the exact
|
|
76
|
+
// path that used to trigger both the scrim's own (now-removed) Escape
|
|
77
|
+
// handler and useOverlayKeyboardTrap's document-level listener, double-
|
|
78
|
+
// firing onClose. Dispatching straight on `document` (the previous form
|
|
79
|
+
// of this test) skips that bubble path and would pass even if the bug
|
|
80
|
+
// were reintroduced.
|
|
81
|
+
const button = screen.getByRole('button', { name: 'Focus me' });
|
|
82
|
+
button.focus();
|
|
83
|
+
fireEvent.keyDown(button, { key: 'Escape' });
|
|
76
84
|
expect(onClose).toHaveBeenCalledOnce();
|
|
77
85
|
});
|
|
78
86
|
|
|
@@ -110,6 +118,47 @@ describe('Drawer', () => {
|
|
|
110
118
|
expect(screen.getByRole('dialog').className).not.toMatch(/panelHalf/);
|
|
111
119
|
});
|
|
112
120
|
|
|
121
|
+
it('does not render OverlayHeader when title is omitted (hidden by default)', () => {
|
|
122
|
+
render(
|
|
123
|
+
<Drawer open onClose={() => {}} aria-label="Nav">
|
|
124
|
+
<button type="button">Own action</button>
|
|
125
|
+
</Drawer>,
|
|
126
|
+
);
|
|
127
|
+
// Only the content's own button — no OverlayHeader close button.
|
|
128
|
+
expect(screen.getAllByRole('button')).toHaveLength(1);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('renders children inside a scrollable body, separate from the grab handle', () => {
|
|
132
|
+
// Regression test: the panel used to be the sole scroll container
|
|
133
|
+
// (overflow-y: auto on .panel itself), so a tall drawer scrolled the
|
|
134
|
+
// grab handle out of view along with the content. The handle must stay
|
|
135
|
+
// a sibling of the scrollable region, not a descendant of it.
|
|
136
|
+
render(
|
|
137
|
+
<Drawer open onClose={() => {}} aria-label="Nav">
|
|
138
|
+
<button type="button">Own action</button>
|
|
139
|
+
</Drawer>,
|
|
140
|
+
);
|
|
141
|
+
const panel = screen.getByRole('dialog');
|
|
142
|
+
const handle = panel.firstElementChild as HTMLElement;
|
|
143
|
+
const button = screen.getByRole('button', { name: 'Own action' });
|
|
144
|
+
const body = button.parentElement as HTMLElement;
|
|
145
|
+
expect(body).not.toBe(panel);
|
|
146
|
+
expect(body.contains(handle)).toBe(false);
|
|
147
|
+
expect(panel.contains(body)).toBe(true);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('renders OverlayHeader (title + close) when title is provided', () => {
|
|
151
|
+
const onClose = vi.fn();
|
|
152
|
+
render(
|
|
153
|
+
<Drawer open onClose={onClose} aria-label="Nav" title="Apps">
|
|
154
|
+
Body
|
|
155
|
+
</Drawer>,
|
|
156
|
+
);
|
|
157
|
+
expect(screen.getByText('Apps')).toBeTruthy();
|
|
158
|
+
fireEvent.click(screen.getByRole('button', { name: 'Close' }));
|
|
159
|
+
expect(onClose).toHaveBeenCalledOnce();
|
|
160
|
+
});
|
|
161
|
+
|
|
113
162
|
describe('swipe-down-to-dismiss (grab handle)', () => {
|
|
114
163
|
// The handle is the panel's first child, aria-hidden and unlabeled —
|
|
115
164
|
// same "select via DOM position" approach the scrim-click test above
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface GripIconProps {
|
|
2
|
+
size?: number;
|
|
3
|
+
className?: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/** Six-dot drag-handle affordance. No baked-in pointer-events or color —
|
|
7
|
+
* a consumer that needs `pointer-events: none` (e.g. a button wrapping it)
|
|
8
|
+
* or a specific color passes those via `className`/`currentColor`. */
|
|
9
|
+
export function GripIcon({ size = 14, className }: GripIconProps) {
|
|
10
|
+
return (
|
|
11
|
+
<svg
|
|
12
|
+
width={size}
|
|
13
|
+
height={size}
|
|
14
|
+
viewBox="0 0 14 14"
|
|
15
|
+
fill="none"
|
|
16
|
+
aria-hidden="true"
|
|
17
|
+
className={className}
|
|
18
|
+
>
|
|
19
|
+
{/* Two columns of three dots */}
|
|
20
|
+
{[3, 7, 11].map((cy) =>
|
|
21
|
+
[4, 10].map((cx) => (
|
|
22
|
+
<circle key={`${cx}-${cy}`} cx={cx} cy={cy} r={1.2} fill="currentColor" />
|
|
23
|
+
)),
|
|
24
|
+
)}
|
|
25
|
+
</svg>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
.header {
|
|
2
|
+
display: flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
justify-content: space-between;
|
|
5
|
+
min-height: 48px;
|
|
6
|
+
padding: var(--sv-space-2) var(--sv-space-4);
|
|
7
|
+
border-bottom: var(--sv-border-width-hairline) solid var(--sv-color-border);
|
|
8
|
+
background-color: var(--sv-color-surface);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.left {
|
|
12
|
+
display: flex;
|
|
13
|
+
align-items: center;
|
|
14
|
+
gap: var(--sv-space-2);
|
|
15
|
+
min-width: 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.right {
|
|
19
|
+
display: flex;
|
|
20
|
+
align-items: center;
|
|
21
|
+
gap: var(--sv-space-1);
|
|
22
|
+
flex-shrink: 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.brandBadge {
|
|
26
|
+
display: flex;
|
|
27
|
+
align-items: center;
|
|
28
|
+
justify-content: center;
|
|
29
|
+
flex-shrink: 0;
|
|
30
|
+
width: 28px;
|
|
31
|
+
height: 28px;
|
|
32
|
+
border-radius: var(--sv-radius-md);
|
|
33
|
+
background-color: var(--sv-color-accent);
|
|
34
|
+
color: var(--sv-color-text-on-accent);
|
|
35
|
+
font-size: var(--sv-font-size-xs);
|
|
36
|
+
font-weight: var(--sv-font-weight-semibold);
|
|
37
|
+
text-decoration: none;
|
|
38
|
+
overflow: hidden;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.brandLogoImg {
|
|
42
|
+
width: 100%;
|
|
43
|
+
height: 100%;
|
|
44
|
+
object-fit: cover;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.plugin {
|
|
48
|
+
display: flex;
|
|
49
|
+
align-items: center;
|
|
50
|
+
gap: var(--sv-space-1);
|
|
51
|
+
min-width: 0;
|
|
52
|
+
text-decoration: none;
|
|
53
|
+
color: var(--sv-color-text-primary);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.pluginName {
|
|
57
|
+
font-size: var(--sv-font-size-md);
|
|
58
|
+
font-weight: var(--sv-font-weight-semibold);
|
|
59
|
+
overflow: hidden;
|
|
60
|
+
text-overflow: ellipsis;
|
|
61
|
+
white-space: nowrap;
|
|
62
|
+
}
|