@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,452 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { Header } from './Header';
|
|
3
|
+
import { MobileHeader } from '../MobileHeader/MobileHeader';
|
|
4
|
+
import { AppsLauncher } from '../AppsLauncher/AppsLauncher';
|
|
5
|
+
import { Icon, type IconName } from '../Icon/Icon';
|
|
6
|
+
import { NotificationsPanel } from '../NotificationsPanel/NotificationsPanel';
|
|
7
|
+
import { UserMenu } from '../UserMenu/UserMenu';
|
|
8
|
+
import { useIsMobile } from '../../hooks';
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Shared doc primitives (mirrors Overview/Mobile Patterns' own doc styling)
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
const ff = 'var(--sv-font-family)';
|
|
15
|
+
|
|
16
|
+
function Heading({ level = 2, children }: { level?: 1 | 2 | 3; children: React.ReactNode }) {
|
|
17
|
+
const sizes: Record<number, string> = { 1: '1.75rem', 2: '1.125rem', 3: '0.9375rem' };
|
|
18
|
+
const weights: Record<number, number> = { 1: 700, 2: 600, 3: 600 };
|
|
19
|
+
const Tag = `h${level}` as 'h1' | 'h2' | 'h3';
|
|
20
|
+
return (
|
|
21
|
+
<Tag
|
|
22
|
+
style={{
|
|
23
|
+
fontFamily: ff,
|
|
24
|
+
fontSize: sizes[level],
|
|
25
|
+
fontWeight: weights[level],
|
|
26
|
+
color: 'var(--sv-color-text-primary)',
|
|
27
|
+
margin: 0,
|
|
28
|
+
lineHeight: 1.3,
|
|
29
|
+
}}
|
|
30
|
+
>
|
|
31
|
+
{children}
|
|
32
|
+
</Tag>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function Body({ children }: { children: React.ReactNode }) {
|
|
37
|
+
return (
|
|
38
|
+
<p
|
|
39
|
+
style={{
|
|
40
|
+
fontFamily: ff,
|
|
41
|
+
fontSize: 'var(--sv-font-size-sm)',
|
|
42
|
+
color: 'var(--sv-color-text-muted)',
|
|
43
|
+
margin: '0.5rem 0 0',
|
|
44
|
+
lineHeight: 1.6,
|
|
45
|
+
}}
|
|
46
|
+
>
|
|
47
|
+
{children}
|
|
48
|
+
</p>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function SectionHeader({ title, subtitle }: { title: string; subtitle?: string }) {
|
|
53
|
+
return (
|
|
54
|
+
<div
|
|
55
|
+
style={{
|
|
56
|
+
paddingBottom: 'var(--sv-space-3)',
|
|
57
|
+
borderBottom: '2px solid var(--sv-color-accent)',
|
|
58
|
+
marginBottom: 'var(--sv-space-5)',
|
|
59
|
+
}}
|
|
60
|
+
>
|
|
61
|
+
<Heading level={2}>{title}</Heading>
|
|
62
|
+
{subtitle && <Body>{subtitle}</Body>}
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function Callout({
|
|
68
|
+
type = 'info',
|
|
69
|
+
children,
|
|
70
|
+
}: {
|
|
71
|
+
type?: 'info' | 'warn' | 'tip';
|
|
72
|
+
children: React.ReactNode;
|
|
73
|
+
}) {
|
|
74
|
+
const map = {
|
|
75
|
+
info: {
|
|
76
|
+
bg: 'var(--sv-color-info-surface)',
|
|
77
|
+
border: 'var(--sv-color-info-border)',
|
|
78
|
+
text: 'var(--sv-color-info-text)',
|
|
79
|
+
label: 'ℹ',
|
|
80
|
+
},
|
|
81
|
+
warn: {
|
|
82
|
+
bg: 'var(--sv-color-warning-surface)',
|
|
83
|
+
border: 'var(--sv-color-warning-border)',
|
|
84
|
+
text: 'var(--sv-color-warning-text)',
|
|
85
|
+
label: '⚠',
|
|
86
|
+
},
|
|
87
|
+
tip: {
|
|
88
|
+
bg: 'var(--sv-color-success-surface)',
|
|
89
|
+
border: 'var(--sv-color-success-border)',
|
|
90
|
+
text: 'var(--sv-color-success-text)',
|
|
91
|
+
label: '✓',
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
const c = map[type];
|
|
95
|
+
return (
|
|
96
|
+
<div
|
|
97
|
+
style={{
|
|
98
|
+
background: c.bg,
|
|
99
|
+
border: `1px solid ${c.border}`,
|
|
100
|
+
borderRadius: 'var(--sv-radius-md)',
|
|
101
|
+
padding: 'var(--sv-space-3) var(--sv-space-4)',
|
|
102
|
+
display: 'flex',
|
|
103
|
+
gap: 'var(--sv-space-2)',
|
|
104
|
+
alignItems: 'flex-start',
|
|
105
|
+
marginBottom: 'var(--sv-space-4)',
|
|
106
|
+
}}
|
|
107
|
+
>
|
|
108
|
+
<span style={{ color: c.text, fontWeight: 700, flexShrink: 0, fontSize: '0.875rem' }}>
|
|
109
|
+
{c.label}
|
|
110
|
+
</span>
|
|
111
|
+
<p
|
|
112
|
+
style={{
|
|
113
|
+
fontFamily: ff,
|
|
114
|
+
fontSize: 'var(--sv-font-size-sm)',
|
|
115
|
+
color: c.text,
|
|
116
|
+
margin: 0,
|
|
117
|
+
lineHeight: 1.55,
|
|
118
|
+
}}
|
|
119
|
+
>
|
|
120
|
+
{children}
|
|
121
|
+
</p>
|
|
122
|
+
</div>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function CodeBlock({ children }: { children: string }) {
|
|
127
|
+
return (
|
|
128
|
+
<pre
|
|
129
|
+
style={{
|
|
130
|
+
fontFamily: 'var(--sv-font-family-mono)',
|
|
131
|
+
fontSize: '0.75rem',
|
|
132
|
+
background: 'var(--sv-color-surface-sunken)',
|
|
133
|
+
border: '1px solid var(--sv-color-border)',
|
|
134
|
+
borderRadius: 'var(--sv-radius-md)',
|
|
135
|
+
padding: 'var(--sv-space-3) var(--sv-space-4)',
|
|
136
|
+
marginTop: 'var(--sv-space-3)',
|
|
137
|
+
overflowX: 'auto',
|
|
138
|
+
color: 'var(--sv-color-text-primary)',
|
|
139
|
+
lineHeight: 1.6,
|
|
140
|
+
}}
|
|
141
|
+
>
|
|
142
|
+
{children}
|
|
143
|
+
</pre>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function Frame({ children }: { children: React.ReactNode }) {
|
|
148
|
+
return (
|
|
149
|
+
<div
|
|
150
|
+
style={{
|
|
151
|
+
border: '1px solid var(--sv-color-border)',
|
|
152
|
+
borderRadius: 'var(--sv-radius-lg)',
|
|
153
|
+
overflow: 'hidden',
|
|
154
|
+
}}
|
|
155
|
+
>
|
|
156
|
+
{children}
|
|
157
|
+
</div>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// Demo pieces — the real, shared AppsLauncher/NotificationsPanel/UserMenu,
|
|
163
|
+
// not story-local mockups.
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* The plugin icon renders at the same 28px as the brand badge next to it,
|
|
168
|
+
* not a smaller inline glyph. `Icon`'s largest built-in size (`lg`, 24px)
|
|
169
|
+
* reads slightly small in a 28px box (lucide-style glyphs carry their own
|
|
170
|
+
* internal inset), so it's scaled up the remaining bit to actually fill the
|
|
171
|
+
* box, the same way a real 28px plugin-icon `<img>` asset would.
|
|
172
|
+
*/
|
|
173
|
+
function HeaderPluginIcon({ name }: { name: IconName }) {
|
|
174
|
+
return (
|
|
175
|
+
<span
|
|
176
|
+
aria-hidden="true"
|
|
177
|
+
style={{
|
|
178
|
+
display: 'inline-flex',
|
|
179
|
+
alignItems: 'center',
|
|
180
|
+
justifyContent: 'center',
|
|
181
|
+
width: 28,
|
|
182
|
+
height: 28,
|
|
183
|
+
}}
|
|
184
|
+
>
|
|
185
|
+
<span style={{ display: 'inline-flex', transform: 'scale(1.15)' }}>
|
|
186
|
+
<Icon name={name} size="lg" aria-hidden />
|
|
187
|
+
</span>
|
|
188
|
+
</span>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const launcherItems = [
|
|
193
|
+
{
|
|
194
|
+
key: 'home',
|
|
195
|
+
icon: <Icon name="house" size="md" aria-hidden />,
|
|
196
|
+
label: 'Home',
|
|
197
|
+
href: '/launcher',
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
key: 'console',
|
|
201
|
+
icon: <Icon name="layout-dashboard" size="md" aria-hidden />,
|
|
202
|
+
label: 'Console',
|
|
203
|
+
href: '/console',
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
key: 'notes',
|
|
207
|
+
icon: <Icon name="file" size="md" aria-hidden />,
|
|
208
|
+
label: 'Notes',
|
|
209
|
+
href: '/notes',
|
|
210
|
+
},
|
|
211
|
+
];
|
|
212
|
+
|
|
213
|
+
const notificationItems = [
|
|
214
|
+
{
|
|
215
|
+
id: '1',
|
|
216
|
+
icon: <Icon name="layers" size="sm" aria-hidden />,
|
|
217
|
+
title: 'Added to a project',
|
|
218
|
+
timeLabel: '2d ago',
|
|
219
|
+
read: false,
|
|
220
|
+
onDismiss: () => {},
|
|
221
|
+
},
|
|
222
|
+
];
|
|
223
|
+
|
|
224
|
+
const accountItems = [
|
|
225
|
+
{ label: 'Account', icon: 'user' as const, href: '/account' },
|
|
226
|
+
{ label: 'Preferences', icon: 'sliders-horizontal' as const, href: '/account/preferences' },
|
|
227
|
+
{ label: 'Sign out', icon: 'log-out' as const, destructive: true, onSelect: () => {} },
|
|
228
|
+
];
|
|
229
|
+
|
|
230
|
+
function Launcher() {
|
|
231
|
+
return <AppsLauncher items={launcherItems} />;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function Notifications() {
|
|
235
|
+
return <NotificationsPanel items={notificationItems} unreadCount={1} />;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function Account() {
|
|
239
|
+
return <UserMenu name="Kasun Benthara" email="kasun@openfs.io" items={accountItems} size="sm" />;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* `Header` is web-only (`shell: minimal`'s desktop top bar) — a plugin
|
|
244
|
+
* renders its own mobile equivalent alongside it, gated with `useIsMobile`
|
|
245
|
+
* (not CSS, to avoid mounting the wrong chrome's hooks on the wrong
|
|
246
|
+
* surface). Resize the Storybook viewport below 768px to see `MobileHeader`
|
|
247
|
+
* render instead. The launcher/notifications/avatar triggers are the real,
|
|
248
|
+
* shared `AppsLauncher`/`NotificationsPanel`/`UserMenu` components — shared
|
|
249
|
+
* verbatim across both branches, not two implementations of the same
|
|
250
|
+
* dropdown, and fully clickable here.
|
|
251
|
+
*/
|
|
252
|
+
function ResponsiveHeaderDemo() {
|
|
253
|
+
const isMobile = useIsMobile();
|
|
254
|
+
|
|
255
|
+
if (isMobile) {
|
|
256
|
+
return <MobileHeader title="Sovereign" bell={<Notifications />} avatarMenu={<Account />} />;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return (
|
|
260
|
+
<Header
|
|
261
|
+
instanceName="Sovereign"
|
|
262
|
+
homeHref="/launcher"
|
|
263
|
+
plugin={{
|
|
264
|
+
name: 'Notes',
|
|
265
|
+
icon: <HeaderPluginIcon name="file" />,
|
|
266
|
+
href: '/notes',
|
|
267
|
+
}}
|
|
268
|
+
launcher={<Launcher />}
|
|
269
|
+
notifications={<Notifications />}
|
|
270
|
+
avatarMenu={<Account />}
|
|
271
|
+
/>
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ---------------------------------------------------------------------------
|
|
276
|
+
// Default — the documentation page
|
|
277
|
+
// ---------------------------------------------------------------------------
|
|
278
|
+
|
|
279
|
+
function HeaderDoc() {
|
|
280
|
+
return (
|
|
281
|
+
<div
|
|
282
|
+
style={{
|
|
283
|
+
padding: 'var(--sv-space-6)',
|
|
284
|
+
background: 'var(--sv-color-surface)',
|
|
285
|
+
minHeight: '100vh',
|
|
286
|
+
fontFamily: ff,
|
|
287
|
+
maxWidth: 760,
|
|
288
|
+
margin: '0 auto',
|
|
289
|
+
}}
|
|
290
|
+
>
|
|
291
|
+
<div style={{ marginBottom: 'var(--sv-space-8)' }}>
|
|
292
|
+
<Heading level={1}>Header</Heading>
|
|
293
|
+
<Body>
|
|
294
|
+
The web top-bar header for <code>shell: minimal</code> plugins — a plugin that owns its
|
|
295
|
+
whole viewport (no platform sidebar) renders this instead, so it doesn't have to hand-roll
|
|
296
|
+
its own top bar from scratch.
|
|
297
|
+
</Body>
|
|
298
|
+
</div>
|
|
299
|
+
|
|
300
|
+
<section style={{ marginBottom: 'var(--sv-space-10)' }}>
|
|
301
|
+
<SectionHeader
|
|
302
|
+
title="Live demo"
|
|
303
|
+
subtitle="Real, clickable components — not mockups. Resize the Storybook viewport below 768px to see MobileHeader render instead of a squeezed desktop bar."
|
|
304
|
+
/>
|
|
305
|
+
<Frame>
|
|
306
|
+
<ResponsiveHeaderDemo />
|
|
307
|
+
</Frame>
|
|
308
|
+
</section>
|
|
309
|
+
|
|
310
|
+
<section style={{ marginBottom: 'var(--sv-space-10)' }}>
|
|
311
|
+
<SectionHeader
|
|
312
|
+
title="Anatomy"
|
|
313
|
+
subtitle="Brand badge + optional active plugin on the left; launcher, notifications, and avatar menu on the right — always rendered, never optional."
|
|
314
|
+
/>
|
|
315
|
+
<ul style={{ margin: 0, paddingLeft: '1.25rem', color: 'var(--sv-color-text-muted)' }}>
|
|
316
|
+
<li>
|
|
317
|
+
<strong style={{ color: 'var(--sv-color-text-primary)' }}>Brand badge</strong> — derives
|
|
318
|
+
its initial from <code>instanceName</code>, links to <code>homeHref</code>.
|
|
319
|
+
</li>
|
|
320
|
+
<li>
|
|
321
|
+
<strong style={{ color: 'var(--sv-color-text-primary)' }}>Active plugin</strong> (
|
|
322
|
+
<code>plugin</code>, optional) — icon + name, links to <code>plugin.href</code>.
|
|
323
|
+
</li>
|
|
324
|
+
<li>
|
|
325
|
+
<strong style={{ color: 'var(--sv-color-text-primary)' }}>launcher</strong> — the real{' '}
|
|
326
|
+
<code>AppsLauncher</code>.
|
|
327
|
+
</li>
|
|
328
|
+
<li>
|
|
329
|
+
<strong style={{ color: 'var(--sv-color-text-primary)' }}>notifications</strong> — the
|
|
330
|
+
real <code>NotificationsPanel</code>.
|
|
331
|
+
</li>
|
|
332
|
+
<li>
|
|
333
|
+
<strong style={{ color: 'var(--sv-color-text-primary)' }}>avatarMenu</strong> — the real{' '}
|
|
334
|
+
<code>UserMenu</code> (<code>size="sm"</code> in this context — see Sizing
|
|
335
|
+
below).
|
|
336
|
+
</li>
|
|
337
|
+
</ul>
|
|
338
|
+
<CodeBlock>{`<Header
|
|
339
|
+
instanceName="Sovereign"
|
|
340
|
+
homeHref="/launcher"
|
|
341
|
+
plugin={{ name: 'Notes', icon: <PluginIcon />, href: '/notes' }}
|
|
342
|
+
launcher={<AppsLauncher items={apps} />}
|
|
343
|
+
notifications={<NotificationsPanel items={items} unreadCount={n} />}
|
|
344
|
+
avatarMenu={<UserMenu name={user.name} email={user.email} items={menuItems} size="sm" />}
|
|
345
|
+
/>`}</CodeBlock>
|
|
346
|
+
</section>
|
|
347
|
+
|
|
348
|
+
<section style={{ marginBottom: 'var(--sv-space-10)' }}>
|
|
349
|
+
<SectionHeader title="Boundary — presentational only" />
|
|
350
|
+
<Body>
|
|
351
|
+
<code>Header</code> owns layout only — never launcher/notification/account interaction
|
|
352
|
+
logic, matching <code>MobileHeader</code>'s own boundary (RFC 0088).{' '}
|
|
353
|
+
<code>launcher</code>/<code>notifications</code>/<code>avatarMenu</code> are supplied
|
|
354
|
+
fully built by the consumer. In practice that consumer is almost always the real{' '}
|
|
355
|
+
<code>AppsLauncher</code>/<code>NotificationsPanel</code>/<code>UserMenu</code> — see
|
|
356
|
+
their own Components pages — not a bespoke reimplementation.
|
|
357
|
+
</Body>
|
|
358
|
+
</section>
|
|
359
|
+
|
|
360
|
+
<section style={{ marginBottom: 'var(--sv-space-6)' }}>
|
|
361
|
+
<SectionHeader
|
|
362
|
+
title="Sizing"
|
|
363
|
+
subtitle="A compact top bar, deliberately smaller than the platform sidebar — this plugin owns its whole viewport, so it isn't bound to platform chrome dimensions."
|
|
364
|
+
/>
|
|
365
|
+
<ul style={{ margin: 0, paddingLeft: '1.25rem', color: 'var(--sv-color-text-muted)' }}>
|
|
366
|
+
<li>Bar height: 49px (48px content + 1px border).</li>
|
|
367
|
+
<li>Brand badge / plugin icon: 28px.</li>
|
|
368
|
+
<li>Launcher / notifications trigger buttons: 28px, with a 20–24px icon inside.</li>
|
|
369
|
+
<li>
|
|
370
|
+
Avatar: 32px here (<code>UserMenu size="sm"</code>) — smaller than the
|
|
371
|
+
platform sidebar's own 36px avatar (<code>UserMenu</code>'s default), to suit
|
|
372
|
+
this compact top bar's own proportions.
|
|
373
|
+
</li>
|
|
374
|
+
</ul>
|
|
375
|
+
<Callout type="tip">
|
|
376
|
+
Want to try different <code>instanceName</code>/<code>homeHref</code>/<code>plugin</code>{' '}
|
|
377
|
+
values live? See the <strong>Playground</strong> story — its Controls panel is wired up
|
|
378
|
+
for exactly that.
|
|
379
|
+
</Callout>
|
|
380
|
+
</section>
|
|
381
|
+
</div>
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// ---------------------------------------------------------------------------
|
|
386
|
+
// Story
|
|
387
|
+
// ---------------------------------------------------------------------------
|
|
388
|
+
|
|
389
|
+
const meta = {
|
|
390
|
+
title: 'Components/Header',
|
|
391
|
+
component: Header,
|
|
392
|
+
parameters: {
|
|
393
|
+
// fullscreen, not padded — this is a full-bleed top bar; Storybook's own
|
|
394
|
+
// canvas gutter around a `padded` story reads as extra height in a
|
|
395
|
+
// screenshot comparison against the real, edge-to-edge running app. The
|
|
396
|
+
// Default doc page supplies its own centered/padded wrapper internally.
|
|
397
|
+
layout: 'fullscreen',
|
|
398
|
+
docs: {
|
|
399
|
+
description: {
|
|
400
|
+
component:
|
|
401
|
+
'The web top-bar header for shell: minimal plugins — the counterpart to the platform sidebar for a plugin that owns its whole viewport. Brand badge and optional active plugin on the left; launcher (AppsLauncher), notifications (NotificationsPanel), and avatar menu (UserMenu) on the right. Below 768px, plugins render MobileHeader instead.',
|
|
402
|
+
},
|
|
403
|
+
},
|
|
404
|
+
},
|
|
405
|
+
argTypes: {
|
|
406
|
+
instanceName: { control: 'text' },
|
|
407
|
+
homeHref: { control: 'text' },
|
|
408
|
+
plugin: { control: 'object' },
|
|
409
|
+
launcher: { control: false },
|
|
410
|
+
notifications: { control: false },
|
|
411
|
+
avatarMenu: { control: false },
|
|
412
|
+
},
|
|
413
|
+
args: {
|
|
414
|
+
instanceName: 'Sovereign',
|
|
415
|
+
homeHref: '/launcher',
|
|
416
|
+
plugin: {
|
|
417
|
+
name: 'Notes',
|
|
418
|
+
icon: <HeaderPluginIcon name="file" />,
|
|
419
|
+
href: '/notes',
|
|
420
|
+
},
|
|
421
|
+
launcher: <Launcher />,
|
|
422
|
+
notifications: <Notifications />,
|
|
423
|
+
avatarMenu: <Account />,
|
|
424
|
+
},
|
|
425
|
+
} satisfies Meta<typeof Header>;
|
|
426
|
+
|
|
427
|
+
export default meta;
|
|
428
|
+
type StoryDetail = StoryObj<typeof meta>;
|
|
429
|
+
|
|
430
|
+
export const Story: StoryDetail = {
|
|
431
|
+
parameters: {
|
|
432
|
+
controls: { disable: true },
|
|
433
|
+
},
|
|
434
|
+
render: () => <HeaderDoc />,
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* A plain, bare `Header` — use the Controls panel below to try different
|
|
439
|
+
* `instanceName`/`homeHref`/`plugin` values live. `launcher`/`notifications`/
|
|
440
|
+
* `avatarMenu` stay fixed to the real shared components (they're ReactNode
|
|
441
|
+
* slots — not meaningfully editable as Controls). `plugin.icon` is omitted
|
|
442
|
+
* here (unlike the live demo above) so the object control stays plain,
|
|
443
|
+
* editable JSON rather than a React element it can't serialize.
|
|
444
|
+
*/
|
|
445
|
+
export const Playground: StoryDetail = {
|
|
446
|
+
args: {
|
|
447
|
+
plugin: {
|
|
448
|
+
name: 'Notes',
|
|
449
|
+
href: '/notes',
|
|
450
|
+
},
|
|
451
|
+
},
|
|
452
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactNode } from 'react';
|
|
2
|
+
import styles from './Header.module.css';
|
|
3
|
+
|
|
4
|
+
export interface HeaderPlugin {
|
|
5
|
+
/** Plugin display name, rendered next to its icon. */
|
|
6
|
+
name: string;
|
|
7
|
+
/** Plugin icon — already sized (28px) and built by the consumer (typically
|
|
8
|
+
* an `<img src="/plugin-icons/<id>.svg">`), matching `MobileHeader`'s own
|
|
9
|
+
* router-agnostic convention for icon-shaped slots. */
|
|
10
|
+
icon?: ReactNode;
|
|
11
|
+
/** Where the plugin name/icon links to — typically the plugin's own
|
|
12
|
+
* `routePrefix`. Omit to render plain (non-interactive) text instead of a
|
|
13
|
+
* link — e.g. a plugin's own home page linking to itself would be a no-op. */
|
|
14
|
+
href?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface HeaderProps extends HTMLAttributes<HTMLElement> {
|
|
18
|
+
/** Instance name — derives the brand badge's initial (its first character,
|
|
19
|
+
* uppercased) and the badge's accessible label. Does not render as text
|
|
20
|
+
* anywhere in this component (unlike `MobileHeader`'s optional `title`). */
|
|
21
|
+
instanceName: string;
|
|
22
|
+
/** Optional instance logo image; falls back to `instanceName`'s initial
|
|
23
|
+
* (matching the runtime shell's own sidebar/mobile-header fallback). */
|
|
24
|
+
instanceLogoUrl?: string;
|
|
25
|
+
/** Where the brand badge links — typically the Launcher plugin's route. */
|
|
26
|
+
homeHref: string;
|
|
27
|
+
/** The active plugin, rendered next to the brand badge. Omit for a page
|
|
28
|
+
* with no single active plugin (e.g. the Launcher itself). */
|
|
29
|
+
plugin?: HeaderPlugin;
|
|
30
|
+
/** Apps launcher trigger — already wired by the consumer (icon button plus
|
|
31
|
+
* whatever popover/drawer it opens). Always rendered, matching
|
|
32
|
+
* `MobileHeader`'s bell/avatarMenu boundary: this component owns layout
|
|
33
|
+
* only, never launcher/notification/account interaction logic. */
|
|
34
|
+
launcher: ReactNode;
|
|
35
|
+
/** Notification bell trigger — always rendered; not overridable. Same
|
|
36
|
+
* "just an icon slot" boundary as `MobileHeader`'s own `bell` prop. */
|
|
37
|
+
notifications: ReactNode;
|
|
38
|
+
/** Avatar / account menu trigger — always rendered; not overridable. */
|
|
39
|
+
avatarMenu: ReactNode;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The web top-bar header: brand badge (+ optional active plugin) on the
|
|
44
|
+
* left, launcher · notifications · avatar menu on the right. This is the
|
|
45
|
+
* `shell: minimal` counterpart to the runtime shell's own sidebar — a plugin
|
|
46
|
+
* that owns its whole viewport (no platform sidebar) renders this instead,
|
|
47
|
+
* the same way it renders `MobileHeader` in place of the platform's mobile
|
|
48
|
+
* chrome. Extracted into a shared component so a `shell: minimal` plugin
|
|
49
|
+
* doesn't have to hand-roll its own top bar from scratch.
|
|
50
|
+
*
|
|
51
|
+
* Presentational only — no data fetching, no account/notification/launcher
|
|
52
|
+
* logic of its own. `launcher`/`notifications`/`avatarMenu` are supplied
|
|
53
|
+
* fully built, exactly like `MobileHeader`'s `bell`/`avatarMenu`: this
|
|
54
|
+
* component stays router-agnostic and interaction-agnostic, and never
|
|
55
|
+
* duplicates a real `AccountMenu`/notification-panel/apps-switcher
|
|
56
|
+
* implementation.
|
|
57
|
+
*/
|
|
58
|
+
export function Header({
|
|
59
|
+
instanceName,
|
|
60
|
+
instanceLogoUrl,
|
|
61
|
+
homeHref,
|
|
62
|
+
plugin,
|
|
63
|
+
launcher,
|
|
64
|
+
notifications,
|
|
65
|
+
avatarMenu,
|
|
66
|
+
className,
|
|
67
|
+
...rest
|
|
68
|
+
}: HeaderProps) {
|
|
69
|
+
const brandInitial = instanceName.charAt(0).toUpperCase() || 'S';
|
|
70
|
+
const cls = [styles.header, className].filter(Boolean).join(' ');
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<header className={cls} {...rest}>
|
|
74
|
+
<div className={styles.left}>
|
|
75
|
+
<a href={homeHref} className={styles.brandBadge} aria-label={`${instanceName} home`}>
|
|
76
|
+
{instanceLogoUrl ? (
|
|
77
|
+
<img src={instanceLogoUrl} alt="" className={styles.brandLogoImg} />
|
|
78
|
+
) : (
|
|
79
|
+
brandInitial
|
|
80
|
+
)}
|
|
81
|
+
</a>
|
|
82
|
+
{plugin &&
|
|
83
|
+
(plugin.href ? (
|
|
84
|
+
<a href={plugin.href} className={styles.plugin}>
|
|
85
|
+
{plugin.icon}
|
|
86
|
+
<span className={styles.pluginName}>{plugin.name}</span>
|
|
87
|
+
</a>
|
|
88
|
+
) : (
|
|
89
|
+
<span className={styles.plugin}>
|
|
90
|
+
{plugin.icon}
|
|
91
|
+
<span className={styles.pluginName}>{plugin.name}</span>
|
|
92
|
+
</span>
|
|
93
|
+
))}
|
|
94
|
+
</div>
|
|
95
|
+
<div className={styles.right}>
|
|
96
|
+
{launcher}
|
|
97
|
+
{notifications}
|
|
98
|
+
{avatarMenu}
|
|
99
|
+
</div>
|
|
100
|
+
</header>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
3
|
+
import { cleanup, render, screen } from '@testing-library/react';
|
|
4
|
+
import { Header } from '../Header';
|
|
5
|
+
|
|
6
|
+
afterEach(() => {
|
|
7
|
+
cleanup();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
describe('Header', () => {
|
|
11
|
+
it('always renders the brand badge, launcher, notifications, and avatar menu slots', () => {
|
|
12
|
+
render(
|
|
13
|
+
<Header
|
|
14
|
+
instanceName="Sovereign"
|
|
15
|
+
homeHref="/launcher"
|
|
16
|
+
launcher={<span data-testid="launcher">launcher</span>}
|
|
17
|
+
notifications={<span data-testid="bell">bell</span>}
|
|
18
|
+
avatarMenu={<span data-testid="avatar">avatar</span>}
|
|
19
|
+
/>,
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
expect(screen.getByText('S')).toBeDefined();
|
|
23
|
+
expect(screen.getByTestId('launcher')).toBeDefined();
|
|
24
|
+
expect(screen.getByTestId('bell')).toBeDefined();
|
|
25
|
+
expect(screen.getByTestId('avatar')).toBeDefined();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('derives the brand badge initial from instanceName, uppercased', () => {
|
|
29
|
+
render(
|
|
30
|
+
<Header
|
|
31
|
+
instanceName="acme"
|
|
32
|
+
homeHref="/launcher"
|
|
33
|
+
launcher={<span />}
|
|
34
|
+
notifications={<span />}
|
|
35
|
+
avatarMenu={<span />}
|
|
36
|
+
/>,
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
expect(screen.getByText('A')).toBeDefined();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('links the brand badge to homeHref', () => {
|
|
43
|
+
render(
|
|
44
|
+
<Header
|
|
45
|
+
instanceName="Sovereign"
|
|
46
|
+
homeHref="/launcher"
|
|
47
|
+
launcher={<span />}
|
|
48
|
+
notifications={<span />}
|
|
49
|
+
avatarMenu={<span />}
|
|
50
|
+
/>,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
expect(screen.getByRole('link', { name: 'Sovereign home' })).toHaveProperty(
|
|
54
|
+
'href',
|
|
55
|
+
expect.stringContaining('/launcher'),
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('renders no plugin by default', () => {
|
|
60
|
+
render(
|
|
61
|
+
<Header
|
|
62
|
+
instanceName="Sovereign"
|
|
63
|
+
homeHref="/launcher"
|
|
64
|
+
launcher={<span />}
|
|
65
|
+
notifications={<span />}
|
|
66
|
+
avatarMenu={<span />}
|
|
67
|
+
/>,
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
expect(screen.queryByText('Notes')).toBeNull();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('renders the active plugin as a link when href is provided', () => {
|
|
74
|
+
render(
|
|
75
|
+
<Header
|
|
76
|
+
instanceName="Sovereign"
|
|
77
|
+
homeHref="/launcher"
|
|
78
|
+
plugin={{ name: 'Notes', icon: <span>icon</span>, href: '/notes' }}
|
|
79
|
+
launcher={<span />}
|
|
80
|
+
notifications={<span />}
|
|
81
|
+
avatarMenu={<span />}
|
|
82
|
+
/>,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const link = screen.getByRole('link', { name: /Notes/ });
|
|
86
|
+
expect(link).toHaveProperty('href', expect.stringContaining('/notes'));
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('renders the active plugin as plain text when href is omitted', () => {
|
|
90
|
+
render(
|
|
91
|
+
<Header
|
|
92
|
+
instanceName="Sovereign"
|
|
93
|
+
homeHref="/launcher"
|
|
94
|
+
plugin={{ name: 'Notes' }}
|
|
95
|
+
launcher={<span />}
|
|
96
|
+
notifications={<span />}
|
|
97
|
+
avatarMenu={<span />}
|
|
98
|
+
/>,
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
expect(screen.getByText('Notes')).toBeDefined();
|
|
102
|
+
expect(screen.queryByRole('link', { name: /Notes/ })).toBeNull();
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
.shell {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
height: 100%;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.header {
|
|
8
|
+
flex-shrink: 0;
|
|
9
|
+
overflow: hidden;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.main {
|
|
13
|
+
flex: 1;
|
|
14
|
+
min-height: 0;
|
|
15
|
+
overflow-y: auto;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.footer {
|
|
19
|
+
flex-shrink: 0;
|
|
20
|
+
overflow: hidden;
|
|
21
|
+
}
|