@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/select.js
CHANGED
|
@@ -4,6 +4,7 @@ import * as React from "react";
|
|
|
4
4
|
import { cn } from "../shadcn/lib/utils.js";
|
|
5
5
|
import { mergeRefs } from "./slot.js";
|
|
6
6
|
import { Portal } from "./portal.js";
|
|
7
|
+
import { registerLayer } from "./layer-stack.js";
|
|
7
8
|
import { useFloating } from "./use-floating.js";
|
|
8
9
|
import { usePresence } from "./use-presence.js";
|
|
9
10
|
import { useControllableState } from "./use-controllable-state.js";
|
|
@@ -33,10 +34,6 @@ function Select({ value, defaultValue, onValueChange, open, defaultOpen = false,
|
|
|
33
34
|
const triggerId = React.useId();
|
|
34
35
|
const [labels, setLabels] = React.useState({});
|
|
35
36
|
const registerLabel = React.useCallback((v, label) => setLabels((prev) => ({ ...prev, [v]: label })), []);
|
|
36
|
-
const unregisterLabel = React.useCallback((v) => setLabels((prev) => {
|
|
37
|
-
const { [v]: _, ...rest } = prev;
|
|
38
|
-
return rest;
|
|
39
|
-
}), []);
|
|
40
37
|
const setValue = React.useCallback((v) => {
|
|
41
38
|
setSelected(v);
|
|
42
39
|
setOpen(false);
|
|
@@ -52,7 +49,6 @@ function Select({ value, defaultValue, onValueChange, open, defaultOpen = false,
|
|
|
52
49
|
triggerId,
|
|
53
50
|
labels,
|
|
54
51
|
registerLabel,
|
|
55
|
-
unregisterLabel,
|
|
56
52
|
}, children: children }));
|
|
57
53
|
}
|
|
58
54
|
function SelectGroup({ className, ...props }) {
|
|
@@ -124,6 +120,8 @@ function SelectContent({ className, children, side = "bottom", sideOffset = 4, a
|
|
|
124
120
|
React.useEffect(() => {
|
|
125
121
|
if (!open)
|
|
126
122
|
return;
|
|
123
|
+
const node = containerRef.current;
|
|
124
|
+
const off = node ? registerLayer(node) : undefined;
|
|
127
125
|
const onPointerDown = (e) => {
|
|
128
126
|
const t = e.target;
|
|
129
127
|
if (triggerRef.current?.contains(t))
|
|
@@ -135,6 +133,7 @@ function SelectContent({ className, children, side = "bottom", sideOffset = 4, a
|
|
|
135
133
|
document.addEventListener("pointerdown", onPointerDown, true);
|
|
136
134
|
return () => {
|
|
137
135
|
document.removeEventListener("pointerdown", onPointerDown, true);
|
|
136
|
+
off?.();
|
|
138
137
|
};
|
|
139
138
|
}, [open, setOpen, triggerRef]);
|
|
140
139
|
// While closed, render the items into a hidden tree so their value→label
|
package/dist/ui/sheet.js
CHANGED
|
@@ -28,9 +28,12 @@ function Sheet({ open, defaultOpen = false, onOpenChange, children }) {
|
|
|
28
28
|
// Title/Description part is actually rendered, so the dialog never points at a
|
|
29
29
|
// missing element (a dangling IDREF that screen readers announce as broken).
|
|
30
30
|
const { titleId, descriptionId, registerTitle, registerDescription, labelledBy, describedBy } = useDialogLabelling();
|
|
31
|
+
// Stable id wiring the trigger's aria-controls to the content's id.
|
|
32
|
+
const contentId = React.useId();
|
|
31
33
|
return (_jsx(SheetContext.Provider, { value: {
|
|
32
34
|
open: isOpen,
|
|
33
35
|
setOpen,
|
|
36
|
+
contentId,
|
|
34
37
|
titleId,
|
|
35
38
|
descriptionId,
|
|
36
39
|
registerTitle,
|
|
@@ -40,10 +43,10 @@ function Sheet({ open, defaultOpen = false, onOpenChange, children }) {
|
|
|
40
43
|
}, children: children }));
|
|
41
44
|
}
|
|
42
45
|
function SheetTrigger({ asChild = false, render, nativeButton: _nativeButton, children, onClick, ...props }) {
|
|
43
|
-
const { open, setOpen } = useSheet();
|
|
46
|
+
const { open, setOpen, contentId } = useSheet();
|
|
44
47
|
const { useSlot, slotChild } = resolveRender(asChild, render, children);
|
|
45
48
|
const Comp = useSlot ? Slot : "button";
|
|
46
|
-
return (_jsx(Comp, { type: useSlot ? undefined : "button", "data-slot": "sheet-trigger", "aria-haspopup": "dialog", "data-state": open ? "open" : "closed", onClick: (e) => {
|
|
49
|
+
return (_jsx(Comp, { type: useSlot ? undefined : "button", "data-slot": "sheet-trigger", "aria-haspopup": "dialog", "aria-expanded": open, "aria-controls": open ? contentId : undefined, "data-state": open ? "open" : "closed", onClick: (e) => {
|
|
47
50
|
onClick?.(e);
|
|
48
51
|
if (!e.defaultPrevented)
|
|
49
52
|
setOpen(true);
|
|
@@ -80,7 +83,7 @@ const SIDE_OUT = {
|
|
|
80
83
|
bottom: "slide-out-to-bottom-52",
|
|
81
84
|
};
|
|
82
85
|
function SheetContent({ className, children, side = "right", showCloseButton = true, ...props }) {
|
|
83
|
-
const { open, setOpen, labelledBy, describedBy } = useSheet();
|
|
86
|
+
const { open, setOpen, contentId, labelledBy, describedBy } = useSheet();
|
|
84
87
|
const ref = React.useRef(null);
|
|
85
88
|
const pointerDownOnBackdrop = React.useRef(false);
|
|
86
89
|
const [mounted, presenceRef] = usePresence(open);
|
|
@@ -98,7 +101,7 @@ function SheetContent({ className, children, side = "right", showCloseButton = t
|
|
|
98
101
|
useScrollLock(mounted);
|
|
99
102
|
if (!mounted)
|
|
100
103
|
return null;
|
|
101
|
-
return (_jsxs("dialog", { ...props, ref: mergeRefs(ref, presenceRef), "aria-labelledby": labelledBy, "aria-describedby": describedBy, "data-slot": "sheet-content", "data-side": side, "data-state": open ? "open" : "closed", "data-open": open ? "" : undefined, "data-closed": open ? undefined : "", onCancel: (e) => {
|
|
104
|
+
return (_jsxs("dialog", { ...props, ref: mergeRefs(ref, presenceRef), id: contentId, "aria-labelledby": labelledBy, "aria-describedby": describedBy, "data-slot": "sheet-content", "data-side": side, "data-state": open ? "open" : "closed", "data-open": open ? "" : undefined, "data-closed": open ? undefined : "", onCancel: (e) => {
|
|
102
105
|
e.preventDefault();
|
|
103
106
|
setOpen(false);
|
|
104
107
|
}, onClose: () => setOpen(false), onPointerDown: (e) => {
|
package/dist/ui/slider.js
CHANGED
|
@@ -4,6 +4,17 @@ import * as React from "react";
|
|
|
4
4
|
import { cn } from "../shadcn/lib/utils.js";
|
|
5
5
|
import { useControllableState } from "./use-controllable-state.js";
|
|
6
6
|
const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
|
|
7
|
+
// Snap a value to the step grid relative to `min`, then round away the
|
|
8
|
+
// floating-point drift that `Math.round(x / step) * step` introduces (e.g.
|
|
9
|
+
// 0.1 * 3 === 0.30000000000000004) so value/aria-valuenow stay clean.
|
|
10
|
+
const snapToStep = (v, min, step) => {
|
|
11
|
+
if (step <= 0)
|
|
12
|
+
return v;
|
|
13
|
+
const snapped = min + Math.round((v - min) / step) * step;
|
|
14
|
+
// Derive the decimal precision from the step (e.g. 0.1 -> 1, 0.25 -> 2).
|
|
15
|
+
const decimals = (String(step).split(".")[1] ?? "").length;
|
|
16
|
+
return decimals > 0 ? Number(snapped.toFixed(decimals)) : snapped;
|
|
17
|
+
};
|
|
7
18
|
/**
|
|
8
19
|
* Self-contained slider with pointer drag and keyboard control. Supports one or
|
|
9
20
|
* more thumbs (the nearest thumb to the pointer is the one that moves). Track,
|
|
@@ -21,6 +32,12 @@ function Slider({ className, value, defaultValue, onValueChange, min = 0, max =
|
|
|
21
32
|
const trackRef = React.useRef(null);
|
|
22
33
|
const activeThumb = React.useRef(0);
|
|
23
34
|
const vertical = orientation === "vertical";
|
|
35
|
+
// Track the latest values so pointer-move handlers (which close over the
|
|
36
|
+
// pointerdown-time render) read fresh state — otherwise, in controlled mode,
|
|
37
|
+
// a parent correction (e.g. min-gap push between thumbs) gets overwritten on
|
|
38
|
+
// the next move.
|
|
39
|
+
const valuesRef = React.useRef(values);
|
|
40
|
+
valuesRef.current = values;
|
|
24
41
|
const percent = (v) => ((v - min) / (max - min)) * 100;
|
|
25
42
|
const valueFromPointer = (clientX, clientY) => {
|
|
26
43
|
const track = trackRef.current;
|
|
@@ -31,17 +48,28 @@ function Slider({ className, value, defaultValue, onValueChange, min = 0, max =
|
|
|
31
48
|
? (rect.bottom - clientY) / rect.height
|
|
32
49
|
: (clientX - rect.left) / rect.width;
|
|
33
50
|
const raw = min + clamp(ratio, 0, 1) * (max - min);
|
|
34
|
-
return clamp(
|
|
51
|
+
return clamp(snapToStep(raw, min, step), min, max);
|
|
35
52
|
};
|
|
36
53
|
const updateThumb = (index, next) => {
|
|
37
|
-
|
|
54
|
+
const current = valuesRef.current;
|
|
55
|
+
// Clamp the moving thumb against its immediate neighbors so values stay
|
|
56
|
+
// ordered. When neighbors share this thumb's value (overlap), only the
|
|
57
|
+
// actively-dragged thumb is allowed to move away — clamping against the
|
|
58
|
+
// neighbor on the side it's heading lets it pass instead of pinning it.
|
|
59
|
+
const lower = index > 0 ? current[index - 1] : min;
|
|
60
|
+
const upper = index < current.length - 1 ? current[index + 1] : max;
|
|
61
|
+
const clamped = clamp(next, lower, upper);
|
|
62
|
+
setValues(current.map((v, i) => (i === index ? clamped : v)));
|
|
38
63
|
};
|
|
39
64
|
const nearestThumb = (v) => {
|
|
40
65
|
let best = 0;
|
|
41
66
|
let bestDist = Infinity;
|
|
42
|
-
|
|
67
|
+
valuesRef.current.forEach((tv, i) => {
|
|
43
68
|
const d = Math.abs(tv - v);
|
|
44
|
-
|
|
69
|
+
// On exact ties prefer the thumb whose move direction matches the
|
|
70
|
+
// pointer, so overlapping thumbs don't both resolve to the lowest index
|
|
71
|
+
// (which would leave the higher-index thumb unmovable).
|
|
72
|
+
if (d < bestDist || (d === bestDist && v > tv)) {
|
|
45
73
|
bestDist = d;
|
|
46
74
|
best = i;
|
|
47
75
|
}
|
|
@@ -56,27 +84,39 @@ function Slider({ className, value, defaultValue, onValueChange, min = 0, max =
|
|
|
56
84
|
activeThumb.current = idx;
|
|
57
85
|
updateThumb(idx, v);
|
|
58
86
|
const target = e.currentTarget;
|
|
59
|
-
|
|
87
|
+
const pointerId = e.pointerId;
|
|
88
|
+
target.setPointerCapture(pointerId);
|
|
60
89
|
const onMove = (ev) => {
|
|
61
90
|
updateThumb(activeThumb.current, valueFromPointer(ev.clientX, ev.clientY));
|
|
62
91
|
};
|
|
63
|
-
|
|
64
|
-
|
|
92
|
+
// Single teardown for every drag-ending event so a cancelled or lost
|
|
93
|
+
// pointer (pointercancel/lostpointercapture) doesn't leak the move/up
|
|
94
|
+
// listeners and leave the drag stuck.
|
|
95
|
+
const teardown = () => {
|
|
96
|
+
if (target.hasPointerCapture(pointerId)) {
|
|
97
|
+
target.releasePointerCapture(pointerId);
|
|
98
|
+
}
|
|
65
99
|
target.removeEventListener("pointermove", onMove);
|
|
66
|
-
target.removeEventListener("pointerup",
|
|
100
|
+
target.removeEventListener("pointerup", teardown);
|
|
101
|
+
target.removeEventListener("pointercancel", teardown);
|
|
102
|
+
target.removeEventListener("lostpointercapture", teardown);
|
|
67
103
|
};
|
|
68
104
|
target.addEventListener("pointermove", onMove);
|
|
69
|
-
target.addEventListener("pointerup",
|
|
105
|
+
target.addEventListener("pointerup", teardown);
|
|
106
|
+
target.addEventListener("pointercancel", teardown);
|
|
107
|
+
target.addEventListener("lostpointercapture", teardown);
|
|
70
108
|
};
|
|
71
109
|
const onThumbKeyDown = (index) => (e) => {
|
|
72
110
|
if (disabled)
|
|
73
111
|
return;
|
|
74
112
|
let next = null;
|
|
75
113
|
const cur = values[index];
|
|
114
|
+
// Round each step back onto the grid so repeated arrow presses don't
|
|
115
|
+
// accumulate float drift in value/aria-valuenow.
|
|
76
116
|
if (e.key === "ArrowRight" || e.key === "ArrowUp")
|
|
77
|
-
next = cur + step;
|
|
117
|
+
next = snapToStep(cur + step, min, step);
|
|
78
118
|
else if (e.key === "ArrowLeft" || e.key === "ArrowDown")
|
|
79
|
-
next = cur - step;
|
|
119
|
+
next = snapToStep(cur - step, min, step);
|
|
80
120
|
else if (e.key === "Home")
|
|
81
121
|
next = min;
|
|
82
122
|
else if (e.key === "End")
|
package/dist/ui/slot.js
CHANGED
|
@@ -24,7 +24,9 @@ function Slot({ children, className, style, ref, ...props }) {
|
|
|
24
24
|
const childProps = child.props;
|
|
25
25
|
// Child props win on plain values, but event handlers (on*) are COMPOSED so
|
|
26
26
|
// the slot-injected behavior (open/close/etc.) runs alongside the child's own
|
|
27
|
-
// handler — matching Radix
|
|
27
|
+
// handler — matching Radix composeEventHandlers instead of clobbering it. The
|
|
28
|
+
// child's own handler runs FIRST; the forwarded slot handler runs ONLY IF the
|
|
29
|
+
// child hasn't called preventDefault, so a child can opt out of slot behavior.
|
|
28
30
|
const merged = { ...props, ...childProps };
|
|
29
31
|
for (const key of Object.keys(props)) {
|
|
30
32
|
const slotHandler = props[key];
|
|
@@ -34,11 +36,11 @@ function Slot({ children, className, style, ref, ...props }) {
|
|
|
34
36
|
typeof childHandler === "function") {
|
|
35
37
|
merged[key] = (...args) => {
|
|
36
38
|
;
|
|
37
|
-
|
|
39
|
+
childHandler(...args);
|
|
38
40
|
const event = args[0];
|
|
39
41
|
if (!event?.defaultPrevented) {
|
|
40
42
|
;
|
|
41
|
-
|
|
43
|
+
slotHandler(...args);
|
|
42
44
|
}
|
|
43
45
|
};
|
|
44
46
|
}
|
package/dist/ui/tabs.js
CHANGED
|
@@ -70,18 +70,25 @@ function TabsTrigger({ className, value, onClick, ref, ...props }) {
|
|
|
70
70
|
const { value: active, setValue, idBase } = useTabs();
|
|
71
71
|
const selected = active === value;
|
|
72
72
|
const btnRef = React.useRef(null);
|
|
73
|
-
// Roving tab index: the selected tab is tabbable. When
|
|
74
|
-
//
|
|
73
|
+
// Roving tab index: the selected tab is tabbable. When no trigger matches the
|
|
74
|
+
// active value — undefined, or a value with no rendered trigger — the first
|
|
75
|
+
// enabled tab must still be reachable so the tablist stays in Tab order
|
|
75
76
|
// (WAI-ARIA: a tablist always has exactly one tab stop).
|
|
76
|
-
const [
|
|
77
|
+
const [isFallbackStop, setIsFallbackStop] = React.useState(false);
|
|
77
78
|
React.useLayoutEffect(() => {
|
|
78
|
-
if (active !== undefined)
|
|
79
|
-
return;
|
|
80
79
|
const el = btnRef.current;
|
|
81
|
-
const
|
|
82
|
-
|
|
80
|
+
const list = el?.parentElement;
|
|
81
|
+
if (!list)
|
|
82
|
+
return;
|
|
83
|
+
const anySelected = list.querySelector('[role="tab"][aria-selected="true"]');
|
|
84
|
+
if (anySelected) {
|
|
85
|
+
setIsFallbackStop(false);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const first = list.querySelector('[role="tab"]:not([disabled]):not([data-disabled])');
|
|
89
|
+
setIsFallbackStop(first === el);
|
|
83
90
|
});
|
|
84
|
-
const tabbable = selected ||
|
|
91
|
+
const tabbable = selected || isFallbackStop;
|
|
85
92
|
return (_jsx("button", { ref: mergeRefs(btnRef, ref), type: "button", role: "tab", id: `${idBase}-tab-${idify(value)}`, "aria-selected": selected, "aria-controls": `${idBase}-panel-${idify(value)}`, tabIndex: tabbable ? 0 : -1, "data-slot": "tabs-trigger", "data-active": selected ? "" : undefined, className: cn("cursor-pointer relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-disabled:pointer-events-none aria-disabled:opacity-50 dark:text-muted-foreground dark:hover:text-foreground group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent", "data-active:bg-background data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 dark:data-active:text-foreground", "after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100", className), onClick: (e) => {
|
|
86
93
|
onClick?.(e);
|
|
87
94
|
if (!e.defaultPrevented)
|
package/dist/ui/use-dismiss.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import * as React from "react";
|
|
3
|
+
import { isInLayer } from "./layer-stack.js";
|
|
3
4
|
// Shared stack of open dismissable layers (most-recently-opened last). Escape
|
|
4
5
|
// only closes the topmost layer, so a popover opened inside a dialog doesn't
|
|
5
6
|
// close both at once.
|
|
@@ -35,6 +36,8 @@ export function useDismiss(open, onDismiss, refs) {
|
|
|
35
36
|
return;
|
|
36
37
|
if (refs.some((r) => r.current && r.current.contains(target)))
|
|
37
38
|
return;
|
|
39
|
+
if (isInLayer(target))
|
|
40
|
+
return;
|
|
38
41
|
onDismissRef.current();
|
|
39
42
|
};
|
|
40
43
|
document.addEventListener("keydown", onKeyDown, true);
|