@sovereignfs/ui 0.54.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/Markdown.module.css +24 -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 +10 -9
- package/dist/MobileFooter.module.css +1 -1
- package/dist/MobileHeader.module.css +14 -1
- 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 +62 -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 +12 -3
- package/dist/SwipableMobileCarouselDots.module.css +15 -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 +1047 -45
- package/dist/index.js +3908 -1456
- package/dist/tokens/primitives.css +11 -0
- package/dist/tokens/semantic.css +32 -0
- package/dist/tokens/theme-presets.css +33 -0
- package/package.json +25 -9
- 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 +7 -4
- 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/Dialog/Dialog.module.css +130 -21
- package/src/components/Dialog/Dialog.stories.tsx +179 -2
- package/src/components/Dialog/Dialog.tsx +129 -35
- 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.tsx +9 -10
- package/src/components/Drawer/__tests__/Drawer.test.tsx +33 -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 +161 -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/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/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/Markdown/Markdown.module.css +24 -0
- package/src/components/Markdown/Markdown.stories.tsx +10 -0
- package/src/components/Markdown/Markdown.tsx +166 -29
- 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 +10 -9
- package/src/components/MobileFooter/MobileFooter.module.css +1 -1
- package/src/components/MobileFooter/MobileFooter.tsx +15 -2
- package/src/components/MobileFooter/__tests__/MobileFooter.test.tsx +58 -0
- package/src/components/MobileHeader/MobileHeader.module.css +14 -1
- package/src/components/MobileHeader/MobileHeader.tsx +15 -2
- package/src/components/MobileHeader/__tests__/MobileHeader.test.tsx +56 -1
- 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/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/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 +62 -0
- package/src/components/PageContainer/PageContainer.tsx +37 -4
- package/src/components/PageContainer/__tests__/PageContainer.test.tsx +41 -8
- package/src/components/PageHeader/PageHeader.module.css +54 -0
- package/src/components/PageHeader/PageHeader.tsx +146 -5
- 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/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 +5 -5
- 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 +12 -3
- package/src/components/SwipableMobileCarousel/SwipableMobileCarousel.tsx +25 -9
- package/src/components/SwipableMobileCarousel/__tests__/SwipableMobileCarousel.test.tsx +81 -0
- package/src/components/SwipableMobileCarouselDots/SwipableMobileCarouselDots.module.css +15 -0
- package/src/components/SwipableMobileCarouselDots/SwipableMobileCarouselDots.tsx +12 -1
- 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__/usePublishShellChromeHeight.test.tsx +118 -0
- package/src/hooks/__tests__/useReorderSensors.test.ts +26 -0
- package/src/hooks/__tests__/useSnapCarousel.test.tsx +82 -0
- package/src/hooks/__tests__/useSwipeStack.test.tsx +155 -0
- package/src/hooks/index.ts +9 -0
- package/src/hooks/usePublishShellChromeHeight.ts +96 -0
- package/src/hooks/useReorderSensors.ts +88 -0
- package/src/hooks/useSnapCarousel.ts +211 -6
- package/src/hooks/useSwipeStack.ts +235 -0
- package/src/index.ts +61 -1
- package/src/overlay-shell.ts +65 -0
- package/src/stories/DesignSystemOverview.stories.tsx +617 -11
- package/src/stories/GripIcon.stories.tsx +29 -0
- package/src/stories/LayoutSystem.stories.tsx +639 -0
- package/src/stories/MenuEntries.stories.tsx +86 -0
- package/src/stories/NavList.stories.tsx +147 -0
- package/src/stories/PageContainer.stories.tsx +52 -7
- package/src/stories/PageHeader.stories.tsx +54 -1
- package/src/stories/StepStrip.stories.tsx +76 -0
- package/src/stories/SwipableMobileCarousel.stories.tsx +58 -0
- package/src/stories/SwipeStack.stories.tsx +200 -0
- package/src/stories/Textarea.stories.tsx +12 -0
- package/src/stories/TokenGallery.stories.tsx +50 -0
- package/src/tokens/primitives.css +11 -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,51 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from 'react';
|
|
4
|
+
import styles from './PageLayout.module.css';
|
|
5
|
+
|
|
6
|
+
export type PageLayoutPadding = 'none' | 'sm' | 'md' | 'lg';
|
|
7
|
+
|
|
8
|
+
export interface PageLayoutProps {
|
|
9
|
+
/**
|
|
10
|
+
* A page-specific header — e.g. a board title + search/share toolbar.
|
|
11
|
+
* Not the app-level header (that's `RootLayout`'s `header`/`shell`
|
|
12
|
+
* variants) — this is a second, contextual row scoped to this one page.
|
|
13
|
+
* Renders edge-to-edge above the padded content, outside `padding`.
|
|
14
|
+
*/
|
|
15
|
+
header?: ReactNode;
|
|
16
|
+
/** The page gutter. Default `'none'` — unlike `PageContainer`, a bare
|
|
17
|
+
* `PageLayout` applies no padding at all; opt in per page. Same four-step
|
|
18
|
+
* scale and `--sv-page-gutter` stand-down hook as `PageContainer` (steps
|
|
19
|
+
* down at the same 768px breakpoint `Dialog`/`MobileHeader` use). */
|
|
20
|
+
padding?: PageLayoutPadding;
|
|
21
|
+
children: ReactNode;
|
|
22
|
+
className?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const paddingClass: Record<PageLayoutPadding, string> = {
|
|
26
|
+
none: styles.padNone as string,
|
|
27
|
+
sm: styles.padSm as string,
|
|
28
|
+
md: styles.padMd as string,
|
|
29
|
+
lg: styles.padLg as string,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* PageLayout — a single page's content area within a plugin (nested inside
|
|
34
|
+
* `RootLayout`'s `main` slot, not a replacement for it). Enforces only
|
|
35
|
+
* structure and dimensions; content always comes from props/children.
|
|
36
|
+
*
|
|
37
|
+
* `min-width: 100%; min-height: 100%` — safe here (unlike `RootLayout`'s own
|
|
38
|
+
* root) because `PageLayout` is always nested inside an already
|
|
39
|
+
* flex-stretched region (`ThreeColumnLayout`/`HeaderFooterLayout`'s
|
|
40
|
+
* `.main`), and a stretched flex item's cross-size is definite for its
|
|
41
|
+
* descendants' percentage resolution, so no separate height-cascading fix
|
|
42
|
+
* is needed the way `RootLayout`'s root required one.
|
|
43
|
+
*/
|
|
44
|
+
export function PageLayout({ header, padding = 'none', children, className }: PageLayoutProps) {
|
|
45
|
+
return (
|
|
46
|
+
<div className={[styles.root, className].filter(Boolean).join(' ')}>
|
|
47
|
+
{header && <div className={styles.header}>{header}</div>}
|
|
48
|
+
<div className={[styles.content, paddingClass[padding]].join(' ')}>{children}</div>
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
3
|
+
import { cleanup, render, screen } from '@testing-library/react';
|
|
4
|
+
import { PageLayout } from '../PageLayout';
|
|
5
|
+
|
|
6
|
+
afterEach(cleanup);
|
|
7
|
+
|
|
8
|
+
describe('PageLayout', () => {
|
|
9
|
+
it('renders children with no header by default', () => {
|
|
10
|
+
render(<PageLayout>Content</PageLayout>);
|
|
11
|
+
|
|
12
|
+
expect(screen.getByText('Content')).toBeDefined();
|
|
13
|
+
expect(screen.queryByText('Header')).toBeNull();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('renders a page-specific header when given', () => {
|
|
17
|
+
render(<PageLayout header={<span>Header</span>}>Content</PageLayout>);
|
|
18
|
+
|
|
19
|
+
expect(screen.getByText('Header')).toBeDefined();
|
|
20
|
+
expect(screen.getByText('Content')).toBeDefined();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('applies no padding by default', () => {
|
|
24
|
+
render(<PageLayout>Content</PageLayout>);
|
|
25
|
+
|
|
26
|
+
expect(screen.getByText('Content').className).toContain('padNone');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('applies the requested padding step', () => {
|
|
30
|
+
render(<PageLayout padding="lg">Content</PageLayout>);
|
|
31
|
+
|
|
32
|
+
expect(screen.getByText('Content').className).toContain('padLg');
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
top: calc(100% + var(--sv-space-2));
|
|
9
9
|
z-index: 60;
|
|
10
10
|
background: var(--sv-color-surface);
|
|
11
|
-
border:
|
|
11
|
+
border: var(--sv-border-width-hairline) solid var(--sv-color-border);
|
|
12
12
|
border-radius: var(--sv-radius-2xl);
|
|
13
13
|
box-shadow: var(--sv-shadow-popover);
|
|
14
14
|
/* auto, not hidden: Popover.tsx applies an inline max-height only when the
|
|
@@ -9,6 +9,11 @@ import {
|
|
|
9
9
|
type ReactElement,
|
|
10
10
|
type ReactNode,
|
|
11
11
|
} from 'react';
|
|
12
|
+
import {
|
|
13
|
+
isTopmostOpenOverlay,
|
|
14
|
+
registerOpenOverlay,
|
|
15
|
+
unregisterOpenOverlay,
|
|
16
|
+
} from '../../overlay-shell';
|
|
12
17
|
import styles from './Popover.module.css';
|
|
13
18
|
|
|
14
19
|
export interface PopoverProps {
|
|
@@ -30,6 +35,17 @@ export interface PopoverProps {
|
|
|
30
35
|
* regardless of CSS module load order, unlike passing an extra className.
|
|
31
36
|
*/
|
|
32
37
|
panelStyle?: CSSProperties;
|
|
38
|
+
/**
|
|
39
|
+
* Class applied to the popover's own wrapper around `trigger` — the
|
|
40
|
+
* element that participates in the caller's layout.
|
|
41
|
+
*
|
|
42
|
+
* Mostly needed for flex sizing: the wrapper is an `inline-flex` box with
|
|
43
|
+
* the default `min-width: auto`, which pins it to the trigger's content
|
|
44
|
+
* width. Inside a flex row that has to shrink, that stops a truncatable
|
|
45
|
+
* trigger from ever giving up space, and pushes its siblings out of the
|
|
46
|
+
* container instead. Pass a class setting `min-width: 0` there.
|
|
47
|
+
*/
|
|
48
|
+
className?: string;
|
|
33
49
|
'aria-label': string;
|
|
34
50
|
children: ReactNode;
|
|
35
51
|
}
|
|
@@ -78,6 +94,7 @@ export function Popover({
|
|
|
78
94
|
align = 'right',
|
|
79
95
|
width = 288,
|
|
80
96
|
panelStyle,
|
|
97
|
+
className,
|
|
81
98
|
'aria-label': ariaLabel,
|
|
82
99
|
children,
|
|
83
100
|
}: PopoverProps) {
|
|
@@ -92,6 +109,36 @@ export function Popover({
|
|
|
92
109
|
// when the panel is taller than that — null leaves it at its natural
|
|
93
110
|
// height, unconstrained, for the common case where it already fits.
|
|
94
111
|
const [maxHeight, setMaxHeight] = useState<number | null>(null);
|
|
112
|
+
// Horizontal viewport clamp, as a `left` override (container-relative px)
|
|
113
|
+
// that replaces the `align`-driven `left: 0` / `right: 0` class rule —
|
|
114
|
+
// null leaves alignment at its natural, class-driven position, for the
|
|
115
|
+
// common case where the panel already fits. Needed because a fixed-width
|
|
116
|
+
// panel anchored to one edge of a trigger near the *opposite* edge of a
|
|
117
|
+
// narrow viewport (e.g. a 340px panel under a bell sitting near the right
|
|
118
|
+
// edge of a 375px mobile screen) has nowhere to go but off-screen — the
|
|
119
|
+
// existing vertical flip only ever solves the above/below axis, never
|
|
120
|
+
// this one.
|
|
121
|
+
const [horizontalOffset, setHorizontalOffset] = useState<number | null>(null);
|
|
122
|
+
const overlayIdRef = useRef<string | null>(null);
|
|
123
|
+
|
|
124
|
+
// Registers into the same open-overlay stack Dialog/Drawer/Sheet/
|
|
125
|
+
// ConfirmDialog use (overlay-shell.ts) purely so a Dialog this Popover was
|
|
126
|
+
// opened from inside of (e.g. a DatePicker/Combobox field in a modal form —
|
|
127
|
+
// Popover is what Menu/Combobox/DatePicker/ContextMenu/HoverCard/UserMenu
|
|
128
|
+
// are all built on) knows it is no longer the topmost overlay, and defers
|
|
129
|
+
// Escape to this one instead of closing itself too. A separate effect keyed
|
|
130
|
+
// only on `open`, matching useOverlayKeyboardTrap's own pattern — identity
|
|
131
|
+
// churn on `onClose` shouldn't re-register and bump this popover back to
|
|
132
|
+
// the top of the stack.
|
|
133
|
+
useEffect(() => {
|
|
134
|
+
if (!open) return;
|
|
135
|
+
const id = registerOpenOverlay();
|
|
136
|
+
overlayIdRef.current = id;
|
|
137
|
+
return () => {
|
|
138
|
+
unregisterOpenOverlay(id);
|
|
139
|
+
overlayIdRef.current = null;
|
|
140
|
+
};
|
|
141
|
+
}, [open]);
|
|
95
142
|
|
|
96
143
|
// width="trigger": measure the trigger's own rendered width and keep the
|
|
97
144
|
// panel matching it, including live resizes (e.g. rotating the device, or
|
|
@@ -127,6 +174,7 @@ export function Popover({
|
|
|
127
174
|
if (!open) {
|
|
128
175
|
setOpenUpward(false);
|
|
129
176
|
setMaxHeight(null);
|
|
177
|
+
setHorizontalOffset(null);
|
|
130
178
|
return;
|
|
131
179
|
}
|
|
132
180
|
const container = containerRef.current;
|
|
@@ -155,10 +203,37 @@ export function Popover({
|
|
|
155
203
|
setOpenUpward(upward);
|
|
156
204
|
const available = upward ? spaceAbove : spaceBelow;
|
|
157
205
|
setMaxHeight(panelHeight > available ? Math.max(available, 0) : null);
|
|
206
|
+
|
|
207
|
+
// Horizontal clamp. Deliberately computed from `containerRect` (the
|
|
208
|
+
// trigger's own position, never affected by anything this effect does)
|
|
209
|
+
// and the panel's rendered *width* (also offset-independent) rather than
|
|
210
|
+
// reading the panel's current `left`/`right` off the DOM — those reflect
|
|
211
|
+
// whatever `horizontalOffset` a *previous* run of this same effect may
|
|
212
|
+
// already have applied. Measuring the rendered position directly is not
|
|
213
|
+
// idempotent: re-running this effect after it already nudged the panel
|
|
214
|
+
// on-screen would see an already-corrected rect that looks like it never
|
|
215
|
+
// needed correcting, and clear the very offset it just set (confirmed
|
|
216
|
+
// live — React StrictMode's dev-only double-invoke of layout effects hit
|
|
217
|
+
// exactly this: the second pass measured the first pass's fix as "fits
|
|
218
|
+
// fine" and reset `horizontalOffset` back to null before first paint).
|
|
219
|
+
// Rebuilding the *natural*, un-offset position analytically from the
|
|
220
|
+
// same `align`-driven rule the CSS class itself encodes (`right: 0` /
|
|
221
|
+
// `left: 0` relative to the container) sidesteps that entirely — the
|
|
222
|
+
// same inputs always produce the same output, however many times this
|
|
223
|
+
// runs.
|
|
224
|
+
const containerRect = container.getBoundingClientRect();
|
|
225
|
+
const panelWidth = panel.getBoundingClientRect().width;
|
|
226
|
+
const naturalLeft = align === 'right' ? containerRect.right - panelWidth : containerRect.left;
|
|
227
|
+
const clampedLeft = Math.min(
|
|
228
|
+
Math.max(naturalLeft, VIEWPORT_MARGIN),
|
|
229
|
+
window.innerWidth - VIEWPORT_MARGIN - panelWidth,
|
|
230
|
+
);
|
|
231
|
+
setHorizontalOffset(clampedLeft !== naturalLeft ? clampedLeft - containerRect.left : null);
|
|
158
232
|
// children included: content that changes the panel's height (e.g. a
|
|
159
233
|
// calendar switching months to a row with fewer/more weeks) should
|
|
160
234
|
// re-run this measurement, not just the open/close transition itself.
|
|
161
|
-
|
|
235
|
+
// align included: the horizontal clamp above reads it directly.
|
|
236
|
+
}, [open, children, align]);
|
|
162
237
|
|
|
163
238
|
// Outside-click dismissal
|
|
164
239
|
useEffect(() => {
|
|
@@ -172,18 +247,24 @@ export function Popover({
|
|
|
172
247
|
return () => document.removeEventListener('mousedown', handleMouseDown);
|
|
173
248
|
}, [open, onClose]);
|
|
174
249
|
|
|
175
|
-
// Escape key dismissal
|
|
250
|
+
// Escape key dismissal — deferred to a nested overlay (if any) via the same
|
|
251
|
+
// topmost-overlay check useOverlayKeyboardTrap uses, so e.g. opening a
|
|
252
|
+
// ConfirmDialog from inside this panel's content doesn't also close the
|
|
253
|
+
// popover out from under it on the same Escape press.
|
|
176
254
|
useEffect(() => {
|
|
177
255
|
if (!open) return;
|
|
178
256
|
function handleKeyDown(e: KeyboardEvent) {
|
|
179
|
-
if (e.key
|
|
257
|
+
if (e.key !== 'Escape') return;
|
|
258
|
+
const id = overlayIdRef.current;
|
|
259
|
+
if (id !== null && !isTopmostOpenOverlay(id)) return;
|
|
260
|
+
onClose();
|
|
180
261
|
}
|
|
181
262
|
document.addEventListener('keydown', handleKeyDown);
|
|
182
263
|
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
183
264
|
}, [open, onClose]);
|
|
184
265
|
|
|
185
266
|
return (
|
|
186
|
-
<div ref={containerRef} className={styles.container}>
|
|
267
|
+
<div ref={containerRef} className={[styles.container, className].filter(Boolean).join(' ')}>
|
|
187
268
|
{trigger}
|
|
188
269
|
{open && (
|
|
189
270
|
<div
|
|
@@ -197,6 +278,7 @@ export function Popover({
|
|
|
197
278
|
style={{
|
|
198
279
|
width: width === 'trigger' ? (triggerWidth ?? undefined) : width,
|
|
199
280
|
maxHeight: maxHeight ?? undefined,
|
|
281
|
+
...(horizontalOffset != null ? { left: horizontalOffset, right: 'auto' } : {}),
|
|
200
282
|
...panelStyle,
|
|
201
283
|
}}
|
|
202
284
|
>
|
|
@@ -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,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
|
+
};
|