@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
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
.container {
|
|
2
2
|
width: 100%;
|
|
3
3
|
margin-inline: auto;
|
|
4
|
+
/* Declared explicitly rather than inherited from a host reset: `maxWidth`
|
|
5
|
+
and `padding` land on this same element, so whether the max-width bound
|
|
6
|
+
includes the padding must not depend on whether the consuming app ships
|
|
7
|
+
a global `box-sizing` reset. @sovereignfs/ui is usable standalone
|
|
8
|
+
(task 9.17), where no such reset is guaranteed. Same reason Input,
|
|
9
|
+
Select, Combobox, DatePicker, and FileDropzone each set it locally. */
|
|
10
|
+
box-sizing: border-box;
|
|
4
11
|
}
|
|
5
12
|
|
|
13
|
+
/* ── Max width ──────────────────────────────────────────────────────────── */
|
|
14
|
+
|
|
15
|
+
/* Outer bound, padding included (see box-sizing above): `md` + `padding="md"`
|
|
16
|
+
yields a 960px box with 896px of content, not 1024px. */
|
|
17
|
+
|
|
6
18
|
.sm {
|
|
7
19
|
max-width: 640px;
|
|
8
20
|
}
|
|
@@ -18,3 +30,53 @@
|
|
|
18
30
|
.full {
|
|
19
31
|
max-width: none;
|
|
20
32
|
}
|
|
33
|
+
|
|
34
|
+
/* ── Padding ────────────────────────────────────────────────────────────── */
|
|
35
|
+
|
|
36
|
+
/* The page gutter, owned by the plugin rather than the runtime shell
|
|
37
|
+
(task 9.25). `md` reproduces the exact values the shell used to apply
|
|
38
|
+
(--sv-space-8 desktop / --sv-space-4 mobile), so a bare <PageContainer>
|
|
39
|
+
renders identically to a pre-9.25 plugin that relied on the shell.
|
|
40
|
+
|
|
41
|
+
A fixed four-step scale, deliberately: the review behind task 9.18 found
|
|
42
|
+
four different ad-hoc padding combinations across plugins, and a free-form
|
|
43
|
+
value would relocate that inconsistency from CSS modules into JSX.
|
|
44
|
+
|
|
45
|
+
--sv-page-gutter is the stand-down hook for an ancestor that *already*
|
|
46
|
+
supplies the gutter: Dialog sets it to 0 on its content region, so an
|
|
47
|
+
overlay plugin whose page is one tree rendered into two hosts — the
|
|
48
|
+
Dialog on soft navigation, the runtime shell on a hard load — is padded
|
|
49
|
+
exactly once in both, with no prop threading and no knowledge of which
|
|
50
|
+
host it is in. Setting it to anything other than 0 defeats the fixed
|
|
51
|
+
scale above and is not a supported use. */
|
|
52
|
+
|
|
53
|
+
.padNone {
|
|
54
|
+
padding: 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.padSm {
|
|
58
|
+
padding: var(--sv-page-gutter, var(--sv-space-4));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.padMd {
|
|
62
|
+
padding: var(--sv-page-gutter, var(--sv-space-8));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.padLg {
|
|
66
|
+
padding: var(--sv-page-gutter, var(--sv-space-12));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* Matches the breakpoint Dialog, MobileHeader, and MobileFooter already use. */
|
|
70
|
+
@media (max-width: 768px) {
|
|
71
|
+
.padSm {
|
|
72
|
+
padding: var(--sv-page-gutter, var(--sv-space-3));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.padMd {
|
|
76
|
+
padding: var(--sv-page-gutter, var(--sv-space-4));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.padLg {
|
|
80
|
+
padding: var(--sv-page-gutter, var(--sv-space-6));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -2,9 +2,23 @@ import type { HTMLAttributes, ReactNode } from 'react';
|
|
|
2
2
|
import styles from './PageContainer.module.css';
|
|
3
3
|
|
|
4
4
|
export interface PageContainerProps extends HTMLAttributes<HTMLDivElement> {
|
|
5
|
-
/**
|
|
6
|
-
*
|
|
5
|
+
/**
|
|
6
|
+
* Constrains and centers content width. The bound includes `padding`
|
|
7
|
+
* (the container is `border-box`). Defaults to `'full'` — narrowing is
|
|
8
|
+
* opt-in, so wrapping a page in `PageContainer` never silently clamps it.
|
|
9
|
+
*/
|
|
7
10
|
maxWidth?: 'sm' | 'md' | 'lg' | 'full';
|
|
11
|
+
/**
|
|
12
|
+
* The page gutter. Responsive — each step is tighter at ≤768px. Defaults
|
|
13
|
+
* to `'md'`, which is exactly what the runtime shell used to apply
|
|
14
|
+
* (`--sv-space-8` desktop / `--sv-space-4` mobile), so a bare
|
|
15
|
+
* `<PageContainer>` is the correct wrapper for a plugin that previously
|
|
16
|
+
* relied on the shell for its padding.
|
|
17
|
+
*
|
|
18
|
+
* Use `'none'` when an ancestor already supplies the gutter — inside a
|
|
19
|
+
* `Dialog`, whose own content region is padded.
|
|
20
|
+
*/
|
|
21
|
+
padding?: 'none' | 'sm' | 'md' | 'lg';
|
|
8
22
|
children: ReactNode;
|
|
9
23
|
}
|
|
10
24
|
|
|
@@ -15,13 +29,32 @@ const maxWidthClass: Record<NonNullable<PageContainerProps['maxWidth']>, string>
|
|
|
15
29
|
full: styles.full as string,
|
|
16
30
|
};
|
|
17
31
|
|
|
32
|
+
const paddingClass: Record<NonNullable<PageContainerProps['padding']>, string> = {
|
|
33
|
+
none: styles.padNone as string,
|
|
34
|
+
sm: styles.padSm as string,
|
|
35
|
+
md: styles.padMd as string,
|
|
36
|
+
lg: styles.padLg as string,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The shared page wrapper every plugin's own layout or page renders through.
|
|
41
|
+
*
|
|
42
|
+
* A plugin owns its page gutter; the runtime shell does not impose one
|
|
43
|
+
* (task 9.25). The shell still reserves space for chrome a plugin cannot
|
|
44
|
+
* know about — the offline banner's height and the mobile footer's — but it
|
|
45
|
+
* contributes no inset of its own, so this component is the single place a
|
|
46
|
+
* plugin's four-sided padding comes from.
|
|
47
|
+
*/
|
|
18
48
|
export function PageContainer({
|
|
19
|
-
maxWidth = '
|
|
49
|
+
maxWidth = 'full',
|
|
50
|
+
padding = 'md',
|
|
20
51
|
className,
|
|
21
52
|
children,
|
|
22
53
|
...rest
|
|
23
54
|
}: PageContainerProps) {
|
|
24
|
-
const cls = [styles.container, maxWidthClass[maxWidth], className]
|
|
55
|
+
const cls = [styles.container, maxWidthClass[maxWidth], paddingClass[padding], className]
|
|
56
|
+
.filter(Boolean)
|
|
57
|
+
.join(' ');
|
|
25
58
|
|
|
26
59
|
return (
|
|
27
60
|
<div className={cls} {...rest}>
|
|
@@ -5,24 +5,57 @@ import { PageContainer } from '../PageContainer';
|
|
|
5
5
|
|
|
6
6
|
afterEach(cleanup);
|
|
7
7
|
|
|
8
|
+
/** Classes are unhashed under Vitest's CSS-module transform, so compare token
|
|
9
|
+
* membership rather than substrings — `toContain('md')` would also match
|
|
10
|
+
* `padMd`, which is exactly the confusion these two prop scales invite. */
|
|
11
|
+
function classes(el: Element): string[] {
|
|
12
|
+
return el.className.split(/\s+/).filter(Boolean);
|
|
13
|
+
}
|
|
14
|
+
|
|
8
15
|
describe('PageContainer', () => {
|
|
9
16
|
it('renders its children', () => {
|
|
10
17
|
render(<PageContainer>content</PageContainer>);
|
|
11
18
|
expect(screen.getByText('content')).toBeDefined();
|
|
12
19
|
});
|
|
13
20
|
|
|
14
|
-
it('defaults to
|
|
21
|
+
it('defaults to full max-width, so wrapping a page never silently clamps it', () => {
|
|
22
|
+
render(<PageContainer>content</PageContainer>);
|
|
23
|
+
expect(classes(screen.getByText('content'))).toContain('full');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('defaults to md padding — the gutter the runtime shell used to apply', () => {
|
|
15
27
|
render(<PageContainer>content</PageContainer>);
|
|
16
|
-
expect(screen.getByText('content')
|
|
28
|
+
expect(classes(screen.getByText('content'))).toContain('padMd');
|
|
17
29
|
});
|
|
18
30
|
|
|
19
|
-
it('applies sm,
|
|
31
|
+
it('applies sm, md, and lg max-width classes', () => {
|
|
20
32
|
const { rerender } = render(<PageContainer maxWidth="sm">content</PageContainer>);
|
|
21
|
-
expect(screen.getByText('content')
|
|
33
|
+
expect(classes(screen.getByText('content'))).toContain('sm');
|
|
34
|
+
rerender(<PageContainer maxWidth="md">content</PageContainer>);
|
|
35
|
+
expect(classes(screen.getByText('content'))).toContain('md');
|
|
22
36
|
rerender(<PageContainer maxWidth="lg">content</PageContainer>);
|
|
23
|
-
expect(screen.getByText('content')
|
|
24
|
-
|
|
25
|
-
|
|
37
|
+
expect(classes(screen.getByText('content'))).toContain('lg');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('applies none, sm, and lg padding classes', () => {
|
|
41
|
+
const { rerender } = render(<PageContainer padding="none">content</PageContainer>);
|
|
42
|
+
expect(classes(screen.getByText('content'))).toContain('padNone');
|
|
43
|
+
rerender(<PageContainer padding="sm">content</PageContainer>);
|
|
44
|
+
expect(classes(screen.getByText('content'))).toContain('padSm');
|
|
45
|
+
rerender(<PageContainer padding="lg">content</PageContainer>);
|
|
46
|
+
expect(classes(screen.getByText('content'))).toContain('padLg');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('composes max-width and padding independently', () => {
|
|
50
|
+
render(
|
|
51
|
+
<PageContainer maxWidth="sm" padding="none">
|
|
52
|
+
content
|
|
53
|
+
</PageContainer>,
|
|
54
|
+
);
|
|
55
|
+
const cls = classes(screen.getByText('content'));
|
|
56
|
+
expect(cls).toContain('sm');
|
|
57
|
+
expect(cls).toContain('padNone');
|
|
58
|
+
expect(cls).not.toContain('padSm');
|
|
26
59
|
});
|
|
27
60
|
|
|
28
61
|
it('forwards className and other div props', () => {
|
|
@@ -32,6 +65,6 @@ describe('PageContainer', () => {
|
|
|
32
65
|
</PageContainer>,
|
|
33
66
|
);
|
|
34
67
|
const el = screen.getByTestId('container');
|
|
35
|
-
expect(el
|
|
68
|
+
expect(classes(el)).toContain('custom');
|
|
36
69
|
});
|
|
37
70
|
});
|
|
@@ -13,6 +13,17 @@
|
|
|
13
13
|
min-width: 0;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
/* Wraps the optional icon + `.title` so the icon sizes/aligns itself
|
|
17
|
+
without touching `.title`'s own rules — every existing icon-less usage
|
|
18
|
+
renders identically, since a single flex child behaves the same as a
|
|
19
|
+
non-flex one. */
|
|
20
|
+
.titleRow {
|
|
21
|
+
display: flex;
|
|
22
|
+
align-items: center;
|
|
23
|
+
gap: var(--sv-space-2);
|
|
24
|
+
color: var(--sv-color-text-primary);
|
|
25
|
+
}
|
|
26
|
+
|
|
16
27
|
.title {
|
|
17
28
|
margin: 0;
|
|
18
29
|
font-size: var(--sv-font-size-2xl);
|
|
@@ -30,3 +41,46 @@
|
|
|
30
41
|
.action {
|
|
31
42
|
flex-shrink: 0;
|
|
32
43
|
}
|
|
44
|
+
|
|
45
|
+
/* ── Mobile ─────────────────────────────────────────────────────────────── */
|
|
46
|
+
/* A compact single row — back (optional) + title + action (optional) —
|
|
47
|
+
generalized from Kanban's own hand-rolled MobileBoardHeader. Deliberately
|
|
48
|
+
no description handling and no border; stays consistent with .header's
|
|
49
|
+
own minimal, borderless treatment, just tighter. */
|
|
50
|
+
|
|
51
|
+
.mobileHeader {
|
|
52
|
+
display: flex;
|
|
53
|
+
align-items: center;
|
|
54
|
+
gap: var(--sv-space-2);
|
|
55
|
+
margin-bottom: var(--sv-space-3);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.mobileBack {
|
|
59
|
+
flex-shrink: 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.mobileTitleIcon {
|
|
63
|
+
flex-shrink: 0;
|
|
64
|
+
color: var(--sv-color-text-primary);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.mobileTitle {
|
|
68
|
+
flex: 1;
|
|
69
|
+
min-width: 0;
|
|
70
|
+
margin: 0;
|
|
71
|
+
overflow: hidden;
|
|
72
|
+
text-overflow: ellipsis;
|
|
73
|
+
white-space: nowrap;
|
|
74
|
+
font-size: var(--sv-font-size-md);
|
|
75
|
+
font-weight: var(--sv-font-weight-semibold);
|
|
76
|
+
color: var(--sv-color-text-primary);
|
|
77
|
+
line-height: 1.2;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.mobileAction {
|
|
81
|
+
flex-shrink: 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.mobileMenu {
|
|
85
|
+
flex-shrink: 0;
|
|
86
|
+
}
|
|
@@ -1,35 +1,176 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
1
3
|
import type { ReactNode } from 'react';
|
|
4
|
+
import { Button } from '../Button/Button';
|
|
5
|
+
import { Icon } from '../Icon/Icon';
|
|
6
|
+
import type { IconName } from '../Icon/Icon';
|
|
7
|
+
import { ResponsiveSurface } from '../ResponsiveSurface/ResponsiveSurface';
|
|
2
8
|
import styles from './PageHeader.module.css';
|
|
3
9
|
|
|
4
10
|
export interface PageHeaderProps {
|
|
5
11
|
title: string;
|
|
6
12
|
description?: string;
|
|
13
|
+
/** Optional icon rendered inline before the title text, on both breakpoints — e.g. `folder-open` next to a folder's name. */
|
|
14
|
+
icon?: IconName;
|
|
15
|
+
/**
|
|
16
|
+
* Page-level controls — anywhere from one button to a whole toolbar.
|
|
17
|
+
* Renders on both breakpoints unchanged; if it needs to look different on
|
|
18
|
+
* mobile (e.g. collapsing into its own "⋮" trigger), that's the action
|
|
19
|
+
* content's own responsibility — same pattern Shopper's `ListHeaderActions`
|
|
20
|
+
* already follows (it calls `useIsMobile()` internally). `PageHeader`
|
|
21
|
+
* itself has no opinion on what's inside this slot.
|
|
22
|
+
*/
|
|
7
23
|
action?: ReactNode;
|
|
8
24
|
className?: string;
|
|
9
25
|
/** Heading level for the title. Defaults to `1` for standalone use; pass
|
|
10
26
|
* `2` or `3` when the page already sits under a shell/plugin `<h1>`, to
|
|
11
27
|
* avoid a duplicate top-level heading. Visual style is unchanged at every
|
|
12
|
-
* level — only the rendered tag changes. */
|
|
28
|
+
* level — only the rendered tag changes. Applies on both breakpoints. */
|
|
13
29
|
headingLevel?: 1 | 2 | 3;
|
|
30
|
+
/** Mobile only — shows a back button (calling this) to the left of the
|
|
31
|
+
* title when provided. Omit it to render without one. */
|
|
32
|
+
onBack?: () => void;
|
|
33
|
+
/** Mobile only — shows a vertical-ellipsis icon button (calling this) at
|
|
34
|
+
* the far right when provided, typically to open a drawer/menu. PageHeader
|
|
35
|
+
* has no opinion on what that menu contains — it only renders the trigger
|
|
36
|
+
* and calls back. Separate from `action`, which renders identically on
|
|
37
|
+
* both breakpoints; this is a mobile-only affordance with no web
|
|
38
|
+
* equivalent. */
|
|
39
|
+
onMenuClick?: () => void;
|
|
14
40
|
}
|
|
15
41
|
|
|
16
42
|
const HEADING_TAG = { 1: 'h1', 2: 'h2', 3: 'h3' } as const;
|
|
17
43
|
|
|
18
|
-
|
|
44
|
+
interface PageHeaderWebProps {
|
|
45
|
+
title: string;
|
|
46
|
+
description?: string;
|
|
47
|
+
icon?: IconName;
|
|
48
|
+
action?: ReactNode;
|
|
49
|
+
headingLevel: 1 | 2 | 3;
|
|
50
|
+
className?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// internal — not exported
|
|
54
|
+
function PageHeaderWeb({
|
|
19
55
|
title,
|
|
20
56
|
description,
|
|
57
|
+
icon,
|
|
21
58
|
action,
|
|
59
|
+
headingLevel,
|
|
22
60
|
className,
|
|
23
|
-
|
|
24
|
-
}: PageHeaderProps) {
|
|
61
|
+
}: PageHeaderWebProps) {
|
|
25
62
|
const Heading = HEADING_TAG[headingLevel];
|
|
26
63
|
return (
|
|
27
64
|
<header className={[styles.header, className].filter(Boolean).join(' ')}>
|
|
28
65
|
<div className={styles.text}>
|
|
29
|
-
<
|
|
66
|
+
<div className={styles.titleRow}>
|
|
67
|
+
{icon && <Icon name={icon} size="lg" aria-hidden={true} />}
|
|
68
|
+
<Heading className={styles.title}>{title}</Heading>
|
|
69
|
+
</div>
|
|
30
70
|
{description && <p className={styles.description}>{description}</p>}
|
|
31
71
|
</div>
|
|
32
72
|
{action && <div className={styles.action}>{action}</div>}
|
|
33
73
|
</header>
|
|
34
74
|
);
|
|
35
75
|
}
|
|
76
|
+
|
|
77
|
+
interface PageHeaderMobileProps {
|
|
78
|
+
title: string;
|
|
79
|
+
icon?: IconName;
|
|
80
|
+
action?: ReactNode;
|
|
81
|
+
headingLevel: 1 | 2 | 3;
|
|
82
|
+
onBack?: () => void;
|
|
83
|
+
onMenuClick?: () => void;
|
|
84
|
+
className?: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// internal — not exported
|
|
88
|
+
// No `description` here, deliberately — matches the one real mobile
|
|
89
|
+
// page-header precedent this was generalized from (Kanban's own
|
|
90
|
+
// MobileBoardHeader), which has no subtitle handling either. A single
|
|
91
|
+
// fixed-shape row, consistent with every other mobile chrome bar in this
|
|
92
|
+
// design system rather than a variable-height two-line header.
|
|
93
|
+
function PageHeaderMobile({
|
|
94
|
+
title,
|
|
95
|
+
icon,
|
|
96
|
+
action,
|
|
97
|
+
headingLevel,
|
|
98
|
+
onBack,
|
|
99
|
+
onMenuClick,
|
|
100
|
+
className,
|
|
101
|
+
}: PageHeaderMobileProps) {
|
|
102
|
+
const Heading = HEADING_TAG[headingLevel];
|
|
103
|
+
return (
|
|
104
|
+
<header className={[styles.mobileHeader, className].filter(Boolean).join(' ')}>
|
|
105
|
+
{onBack && (
|
|
106
|
+
<Button
|
|
107
|
+
variant="ghost"
|
|
108
|
+
size="sm"
|
|
109
|
+
aria-label="Back"
|
|
110
|
+
onClick={onBack}
|
|
111
|
+
className={styles.mobileBack}
|
|
112
|
+
>
|
|
113
|
+
<Icon name="circle-chevron-left" size="md" aria-hidden />
|
|
114
|
+
</Button>
|
|
115
|
+
)}
|
|
116
|
+
{icon && <Icon name={icon} size="md" aria-hidden={true} className={styles.mobileTitleIcon} />}
|
|
117
|
+
<Heading className={styles.mobileTitle}>{title}</Heading>
|
|
118
|
+
{action && <div className={styles.mobileAction}>{action}</div>}
|
|
119
|
+
{onMenuClick && (
|
|
120
|
+
<Button
|
|
121
|
+
variant="ghost"
|
|
122
|
+
size="sm"
|
|
123
|
+
aria-label="Menu"
|
|
124
|
+
onClick={onMenuClick}
|
|
125
|
+
className={styles.mobileMenu}
|
|
126
|
+
>
|
|
127
|
+
<Icon name="ellipsis-vertical" size="md" aria-hidden />
|
|
128
|
+
</Button>
|
|
129
|
+
)}
|
|
130
|
+
</header>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* PageHeader — a page's title section. Web: title + description on the
|
|
136
|
+
* left, action on the right. Mobile: an optional back button, a single-line
|
|
137
|
+
* title, and the same action slot — a compact single row, matching Kanban's
|
|
138
|
+
* own hand-rolled MobileBoardHeader this was generalized from. The
|
|
139
|
+
* web/mobile fork is `ResponsiveSurface` — only one tree is ever mounted.
|
|
140
|
+
*/
|
|
141
|
+
export function PageHeader({
|
|
142
|
+
title,
|
|
143
|
+
description,
|
|
144
|
+
icon,
|
|
145
|
+
action,
|
|
146
|
+
className,
|
|
147
|
+
headingLevel = 1,
|
|
148
|
+
onBack,
|
|
149
|
+
onMenuClick,
|
|
150
|
+
}: PageHeaderProps) {
|
|
151
|
+
return (
|
|
152
|
+
<ResponsiveSurface
|
|
153
|
+
web={
|
|
154
|
+
<PageHeaderWeb
|
|
155
|
+
title={title}
|
|
156
|
+
description={description}
|
|
157
|
+
icon={icon}
|
|
158
|
+
action={action}
|
|
159
|
+
headingLevel={headingLevel}
|
|
160
|
+
className={className}
|
|
161
|
+
/>
|
|
162
|
+
}
|
|
163
|
+
mobile={
|
|
164
|
+
<PageHeaderMobile
|
|
165
|
+
title={title}
|
|
166
|
+
icon={icon}
|
|
167
|
+
action={action}
|
|
168
|
+
headingLevel={headingLevel}
|
|
169
|
+
onBack={onBack}
|
|
170
|
+
onMenuClick={onMenuClick}
|
|
171
|
+
className={className}
|
|
172
|
+
/>
|
|
173
|
+
}
|
|
174
|
+
/>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
|
|
4
|
+
import { PageHeader } from '../PageHeader';
|
|
5
|
+
|
|
6
|
+
let matches = false;
|
|
7
|
+
|
|
8
|
+
function installMatchMedia() {
|
|
9
|
+
vi.stubGlobal(
|
|
10
|
+
'matchMedia',
|
|
11
|
+
vi.fn().mockImplementation((query: string) => ({
|
|
12
|
+
matches,
|
|
13
|
+
media: query,
|
|
14
|
+
addEventListener: vi.fn(),
|
|
15
|
+
removeEventListener: vi.fn(),
|
|
16
|
+
addListener: vi.fn(),
|
|
17
|
+
removeListener: vi.fn(),
|
|
18
|
+
dispatchEvent: vi.fn(),
|
|
19
|
+
})),
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('PageHeader', () => {
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
matches = false;
|
|
26
|
+
installMatchMedia();
|
|
27
|
+
});
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
vi.unstubAllGlobals();
|
|
30
|
+
cleanup();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('web: renders title, description, and action', () => {
|
|
34
|
+
render(
|
|
35
|
+
<PageHeader
|
|
36
|
+
title="Users"
|
|
37
|
+
description="Manage who has access to this instance."
|
|
38
|
+
action={<button type="button">Invite user</button>}
|
|
39
|
+
/>,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
expect(screen.getByRole('heading', { name: 'Users' })).toBeDefined();
|
|
43
|
+
expect(screen.getByText('Manage who has access to this instance.')).toBeDefined();
|
|
44
|
+
expect(screen.getByRole('button', { name: 'Invite user' })).toBeDefined();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('web: renders no back button even when onBack is given', () => {
|
|
48
|
+
render(<PageHeader title="Users" onBack={() => undefined} />);
|
|
49
|
+
|
|
50
|
+
expect(screen.queryByRole('button', { name: 'Back' })).toBeNull();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('mobile: renders no description, no back button, no menu button by default', () => {
|
|
54
|
+
matches = true;
|
|
55
|
+
installMatchMedia();
|
|
56
|
+
|
|
57
|
+
render(<PageHeader title="Users" description="Manage who has access to this instance." />);
|
|
58
|
+
|
|
59
|
+
expect(screen.getByRole('heading', { name: 'Users' })).toBeDefined();
|
|
60
|
+
expect(screen.queryByText('Manage who has access to this instance.')).toBeNull();
|
|
61
|
+
expect(screen.queryByRole('button', { name: 'Back' })).toBeNull();
|
|
62
|
+
expect(screen.queryByRole('button', { name: 'Menu' })).toBeNull();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('mobile: renders a back button that calls onBack when given', () => {
|
|
66
|
+
matches = true;
|
|
67
|
+
installMatchMedia();
|
|
68
|
+
const onBack = vi.fn();
|
|
69
|
+
|
|
70
|
+
render(<PageHeader title="Users" onBack={onBack} />);
|
|
71
|
+
|
|
72
|
+
const back = screen.getByRole('button', { name: 'Back' });
|
|
73
|
+
fireEvent.click(back);
|
|
74
|
+
expect(onBack).toHaveBeenCalledOnce();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('mobile: renders a menu button that calls onMenuClick when given', () => {
|
|
78
|
+
matches = true;
|
|
79
|
+
installMatchMedia();
|
|
80
|
+
const onMenuClick = vi.fn();
|
|
81
|
+
|
|
82
|
+
render(<PageHeader title="Users" onMenuClick={onMenuClick} />);
|
|
83
|
+
|
|
84
|
+
const menu = screen.getByRole('button', { name: 'Menu' });
|
|
85
|
+
fireEvent.click(menu);
|
|
86
|
+
expect(onMenuClick).toHaveBeenCalledOnce();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('web: renders no menu button even when onMenuClick is given', () => {
|
|
90
|
+
render(<PageHeader title="Users" onMenuClick={() => undefined} />);
|
|
91
|
+
|
|
92
|
+
expect(screen.queryByRole('button', { name: 'Menu' })).toBeNull();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('mobile: renders the same action content as web', () => {
|
|
96
|
+
matches = true;
|
|
97
|
+
installMatchMedia();
|
|
98
|
+
|
|
99
|
+
render(<PageHeader title="Users" action={<button type="button">Invite user</button>} />);
|
|
100
|
+
|
|
101
|
+
expect(screen.getByRole('button', { name: 'Invite user' })).toBeDefined();
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
.root {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
min-width: 100%;
|
|
5
|
+
min-height: 100%;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.header {
|
|
9
|
+
flex-shrink: 0;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.content {
|
|
13
|
+
flex: 1;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/* ── Padding ────────────────────────────────────────────────────────────── */
|
|
17
|
+
/* Same four-step scale and --sv-page-gutter stand-down hook as
|
|
18
|
+
PageContainer (see that component's own CSS comment for the full
|
|
19
|
+
rationale) — kept identical so this can absorb PageContainer's role
|
|
20
|
+
later without a behavior change for existing consumers. */
|
|
21
|
+
|
|
22
|
+
.padNone {
|
|
23
|
+
padding: 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.padSm {
|
|
27
|
+
padding: var(--sv-page-gutter, var(--sv-space-4));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.padMd {
|
|
31
|
+
padding: var(--sv-page-gutter, var(--sv-space-8));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.padLg {
|
|
35
|
+
padding: var(--sv-page-gutter, var(--sv-space-12));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* Matches the breakpoint Dialog, MobileHeader, and MobileFooter already use. */
|
|
39
|
+
@media (max-width: 768px) {
|
|
40
|
+
.padSm {
|
|
41
|
+
padding: var(--sv-page-gutter, var(--sv-space-3));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.padMd {
|
|
45
|
+
padding: var(--sv-page-gutter, var(--sv-space-4));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.padLg {
|
|
49
|
+
padding: var(--sv-page-gutter, var(--sv-space-6));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { PageLayout } from './PageLayout';
|
|
3
|
+
|
|
4
|
+
function Content() {
|
|
5
|
+
return (
|
|
6
|
+
<div
|
|
7
|
+
style={{
|
|
8
|
+
fontFamily: 'var(--sv-font-family)',
|
|
9
|
+
fontSize: 'var(--sv-font-size-sm)',
|
|
10
|
+
color: 'var(--sv-color-text-primary)',
|
|
11
|
+
}}
|
|
12
|
+
>
|
|
13
|
+
{Array.from({ length: 6 }, (_, index) => (
|
|
14
|
+
<p key={index}>Page content row {index + 1}.</p>
|
|
15
|
+
))}
|
|
16
|
+
</div>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function PageHeader() {
|
|
21
|
+
return (
|
|
22
|
+
<div
|
|
23
|
+
style={{
|
|
24
|
+
display: 'flex',
|
|
25
|
+
alignItems: 'center',
|
|
26
|
+
justifyContent: 'space-between',
|
|
27
|
+
padding: 'var(--sv-space-3) var(--sv-space-4)',
|
|
28
|
+
borderBottom: '1px solid var(--sv-color-border)',
|
|
29
|
+
fontFamily: 'var(--sv-font-family)',
|
|
30
|
+
fontSize: 'var(--sv-font-size-md)',
|
|
31
|
+
fontWeight: 600,
|
|
32
|
+
color: 'var(--sv-color-text-primary)',
|
|
33
|
+
}}
|
|
34
|
+
>
|
|
35
|
+
Sv Wallet
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const meta = {
|
|
41
|
+
title: 'Components/PageLayout',
|
|
42
|
+
component: PageLayout,
|
|
43
|
+
parameters: {
|
|
44
|
+
layout: 'padded',
|
|
45
|
+
docs: {
|
|
46
|
+
description: {
|
|
47
|
+
component:
|
|
48
|
+
"A single page's content area, nested inside RootLayout's main slot — not a replacement for RootLayout. No padding by default; opt in via the padding prop (same scale as PageContainer). The optional header is page-specific (e.g. a board title/toolbar), not the app-level header RootLayout already renders.",
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
args: {
|
|
53
|
+
children: <Content />,
|
|
54
|
+
},
|
|
55
|
+
} satisfies Meta<typeof PageLayout>;
|
|
56
|
+
|
|
57
|
+
export default meta;
|
|
58
|
+
type Story = StoryObj<typeof meta>;
|
|
59
|
+
|
|
60
|
+
export const Default: Story = {};
|
|
61
|
+
|
|
62
|
+
export const WithPadding: Story = {
|
|
63
|
+
args: {
|
|
64
|
+
padding: 'lg',
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const WithHeader: Story = {
|
|
69
|
+
args: {
|
|
70
|
+
header: <PageHeader />,
|
|
71
|
+
padding: 'md',
|
|
72
|
+
},
|
|
73
|
+
};
|