@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,76 @@
|
|
|
1
|
+
.header {
|
|
2
|
+
display: flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
justify-content: space-between;
|
|
5
|
+
gap: var(--sv-space-3);
|
|
6
|
+
width: 100%;
|
|
7
|
+
/* Mobile-only component — never render wider than the platform's own
|
|
8
|
+
mobile breakpoint, even if a consumer accidentally mounts it in a wider
|
|
9
|
+
container (e.g. an un-gated preview). At real mobile widths this is a
|
|
10
|
+
no-op since the viewport itself is already narrower. */
|
|
11
|
+
max-width: 768px;
|
|
12
|
+
margin-inline: auto;
|
|
13
|
+
min-height: var(--sv-shell-header-height, 60px);
|
|
14
|
+
padding: var(--sv-space-3) var(--sv-space-4);
|
|
15
|
+
padding-top: max(var(--sv-space-3), env(safe-area-inset-top));
|
|
16
|
+
padding-right: max(var(--sv-space-4), env(safe-area-inset-right));
|
|
17
|
+
padding-left: max(var(--sv-space-4), env(safe-area-inset-left));
|
|
18
|
+
background-color: var(--sv-color-surface);
|
|
19
|
+
border-bottom: var(--sv-border-width-hairline) solid var(--sv-color-border);
|
|
20
|
+
position: sticky;
|
|
21
|
+
top: 0;
|
|
22
|
+
z-index: 101;
|
|
23
|
+
/* WebKit momentum-scroll re-tiling quirk: a position: sticky element with
|
|
24
|
+
no compositing layer of its own can go stale/blank for a stretch of
|
|
25
|
+
frames during a fast scroll on iOS Safari instead of repainting at its
|
|
26
|
+
stuck position — only reachable when a consumer mounts this component
|
|
27
|
+
inside its own scrolling container (the platform shell's own mobile
|
|
28
|
+
header sits in a separate, non-scrolling grid row, so this is a no-op
|
|
29
|
+
there; a plugin self-rendering this via shellConfig.mobileHeader: false
|
|
30
|
+
inside a scrollable page is exactly the case this protects). Found and
|
|
31
|
+
fixed across three sticky elements in sovereign-tasks before being
|
|
32
|
+
applied here proactively — see docs/architecture-rules.md.
|
|
33
|
+
translateZ(0) promotes it to its own layer, rasterized independently of
|
|
34
|
+
the scroller's tile grid. */
|
|
35
|
+
transform: translateZ(0);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.brandGroup {
|
|
39
|
+
display: flex;
|
|
40
|
+
align-items: center;
|
|
41
|
+
gap: var(--sv-space-3);
|
|
42
|
+
min-width: 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* Matches the runtime shell's own .mobileBrandIcon (shell.module.css) — the
|
|
46
|
+
default logo shown when the consumer doesn't supply one. */
|
|
47
|
+
.defaultLogo {
|
|
48
|
+
display: flex;
|
|
49
|
+
align-items: center;
|
|
50
|
+
justify-content: center;
|
|
51
|
+
flex-shrink: 0;
|
|
52
|
+
width: 36px;
|
|
53
|
+
height: 36px;
|
|
54
|
+
border-radius: var(--sv-radius-md);
|
|
55
|
+
background-color: var(--sv-color-accent);
|
|
56
|
+
color: var(--sv-color-text-on-accent);
|
|
57
|
+
font-weight: var(--sv-font-weight-semibold);
|
|
58
|
+
overflow: hidden;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.title {
|
|
62
|
+
font-size: var(--sv-font-size-md);
|
|
63
|
+
font-weight: var(--sv-font-weight-semibold);
|
|
64
|
+
color: var(--sv-color-text-primary);
|
|
65
|
+
overflow: hidden;
|
|
66
|
+
text-overflow: ellipsis;
|
|
67
|
+
white-space: nowrap;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.right {
|
|
71
|
+
display: flex;
|
|
72
|
+
align-items: center;
|
|
73
|
+
gap: var(--sv-space-2);
|
|
74
|
+
margin-left: auto;
|
|
75
|
+
flex-shrink: 0;
|
|
76
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { MobileHeader } from './MobileHeader';
|
|
3
|
+
import { Avatar } from '../Avatar/Avatar';
|
|
4
|
+
import { Icon } from '../Icon/Icon';
|
|
5
|
+
|
|
6
|
+
function Logo() {
|
|
7
|
+
return (
|
|
8
|
+
<span
|
|
9
|
+
aria-label="Home"
|
|
10
|
+
style={{
|
|
11
|
+
display: 'flex',
|
|
12
|
+
alignItems: 'center',
|
|
13
|
+
justifyContent: 'center',
|
|
14
|
+
width: 28,
|
|
15
|
+
height: 28,
|
|
16
|
+
borderRadius: 'var(--sv-radius-sm)',
|
|
17
|
+
background: 'var(--sv-color-accent)',
|
|
18
|
+
color: 'var(--sv-color-text-on-accent)',
|
|
19
|
+
fontWeight: 700,
|
|
20
|
+
fontSize: 13,
|
|
21
|
+
flexShrink: 0,
|
|
22
|
+
}}
|
|
23
|
+
>
|
|
24
|
+
S
|
|
25
|
+
</span>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const meta = {
|
|
30
|
+
title: 'Components/MobileHeader',
|
|
31
|
+
component: MobileHeader,
|
|
32
|
+
parameters: {
|
|
33
|
+
layout: 'padded',
|
|
34
|
+
docs: {
|
|
35
|
+
description: {
|
|
36
|
+
component:
|
|
37
|
+
'Mobile shell header content row (RFC 0088). Logo, bell, and avatarMenu are always rendered by the consumer — the optional title is the only overridable part. Presentational only, no data fetching. See Overview/Mobile Patterns for the full immutable/overridable boundary.',
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
args: {
|
|
42
|
+
logo: <Logo />,
|
|
43
|
+
bell: <Icon name="bell" size="md" aria-label="Notifications" />,
|
|
44
|
+
avatarMenu: <Avatar name="Jamie Doe" size="sm" />,
|
|
45
|
+
},
|
|
46
|
+
} satisfies Meta<typeof MobileHeader>;
|
|
47
|
+
|
|
48
|
+
export default meta;
|
|
49
|
+
type Story = StoryObj<typeof meta>;
|
|
50
|
+
|
|
51
|
+
export const Default: Story = {};
|
|
52
|
+
|
|
53
|
+
export const WithTitle: Story = {
|
|
54
|
+
args: {
|
|
55
|
+
title: 'Tasks',
|
|
56
|
+
},
|
|
57
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useRef, type HTMLAttributes, type ReactNode } from 'react';
|
|
4
|
+
import { usePublishShellChromeHeight } from '../../hooks/usePublishShellChromeHeight';
|
|
5
|
+
import styles from './MobileHeader.module.css';
|
|
6
|
+
|
|
7
|
+
export interface MobileHeaderProps extends HTMLAttributes<HTMLDivElement> {
|
|
8
|
+
/** The brand/logo element — already a complete, clickable link home (e.g. an
|
|
9
|
+
* instance logo wrapped in the consumer's own router `Link`). Always
|
|
10
|
+
* rendered. `MobileHeader` stays router-agnostic, so this is provided
|
|
11
|
+
* fully built rather than as a bare icon + href. Optional — omit it to get
|
|
12
|
+
* a default "S" badge (matching the runtime shell's own fallback brand
|
|
13
|
+
* icon when no instance logo is configured), sized and styled to the same
|
|
14
|
+
* 36px/radius-md convention a custom logo is expected to follow. */
|
|
15
|
+
logo?: ReactNode;
|
|
16
|
+
/** Optional contextual title rendered beside the logo (e.g. the active
|
|
17
|
+
* plugin's name). Absent by default — this is the one overridable part of
|
|
18
|
+
* the header. See RFC 0088. */
|
|
19
|
+
title?: string;
|
|
20
|
+
/** Notification bell — always rendered; not overridable.
|
|
21
|
+
* TODO: this is just an icon slot today (the consumer wires up its own
|
|
22
|
+
* click handler/content) — it doesn't yet accommodate a full notification
|
|
23
|
+
* feature (unread badge, dropdown panel, etc. — see the runtime shell's
|
|
24
|
+
* own NotificationBell). Deferred; revisit once a plugin actually needs
|
|
25
|
+
* more than a bare bell icon here. */
|
|
26
|
+
bell: ReactNode;
|
|
27
|
+
/** Avatar / account menu trigger — always rendered; not overridable. */
|
|
28
|
+
avatarMenu: ReactNode;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Default fallback logo — matches the runtime shell's own .mobileBrandIcon
|
|
32
|
+
* exactly (36px, radius-md, accent background) for the "no instance logo
|
|
33
|
+
* configured" case, just with a fixed "S" instead of the instance name's
|
|
34
|
+
* first letter (this component can't know the instance name — no data
|
|
35
|
+
* fetching, see the component's own doc comment). */
|
|
36
|
+
function DefaultLogo() {
|
|
37
|
+
return (
|
|
38
|
+
<span className={styles.defaultLogo} aria-hidden="true">
|
|
39
|
+
S
|
|
40
|
+
</span>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The mobile header bar: brand/logo (always shown), an optional contextual
|
|
46
|
+
* title, and a fixed bell + avatar-menu cluster. Owns its own chrome —
|
|
47
|
+
* background, border-bottom, sticky positioning, safe-area-aware padding,
|
|
48
|
+
* and a 768px max-width (so it never stretches full-bleed if mounted outside
|
|
49
|
+
* an actual mobile viewport) — matching the runtime shell's own mobile
|
|
50
|
+
* header exactly, since this component is meant to replace that hand-rolled
|
|
51
|
+
* markup. Presentational only — no data fetching; the consumer supplies
|
|
52
|
+
* `bell`/`avatarMenu` (and, optionally, `logo`) as already-wired components
|
|
53
|
+
* (their own sizing — 36px logo/avatar, 44px bell tap target — is the
|
|
54
|
+
* consumer's responsibility, matching the reference implementation in the
|
|
55
|
+
* runtime shell). See RFC 0088 for the immutable/overridable boundary this
|
|
56
|
+
* enforces.
|
|
57
|
+
*
|
|
58
|
+
* Self-measures its own rendered height and publishes it as
|
|
59
|
+
* `--sv-shell-header-height` on the shell root — see
|
|
60
|
+
* `usePublishShellChromeHeight`'s own doc comment for why this exists (it's
|
|
61
|
+
* what makes a self-rendered header, the `shellConfig.mobileHeader: false`
|
|
62
|
+
* pattern, correctly position `Dialog`'s inset and any other consumer of
|
|
63
|
+
* that variable instead of being overlapped by it).
|
|
64
|
+
*/
|
|
65
|
+
export function MobileHeader({
|
|
66
|
+
logo,
|
|
67
|
+
title,
|
|
68
|
+
bell,
|
|
69
|
+
avatarMenu,
|
|
70
|
+
className,
|
|
71
|
+
...rest
|
|
72
|
+
}: MobileHeaderProps) {
|
|
73
|
+
const headerRef = useRef<HTMLDivElement>(null);
|
|
74
|
+
usePublishShellChromeHeight(headerRef, '--sv-shell-header-height');
|
|
75
|
+
|
|
76
|
+
const cls = [styles.header, className].filter(Boolean).join(' ');
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div className={cls} {...rest} ref={headerRef}>
|
|
80
|
+
<div className={styles.brandGroup}>
|
|
81
|
+
{logo ?? <DefaultLogo />}
|
|
82
|
+
{title && <span className={styles.title}>{title}</span>}
|
|
83
|
+
</div>
|
|
84
|
+
<div className={styles.right}>
|
|
85
|
+
{bell}
|
|
86
|
+
{avatarMenu}
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { cleanup, render, screen } from '@testing-library/react';
|
|
4
|
+
import { MobileHeader } from '../MobileHeader';
|
|
5
|
+
|
|
6
|
+
afterEach(() => {
|
|
7
|
+
cleanup();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
describe('MobileHeader self-measured height', () => {
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
document.getElementById('sv-app-shell')?.remove();
|
|
13
|
+
vi.restoreAllMocks();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('publishes its rendered height as --sv-shell-header-height on #sv-app-shell', () => {
|
|
17
|
+
const shell = document.createElement('div');
|
|
18
|
+
shell.id = 'sv-app-shell';
|
|
19
|
+
document.body.appendChild(shell);
|
|
20
|
+
vi.spyOn(Element.prototype, 'getBoundingClientRect').mockReturnValue({
|
|
21
|
+
height: 60,
|
|
22
|
+
width: 0,
|
|
23
|
+
top: 0,
|
|
24
|
+
left: 0,
|
|
25
|
+
right: 0,
|
|
26
|
+
bottom: 0,
|
|
27
|
+
x: 0,
|
|
28
|
+
y: 0,
|
|
29
|
+
toJSON() {},
|
|
30
|
+
} as DOMRect);
|
|
31
|
+
|
|
32
|
+
render(<MobileHeader bell={<span />} avatarMenu={<span />} />);
|
|
33
|
+
|
|
34
|
+
expect(shell.style.getPropertyValue('--sv-shell-header-height')).toBe('60px');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('removes the override on unmount', () => {
|
|
38
|
+
const shell = document.createElement('div');
|
|
39
|
+
shell.id = 'sv-app-shell';
|
|
40
|
+
document.body.appendChild(shell);
|
|
41
|
+
vi.spyOn(Element.prototype, 'getBoundingClientRect').mockReturnValue({
|
|
42
|
+
height: 60,
|
|
43
|
+
width: 0,
|
|
44
|
+
top: 0,
|
|
45
|
+
left: 0,
|
|
46
|
+
right: 0,
|
|
47
|
+
bottom: 0,
|
|
48
|
+
x: 0,
|
|
49
|
+
y: 0,
|
|
50
|
+
toJSON() {},
|
|
51
|
+
} as DOMRect);
|
|
52
|
+
|
|
53
|
+
const { unmount } = render(<MobileHeader bell={<span />} avatarMenu={<span />} />);
|
|
54
|
+
expect(shell.style.getPropertyValue('--sv-shell-header-height')).toBe('60px');
|
|
55
|
+
|
|
56
|
+
unmount();
|
|
57
|
+
expect(shell.style.getPropertyValue('--sv-shell-header-height')).toBe('');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('never throws when #sv-app-shell is absent (Storybook, unit tests, other hosts)', () => {
|
|
61
|
+
expect(() => render(<MobileHeader bell={<span />} avatarMenu={<span />} />)).not.toThrow();
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('MobileHeader', () => {
|
|
66
|
+
it('always renders the logo, bell, and avatar menu slots', () => {
|
|
67
|
+
render(
|
|
68
|
+
<MobileHeader
|
|
69
|
+
logo={<a href="/">logo</a>}
|
|
70
|
+
bell={<span data-testid="bell">bell</span>}
|
|
71
|
+
avatarMenu={<span data-testid="avatar">avatar</span>}
|
|
72
|
+
/>,
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
expect(screen.getByText('logo')).toBeDefined();
|
|
76
|
+
expect(screen.getByTestId('bell')).toBeDefined();
|
|
77
|
+
expect(screen.getByTestId('avatar')).toBeDefined();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('renders no title by default', () => {
|
|
81
|
+
render(<MobileHeader logo={<a href="/">logo</a>} bell={<span />} avatarMenu={<span />} />);
|
|
82
|
+
|
|
83
|
+
expect(screen.queryByText(/tasks/i)).toBeNull();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('renders the optional title when provided', () => {
|
|
87
|
+
render(
|
|
88
|
+
<MobileHeader
|
|
89
|
+
logo={<a href="/">logo</a>}
|
|
90
|
+
title="Tasks"
|
|
91
|
+
bell={<span />}
|
|
92
|
+
avatarMenu={<span />}
|
|
93
|
+
/>,
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
expect(screen.getByText('Tasks')).toBeDefined();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('renders a default "S" badge when logo is omitted', () => {
|
|
100
|
+
render(<MobileHeader bell={<span />} avatarMenu={<span />} />);
|
|
101
|
+
|
|
102
|
+
expect(screen.getByText('S')).toBeDefined();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('renders the supplied logo instead of the default badge when provided', () => {
|
|
106
|
+
render(
|
|
107
|
+
<MobileHeader logo={<a href="/">custom logo</a>} bell={<span />} avatarMenu={<span />} />,
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
expect(screen.getByText('custom logo')).toBeDefined();
|
|
111
|
+
expect(screen.queryByText('S')).toBeNull();
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
.nav {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
gap: var(--sv-space-5);
|
|
5
|
+
width: 100%;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.group {
|
|
9
|
+
display: flex;
|
|
10
|
+
flex-direction: column;
|
|
11
|
+
gap: 2px;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.groupLabel {
|
|
15
|
+
padding: 0 var(--sv-space-3);
|
|
16
|
+
margin-bottom: var(--sv-space-1);
|
|
17
|
+
font-size: var(--sv-font-size-xs);
|
|
18
|
+
font-weight: var(--sv-font-weight-semibold);
|
|
19
|
+
letter-spacing: 0.04em;
|
|
20
|
+
text-transform: uppercase;
|
|
21
|
+
color: var(--sv-color-text-muted);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.row {
|
|
25
|
+
display: flex;
|
|
26
|
+
align-items: center;
|
|
27
|
+
gap: var(--sv-space-3);
|
|
28
|
+
min-height: var(--sv-touch-target-min);
|
|
29
|
+
padding: var(--sv-space-2) var(--sv-space-3);
|
|
30
|
+
border-radius: var(--sv-radius-md);
|
|
31
|
+
color: var(--sv-color-text-muted);
|
|
32
|
+
text-decoration: none;
|
|
33
|
+
font-size: var(--sv-font-size-sm);
|
|
34
|
+
font-weight: var(--sv-font-weight-medium);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.icon {
|
|
38
|
+
flex-shrink: 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.label {
|
|
42
|
+
flex: 1;
|
|
43
|
+
min-width: 0;
|
|
44
|
+
overflow: hidden;
|
|
45
|
+
text-overflow: ellipsis;
|
|
46
|
+
white-space: nowrap;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.badge {
|
|
50
|
+
flex-shrink: 0;
|
|
51
|
+
color: var(--sv-color-text-muted);
|
|
52
|
+
font-size: var(--sv-font-size-xs);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.chevron {
|
|
56
|
+
flex-shrink: 0;
|
|
57
|
+
color: var(--sv-color-text-muted);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* ── static (desktop sidebar) ─────────────────────────────────────────── */
|
|
61
|
+
|
|
62
|
+
.static .row {
|
|
63
|
+
transition:
|
|
64
|
+
background-color 0.1s ease,
|
|
65
|
+
color 0.1s ease;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@media (hover: hover) {
|
|
69
|
+
.static .row:hover {
|
|
70
|
+
background-color: var(--sv-color-surface-sunken);
|
|
71
|
+
color: var(--sv-color-text-primary);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.static .row:focus-visible {
|
|
76
|
+
outline: 2px solid var(--sv-color-focus-ring);
|
|
77
|
+
outline-offset: -2px;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.static .rowActive {
|
|
81
|
+
background-color: var(--sv-color-surface-sunken);
|
|
82
|
+
color: var(--sv-color-text-primary);
|
|
83
|
+
font-weight: var(--sv-font-weight-semibold);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.static .rowActive .icon {
|
|
87
|
+
color: var(--sv-color-text-primary);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/* Only meaningful combined with .static — see NavListProps.density's own
|
|
91
|
+
doc comment for why .drilldown rows never shrink. */
|
|
92
|
+
.compact.static .row {
|
|
93
|
+
min-height: 0;
|
|
94
|
+
padding: var(--sv-space-2) var(--sv-space-3);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/* ── drilldown (mobile index) — grouped card, native-Settings-app rows ──── */
|
|
98
|
+
|
|
99
|
+
.drilldown .group {
|
|
100
|
+
gap: 0;
|
|
101
|
+
border: 1px solid var(--sv-color-border);
|
|
102
|
+
border-radius: var(--sv-radius-lg);
|
|
103
|
+
overflow: hidden;
|
|
104
|
+
background-color: var(--sv-color-surface-raised);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.drilldown .row {
|
|
108
|
+
border-radius: 0;
|
|
109
|
+
padding: var(--sv-space-3) var(--sv-space-4);
|
|
110
|
+
color: var(--sv-color-text-primary);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.drilldown .row:not(:last-child) {
|
|
114
|
+
border-bottom: var(--sv-border-width-hairline) solid var(--sv-color-border);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
@media (hover: hover) {
|
|
118
|
+
.drilldown .row:hover {
|
|
119
|
+
background-color: var(--sv-color-surface-sunken);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.drilldown .row:focus-visible {
|
|
124
|
+
outline: 2px solid var(--sv-color-focus-ring);
|
|
125
|
+
outline-offset: -2px;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.drilldown .icon {
|
|
129
|
+
color: var(--sv-color-text-muted);
|
|
130
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { Fragment, type ReactNode } from 'react';
|
|
2
|
+
import { Icon } from '../Icon/Icon';
|
|
3
|
+
import type { IconName } from '../Icon/Icon';
|
|
4
|
+
import styles from './NavList.module.css';
|
|
5
|
+
|
|
6
|
+
export interface NavListItem {
|
|
7
|
+
id: string;
|
|
8
|
+
label: string;
|
|
9
|
+
href: string;
|
|
10
|
+
icon: IconName;
|
|
11
|
+
/** Trailing content, e.g. a count — reserved for a future consumer, not used by `variant="drilldown"`'s own chevron slot. */
|
|
12
|
+
badge?: ReactNode;
|
|
13
|
+
/**
|
|
14
|
+
* Only meaningful for `variant="static"` — whether this row represents the
|
|
15
|
+
* current page. Computed by the consumer (e.g. via `usePathname()` plus a
|
|
16
|
+
* longest-prefix match against every item's `href`), since this package
|
|
17
|
+
* doesn't depend on `next/navigation` — same reasoning as `NavTabs`.
|
|
18
|
+
*/
|
|
19
|
+
active?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface NavListGroup {
|
|
23
|
+
id: string;
|
|
24
|
+
/** Omit for an ungrouped leading item/group — e.g. a pinned "Overview" row above the grouped sections. */
|
|
25
|
+
label?: string;
|
|
26
|
+
items: NavListItem[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Props to spread onto a consumer-supplied link element — mirrors `NavTabs`' `NavTabLinkProps`. */
|
|
30
|
+
export interface NavListLinkProps {
|
|
31
|
+
href: string;
|
|
32
|
+
className: string;
|
|
33
|
+
'aria-current': 'page' | undefined;
|
|
34
|
+
children: ReactNode;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface NavListProps {
|
|
38
|
+
groups: NavListGroup[];
|
|
39
|
+
/**
|
|
40
|
+
* `"static"`: active row highlighted via `item.active`, no chevron — a
|
|
41
|
+
* persistent sidebar nav. `"drilldown"`: every row gets a trailing chevron
|
|
42
|
+
* and no active-state, tapping navigates to a full-screen section — a
|
|
43
|
+
* native Settings-app style index.
|
|
44
|
+
*/
|
|
45
|
+
variant: 'static' | 'drilldown';
|
|
46
|
+
/**
|
|
47
|
+
* `"default"`: each row is a full `--sv-touch-target-min` tall tap target
|
|
48
|
+
* — the right choice for `variant="drilldown"`'s mobile index, and for any
|
|
49
|
+
* `static` sidebar list long enough that scanability matters more than
|
|
50
|
+
* density. `"compact"` shrinks row height/padding for a short, always-
|
|
51
|
+
* visible `static` list (e.g. 2-4 items above a longer scrollable list of
|
|
52
|
+
* something else) where the default's touch-target height reads as
|
|
53
|
+
* needlessly spaced out. Only meaningful for `variant="static"` —
|
|
54
|
+
* `drilldown` rows stay full-height regardless, since that variant is
|
|
55
|
+
* mobile-only and touch-target size there isn't optional. Defaults to
|
|
56
|
+
* `"default"` so every existing consumer (e.g. Console's own sidebar) is
|
|
57
|
+
* visually unchanged.
|
|
58
|
+
*/
|
|
59
|
+
density?: 'default' | 'compact';
|
|
60
|
+
'aria-label': string;
|
|
61
|
+
className?: string;
|
|
62
|
+
/**
|
|
63
|
+
* Render a row as something other than a plain `<a href>` — e.g. Next's
|
|
64
|
+
* `<Link>`. Receives the item plus the class name/aria attributes NavList
|
|
65
|
+
* would otherwise apply, matching `NavTabs`' `renderLink` contract.
|
|
66
|
+
*/
|
|
67
|
+
renderLink?: (item: NavListItem, linkProps: NavListLinkProps) => ReactNode;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* NavList — a vertical list of icon+label rows, optionally grouped under
|
|
72
|
+
* section headers. Two presentations of the same `groups` data: `static` (a
|
|
73
|
+
* persistent sidebar, active item highlighted) and `drilldown` (a
|
|
74
|
+
* native-Settings-app style index, every row navigates to a full section).
|
|
75
|
+
*
|
|
76
|
+
* Framework-agnostic like `NavTabs` — no `next/navigation`/`next/link`
|
|
77
|
+
* import, no internal pathname reading. Active state and navigation behavior
|
|
78
|
+
* are supplied by the consumer via `item.active` and `renderLink`.
|
|
79
|
+
*/
|
|
80
|
+
export function NavList({
|
|
81
|
+
groups,
|
|
82
|
+
variant,
|
|
83
|
+
density = 'default',
|
|
84
|
+
'aria-label': ariaLabel,
|
|
85
|
+
className,
|
|
86
|
+
renderLink,
|
|
87
|
+
}: NavListProps) {
|
|
88
|
+
return (
|
|
89
|
+
<nav
|
|
90
|
+
className={[styles.nav, styles[variant], density === 'compact' && styles.compact, className]
|
|
91
|
+
.filter(Boolean)
|
|
92
|
+
.join(' ')}
|
|
93
|
+
aria-label={ariaLabel}
|
|
94
|
+
>
|
|
95
|
+
{groups.map((group) => (
|
|
96
|
+
<div key={group.id} className={styles.group}>
|
|
97
|
+
{group.label && <div className={styles.groupLabel}>{group.label}</div>}
|
|
98
|
+
{group.items.map((item) => {
|
|
99
|
+
const isActive = variant === 'static' && !!item.active;
|
|
100
|
+
const rowContent = (
|
|
101
|
+
<>
|
|
102
|
+
<Icon name={item.icon} size="sm" aria-hidden className={styles.icon} />
|
|
103
|
+
<span className={styles.label}>{item.label}</span>
|
|
104
|
+
{item.badge && <span className={styles.badge}>{item.badge}</span>}
|
|
105
|
+
{variant === 'drilldown' && (
|
|
106
|
+
<Icon name="chevron-right" size="sm" aria-hidden className={styles.chevron} />
|
|
107
|
+
)}
|
|
108
|
+
</>
|
|
109
|
+
);
|
|
110
|
+
const linkProps: NavListLinkProps = {
|
|
111
|
+
href: item.href,
|
|
112
|
+
className: [styles.row, isActive && styles.rowActive].filter(Boolean).join(' '),
|
|
113
|
+
'aria-current': isActive ? 'page' : undefined,
|
|
114
|
+
children: rowContent,
|
|
115
|
+
};
|
|
116
|
+
return (
|
|
117
|
+
<Fragment key={item.id}>
|
|
118
|
+
{renderLink ? (
|
|
119
|
+
renderLink(item, linkProps)
|
|
120
|
+
) : (
|
|
121
|
+
<a
|
|
122
|
+
href={linkProps.href}
|
|
123
|
+
className={linkProps.className}
|
|
124
|
+
aria-current={linkProps['aria-current']}
|
|
125
|
+
>
|
|
126
|
+
{linkProps.children}
|
|
127
|
+
</a>
|
|
128
|
+
)}
|
|
129
|
+
</Fragment>
|
|
130
|
+
);
|
|
131
|
+
})}
|
|
132
|
+
</div>
|
|
133
|
+
))}
|
|
134
|
+
</nav>
|
|
135
|
+
);
|
|
136
|
+
}
|