@stevederico/skateboard-ui 4.10.0 → 4.12.0
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/CHANGELOG.md +20 -0
- package/dist/App.js +1 -3
- package/dist/components/core/Calendar.js +26 -2
- package/dist/components/core/Command.d.ts +8 -16
- package/dist/components/core/Command.js +25 -19
- package/dist/components/core/Context.js +3 -1
- package/dist/components/core/Utilities.js +13 -4
- package/dist/components/layout/TabBar.js +2 -1
- package/dist/components/views/LandingView.js +1 -1
- package/dist/components/views/NotFound.d.ts +3 -4
- package/dist/components/views/SettingsView.js +8 -3
- package/dist/components/views/SignInView.js +7 -1
- package/dist/components/views/SignUpView.js +8 -1
- package/dist/ui/accordion.js +32 -10
- package/dist/ui/alert-dialog.js +7 -4
- package/dist/ui/avatar.js +14 -1
- package/dist/ui/context-menu.js +62 -9
- package/dist/ui/dialog.js +7 -5
- package/dist/ui/drawer.js +7 -4
- package/dist/ui/dropdown-menu.d.ts +2 -2
- package/dist/ui/dropdown-menu.js +42 -3
- package/dist/ui/field.d.ts +11 -10
- package/dist/ui/field.js +2 -2
- package/dist/ui/hover-card.js +1 -1
- package/dist/ui/input-group.js +3 -1
- package/dist/ui/layer-stack.d.ts +2 -0
- package/dist/ui/layer-stack.js +16 -0
- package/dist/ui/menubar.js +51 -8
- package/dist/ui/popover.js +34 -0
- package/dist/ui/radio-group.js +12 -1
- package/dist/ui/select.js +4 -5
- package/dist/ui/sheet.js +7 -4
- package/dist/ui/slider.js +51 -11
- package/dist/ui/slot.js +5 -3
- package/dist/ui/tabs.js +15 -8
- package/dist/ui/use-dismiss.js +3 -0
- package/package.json +1 -1
package/dist/ui/context-menu.js
CHANGED
|
@@ -8,6 +8,7 @@ import { useFloating } from "./use-floating.js";
|
|
|
8
8
|
import { usePresence } from "./use-presence.js";
|
|
9
9
|
import { useTypeahead } from "./use-typeahead.js";
|
|
10
10
|
import { usePointerMoved } from "./use-pointer-moved.js";
|
|
11
|
+
import { registerLayer } from "./layer-stack.js";
|
|
11
12
|
import { ChevronRightIcon, CheckIcon } from "../icons/index.js";
|
|
12
13
|
const MenuContext = React.createContext(null);
|
|
13
14
|
function useMenu() {
|
|
@@ -116,12 +117,36 @@ function ContextMenuContent({ className, children, onKeyDown, ...props }) {
|
|
|
116
117
|
};
|
|
117
118
|
}, [mounted, point]);
|
|
118
119
|
React.useEffect(() => {
|
|
119
|
-
|
|
120
|
-
if (!node)
|
|
120
|
+
if (!mounted)
|
|
121
121
|
return;
|
|
122
|
-
|
|
122
|
+
// The popup is portaled and attaches a frame late (Portal defers its child to
|
|
123
|
+
// its own effect), so containerRef.current is null when this effect first runs
|
|
124
|
+
// and the deps don't change once it attaches. Retry each frame until the node
|
|
125
|
+
// exists, then register it. Without this the dismiss handler's `layers` set
|
|
126
|
+
// stays empty, so a pointerdown INSIDE the menu fails the contains() check and
|
|
127
|
+
// is treated as an outside click — closing the menu before the item's onClick
|
|
128
|
+
// fires (real mouse only; a synthetic click with no pointerdown masks it).
|
|
129
|
+
let raf = 0;
|
|
130
|
+
let added = null;
|
|
131
|
+
let off = null;
|
|
132
|
+
const attach = () => {
|
|
133
|
+
const node = containerRef.current;
|
|
134
|
+
if (!node) {
|
|
135
|
+
raf = requestAnimationFrame(attach);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
added = node;
|
|
139
|
+
layers.current.add(node);
|
|
140
|
+
// Register in the shared cross-overlay stack so an outer popover's
|
|
141
|
+
// outside-press dismiss doesn't fire when a nested context menu is clicked.
|
|
142
|
+
off = registerLayer(node);
|
|
143
|
+
};
|
|
144
|
+
attach();
|
|
123
145
|
return () => {
|
|
124
|
-
|
|
146
|
+
cancelAnimationFrame(raf);
|
|
147
|
+
if (added)
|
|
148
|
+
layers.current.delete(added);
|
|
149
|
+
off?.();
|
|
125
150
|
};
|
|
126
151
|
}, [mounted, layers]);
|
|
127
152
|
// Focus the first item once the popup is actually visible — a
|
|
@@ -181,6 +206,8 @@ function ContextMenuContent({ className, children, onKeyDown, ...props }) {
|
|
|
181
206
|
if (e.defaultPrevented)
|
|
182
207
|
return;
|
|
183
208
|
const c = containerRef.current;
|
|
209
|
+
if (!c)
|
|
210
|
+
return;
|
|
184
211
|
if (e.key === "ArrowDown")
|
|
185
212
|
(e.preventDefault(), moveFocus(c, "next"));
|
|
186
213
|
else if (e.key === "ArrowUp")
|
|
@@ -213,7 +240,7 @@ function ContextMenuContent({ className, children, onKeyDown, ...props }) {
|
|
|
213
240
|
match.focus();
|
|
214
241
|
}
|
|
215
242
|
}
|
|
216
|
-
}, className: cn("z-50 max-h-(--available-height) min-w-36 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-md bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 transition-none outline-none data-[side=bottom]:slide-in-from-top-2 data-[side=
|
|
243
|
+
}, className: cn("z-50 max-h-(--available-height) min-w-36 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-md bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 transition-none outline-none data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className), ...props, children: children }) }));
|
|
217
244
|
}
|
|
218
245
|
function ContextMenuGroup({ className, ...props }) {
|
|
219
246
|
return _jsx("div", { role: "group", "data-slot": "context-menu-group", className: className, ...props });
|
|
@@ -317,12 +344,36 @@ function ContextMenuSubContent({ className, children, onKeyDown, ...props }) {
|
|
|
317
344
|
const containerRef = React.useRef(null);
|
|
318
345
|
const { onTypeaheadKeyDown } = useTypeahead();
|
|
319
346
|
React.useEffect(() => {
|
|
320
|
-
|
|
321
|
-
if (!node)
|
|
347
|
+
if (!mounted)
|
|
322
348
|
return;
|
|
323
|
-
|
|
349
|
+
// The popup is portaled and attaches a frame late (Portal defers its child to
|
|
350
|
+
// its own effect), so containerRef.current is null when this effect first runs
|
|
351
|
+
// and the deps don't change once it attaches. Retry each frame until the node
|
|
352
|
+
// exists, then register it. Without this the dismiss handler's `layers` set
|
|
353
|
+
// stays empty, so a pointerdown INSIDE the submenu fails the contains() check
|
|
354
|
+
// and is treated as an outside click — closing it before the item's onClick
|
|
355
|
+
// fires (real mouse only; a synthetic click with no pointerdown masks it).
|
|
356
|
+
let raf = 0;
|
|
357
|
+
let added = null;
|
|
358
|
+
let off = null;
|
|
359
|
+
const attach = () => {
|
|
360
|
+
const node = containerRef.current;
|
|
361
|
+
if (!node) {
|
|
362
|
+
raf = requestAnimationFrame(attach);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
added = node;
|
|
366
|
+
layers.current.add(node);
|
|
367
|
+
// Register in the shared cross-overlay stack so an outer popover's
|
|
368
|
+
// outside-press dismiss doesn't fire when this nested submenu is clicked.
|
|
369
|
+
off = registerLayer(node);
|
|
370
|
+
};
|
|
371
|
+
attach();
|
|
324
372
|
return () => {
|
|
325
|
-
|
|
373
|
+
cancelAnimationFrame(raf);
|
|
374
|
+
if (added)
|
|
375
|
+
layers.current.delete(added);
|
|
376
|
+
off?.();
|
|
326
377
|
};
|
|
327
378
|
}, [mounted, layers]);
|
|
328
379
|
const didFocus = React.useRef(false);
|
|
@@ -361,6 +412,8 @@ function ContextMenuSubContent({ className, children, onKeyDown, ...props }) {
|
|
|
361
412
|
if (e.defaultPrevented)
|
|
362
413
|
return;
|
|
363
414
|
const c = containerRef.current;
|
|
415
|
+
if (!c)
|
|
416
|
+
return;
|
|
364
417
|
if (e.key === "ArrowDown")
|
|
365
418
|
(e.preventDefault(), moveFocus(c, "next"));
|
|
366
419
|
else if (e.key === "ArrowUp")
|
package/dist/ui/dialog.js
CHANGED
|
@@ -29,13 +29,15 @@ function Dialog({ open, defaultOpen = false, onOpenChange, children }) {
|
|
|
29
29
|
// Track which labelling parts actually render so DialogContent can reference
|
|
30
30
|
// them only when present, avoiding a dangling aria-labelledby/-describedby IDREF.
|
|
31
31
|
const labelling = useDialogLabelling();
|
|
32
|
-
|
|
32
|
+
// Stable id wiring the trigger's aria-controls to the content's id.
|
|
33
|
+
const contentId = React.useId();
|
|
34
|
+
return (_jsx(DialogContext.Provider, { value: { open: isOpen, setOpen, contentId, ...labelling }, children: children }));
|
|
33
35
|
}
|
|
34
36
|
function DialogTrigger({ asChild = false, render, nativeButton: _nativeButton, children, onClick, ...props }) {
|
|
35
|
-
const { open, setOpen } = useDialog();
|
|
37
|
+
const { open, setOpen, contentId } = useDialog();
|
|
36
38
|
const { useSlot, slotChild } = resolveRender(asChild, render, children);
|
|
37
39
|
const Comp = useSlot ? Slot : "button";
|
|
38
|
-
return (_jsx(Comp, { type: useSlot ? undefined : "button", "data-slot": "dialog-trigger", "aria-haspopup": "dialog", "aria-expanded": open, "data-state": open ? "open" : "closed", onClick: (e) => {
|
|
40
|
+
return (_jsx(Comp, { type: useSlot ? undefined : "button", "data-slot": "dialog-trigger", "aria-haspopup": "dialog", "aria-expanded": open, "aria-controls": open ? contentId : undefined, "data-state": open ? "open" : "closed", onClick: (e) => {
|
|
39
41
|
onClick?.(e);
|
|
40
42
|
if (!e.defaultPrevented)
|
|
41
43
|
setOpen(true);
|
|
@@ -60,7 +62,7 @@ function DialogOverlay() {
|
|
|
60
62
|
return null;
|
|
61
63
|
}
|
|
62
64
|
function DialogContent({ className, children, showCloseButton = true, ...props }) {
|
|
63
|
-
const { open, setOpen, labelledBy, describedBy } = useDialog();
|
|
65
|
+
const { open, setOpen, contentId, labelledBy, describedBy } = useDialog();
|
|
64
66
|
const ref = React.useRef(null);
|
|
65
67
|
const pointerDownOnBackdrop = React.useRef(false);
|
|
66
68
|
React.useEffect(() => {
|
|
@@ -87,7 +89,7 @@ function DialogContent({ className, children, showCloseButton = true, ...props }
|
|
|
87
89
|
if (e.target === ref.current && pointerDownOnBackdrop.current) {
|
|
88
90
|
setOpen(false);
|
|
89
91
|
}
|
|
90
|
-
}, className: "group/dialog m-0 grid max-h-none max-w-none place-items-center bg-transparent p-4 backdrop:bg-black/10 backdrop:duration-100 supports-backdrop-filter:backdrop:backdrop-blur-xs open:fixed open:inset-0 open:size-full", children: open ? (_jsxs("div", { "data-slot": "dialog-content", "data-state": open ? "open" : "closed", "data-open": open ? "" : undefined, className: cn("relative grid w-full max-w-[calc(100%-2rem)] gap-6 rounded-xl bg-background p-6 text-sm text-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-md data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95", className), ...props, children: [children, showCloseButton && (_jsx(DialogClose, { asChild: true, children: _jsxs(Button, { variant: "ghost", className: "absolute top-4 right-4", size: "icon-sm", children: [_jsx(XIcon, {}), _jsx("span", { className: "sr-only", children: "Close" })] }) }))] })) : null }));
|
|
92
|
+
}, className: "group/dialog m-0 grid max-h-none max-w-none place-items-center bg-transparent p-4 backdrop:bg-black/10 backdrop:duration-100 supports-backdrop-filter:backdrop:backdrop-blur-xs open:fixed open:inset-0 open:size-full", children: open ? (_jsxs("div", { id: contentId, "data-slot": "dialog-content", "data-state": open ? "open" : "closed", "data-open": open ? "" : undefined, className: cn("relative grid w-full max-w-[calc(100%-2rem)] gap-6 rounded-xl bg-background p-6 text-sm text-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-md data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95", className), ...props, children: [children, showCloseButton && (_jsx(DialogClose, { asChild: true, children: _jsxs(Button, { variant: "ghost", className: "absolute top-4 right-4", size: "icon-sm", children: [_jsx(XIcon, {}), _jsx("span", { className: "sr-only", children: "Close" })] }) }))] })) : null }));
|
|
91
93
|
}
|
|
92
94
|
function DialogHeader({ className, ...props }) {
|
|
93
95
|
return (_jsx("div", { "data-slot": "dialog-header", className: cn("flex flex-col gap-2", className), ...props }));
|
package/dist/ui/drawer.js
CHANGED
|
@@ -54,9 +54,12 @@ function Drawer({ open, defaultOpen = false, onOpenChange, children }) {
|
|
|
54
54
|
onChange: onOpenChange,
|
|
55
55
|
});
|
|
56
56
|
const { titleId, descriptionId, registerTitle, registerDescription, labelledBy, describedBy } = useDialogLabelling();
|
|
57
|
+
// Stable id wiring the trigger's aria-controls to the content's id.
|
|
58
|
+
const contentId = React.useId();
|
|
57
59
|
return (_jsx(DrawerContext.Provider, { value: {
|
|
58
60
|
open: isOpen,
|
|
59
61
|
setOpen,
|
|
62
|
+
contentId,
|
|
60
63
|
titleId,
|
|
61
64
|
descriptionId,
|
|
62
65
|
registerTitle,
|
|
@@ -66,10 +69,10 @@ function Drawer({ open, defaultOpen = false, onOpenChange, children }) {
|
|
|
66
69
|
}, children: children }));
|
|
67
70
|
}
|
|
68
71
|
function DrawerTrigger({ asChild = false, render, nativeButton: _nativeButton, children, onClick, ...props }) {
|
|
69
|
-
const { open, setOpen } = useDrawer();
|
|
72
|
+
const { open, setOpen, contentId } = useDrawer();
|
|
70
73
|
const { useSlot, slotChild } = resolveRender(asChild, render, children);
|
|
71
74
|
const Comp = useSlot ? Slot : "button";
|
|
72
|
-
return (_jsx(Comp, { type: useSlot ? undefined : "button", "data-slot": "drawer-trigger", "aria-haspopup": "dialog", "data-state": open ? "open" : "closed", onClick: (e) => {
|
|
75
|
+
return (_jsx(Comp, { type: useSlot ? undefined : "button", "data-slot": "drawer-trigger", "aria-haspopup": "dialog", "aria-expanded": open, "aria-controls": open ? contentId : undefined, "data-state": open ? "open" : "closed", onClick: (e) => {
|
|
73
76
|
onClick?.(e);
|
|
74
77
|
if (!e.defaultPrevented)
|
|
75
78
|
setOpen(true);
|
|
@@ -94,7 +97,7 @@ function DrawerOverlay() {
|
|
|
94
97
|
return null;
|
|
95
98
|
}
|
|
96
99
|
function DrawerContent({ className, children, onPointerDown, onPointerMove, onPointerUp, onPointerCancel, ...props }) {
|
|
97
|
-
const { open, setOpen, labelledBy, describedBy } = useDrawer();
|
|
100
|
+
const { open, setOpen, contentId, labelledBy, describedBy } = useDrawer();
|
|
98
101
|
const ref = React.useRef(null);
|
|
99
102
|
const pointerDownOnBackdrop = React.useRef(false);
|
|
100
103
|
const [mounted, presenceRef] = usePresence(open);
|
|
@@ -213,7 +216,7 @@ function DrawerContent({ className, children, onPointerDown, onPointerMove, onPo
|
|
|
213
216
|
};
|
|
214
217
|
if (!mounted)
|
|
215
218
|
return null;
|
|
216
|
-
return (_jsxs("dialog", { ...props, ref: mergeRefs(ref, presenceRef), "aria-labelledby": labelledBy, "aria-describedby": describedBy, "data-slot": "drawer-content", "data-state": open ? "open" : "closed", "data-open": open ? "" : undefined, "data-closed": open ? undefined : "", onCancel: (e) => {
|
|
219
|
+
return (_jsxs("dialog", { ...props, ref: mergeRefs(ref, presenceRef), id: contentId, "aria-labelledby": labelledBy, "aria-describedby": describedBy, "data-slot": "drawer-content", "data-state": open ? "open" : "closed", "data-open": open ? "" : undefined, "data-closed": open ? undefined : "", onCancel: (e) => {
|
|
217
220
|
e.preventDefault();
|
|
218
221
|
setOpen(false);
|
|
219
222
|
}, onClose: () => setOpen(false), onClick: (e) => {
|
|
@@ -54,8 +54,8 @@ declare function DropdownMenuSub({ defaultOpen, children, }: {
|
|
|
54
54
|
defaultOpen?: boolean;
|
|
55
55
|
children?: React.ReactNode;
|
|
56
56
|
}): React.JSX.Element;
|
|
57
|
-
declare function DropdownMenuSubTrigger({ className, inset, children, onClick, onKeyDown, onMouseEnter, ...props }: React.ComponentProps<"div"> & {
|
|
57
|
+
declare function DropdownMenuSubTrigger({ className, inset, children, onClick, onKeyDown, onMouseEnter, onPointerLeave, ...props }: React.ComponentProps<"div"> & {
|
|
58
58
|
inset?: boolean;
|
|
59
59
|
}): React.JSX.Element;
|
|
60
|
-
declare function DropdownMenuSubContent({ className, children, onKeyDown, ...props }: React.ComponentProps<"div">): React.JSX.Element | null;
|
|
60
|
+
declare function DropdownMenuSubContent({ className, children, onKeyDown, onPointerLeave, ...props }: React.ComponentProps<"div">): React.JSX.Element | null;
|
|
61
61
|
export { DropdownMenu, DropdownMenuPortal, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuGroup, DropdownMenuLabel, DropdownMenuItem, DropdownMenuCheckboxItem, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubTrigger, DropdownMenuSubContent, };
|
package/dist/ui/dropdown-menu.js
CHANGED
|
@@ -9,6 +9,7 @@ import { usePresence } from "./use-presence.js";
|
|
|
9
9
|
import { useControllableState } from "./use-controllable-state.js";
|
|
10
10
|
import { useTypeahead } from "./use-typeahead.js";
|
|
11
11
|
import { usePointerMoved } from "./use-pointer-moved.js";
|
|
12
|
+
import { registerLayer } from "./layer-stack.js";
|
|
12
13
|
import { ChevronRightIcon, CheckIcon } from "../icons/index.js";
|
|
13
14
|
const MenuContext = React.createContext(null);
|
|
14
15
|
function useMenu() {
|
|
@@ -95,6 +96,7 @@ function DropdownMenuContent({ align = "start", alignOffset = 0, side = "bottom"
|
|
|
95
96
|
// fires (real mouse only; a synthetic click with no pointerdown masks it).
|
|
96
97
|
let raf = 0;
|
|
97
98
|
let added = null;
|
|
99
|
+
let off = null;
|
|
98
100
|
const attach = () => {
|
|
99
101
|
const node = containerRef.current;
|
|
100
102
|
if (!node) {
|
|
@@ -103,12 +105,16 @@ function DropdownMenuContent({ align = "start", alignOffset = 0, side = "bottom"
|
|
|
103
105
|
}
|
|
104
106
|
added = node;
|
|
105
107
|
layers.current.add(node);
|
|
108
|
+
// Also register in the shared cross-overlay stack so a dropdown nested in a
|
|
109
|
+
// popover doesn't trip the popover's outside-press dismiss.
|
|
110
|
+
off = registerLayer(node);
|
|
106
111
|
};
|
|
107
112
|
attach();
|
|
108
113
|
return () => {
|
|
109
114
|
cancelAnimationFrame(raf);
|
|
110
115
|
if (added)
|
|
111
116
|
layers.current.delete(added);
|
|
117
|
+
off?.();
|
|
112
118
|
};
|
|
113
119
|
}, [mounted, layers]);
|
|
114
120
|
// Focus the first item once the popup is actually visible — a
|
|
@@ -172,6 +178,8 @@ function DropdownMenuContent({ align = "start", alignOffset = 0, side = "bottom"
|
|
|
172
178
|
if (e.defaultPrevented)
|
|
173
179
|
return;
|
|
174
180
|
const c = containerRef.current;
|
|
181
|
+
if (!c)
|
|
182
|
+
return;
|
|
175
183
|
if (e.key === "ArrowDown")
|
|
176
184
|
(e.preventDefault(), moveFocus(c, "next"));
|
|
177
185
|
else if (e.key === "ArrowUp")
|
|
@@ -186,7 +194,10 @@ function DropdownMenuContent({ align = "start", alignOffset = 0, side = "bottom"
|
|
|
186
194
|
triggerRef.current?.focus();
|
|
187
195
|
}
|
|
188
196
|
else if (e.key === "Tab") {
|
|
197
|
+
// Restore focus to the trigger so the browser's default Tab advances
|
|
198
|
+
// from there — content is portaled to end of body, orphaning focus.
|
|
189
199
|
setOpen(false);
|
|
200
|
+
triggerRef.current?.focus();
|
|
190
201
|
}
|
|
191
202
|
else if (e.key === "Enter" || e.key === " ") {
|
|
192
203
|
const active = document.activeElement;
|
|
@@ -278,13 +289,24 @@ function DropdownMenuSub({ defaultOpen = false, children, }) {
|
|
|
278
289
|
const subContentId = React.useId();
|
|
279
290
|
return (_jsx(SubContext.Provider, { value: { open, setOpen, triggerRef, subContentId }, children: children }));
|
|
280
291
|
}
|
|
281
|
-
function DropdownMenuSubTrigger({ className, inset, children, onClick, onKeyDown, onMouseEnter, ...props }) {
|
|
292
|
+
function DropdownMenuSubTrigger({ className, inset, children, onClick, onKeyDown, onMouseEnter, onPointerLeave, ...props }) {
|
|
282
293
|
const { open, setOpen, triggerRef, subContentId } = useSub();
|
|
283
294
|
return (_jsxs("div", { ref: triggerRef, role: "menuitem", tabIndex: -1, "aria-haspopup": "menu", "aria-expanded": open, "aria-controls": open ? subContentId : undefined, "data-slot": "dropdown-menu-sub-trigger", "data-inset": inset ? "" : undefined, "data-popup-open": open ? "" : undefined, "data-open": open ? "" : undefined, className: cn(
|
|
284
295
|
// focus-visible ring: keyboard-only focus target (focus:bg-accent is below WCAG 3:1).
|
|
285
296
|
"flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset data-inset:pl-8 data-popup-open:bg-accent data-popup-open:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className), onMouseEnter: (e) => {
|
|
286
297
|
onMouseEnter?.(e);
|
|
287
298
|
setOpen(true);
|
|
299
|
+
}, onPointerLeave: (e) => {
|
|
300
|
+
onPointerLeave?.(e);
|
|
301
|
+
if (e.defaultPrevented)
|
|
302
|
+
return;
|
|
303
|
+
// Close when the pointer leaves the trigger toward anything that isn't the
|
|
304
|
+
// open submenu panel (keeps it open while moving into the sub-content).
|
|
305
|
+
const related = e.relatedTarget;
|
|
306
|
+
const subContent = document.getElementById(subContentId);
|
|
307
|
+
if (related instanceof Node && subContent?.contains(related))
|
|
308
|
+
return;
|
|
309
|
+
setOpen(false);
|
|
288
310
|
}, onClick: (e) => {
|
|
289
311
|
onClick?.(e);
|
|
290
312
|
if (!e.defaultPrevented)
|
|
@@ -299,7 +321,7 @@ function DropdownMenuSubTrigger({ className, inset, children, onClick, onKeyDown
|
|
|
299
321
|
}
|
|
300
322
|
}, ...props, children: [children, _jsx(ChevronRightIcon, { className: "ml-auto" })] }));
|
|
301
323
|
}
|
|
302
|
-
function DropdownMenuSubContent({ className, children, onKeyDown, ...props }) {
|
|
324
|
+
function DropdownMenuSubContent({ className, children, onKeyDown, onPointerLeave, ...props }) {
|
|
303
325
|
const { open, setOpen, triggerRef, subContentId } = useSub();
|
|
304
326
|
const { layers } = useMenu();
|
|
305
327
|
const [mounted, presenceRef] = usePresence(open);
|
|
@@ -324,6 +346,7 @@ function DropdownMenuSubContent({ className, children, onKeyDown, ...props }) {
|
|
|
324
346
|
// fires (real mouse only; a synthetic click with no pointerdown masks it).
|
|
325
347
|
let raf = 0;
|
|
326
348
|
let added = null;
|
|
349
|
+
let off = null;
|
|
327
350
|
const attach = () => {
|
|
328
351
|
const node = containerRef.current;
|
|
329
352
|
if (!node) {
|
|
@@ -332,12 +355,16 @@ function DropdownMenuSubContent({ className, children, onKeyDown, ...props }) {
|
|
|
332
355
|
}
|
|
333
356
|
added = node;
|
|
334
357
|
layers.current.add(node);
|
|
358
|
+
// Also register in the shared cross-overlay stack so a dropdown nested in a
|
|
359
|
+
// popover doesn't trip the popover's outside-press dismiss.
|
|
360
|
+
off = registerLayer(node);
|
|
335
361
|
};
|
|
336
362
|
attach();
|
|
337
363
|
return () => {
|
|
338
364
|
cancelAnimationFrame(raf);
|
|
339
365
|
if (added)
|
|
340
366
|
layers.current.delete(added);
|
|
367
|
+
off?.();
|
|
341
368
|
};
|
|
342
369
|
}, [mounted, layers]);
|
|
343
370
|
const didFocus = React.useRef(false);
|
|
@@ -379,11 +406,23 @@ function DropdownMenuSubContent({ className, children, onKeyDown, ...props }) {
|
|
|
379
406
|
const item = e.target.closest('[role^="menuitem"]:not([data-disabled])');
|
|
380
407
|
if (item && item !== document.activeElement)
|
|
381
408
|
item.focus();
|
|
409
|
+
}, onPointerLeave: (e) => {
|
|
410
|
+
onPointerLeave?.(e);
|
|
411
|
+
if (e.defaultPrevented)
|
|
412
|
+
return;
|
|
413
|
+
// Close when the pointer leaves the panel toward anything that isn't the
|
|
414
|
+
// sub-trigger (keeps it open while moving back onto the trigger).
|
|
415
|
+
const related = e.relatedTarget;
|
|
416
|
+
if (related instanceof Node && triggerRef.current?.contains(related))
|
|
417
|
+
return;
|
|
418
|
+
setOpen(false);
|
|
382
419
|
}, onKeyDown: (e) => {
|
|
383
420
|
onKeyDown?.(e);
|
|
384
421
|
if (e.defaultPrevented)
|
|
385
422
|
return;
|
|
386
423
|
const c = containerRef.current;
|
|
424
|
+
if (!c)
|
|
425
|
+
return;
|
|
387
426
|
if (e.key === "ArrowDown")
|
|
388
427
|
(e.preventDefault(), moveFocus(c, "next"));
|
|
389
428
|
else if (e.key === "ArrowUp")
|
|
@@ -395,7 +434,7 @@ function DropdownMenuSubContent({ className, children, onKeyDown, ...props }) {
|
|
|
395
434
|
}
|
|
396
435
|
else if (e.key === "Enter" || e.key === " ") {
|
|
397
436
|
const active = document.activeElement;
|
|
398
|
-
if (active && c.contains(active)) {
|
|
437
|
+
if (active && c.contains(active) && active.getAttribute("role")?.startsWith("menuitem")) {
|
|
399
438
|
e.preventDefault();
|
|
400
439
|
active.click();
|
|
401
440
|
}
|
package/dist/ui/field.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import * as React from "react";
|
|
1
2
|
import { type VariantProps } from "../shadcn/lib/cva.js";
|
|
2
3
|
import { Label } from "./label.js";
|
|
3
|
-
declare function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">):
|
|
4
|
+
declare function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">): React.JSX.Element;
|
|
4
5
|
declare function FieldLegend({ className, variant, ...props }: React.ComponentProps<"legend"> & {
|
|
5
6
|
variant?: "legend" | "label";
|
|
6
|
-
}):
|
|
7
|
-
declare function FieldGroup({ className, ...props }: React.ComponentProps<"div">):
|
|
7
|
+
}): React.JSX.Element;
|
|
8
|
+
declare function FieldGroup({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
|
|
8
9
|
declare const fieldVariants: (props?: import("../shadcn/lib/cva.js").CvaProps<{
|
|
9
10
|
orientation: {
|
|
10
11
|
vertical: string;
|
|
@@ -12,17 +13,17 @@ declare const fieldVariants: (props?: import("../shadcn/lib/cva.js").CvaProps<{
|
|
|
12
13
|
responsive: string;
|
|
13
14
|
};
|
|
14
15
|
}> | undefined) => string;
|
|
15
|
-
declare function Field({ className, orientation, ...props }: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>):
|
|
16
|
-
declare function FieldContent({ className, ...props }: React.ComponentProps<"div">):
|
|
17
|
-
declare function FieldLabel({ className, ...props }: React.ComponentProps<typeof Label>):
|
|
18
|
-
declare function FieldTitle({ className, ...props }: React.ComponentProps<"div">):
|
|
19
|
-
declare function FieldDescription({ className, ...props }: React.ComponentProps<"p">):
|
|
16
|
+
declare function Field({ className, orientation, ...props }: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>): React.JSX.Element;
|
|
17
|
+
declare function FieldContent({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
|
|
18
|
+
declare function FieldLabel({ className, ...props }: React.ComponentProps<typeof Label>): React.JSX.Element;
|
|
19
|
+
declare function FieldTitle({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
|
|
20
|
+
declare function FieldDescription({ className, ...props }: React.ComponentProps<"p">): React.JSX.Element;
|
|
20
21
|
declare function FieldSeparator({ children, className, ...props }: React.ComponentProps<"div"> & {
|
|
21
22
|
children?: React.ReactNode;
|
|
22
|
-
}):
|
|
23
|
+
}): React.JSX.Element;
|
|
23
24
|
declare function FieldError({ className, children, errors, ...props }: React.ComponentProps<"div"> & {
|
|
24
25
|
errors?: Array<{
|
|
25
26
|
message?: string;
|
|
26
27
|
} | undefined>;
|
|
27
|
-
}):
|
|
28
|
+
}): React.JSX.Element | null;
|
|
28
29
|
export { Field, FieldLabel, FieldDescription, FieldError, FieldGroup, FieldLegend, FieldSeparator, FieldSet, FieldContent, FieldTitle, };
|
package/dist/ui/field.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import
|
|
3
|
+
import * as React from "react";
|
|
4
4
|
import { cva } from "../shadcn/lib/cva.js";
|
|
5
5
|
import { cn } from "../shadcn/lib/utils.js";
|
|
6
6
|
import { Label } from "./label.js";
|
|
@@ -45,7 +45,7 @@ function FieldSeparator({ children, className, ...props }) {
|
|
|
45
45
|
return (_jsxs("div", { "data-slot": "field-separator", "data-content": !!children, className: cn("relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2", className), ...props, children: [_jsx(Separator, { className: "absolute inset-0 top-1/2" }), children && (_jsx("span", { className: "relative mx-auto block w-fit bg-background px-2 text-muted-foreground", "data-slot": "field-separator-content", children: children }))] }));
|
|
46
46
|
}
|
|
47
47
|
function FieldError({ className, children, errors, ...props }) {
|
|
48
|
-
const content = useMemo(() => {
|
|
48
|
+
const content = React.useMemo(() => {
|
|
49
49
|
if (children) {
|
|
50
50
|
return children;
|
|
51
51
|
}
|
package/dist/ui/hover-card.js
CHANGED
|
@@ -114,6 +114,6 @@ function HoverCardContent({ className, style, ref, side = "bottom", sideOffset =
|
|
|
114
114
|
}, onMouseLeave: (e) => {
|
|
115
115
|
onMouseLeave?.(e);
|
|
116
116
|
scheduleClose();
|
|
117
|
-
}, className: cn("isolate z-50 w-64 origin-(--transform-origin) rounded-lg bg-popover p-4 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-hidden transition-none data-[side=bottom]:slide-in-from-top-2 data-[side=
|
|
117
|
+
}, className: cn("isolate z-50 w-64 origin-(--transform-origin) rounded-lg bg-popover p-4 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-hidden transition-none data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className), ...props }) }));
|
|
118
118
|
}
|
|
119
119
|
export { HoverCard, HoverCardTrigger, HoverCardContent };
|
package/dist/ui/input-group.js
CHANGED
|
@@ -27,7 +27,9 @@ function InputGroupAddon({ className, align = "inline-start", ...props }) {
|
|
|
27
27
|
if (e.target.closest("button")) {
|
|
28
28
|
return;
|
|
29
29
|
}
|
|
30
|
-
e.currentTarget.parentElement
|
|
30
|
+
e.currentTarget.parentElement
|
|
31
|
+
?.querySelector("input, textarea")
|
|
32
|
+
?.focus();
|
|
31
33
|
}, ...props }));
|
|
32
34
|
}
|
|
33
35
|
const inputGroupButtonVariants = cva("flex items-center gap-2 text-sm shadow-none", {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const layers = new Set();
|
|
2
|
+
export function registerLayer(node) {
|
|
3
|
+
layers.add(node);
|
|
4
|
+
return () => {
|
|
5
|
+
layers.delete(node);
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
export function isInLayer(target) {
|
|
9
|
+
if (!target)
|
|
10
|
+
return false;
|
|
11
|
+
for (const node of layers) {
|
|
12
|
+
if (node.contains(target))
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
return false;
|
|
16
|
+
}
|
package/dist/ui/menubar.js
CHANGED
|
@@ -8,6 +8,7 @@ import { useFloating } from "./use-floating.js";
|
|
|
8
8
|
import { usePresence } from "./use-presence.js";
|
|
9
9
|
import { useTypeahead } from "./use-typeahead.js";
|
|
10
10
|
import { usePointerMoved } from "./use-pointer-moved.js";
|
|
11
|
+
import { registerLayer } from "./layer-stack.js";
|
|
11
12
|
import { ChevronRightIcon, CheckIcon } from "../icons/index.js";
|
|
12
13
|
const MenubarRootContext = React.createContext(null);
|
|
13
14
|
function useMenubarRoot() {
|
|
@@ -134,12 +135,34 @@ function MenubarContent({ align = "start", alignOffset = -4, side = "bottom", si
|
|
|
134
135
|
const { onTypeaheadKeyDown } = useTypeahead();
|
|
135
136
|
const hasPointerMoved = usePointerMoved();
|
|
136
137
|
React.useEffect(() => {
|
|
137
|
-
|
|
138
|
-
if (!node)
|
|
138
|
+
if (!mounted)
|
|
139
139
|
return;
|
|
140
|
-
|
|
140
|
+
// The popup is portaled and attaches a frame late (Portal defers its child to
|
|
141
|
+
// its own effect), so containerRef.current is null when this effect first runs
|
|
142
|
+
// and the deps don't change once it attaches. Retry each frame until the node
|
|
143
|
+
// exists, then register it. Without this the dismiss handler's `layers` set
|
|
144
|
+
// stays empty, so a pointerdown INSIDE the menu fails the contains() check and
|
|
145
|
+
// is treated as an outside click — closing the menu before the item's onClick
|
|
146
|
+
// fires (real mouse only; a synthetic click with no pointerdown masks it).
|
|
147
|
+
let raf = 0;
|
|
148
|
+
let added = null;
|
|
149
|
+
let off = null;
|
|
150
|
+
const attach = () => {
|
|
151
|
+
const node = containerRef.current;
|
|
152
|
+
if (!node) {
|
|
153
|
+
raf = requestAnimationFrame(attach);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
added = node;
|
|
157
|
+
layers.current.add(node);
|
|
158
|
+
off = registerLayer(node);
|
|
159
|
+
};
|
|
160
|
+
attach();
|
|
141
161
|
return () => {
|
|
142
|
-
|
|
162
|
+
cancelAnimationFrame(raf);
|
|
163
|
+
if (added)
|
|
164
|
+
layers.current.delete(added);
|
|
165
|
+
off?.();
|
|
143
166
|
};
|
|
144
167
|
}, [mounted, layers]);
|
|
145
168
|
// Focus the first item once the popup is actually visible — a
|
|
@@ -348,12 +371,32 @@ function MenubarSubContent({ className, children, onKeyDown, ...props }) {
|
|
|
348
371
|
const containerRef = React.useRef(null);
|
|
349
372
|
const hasPointerMoved = usePointerMoved();
|
|
350
373
|
React.useEffect(() => {
|
|
351
|
-
|
|
352
|
-
if (!node)
|
|
374
|
+
if (!mounted)
|
|
353
375
|
return;
|
|
354
|
-
|
|
376
|
+
// The sub-popup is portaled and attaches a frame late, so containerRef.current
|
|
377
|
+
// is null when this effect first runs and the deps don't change once it
|
|
378
|
+
// attaches. Retry each frame until the node exists, then register it — without
|
|
379
|
+
// this the dismiss handler's `layers` set stays empty and a real-mouse click
|
|
380
|
+
// inside the submenu reads as an outside click, closing it before onClick fires.
|
|
381
|
+
let raf = 0;
|
|
382
|
+
let added = null;
|
|
383
|
+
let off = null;
|
|
384
|
+
const attach = () => {
|
|
385
|
+
const node = containerRef.current;
|
|
386
|
+
if (!node) {
|
|
387
|
+
raf = requestAnimationFrame(attach);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
added = node;
|
|
391
|
+
layers.current.add(node);
|
|
392
|
+
off = registerLayer(node);
|
|
393
|
+
};
|
|
394
|
+
attach();
|
|
355
395
|
return () => {
|
|
356
|
-
|
|
396
|
+
cancelAnimationFrame(raf);
|
|
397
|
+
if (added)
|
|
398
|
+
layers.current.delete(added);
|
|
399
|
+
off?.();
|
|
357
400
|
};
|
|
358
401
|
}, [mounted, layers]);
|
|
359
402
|
const didFocus = React.useRef(false);
|
package/dist/ui/popover.js
CHANGED
|
@@ -6,6 +6,7 @@ import { Slot, mergeRefs, resolveRender } from "./slot.js";
|
|
|
6
6
|
import { Portal } from "./portal.js";
|
|
7
7
|
import { useFloating } from "./use-floating.js";
|
|
8
8
|
import { useDismiss } from "./use-dismiss.js";
|
|
9
|
+
import { registerLayer } from "./layer-stack.js";
|
|
9
10
|
import { usePresence } from "./use-presence.js";
|
|
10
11
|
import { useControllableState } from "./use-controllable-state.js";
|
|
11
12
|
import { useDialogLabelling } from "./use-labelling.js";
|
|
@@ -73,14 +74,35 @@ function PopoverContent({ className, style, ref, align = "center", alignOffset =
|
|
|
73
74
|
// mounts it a tick after `open` flips, and it stays visibility:hidden until
|
|
74
75
|
// useFloating positions it — focus() on a missing or hidden node is a silent
|
|
75
76
|
// no-op. The effect cleanup cancels this when the popover closes.
|
|
77
|
+
let tries = 0;
|
|
76
78
|
const focusIn = () => {
|
|
79
|
+
// Safety bound (~1.5s of frames) so a popover that can never take focus
|
|
80
|
+
// doesn't spin forever.
|
|
81
|
+
if (tries++ > 90)
|
|
82
|
+
return;
|
|
77
83
|
const node = contentRef.current;
|
|
84
|
+
// Wait while the content is still mounting (presence renders it a tick
|
|
85
|
+
// after `open` flips) or still positioning (it stays visibility:hidden
|
|
86
|
+
// until useFloating measures it) — focus() on a missing/hidden node is a
|
|
87
|
+
// silent no-op.
|
|
78
88
|
if (!node || getComputedStyle(node).visibility === "hidden") {
|
|
79
89
|
raf = requestAnimationFrame(focusIn);
|
|
80
90
|
return;
|
|
81
91
|
}
|
|
92
|
+
// Focus is already where it belongs.
|
|
93
|
+
if (node.contains(document.activeElement))
|
|
94
|
+
return;
|
|
95
|
+
// Don't fight the user: if focus moved somewhere that isn't the trigger or
|
|
96
|
+
// <body>, they placed it deliberately — stop chasing.
|
|
97
|
+
const active = document.activeElement;
|
|
98
|
+
if (active && active !== trigger && active !== document.body)
|
|
99
|
+
return;
|
|
82
100
|
const first = node.querySelector(FOCUSABLE_SELECTOR);
|
|
83
101
|
(first ?? node).focus();
|
|
102
|
+
// Even once visible, the target can be a frame away from being focusable,
|
|
103
|
+
// so focus() may no-op — retry until focus genuinely lands inside.
|
|
104
|
+
if (!node.contains(document.activeElement))
|
|
105
|
+
raf = requestAnimationFrame(focusIn);
|
|
84
106
|
};
|
|
85
107
|
raf = requestAnimationFrame(focusIn);
|
|
86
108
|
return () => {
|
|
@@ -92,6 +114,18 @@ function PopoverContent({ className, style, ref, align = "center", alignOffset =
|
|
|
92
114
|
trigger.focus();
|
|
93
115
|
};
|
|
94
116
|
}, [open, triggerRef]);
|
|
117
|
+
// Register the floating content as an open dismissable layer while mounted, so
|
|
118
|
+
// an outer overlay's outside-press dismiss ignores clicks landing inside this
|
|
119
|
+
// popover (e.g. a popover nested in another popover). Keyed on `mounted` so it
|
|
120
|
+
// runs once contentRef attaches.
|
|
121
|
+
React.useEffect(() => {
|
|
122
|
+
if (!mounted)
|
|
123
|
+
return;
|
|
124
|
+
const node = contentRef.current;
|
|
125
|
+
if (!node)
|
|
126
|
+
return;
|
|
127
|
+
return registerLayer(node);
|
|
128
|
+
}, [mounted]);
|
|
95
129
|
if (!mounted)
|
|
96
130
|
return null;
|
|
97
131
|
return (_jsx(Portal, { children: _jsx("div", { ref: mergeRefs(floatingRef, presenceRef, contentRef, ref), id: contentId, role: "dialog", "aria-labelledby": labelledBy, "aria-describedby": describedBy, tabIndex: -1, "data-slot": "popover-content", "data-state": open ? "open" : "closed", "data-open": open ? "" : undefined, "data-closed": open ? undefined : "", "data-side": pos?.side ?? side, onKeyDown: (e) => {
|
package/dist/ui/radio-group.js
CHANGED
|
@@ -53,8 +53,19 @@ function RadioGroupItem({ className, value, onClick, disabled, ref, ...props })
|
|
|
53
53
|
if (selected !== undefined)
|
|
54
54
|
return;
|
|
55
55
|
const el = btnRef.current;
|
|
56
|
-
|
|
56
|
+
// Scope to the radiogroup (not the immediate parent) so per-item wrappers
|
|
57
|
+
// like <label>/<div> don't change which radio counts as "first enabled" —
|
|
58
|
+
// this matches the arrow-key handler, which queries the group element.
|
|
59
|
+
const group = el?.closest('[role="radiogroup"]');
|
|
60
|
+
const first = (group ?? el)?.querySelector('[role="radio"]:not([disabled]):not([data-disabled])');
|
|
57
61
|
setIsFirstEnabled(first === el);
|
|
62
|
+
// No dependency array on purpose: "first enabled" depends on the whole
|
|
63
|
+
// sibling set, so an item must recompute when ANY sibling's disabled state
|
|
64
|
+
// or order changes — a per-item dep list ([selected, value, disabled])
|
|
65
|
+
// misses those cross-sibling changes and can leave the group with zero tab
|
|
66
|
+
// stops. The body is cheap (two querySelectors) and setIsFirstEnabled bails
|
|
67
|
+
// out of re-render when the value is unchanged, so running every render is
|
|
68
|
+
// negligible.
|
|
58
69
|
});
|
|
59
70
|
const tabbable = checked || (selected === undefined && isFirstEnabled);
|
|
60
71
|
return (_jsx("button", { ref: mergeRefs(btnRef, ref), type: "button", role: "radio", "aria-checked": checked, disabled: disabled, tabIndex: tabbable ? 0 : -1, "data-slot": "radio-group-item", "data-state": checked ? "checked" : "unchecked", "data-checked": checked ? "" : undefined, className: cn("cursor-pointer group/radio-group-item peer relative flex aspect-square size-4 shrink-0 items-center justify-center rounded-full border border-input outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary", className), onClick: (e) => {
|