@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,132 @@
|
|
|
1
|
+
.row {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-wrap: wrap;
|
|
4
|
+
gap: var(--sv-space-2);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.swatch {
|
|
8
|
+
width: var(--sv-space-8);
|
|
9
|
+
height: var(--sv-space-8);
|
|
10
|
+
border-radius: var(--sv-radius-md);
|
|
11
|
+
border: 2px solid transparent;
|
|
12
|
+
padding: 0;
|
|
13
|
+
cursor: pointer;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.swatch:disabled {
|
|
17
|
+
opacity: 0.5;
|
|
18
|
+
cursor: not-allowed;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.swatchSelected {
|
|
22
|
+
border-color: var(--sv-color-accent);
|
|
23
|
+
outline: 2px solid var(--sv-color-focus-ring);
|
|
24
|
+
outline-offset: 1px;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/* Dashed/outlined rather than filled — there's no real color value to paint
|
|
28
|
+
for "no color". */
|
|
29
|
+
.swatchNone {
|
|
30
|
+
display: flex;
|
|
31
|
+
align-items: center;
|
|
32
|
+
justify-content: center;
|
|
33
|
+
background-color: var(--sv-color-surface-sunken);
|
|
34
|
+
border: 2px dashed var(--sv-color-border-strong);
|
|
35
|
+
color: var(--sv-color-text-muted);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.swatchNone.swatchSelected {
|
|
39
|
+
border-style: solid;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* Wraps the native color input so a decorative "+" glyph can sit on top of
|
|
43
|
+
it while it's empty (a native `<input type="color">` can't have DOM
|
|
44
|
+
children of its own) and so the same `.swatchSelected` ring used above can
|
|
45
|
+
be applied to the wrapper without fighting the input's own default
|
|
46
|
+
focus/border rendering. */
|
|
47
|
+
.customWrapper {
|
|
48
|
+
position: relative;
|
|
49
|
+
display: inline-flex;
|
|
50
|
+
align-items: center;
|
|
51
|
+
justify-content: center;
|
|
52
|
+
width: var(--sv-space-8);
|
|
53
|
+
height: var(--sv-space-8);
|
|
54
|
+
border-radius: var(--sv-radius-md);
|
|
55
|
+
border: 2px solid transparent;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.customWrapper.swatchSelected {
|
|
59
|
+
/* Same visual treatment as .swatchSelected, but scoped to the wrapper —
|
|
60
|
+
the input itself is styled borderless below, so this is the only
|
|
61
|
+
border/outline actually rendered for the custom trigger. */
|
|
62
|
+
border-color: var(--sv-color-accent);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* Before a custom color is actually chosen — same dashed/outlined treatment
|
|
66
|
+
as .swatchNone, since there's equally no real color value to paint yet. */
|
|
67
|
+
.customWrapperEmpty {
|
|
68
|
+
background-color: var(--sv-color-surface-sunken);
|
|
69
|
+
border-style: dashed;
|
|
70
|
+
border-color: var(--sv-color-border-strong);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* Reset the native control to a plain color swatch matching the curated
|
|
74
|
+
ones above — browsers default `<input type="color">` to its own chrome
|
|
75
|
+
(a bordered box with visible padding around the swatch). */
|
|
76
|
+
.customInput {
|
|
77
|
+
width: 100%;
|
|
78
|
+
height: 100%;
|
|
79
|
+
padding: 0;
|
|
80
|
+
border: none;
|
|
81
|
+
border-radius: var(--sv-radius-md);
|
|
82
|
+
cursor: pointer;
|
|
83
|
+
background: none;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* Invisible but still clickable/focusable while empty — the input always
|
|
87
|
+
holds *some* real hex internally (no true empty state), but that value
|
|
88
|
+
was never actually chosen by the user, so it must never render as this
|
|
89
|
+
trigger's own swatch color. Becomes visible again the moment a real
|
|
90
|
+
choice makes it into `value` (`.customWrapperEmpty` stops applying). */
|
|
91
|
+
.customWrapperEmpty .customInput {
|
|
92
|
+
opacity: 0;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.customInput:disabled {
|
|
96
|
+
opacity: 0.5;
|
|
97
|
+
cursor: not-allowed;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.customWrapperEmpty .customInput:disabled {
|
|
101
|
+
opacity: 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.customInput::-webkit-color-swatch-wrapper {
|
|
105
|
+
padding: 0;
|
|
106
|
+
border-radius: var(--sv-radius-md);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.customInput::-webkit-color-swatch {
|
|
110
|
+
border: none;
|
|
111
|
+
border-radius: var(--sv-radius-md);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.customInput::-moz-color-swatch {
|
|
115
|
+
border: none;
|
|
116
|
+
border-radius: var(--sv-radius-md);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* `inset: 0` alone doesn't center this — the icon already has an explicit
|
|
120
|
+
width/height (its own size class), so an absolutely positioned box with
|
|
121
|
+
all four insets pinned to 0 resolves against top/left rather than
|
|
122
|
+
stretching to let flex centering do anything, landing the glyph in the
|
|
123
|
+
corner instead of the middle. `top/left: 50%` + `translate(-50%, -50%)`
|
|
124
|
+
centers a fixed-size element regardless of its own dimensions. */
|
|
125
|
+
.customPlusIcon {
|
|
126
|
+
position: absolute;
|
|
127
|
+
top: 50%;
|
|
128
|
+
left: 50%;
|
|
129
|
+
transform: translate(-50%, -50%);
|
|
130
|
+
color: var(--sv-color-text-muted);
|
|
131
|
+
pointer-events: none;
|
|
132
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
3
|
+
import { ColorPicker } from './ColorPicker';
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: 'Components/ColorPicker',
|
|
7
|
+
component: ColorPicker,
|
|
8
|
+
parameters: {
|
|
9
|
+
layout: 'padded',
|
|
10
|
+
docs: {
|
|
11
|
+
description: {
|
|
12
|
+
component:
|
|
13
|
+
'A row of curated swatch suggestions plus a native `<input type="color">` trigger for picking any color, not just the curated set — every browser\'s own full-spectrum picker, guaranteed valid hex output, no bespoke a11y work.',
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
args: {
|
|
18
|
+
swatches: [],
|
|
19
|
+
value: null,
|
|
20
|
+
onChange: () => {},
|
|
21
|
+
onSelectionComplete: () => {},
|
|
22
|
+
'aria-label': 'Color',
|
|
23
|
+
},
|
|
24
|
+
} satisfies Meta<typeof ColorPicker>;
|
|
25
|
+
|
|
26
|
+
export default meta;
|
|
27
|
+
type Story = StoryObj<typeof meta>;
|
|
28
|
+
|
|
29
|
+
const SWATCHES = [
|
|
30
|
+
{ label: 'Sky', value: '#b5c9e8' },
|
|
31
|
+
{ label: 'Sage', value: '#c9e0c4' },
|
|
32
|
+
{ label: 'Sand', value: '#e8d3b5' },
|
|
33
|
+
{ label: 'Clay', value: '#e0c4c4' },
|
|
34
|
+
{ label: 'Lilac', value: '#d8c4e0' },
|
|
35
|
+
{ label: 'Ink', value: '#3b5166' },
|
|
36
|
+
{ label: 'Forest', value: '#3f5c4c' },
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
function ControlledColorPicker({
|
|
40
|
+
initial = null,
|
|
41
|
+
allowNone = false,
|
|
42
|
+
}: {
|
|
43
|
+
initial?: string | null;
|
|
44
|
+
allowNone?: boolean;
|
|
45
|
+
}) {
|
|
46
|
+
const [value, setValue] = useState<string | null>(initial);
|
|
47
|
+
return (
|
|
48
|
+
<ColorPicker
|
|
49
|
+
swatches={SWATCHES}
|
|
50
|
+
value={value}
|
|
51
|
+
onChange={setValue}
|
|
52
|
+
allowNone={allowNone}
|
|
53
|
+
aria-label="Board color"
|
|
54
|
+
/>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const Default: Story = {
|
|
59
|
+
render: () => <ControlledColorPicker initial="#c9e0c4" />,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const WithNoColorOption: Story = {
|
|
63
|
+
render: () => <ControlledColorPicker allowNone />,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const CustomColorSelected: Story = {
|
|
67
|
+
render: () => <ControlledColorPicker initial="#7a3fd6" />,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const Disabled: Story = {
|
|
71
|
+
render: () => (
|
|
72
|
+
<ColorPicker
|
|
73
|
+
swatches={SWATCHES}
|
|
74
|
+
value="#b5c9e8"
|
|
75
|
+
onChange={() => {}}
|
|
76
|
+
aria-label="Board color"
|
|
77
|
+
disabled
|
|
78
|
+
/>
|
|
79
|
+
),
|
|
80
|
+
};
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Icon } from '../Icon/Icon';
|
|
4
|
+
import styles from './ColorPicker.module.css';
|
|
5
|
+
|
|
6
|
+
export interface ColorPickerSwatch {
|
|
7
|
+
/** Accessible name and tooltip, e.g. "Sky". */
|
|
8
|
+
label: string;
|
|
9
|
+
/** 6-digit hex, e.g. "#b5c9e8". */
|
|
10
|
+
value: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ColorPickerProps {
|
|
14
|
+
/** Curated quick-pick suggestions, in display order. */
|
|
15
|
+
swatches: readonly ColorPickerSwatch[];
|
|
16
|
+
/** Current hex value, or `null` for no color. Compared case-insensitively
|
|
17
|
+
* against `swatches` — a value that doesn't match any swatch renders as
|
|
18
|
+
* the active "custom" selection instead. */
|
|
19
|
+
value: string | null;
|
|
20
|
+
onChange: (value: string | null) => void;
|
|
21
|
+
/**
|
|
22
|
+
* Fires once after a discrete, complete pick — a swatch click or the "no
|
|
23
|
+
* color" click. Optional; most callers don't need it. Deliberately NOT
|
|
24
|
+
* fired from the native custom-color `<input type="color">`'s `onChange`
|
|
25
|
+
* — that can fire repeatedly while the user is still dragging inside the
|
|
26
|
+
* browser's own color dialog, so a caller using this to auto-close a
|
|
27
|
+
* wrapping `Popover` would otherwise slam it shut mid-adjustment.
|
|
28
|
+
*/
|
|
29
|
+
onSelectionComplete?: () => void;
|
|
30
|
+
/** Shows a leading "no color" option. Off by default — most consumers
|
|
31
|
+
* (anything painting a fixed-size chip) always need a real color; only
|
|
32
|
+
* a board canvas-style consumer typically wants "none" as a real choice. */
|
|
33
|
+
allowNone?: boolean;
|
|
34
|
+
noneLabel?: string;
|
|
35
|
+
/** Accessible name for the native color-input trigger that lets a user
|
|
36
|
+
* pick any color, not just a curated swatch. */
|
|
37
|
+
customLabel?: string;
|
|
38
|
+
disabled?: boolean;
|
|
39
|
+
'aria-label': string;
|
|
40
|
+
className?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalize(hex: string): string {
|
|
44
|
+
return hex.toLowerCase();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* ColorPicker — a row of curated swatch suggestions plus a native
|
|
49
|
+
* `<input type="color">` trigger for picking any color, not just the
|
|
50
|
+
* curated set. The native color input is deliberately not a custom-built
|
|
51
|
+
* hue/saturation picker: it already gives every browser's own full-spectrum
|
|
52
|
+
* picker (with an eyedropper tool in most), guarantees valid hex output, and
|
|
53
|
+
* needs no bespoke a11y work of its own.
|
|
54
|
+
*
|
|
55
|
+
* `value` drives which affordance reads as selected: a hex matching a
|
|
56
|
+
* swatch highlights that swatch; a hex matching none of them highlights the
|
|
57
|
+
* custom trigger instead (its own dial shows that color); `null` (only
|
|
58
|
+
* meaningful with `allowNone`) highlights the "no color" option.
|
|
59
|
+
*
|
|
60
|
+
* The custom trigger shows a plain "+" on a neutral dashed box until a
|
|
61
|
+
* genuinely custom color is active — it never previews an arbitrary dial
|
|
62
|
+
* color the user hasn't actually chosen. The underlying native color input
|
|
63
|
+
* still needs *some* real hex to hold internally at all times (it has no
|
|
64
|
+
* true empty state), so that starting value stays invisible (`opacity: 0`
|
|
65
|
+
* on the input itself) rather than painted as this trigger's own swatch.
|
|
66
|
+
*/
|
|
67
|
+
export function ColorPicker({
|
|
68
|
+
swatches,
|
|
69
|
+
value,
|
|
70
|
+
onChange,
|
|
71
|
+
onSelectionComplete,
|
|
72
|
+
allowNone = false,
|
|
73
|
+
noneLabel = 'No color',
|
|
74
|
+
customLabel = 'Custom color',
|
|
75
|
+
disabled = false,
|
|
76
|
+
'aria-label': ariaLabel,
|
|
77
|
+
className,
|
|
78
|
+
}: ColorPickerProps) {
|
|
79
|
+
const normalizedValue = value ? normalize(value) : null;
|
|
80
|
+
const matchedSwatch = swatches.find((s) => normalize(s.value) === normalizedValue);
|
|
81
|
+
const isCustomActive = normalizedValue !== null && !matchedSwatch;
|
|
82
|
+
// The native input always needs a real hex to display (it has no "empty"
|
|
83
|
+
// state) — the active custom color when there is one, otherwise a neutral
|
|
84
|
+
// starting point for whenever it's next opened.
|
|
85
|
+
const customDialValue = isCustomActive && value ? value : (swatches[0]?.value ?? '#000000');
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<div
|
|
89
|
+
className={[styles.row, className].filter(Boolean).join(' ')}
|
|
90
|
+
role="group"
|
|
91
|
+
aria-label={ariaLabel}
|
|
92
|
+
>
|
|
93
|
+
{allowNone && (
|
|
94
|
+
<button
|
|
95
|
+
type="button"
|
|
96
|
+
role="radio"
|
|
97
|
+
aria-checked={value === null}
|
|
98
|
+
aria-label={noneLabel}
|
|
99
|
+
title={noneLabel}
|
|
100
|
+
disabled={disabled}
|
|
101
|
+
className={[styles.swatch, styles.swatchNone, value === null ? styles.swatchSelected : '']
|
|
102
|
+
.filter(Boolean)
|
|
103
|
+
.join(' ')}
|
|
104
|
+
onClick={() => {
|
|
105
|
+
onChange(null);
|
|
106
|
+
onSelectionComplete?.();
|
|
107
|
+
}}
|
|
108
|
+
>
|
|
109
|
+
<Icon name="x" size="sm" aria-hidden={true} />
|
|
110
|
+
</button>
|
|
111
|
+
)}
|
|
112
|
+
{swatches.map((s) => (
|
|
113
|
+
<button
|
|
114
|
+
key={s.value}
|
|
115
|
+
type="button"
|
|
116
|
+
role="radio"
|
|
117
|
+
aria-checked={normalize(s.value) === normalizedValue}
|
|
118
|
+
aria-label={s.label}
|
|
119
|
+
title={s.label}
|
|
120
|
+
disabled={disabled}
|
|
121
|
+
className={[
|
|
122
|
+
styles.swatch,
|
|
123
|
+
normalize(s.value) === normalizedValue ? styles.swatchSelected : '',
|
|
124
|
+
]
|
|
125
|
+
.filter(Boolean)
|
|
126
|
+
.join(' ')}
|
|
127
|
+
style={{ backgroundColor: s.value }}
|
|
128
|
+
onClick={() => {
|
|
129
|
+
onChange(s.value);
|
|
130
|
+
onSelectionComplete?.();
|
|
131
|
+
}}
|
|
132
|
+
/>
|
|
133
|
+
))}
|
|
134
|
+
<span
|
|
135
|
+
className={[
|
|
136
|
+
styles.customWrapper,
|
|
137
|
+
isCustomActive ? styles.swatchSelected : styles.customWrapperEmpty,
|
|
138
|
+
]
|
|
139
|
+
.filter(Boolean)
|
|
140
|
+
.join(' ')}
|
|
141
|
+
>
|
|
142
|
+
<input
|
|
143
|
+
type="color"
|
|
144
|
+
className={styles.customInput}
|
|
145
|
+
aria-label={customLabel}
|
|
146
|
+
title={customLabel}
|
|
147
|
+
disabled={disabled}
|
|
148
|
+
value={customDialValue}
|
|
149
|
+
onChange={(e) => onChange(e.target.value)}
|
|
150
|
+
/>
|
|
151
|
+
{!isCustomActive && (
|
|
152
|
+
<Icon name="plus" size="sm" aria-hidden={true} className={styles.customPlusIcon} />
|
|
153
|
+
)}
|
|
154
|
+
</span>
|
|
155
|
+
</div>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
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 { ColorPicker } from '../ColorPicker';
|
|
5
|
+
|
|
6
|
+
afterEach(cleanup);
|
|
7
|
+
|
|
8
|
+
const SWATCHES = [
|
|
9
|
+
{ label: 'Sky', value: '#b5c9e8' },
|
|
10
|
+
{ label: 'Sage', value: '#c9e0c4' },
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
describe('ColorPicker', () => {
|
|
14
|
+
it('renders a swatch button per entry', () => {
|
|
15
|
+
render(<ColorPicker swatches={SWATCHES} value={null} onChange={() => {}} aria-label="Color" />);
|
|
16
|
+
expect(screen.getByRole('radio', { name: 'Sky' })).toBeTruthy();
|
|
17
|
+
expect(screen.getByRole('radio', { name: 'Sage' })).toBeTruthy();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('marks the swatch matching value as checked', () => {
|
|
21
|
+
render(
|
|
22
|
+
<ColorPicker swatches={SWATCHES} value="#c9e0c4" onChange={() => {}} aria-label="Color" />,
|
|
23
|
+
);
|
|
24
|
+
expect(screen.getByRole('radio', { name: 'Sage' }).getAttribute('aria-checked')).toBe('true');
|
|
25
|
+
expect(screen.getByRole('radio', { name: 'Sky' }).getAttribute('aria-checked')).toBe('false');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('matches swatches case-insensitively', () => {
|
|
29
|
+
render(
|
|
30
|
+
<ColorPicker swatches={SWATCHES} value="#C9E0C4" onChange={() => {}} aria-label="Color" />,
|
|
31
|
+
);
|
|
32
|
+
expect(screen.getByRole('radio', { name: 'Sage' }).getAttribute('aria-checked')).toBe('true');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('calls onChange with the swatch value when clicked', () => {
|
|
36
|
+
const onChange = vi.fn();
|
|
37
|
+
render(<ColorPicker swatches={SWATCHES} value={null} onChange={onChange} aria-label="Color" />);
|
|
38
|
+
fireEvent.click(screen.getByRole('radio', { name: 'Sky' }));
|
|
39
|
+
expect(onChange).toHaveBeenCalledWith('#b5c9e8');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('does not render a "no color" option by default', () => {
|
|
43
|
+
render(<ColorPicker swatches={SWATCHES} value={null} onChange={() => {}} aria-label="Color" />);
|
|
44
|
+
expect(screen.queryByRole('radio', { name: 'No color' })).toBeNull();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('renders and wires up the "no color" option when allowNone is set', () => {
|
|
48
|
+
const onChange = vi.fn();
|
|
49
|
+
render(
|
|
50
|
+
<ColorPicker
|
|
51
|
+
swatches={SWATCHES}
|
|
52
|
+
value="#b5c9e8"
|
|
53
|
+
onChange={onChange}
|
|
54
|
+
allowNone
|
|
55
|
+
aria-label="Color"
|
|
56
|
+
/>,
|
|
57
|
+
);
|
|
58
|
+
const noneOption = screen.getByRole('radio', { name: 'No color' });
|
|
59
|
+
expect(noneOption.getAttribute('aria-checked')).toBe('false');
|
|
60
|
+
fireEvent.click(noneOption);
|
|
61
|
+
expect(onChange).toHaveBeenCalledWith(null);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('marks "no color" as checked when value is null', () => {
|
|
65
|
+
render(
|
|
66
|
+
<ColorPicker
|
|
67
|
+
swatches={SWATCHES}
|
|
68
|
+
value={null}
|
|
69
|
+
onChange={() => {}}
|
|
70
|
+
allowNone
|
|
71
|
+
aria-label="Color"
|
|
72
|
+
/>,
|
|
73
|
+
);
|
|
74
|
+
expect(screen.getByRole('radio', { name: 'No color' }).getAttribute('aria-checked')).toBe(
|
|
75
|
+
'true',
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('renders the native color input trigger for custom colors', () => {
|
|
80
|
+
render(<ColorPicker swatches={SWATCHES} value={null} onChange={() => {}} aria-label="Color" />);
|
|
81
|
+
const custom = screen.getByLabelText('Custom color') as HTMLInputElement;
|
|
82
|
+
expect(custom.type).toBe('color');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('shows the custom value on the native input when it matches no swatch', () => {
|
|
86
|
+
render(
|
|
87
|
+
<ColorPicker swatches={SWATCHES} value="#7a3fd6" onChange={() => {}} aria-label="Color" />,
|
|
88
|
+
);
|
|
89
|
+
const custom = screen.getByLabelText('Custom color') as HTMLInputElement;
|
|
90
|
+
expect(custom.value).toBe('#7a3fd6');
|
|
91
|
+
// Neither curated swatch should read as checked once a custom color is active.
|
|
92
|
+
expect(screen.getByRole('radio', { name: 'Sky' }).getAttribute('aria-checked')).toBe('false');
|
|
93
|
+
expect(screen.getByRole('radio', { name: 'Sage' }).getAttribute('aria-checked')).toBe('false');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('calls onChange with the picked value when the custom input changes', () => {
|
|
97
|
+
const onChange = vi.fn();
|
|
98
|
+
render(<ColorPicker swatches={SWATCHES} value={null} onChange={onChange} aria-label="Color" />);
|
|
99
|
+
const custom = screen.getByLabelText('Custom color') as HTMLInputElement;
|
|
100
|
+
fireEvent.change(custom, { target: { value: '#123456' } });
|
|
101
|
+
expect(onChange).toHaveBeenCalledWith('#123456');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('disables every swatch and the custom input when disabled', () => {
|
|
105
|
+
render(
|
|
106
|
+
<ColorPicker
|
|
107
|
+
swatches={SWATCHES}
|
|
108
|
+
value={null}
|
|
109
|
+
onChange={() => {}}
|
|
110
|
+
disabled
|
|
111
|
+
aria-label="Color"
|
|
112
|
+
/>,
|
|
113
|
+
);
|
|
114
|
+
expect((screen.getByRole('radio', { name: 'Sky' }) as HTMLButtonElement).disabled).toBe(true);
|
|
115
|
+
expect((screen.getByLabelText('Custom color') as HTMLInputElement).disabled).toBe(true);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
line-height: 1.5;
|
|
16
16
|
color: var(--sv-color-text-primary);
|
|
17
17
|
background-color: var(--sv-color-surface);
|
|
18
|
-
border:
|
|
18
|
+
border: var(--sv-border-width-hairline) solid var(--sv-color-border-strong);
|
|
19
19
|
border-radius: var(--sv-radius-md);
|
|
20
20
|
cursor: pointer;
|
|
21
21
|
transition:
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
flex-shrink: 0;
|
|
62
62
|
width: 100%;
|
|
63
63
|
border: none;
|
|
64
|
-
border-bottom:
|
|
64
|
+
border-bottom: var(--sv-border-width-hairline) solid var(--sv-color-border);
|
|
65
65
|
background: transparent;
|
|
66
66
|
color: var(--sv-color-text-primary);
|
|
67
67
|
font: inherit;
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
flex-shrink: 0;
|
|
14
14
|
width: 100%;
|
|
15
15
|
border: none;
|
|
16
|
-
border-bottom:
|
|
16
|
+
border-bottom: var(--sv-border-width-hairline) solid var(--sv-color-border);
|
|
17
17
|
background: transparent;
|
|
18
18
|
color: var(--sv-color-text-primary);
|
|
19
19
|
font: inherit;
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
font-size: var(--sv-font-size-xs);
|
|
42
42
|
color: var(--sv-color-error-text);
|
|
43
43
|
padding: var(--sv-space-2) var(--sv-space-3);
|
|
44
|
-
border:
|
|
44
|
+
border: var(--sv-border-width-hairline) solid var(--sv-color-error-border);
|
|
45
45
|
border-radius: var(--sv-radius-sm);
|
|
46
46
|
background-color: var(--sv-color-error-surface);
|
|
47
47
|
}
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
Button). Matches Button's md padding, updated alongside it (10px,
|
|
71
71
|
36px rendered height). */
|
|
72
72
|
padding: 10px var(--sv-space-4);
|
|
73
|
-
border:
|
|
73
|
+
border: var(--sv-border-width-hairline) solid transparent;
|
|
74
74
|
border-radius: var(--sv-radius-md);
|
|
75
75
|
font-family: var(--sv-font-family);
|
|
76
76
|
font-size: var(--sv-font-size-sm);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { type ReactNode, useEffect, useRef } from 'react';
|
|
4
|
+
import { registerOpenOverlay, unregisterOpenOverlay } from '../../overlay-shell';
|
|
4
5
|
import { Button } from '../Button/Button';
|
|
5
6
|
import styles from './ConfirmDialog.module.css';
|
|
6
7
|
|
|
@@ -59,6 +60,19 @@ export function ConfirmDialog({
|
|
|
59
60
|
}: ConfirmDialogProps) {
|
|
60
61
|
const dialogRef = useRef<HTMLDialogElement>(null);
|
|
61
62
|
|
|
63
|
+
// Registers into the same open-overlay stack Dialog/Drawer/Sheet use
|
|
64
|
+
// (overlay-shell.ts) purely so a Dialog this ConfirmDialog was opened
|
|
65
|
+
// from inside of knows it is no longer the topmost overlay, and defers
|
|
66
|
+
// Escape to this one instead of closing itself. ConfirmDialog does not
|
|
67
|
+
// otherwise adopt useOverlayFocusCapture/useOverlayScrollLock/
|
|
68
|
+
// useOverlayKeyboardTrap — see this component's own doc comment for why
|
|
69
|
+
// it stays on the native <dialog> element for everything else.
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (!open) return;
|
|
72
|
+
const id = registerOpenOverlay();
|
|
73
|
+
return () => unregisterOpenOverlay(id);
|
|
74
|
+
}, [open]);
|
|
75
|
+
|
|
62
76
|
useEffect(() => {
|
|
63
77
|
const el = dialogRef.current;
|
|
64
78
|
if (!el) return;
|
|
@@ -70,6 +84,13 @@ export function ConfirmDialog({
|
|
|
70
84
|
// listener needed for that) and whenever .close() runs — including the
|
|
71
85
|
// effect above reacting to `open` going false. Listening for it here is
|
|
72
86
|
// what makes Esc call the caller's onClose without a manual keydown handler.
|
|
87
|
+
// It is also the SINGLE call site for onClose: Cancel and backdrop-click
|
|
88
|
+
// below call dialogRef.current?.close() rather than onClose directly, so
|
|
89
|
+
// every dismissal path funnels through this one 'close' listener. Calling
|
|
90
|
+
// onClose directly from those handlers used to double-fire it — the
|
|
91
|
+
// consumer's onClose sets `open` to false as documented, which re-runs the
|
|
92
|
+
// effect above, which calls el.close() again, firing this same 'close'
|
|
93
|
+
// listener a second time for what the user experienced as one click.
|
|
73
94
|
useEffect(() => {
|
|
74
95
|
const el = dialogRef.current;
|
|
75
96
|
if (!el) return;
|
|
@@ -85,7 +106,7 @@ export function ConfirmDialog({
|
|
|
85
106
|
className={styles.dialog}
|
|
86
107
|
aria-label={title}
|
|
87
108
|
onClick={(e) => {
|
|
88
|
-
if (e.target === e.currentTarget)
|
|
109
|
+
if (e.target === e.currentTarget) dialogRef.current?.close();
|
|
89
110
|
}}
|
|
90
111
|
>
|
|
91
112
|
<div className={styles.body}>
|
|
@@ -93,7 +114,12 @@ export function ConfirmDialog({
|
|
|
93
114
|
<div className={styles.message}>{message}</div>
|
|
94
115
|
{error && <p className={styles.error}>{error}</p>}
|
|
95
116
|
<div className={styles.actions}>
|
|
96
|
-
<Button
|
|
117
|
+
<Button
|
|
118
|
+
type="button"
|
|
119
|
+
variant="secondary"
|
|
120
|
+
onClick={() => dialogRef.current?.close()}
|
|
121
|
+
disabled={pending}
|
|
122
|
+
>
|
|
97
123
|
{cancelLabel}
|
|
98
124
|
</Button>
|
|
99
125
|
{destructive ? (
|