@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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
|
+
import { StrictMode, useState } from 'react';
|
|
2
3
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
3
4
|
import { cleanup, render, screen, fireEvent } from '@testing-library/react';
|
|
4
5
|
import { Popover } from '../Popover';
|
|
@@ -76,6 +77,53 @@ describe('Popover', () => {
|
|
|
76
77
|
expect(onClose).toHaveBeenCalledOnce();
|
|
77
78
|
});
|
|
78
79
|
|
|
80
|
+
describe('Escape precedence with a nested Popover', () => {
|
|
81
|
+
it('defers Escape to a nested open Popover instead of closing the outer one', () => {
|
|
82
|
+
const outerOnClose = vi.fn();
|
|
83
|
+
|
|
84
|
+
function NestedHarness() {
|
|
85
|
+
// Starts closed and opens via a later click, matching the real
|
|
86
|
+
// sequence (e.g. a date-picker Popover opened from a field inside a
|
|
87
|
+
// Combobox's own popover content) — mounting both already-open in
|
|
88
|
+
// the same initial render would instead have React fire the child's
|
|
89
|
+
// registration effect before the parent's in that one shared commit,
|
|
90
|
+
// the one case this stack-order approach doesn't handle (see the
|
|
91
|
+
// equivalent Dialog+ConfirmDialog regression test's own note).
|
|
92
|
+
const [innerOpen, setInnerOpen] = useState(false);
|
|
93
|
+
return (
|
|
94
|
+
<Popover open={true} onClose={outerOnClose} aria-label="Outer" trigger={trigger}>
|
|
95
|
+
<button type="button" onClick={() => setInnerOpen(true)}>
|
|
96
|
+
Open inner
|
|
97
|
+
</button>
|
|
98
|
+
<Popover
|
|
99
|
+
open={innerOpen}
|
|
100
|
+
onClose={() => setInnerOpen(false)}
|
|
101
|
+
aria-label="Inner"
|
|
102
|
+
trigger={<span />}
|
|
103
|
+
>
|
|
104
|
+
Inner content
|
|
105
|
+
</Popover>
|
|
106
|
+
</Popover>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
render(<NestedHarness />);
|
|
111
|
+
fireEvent.click(screen.getByRole('button', { name: 'Open inner' }));
|
|
112
|
+
expect(screen.getByRole('dialog', { name: 'Inner' })).toBeTruthy();
|
|
113
|
+
|
|
114
|
+
// The nested Popover is topmost — the outer Popover's own Escape
|
|
115
|
+
// listener must not act while it's open.
|
|
116
|
+
fireEvent.keyDown(document, { key: 'Escape' });
|
|
117
|
+
expect(outerOnClose).not.toHaveBeenCalled();
|
|
118
|
+
expect(screen.queryByRole('dialog', { name: 'Inner' })).toBeNull();
|
|
119
|
+
|
|
120
|
+
// With the inner one dismissed, the outer Popover is topmost again —
|
|
121
|
+
// a second Escape now reaches it.
|
|
122
|
+
fireEvent.keyDown(document, { key: 'Escape' });
|
|
123
|
+
expect(outerOnClose).toHaveBeenCalledOnce();
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
79
127
|
it('does not call onClose when clicking inside the panel', () => {
|
|
80
128
|
const onClose = vi.fn();
|
|
81
129
|
render(
|
|
@@ -95,4 +143,121 @@ describe('Popover', () => {
|
|
|
95
143
|
);
|
|
96
144
|
expect((screen.getByRole('dialog') as HTMLElement).style.width).toBe('320px');
|
|
97
145
|
});
|
|
146
|
+
|
|
147
|
+
describe('horizontal viewport clamp', () => {
|
|
148
|
+
const rect = (partial: Partial<DOMRect>): DOMRect =>
|
|
149
|
+
({
|
|
150
|
+
top: 0,
|
|
151
|
+
left: 0,
|
|
152
|
+
right: 0,
|
|
153
|
+
bottom: 0,
|
|
154
|
+
width: 0,
|
|
155
|
+
height: 0,
|
|
156
|
+
x: 0,
|
|
157
|
+
y: 0,
|
|
158
|
+
toJSON() {},
|
|
159
|
+
...partial,
|
|
160
|
+
}) as DOMRect;
|
|
161
|
+
|
|
162
|
+
afterEach(() => {
|
|
163
|
+
vi.restoreAllMocks();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('shifts the panel back on-screen when align="right" would overflow the left edge', () => {
|
|
167
|
+
// A 340px-wide panel right-aligned under a trigger sitting near the
|
|
168
|
+
// right edge of a 375px viewport (e.g. a mobile header bell) — its
|
|
169
|
+
// natural `right: 0` position runs off the left edge, mirroring the
|
|
170
|
+
// real KanbanNotificationBell overflow found live-testing at 375px.
|
|
171
|
+
Object.defineProperty(window, 'innerWidth', { value: 375, configurable: true });
|
|
172
|
+
Object.defineProperty(window, 'innerHeight', { value: 800, configurable: true });
|
|
173
|
+
const containerRect = rect({ left: 275, right: 319, top: 12, bottom: 56 });
|
|
174
|
+
const panelRect = rect({
|
|
175
|
+
left: -21,
|
|
176
|
+
right: 319,
|
|
177
|
+
top: 64,
|
|
178
|
+
bottom: 200,
|
|
179
|
+
width: 340,
|
|
180
|
+
height: 136,
|
|
181
|
+
});
|
|
182
|
+
vi.spyOn(Element.prototype, 'getBoundingClientRect').mockImplementation(function (
|
|
183
|
+
this: Element,
|
|
184
|
+
) {
|
|
185
|
+
return this.getAttribute('role') === 'dialog' ? panelRect : containerRect;
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
render(
|
|
189
|
+
<Popover open={true} onClose={() => {}} aria-label="Menu" width={340} trigger={trigger}>
|
|
190
|
+
Content
|
|
191
|
+
</Popover>,
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
const panel = screen.getByRole('dialog') as HTMLElement;
|
|
195
|
+
// containerRect.left (275) + style.left must land the panel's left
|
|
196
|
+
// edge at the 4px viewport margin: 4 - 275 = -271.
|
|
197
|
+
expect(panel.style.left).toBe('-271px');
|
|
198
|
+
expect(panel.style.right).toBe('auto');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('leaves alignment untouched when the panel already fits the viewport', () => {
|
|
202
|
+
Object.defineProperty(window, 'innerWidth', { value: 1280, configurable: true });
|
|
203
|
+
Object.defineProperty(window, 'innerHeight', { value: 800, configurable: true });
|
|
204
|
+
const containerRect = rect({ left: 900, right: 944, top: 12, bottom: 56 });
|
|
205
|
+
const panelRect = rect({
|
|
206
|
+
left: 604,
|
|
207
|
+
right: 944,
|
|
208
|
+
top: 64,
|
|
209
|
+
bottom: 200,
|
|
210
|
+
width: 340,
|
|
211
|
+
height: 136,
|
|
212
|
+
});
|
|
213
|
+
vi.spyOn(Element.prototype, 'getBoundingClientRect').mockImplementation(function (
|
|
214
|
+
this: Element,
|
|
215
|
+
) {
|
|
216
|
+
return this.getAttribute('role') === 'dialog' ? panelRect : containerRect;
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
render(
|
|
220
|
+
<Popover open={true} onClose={() => {}} aria-label="Menu" width={340} trigger={trigger}>
|
|
221
|
+
Content
|
|
222
|
+
</Popover>,
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
const panel = screen.getByRole('dialog') as HTMLElement;
|
|
226
|
+
expect(panel.style.left).toBe('');
|
|
227
|
+
expect(panel.style.right).toBe('');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('stays clamped under StrictMode double-invoked layout effects', () => {
|
|
231
|
+
// Regression test: an earlier version of this clamp measured the
|
|
232
|
+
// panel's *current* `left`/`right` off the DOM to decide whether a
|
|
233
|
+
// correction was needed. That's not idempotent — StrictMode's dev-only
|
|
234
|
+
// double-invoke of layout effects re-ran the same measurement against
|
|
235
|
+
// the panel's own just-applied correction, saw a rect that already
|
|
236
|
+
// looked fine, and reset `horizontalOffset` back to null before first
|
|
237
|
+
// paint (found live: the panel visibly overflowed again after HMR).
|
|
238
|
+
// The fix derives the natural position analytically from `containerRect`
|
|
239
|
+
// instead, which StrictMode's replay can't perturb.
|
|
240
|
+
Object.defineProperty(window, 'innerWidth', { value: 375, configurable: true });
|
|
241
|
+
Object.defineProperty(window, 'innerHeight', { value: 800, configurable: true });
|
|
242
|
+
const containerRect = rect({ left: 275, right: 319, top: 12, bottom: 56 });
|
|
243
|
+
const panelWidthOnly = rect({ width: 340, height: 136, top: 64, bottom: 200 });
|
|
244
|
+
vi.spyOn(Element.prototype, 'getBoundingClientRect').mockImplementation(function (
|
|
245
|
+
this: Element,
|
|
246
|
+
) {
|
|
247
|
+
return this.getAttribute('role') === 'dialog' ? panelWidthOnly : containerRect;
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
render(
|
|
251
|
+
<StrictMode>
|
|
252
|
+
<Popover open={true} onClose={() => {}} aria-label="Menu" width={340} trigger={trigger}>
|
|
253
|
+
Content
|
|
254
|
+
</Popover>
|
|
255
|
+
</StrictMode>,
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
const panel = screen.getByRole('dialog') as HTMLElement;
|
|
259
|
+
expect(panel.style.left).toBe('-271px');
|
|
260
|
+
expect(panel.style.right).toBe('auto');
|
|
261
|
+
});
|
|
262
|
+
});
|
|
98
263
|
});
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
align-items: center;
|
|
4
4
|
gap: var(--sv-space-1);
|
|
5
5
|
padding: var(--sv-space-1);
|
|
6
|
-
border:
|
|
6
|
+
border: var(--sv-border-width-hairline) solid var(--sv-color-border-strong);
|
|
7
7
|
border-radius: var(--sv-radius-full);
|
|
8
8
|
background: var(--sv-color-surface-sunken);
|
|
9
9
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
width: 100%;
|
|
5
5
|
height: 100%;
|
|
6
6
|
min-height: 320px;
|
|
7
|
-
border:
|
|
7
|
+
border: var(--sv-border-width-hairline) solid var(--sv-color-border);
|
|
8
8
|
border-radius: var(--sv-radius-lg);
|
|
9
9
|
overflow: hidden;
|
|
10
10
|
background: var(--sv-color-surface);
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
width: 12px;
|
|
28
28
|
padding: 0;
|
|
29
29
|
border: 0;
|
|
30
|
-
border-inline:
|
|
30
|
+
border-inline: var(--sv-border-width-hairline) solid var(--sv-color-border);
|
|
31
31
|
cursor: col-resize;
|
|
32
32
|
background: var(--sv-color-surface-sunken);
|
|
33
33
|
}
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
width: 100%;
|
|
37
37
|
height: 12px;
|
|
38
38
|
border-inline: 0;
|
|
39
|
-
border-block:
|
|
39
|
+
border-block: var(--sv-border-width-hairline) solid var(--sv-color-border);
|
|
40
40
|
cursor: row-resize;
|
|
41
41
|
}
|
|
42
42
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { useResponsiveLayout } from '../../hooks/useResponsiveLayout';
|
|
3
|
+
|
|
4
|
+
export interface ResponsiveSurfaceProps {
|
|
5
|
+
web: ReactNode;
|
|
6
|
+
mobile: ReactNode;
|
|
7
|
+
breakpointPx?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* ResponsiveSurface — thin JSX convenience wrapper around
|
|
12
|
+
* `useResponsiveLayout` for the common two-tree case: render an entirely
|
|
13
|
+
* different component tree below the breakpoint, not a CSS squeeze of the
|
|
14
|
+
* same one. Renders only whichever side is active — the other tree is never
|
|
15
|
+
* mounted.
|
|
16
|
+
*
|
|
17
|
+
* This does not replace a plugin's own shell component (e.g. a
|
|
18
|
+
* `MobileAwareShell` that also threads plugin-specific props like a refresh
|
|
19
|
+
* signal) — those stay a thin per-plugin wrapper around this.
|
|
20
|
+
*/
|
|
21
|
+
export function ResponsiveSurface({ web, mobile, breakpointPx }: ResponsiveSurfaceProps) {
|
|
22
|
+
const { value } = useResponsiveLayout({ web, mobile, breakpointPx });
|
|
23
|
+
return <>{value}</>;
|
|
24
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { act, cleanup, render, screen } from '@testing-library/react';
|
|
4
|
+
import { ResponsiveSurface } from '../ResponsiveSurface';
|
|
5
|
+
|
|
6
|
+
let changeHandler: ((e: MediaQueryListEvent) => void) | null = null;
|
|
7
|
+
let matches = false;
|
|
8
|
+
|
|
9
|
+
function installMatchMedia() {
|
|
10
|
+
changeHandler = null;
|
|
11
|
+
vi.stubGlobal(
|
|
12
|
+
'matchMedia',
|
|
13
|
+
vi.fn().mockImplementation((query: string) => ({
|
|
14
|
+
matches,
|
|
15
|
+
media: query,
|
|
16
|
+
addEventListener: (_: string, cb: (e: MediaQueryListEvent) => void) => {
|
|
17
|
+
changeHandler = cb;
|
|
18
|
+
},
|
|
19
|
+
removeEventListener: vi.fn(),
|
|
20
|
+
addListener: vi.fn(),
|
|
21
|
+
removeListener: vi.fn(),
|
|
22
|
+
dispatchEvent: vi.fn(),
|
|
23
|
+
})),
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('ResponsiveSurface', () => {
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
matches = false;
|
|
30
|
+
installMatchMedia();
|
|
31
|
+
});
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
vi.unstubAllGlobals();
|
|
34
|
+
cleanup();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('renders the web tree by default and never mounts the mobile tree', () => {
|
|
38
|
+
render(
|
|
39
|
+
<ResponsiveSurface
|
|
40
|
+
web={<div data-testid="web">web</div>}
|
|
41
|
+
mobile={<div data-testid="mobile">mobile</div>}
|
|
42
|
+
/>,
|
|
43
|
+
);
|
|
44
|
+
expect(screen.getByTestId('web')).toBeDefined();
|
|
45
|
+
expect(screen.queryByTestId('mobile')).toBeNull();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('renders only the mobile tree below the breakpoint', () => {
|
|
49
|
+
matches = true;
|
|
50
|
+
installMatchMedia();
|
|
51
|
+
render(
|
|
52
|
+
<ResponsiveSurface
|
|
53
|
+
web={<div data-testid="web">web</div>}
|
|
54
|
+
mobile={<div data-testid="mobile">mobile</div>}
|
|
55
|
+
/>,
|
|
56
|
+
);
|
|
57
|
+
expect(screen.getByTestId('mobile')).toBeDefined();
|
|
58
|
+
expect(screen.queryByTestId('web')).toBeNull();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('swaps trees live when the viewport crosses the breakpoint', () => {
|
|
62
|
+
render(
|
|
63
|
+
<ResponsiveSurface
|
|
64
|
+
web={<div data-testid="web">web</div>}
|
|
65
|
+
mobile={<div data-testid="mobile">mobile</div>}
|
|
66
|
+
/>,
|
|
67
|
+
);
|
|
68
|
+
expect(screen.getByTestId('web')).toBeDefined();
|
|
69
|
+
act(() => changeHandler?.({ matches: true } as MediaQueryListEvent));
|
|
70
|
+
expect(screen.getByTestId('mobile')).toBeDefined();
|
|
71
|
+
expect(screen.queryByTestId('web')).toBeNull();
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/* A definite height, not min-height: ThreeColumnLayout/HeaderFooterLayout's
|
|
2
|
+
own .shell use height: 100% internally (fixed box, independently-
|
|
3
|
+
scrolling children via overflow-y: auto) — a percentage height only
|
|
4
|
+
resolves against an ancestor with a definite (not min-constrained)
|
|
5
|
+
height, so min-height here would leave their .shell unable to resolve
|
|
6
|
+
and collapse to content size. This doesn't clip taller content on its
|
|
7
|
+
own — nothing here sets overflow: hidden, so content taller than the
|
|
8
|
+
box simply overflows past it rather than being cut off. */
|
|
9
|
+
.root {
|
|
10
|
+
width: 100%;
|
|
11
|
+
height: 100%;
|
|
12
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { RootLayout } from './RootLayout';
|
|
3
|
+
|
|
4
|
+
function Block({ label }: { label: string }) {
|
|
5
|
+
return (
|
|
6
|
+
<div
|
|
7
|
+
style={{
|
|
8
|
+
display: 'flex',
|
|
9
|
+
alignItems: 'center',
|
|
10
|
+
justifyContent: 'center',
|
|
11
|
+
height: '100%',
|
|
12
|
+
padding: 'var(--sv-space-4)',
|
|
13
|
+
fontFamily: 'var(--sv-font-family)',
|
|
14
|
+
fontSize: 'var(--sv-font-size-sm)',
|
|
15
|
+
fontWeight: 600,
|
|
16
|
+
color: 'var(--sv-color-text-primary)',
|
|
17
|
+
background: 'var(--sv-color-surface-raised)',
|
|
18
|
+
}}
|
|
19
|
+
>
|
|
20
|
+
{label}
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const meta = {
|
|
26
|
+
title: 'Components/RootLayout',
|
|
27
|
+
component: RootLayout,
|
|
28
|
+
parameters: {
|
|
29
|
+
layout: 'fullscreen',
|
|
30
|
+
docs: {
|
|
31
|
+
description: {
|
|
32
|
+
component:
|
|
33
|
+
"The root-level layout a plugin's page tree sits in. Enforces only structure and dimensions — content always comes from children. Each variant is a fixed web+mobile pairing, not two independent axes; resize the viewport (or check the Mobile story) to see the fork. 'header' keeps the same header+main structure on both breakpoints, but headerHeight (48px, web) and mobileHeaderHeight (60px, mobile) are deliberately different — matching how real shell: minimal plugins do this in production.",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
args: {
|
|
38
|
+
children: <Block label="Main" />,
|
|
39
|
+
},
|
|
40
|
+
} satisfies Meta<typeof RootLayout>;
|
|
41
|
+
|
|
42
|
+
export default meta;
|
|
43
|
+
type Story = StoryObj<typeof meta>;
|
|
44
|
+
|
|
45
|
+
/** variant="plain" is the prop's own default — a single child, no chrome. */
|
|
46
|
+
export const Default: Story = {
|
|
47
|
+
render: () => (
|
|
48
|
+
<div style={{ height: '100vh' }}>
|
|
49
|
+
<RootLayout variant="plain">
|
|
50
|
+
<Block label="Main" />
|
|
51
|
+
</RootLayout>
|
|
52
|
+
</div>
|
|
53
|
+
),
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const Sidebar: Story = {
|
|
57
|
+
render: () => (
|
|
58
|
+
<div style={{ height: '100vh' }}>
|
|
59
|
+
<RootLayout variant="sidebar">
|
|
60
|
+
<Block label="Sidebar" />
|
|
61
|
+
<Block label="Main" />
|
|
62
|
+
</RootLayout>
|
|
63
|
+
</div>
|
|
64
|
+
),
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const SidebarMobile: Story = {
|
|
68
|
+
parameters: { viewport: { defaultViewport: 'mobile1' } },
|
|
69
|
+
render: () => (
|
|
70
|
+
<div style={{ height: '100vh' }}>
|
|
71
|
+
<RootLayout variant="sidebar">
|
|
72
|
+
<Block label="Sidebar (hidden on mobile)" />
|
|
73
|
+
<Block label="Main" />
|
|
74
|
+
</RootLayout>
|
|
75
|
+
</div>
|
|
76
|
+
),
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const Header: Story = {
|
|
80
|
+
render: () => (
|
|
81
|
+
<div style={{ height: '100vh' }}>
|
|
82
|
+
<RootLayout variant="header">
|
|
83
|
+
<Block label="Header (48px)" />
|
|
84
|
+
<Block label="Main" />
|
|
85
|
+
</RootLayout>
|
|
86
|
+
</div>
|
|
87
|
+
),
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export const HeaderMobile: Story = {
|
|
91
|
+
parameters: { viewport: { defaultViewport: 'mobile1' } },
|
|
92
|
+
render: () => (
|
|
93
|
+
<div style={{ height: '100vh' }}>
|
|
94
|
+
<RootLayout variant="header">
|
|
95
|
+
<Block label="Header (60px)" />
|
|
96
|
+
<Block label="Main" />
|
|
97
|
+
</RootLayout>
|
|
98
|
+
</div>
|
|
99
|
+
),
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export const Shell: Story = {
|
|
103
|
+
render: () => (
|
|
104
|
+
<div style={{ height: '100vh' }}>
|
|
105
|
+
<RootLayout variant="shell">
|
|
106
|
+
<Block label="Header (mobile only)" />
|
|
107
|
+
<Block label="Main" />
|
|
108
|
+
<Block label="Footer (mobile only)" />
|
|
109
|
+
</RootLayout>
|
|
110
|
+
</div>
|
|
111
|
+
),
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export const ShellMobile: Story = {
|
|
115
|
+
parameters: { viewport: { defaultViewport: 'mobile1' } },
|
|
116
|
+
render: () => (
|
|
117
|
+
<div style={{ height: '100vh' }}>
|
|
118
|
+
<RootLayout variant="shell">
|
|
119
|
+
<Block label="Header" />
|
|
120
|
+
<Block label="Main" />
|
|
121
|
+
<Block label="Footer" />
|
|
122
|
+
</RootLayout>
|
|
123
|
+
</div>
|
|
124
|
+
),
|
|
125
|
+
};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Children } from 'react';
|
|
4
|
+
import type { ReactNode } from 'react';
|
|
5
|
+
import { HeaderFooterLayout } from '../HeaderFooterLayout/HeaderFooterLayout';
|
|
6
|
+
import { ResponsiveSurface } from '../ResponsiveSurface/ResponsiveSurface';
|
|
7
|
+
import { ThreeColumnLayout } from '../ThreeColumnLayout/ThreeColumnLayout';
|
|
8
|
+
import styles from './RootLayout.module.css';
|
|
9
|
+
|
|
10
|
+
export type RootLayoutVariant = 'plain' | 'sidebar' | 'header' | 'shell';
|
|
11
|
+
|
|
12
|
+
export interface RootLayoutProps {
|
|
13
|
+
/**
|
|
14
|
+
* Which chrome shape this page uses. Children are positional and their
|
|
15
|
+
* count is fixed per variant:
|
|
16
|
+
* - `'plain'` — 1 child: `[main]`. No chrome on either breakpoint.
|
|
17
|
+
* - `'sidebar'` — 2 children: `[sidebar, main]`. Sidebar + main on web
|
|
18
|
+
* only — on mobile, sidebar is dropped and only main renders.
|
|
19
|
+
* - `'header'` — 2 children: `[header, main]`. Header + main on both
|
|
20
|
+
* breakpoints — same structure, but the header's own height differs
|
|
21
|
+
* per breakpoint (`headerHeight` on web, `mobileHeaderHeight` on
|
|
22
|
+
* mobile), matching how real `shell: minimal` plugins actually do
|
|
23
|
+
* this (a compact web top bar vs. the platform's mobile chrome
|
|
24
|
+
* height).
|
|
25
|
+
* - `'shell'` — 3 children: `[header, main, footer]`. Header + main +
|
|
26
|
+
* footer on mobile only — on web, header/footer are dropped and only
|
|
27
|
+
* main renders.
|
|
28
|
+
* Default `'plain'`.
|
|
29
|
+
*/
|
|
30
|
+
variant?: RootLayoutVariant;
|
|
31
|
+
children: ReactNode;
|
|
32
|
+
/** px. Default 64 — matches the platform's own persistent nav rail
|
|
33
|
+
* (`--sv-shell-sidebar-width`, runtime/app/(platform)/shell.module.css).
|
|
34
|
+
* Only used by `'sidebar'`. */
|
|
35
|
+
sidebarWidth?: number;
|
|
36
|
+
/** px. Default 48 — matches a real `shell: minimal` plugin's own compact
|
|
37
|
+
* web header (e.g. Kanban's `KanbanHeader`, deliberately not the
|
|
38
|
+
* platform's 60px shell header since a `shell: minimal` plugin owns its
|
|
39
|
+
* whole viewport). Web only — used by `'header'` on web. */
|
|
40
|
+
headerHeight?: number;
|
|
41
|
+
/** px. Default 60 — matches the platform's own mobile chrome
|
|
42
|
+
* (`--sv-shell-header-height`, `MobileHeader`'s default). Mobile only —
|
|
43
|
+
* used by `'header'` and `'shell'` on mobile. */
|
|
44
|
+
mobileHeaderHeight?: number;
|
|
45
|
+
/** px. Default 60 — matches the platform's own mobile chrome
|
|
46
|
+
* (`--sv-shell-footer-height`, `MobileFooter`'s default). Mobile only —
|
|
47
|
+
* used by `'shell'`. */
|
|
48
|
+
footerHeight?: number;
|
|
49
|
+
className?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const EXPECTED_CHILD_COUNT: Record<RootLayoutVariant, number> = {
|
|
53
|
+
plain: 1,
|
|
54
|
+
sidebar: 2,
|
|
55
|
+
header: 2,
|
|
56
|
+
shell: 3,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
interface RootLayoutWebProps {
|
|
60
|
+
variant: RootLayoutVariant;
|
|
61
|
+
items: ReactNode[];
|
|
62
|
+
sidebarWidth: number;
|
|
63
|
+
headerHeight: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// internal — not exported
|
|
67
|
+
function RootLayoutWeb({ variant, items, sidebarWidth, headerHeight }: RootLayoutWebProps) {
|
|
68
|
+
if (variant === 'sidebar') {
|
|
69
|
+
const [sidebar, main] = items;
|
|
70
|
+
return (
|
|
71
|
+
<ThreeColumnLayout sidebarWidth={sidebarWidth}>
|
|
72
|
+
{sidebar}
|
|
73
|
+
{main}
|
|
74
|
+
</ThreeColumnLayout>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
if (variant === 'header') {
|
|
78
|
+
const [header, main] = items;
|
|
79
|
+
return (
|
|
80
|
+
<HeaderFooterLayout header={header} headerHeight={headerHeight}>
|
|
81
|
+
{main}
|
|
82
|
+
</HeaderFooterLayout>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
// 'plain' and 'shell' both render plain on web — shell's chrome is mobile-only.
|
|
86
|
+
const main = variant === 'shell' ? items[1] : items[0];
|
|
87
|
+
return <>{main}</>;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
interface RootLayoutMobileProps {
|
|
91
|
+
variant: RootLayoutVariant;
|
|
92
|
+
items: ReactNode[];
|
|
93
|
+
mobileHeaderHeight: number;
|
|
94
|
+
footerHeight: number;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// internal — not exported
|
|
98
|
+
function RootLayoutMobile({
|
|
99
|
+
variant,
|
|
100
|
+
items,
|
|
101
|
+
mobileHeaderHeight,
|
|
102
|
+
footerHeight,
|
|
103
|
+
}: RootLayoutMobileProps) {
|
|
104
|
+
if (variant === 'header') {
|
|
105
|
+
const [header, main] = items;
|
|
106
|
+
return (
|
|
107
|
+
<HeaderFooterLayout header={header} headerHeight={mobileHeaderHeight}>
|
|
108
|
+
{main}
|
|
109
|
+
</HeaderFooterLayout>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
if (variant === 'shell') {
|
|
113
|
+
const [header, main, footer] = items;
|
|
114
|
+
return (
|
|
115
|
+
<HeaderFooterLayout
|
|
116
|
+
header={header}
|
|
117
|
+
footer={footer}
|
|
118
|
+
headerHeight={mobileHeaderHeight}
|
|
119
|
+
footerHeight={footerHeight}
|
|
120
|
+
>
|
|
121
|
+
{main}
|
|
122
|
+
</HeaderFooterLayout>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
// 'plain' and 'sidebar' both render plain on mobile — sidebar's chrome is web-only.
|
|
126
|
+
const main = variant === 'sidebar' ? items[1] : items[0];
|
|
127
|
+
return <>{main}</>;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* RootLayout — the root-level layout a plugin's page tree sits in. Enforces
|
|
132
|
+
* only structure and dimensions (`width: 100%; height: 100%`, filling
|
|
133
|
+
* whatever real height the platform shell's own ancestor chain already
|
|
134
|
+
* establishes — not viewport units, since a plugin isn't alone on the
|
|
135
|
+
* page). A definite height, not min-height: ThreeColumnLayout/
|
|
136
|
+
* HeaderFooterLayout's own .shell need one to resolve their own height:
|
|
137
|
+
* 100% against (see RootLayout.module.css's comment). All content comes
|
|
138
|
+
* from children; RootLayout has no opinion on what's inside any region.
|
|
139
|
+
*
|
|
140
|
+
* The web/mobile fork is `ResponsiveSurface` — only one of
|
|
141
|
+
* `RootLayoutWeb`/`RootLayoutMobile` is ever mounted, never a CSS squeeze of
|
|
142
|
+
* the same tree. Each variant's actual shape is delegated entirely to
|
|
143
|
+
* `ThreeColumnLayout`/`HeaderFooterLayout`; RootLayout itself contains no
|
|
144
|
+
* media queries.
|
|
145
|
+
*/
|
|
146
|
+
export function RootLayout({
|
|
147
|
+
variant = 'plain',
|
|
148
|
+
children,
|
|
149
|
+
sidebarWidth = 64,
|
|
150
|
+
headerHeight = 48,
|
|
151
|
+
mobileHeaderHeight = 60,
|
|
152
|
+
footerHeight = 60,
|
|
153
|
+
className,
|
|
154
|
+
}: RootLayoutProps) {
|
|
155
|
+
const items = Children.toArray(children);
|
|
156
|
+
|
|
157
|
+
if (process.env.NODE_ENV !== 'production' && items.length !== EXPECTED_CHILD_COUNT[variant]) {
|
|
158
|
+
console.warn(
|
|
159
|
+
`[RootLayout] variant="${variant}" expects ${EXPECTED_CHILD_COUNT[variant]} children; received ${items.length}.`,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<div className={[styles.root, className].filter(Boolean).join(' ')}>
|
|
165
|
+
<ResponsiveSurface
|
|
166
|
+
web={
|
|
167
|
+
<RootLayoutWeb
|
|
168
|
+
variant={variant}
|
|
169
|
+
items={items}
|
|
170
|
+
sidebarWidth={sidebarWidth}
|
|
171
|
+
headerHeight={headerHeight}
|
|
172
|
+
/>
|
|
173
|
+
}
|
|
174
|
+
mobile={
|
|
175
|
+
<RootLayoutMobile
|
|
176
|
+
variant={variant}
|
|
177
|
+
items={items}
|
|
178
|
+
mobileHeaderHeight={mobileHeaderHeight}
|
|
179
|
+
footerHeight={footerHeight}
|
|
180
|
+
/>
|
|
181
|
+
}
|
|
182
|
+
/>
|
|
183
|
+
</div>
|
|
184
|
+
);
|
|
185
|
+
}
|