@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,7 +1,9 @@
|
|
|
1
1
|
/* Toast — transient notification snackbar.
|
|
2
2
|
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* Desktop: fixed top-right, 320px, slides in from above. Mobile floats
|
|
4
|
+
* bottom-center instead (see the media query near the bottom of this file
|
|
5
|
+
* for why). Each toast auto-dismisses after `duration` ms. Leading status
|
|
6
|
+
* icon reinforces category at a glance.
|
|
5
7
|
*
|
|
6
8
|
* Category variants map to the semantic status tokens (info/success/warning/
|
|
7
9
|
* error) so the visual language is consistent with banners and badges.
|
|
@@ -28,7 +30,7 @@
|
|
|
28
30
|
padding: var(--sv-space-3) var(--sv-space-4);
|
|
29
31
|
background: var(--sv-color-surface-raised);
|
|
30
32
|
color: var(--sv-color-text-primary);
|
|
31
|
-
border:
|
|
33
|
+
border: var(--sv-border-width-hairline) solid var(--sv-color-border);
|
|
32
34
|
border-radius: var(--sv-radius-md);
|
|
33
35
|
box-shadow: var(--sv-shadow-overlay);
|
|
34
36
|
pointer-events: all;
|
|
@@ -124,6 +126,63 @@
|
|
|
124
126
|
}
|
|
125
127
|
}
|
|
126
128
|
|
|
129
|
+
/* Mobile (<=768px, the design system's canonical breakpoint): float
|
|
130
|
+
* bottom-center instead of top-right. Clears any mobile footer/tab bar
|
|
131
|
+
* (--sv-shell-footer-height) and the home-indicator safe area, plus extra
|
|
132
|
+
* breathing room so it visibly floats above the edge rather than sitting
|
|
133
|
+
* flush against it — matches the platform convention (Android Toast,
|
|
134
|
+
* Material 3's "floating" Snackbar). Deliberately not threaded through
|
|
135
|
+
* --sv-shell-header-height instead: a toast can appear on a page with or
|
|
136
|
+
* without a self-rendered mobile header, and bottom-anchoring sidesteps that
|
|
137
|
+
* entirely rather than requiring every call site to account for it.
|
|
138
|
+
* Each toast is centered and content-sized (capped at 360px) rather than
|
|
139
|
+
* stretched to the region's full width, and slides up from below instead of
|
|
140
|
+
* down from above to match its new anchor. */
|
|
141
|
+
@media (max-width: 768px) {
|
|
142
|
+
.region {
|
|
143
|
+
top: auto;
|
|
144
|
+
left: var(--sv-space-4);
|
|
145
|
+
right: var(--sv-space-4);
|
|
146
|
+
bottom: calc(
|
|
147
|
+
var(--sv-shell-footer-height, 0px) + env(safe-area-inset-bottom, 0px) + var(--sv-space-4)
|
|
148
|
+
);
|
|
149
|
+
width: auto;
|
|
150
|
+
align-items: center;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.toast {
|
|
154
|
+
width: 100%;
|
|
155
|
+
max-width: 360px;
|
|
156
|
+
animation-name: toastInMobile;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.toast.exiting {
|
|
160
|
+
animation-name: toastOutMobile;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
@keyframes toastInMobile {
|
|
165
|
+
from {
|
|
166
|
+
opacity: 0;
|
|
167
|
+
transform: translateY(8px);
|
|
168
|
+
}
|
|
169
|
+
to {
|
|
170
|
+
opacity: 1;
|
|
171
|
+
transform: translateY(0);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
@keyframes toastOutMobile {
|
|
176
|
+
from {
|
|
177
|
+
opacity: 1;
|
|
178
|
+
transform: translateY(0);
|
|
179
|
+
}
|
|
180
|
+
to {
|
|
181
|
+
opacity: 0;
|
|
182
|
+
transform: translateY(8px);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
127
186
|
@media (prefers-reduced-motion: reduce) {
|
|
128
187
|
.toast,
|
|
129
188
|
.toast.exiting {
|
|
@@ -10,7 +10,7 @@ const meta = {
|
|
|
10
10
|
docs: {
|
|
11
11
|
description: {
|
|
12
12
|
component:
|
|
13
|
-
'Fixed top-right notification stack. Use `<ToastProvider>` at the app root and `useToast()` to imperatively show toasts. Each toast has a leading status icon matched to its category.',
|
|
13
|
+
'Fixed top-right notification stack on desktop; floats bottom-center on mobile (<=768px), clearing any mobile footer/tab bar and the home-indicator safe area. Use `<ToastProvider>` at the app root and `useToast()` to imperatively show toasts. Each toast has a leading status icon matched to its category. Switch the viewport toolbar to a mobile width to see the bottom-center placement.',
|
|
14
14
|
},
|
|
15
15
|
},
|
|
16
16
|
},
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
white-space: nowrap;
|
|
10
10
|
padding: var(--sv-space-1) var(--sv-space-2);
|
|
11
11
|
background: var(--sv-color-surface-raised);
|
|
12
|
-
border:
|
|
12
|
+
border: var(--sv-border-width-hairline) solid var(--sv-color-border);
|
|
13
13
|
border-radius: var(--sv-radius-md);
|
|
14
14
|
box-shadow: var(--sv-shadow-popover);
|
|
15
15
|
font-size: var(--sv-font-size-xs);
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
.trigger {
|
|
2
|
+
display: inline-flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
justify-content: center;
|
|
5
|
+
flex-shrink: 0;
|
|
6
|
+
width: 36px;
|
|
7
|
+
height: 36px;
|
|
8
|
+
padding: 0;
|
|
9
|
+
border: var(--sv-border-width-hairline) solid var(--sv-color-accent);
|
|
10
|
+
border-radius: var(--sv-radius-full);
|
|
11
|
+
background-color: var(--sv-color-accent);
|
|
12
|
+
color: var(--sv-color-text-on-accent);
|
|
13
|
+
font-size: var(--sv-font-size-xs);
|
|
14
|
+
font-weight: var(--sv-font-weight-semibold);
|
|
15
|
+
overflow: hidden;
|
|
16
|
+
cursor: pointer;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/* The compact `Header` top-bar context's own avatar size. */
|
|
20
|
+
.triggerSm {
|
|
21
|
+
width: 32px;
|
|
22
|
+
height: 32px;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.triggerImg {
|
|
26
|
+
width: 100%;
|
|
27
|
+
height: 100%;
|
|
28
|
+
object-fit: cover;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.panelHeader {
|
|
32
|
+
padding: var(--sv-space-1) var(--sv-space-1) 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.userHeader {
|
|
36
|
+
display: flex;
|
|
37
|
+
align-items: center;
|
|
38
|
+
gap: var(--sv-space-3);
|
|
39
|
+
padding: var(--sv-space-2) var(--sv-space-3);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.menuAvatar {
|
|
43
|
+
display: flex;
|
|
44
|
+
align-items: center;
|
|
45
|
+
justify-content: center;
|
|
46
|
+
flex-shrink: 0;
|
|
47
|
+
width: 38px;
|
|
48
|
+
height: 38px;
|
|
49
|
+
border-radius: var(--sv-radius-full);
|
|
50
|
+
background-color: var(--sv-color-accent);
|
|
51
|
+
color: var(--sv-color-text-on-accent);
|
|
52
|
+
font-size: var(--sv-font-size-sm);
|
|
53
|
+
font-weight: var(--sv-font-weight-semibold);
|
|
54
|
+
overflow: hidden;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.menuAvatarImg {
|
|
58
|
+
width: 100%;
|
|
59
|
+
height: 100%;
|
|
60
|
+
object-fit: cover;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.userInfo {
|
|
64
|
+
display: flex;
|
|
65
|
+
flex-direction: column;
|
|
66
|
+
min-width: 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.userName {
|
|
70
|
+
font-size: var(--sv-font-size-sm);
|
|
71
|
+
font-weight: var(--sv-font-weight-semibold);
|
|
72
|
+
color: var(--sv-color-text-primary);
|
|
73
|
+
white-space: nowrap;
|
|
74
|
+
overflow: hidden;
|
|
75
|
+
text-overflow: ellipsis;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.userEmail {
|
|
79
|
+
font-size: var(--sv-font-size-caption);
|
|
80
|
+
color: var(--sv-color-text-muted);
|
|
81
|
+
white-space: nowrap;
|
|
82
|
+
overflow: hidden;
|
|
83
|
+
text-overflow: ellipsis;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.divider {
|
|
87
|
+
margin: var(--sv-space-1) 0 0;
|
|
88
|
+
border: none;
|
|
89
|
+
border-top: var(--sv-border-width-hairline) solid var(--sv-color-border);
|
|
90
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { UserMenu } from './UserMenu';
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: 'Components/UserMenu',
|
|
6
|
+
component: UserMenu,
|
|
7
|
+
parameters: {
|
|
8
|
+
layout: 'centered',
|
|
9
|
+
docs: {
|
|
10
|
+
description: {
|
|
11
|
+
component:
|
|
12
|
+
'The account avatar trigger + dropdown, generalized from the runtime shell\'s own AccountMenu — accent-filled avatar, name/email header block, then a plain item list. Presentational only: items carry their own onSelect and/or href. size="md" (36px, default) matches the platform sidebar\'s own avatar; size="sm" (32px) suits the more compact Header top-bar context instead.',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
args: {
|
|
17
|
+
name: 'Kasun Benthara',
|
|
18
|
+
email: 'kasun@openfs.io',
|
|
19
|
+
items: [
|
|
20
|
+
{ label: 'Account', icon: 'user', href: '/account' },
|
|
21
|
+
{ label: 'Preferences', icon: 'sliders-horizontal', href: '/account/preferences' },
|
|
22
|
+
{ label: 'Sign out', icon: 'log-out', destructive: true, onSelect: () => {} },
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
} satisfies Meta<typeof UserMenu>;
|
|
26
|
+
|
|
27
|
+
export default meta;
|
|
28
|
+
type Story = StoryObj<typeof meta>;
|
|
29
|
+
|
|
30
|
+
export const Default: Story = {};
|
|
31
|
+
|
|
32
|
+
export const WithAvatarImage: Story = {
|
|
33
|
+
args: {
|
|
34
|
+
avatarUrl: 'https://i.pravatar.cc/80',
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const NoUserInfo: Story = {
|
|
39
|
+
args: {
|
|
40
|
+
name: undefined,
|
|
41
|
+
email: undefined,
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const CompactSize: Story = {
|
|
46
|
+
args: {
|
|
47
|
+
size: 'sm',
|
|
48
|
+
},
|
|
49
|
+
};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import type { IconName } from '../Icon/Icon';
|
|
5
|
+
import { MenuEntries, type MenuItem } from '../Menu/Menu';
|
|
6
|
+
import { Popover } from '../Popover/Popover';
|
|
7
|
+
import styles from './UserMenu.module.css';
|
|
8
|
+
|
|
9
|
+
export interface UserMenuItem {
|
|
10
|
+
label: string;
|
|
11
|
+
icon: IconName;
|
|
12
|
+
/** Optional when `href` is provided (a pure navigation entry needs no
|
|
13
|
+
* extra callback). Required otherwise (e.g. "Sign out"). */
|
|
14
|
+
onSelect?: () => void;
|
|
15
|
+
/** Renders the entry as a link (`<a href>`) instead of a button — same
|
|
16
|
+
* href-vs-onClick convention as `MobileAppsDrawer`'s items. */
|
|
17
|
+
href?: string;
|
|
18
|
+
destructive?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface UserMenuProps {
|
|
22
|
+
name?: string;
|
|
23
|
+
email?: string;
|
|
24
|
+
avatarUrl?: string;
|
|
25
|
+
items: UserMenuItem[];
|
|
26
|
+
/** Trigger button's accessible label. Defaults to "Account". */
|
|
27
|
+
'aria-label'?: string;
|
|
28
|
+
align?: 'left' | 'right';
|
|
29
|
+
/** Trigger avatar size: `'md'` (36px) matches the platform sidebar's own
|
|
30
|
+
* `.avatar` exactly — the default, for that context. `'sm'` (32px) suits
|
|
31
|
+
* the more compact `Header` top-bar context instead. The panel's own
|
|
32
|
+
* user-info avatar (38px) is unaffected either way — only the trigger
|
|
33
|
+
* scales. */
|
|
34
|
+
size?: 'sm' | 'md';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function initials(value: string): string {
|
|
38
|
+
const trimmed = value.trim();
|
|
39
|
+
if (!trimmed) return '?';
|
|
40
|
+
const [first = '', second = ''] = trimmed.split(/\s+/);
|
|
41
|
+
return (second ? first.charAt(0) + second.charAt(0) : first.slice(0, 2)).toUpperCase();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* UserMenu — the account avatar trigger + dropdown, generalized from the
|
|
46
|
+
* runtime shell's own `AccountMenu.tsx` (the real, currently-shipped
|
|
47
|
+
* component that every `shell: minimal` plugin would otherwise have to
|
|
48
|
+
* hand-rebuild locally instead of importing). Same visual structure and
|
|
49
|
+
* copy: an accent-filled avatar trigger (36px, matching the sidebar's own
|
|
50
|
+
* `.avatar`), a name+email header block (38px avatar) above a divider, then
|
|
51
|
+
* a plain item list.
|
|
52
|
+
*
|
|
53
|
+
* Presentational only — no auth/session/routing logic of its own. The
|
|
54
|
+
* consumer supplies `items`, each with its own `onSelect` and/or `href`,
|
|
55
|
+
* matching `MobileHeader`'s "consumer wires it up" boundary; a real
|
|
56
|
+
* "Sign out" is just an item whose `onSelect` does whatever the consuming
|
|
57
|
+
* app's sign-out actually requires. Built on the shared `Popover` primitive
|
|
58
|
+
* (self-contained open state, matching the original's own architecture)
|
|
59
|
+
* rather than a hand-rolled outside-click/Escape listener — the original
|
|
60
|
+
* `AccountMenu` and every plugin-local rebuild of it independently re-derive
|
|
61
|
+
* that same logic; one primitive here means one implementation to get
|
|
62
|
+
* right. Always a small anchored dropdown, never a full-width sheet —
|
|
63
|
+
* this is deliberately not built on the adaptive `Menu` component (which
|
|
64
|
+
* becomes a bottom `Drawer` on mobile): the real account menu is a compact
|
|
65
|
+
* floating panel on every surface, mobile included.
|
|
66
|
+
*/
|
|
67
|
+
export function UserMenu({
|
|
68
|
+
name,
|
|
69
|
+
email,
|
|
70
|
+
avatarUrl,
|
|
71
|
+
items,
|
|
72
|
+
'aria-label': ariaLabel = 'Account',
|
|
73
|
+
align = 'right',
|
|
74
|
+
size = 'md',
|
|
75
|
+
}: UserMenuProps) {
|
|
76
|
+
const [open, setOpen] = useState(false);
|
|
77
|
+
const displayName = name || email || '';
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<Popover
|
|
81
|
+
align={align}
|
|
82
|
+
width={240}
|
|
83
|
+
aria-label={ariaLabel}
|
|
84
|
+
open={open}
|
|
85
|
+
onClose={() => setOpen(false)}
|
|
86
|
+
trigger={
|
|
87
|
+
<button
|
|
88
|
+
type="button"
|
|
89
|
+
aria-label={ariaLabel}
|
|
90
|
+
aria-haspopup="menu"
|
|
91
|
+
aria-expanded={open}
|
|
92
|
+
onClick={() => setOpen((v) => !v)}
|
|
93
|
+
className={[styles.trigger, size === 'sm' ? styles.triggerSm : '']
|
|
94
|
+
.filter(Boolean)
|
|
95
|
+
.join(' ')}
|
|
96
|
+
>
|
|
97
|
+
{avatarUrl ? (
|
|
98
|
+
<img src={avatarUrl} alt="" className={styles.triggerImg} />
|
|
99
|
+
) : (
|
|
100
|
+
initials(displayName)
|
|
101
|
+
)}
|
|
102
|
+
</button>
|
|
103
|
+
}
|
|
104
|
+
>
|
|
105
|
+
{displayName && (
|
|
106
|
+
<div className={styles.panelHeader}>
|
|
107
|
+
<div className={styles.userHeader}>
|
|
108
|
+
<span className={styles.menuAvatar} aria-hidden="true">
|
|
109
|
+
{avatarUrl ? (
|
|
110
|
+
<img src={avatarUrl} alt="" className={styles.menuAvatarImg} />
|
|
111
|
+
) : (
|
|
112
|
+
initials(displayName)
|
|
113
|
+
)}
|
|
114
|
+
</span>
|
|
115
|
+
<span className={styles.userInfo}>
|
|
116
|
+
{name && <span className={styles.userName}>{name}</span>}
|
|
117
|
+
{email && <span className={styles.userEmail}>{email}</span>}
|
|
118
|
+
</span>
|
|
119
|
+
</div>
|
|
120
|
+
<hr className={styles.divider} />
|
|
121
|
+
</div>
|
|
122
|
+
)}
|
|
123
|
+
<MenuEntries
|
|
124
|
+
items={items}
|
|
125
|
+
onSelect={(entry: MenuItem) => {
|
|
126
|
+
setOpen(false);
|
|
127
|
+
entry.onSelect?.();
|
|
128
|
+
}}
|
|
129
|
+
/>
|
|
130
|
+
</Popover>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
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 { UserMenu } from '../UserMenu';
|
|
5
|
+
|
|
6
|
+
afterEach(() => {
|
|
7
|
+
cleanup();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const items = [
|
|
11
|
+
{ label: 'Account', icon: 'user' as const, href: '/account' },
|
|
12
|
+
{ label: 'Preferences', icon: 'sliders-horizontal' as const, href: '/account/preferences' },
|
|
13
|
+
{ label: 'Sign out', icon: 'log-out' as const, destructive: true, onSelect: vi.fn() },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
describe('UserMenu', () => {
|
|
17
|
+
it('renders a closed trigger by default, showing initials from name', () => {
|
|
18
|
+
render(<UserMenu name="Jamie Doe" email="jamie@example.com" items={items} />);
|
|
19
|
+
expect(screen.getByRole('button', { name: 'Account' })).toBeTruthy();
|
|
20
|
+
expect(screen.getByText('JD')).toBeTruthy();
|
|
21
|
+
expect(screen.queryByRole('menuitem')).toBeNull();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('opens the panel on trigger click, showing name, email, and items', () => {
|
|
25
|
+
render(<UserMenu name="Jamie Doe" email="jamie@example.com" items={items} />);
|
|
26
|
+
fireEvent.click(screen.getByRole('button', { name: 'Account' }));
|
|
27
|
+
|
|
28
|
+
expect(screen.getByText('Jamie Doe')).toBeTruthy();
|
|
29
|
+
expect(screen.getByText('jamie@example.com')).toBeTruthy();
|
|
30
|
+
expect(screen.getByRole('menuitem', { name: /Account/ })).toBeTruthy();
|
|
31
|
+
expect(screen.getByRole('menuitem', { name: /Preferences/ })).toBeTruthy();
|
|
32
|
+
expect(screen.getByRole('menuitem', { name: /Sign out/ })).toBeTruthy();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('renders href items as links', () => {
|
|
36
|
+
render(<UserMenu name="Jamie Doe" items={items} />);
|
|
37
|
+
fireEvent.click(screen.getByRole('button', { name: 'Account' }));
|
|
38
|
+
const accountLink = screen.getByRole('menuitem', { name: /Account/ });
|
|
39
|
+
expect(accountLink.tagName).toBe('A');
|
|
40
|
+
expect(accountLink.getAttribute('href')).toBe('/account');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('closes and calls onSelect when a plain item is clicked', () => {
|
|
44
|
+
const onSelect = vi.fn();
|
|
45
|
+
render(
|
|
46
|
+
<UserMenu name="Jamie Doe" items={[{ label: 'Sign out', icon: 'log-out', onSelect }]} />,
|
|
47
|
+
);
|
|
48
|
+
fireEvent.click(screen.getByRole('button', { name: 'Account' }));
|
|
49
|
+
fireEvent.click(screen.getByRole('menuitem', { name: /Sign out/ }));
|
|
50
|
+
expect(onSelect).toHaveBeenCalledOnce();
|
|
51
|
+
expect(screen.queryByRole('menuitem')).toBeNull();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('renders no user header block when neither name nor email is provided', () => {
|
|
55
|
+
render(<UserMenu items={items} />);
|
|
56
|
+
fireEvent.click(screen.getByRole('button', { name: 'Account' }));
|
|
57
|
+
expect(screen.queryByText('Jamie Doe')).toBeNull();
|
|
58
|
+
expect(screen.getByRole('menuitem', { name: /Account/ })).toBeTruthy();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('falls back to email initials when name is omitted', () => {
|
|
62
|
+
render(<UserMenu email="jamie@example.com" items={items} />);
|
|
63
|
+
expect(screen.getByText('JA')).toBeTruthy();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('defaults to the md (36px sidebar) trigger size', () => {
|
|
67
|
+
render(<UserMenu name="Jamie Doe" items={items} />);
|
|
68
|
+
const trigger = screen.getByRole('button', { name: 'Account' });
|
|
69
|
+
expect(trigger.className).not.toMatch(/triggerSm/);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('applies the sm (32px, Header top-bar) trigger size when requested', () => {
|
|
73
|
+
render(<UserMenu name="Jamie Doe" items={items} size="sm" />);
|
|
74
|
+
const trigger = screen.getByRole('button', { name: 'Account' });
|
|
75
|
+
expect(trigger.className).toMatch(/triggerSm/);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { act, cleanup, render } from '@testing-library/react';
|
|
4
|
+
import { useRef } from 'react';
|
|
5
|
+
import { usePublishShellChromeHeight } from '../usePublishShellChromeHeight';
|
|
6
|
+
|
|
7
|
+
function Harness() {
|
|
8
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
9
|
+
usePublishShellChromeHeight(ref, '--sv-shell-footer-height');
|
|
10
|
+
return <div ref={ref} />;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function HeaderHarness() {
|
|
14
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
15
|
+
usePublishShellChromeHeight(ref, '--sv-shell-header-height');
|
|
16
|
+
return <div ref={ref} />;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function stubHeight(height: number) {
|
|
20
|
+
return vi.spyOn(Element.prototype, 'getBoundingClientRect').mockReturnValue({
|
|
21
|
+
height,
|
|
22
|
+
width: 0,
|
|
23
|
+
top: 0,
|
|
24
|
+
left: 0,
|
|
25
|
+
right: 0,
|
|
26
|
+
bottom: 0,
|
|
27
|
+
x: 0,
|
|
28
|
+
y: 0,
|
|
29
|
+
toJSON() {},
|
|
30
|
+
} as DOMRect);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('usePublishShellChromeHeight', () => {
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
cleanup();
|
|
36
|
+
document.getElementById('sv-app-shell')?.remove();
|
|
37
|
+
vi.restoreAllMocks();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('re-measures on window resize', () => {
|
|
41
|
+
const shell = document.createElement('div');
|
|
42
|
+
shell.id = 'sv-app-shell';
|
|
43
|
+
document.body.appendChild(shell);
|
|
44
|
+
const rectSpy = stubHeight(50);
|
|
45
|
+
|
|
46
|
+
render(<Harness />);
|
|
47
|
+
expect(shell.style.getPropertyValue('--sv-shell-footer-height')).toBe('50px');
|
|
48
|
+
|
|
49
|
+
rectSpy.mockReturnValue({
|
|
50
|
+
height: 80,
|
|
51
|
+
width: 0,
|
|
52
|
+
top: 0,
|
|
53
|
+
left: 0,
|
|
54
|
+
right: 0,
|
|
55
|
+
bottom: 0,
|
|
56
|
+
x: 0,
|
|
57
|
+
y: 0,
|
|
58
|
+
toJSON() {},
|
|
59
|
+
} as DOMRect);
|
|
60
|
+
act(() => window.dispatchEvent(new Event('resize')));
|
|
61
|
+
|
|
62
|
+
expect(shell.style.getPropertyValue('--sv-shell-footer-height')).toBe('80px');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('removes its resize listener on unmount', () => {
|
|
66
|
+
const shell = document.createElement('div');
|
|
67
|
+
shell.id = 'sv-app-shell';
|
|
68
|
+
document.body.appendChild(shell);
|
|
69
|
+
stubHeight(50);
|
|
70
|
+
const removeSpy = vi.spyOn(window, 'removeEventListener');
|
|
71
|
+
|
|
72
|
+
const { unmount } = render(<Harness />);
|
|
73
|
+
unmount();
|
|
74
|
+
|
|
75
|
+
expect(removeSpy).toHaveBeenCalledWith('resize', expect.any(Function));
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('is a no-op when #sv-app-shell does not exist', () => {
|
|
79
|
+
stubHeight(50);
|
|
80
|
+
expect(() => render(<Harness />)).not.toThrow();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('also mirrors --sv-shell-header-height onto --sv-dialog-inset-top on :root', () => {
|
|
84
|
+
const shell = document.createElement('div');
|
|
85
|
+
shell.id = 'sv-app-shell';
|
|
86
|
+
document.body.appendChild(shell);
|
|
87
|
+
stubHeight(69);
|
|
88
|
+
|
|
89
|
+
render(<HeaderHarness />);
|
|
90
|
+
|
|
91
|
+
expect(shell.style.getPropertyValue('--sv-shell-header-height')).toBe('69px');
|
|
92
|
+
expect(document.documentElement.style.getPropertyValue('--sv-dialog-inset-top')).toBe('69px');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('removes --sv-dialog-inset-top from :root on unmount', () => {
|
|
96
|
+
const shell = document.createElement('div');
|
|
97
|
+
shell.id = 'sv-app-shell';
|
|
98
|
+
document.body.appendChild(shell);
|
|
99
|
+
stubHeight(69);
|
|
100
|
+
|
|
101
|
+
const { unmount } = render(<HeaderHarness />);
|
|
102
|
+
expect(document.documentElement.style.getPropertyValue('--sv-dialog-inset-top')).toBe('69px');
|
|
103
|
+
|
|
104
|
+
unmount();
|
|
105
|
+
expect(document.documentElement.style.getPropertyValue('--sv-dialog-inset-top')).toBe('');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('never touches --sv-dialog-inset-top for the footer variable', () => {
|
|
109
|
+
const shell = document.createElement('div');
|
|
110
|
+
shell.id = 'sv-app-shell';
|
|
111
|
+
document.body.appendChild(shell);
|
|
112
|
+
stubHeight(50);
|
|
113
|
+
|
|
114
|
+
render(<Harness />);
|
|
115
|
+
|
|
116
|
+
expect(document.documentElement.style.getPropertyValue('--sv-dialog-inset-top')).toBe('');
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { shouldHandleDndEvent } from '../useReorderSensors';
|
|
4
|
+
|
|
5
|
+
describe('shouldHandleDndEvent', () => {
|
|
6
|
+
it('refuses an element nested inside a data-no-dnd ancestor', () => {
|
|
7
|
+
const wrapper = document.createElement('div');
|
|
8
|
+
wrapper.setAttribute('data-no-dnd', '');
|
|
9
|
+
const child = document.createElement('button');
|
|
10
|
+
wrapper.appendChild(child);
|
|
11
|
+
document.body.appendChild(wrapper);
|
|
12
|
+
|
|
13
|
+
expect(shouldHandleDndEvent(child)).toBe(false);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('allows an element with no data-no-dnd ancestor', () => {
|
|
17
|
+
const el = document.createElement('div');
|
|
18
|
+
document.body.appendChild(el);
|
|
19
|
+
|
|
20
|
+
expect(shouldHandleDndEvent(el)).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('allows a non-Element target', () => {
|
|
24
|
+
expect(shouldHandleDndEvent(null)).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
});
|