@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,258 @@
|
|
|
1
|
+
.trigger {
|
|
2
|
+
position: relative;
|
|
3
|
+
display: inline-flex;
|
|
4
|
+
align-items: center;
|
|
5
|
+
justify-content: center;
|
|
6
|
+
flex-shrink: 0;
|
|
7
|
+
width: 28px;
|
|
8
|
+
height: 28px;
|
|
9
|
+
padding: 0;
|
|
10
|
+
border: none;
|
|
11
|
+
border-radius: var(--sv-radius-md);
|
|
12
|
+
background: transparent;
|
|
13
|
+
color: var(--sv-color-text-primary);
|
|
14
|
+
cursor: pointer;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@media (hover: hover) {
|
|
18
|
+
.trigger:hover {
|
|
19
|
+
background: var(--sv-color-surface-sunken);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.badge {
|
|
24
|
+
position: absolute;
|
|
25
|
+
top: 2px;
|
|
26
|
+
right: 2px;
|
|
27
|
+
min-width: 14px;
|
|
28
|
+
height: 14px;
|
|
29
|
+
padding: 0 3px;
|
|
30
|
+
background: var(--sv-color-error-text);
|
|
31
|
+
color: white;
|
|
32
|
+
border-radius: var(--sv-radius-full);
|
|
33
|
+
font-size: 9px;
|
|
34
|
+
font-weight: 700;
|
|
35
|
+
line-height: 14px;
|
|
36
|
+
text-align: center;
|
|
37
|
+
pointer-events: none;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* ── Header ─────────────────────────────────────────────────────────────── */
|
|
41
|
+
|
|
42
|
+
.header {
|
|
43
|
+
display: flex;
|
|
44
|
+
align-items: center;
|
|
45
|
+
justify-content: space-between;
|
|
46
|
+
gap: var(--sv-space-2);
|
|
47
|
+
padding: var(--sv-space-3) var(--sv-space-3) var(--sv-space-3) var(--sv-space-4);
|
|
48
|
+
border-bottom: var(--sv-border-width-hairline) solid var(--sv-color-border);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.headerTitle {
|
|
52
|
+
font-size: var(--sv-font-size-sm);
|
|
53
|
+
font-weight: var(--sv-font-weight-semibold);
|
|
54
|
+
color: var(--sv-color-text-primary);
|
|
55
|
+
flex: 1;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.headerActions {
|
|
59
|
+
display: flex;
|
|
60
|
+
align-items: center;
|
|
61
|
+
gap: var(--sv-space-2);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.actionBtn {
|
|
65
|
+
background: none;
|
|
66
|
+
border: none;
|
|
67
|
+
padding: 0;
|
|
68
|
+
cursor: pointer;
|
|
69
|
+
font-size: var(--sv-font-size-caption);
|
|
70
|
+
color: var(--sv-color-text-muted);
|
|
71
|
+
white-space: nowrap;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@media (hover: hover) {
|
|
75
|
+
.actionBtn:hover {
|
|
76
|
+
color: var(--sv-color-text-primary);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.closeBtn {
|
|
81
|
+
display: flex;
|
|
82
|
+
align-items: center;
|
|
83
|
+
justify-content: center;
|
|
84
|
+
width: 24px;
|
|
85
|
+
height: 24px;
|
|
86
|
+
padding: 0;
|
|
87
|
+
border: none;
|
|
88
|
+
border-radius: var(--sv-radius-sm);
|
|
89
|
+
background: none;
|
|
90
|
+
color: var(--sv-color-text-muted);
|
|
91
|
+
cursor: pointer;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
@media (hover: hover) {
|
|
95
|
+
.closeBtn:hover {
|
|
96
|
+
color: var(--sv-color-text-primary);
|
|
97
|
+
background: var(--sv-color-surface-sunken);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* ── List ───────────────────────────────────────────────────────────────── */
|
|
102
|
+
|
|
103
|
+
.list {
|
|
104
|
+
list-style: none;
|
|
105
|
+
margin: 0;
|
|
106
|
+
padding: 0;
|
|
107
|
+
max-height: 420px;
|
|
108
|
+
overflow-y: auto;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/* ── Empty state ────────────────────────────────────────────────────────── */
|
|
112
|
+
|
|
113
|
+
.empty {
|
|
114
|
+
display: flex;
|
|
115
|
+
flex-direction: column;
|
|
116
|
+
align-items: center;
|
|
117
|
+
gap: var(--sv-space-3);
|
|
118
|
+
padding: var(--sv-space-10) var(--sv-space-4);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.emptyIcon {
|
|
122
|
+
display: flex;
|
|
123
|
+
align-items: center;
|
|
124
|
+
justify-content: center;
|
|
125
|
+
width: 44px;
|
|
126
|
+
height: 44px;
|
|
127
|
+
border-radius: var(--sv-radius-full);
|
|
128
|
+
background: var(--sv-color-surface-sunken);
|
|
129
|
+
color: var(--sv-color-text-muted);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.emptyText {
|
|
133
|
+
font-size: var(--sv-font-size-sm);
|
|
134
|
+
color: var(--sv-color-text-muted);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* ── Notification item ─────────────────────────────────────────────────── */
|
|
138
|
+
|
|
139
|
+
.item {
|
|
140
|
+
display: flex;
|
|
141
|
+
align-items: flex-start;
|
|
142
|
+
gap: var(--sv-space-3);
|
|
143
|
+
padding: var(--sv-space-3);
|
|
144
|
+
border-bottom: var(--sv-border-width-hairline) solid var(--sv-color-border);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.item:last-child {
|
|
148
|
+
border-bottom: none;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.categoryIcon {
|
|
152
|
+
display: flex;
|
|
153
|
+
align-items: center;
|
|
154
|
+
justify-content: center;
|
|
155
|
+
flex: none;
|
|
156
|
+
width: 32px;
|
|
157
|
+
height: 32px;
|
|
158
|
+
border-radius: var(--sv-radius-full);
|
|
159
|
+
margin-top: 1px;
|
|
160
|
+
background: var(--sv-color-surface-sunken);
|
|
161
|
+
color: var(--sv-color-text-muted);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.itemBody {
|
|
165
|
+
flex: 1 1 auto;
|
|
166
|
+
min-width: 0;
|
|
167
|
+
display: flex;
|
|
168
|
+
flex-direction: column;
|
|
169
|
+
gap: 2px;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.itemTitle {
|
|
173
|
+
display: block;
|
|
174
|
+
width: 100%;
|
|
175
|
+
border: none;
|
|
176
|
+
padding: 0;
|
|
177
|
+
background: none;
|
|
178
|
+
font: inherit;
|
|
179
|
+
font-size: var(--sv-font-size-sm);
|
|
180
|
+
font-weight: var(--sv-font-weight-semibold);
|
|
181
|
+
color: var(--sv-color-text-primary);
|
|
182
|
+
text-decoration: none;
|
|
183
|
+
text-align: left;
|
|
184
|
+
line-height: 1.4;
|
|
185
|
+
cursor: pointer;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
@media (hover: hover) {
|
|
189
|
+
a.itemTitle:hover,
|
|
190
|
+
button.itemTitle:hover {
|
|
191
|
+
text-decoration: underline;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.itemRead .itemTitle {
|
|
196
|
+
color: var(--sv-color-text-subtle);
|
|
197
|
+
cursor: default;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.itemTime {
|
|
201
|
+
font-size: var(--sv-font-size-caption);
|
|
202
|
+
color: var(--sv-color-text-muted);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.itemEnd {
|
|
206
|
+
display: flex;
|
|
207
|
+
align-items: center;
|
|
208
|
+
gap: var(--sv-space-2);
|
|
209
|
+
flex: none;
|
|
210
|
+
padding-top: 2px;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.unreadDot {
|
|
214
|
+
display: block;
|
|
215
|
+
width: 7px;
|
|
216
|
+
height: 7px;
|
|
217
|
+
border-radius: var(--sv-radius-full);
|
|
218
|
+
background: var(--sv-color-accent);
|
|
219
|
+
flex: none;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.dismissBtn {
|
|
223
|
+
display: flex;
|
|
224
|
+
align-items: center;
|
|
225
|
+
justify-content: center;
|
|
226
|
+
width: 20px;
|
|
227
|
+
height: 20px;
|
|
228
|
+
padding: 0;
|
|
229
|
+
border: none;
|
|
230
|
+
border-radius: var(--sv-radius-sm);
|
|
231
|
+
background: none;
|
|
232
|
+
color: var(--sv-color-text-muted);
|
|
233
|
+
cursor: pointer;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
@media (hover: hover) {
|
|
237
|
+
.dismissBtn:hover {
|
|
238
|
+
color: var(--sv-color-text-primary);
|
|
239
|
+
background: var(--sv-color-surface-sunken);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.viewAll {
|
|
244
|
+
display: block;
|
|
245
|
+
text-align: center;
|
|
246
|
+
padding: var(--sv-space-2) var(--sv-space-3);
|
|
247
|
+
border-top: var(--sv-border-width-hairline) solid var(--sv-color-border);
|
|
248
|
+
font-size: var(--sv-font-size-caption);
|
|
249
|
+
color: var(--sv-color-text-muted);
|
|
250
|
+
text-decoration: none;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
@media (hover: hover) {
|
|
254
|
+
.viewAll:hover {
|
|
255
|
+
color: var(--sv-color-text-primary);
|
|
256
|
+
background: var(--sv-color-surface-sunken);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { NotificationsPanel } from './NotificationsPanel';
|
|
3
|
+
import { Icon } from '../Icon/Icon';
|
|
4
|
+
|
|
5
|
+
const items = [
|
|
6
|
+
{
|
|
7
|
+
id: '1',
|
|
8
|
+
icon: <Icon name="layers" size="sm" aria-hidden />,
|
|
9
|
+
title: 'Added to a project',
|
|
10
|
+
timeLabel: '2d ago',
|
|
11
|
+
read: false,
|
|
12
|
+
onDismiss: () => {},
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
id: '2',
|
|
16
|
+
icon: <Icon name="user-round-plus" size="sm" aria-hidden />,
|
|
17
|
+
title: 'Jamie invited you to Notes',
|
|
18
|
+
timeLabel: '3d ago',
|
|
19
|
+
read: true,
|
|
20
|
+
href: '/notes',
|
|
21
|
+
onDismiss: () => {},
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const meta = {
|
|
26
|
+
title: 'Components/NotificationsPanel',
|
|
27
|
+
component: NotificationsPanel,
|
|
28
|
+
parameters: {
|
|
29
|
+
layout: 'centered',
|
|
30
|
+
docs: {
|
|
31
|
+
description: {
|
|
32
|
+
component:
|
|
33
|
+
"The bell trigger + dropdown, generalized from the runtime shell's own NotificationBell. Pure presentation — no fetch/SSE/mark-read logic; the consumer supplies already-resolved items and wires onOpen/onDismiss/onMarkAllRead/onClearAll.",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
args: {
|
|
38
|
+
items,
|
|
39
|
+
unreadCount: 1,
|
|
40
|
+
onMarkAllRead: () => {},
|
|
41
|
+
onClearAll: () => {},
|
|
42
|
+
},
|
|
43
|
+
} satisfies Meta<typeof NotificationsPanel>;
|
|
44
|
+
|
|
45
|
+
export default meta;
|
|
46
|
+
type Story = StoryObj<typeof meta>;
|
|
47
|
+
|
|
48
|
+
export const Default: Story = {};
|
|
49
|
+
|
|
50
|
+
export const Empty: Story = {
|
|
51
|
+
args: {
|
|
52
|
+
items: [],
|
|
53
|
+
unreadCount: 0,
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const WithViewAll: Story = {
|
|
58
|
+
args: {
|
|
59
|
+
viewAllHref: '/inbox',
|
|
60
|
+
},
|
|
61
|
+
};
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, type ReactNode } from 'react';
|
|
4
|
+
import { Icon } from '../Icon/Icon';
|
|
5
|
+
import { Popover } from '../Popover/Popover';
|
|
6
|
+
import styles from './NotificationsPanel.module.css';
|
|
7
|
+
|
|
8
|
+
export interface NotificationsPanelItem {
|
|
9
|
+
id: string;
|
|
10
|
+
/** Category icon, already resolved (sized + colored) by the consumer —
|
|
11
|
+
* this component doesn't own category → color mapping. */
|
|
12
|
+
icon: ReactNode;
|
|
13
|
+
title: string;
|
|
14
|
+
/** Pre-formatted relative time (e.g. "2d ago") — formatting stays
|
|
15
|
+
* consumer-side so this component needs no Date/clock dependency. */
|
|
16
|
+
timeLabel: string;
|
|
17
|
+
read: boolean;
|
|
18
|
+
/** Renders the title as a link when set. */
|
|
19
|
+
href?: string;
|
|
20
|
+
/** Called when the title is clicked/tapped — typically "mark as read". */
|
|
21
|
+
onOpen?: () => void;
|
|
22
|
+
onDismiss: () => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface NotificationsPanelProps {
|
|
26
|
+
items: NotificationsPanelItem[];
|
|
27
|
+
unreadCount: number;
|
|
28
|
+
onMarkAllRead?: () => void;
|
|
29
|
+
onClearAll?: () => void;
|
|
30
|
+
emptyMessage?: string;
|
|
31
|
+
/** Trigger button's accessible label base — the unread count is appended
|
|
32
|
+
* automatically, matching the real NotificationBell. Defaults to
|
|
33
|
+
* "Notifications". */
|
|
34
|
+
'aria-label'?: string;
|
|
35
|
+
align?: 'left' | 'right';
|
|
36
|
+
/** Optional footer link (e.g. to the consumer's own full notification
|
|
37
|
+
* list/detail view — RFC 0048) — mirrors the real `NotificationBell`'s
|
|
38
|
+
* "View all" link. Omit to render no footer, matching this component's
|
|
39
|
+
* behavior before this prop existed. */
|
|
40
|
+
viewAllHref?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function NotificationRow({ item }: { item: NotificationsPanelItem }) {
|
|
44
|
+
const titleContent = item.href ? (
|
|
45
|
+
<a href={item.href} className={styles.itemTitle} onClick={item.onOpen}>
|
|
46
|
+
{item.title}
|
|
47
|
+
</a>
|
|
48
|
+
) : !item.read ? (
|
|
49
|
+
<button
|
|
50
|
+
type="button"
|
|
51
|
+
className={styles.itemTitle}
|
|
52
|
+
aria-label={`Mark as read: ${item.title}`}
|
|
53
|
+
onClick={item.onOpen}
|
|
54
|
+
>
|
|
55
|
+
{item.title}
|
|
56
|
+
</button>
|
|
57
|
+
) : (
|
|
58
|
+
<span className={styles.itemTitle}>{item.title}</span>
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<li className={[styles.item, item.read ? styles.itemRead : ''].filter(Boolean).join(' ')}>
|
|
63
|
+
<div className={styles.categoryIcon}>{item.icon}</div>
|
|
64
|
+
<div className={styles.itemBody}>
|
|
65
|
+
{titleContent}
|
|
66
|
+
<span className={styles.itemTime}>{item.timeLabel}</span>
|
|
67
|
+
</div>
|
|
68
|
+
<div className={styles.itemEnd}>
|
|
69
|
+
{!item.read && <span className={styles.unreadDot} aria-label="Unread" />}
|
|
70
|
+
<button
|
|
71
|
+
type="button"
|
|
72
|
+
className={styles.dismissBtn}
|
|
73
|
+
aria-label={`Dismiss: ${item.title}`}
|
|
74
|
+
onClick={item.onDismiss}
|
|
75
|
+
>
|
|
76
|
+
<Icon name="x" size="sm" aria-hidden />
|
|
77
|
+
</button>
|
|
78
|
+
</div>
|
|
79
|
+
</li>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* NotificationsPanel — the bell trigger + dropdown, generalized from the
|
|
85
|
+
* runtime shell's own `NotificationBell.tsx` visual structure and copy
|
|
86
|
+
* (title / Mark all read / Clear all / close, then a list of category-icon +
|
|
87
|
+
* title + time + unread-dot + dismiss rows, or an empty state).
|
|
88
|
+
*
|
|
89
|
+
* Deliberately **presentational only** — no fetch/SSE/polling, no
|
|
90
|
+
* category → color mapping, no mark-read/dismiss networking. The real
|
|
91
|
+
* `NotificationBell` owns real platform data (`/api/account/notifications`,
|
|
92
|
+
* a shared cross-instance store); this component can't and shouldn't know
|
|
93
|
+
* about that (SDK boundary). The consumer supplies `items` already resolved
|
|
94
|
+
* and wires `onOpen`/`onDismiss`/`onMarkAllRead`/`onClearAll` to whatever
|
|
95
|
+
* its own data layer needs, against a shared panel instead of a
|
|
96
|
+
* `shell: minimal` plugin hand-rebuilding one locally. Built on the shared
|
|
97
|
+
* `Popover` primitive (self-contained open state) rather than the
|
|
98
|
+
* original's hand-rolled outside-click/Escape listener.
|
|
99
|
+
*/
|
|
100
|
+
export function NotificationsPanel({
|
|
101
|
+
items,
|
|
102
|
+
unreadCount,
|
|
103
|
+
onMarkAllRead,
|
|
104
|
+
onClearAll,
|
|
105
|
+
emptyMessage = 'No notifications.',
|
|
106
|
+
'aria-label': ariaLabel = 'Notifications',
|
|
107
|
+
align = 'right',
|
|
108
|
+
viewAllHref,
|
|
109
|
+
}: NotificationsPanelProps) {
|
|
110
|
+
const [open, setOpen] = useState(false);
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<Popover
|
|
114
|
+
align={align}
|
|
115
|
+
width={340}
|
|
116
|
+
aria-label={ariaLabel}
|
|
117
|
+
open={open}
|
|
118
|
+
onClose={() => setOpen(false)}
|
|
119
|
+
trigger={
|
|
120
|
+
<button
|
|
121
|
+
type="button"
|
|
122
|
+
aria-label={unreadCount > 0 ? `${ariaLabel} (${unreadCount} unread)` : ariaLabel}
|
|
123
|
+
aria-haspopup="true"
|
|
124
|
+
aria-expanded={open}
|
|
125
|
+
onClick={() => setOpen((v) => !v)}
|
|
126
|
+
className={styles.trigger}
|
|
127
|
+
>
|
|
128
|
+
{/* size="lg" (24px), not "md" (20px) — matches the real
|
|
129
|
+
NotificationBell's own inline SVG, which is literally
|
|
130
|
+
width="24" height="24", not the DS Icon scale's "md" step. */}
|
|
131
|
+
<Icon name="bell" size="lg" aria-hidden />
|
|
132
|
+
{unreadCount > 0 && (
|
|
133
|
+
<span className={styles.badge} aria-hidden="true">
|
|
134
|
+
{unreadCount > 99 ? '99+' : unreadCount}
|
|
135
|
+
</span>
|
|
136
|
+
)}
|
|
137
|
+
</button>
|
|
138
|
+
}
|
|
139
|
+
>
|
|
140
|
+
<div className={styles.header}>
|
|
141
|
+
<span className={styles.headerTitle}>{ariaLabel}</span>
|
|
142
|
+
<div className={styles.headerActions}>
|
|
143
|
+
{items.length > 0 && (
|
|
144
|
+
<>
|
|
145
|
+
{unreadCount > 0 && onMarkAllRead && (
|
|
146
|
+
<button type="button" className={styles.actionBtn} onClick={onMarkAllRead}>
|
|
147
|
+
Mark all read
|
|
148
|
+
</button>
|
|
149
|
+
)}
|
|
150
|
+
{onClearAll && (
|
|
151
|
+
<button type="button" className={styles.actionBtn} onClick={onClearAll}>
|
|
152
|
+
Clear all
|
|
153
|
+
</button>
|
|
154
|
+
)}
|
|
155
|
+
</>
|
|
156
|
+
)}
|
|
157
|
+
<button
|
|
158
|
+
type="button"
|
|
159
|
+
className={styles.closeBtn}
|
|
160
|
+
aria-label={`Close ${ariaLabel.toLowerCase()}`}
|
|
161
|
+
onClick={() => setOpen(false)}
|
|
162
|
+
>
|
|
163
|
+
<Icon name="x" size="sm" aria-hidden />
|
|
164
|
+
</button>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<ul className={styles.list} aria-label={`${ariaLabel} list`}>
|
|
169
|
+
{items.length === 0 ? (
|
|
170
|
+
<li className={styles.empty}>
|
|
171
|
+
<span className={styles.emptyIcon} aria-hidden="true">
|
|
172
|
+
<Icon name="bell" size="lg" aria-hidden />
|
|
173
|
+
</span>
|
|
174
|
+
<span className={styles.emptyText}>{emptyMessage}</span>
|
|
175
|
+
</li>
|
|
176
|
+
) : (
|
|
177
|
+
items.map((item) => <NotificationRow key={item.id} item={item} />)
|
|
178
|
+
)}
|
|
179
|
+
</ul>
|
|
180
|
+
{viewAllHref && (
|
|
181
|
+
<a href={viewAllHref} className={styles.viewAll} onClick={() => setOpen(false)}>
|
|
182
|
+
View all
|
|
183
|
+
</a>
|
|
184
|
+
)}
|
|
185
|
+
</Popover>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
|
|
4
|
+
import { NotificationsPanel } from '../NotificationsPanel';
|
|
5
|
+
|
|
6
|
+
afterEach(() => {
|
|
7
|
+
cleanup();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const items = [
|
|
11
|
+
{
|
|
12
|
+
id: '1',
|
|
13
|
+
icon: <span>icon</span>,
|
|
14
|
+
title: 'Added to a project',
|
|
15
|
+
timeLabel: '2d ago',
|
|
16
|
+
read: false,
|
|
17
|
+
onOpen: vi.fn(),
|
|
18
|
+
onDismiss: vi.fn(),
|
|
19
|
+
},
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
describe('NotificationsPanel', () => {
|
|
23
|
+
it('renders a closed trigger with an unread badge', () => {
|
|
24
|
+
render(<NotificationsPanel items={items} unreadCount={1} />);
|
|
25
|
+
expect(screen.getByRole('button', { name: /1 unread/ })).toBeTruthy();
|
|
26
|
+
expect(screen.queryByText('Added to a project')).toBeNull();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('renders no badge when there are no unread notifications', () => {
|
|
30
|
+
render(<NotificationsPanel items={[]} unreadCount={0} />);
|
|
31
|
+
expect(screen.getByRole('button', { name: 'Notifications' })).toBeTruthy();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('opens the panel on trigger click, showing items', () => {
|
|
35
|
+
render(<NotificationsPanel items={items} unreadCount={1} />);
|
|
36
|
+
fireEvent.click(screen.getByRole('button', { name: /unread/ }));
|
|
37
|
+
expect(screen.getByText('Added to a project')).toBeTruthy();
|
|
38
|
+
expect(screen.getByText('2d ago')).toBeTruthy();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('shows an empty state when there are no items', () => {
|
|
42
|
+
render(<NotificationsPanel items={[]} unreadCount={0} />);
|
|
43
|
+
fireEvent.click(screen.getByRole('button', { name: 'Notifications' }));
|
|
44
|
+
expect(screen.getByText('No notifications.')).toBeTruthy();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('calls onOpen when an unread title is clicked', () => {
|
|
48
|
+
render(<NotificationsPanel items={items} unreadCount={1} />);
|
|
49
|
+
fireEvent.click(screen.getByRole('button', { name: /unread/ }));
|
|
50
|
+
fireEvent.click(screen.getByRole('button', { name: 'Mark as read: Added to a project' }));
|
|
51
|
+
expect(items[0]?.onOpen).toHaveBeenCalledOnce();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('calls onDismiss when the dismiss button is clicked', () => {
|
|
55
|
+
render(<NotificationsPanel items={items} unreadCount={1} />);
|
|
56
|
+
fireEvent.click(screen.getByRole('button', { name: /unread/ }));
|
|
57
|
+
fireEvent.click(screen.getByRole('button', { name: 'Dismiss: Added to a project' }));
|
|
58
|
+
expect(items[0]?.onDismiss).toHaveBeenCalledOnce();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('calls onMarkAllRead and onClearAll from the header actions', () => {
|
|
62
|
+
const onMarkAllRead = vi.fn();
|
|
63
|
+
const onClearAll = vi.fn();
|
|
64
|
+
render(
|
|
65
|
+
<NotificationsPanel
|
|
66
|
+
items={items}
|
|
67
|
+
unreadCount={1}
|
|
68
|
+
onMarkAllRead={onMarkAllRead}
|
|
69
|
+
onClearAll={onClearAll}
|
|
70
|
+
/>,
|
|
71
|
+
);
|
|
72
|
+
fireEvent.click(screen.getByRole('button', { name: /unread/ }));
|
|
73
|
+
fireEvent.click(screen.getByRole('button', { name: 'Mark all read' }));
|
|
74
|
+
fireEvent.click(screen.getByRole('button', { name: 'Clear all' }));
|
|
75
|
+
expect(onMarkAllRead).toHaveBeenCalledOnce();
|
|
76
|
+
expect(onClearAll).toHaveBeenCalledOnce();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -5,8 +5,12 @@ import styles from './OverlayHeader.module.css';
|
|
|
5
5
|
export interface OverlayHeaderProps {
|
|
6
6
|
/** Overlay title, shown between the optional back button and the trailing
|
|
7
7
|
* slots. Optional — some consumers (e.g. `Dialog`'s mobile bar) need the
|
|
8
|
-
* row's close button present even when no title applies.
|
|
9
|
-
|
|
8
|
+
* row's close button present even when no title applies. Accepts any
|
|
9
|
+
* `ReactNode`, not just a string — e.g. `Dialog`'s `header` prop passes
|
|
10
|
+
* arbitrary content through here (an icon next to text, etc.); the
|
|
11
|
+
* row's title slot still assumes single-line content sized like text
|
|
12
|
+
* (see the component's own doc comment for the line this doesn't cross). */
|
|
13
|
+
title?: ReactNode;
|
|
10
14
|
/** Always renders a close button; called on click. */
|
|
11
15
|
onClose: () => void;
|
|
12
16
|
/** When provided, renders a back-chevron button before the title (for
|
|
@@ -18,6 +22,15 @@ export interface OverlayHeaderProps {
|
|
|
18
22
|
/** A second row below the title row — e.g. a tab strip. */
|
|
19
23
|
secondRow?: ReactNode;
|
|
20
24
|
className?: string;
|
|
25
|
+
/** Override the title row's own className (e.g. tighter padding to match
|
|
26
|
+
* a consumer's own compact header elsewhere in the same surface) — the
|
|
27
|
+
* row keeps its standard title/back/action/close layout, just restyled.
|
|
28
|
+
* For a genuinely different shape, don't use `OverlayHeader` at all (see
|
|
29
|
+
* this component's own doc comment). */
|
|
30
|
+
rowClassName?: string;
|
|
31
|
+
/** Override the title text's own className (e.g. a smaller font size to
|
|
32
|
+
* match a consumer's own compact header elsewhere in the same surface). */
|
|
33
|
+
titleClassName?: string;
|
|
21
34
|
}
|
|
22
35
|
|
|
23
36
|
/**
|
|
@@ -41,16 +54,18 @@ export function OverlayHeader({
|
|
|
41
54
|
action,
|
|
42
55
|
secondRow,
|
|
43
56
|
className,
|
|
57
|
+
rowClassName,
|
|
58
|
+
titleClassName,
|
|
44
59
|
}: OverlayHeaderProps) {
|
|
45
60
|
return (
|
|
46
61
|
<div className={[styles.header, className].filter(Boolean).join(' ')}>
|
|
47
|
-
<div className={styles.row}>
|
|
62
|
+
<div className={[styles.row, rowClassName].filter(Boolean).join(' ')}>
|
|
48
63
|
{onBack && (
|
|
49
64
|
<button type="button" className={styles.backButton} aria-label="Back" onClick={onBack}>
|
|
50
65
|
<Icon name="chevron-left" size="md" aria-hidden />
|
|
51
66
|
</button>
|
|
52
67
|
)}
|
|
53
|
-
<span className={styles.title}>{title}</span>
|
|
68
|
+
<span className={[styles.title, titleClassName].filter(Boolean).join(' ')}>{title}</span>
|
|
54
69
|
{action && <div className={styles.action}>{action}</div>}
|
|
55
70
|
<button type="button" className={styles.closeButton} aria-label="Close" onClick={onClose}>
|
|
56
71
|
<Icon name="x" size="sm" aria-hidden />
|
|
@@ -48,4 +48,22 @@ describe('OverlayHeader', () => {
|
|
|
48
48
|
render(<OverlayHeader title="Account" onClose={() => {}} secondRow={<nav>Tab strip</nav>} />);
|
|
49
49
|
expect(screen.getByText('Tab strip')).toBeTruthy();
|
|
50
50
|
});
|
|
51
|
+
|
|
52
|
+
it('accepts arbitrary ReactNode content for title, not just a string', () => {
|
|
53
|
+
// Locks in the type widening (title?: string -> title?: ReactNode) that
|
|
54
|
+
// Dialog's `header` prop relies on (Dialog.tsx passes header through
|
|
55
|
+
// here as `title`) — a plain string title must keep working unchanged.
|
|
56
|
+
render(
|
|
57
|
+
<OverlayHeader
|
|
58
|
+
title={
|
|
59
|
+
<>
|
|
60
|
+
<strong>Card</strong> detail
|
|
61
|
+
</>
|
|
62
|
+
}
|
|
63
|
+
onClose={() => {}}
|
|
64
|
+
/>,
|
|
65
|
+
);
|
|
66
|
+
expect(screen.getByText('Card').tagName).toBe('STRONG');
|
|
67
|
+
expect(screen.getByText('detail')).toBeTruthy();
|
|
68
|
+
});
|
|
51
69
|
});
|