@weichenlin/swell-ui 0.0.0-stage → 0.2.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/README.md +287 -2
- package/dist/index.d.ts +659 -0
- package/dist/index.js +3453 -0
- package/dist/styles.css +6 -0
- package/dist/theme.css +207 -0
- package/dist/themes/warm.css +63 -0
- package/dist/typography.css +12 -0
- package/package.json +74 -3
package/dist/index.js
ADDED
|
@@ -0,0 +1,3453 @@
|
|
|
1
|
+
import { Check, ChevronDown, CircleAlert, CircleCheck, CircleDashed, CircleX, Copy, Eye, EyeOff, Info, LoaderCircle, Menu, Moon, Search, Sun, TriangleAlert, X } from "lucide-react";
|
|
2
|
+
import { clsx } from "clsx";
|
|
3
|
+
import { extendTailwindMerge } from "tailwind-merge";
|
|
4
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
import { cva } from "class-variance-authority";
|
|
6
|
+
import { Children, cloneElement, createContext, isValidElement, useCallback, useContext, useEffect, useId, useInsertionEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
7
|
+
import { createPortal } from "react-dom";
|
|
8
|
+
//#region src/lib/cn.ts
|
|
9
|
+
const tone = (name) => [
|
|
10
|
+
name,
|
|
11
|
+
`${name}-fg`,
|
|
12
|
+
`${name}-subtle-bg`,
|
|
13
|
+
`${name}-subtle-fg`,
|
|
14
|
+
`${name}-border`
|
|
15
|
+
];
|
|
16
|
+
const twMerge = extendTailwindMerge({ extend: { theme: {
|
|
17
|
+
color: [
|
|
18
|
+
"bg",
|
|
19
|
+
"surface",
|
|
20
|
+
"surface-raised",
|
|
21
|
+
"surface-muted",
|
|
22
|
+
"fg",
|
|
23
|
+
"fg-strong",
|
|
24
|
+
"fg-muted",
|
|
25
|
+
"fg-subtle",
|
|
26
|
+
"border",
|
|
27
|
+
"border-strong",
|
|
28
|
+
"ring",
|
|
29
|
+
"ring-deep",
|
|
30
|
+
"primary",
|
|
31
|
+
"primary-fg",
|
|
32
|
+
"accent",
|
|
33
|
+
"inverse",
|
|
34
|
+
"inverse-fg",
|
|
35
|
+
"focus",
|
|
36
|
+
"input-bg",
|
|
37
|
+
"input-bg-hover",
|
|
38
|
+
"input-border",
|
|
39
|
+
"input-border-hover",
|
|
40
|
+
"input-placeholder",
|
|
41
|
+
"input-focus",
|
|
42
|
+
"input-highlight",
|
|
43
|
+
"input-halo",
|
|
44
|
+
"input-halo-invalid",
|
|
45
|
+
"input-halo-invalid-focus",
|
|
46
|
+
"control-checked",
|
|
47
|
+
"control-checked-fg",
|
|
48
|
+
"popover-bg",
|
|
49
|
+
"popover-border",
|
|
50
|
+
"popover-highlight",
|
|
51
|
+
"dialog-backdrop",
|
|
52
|
+
...[
|
|
53
|
+
"success",
|
|
54
|
+
"warning",
|
|
55
|
+
"danger",
|
|
56
|
+
"info",
|
|
57
|
+
"neutral"
|
|
58
|
+
].flatMap(tone)
|
|
59
|
+
],
|
|
60
|
+
shadow: [
|
|
61
|
+
"input",
|
|
62
|
+
"input-lift",
|
|
63
|
+
"switch-thumb",
|
|
64
|
+
"popover",
|
|
65
|
+
"dialog",
|
|
66
|
+
"whisper"
|
|
67
|
+
],
|
|
68
|
+
container: ["page"],
|
|
69
|
+
font: [
|
|
70
|
+
"sans",
|
|
71
|
+
"serif",
|
|
72
|
+
"mono"
|
|
73
|
+
]
|
|
74
|
+
} } });
|
|
75
|
+
function cn(...inputs) {
|
|
76
|
+
return twMerge(clsx(inputs));
|
|
77
|
+
}
|
|
78
|
+
//#endregion
|
|
79
|
+
//#region src/components/badge/badge.tsx
|
|
80
|
+
const toneClasses$1 = {
|
|
81
|
+
neutral: {
|
|
82
|
+
subtle: ["border-neutral-border bg-neutral-subtle-bg text-neutral-subtle-fg", "text-neutral"],
|
|
83
|
+
outline: ["border-neutral-border bg-transparent text-neutral-subtle-fg", "text-neutral"],
|
|
84
|
+
solid: ["border-neutral bg-neutral text-neutral-fg", "text-neutral-fg"]
|
|
85
|
+
},
|
|
86
|
+
info: {
|
|
87
|
+
subtle: ["border-info-border bg-info-subtle-bg text-info-subtle-fg", "text-info"],
|
|
88
|
+
outline: ["border-info-border bg-transparent text-info-subtle-fg", "text-info"],
|
|
89
|
+
solid: ["border-info bg-info text-info-fg", "text-info-fg"]
|
|
90
|
+
},
|
|
91
|
+
success: {
|
|
92
|
+
subtle: ["border-success-border bg-success-subtle-bg text-success-subtle-fg", "text-success"],
|
|
93
|
+
outline: ["border-success-border bg-transparent text-success-subtle-fg", "text-success"],
|
|
94
|
+
solid: ["border-success bg-success text-success-fg", "text-success-fg"]
|
|
95
|
+
},
|
|
96
|
+
warning: {
|
|
97
|
+
subtle: ["border-warning-border bg-warning-subtle-bg text-warning-subtle-fg", "text-warning"],
|
|
98
|
+
outline: ["border-warning-border bg-transparent text-warning-subtle-fg", "text-warning"],
|
|
99
|
+
solid: ["border-warning bg-warning text-warning-fg", "text-warning-fg"]
|
|
100
|
+
},
|
|
101
|
+
danger: {
|
|
102
|
+
subtle: ["border-danger-border bg-danger-subtle-bg text-danger-subtle-fg", "text-danger"],
|
|
103
|
+
outline: ["border-danger-border bg-transparent text-danger-subtle-fg", "text-danger"],
|
|
104
|
+
solid: ["border-danger bg-danger text-danger-fg", "text-danger-fg"]
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
const sizeClasses$3 = {
|
|
108
|
+
sm: {
|
|
109
|
+
base: "h-5 px-1.5",
|
|
110
|
+
withIcon: "ps-1"
|
|
111
|
+
},
|
|
112
|
+
md: {
|
|
113
|
+
base: "h-6 px-2",
|
|
114
|
+
withIcon: "ps-1.5"
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
function Badge({ tone = "neutral", variant = "subtle", size = "md", icon, busy = false, context, className, children, ...props }) {
|
|
118
|
+
const [toneClass, iconClass] = toneClasses$1[tone][variant];
|
|
119
|
+
const hasIcon = busy || icon !== void 0 && icon !== null && icon !== false;
|
|
120
|
+
return /* @__PURE__ */ jsxs("span", {
|
|
121
|
+
...props,
|
|
122
|
+
"data-slot": "badge",
|
|
123
|
+
className: cn("inline-flex items-center gap-1 rounded-md border text-xs leading-none font-medium whitespace-nowrap", "forced-colors:border-[CanvasText]", sizeClasses$3[size].base, hasIcon && sizeClasses$3[size].withIcon, toneClass, className),
|
|
124
|
+
children: [hasIcon && /* @__PURE__ */ jsx("span", {
|
|
125
|
+
"aria-hidden": "true",
|
|
126
|
+
"data-slot": "badge-icon",
|
|
127
|
+
className: cn("inline-flex shrink-0 items-center [&_svg]:size-3 [&_svg]:shrink-0", iconClass),
|
|
128
|
+
children: busy ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(LoaderCircle, { className: "animate-spin motion-reduce:hidden" }), /* @__PURE__ */ jsx(CircleDashed, { className: "hidden motion-reduce:block" })] }) : icon
|
|
129
|
+
}), /* @__PURE__ */ jsxs("span", {
|
|
130
|
+
"data-slot": "badge-label",
|
|
131
|
+
className: "min-w-0 truncate leading-4",
|
|
132
|
+
children: [context && /* @__PURE__ */ jsxs("span", {
|
|
133
|
+
className: "sr-only",
|
|
134
|
+
children: [context, ": "]
|
|
135
|
+
}), children]
|
|
136
|
+
})]
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
//#endregion
|
|
140
|
+
//#region src/hooks/use-press-feedback.ts
|
|
141
|
+
const GROW_MS = 450;
|
|
142
|
+
const FADE_MS = 300;
|
|
143
|
+
const OPACITY = .16;
|
|
144
|
+
const RELEASE_EVENTS = [
|
|
145
|
+
"pointerup",
|
|
146
|
+
"pointerleave",
|
|
147
|
+
"pointercancel",
|
|
148
|
+
"keyup",
|
|
149
|
+
"blur"
|
|
150
|
+
];
|
|
151
|
+
/**
|
|
152
|
+
* Press feedback for buttons and button-styled links: toggles `data-pressed`
|
|
153
|
+
* on the host (styled as a scale) and, when `ripple` is on, spawns a
|
|
154
|
+
* Material-style ripple via the Web Animations API.
|
|
155
|
+
*
|
|
156
|
+
* Attach the returned ref to an absolutely positioned, clipped overlay inside
|
|
157
|
+
* the host. Listeners are bound natively to the overlay's parent so the host's
|
|
158
|
+
* own props/ref stay untouched. Hosts with aria-disabled="true" get no feedback.
|
|
159
|
+
*/
|
|
160
|
+
function usePressFeedback(ripple = true) {
|
|
161
|
+
const ref = useRef(null);
|
|
162
|
+
useEffect(() => {
|
|
163
|
+
const layer = ref.current;
|
|
164
|
+
const host = layer?.parentElement;
|
|
165
|
+
if (!layer || !host) return;
|
|
166
|
+
const live = /* @__PURE__ */ new Set();
|
|
167
|
+
const activationKeys = host instanceof HTMLAnchorElement ? ["Enter"] : ["Enter", " "];
|
|
168
|
+
const isInert = () => host.getAttribute("aria-disabled") === "true";
|
|
169
|
+
const canRipple = () => ripple && typeof layer.animate === "function" && !(typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches);
|
|
170
|
+
const spawn = (x, y, rect) => {
|
|
171
|
+
const r = Math.hypot(Math.max(x, rect.width - x), Math.max(y, rect.height - y));
|
|
172
|
+
const el = document.createElement("span");
|
|
173
|
+
el.style.cssText = `position:absolute;left:${x - r}px;top:${y - r}px;width:${r * 2}px;height:${r * 2}px;border-radius:50%;background:currentColor;opacity:${OPACITY};pointer-events:none`;
|
|
174
|
+
layer.append(el);
|
|
175
|
+
const grow = el.animate([{ transform: "scale(0)" }, { transform: "scale(1)" }], {
|
|
176
|
+
duration: GROW_MS,
|
|
177
|
+
easing: "cubic-bezier(0.2, 0, 0, 1)",
|
|
178
|
+
fill: "forwards"
|
|
179
|
+
});
|
|
180
|
+
live.add({
|
|
181
|
+
el,
|
|
182
|
+
grow
|
|
183
|
+
});
|
|
184
|
+
};
|
|
185
|
+
const press = (x, y) => {
|
|
186
|
+
host.dataset.pressed = "";
|
|
187
|
+
if (!canRipple()) return;
|
|
188
|
+
const rect = layer.getBoundingClientRect();
|
|
189
|
+
spawn(x ?? rect.width / 2, y ?? rect.height / 2, rect);
|
|
190
|
+
};
|
|
191
|
+
const release = () => {
|
|
192
|
+
delete host.dataset.pressed;
|
|
193
|
+
for (const r of live) {
|
|
194
|
+
live.delete(r);
|
|
195
|
+
r.grow.finished.catch(() => {}).then(() => r.el.animate([{ opacity: 0 }], {
|
|
196
|
+
duration: FADE_MS,
|
|
197
|
+
easing: "linear",
|
|
198
|
+
fill: "forwards"
|
|
199
|
+
}).finished).catch(() => {}).finally(() => r.el.remove());
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
const onPointerDown = (e) => {
|
|
203
|
+
if (e.button !== 0 || e.isPrimary === false || isInert()) return;
|
|
204
|
+
const rect = layer.getBoundingClientRect();
|
|
205
|
+
press(e.clientX - rect.left, e.clientY - rect.top);
|
|
206
|
+
};
|
|
207
|
+
const onKeyDown = (e) => {
|
|
208
|
+
if (e.repeat || !activationKeys.includes(e.key) || isInert()) return;
|
|
209
|
+
press();
|
|
210
|
+
};
|
|
211
|
+
host.addEventListener("pointerdown", onPointerDown);
|
|
212
|
+
host.addEventListener("keydown", onKeyDown);
|
|
213
|
+
for (const type of RELEASE_EVENTS) host.addEventListener(type, release);
|
|
214
|
+
return () => {
|
|
215
|
+
host.removeEventListener("pointerdown", onPointerDown);
|
|
216
|
+
host.removeEventListener("keydown", onKeyDown);
|
|
217
|
+
for (const type of RELEASE_EVENTS) host.removeEventListener(type, release);
|
|
218
|
+
release();
|
|
219
|
+
};
|
|
220
|
+
}, [ripple]);
|
|
221
|
+
return ref;
|
|
222
|
+
}
|
|
223
|
+
//#endregion
|
|
224
|
+
//#region src/components/tooltip.tsx
|
|
225
|
+
const OPEN_DELAY_MS = 300;
|
|
226
|
+
const GAP_PX = 6;
|
|
227
|
+
const VIEWPORT_PADDING_PX = 8;
|
|
228
|
+
const opposite = {
|
|
229
|
+
top: "bottom",
|
|
230
|
+
bottom: "top",
|
|
231
|
+
left: "right",
|
|
232
|
+
right: "left"
|
|
233
|
+
};
|
|
234
|
+
function isFocusVisible$1(el) {
|
|
235
|
+
try {
|
|
236
|
+
return el.matches(":focus-visible");
|
|
237
|
+
} catch {
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
function clamp(value, min, max) {
|
|
242
|
+
return Math.max(min, Math.min(value, Math.max(min, max)));
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Bubble placement in viewport coordinates. left/top are for the outer box, which
|
|
246
|
+
* includes the GAP_PX padding facing the trigger, so the box always touches the trigger.
|
|
247
|
+
*/
|
|
248
|
+
function place(anchor, width, height, preferred) {
|
|
249
|
+
const vw = document.documentElement.clientWidth;
|
|
250
|
+
const vh = document.documentElement.clientHeight;
|
|
251
|
+
const room = {
|
|
252
|
+
top: anchor.top,
|
|
253
|
+
bottom: vh - anchor.bottom,
|
|
254
|
+
left: anchor.left,
|
|
255
|
+
right: vw - anchor.right
|
|
256
|
+
};
|
|
257
|
+
const need = (s) => (s === "top" || s === "bottom" ? height : width) + GAP_PX + VIEWPORT_PADDING_PX;
|
|
258
|
+
const side = room[preferred] < need(preferred) && room[opposite[preferred]] > room[preferred] ? opposite[preferred] : preferred;
|
|
259
|
+
if (side === "top" || side === "bottom") return {
|
|
260
|
+
side,
|
|
261
|
+
left: clamp(anchor.left + anchor.width / 2 - width / 2, VIEWPORT_PADDING_PX, vw - width - VIEWPORT_PADDING_PX),
|
|
262
|
+
top: side === "top" ? anchor.top - height - GAP_PX : anchor.bottom
|
|
263
|
+
};
|
|
264
|
+
return {
|
|
265
|
+
side,
|
|
266
|
+
top: clamp(anchor.top + anchor.height / 2 - height / 2, VIEWPORT_PADDING_PX, vh - height - VIEWPORT_PADDING_PX),
|
|
267
|
+
left: side === "left" ? anchor.left - width - GAP_PX : anchor.right
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Hover/focus tooltip around any element. The bubble is portaled to `<body>` with
|
|
272
|
+
* fixed positioning, so scroll containers (tables, drawers) never clip it.
|
|
273
|
+
* Events are read on the wrapper, so aria-disabled triggers work; React still
|
|
274
|
+
* treats the portaled bubble as inside the wrapper, so the pointer can rest on it.
|
|
275
|
+
*/
|
|
276
|
+
function Tooltip({ content, side = "top", describe = true, className, children }) {
|
|
277
|
+
const id = useId();
|
|
278
|
+
const [open, setOpen] = useState(false);
|
|
279
|
+
const [position, setPosition] = useState();
|
|
280
|
+
const anchorRef = useRef(null);
|
|
281
|
+
const bubbleRef = useRef(null);
|
|
282
|
+
const timer = useRef(void 0);
|
|
283
|
+
const active = content != null && content !== false && content !== "";
|
|
284
|
+
const clearTimer = () => clearTimeout(timer.current);
|
|
285
|
+
const show = () => {
|
|
286
|
+
clearTimer();
|
|
287
|
+
setOpen(true);
|
|
288
|
+
};
|
|
289
|
+
const hide = () => {
|
|
290
|
+
clearTimer();
|
|
291
|
+
setOpen(false);
|
|
292
|
+
};
|
|
293
|
+
useEffect(() => () => clearTimeout(timer.current), []);
|
|
294
|
+
const [wasActive, setWasActive] = useState(active);
|
|
295
|
+
if (active !== wasActive) {
|
|
296
|
+
setWasActive(active);
|
|
297
|
+
if (!active) setOpen(false);
|
|
298
|
+
}
|
|
299
|
+
useEffect(() => {
|
|
300
|
+
if (!open) return;
|
|
301
|
+
const onKeyDown = (e) => {
|
|
302
|
+
if (e.key === "Escape") setOpen(false);
|
|
303
|
+
};
|
|
304
|
+
document.addEventListener("keydown", onKeyDown);
|
|
305
|
+
return () => document.removeEventListener("keydown", onKeyDown);
|
|
306
|
+
}, [open]);
|
|
307
|
+
useLayoutEffect(() => {
|
|
308
|
+
if (!open) return;
|
|
309
|
+
const update = () => {
|
|
310
|
+
const anchor = anchorRef.current;
|
|
311
|
+
const bubble = bubbleRef.current;
|
|
312
|
+
if (!anchor || !bubble) return;
|
|
313
|
+
const next = place(anchor.getBoundingClientRect(), bubble.offsetWidth, bubble.offsetHeight, side);
|
|
314
|
+
setPosition({
|
|
315
|
+
...next,
|
|
316
|
+
left: Math.round(next.left),
|
|
317
|
+
top: Math.round(next.top)
|
|
318
|
+
});
|
|
319
|
+
};
|
|
320
|
+
update();
|
|
321
|
+
window.addEventListener("scroll", update, true);
|
|
322
|
+
window.addEventListener("resize", update);
|
|
323
|
+
return () => {
|
|
324
|
+
window.removeEventListener("scroll", update, true);
|
|
325
|
+
window.removeEventListener("resize", update);
|
|
326
|
+
};
|
|
327
|
+
}, [
|
|
328
|
+
open,
|
|
329
|
+
side,
|
|
330
|
+
content
|
|
331
|
+
]);
|
|
332
|
+
const describedBy = active && describe ? [children.props["aria-describedby"], id].filter(Boolean).join(" ") : children.props["aria-describedby"];
|
|
333
|
+
const placed = position?.side ?? side;
|
|
334
|
+
return /* @__PURE__ */ jsxs("span", {
|
|
335
|
+
ref: anchorRef,
|
|
336
|
+
className: cn("relative inline-flex", className),
|
|
337
|
+
onPointerEnter: () => {
|
|
338
|
+
if (!active) return;
|
|
339
|
+
clearTimer();
|
|
340
|
+
timer.current = setTimeout(() => setOpen(true), OPEN_DELAY_MS);
|
|
341
|
+
},
|
|
342
|
+
onPointerLeave: hide,
|
|
343
|
+
onPointerDown: hide,
|
|
344
|
+
onFocus: (e) => {
|
|
345
|
+
if (active && isFocusVisible$1(e.target)) show();
|
|
346
|
+
},
|
|
347
|
+
onBlur: (e) => {
|
|
348
|
+
if (!e.currentTarget.contains(e.relatedTarget)) hide();
|
|
349
|
+
},
|
|
350
|
+
children: [cloneElement(children, { "aria-describedby": describedBy }), active && createPortal(/* @__PURE__ */ jsx("span", {
|
|
351
|
+
id,
|
|
352
|
+
role: "tooltip",
|
|
353
|
+
"aria-hidden": describe ? void 0 : true,
|
|
354
|
+
hidden: !open,
|
|
355
|
+
"data-side": placed,
|
|
356
|
+
style: position && {
|
|
357
|
+
left: position.left,
|
|
358
|
+
top: position.top
|
|
359
|
+
},
|
|
360
|
+
className: cn("fixed top-0 left-0 z-50", {
|
|
361
|
+
top: "pb-1.5",
|
|
362
|
+
bottom: "pt-1.5",
|
|
363
|
+
left: "pr-1.5",
|
|
364
|
+
right: "pl-1.5"
|
|
365
|
+
}[placed]),
|
|
366
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
367
|
+
ref: bubbleRef,
|
|
368
|
+
className: cn("block w-max max-w-xs rounded-md border border-border-strong bg-inverse px-2 py-1", "text-xs/snug font-medium text-inverse-fg shadow-whisper", "transition-[opacity,translate] duration-150 ease-out starting:opacity-0", {
|
|
369
|
+
top: "starting:translate-y-0.5",
|
|
370
|
+
bottom: "starting:-translate-y-0.5",
|
|
371
|
+
left: "starting:translate-x-0.5",
|
|
372
|
+
right: "starting:-translate-x-0.5"
|
|
373
|
+
}[placed], "motion-reduce:transition-none"),
|
|
374
|
+
children: content
|
|
375
|
+
})
|
|
376
|
+
}), document.body)]
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
//#endregion
|
|
380
|
+
//#region src/components/button/button.tsx
|
|
381
|
+
const buttonVariants = cva([
|
|
382
|
+
"relative inline-flex cursor-pointer items-center justify-center font-medium whitespace-nowrap",
|
|
383
|
+
"touch-manipulation select-none [-webkit-tap-highlight-color:transparent]",
|
|
384
|
+
"transition-[color,background-color,box-shadow,opacity,scale] duration-150 ease-out",
|
|
385
|
+
"data-pressed:scale-[0.97]",
|
|
386
|
+
"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus",
|
|
387
|
+
"after:pointer-events-none after:absolute after:inset-0 after:rounded-[inherit] after:border after:border-transparent",
|
|
388
|
+
"forced-colors:after:border-[ButtonText]",
|
|
389
|
+
"pointer-coarse:before:absolute pointer-coarse:before:top-1/2 pointer-coarse:before:left-1/2",
|
|
390
|
+
"pointer-coarse:before:size-full pointer-coarse:before:min-h-11 pointer-coarse:before:min-w-11",
|
|
391
|
+
"pointer-coarse:before:-translate-1/2"
|
|
392
|
+
], {
|
|
393
|
+
variants: {
|
|
394
|
+
intent: {
|
|
395
|
+
primary: "bg-primary text-primary-fg rounded-lg shadow-[0_0_0_1px_var(--color-primary)] hover:not-aria-disabled:opacity-90",
|
|
396
|
+
secondary: "bg-surface-muted text-fg-strong rounded-lg shadow-[0_0_0_1px_var(--color-ring)] hover:not-aria-disabled:bg-ring",
|
|
397
|
+
surface: "bg-surface-raised text-fg rounded-xl hover:not-aria-disabled:bg-surface-muted",
|
|
398
|
+
dark: "bg-inverse text-inverse-fg rounded-lg shadow-[0_0_0_1px_var(--color-ring)] hover:not-aria-disabled:opacity-90",
|
|
399
|
+
ghost: "bg-transparent text-fg rounded-lg hover:not-aria-disabled:bg-surface-muted"
|
|
400
|
+
},
|
|
401
|
+
size: {
|
|
402
|
+
sm: "h-8.5 px-3 text-sm",
|
|
403
|
+
md: "h-9.5 px-4 text-sm",
|
|
404
|
+
lg: "h-10.5 px-5 text-base"
|
|
405
|
+
},
|
|
406
|
+
tone: {
|
|
407
|
+
default: "",
|
|
408
|
+
danger: ""
|
|
409
|
+
},
|
|
410
|
+
fullWidth: {
|
|
411
|
+
true: "w-full",
|
|
412
|
+
false: ""
|
|
413
|
+
},
|
|
414
|
+
square: {
|
|
415
|
+
true: "aspect-square px-0",
|
|
416
|
+
false: ""
|
|
417
|
+
}
|
|
418
|
+
},
|
|
419
|
+
compoundVariants: [
|
|
420
|
+
{
|
|
421
|
+
intent: "primary",
|
|
422
|
+
tone: "danger",
|
|
423
|
+
class: "bg-danger text-danger-fg shadow-[0_0_0_1px_var(--color-danger)]"
|
|
424
|
+
},
|
|
425
|
+
{
|
|
426
|
+
intent: "secondary",
|
|
427
|
+
tone: "danger",
|
|
428
|
+
class: "text-danger shadow-[0_0_0_1px_var(--color-danger)] hover:not-aria-disabled:bg-danger hover:not-aria-disabled:text-danger-fg"
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
intent: "surface",
|
|
432
|
+
tone: "danger",
|
|
433
|
+
class: "text-danger hover:not-aria-disabled:bg-danger hover:not-aria-disabled:text-danger-fg"
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
intent: "dark",
|
|
437
|
+
tone: "danger",
|
|
438
|
+
class: "bg-danger text-danger-fg shadow-[0_0_0_1px_var(--color-danger)]"
|
|
439
|
+
},
|
|
440
|
+
{
|
|
441
|
+
intent: "ghost",
|
|
442
|
+
tone: "danger",
|
|
443
|
+
class: "text-danger hover:not-aria-disabled:bg-danger/10"
|
|
444
|
+
}
|
|
445
|
+
],
|
|
446
|
+
defaultVariants: {
|
|
447
|
+
intent: "primary",
|
|
448
|
+
size: "md",
|
|
449
|
+
tone: "default",
|
|
450
|
+
fullWidth: false,
|
|
451
|
+
square: false
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
const spinnerSize = {
|
|
455
|
+
sm: 16,
|
|
456
|
+
md: 18,
|
|
457
|
+
lg: 20
|
|
458
|
+
};
|
|
459
|
+
const iconSize$1 = {
|
|
460
|
+
sm: "[&_svg]:size-4",
|
|
461
|
+
md: "[&_svg]:size-4",
|
|
462
|
+
lg: "[&_svg]:size-[18px]"
|
|
463
|
+
};
|
|
464
|
+
const iconStartPadding$1 = {
|
|
465
|
+
sm: "ps-2",
|
|
466
|
+
md: "ps-3",
|
|
467
|
+
lg: "ps-4"
|
|
468
|
+
};
|
|
469
|
+
const iconEndPadding = {
|
|
470
|
+
sm: "pe-2",
|
|
471
|
+
md: "pe-3",
|
|
472
|
+
lg: "pe-4"
|
|
473
|
+
};
|
|
474
|
+
/** Press layer + content + spinner; shared by every button-styled host. */
|
|
475
|
+
function ButtonContent({ size, ripple, loading = false, iconLeft, iconRight, children }) {
|
|
476
|
+
const layerRef = usePressFeedback(ripple);
|
|
477
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
478
|
+
/* @__PURE__ */ jsx("span", {
|
|
479
|
+
ref: layerRef,
|
|
480
|
+
"data-ripple": ripple ? "" : void 0,
|
|
481
|
+
"aria-hidden": true,
|
|
482
|
+
className: "pointer-events-none absolute inset-0 overflow-hidden rounded-[inherit] forced-colors:hidden"
|
|
483
|
+
}),
|
|
484
|
+
/* @__PURE__ */ jsxs("span", {
|
|
485
|
+
className: cn("relative inline-flex items-center gap-2 [&_svg]:shrink-0", iconSize$1[size], loading && "opacity-0"),
|
|
486
|
+
children: [
|
|
487
|
+
iconLeft && /* @__PURE__ */ jsx("span", {
|
|
488
|
+
"aria-hidden": true,
|
|
489
|
+
className: "inline-flex",
|
|
490
|
+
children: iconLeft
|
|
491
|
+
}),
|
|
492
|
+
children,
|
|
493
|
+
iconRight && /* @__PURE__ */ jsx("span", {
|
|
494
|
+
"aria-hidden": true,
|
|
495
|
+
className: "inline-flex",
|
|
496
|
+
children: iconRight
|
|
497
|
+
})
|
|
498
|
+
]
|
|
499
|
+
}),
|
|
500
|
+
loading && /* @__PURE__ */ jsx("span", {
|
|
501
|
+
className: "absolute inset-0 flex items-center justify-center",
|
|
502
|
+
children: /* @__PURE__ */ jsx(LoaderCircle, {
|
|
503
|
+
className: "animate-spin forced-colors:text-[ButtonText]",
|
|
504
|
+
size: spinnerSize[size],
|
|
505
|
+
"aria-hidden": true
|
|
506
|
+
})
|
|
507
|
+
})
|
|
508
|
+
] });
|
|
509
|
+
}
|
|
510
|
+
function iconPadding(size, iconLeft, iconRight) {
|
|
511
|
+
return [iconLeft ? iconStartPadding$1[size] : "", iconRight ? iconEndPadding[size] : ""];
|
|
512
|
+
}
|
|
513
|
+
function stateClass(disabled, loading) {
|
|
514
|
+
if (loading) return "cursor-progress opacity-70";
|
|
515
|
+
if (disabled) return "cursor-not-allowed opacity-50 forced-colors:opacity-100 forced-colors:text-[GrayText] forced-colors:after:border-[GrayText]";
|
|
516
|
+
}
|
|
517
|
+
function ButtonImpl({ className, intent, size, tone, fullWidth, square, loading = false, loadingText = "Loading", disabled = false, iconLeft, iconRight, ripple, tooltip, type = "button", onClick, children, ...props }) {
|
|
518
|
+
const s = size ?? "md";
|
|
519
|
+
const inert = disabled || loading;
|
|
520
|
+
const button = /* @__PURE__ */ jsx("button", {
|
|
521
|
+
...props,
|
|
522
|
+
type,
|
|
523
|
+
"data-slot": "button",
|
|
524
|
+
"aria-disabled": inert || void 0,
|
|
525
|
+
"aria-busy": loading || void 0,
|
|
526
|
+
className: cn(buttonVariants({
|
|
527
|
+
intent,
|
|
528
|
+
size,
|
|
529
|
+
tone,
|
|
530
|
+
fullWidth,
|
|
531
|
+
square
|
|
532
|
+
}), iconPadding(s, iconLeft, iconRight), stateClass(disabled, loading), className),
|
|
533
|
+
onClick: (e) => {
|
|
534
|
+
if (inert) {
|
|
535
|
+
e.preventDefault();
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
onClick?.(e);
|
|
539
|
+
},
|
|
540
|
+
children: /* @__PURE__ */ jsx(ButtonContent, {
|
|
541
|
+
size: s,
|
|
542
|
+
ripple: ripple ?? true,
|
|
543
|
+
loading,
|
|
544
|
+
iconLeft,
|
|
545
|
+
iconRight,
|
|
546
|
+
children
|
|
547
|
+
})
|
|
548
|
+
});
|
|
549
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [tooltip ? /* @__PURE__ */ jsx(Tooltip, {
|
|
550
|
+
...tooltip,
|
|
551
|
+
className: fullWidth ? "flex w-full" : void 0,
|
|
552
|
+
children: button
|
|
553
|
+
}) : button, /* @__PURE__ */ jsx("span", {
|
|
554
|
+
"aria-live": "polite",
|
|
555
|
+
"aria-atomic": true,
|
|
556
|
+
className: "sr-only",
|
|
557
|
+
children: loading ? loadingText : ""
|
|
558
|
+
})] });
|
|
559
|
+
}
|
|
560
|
+
function IconOnlyButton({ icon, tooltip = true, tooltipSide, disabledReason, className, size, ...props }) {
|
|
561
|
+
const reason = props.disabled ? disabledReason : void 0;
|
|
562
|
+
const tip = reason ? {
|
|
563
|
+
content: reason,
|
|
564
|
+
side: tooltipSide
|
|
565
|
+
} : tooltip ? {
|
|
566
|
+
content: props["aria-label"],
|
|
567
|
+
side: tooltipSide,
|
|
568
|
+
describe: false
|
|
569
|
+
} : disabledReason !== void 0 ? { content: void 0 } : void 0;
|
|
570
|
+
return /* @__PURE__ */ jsx(ButtonImpl, {
|
|
571
|
+
...props,
|
|
572
|
+
size,
|
|
573
|
+
tooltip: tip,
|
|
574
|
+
square: true,
|
|
575
|
+
className,
|
|
576
|
+
children: icon
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
function Button(props) {
|
|
580
|
+
if (props.icon !== void 0) return /* @__PURE__ */ jsx(IconOnlyButton, { ...props });
|
|
581
|
+
const { disabledReason, ...rest } = props;
|
|
582
|
+
const tooltip = disabledReason === void 0 ? void 0 : { content: rest.disabled ? disabledReason : void 0 };
|
|
583
|
+
return /* @__PURE__ */ jsx(ButtonImpl, {
|
|
584
|
+
...rest,
|
|
585
|
+
tooltip
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
//#endregion
|
|
589
|
+
//#region src/components/card/card.tsx
|
|
590
|
+
const CardContext = createContext({
|
|
591
|
+
titleId: "",
|
|
592
|
+
setTitled: () => {}
|
|
593
|
+
});
|
|
594
|
+
/**
|
|
595
|
+
* Bordered surface for a group of related content. No padding of its own: CardHeader and
|
|
596
|
+
* CardContent add it, so full-bleed rows (lists, tables) can sit edge to edge.
|
|
597
|
+
*/
|
|
598
|
+
function Card({ as: Comp = "div", className, ...props }) {
|
|
599
|
+
const titleId = useId();
|
|
600
|
+
const [titled, setTitled] = useState(false);
|
|
601
|
+
return /* @__PURE__ */ jsx(CardContext, {
|
|
602
|
+
value: {
|
|
603
|
+
titleId,
|
|
604
|
+
setTitled
|
|
605
|
+
},
|
|
606
|
+
children: /* @__PURE__ */ jsx(Comp, {
|
|
607
|
+
"aria-labelledby": (Comp === "section" || Comp === "article") && titled && !props["aria-label"] ? titleId : void 0,
|
|
608
|
+
...props,
|
|
609
|
+
"data-slot": "card",
|
|
610
|
+
className: cn("rounded-xl border border-border-strong bg-surface", className)
|
|
611
|
+
})
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
function CardHeader({ title, description, action, headingLevel = 2, className, ...props }) {
|
|
615
|
+
const { titleId, setTitled } = useContext(CardContext);
|
|
616
|
+
useLayoutEffect(() => {
|
|
617
|
+
setTitled(true);
|
|
618
|
+
return () => setTitled(false);
|
|
619
|
+
}, [setTitled]);
|
|
620
|
+
const Heading = headingLevel === 2 ? "h2" : "h3";
|
|
621
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
622
|
+
...props,
|
|
623
|
+
"data-slot": "card-header",
|
|
624
|
+
className: cn("flex items-start justify-between gap-4 px-5 pt-5", className),
|
|
625
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
626
|
+
className: "min-w-0",
|
|
627
|
+
children: [/* @__PURE__ */ jsx(Heading, {
|
|
628
|
+
id: titleId,
|
|
629
|
+
className: "font-sans text-sm font-medium",
|
|
630
|
+
children: title
|
|
631
|
+
}), description && /* @__PURE__ */ jsx("p", {
|
|
632
|
+
className: "text-xs text-fg-muted",
|
|
633
|
+
children: description
|
|
634
|
+
})]
|
|
635
|
+
}), action && /* @__PURE__ */ jsx("div", {
|
|
636
|
+
className: "shrink-0",
|
|
637
|
+
children: action
|
|
638
|
+
})]
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
function CardContent({ className, ...props }) {
|
|
642
|
+
return /* @__PURE__ */ jsx("div", {
|
|
643
|
+
...props,
|
|
644
|
+
"data-slot": "card-content",
|
|
645
|
+
className: cn("p-5 [[data-slot=card-header]+&]:pt-4", className)
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
//#endregion
|
|
649
|
+
//#region src/components/input/field.tsx
|
|
650
|
+
const controlHeight = {
|
|
651
|
+
sm: "h-[34px]",
|
|
652
|
+
md: "h-[38px]",
|
|
653
|
+
lg: "h-[42px]"
|
|
654
|
+
};
|
|
655
|
+
const textSize = {
|
|
656
|
+
sm: "text-sm",
|
|
657
|
+
md: "text-sm",
|
|
658
|
+
lg: "text-base"
|
|
659
|
+
};
|
|
660
|
+
const iconSize = {
|
|
661
|
+
sm: "[&_svg]:size-4",
|
|
662
|
+
md: "[&_svg]:size-4",
|
|
663
|
+
lg: "[&_svg]:size-[18px]"
|
|
664
|
+
};
|
|
665
|
+
const inputPadding = {
|
|
666
|
+
sm: "px-3",
|
|
667
|
+
md: "px-3",
|
|
668
|
+
lg: "px-4"
|
|
669
|
+
};
|
|
670
|
+
const startSlotPadding = {
|
|
671
|
+
sm: "ps-2",
|
|
672
|
+
md: "ps-2.5",
|
|
673
|
+
lg: "ps-3"
|
|
674
|
+
};
|
|
675
|
+
const endSlotPadding = {
|
|
676
|
+
sm: "pe-2",
|
|
677
|
+
md: "pe-2.5",
|
|
678
|
+
lg: "pe-3"
|
|
679
|
+
};
|
|
680
|
+
const actionsPadding = {
|
|
681
|
+
sm: "pe-[3px]",
|
|
682
|
+
md: "pe-1",
|
|
683
|
+
lg: "pe-1"
|
|
684
|
+
};
|
|
685
|
+
/** Compact Button square that fits inside the field's inner height with an even inset. */
|
|
686
|
+
const compactButton = {
|
|
687
|
+
sm: "size-[26px]",
|
|
688
|
+
md: "size-7",
|
|
689
|
+
lg: "size-8"
|
|
690
|
+
};
|
|
691
|
+
const labelStart = {
|
|
692
|
+
sm: "start-2",
|
|
693
|
+
md: "start-2",
|
|
694
|
+
lg: "start-3"
|
|
695
|
+
};
|
|
696
|
+
const floatShift = {
|
|
697
|
+
sm: "[[data-slot=input-control]:is([data-start],:has(input:focus),:has(input:not(:placeholder-shown)),:has(input:autofill),:has(input:-webkit-autofill),:has(select:focus),:has(select:open),:has(select):not(:has(option[value=\"\"]:checked)))_&]:-translate-y-[calc(50%+17px)]",
|
|
698
|
+
md: "[[data-slot=input-control]:is([data-start],:has(input:focus),:has(input:not(:placeholder-shown)),:has(input:autofill),:has(input:-webkit-autofill),:has(select:focus),:has(select:open),:has(select):not(:has(option[value=\"\"]:checked)))_&]:-translate-y-[calc(50%+19px)]",
|
|
699
|
+
lg: "[[data-slot=input-control]:is([data-start],:has(input:focus),:has(input:not(:placeholder-shown)),:has(input:autofill),:has(input:-webkit-autofill),:has(select:focus),:has(select:open),:has(select):not(:has(option[value=\"\"]:checked)))_&]:-translate-y-[calc(50%+21px)]"
|
|
700
|
+
};
|
|
701
|
+
const isText = (node) => typeof node === "string" || typeof node === "number";
|
|
702
|
+
const isPresent = (node) => node != null && node !== false && node !== "";
|
|
703
|
+
function Adornment({ node, side, size }) {
|
|
704
|
+
const text = isText(node);
|
|
705
|
+
return /* @__PURE__ */ jsx("span", {
|
|
706
|
+
"aria-hidden": text ? void 0 : true,
|
|
707
|
+
className: cn("flex shrink-0 items-center self-stretch text-fg-muted [&_svg]:shrink-0", text ? ["whitespace-nowrap select-none pointer-coarse:text-base", textSize[size]] : iconSize[size], side === "start" ? startSlotPadding[size] : endSlotPadding[size]),
|
|
708
|
+
children: node
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
const notchPadding = {
|
|
712
|
+
border: {
|
|
713
|
+
sm: "ps-2 pe-2",
|
|
714
|
+
md: "ps-2 pe-2",
|
|
715
|
+
lg: "ps-3 pe-1"
|
|
716
|
+
},
|
|
717
|
+
halo: {
|
|
718
|
+
sm: "ps-[9px] pe-[9px]",
|
|
719
|
+
md: "ps-[9px] pe-[9px]",
|
|
720
|
+
lg: "ps-[13px] pe-[5px]"
|
|
721
|
+
}
|
|
722
|
+
};
|
|
723
|
+
/**
|
|
724
|
+
* MUI-style notched outline: a fieldset draws the ring and an invisible legend holding the label
|
|
725
|
+
* text at its floated size cuts the gap. The legend collapses while the label rests in the field.
|
|
726
|
+
* A legend as tall as the border keeps the border at the fieldset's top edge.
|
|
727
|
+
*/
|
|
728
|
+
function Notch({ ring, size, disabled, children }) {
|
|
729
|
+
const halo = ring === "halo";
|
|
730
|
+
return /* @__PURE__ */ jsx("fieldset", {
|
|
731
|
+
"aria-hidden": true,
|
|
732
|
+
className: cn("pointer-events-none absolute m-0 min-w-0 p-0", "transition-[border-color] duration-200 ease-in-out motion-reduce:transition-none", notchPadding[ring][size], halo ? ["-inset-1 rounded-[calc(var(--radius-lg)+3px)] border-[3px] border-(--field-halo)", "forced-colors:invisible forced-colors:group-has-[:is(input,select):is(:focus,:open)]/control:visible forced-colors:border-[Highlight]"] : ["-inset-px rounded-[inherit] border border-(--field-border)", disabled && "forced-colors:border-[GrayText]"]),
|
|
733
|
+
children: /* @__PURE__ */ jsx("legend", {
|
|
734
|
+
className: cn("invisible float-none block w-auto max-w-[0.01px] overflow-hidden p-0 text-xs whitespace-nowrap", halo ? "h-0.75" : "h-px", "transition-[max-width] duration-50 ease-out motion-reduce:transition-none", "[[data-slot=input-control]:is([data-start],:has(input:focus),:has(input:not(:placeholder-shown)),:has(input:autofill),:has(input:-webkit-autofill),:has(select:focus),:has(select:open),:has(select):not(:has(option[value=\"\"]:checked)))_&]:max-w-full", "[[data-slot=input-control]:is([data-start],:has(input:focus),:has(input:not(:placeholder-shown)),:has(input:autofill),:has(input:-webkit-autofill),:has(select:focus),:has(select:open),:has(select):not(:has(option[value=\"\"]:checked)))_&]:delay-50", "[[data-slot=input-control]:is([data-start],:has(input:focus),:has(input:not(:placeholder-shown)),:has(input:autofill),:has(input:-webkit-autofill),:has(select:focus),:has(select:open),:has(select):not(:has(option[value=\"\"]:checked)))_&]:duration-100"),
|
|
735
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
736
|
+
className: "px-1",
|
|
737
|
+
children
|
|
738
|
+
})
|
|
739
|
+
})
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
/** Keeps an internal ref (focus forwarding, clear, reset) alongside the caller's ref. */
|
|
743
|
+
function useMergedRef(ownRef, ref) {
|
|
744
|
+
return useCallback((node) => {
|
|
745
|
+
ownRef.current = node;
|
|
746
|
+
if (typeof ref === "function") return ref(node);
|
|
747
|
+
if (ref) ref.current = node;
|
|
748
|
+
}, [ownRef, ref]);
|
|
749
|
+
}
|
|
750
|
+
const focusControl = (control) => control.focus();
|
|
751
|
+
/**
|
|
752
|
+
* The frame every field shares (input family, Select): label above or floating with a notched
|
|
753
|
+
* outline, the bordered control with its border/halo/hover states, description and live region.
|
|
754
|
+
* State selectors match `input` or `select`, never the inner buttons, so a focused clear or copy
|
|
755
|
+
* button shows only its own ring. `:open` too: an open base-select picker moves focus onto its
|
|
756
|
+
* options, and the select then matches neither `:focus` nor `:focus-within`. Inside `:is()` an
|
|
757
|
+
* unsupported `:open` is dropped alone instead of invalidating the rule.
|
|
758
|
+
*/
|
|
759
|
+
function FieldShell({ label, floating = false, helperText, error, size, id: idProp, required, disabled, readOnly, "aria-describedby": describedByProp, hasStart = false, className, status = "", controlRef, activate = focusControl, children }) {
|
|
760
|
+
const autoId = useId();
|
|
761
|
+
const id = idProp ?? `${autoId}-control`;
|
|
762
|
+
const descriptionId = `${autoId}-description`;
|
|
763
|
+
const invalid = Boolean(error);
|
|
764
|
+
const description = invalid ? error === true ? null : error : helperText;
|
|
765
|
+
const hasDescription = isPresent(description);
|
|
766
|
+
const describedBy = [describedByProp, hasDescription ? descriptionId : null].filter(Boolean).join(" ") || void 0;
|
|
767
|
+
const hasLabel = Boolean(label);
|
|
768
|
+
const floatingLabel = floating && hasLabel;
|
|
769
|
+
const asterisk = required ? /* @__PURE__ */ jsx("span", {
|
|
770
|
+
"aria-hidden": true,
|
|
771
|
+
className: "text-danger",
|
|
772
|
+
children: " *"
|
|
773
|
+
}) : null;
|
|
774
|
+
const forwardClick = (e) => {
|
|
775
|
+
const control = controlRef.current;
|
|
776
|
+
const target = e.target;
|
|
777
|
+
if (!control || control.contains(target) || target.closest("button")) return;
|
|
778
|
+
e.preventDefault();
|
|
779
|
+
activate(control);
|
|
780
|
+
};
|
|
781
|
+
const renderFloatingLabel = (restingWidth) => floatingLabel ? /* @__PURE__ */ jsxs("label", {
|
|
782
|
+
htmlFor: id,
|
|
783
|
+
className: cn("pointer-events-none absolute top-1/2 -translate-y-1/2 truncate px-1 leading-normal", "text-fg-muted pointer-coarse:text-base", "transition-[translate,font-size,color] duration-150 ease-out motion-reduce:transition-none", labelStart[size], textSize[size], restingWidth, floatShift[size], "[[data-slot=input-control]:is([data-start],:has(input:focus),:has(input:not(:placeholder-shown)),:has(input:autofill),:has(input:-webkit-autofill),:has(select:focus),:has(select:open),:has(select):not(:has(option[value=\"\"]:checked)))_&]:max-w-[calc(100%-1rem)]", "[[data-slot=input-control]:is([data-start],:has(input:focus),:has(input:not(:placeholder-shown)),:has(input:autofill),:has(input:-webkit-autofill),:has(select:focus),:has(select:open),:has(select):not(:has(option[value=\"\"]:checked)))_&]:text-xs", invalid ? "text-danger" : ["[[data-slot=input-control]:is([data-start],:has(input:focus),:has(input:not(:placeholder-shown)),:has(input:autofill),:has(input:-webkit-autofill),:has(select:focus),:has(select:open),:has(select):not(:has(option[value=\"\"]:checked)))_&]:group-has-[:is(input,select):not(:is(:focus,:open))]/control:text-fg-strong", "group-has-[:is(input,select):is(:focus,:open)]/control:text-input-focus"]),
|
|
784
|
+
children: [label, asterisk]
|
|
785
|
+
}) : null;
|
|
786
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
787
|
+
"data-slot": "input-field",
|
|
788
|
+
className: cn("flex w-full flex-col gap-1.5", disabled && "opacity-50", className),
|
|
789
|
+
children: [
|
|
790
|
+
hasLabel && !floating && /* @__PURE__ */ jsxs("label", {
|
|
791
|
+
htmlFor: id,
|
|
792
|
+
className: "self-start text-sm font-medium text-fg",
|
|
793
|
+
children: [label, asterisk]
|
|
794
|
+
}),
|
|
795
|
+
/* @__PURE__ */ jsxs("div", {
|
|
796
|
+
"data-slot": "input-control",
|
|
797
|
+
"data-start": hasStart ? "" : void 0,
|
|
798
|
+
onMouseDown: forwardClick,
|
|
799
|
+
className: cn("group/control relative isolate flex items-center rounded-lg border", floatingLabel ? "bg-transparent" : "[--field-bg:var(--color-input-bg)] bg-(--field-bg)", "transition-[border-color,box-shadow,background-color] duration-200 ease-in-out motion-reduce:transition-none", controlHeight[size], invalid ? ["[--field-border:var(--color-danger)] [--field-halo:var(--color-input-halo-invalid)]", "has-[:is(input,select):is(:focus,:open)]:[--field-halo:var(--color-input-halo-invalid-focus)]"] : ["[--field-border:var(--color-input-border)] [--field-halo:transparent]", "has-[:is(input,select):is(:focus,:open)]:[--field-border:var(--color-input-focus)] has-[:is(input,select):is(:focus,:open)]:[--field-halo:var(--color-input-halo)]"], "[--field-lift:0_0_#0000]", !invalid && !disabled && !readOnly && [
|
|
800
|
+
"hover:not-has-[:is(input,select):is(:focus,:open)]:[--field-border:var(--color-input-border-hover)]",
|
|
801
|
+
!floatingLabel && "hover:not-has-[:is(input,select):is(:focus,:open)]:[--field-bg:var(--color-input-bg-hover)]",
|
|
802
|
+
"hover:not-has-[:is(input,select):is(:focus,:open)]:[--field-lift:var(--shadow-input-lift)]"
|
|
803
|
+
], floatingLabel ? ["border-transparent forced-colors:border-[Canvas]", !readOnly && "[box-shadow:var(--field-lift)]"] : [
|
|
804
|
+
"border-(--field-border)",
|
|
805
|
+
readOnly ? "[box-shadow:0_0_0_3px_var(--field-halo)]" : "[box-shadow:var(--shadow-input),var(--field-lift),0_0_0_3px_var(--field-halo)]",
|
|
806
|
+
"has-[:is(input,select):is(:focus,:open)]:outline-hidden",
|
|
807
|
+
disabled && "forced-colors:border-[GrayText]"
|
|
808
|
+
], readOnly && !floatingLabel && "[--field-bg:var(--color-bg)]", disabled && "cursor-not-allowed", "pointer-coarse:before:absolute pointer-coarse:before:inset-x-0 pointer-coarse:before:top-1/2", "pointer-coarse:before:-z-10 pointer-coarse:before:h-11 pointer-coarse:before:-translate-y-1/2"),
|
|
809
|
+
children: [floatingLabel && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs(Notch, {
|
|
810
|
+
size,
|
|
811
|
+
ring: "halo",
|
|
812
|
+
disabled,
|
|
813
|
+
children: [label, asterisk]
|
|
814
|
+
}), /* @__PURE__ */ jsxs(Notch, {
|
|
815
|
+
size,
|
|
816
|
+
ring: "border",
|
|
817
|
+
disabled,
|
|
818
|
+
children: [label, asterisk]
|
|
819
|
+
})] }), children({
|
|
820
|
+
id,
|
|
821
|
+
invalid,
|
|
822
|
+
describedBy,
|
|
823
|
+
floating: floatingLabel,
|
|
824
|
+
floatingLabel: renderFloatingLabel
|
|
825
|
+
})]
|
|
826
|
+
}),
|
|
827
|
+
hasDescription && /* @__PURE__ */ jsxs("p", {
|
|
828
|
+
id: descriptionId,
|
|
829
|
+
className: cn("m-0 flex items-start gap-1 text-xs/snug", invalid ? "text-danger" : "text-fg-muted"),
|
|
830
|
+
children: [invalid && /* @__PURE__ */ jsx(CircleAlert, {
|
|
831
|
+
"aria-hidden": true,
|
|
832
|
+
className: "mt-px size-3.5 shrink-0"
|
|
833
|
+
}), /* @__PURE__ */ jsx("span", { children: description })]
|
|
834
|
+
}),
|
|
835
|
+
/* @__PURE__ */ jsx("span", {
|
|
836
|
+
"aria-live": "polite",
|
|
837
|
+
"aria-atomic": true,
|
|
838
|
+
className: "sr-only",
|
|
839
|
+
children: status
|
|
840
|
+
})
|
|
841
|
+
]
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
/** The input family's frame: adornments, actions and the input itself inside FieldShell. */
|
|
845
|
+
function FieldFrame({ label, floating = false, helperText, error, size = "md", start, end, mono = false, validating = false, validatingText = "Checking…", className, inputClassName, inputRef: inputRefProp, actions, status, ref, id, required, disabled, readOnly, placeholder, "aria-describedby": describedByProp, "aria-invalid": ariaInvalid, ...props }) {
|
|
846
|
+
const ownRef = useRef(null);
|
|
847
|
+
const inputRef = inputRefProp ?? ownRef;
|
|
848
|
+
const setRef = useMergedRef(inputRef, ref);
|
|
849
|
+
const hasStart = isPresent(start);
|
|
850
|
+
const hasEnd = isPresent(end);
|
|
851
|
+
return /* @__PURE__ */ jsx(FieldShell, {
|
|
852
|
+
label,
|
|
853
|
+
floating,
|
|
854
|
+
helperText,
|
|
855
|
+
error,
|
|
856
|
+
size,
|
|
857
|
+
id,
|
|
858
|
+
required,
|
|
859
|
+
disabled,
|
|
860
|
+
readOnly,
|
|
861
|
+
"aria-describedby": describedByProp,
|
|
862
|
+
hasStart,
|
|
863
|
+
className,
|
|
864
|
+
status: validating ? validatingText : status ?? "",
|
|
865
|
+
controlRef: inputRef,
|
|
866
|
+
children: (field) => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
867
|
+
hasStart ? /* @__PURE__ */ jsx(Adornment, {
|
|
868
|
+
node: start,
|
|
869
|
+
side: "start",
|
|
870
|
+
size
|
|
871
|
+
}) : null,
|
|
872
|
+
/* @__PURE__ */ jsxs("span", {
|
|
873
|
+
className: "@container flex min-w-0 flex-1 self-stretch rounded-[inherit]",
|
|
874
|
+
children: [field.floatingLabel("max-w-[calc(100cqw-1rem)]"), /* @__PURE__ */ jsx("input", {
|
|
875
|
+
...props,
|
|
876
|
+
ref: setRef,
|
|
877
|
+
id: field.id,
|
|
878
|
+
required,
|
|
879
|
+
disabled,
|
|
880
|
+
readOnly,
|
|
881
|
+
placeholder: field.floating ? placeholder || " " : placeholder,
|
|
882
|
+
"aria-invalid": field.invalid ? true : ariaInvalid,
|
|
883
|
+
"aria-describedby": field.describedBy,
|
|
884
|
+
spellCheck: mono ? false : props.spellCheck,
|
|
885
|
+
autoCapitalize: mono ? "off" : props.autoCapitalize,
|
|
886
|
+
autoCorrect: mono ? "off" : props.autoCorrect,
|
|
887
|
+
className: cn("h-full min-w-0 flex-1 rounded-[inherit] bg-transparent text-fg outline-none", "placeholder:text-input-placeholder pointer-coarse:text-base", "disabled:cursor-not-allowed", "autofill:[transition:background-color_0s_600000s] autofill:[-webkit-text-fill-color:var(--color-fg)]", textSize[size], inputPadding[size], hasStart && (isText(start) ? "ps-1" : "ps-2"), !validating && hasEnd && isText(end) ? "pe-1" : (validating || hasEnd || actions) && "pe-2", mono && "font-mono", field.floating && ["placeholder:opacity-0", "[[data-slot=input-control]:is([data-start],:has(input:focus),:has(input:not(:placeholder-shown)),:has(input:autofill),:has(input:-webkit-autofill),:has(select:focus),:has(select:open),:has(select):not(:has(option[value=\"\"]:checked)))_&]:placeholder:opacity-100"], inputClassName)
|
|
888
|
+
})]
|
|
889
|
+
}),
|
|
890
|
+
validating ? /* @__PURE__ */ jsx("span", {
|
|
891
|
+
className: cn("flex shrink-0 items-center self-stretch text-fg-muted", iconSize[size], endSlotPadding[size]),
|
|
892
|
+
children: /* @__PURE__ */ jsx(LoaderCircle, {
|
|
893
|
+
"aria-hidden": true,
|
|
894
|
+
className: "animate-spin motion-reduce:[animation-duration:2.5s]"
|
|
895
|
+
})
|
|
896
|
+
}) : hasEnd ? /* @__PURE__ */ jsx(Adornment, {
|
|
897
|
+
node: end,
|
|
898
|
+
side: "end",
|
|
899
|
+
size
|
|
900
|
+
}) : null,
|
|
901
|
+
actions ? /* @__PURE__ */ jsx("span", {
|
|
902
|
+
className: cn("flex shrink-0 items-center gap-0.5", actionsPadding[size]),
|
|
903
|
+
children: actions
|
|
904
|
+
}) : null
|
|
905
|
+
] })
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
//#endregion
|
|
909
|
+
//#region src/components/checkbox/choice-row.tsx
|
|
910
|
+
/**
|
|
911
|
+
* The row Switch and Checkbox share: one `<label>` holding the control, its label, description
|
|
912
|
+
* and error, so the whole row toggles it natively. The label line is 24px tall, which is the
|
|
913
|
+
* WCAG 2.5.8 minimum target; touch gets a 44px band without changing the layout.
|
|
914
|
+
* Disabled styling reads `:disabled` off the input, so a disabled fieldset dims rows too.
|
|
915
|
+
*/
|
|
916
|
+
function ChoiceRow({ slot, label, description, error, labelPosition = "end", gap, controlOffset, className, "aria-describedby": describedByProp, control }) {
|
|
917
|
+
const id = useId();
|
|
918
|
+
const labelId = `${id}-label`;
|
|
919
|
+
const descriptionId = `${id}-description`;
|
|
920
|
+
const errorId = `${id}-error`;
|
|
921
|
+
const hasLabel = isPresent(label);
|
|
922
|
+
const hasDescription = isPresent(description);
|
|
923
|
+
const invalid = Boolean(error);
|
|
924
|
+
const hasErrorText = invalid && error !== true && isPresent(error);
|
|
925
|
+
const describedBy = [
|
|
926
|
+
describedByProp,
|
|
927
|
+
hasDescription && descriptionId,
|
|
928
|
+
hasErrorText && errorId
|
|
929
|
+
].filter(Boolean).join(" ") || void 0;
|
|
930
|
+
const text = hasLabel || hasDescription || hasErrorText ? /* @__PURE__ */ jsxs("span", {
|
|
931
|
+
className: "flex min-w-0 flex-col",
|
|
932
|
+
children: [
|
|
933
|
+
hasLabel && /* @__PURE__ */ jsx("span", {
|
|
934
|
+
id: labelId,
|
|
935
|
+
children: label
|
|
936
|
+
}),
|
|
937
|
+
hasDescription && /* @__PURE__ */ jsx("span", {
|
|
938
|
+
id: descriptionId,
|
|
939
|
+
className: "text-xs/snug text-fg-muted",
|
|
940
|
+
children: description
|
|
941
|
+
}),
|
|
942
|
+
hasErrorText && /* @__PURE__ */ jsxs("span", {
|
|
943
|
+
id: errorId,
|
|
944
|
+
className: cn("flex items-start gap-1 text-xs/snug text-danger", hasDescription && "mt-1"),
|
|
945
|
+
children: [/* @__PURE__ */ jsx(CircleAlert, {
|
|
946
|
+
"aria-hidden": true,
|
|
947
|
+
className: "mt-px size-3.5 shrink-0"
|
|
948
|
+
}), /* @__PURE__ */ jsx("span", { children: error })]
|
|
949
|
+
})
|
|
950
|
+
]
|
|
951
|
+
}) : null;
|
|
952
|
+
const controlSlot = /* @__PURE__ */ jsx("span", {
|
|
953
|
+
className: cn("flex shrink-0", controlOffset),
|
|
954
|
+
children: control({
|
|
955
|
+
labelledBy: hasLabel ? labelId : void 0,
|
|
956
|
+
describedBy,
|
|
957
|
+
invalid
|
|
958
|
+
})
|
|
959
|
+
});
|
|
960
|
+
return /* @__PURE__ */ jsx("label", {
|
|
961
|
+
"data-slot": slot,
|
|
962
|
+
className: cn("relative isolate flex min-h-6 min-w-6 items-start text-sm/6 text-fg", "cursor-pointer has-[input:disabled]:cursor-not-allowed has-[input[aria-busy=true]]:cursor-progress", "has-[input:disabled]:opacity-50", "forced-colors:has-[input:disabled]:text-[GrayText] forced-colors:has-[input:disabled]:opacity-100", "pointer-coarse:before:absolute pointer-coarse:before:top-1/2 pointer-coarse:before:left-1/2", "pointer-coarse:before:-z-10 pointer-coarse:before:size-full pointer-coarse:before:min-h-11", "pointer-coarse:before:min-w-11 pointer-coarse:before:-translate-1/2", gap, labelPosition === "start" ? "w-full justify-between" : "w-fit max-w-full", className),
|
|
963
|
+
children: labelPosition === "start" ? /* @__PURE__ */ jsxs(Fragment, { children: [text, controlSlot] }) : /* @__PURE__ */ jsxs(Fragment, { children: [controlSlot, text] })
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
//#endregion
|
|
967
|
+
//#region src/components/checkbox/checkbox.tsx
|
|
968
|
+
/**
|
|
969
|
+
* Drawn from the short end, like a pen stroke. As wide as the dash (Geist's 8-unit check reads
|
|
970
|
+
* small next to it). Length ~14.1; the dash pattern rounds it up to 15.
|
|
971
|
+
*/
|
|
972
|
+
const CHECK_PATH = "M5 10L8.5 13.5L15 7";
|
|
973
|
+
const markBase = ["pointer-events-none col-start-1 row-start-1 size-3.5 text-control-checked-fg", "forced-colors:forced-color-adjust-none forced-colors:text-[HighlightText] forced-colors:peer-disabled:text-[Canvas]"];
|
|
974
|
+
function Checkbox({ label, description, error, indeterminate = false, className, inputClassName, ref, checked, "aria-describedby": describedBy, "aria-invalid": ariaInvalid, ...props }) {
|
|
975
|
+
const ownRef = useRef(null);
|
|
976
|
+
const setRef = useMergedRef(ownRef, ref);
|
|
977
|
+
useLayoutEffect(() => {
|
|
978
|
+
if (ownRef.current) ownRef.current.indeterminate = indeterminate;
|
|
979
|
+
}, [indeterminate, checked]);
|
|
980
|
+
return /* @__PURE__ */ jsx(ChoiceRow, {
|
|
981
|
+
slot: "checkbox",
|
|
982
|
+
label,
|
|
983
|
+
description,
|
|
984
|
+
error,
|
|
985
|
+
gap: "gap-2",
|
|
986
|
+
controlOffset: "mt-1",
|
|
987
|
+
className,
|
|
988
|
+
"aria-describedby": describedBy,
|
|
989
|
+
control: (row) => /* @__PURE__ */ jsxs("span", {
|
|
990
|
+
className: cn("relative inline-grid size-4 place-items-center", "transition-[scale] duration-100 ease-out has-[input:enabled:active]:scale-[0.92]"),
|
|
991
|
+
children: [
|
|
992
|
+
/* @__PURE__ */ jsx("input", {
|
|
993
|
+
...props,
|
|
994
|
+
ref: setRef,
|
|
995
|
+
type: "checkbox",
|
|
996
|
+
checked,
|
|
997
|
+
"aria-labelledby": props["aria-labelledby"] ?? row.labelledBy,
|
|
998
|
+
"aria-describedby": row.describedBy,
|
|
999
|
+
"aria-invalid": row.invalid ? true : ariaInvalid,
|
|
1000
|
+
className: cn("peer col-start-1 row-start-1 m-0 size-4 cursor-pointer appearance-none rounded-sm border outline-hidden", "disabled:cursor-not-allowed", "border-(--checkbox-border) bg-(--checkbox-bg) [box-shadow:0_0_0_3px_var(--checkbox-halo)]", "transition-[background-color,border-color,box-shadow] duration-200 ease-in-out", "[--checkbox-bg:var(--color-input-bg)] [--checkbox-halo:transparent]", "checked:[--checkbox-bg:var(--color-control-checked)] indeterminate:[--checkbox-bg:var(--color-control-checked)]", row.invalid ? [
|
|
1001
|
+
"[--checkbox-border:var(--color-danger)] [--checkbox-halo:var(--color-input-halo-invalid)]",
|
|
1002
|
+
"focus-visible:[--checkbox-halo:var(--color-input-halo-invalid-focus)]",
|
|
1003
|
+
"not-forced-colors:checked:focus-visible:outline-danger not-forced-colors:indeterminate:focus-visible:outline-danger"
|
|
1004
|
+
] : [
|
|
1005
|
+
"[--checkbox-border:var(--color-input-border)]",
|
|
1006
|
+
"checked:[--checkbox-border:var(--color-control-checked)] indeterminate:[--checkbox-border:var(--color-control-checked)]",
|
|
1007
|
+
"enabled:not-checked:not-indeterminate:hover:[--checkbox-border:var(--color-input-border-hover)]",
|
|
1008
|
+
"enabled:not-checked:not-indeterminate:hover:[--checkbox-bg:var(--color-input-bg-hover)]",
|
|
1009
|
+
"focus-visible:[--checkbox-border:var(--color-input-focus)] focus-visible:[--checkbox-halo:var(--color-input-halo)]",
|
|
1010
|
+
"not-forced-colors:checked:focus-visible:outline-control-checked not-forced-colors:indeterminate:focus-visible:outline-control-checked"
|
|
1011
|
+
], "checked:focus-visible:[--checkbox-halo:transparent] indeterminate:focus-visible:[--checkbox-halo:transparent]", "checked:focus-visible:outline-solid checked:focus-visible:outline-2 checked:focus-visible:outline-offset-2", "indeterminate:focus-visible:outline-solid indeterminate:focus-visible:outline-2 indeterminate:focus-visible:outline-offset-2", "forced-colors:forced-color-adjust-none forced-colors:[box-shadow:none]", "forced-colors:border-[CanvasText] forced-colors:bg-[Canvas]", "forced-colors:checked:border-[Highlight] forced-colors:checked:bg-[Highlight]", "forced-colors:indeterminate:border-[Highlight] forced-colors:indeterminate:bg-[Highlight]", "forced-colors:disabled:border-[GrayText] forced-colors:disabled:checked:bg-[GrayText] forced-colors:disabled:indeterminate:bg-[GrayText]", "forced-colors:focus-visible:outline-[CanvasText]", inputClassName)
|
|
1012
|
+
}),
|
|
1013
|
+
/* @__PURE__ */ jsx("svg", {
|
|
1014
|
+
"aria-hidden": true,
|
|
1015
|
+
viewBox: "0 0 20 20",
|
|
1016
|
+
fill: "none",
|
|
1017
|
+
stroke: "currentColor",
|
|
1018
|
+
strokeWidth: 2,
|
|
1019
|
+
strokeLinecap: "round",
|
|
1020
|
+
strokeLinejoin: "round",
|
|
1021
|
+
className: cn(markBase, "[stroke-dasharray:15] [stroke-dashoffset:15] opacity-0", "[transition:opacity_80ms_ease-out,stroke-dashoffset_0s_80ms]", "peer-[:checked:not(:indeterminate)]:[stroke-dashoffset:0] peer-[:checked:not(:indeterminate)]:opacity-100", "peer-[:checked:not(:indeterminate)]:[transition:stroke-dashoffset_150ms_ease-out]"),
|
|
1022
|
+
children: /* @__PURE__ */ jsx("path", { d: CHECK_PATH })
|
|
1023
|
+
}),
|
|
1024
|
+
/* @__PURE__ */ jsx("svg", {
|
|
1025
|
+
"aria-hidden": true,
|
|
1026
|
+
viewBox: "0 0 20 20",
|
|
1027
|
+
fill: "none",
|
|
1028
|
+
stroke: "currentColor",
|
|
1029
|
+
strokeWidth: 2,
|
|
1030
|
+
strokeLinecap: "round",
|
|
1031
|
+
className: cn(markBase, "scale-x-0 opacity-0 [transition:opacity_80ms_ease-out,scale_0s_80ms]", "peer-indeterminate:scale-x-100 peer-indeterminate:opacity-100", "peer-indeterminate:[transition:scale_150ms_ease-out]"),
|
|
1032
|
+
children: /* @__PURE__ */ jsx("path", { d: "M5 10H15" })
|
|
1033
|
+
})
|
|
1034
|
+
]
|
|
1035
|
+
})
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
//#endregion
|
|
1039
|
+
//#region src/components/checkbox/checkbox-group.tsx
|
|
1040
|
+
/** A `<fieldset>` + `<legend>` around related checkboxes; holds no value of its own. */
|
|
1041
|
+
function CheckboxGroup({ legend, description, error, orientation = "vertical", className, "aria-describedby": describedByProp, children, ...props }) {
|
|
1042
|
+
const id = useId();
|
|
1043
|
+
const descriptionId = `${id}-description`;
|
|
1044
|
+
const errorId = `${id}-error`;
|
|
1045
|
+
const hasDescription = isPresent(description);
|
|
1046
|
+
const hasError = Boolean(error) && error !== true && isPresent(error);
|
|
1047
|
+
const describedBy = [
|
|
1048
|
+
describedByProp,
|
|
1049
|
+
hasDescription && descriptionId,
|
|
1050
|
+
hasError && errorId
|
|
1051
|
+
].filter(Boolean).join(" ") || void 0;
|
|
1052
|
+
return /* @__PURE__ */ jsxs("fieldset", {
|
|
1053
|
+
...props,
|
|
1054
|
+
"data-slot": "checkbox-group",
|
|
1055
|
+
"aria-describedby": describedBy,
|
|
1056
|
+
className: cn("group/checkbox-group m-0 min-w-0 border-0 p-0", className),
|
|
1057
|
+
children: [/* @__PURE__ */ jsx("legend", {
|
|
1058
|
+
className: "float-start mb-1.5 w-full p-0 text-sm font-medium text-fg group-disabled/checkbox-group:opacity-50",
|
|
1059
|
+
children: legend
|
|
1060
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
1061
|
+
className: "clear-both flex flex-col",
|
|
1062
|
+
children: [
|
|
1063
|
+
hasDescription && /* @__PURE__ */ jsx("p", {
|
|
1064
|
+
id: descriptionId,
|
|
1065
|
+
className: "m-0 -mt-1 mb-2.5 text-xs/snug text-fg-muted group-disabled/checkbox-group:opacity-50",
|
|
1066
|
+
children: description
|
|
1067
|
+
}),
|
|
1068
|
+
/* @__PURE__ */ jsx("div", {
|
|
1069
|
+
"data-orientation": orientation,
|
|
1070
|
+
className: cn("flex", orientation === "vertical" ? "flex-col gap-2" : "flex-row flex-wrap gap-x-6 gap-y-2"),
|
|
1071
|
+
children
|
|
1072
|
+
}),
|
|
1073
|
+
hasError && /* @__PURE__ */ jsxs("p", {
|
|
1074
|
+
id: errorId,
|
|
1075
|
+
className: "m-0 mt-2 flex items-start gap-1 text-xs/snug text-danger",
|
|
1076
|
+
children: [/* @__PURE__ */ jsx(CircleAlert, {
|
|
1077
|
+
"aria-hidden": true,
|
|
1078
|
+
className: "mt-px size-3.5 shrink-0"
|
|
1079
|
+
}), /* @__PURE__ */ jsx("span", { children: error })]
|
|
1080
|
+
})
|
|
1081
|
+
]
|
|
1082
|
+
})]
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
//#endregion
|
|
1086
|
+
//#region src/components/modal/modal-shell.tsx
|
|
1087
|
+
const ModalContext = createContext(null);
|
|
1088
|
+
/** Parts of one wrapper must not work inside the other: a DialogHeader in a Drawer throws too. */
|
|
1089
|
+
function useModalContext(part, kind, owner) {
|
|
1090
|
+
const context = useContext(ModalContext);
|
|
1091
|
+
if (!context || context.kind !== kind) throw new Error(`${part} must be rendered inside <${owner}>`);
|
|
1092
|
+
return context;
|
|
1093
|
+
}
|
|
1094
|
+
/** Registers a rendered title (and description) so the dialog's aria-* point at them. */
|
|
1095
|
+
function useAnnounceHeading({ setHasTitle, setHasDescription }, hasDescription) {
|
|
1096
|
+
useLayoutEffect(() => {
|
|
1097
|
+
setHasTitle(true);
|
|
1098
|
+
return () => setHasTitle(false);
|
|
1099
|
+
}, [setHasTitle]);
|
|
1100
|
+
useLayoutEffect(() => {
|
|
1101
|
+
if (!hasDescription) return;
|
|
1102
|
+
setHasDescription(true);
|
|
1103
|
+
return () => setHasDescription(false);
|
|
1104
|
+
}, [hasDescription, setHasDescription]);
|
|
1105
|
+
}
|
|
1106
|
+
const lockHolders = /* @__PURE__ */ new Set();
|
|
1107
|
+
let unlockedStyle = {
|
|
1108
|
+
overflow: "",
|
|
1109
|
+
paddingInlineEnd: ""
|
|
1110
|
+
};
|
|
1111
|
+
function lockScroll(el) {
|
|
1112
|
+
if (lockHolders.has(el)) return;
|
|
1113
|
+
lockHolders.add(el);
|
|
1114
|
+
if (lockHolders.size > 1) return;
|
|
1115
|
+
const root = document.documentElement;
|
|
1116
|
+
unlockedStyle = {
|
|
1117
|
+
overflow: root.style.overflow,
|
|
1118
|
+
paddingInlineEnd: root.style.paddingInlineEnd
|
|
1119
|
+
};
|
|
1120
|
+
root.style.paddingInlineEnd = `${window.innerWidth - root.clientWidth}px`;
|
|
1121
|
+
root.style.overflow = "hidden";
|
|
1122
|
+
}
|
|
1123
|
+
function unlockScroll(el) {
|
|
1124
|
+
if (!lockHolders.delete(el) || lockHolders.size > 0) return;
|
|
1125
|
+
const root = document.documentElement;
|
|
1126
|
+
root.style.overflow = unlockedStyle.overflow;
|
|
1127
|
+
root.style.paddingInlineEnd = unlockedStyle.paddingInlineEnd;
|
|
1128
|
+
}
|
|
1129
|
+
const BOUNCE_MS = 200;
|
|
1130
|
+
const bounces = /* @__PURE__ */ new WeakMap();
|
|
1131
|
+
function bounce(el, keyframes) {
|
|
1132
|
+
if (!keyframes || typeof el.animate !== "function" || typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
|
1133
|
+
bounces.get(el)?.cancel();
|
|
1134
|
+
bounces.set(el, el.animate(keyframes(), { duration: BOUNCE_MS }));
|
|
1135
|
+
}
|
|
1136
|
+
/**
|
|
1137
|
+
* Native `<dialog>` + showModal(): top layer (no portal), inert page, focus containment and
|
|
1138
|
+
* restore, and Esc all come from the platform. Controlled: `open` drives it and every close
|
|
1139
|
+
* request goes through `onOpenChange`. `children` is the wrapper's panel, mounted only while open
|
|
1140
|
+
* or exiting.
|
|
1141
|
+
*/
|
|
1142
|
+
function ModalShell({ kind, open, onOpenChange, alert = false, dismissible = true, bounceKeyframes, className, children, ref, onCancel, onClose, onPointerDown, onMouseDown, onClick, "aria-labelledby": labelledByProp, "aria-describedby": describedByProp, ...props }) {
|
|
1143
|
+
const dialogRef = useRef(null);
|
|
1144
|
+
const setRef = useMergedRef(dialogRef, ref);
|
|
1145
|
+
const titleId = useId();
|
|
1146
|
+
const descriptionId = useId();
|
|
1147
|
+
const [hasTitle, setHasTitle] = useState(false);
|
|
1148
|
+
const [hasDescription, setHasDescription] = useState(false);
|
|
1149
|
+
const [present, setPresent] = useState(open);
|
|
1150
|
+
if (open && !present) setPresent(true);
|
|
1151
|
+
const [closeCount, setCloseCount] = useState(0);
|
|
1152
|
+
const latest = useRef({
|
|
1153
|
+
open,
|
|
1154
|
+
onOpenChange,
|
|
1155
|
+
alert,
|
|
1156
|
+
dismissible,
|
|
1157
|
+
bounceKeyframes
|
|
1158
|
+
});
|
|
1159
|
+
useInsertionEffect(() => {
|
|
1160
|
+
latest.current = {
|
|
1161
|
+
open,
|
|
1162
|
+
onOpenChange,
|
|
1163
|
+
alert,
|
|
1164
|
+
dismissible,
|
|
1165
|
+
bounceKeyframes
|
|
1166
|
+
};
|
|
1167
|
+
});
|
|
1168
|
+
const requestClose = useCallback((reason) => {
|
|
1169
|
+
const el = dialogRef.current;
|
|
1170
|
+
const { open, onOpenChange, alert, dismissible, bounceKeyframes } = latest.current;
|
|
1171
|
+
if (!el || !open || el.hasAttribute("data-closing")) return;
|
|
1172
|
+
if (!dismissible || alert && reason === "backdrop") {
|
|
1173
|
+
bounce(el, bounceKeyframes);
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
onOpenChange(false, reason);
|
|
1177
|
+
}, []);
|
|
1178
|
+
const showOnAttach = useCallback((node) => {
|
|
1179
|
+
const el = node?.parentElement;
|
|
1180
|
+
if (!open || !(el instanceof HTMLDialogElement)) return;
|
|
1181
|
+
el.removeAttribute("data-closing");
|
|
1182
|
+
if (!el.open) {
|
|
1183
|
+
lockScroll(el);
|
|
1184
|
+
el.showModal();
|
|
1185
|
+
}
|
|
1186
|
+
}, [open]);
|
|
1187
|
+
useLayoutEffect(() => {
|
|
1188
|
+
const el = dialogRef.current;
|
|
1189
|
+
if (open || !el?.open) return;
|
|
1190
|
+
el.setAttribute("data-closing", "");
|
|
1191
|
+
let cancelled = false;
|
|
1192
|
+
Promise.allSettled(el.getAnimations().map((animation) => animation.finished)).then(() => {
|
|
1193
|
+
if (!cancelled && el.open && el.hasAttribute("data-closing")) el.close();
|
|
1194
|
+
});
|
|
1195
|
+
return () => {
|
|
1196
|
+
cancelled = true;
|
|
1197
|
+
};
|
|
1198
|
+
}, [open]);
|
|
1199
|
+
const handleCancel = (event) => {
|
|
1200
|
+
onCancel?.(event);
|
|
1201
|
+
event.preventDefault();
|
|
1202
|
+
requestClose("escape");
|
|
1203
|
+
};
|
|
1204
|
+
const handleClose = (event) => {
|
|
1205
|
+
onClose?.(event);
|
|
1206
|
+
const el = event.currentTarget;
|
|
1207
|
+
if (el.open) return;
|
|
1208
|
+
el.removeAttribute("data-closing");
|
|
1209
|
+
unlockScroll(el);
|
|
1210
|
+
setPresent(false);
|
|
1211
|
+
setCloseCount((count) => count + 1);
|
|
1212
|
+
if (latest.current.open) latest.current.onOpenChange(false, "escape");
|
|
1213
|
+
};
|
|
1214
|
+
const pressedBackdrop = useRef(false);
|
|
1215
|
+
const handlePointerDown = (event) => {
|
|
1216
|
+
onPointerDown?.(event);
|
|
1217
|
+
pressedBackdrop.current = event.target === event.currentTarget;
|
|
1218
|
+
};
|
|
1219
|
+
const handleMouseDown = (event) => {
|
|
1220
|
+
onMouseDown?.(event);
|
|
1221
|
+
if (event.target === event.currentTarget) event.preventDefault();
|
|
1222
|
+
};
|
|
1223
|
+
const handleClick = (event) => {
|
|
1224
|
+
onClick?.(event);
|
|
1225
|
+
const onBackdrop = pressedBackdrop.current && event.target === event.currentTarget;
|
|
1226
|
+
pressedBackdrop.current = false;
|
|
1227
|
+
if (onBackdrop) requestClose("backdrop");
|
|
1228
|
+
};
|
|
1229
|
+
useEffect(() => {
|
|
1230
|
+
const el = dialogRef.current;
|
|
1231
|
+
if (!el) return;
|
|
1232
|
+
if (el.open) lockScroll(el);
|
|
1233
|
+
return () => unlockScroll(el);
|
|
1234
|
+
}, []);
|
|
1235
|
+
const rendered = open || present;
|
|
1236
|
+
return /* @__PURE__ */ jsx(ModalContext, {
|
|
1237
|
+
value: {
|
|
1238
|
+
kind,
|
|
1239
|
+
titleId,
|
|
1240
|
+
descriptionId,
|
|
1241
|
+
alert,
|
|
1242
|
+
dismissible,
|
|
1243
|
+
setHasTitle,
|
|
1244
|
+
setHasDescription,
|
|
1245
|
+
requestClose
|
|
1246
|
+
},
|
|
1247
|
+
children: /* @__PURE__ */ jsx("dialog", {
|
|
1248
|
+
...props,
|
|
1249
|
+
ref: setRef,
|
|
1250
|
+
"data-slot": kind,
|
|
1251
|
+
role: alert ? "alertdialog" : void 0,
|
|
1252
|
+
"aria-labelledby": labelledByProp ?? (hasTitle ? titleId : void 0),
|
|
1253
|
+
"aria-describedby": describedByProp ?? (hasDescription ? descriptionId : void 0),
|
|
1254
|
+
onCancel: handleCancel,
|
|
1255
|
+
onClose: handleClose,
|
|
1256
|
+
onPointerDown: handlePointerDown,
|
|
1257
|
+
onMouseDown: handleMouseDown,
|
|
1258
|
+
onClick: handleClick,
|
|
1259
|
+
className,
|
|
1260
|
+
children: rendered && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
|
|
1261
|
+
hidden: true,
|
|
1262
|
+
ref: showOnAttach
|
|
1263
|
+
}, closeCount), children] })
|
|
1264
|
+
})
|
|
1265
|
+
});
|
|
1266
|
+
}
|
|
1267
|
+
//#endregion
|
|
1268
|
+
//#region src/components/dialog/dialog.tsx
|
|
1269
|
+
function useDialogContext(part) {
|
|
1270
|
+
return useModalContext(part, "dialog", "Dialog");
|
|
1271
|
+
}
|
|
1272
|
+
const sizeClasses$2 = {
|
|
1273
|
+
sm: "max-w-[384px]",
|
|
1274
|
+
md: "max-w-[480px]",
|
|
1275
|
+
lg: "max-w-[640px]"
|
|
1276
|
+
};
|
|
1277
|
+
const motionClasses$1 = [
|
|
1278
|
+
"transition-[opacity,translate,scale] duration-200 ease-[cubic-bezier(0.16,1,0.3,1)]",
|
|
1279
|
+
"starting:open:translate-y-1 starting:open:scale-96 starting:open:opacity-0",
|
|
1280
|
+
"data-closing:pointer-events-none data-closing:scale-98 data-closing:opacity-0",
|
|
1281
|
+
"data-closing:duration-120 data-closing:ease-[cubic-bezier(0.2,0,1,0.9)]",
|
|
1282
|
+
"backdrop:bg-dialog-backdrop backdrop:transition-opacity backdrop:duration-200 backdrop:ease-[cubic-bezier(0.16,1,0.3,1)]",
|
|
1283
|
+
"starting:open:backdrop:opacity-0",
|
|
1284
|
+
"data-closing:backdrop:opacity-0 data-closing:backdrop:duration-120",
|
|
1285
|
+
"data-closing:backdrop:ease-[cubic-bezier(0.2,0,1,0.9)]"
|
|
1286
|
+
];
|
|
1287
|
+
const sheetClasses = [
|
|
1288
|
+
"max-sm:mx-0 max-sm:mt-auto max-sm:mb-0 max-sm:w-full max-sm:max-w-none",
|
|
1289
|
+
"max-sm:max-h-[calc(100dvh-32px)]",
|
|
1290
|
+
"max-sm:duration-250 max-sm:ease-[cubic-bezier(0.33,1,0.68,1)]",
|
|
1291
|
+
"max-sm:starting:open:translate-y-full max-sm:starting:open:scale-100 max-sm:starting:open:opacity-100",
|
|
1292
|
+
"max-sm:data-closing:translate-y-full max-sm:data-closing:scale-100 max-sm:data-closing:opacity-100",
|
|
1293
|
+
"max-sm:data-closing:duration-180",
|
|
1294
|
+
"max-sm:backdrop:duration-250 max-sm:backdrop:ease-[cubic-bezier(0.33,1,0.68,1)]",
|
|
1295
|
+
"max-sm:data-closing:backdrop:duration-180"
|
|
1296
|
+
];
|
|
1297
|
+
function bounceKeyframes() {
|
|
1298
|
+
const sheet = typeof window.matchMedia === "function" && window.matchMedia("(width < 40rem)").matches;
|
|
1299
|
+
const rest = sheet ? { translate: "0 0" } : { scale: "1" };
|
|
1300
|
+
const peak = sheet ? { translate: "0 -6px" } : { scale: "1.02" };
|
|
1301
|
+
return [
|
|
1302
|
+
{
|
|
1303
|
+
...rest,
|
|
1304
|
+
easing: "ease-out"
|
|
1305
|
+
},
|
|
1306
|
+
{
|
|
1307
|
+
...peak,
|
|
1308
|
+
easing: "ease-out"
|
|
1309
|
+
},
|
|
1310
|
+
rest
|
|
1311
|
+
];
|
|
1312
|
+
}
|
|
1313
|
+
/**
|
|
1314
|
+
* Modal dialog on the native `<dialog>` + showModal() (behavior in ModalShell). Controlled: `open`
|
|
1315
|
+
* drives it and every close request goes through `onOpenChange`.
|
|
1316
|
+
*/
|
|
1317
|
+
function Dialog({ size = "md", className, children, ...props }) {
|
|
1318
|
+
return /* @__PURE__ */ jsx(ModalShell, {
|
|
1319
|
+
...props,
|
|
1320
|
+
kind: "dialog",
|
|
1321
|
+
bounceKeyframes,
|
|
1322
|
+
className: cn("m-auto w-[calc(100%-2rem)] max-h-[calc(100dvh-64px)] overflow-visible border-0 bg-transparent p-0 text-fg", "overscroll-contain outline-none", sizeClasses$2[size], motionClasses$1, sheetClasses, className),
|
|
1323
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
1324
|
+
"data-slot": "dialog-panel",
|
|
1325
|
+
className: cn("flex max-h-[inherit] flex-col rounded-xl border border-popover-border bg-popover-bg shadow-dialog", "forced-colors:border-[CanvasText]", "max-sm:rounded-b-none max-sm:border-b-0 max-sm:pb-[env(safe-area-inset-bottom)]"),
|
|
1326
|
+
children
|
|
1327
|
+
})
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
function DialogHeader({ title, description, closeButton, className, ...props }) {
|
|
1331
|
+
const context = useDialogContext("DialogHeader");
|
|
1332
|
+
const hasDescription = description != null && description !== false && description !== "";
|
|
1333
|
+
const showClose = closeButton ?? !context.alert;
|
|
1334
|
+
useAnnounceHeading(context, hasDescription);
|
|
1335
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1336
|
+
...props,
|
|
1337
|
+
"data-slot": "dialog-header",
|
|
1338
|
+
className: cn("flex shrink-0 items-start gap-4 px-6 pt-6 pb-4", className),
|
|
1339
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1340
|
+
className: "min-w-0 flex-1",
|
|
1341
|
+
children: [/* @__PURE__ */ jsx("h2", {
|
|
1342
|
+
id: context.titleId,
|
|
1343
|
+
className: "m-0 text-xl/tight font-medium text-fg",
|
|
1344
|
+
children: title
|
|
1345
|
+
}), hasDescription && /* @__PURE__ */ jsx("p", {
|
|
1346
|
+
id: context.descriptionId,
|
|
1347
|
+
className: "mt-1.5 mb-0 text-sm text-fg-muted",
|
|
1348
|
+
children: description
|
|
1349
|
+
})]
|
|
1350
|
+
}), showClose && /* @__PURE__ */ jsx(Button, {
|
|
1351
|
+
intent: "ghost",
|
|
1352
|
+
size: "sm",
|
|
1353
|
+
icon: /* @__PURE__ */ jsx(X, {}),
|
|
1354
|
+
"aria-label": "Close",
|
|
1355
|
+
tooltip: false,
|
|
1356
|
+
disabled: !context.dismissible,
|
|
1357
|
+
className: "-me-2 -mt-1 shrink-0",
|
|
1358
|
+
onClick: () => context.requestClose("close-button")
|
|
1359
|
+
})]
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
/** Scrolls when the content is taller than the space left between header and footer. */
|
|
1363
|
+
function DialogBody({ className, ...props }) {
|
|
1364
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1365
|
+
...props,
|
|
1366
|
+
"data-slot": "dialog-body",
|
|
1367
|
+
className: cn("min-h-0 overflow-y-auto overscroll-contain px-6 py-1 text-sm text-fg", className)
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
/** Actions, right-aligned: Cancel first, the primary action last (rightmost). */
|
|
1371
|
+
function DialogFooter({ className, ...props }) {
|
|
1372
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1373
|
+
...props,
|
|
1374
|
+
"data-slot": "dialog-footer",
|
|
1375
|
+
className: cn("flex shrink-0 flex-wrap items-center justify-end gap-2 px-6 pt-5 pb-6", "max-sm:*:data-[slot=button]:flex-1", className)
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1378
|
+
//#endregion
|
|
1379
|
+
//#region src/components/drawer/drawer.tsx
|
|
1380
|
+
function useDrawerContext(part) {
|
|
1381
|
+
return useModalContext(part, "drawer", "Drawer");
|
|
1382
|
+
}
|
|
1383
|
+
const sizeClasses$1 = {
|
|
1384
|
+
sm: "w-[280px]",
|
|
1385
|
+
md: "w-[384px]",
|
|
1386
|
+
lg: "w-[480px]"
|
|
1387
|
+
};
|
|
1388
|
+
const sideClasses = {
|
|
1389
|
+
left: ["mr-auto ml-0", "motion-safe:starting:open:-translate-x-full motion-safe:data-closing:-translate-x-full"],
|
|
1390
|
+
right: ["mr-0 ml-auto", "motion-safe:starting:open:translate-x-full motion-safe:data-closing:translate-x-full"]
|
|
1391
|
+
};
|
|
1392
|
+
const motionClasses = [
|
|
1393
|
+
"transition-[translate,opacity] duration-200 ease-[cubic-bezier(0.4,0,0.2,1)]",
|
|
1394
|
+
"motion-reduce:starting:open:opacity-0 motion-reduce:data-closing:opacity-0",
|
|
1395
|
+
"data-closing:pointer-events-none",
|
|
1396
|
+
"backdrop:bg-dialog-backdrop backdrop:transition-opacity backdrop:duration-200 backdrop:ease-[cubic-bezier(0.4,0,0.2,1)]",
|
|
1397
|
+
"starting:open:backdrop:opacity-0 data-closing:backdrop:opacity-0"
|
|
1398
|
+
];
|
|
1399
|
+
const panelSideClasses = {
|
|
1400
|
+
left: "border-r",
|
|
1401
|
+
right: "border-l"
|
|
1402
|
+
};
|
|
1403
|
+
/**
|
|
1404
|
+
* Modal side panel on the native `<dialog>` (behavior shared with Dialog through ModalShell).
|
|
1405
|
+
* Controlled through `open` / `onOpenChange(open, reason)`; Esc, the backdrop, DrawerHeader's close
|
|
1406
|
+
* button and DrawerClose all ask through `onOpenChange`. Needs a name: `aria-label` /
|
|
1407
|
+
* `aria-labelledby`, or a DrawerHeader title.
|
|
1408
|
+
*/
|
|
1409
|
+
function Drawer({ side = "right", size = "md", className, children, ref, ...props }) {
|
|
1410
|
+
const dialogRef = useRef(null);
|
|
1411
|
+
const setRef = useMergedRef(dialogRef, ref);
|
|
1412
|
+
useEffect(() => {
|
|
1413
|
+
if (process.env.NODE_ENV === "production" || !props.open) return;
|
|
1414
|
+
const timer = setTimeout(() => {
|
|
1415
|
+
const el = dialogRef.current;
|
|
1416
|
+
if (!el?.open) return;
|
|
1417
|
+
if (el.getAttribute("aria-label")?.trim() || el.getAttribute("aria-labelledby")) return;
|
|
1418
|
+
console.warn("Drawer needs an accessible name: pass aria-label or render a DrawerHeader title.");
|
|
1419
|
+
});
|
|
1420
|
+
return () => clearTimeout(timer);
|
|
1421
|
+
}, [props.open]);
|
|
1422
|
+
return /* @__PURE__ */ jsx(ModalShell, {
|
|
1423
|
+
...props,
|
|
1424
|
+
ref: setRef,
|
|
1425
|
+
kind: "drawer",
|
|
1426
|
+
"data-side": side,
|
|
1427
|
+
"data-size": size,
|
|
1428
|
+
className: cn("my-0 h-dvh max-h-none max-w-[calc(100vw-56px)] overflow-visible border-0 bg-transparent p-0 text-fg", "overscroll-contain outline-none", sizeClasses$1[size], sideClasses[side], motionClasses, className),
|
|
1429
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
1430
|
+
"data-slot": "drawer-panel",
|
|
1431
|
+
className: cn("flex h-full flex-col border-popover-border bg-popover-bg shadow-dialog", panelSideClasses[side], "forced-colors:border-[CanvasText]"),
|
|
1432
|
+
children
|
|
1433
|
+
})
|
|
1434
|
+
});
|
|
1435
|
+
}
|
|
1436
|
+
/** Title, optional description and the close button, pinned above the body. */
|
|
1437
|
+
function DrawerHeader({ title, description, className, ...props }) {
|
|
1438
|
+
const context = useDrawerContext("DrawerHeader");
|
|
1439
|
+
const hasDescription = description != null && description !== false && description !== "";
|
|
1440
|
+
useAnnounceHeading(context, hasDescription);
|
|
1441
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1442
|
+
...props,
|
|
1443
|
+
"data-slot": "drawer-header",
|
|
1444
|
+
className: cn("flex shrink-0 items-start gap-4 px-6 pt-6 pb-4", className),
|
|
1445
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1446
|
+
className: "min-w-0 flex-1",
|
|
1447
|
+
children: [/* @__PURE__ */ jsx("h2", {
|
|
1448
|
+
id: context.titleId,
|
|
1449
|
+
className: "m-0 text-xl/tight font-medium wrap-anywhere text-fg",
|
|
1450
|
+
children: title
|
|
1451
|
+
}), hasDescription && /* @__PURE__ */ jsx("p", {
|
|
1452
|
+
id: context.descriptionId,
|
|
1453
|
+
className: "mt-1.5 mb-0 text-sm wrap-anywhere text-fg-muted",
|
|
1454
|
+
children: description
|
|
1455
|
+
})]
|
|
1456
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
1457
|
+
intent: "ghost",
|
|
1458
|
+
size: "sm",
|
|
1459
|
+
icon: /* @__PURE__ */ jsx(X, {}),
|
|
1460
|
+
"aria-label": "Close",
|
|
1461
|
+
tooltip: false,
|
|
1462
|
+
className: "-me-2 -mt-1 shrink-0",
|
|
1463
|
+
onClick: () => context.requestClose("close-button")
|
|
1464
|
+
})]
|
|
1465
|
+
});
|
|
1466
|
+
}
|
|
1467
|
+
/** Fills the space between header and footer and scrolls on its own; they stay put. */
|
|
1468
|
+
function DrawerBody({ className, ...props }) {
|
|
1469
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1470
|
+
...props,
|
|
1471
|
+
"data-slot": "drawer-body",
|
|
1472
|
+
className: cn("min-h-0 flex-1 overflow-y-auto overscroll-contain px-6 py-1 text-sm text-fg", className)
|
|
1473
|
+
});
|
|
1474
|
+
}
|
|
1475
|
+
/** Actions, right-aligned: Cancel first, the primary action last (rightmost). */
|
|
1476
|
+
function DrawerFooter({ className, ...props }) {
|
|
1477
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1478
|
+
...props,
|
|
1479
|
+
"data-slot": "drawer-footer",
|
|
1480
|
+
className: cn("flex shrink-0 flex-wrap items-center justify-end gap-2 px-6 pt-5 pb-6", className)
|
|
1481
|
+
});
|
|
1482
|
+
}
|
|
1483
|
+
/** A ghost icon button that closes the drawer, for custom headers. */
|
|
1484
|
+
function DrawerClose({ size = "sm", icon, ...props }) {
|
|
1485
|
+
const { requestClose } = useDrawerContext("DrawerClose");
|
|
1486
|
+
return /* @__PURE__ */ jsx(Button, {
|
|
1487
|
+
...props,
|
|
1488
|
+
intent: "ghost",
|
|
1489
|
+
size,
|
|
1490
|
+
icon: icon ?? /* @__PURE__ */ jsx(X, {}),
|
|
1491
|
+
tooltip: false,
|
|
1492
|
+
onClick: () => requestClose("close-button")
|
|
1493
|
+
});
|
|
1494
|
+
}
|
|
1495
|
+
//#endregion
|
|
1496
|
+
//#region src/components/input/copy-input.tsx
|
|
1497
|
+
const RESET_MS = 1500;
|
|
1498
|
+
function CopyInput({ value, mono = true, size = "md", onFocus, onClick, ...props }) {
|
|
1499
|
+
const inputRef = useRef(null);
|
|
1500
|
+
const [result, setResult] = useState("idle");
|
|
1501
|
+
const timer = useRef(void 0);
|
|
1502
|
+
useEffect(() => () => clearTimeout(timer.current), []);
|
|
1503
|
+
const copy = async () => {
|
|
1504
|
+
clearTimeout(timer.current);
|
|
1505
|
+
try {
|
|
1506
|
+
await navigator.clipboard.writeText(value);
|
|
1507
|
+
setResult("copied");
|
|
1508
|
+
} catch {
|
|
1509
|
+
setResult("failed");
|
|
1510
|
+
}
|
|
1511
|
+
timer.current = setTimeout(() => setResult("idle"), RESET_MS);
|
|
1512
|
+
};
|
|
1513
|
+
return /* @__PURE__ */ jsx(FieldFrame, {
|
|
1514
|
+
...props,
|
|
1515
|
+
value,
|
|
1516
|
+
readOnly: true,
|
|
1517
|
+
mono,
|
|
1518
|
+
size,
|
|
1519
|
+
inputRef,
|
|
1520
|
+
status: result === "copied" ? "Copied" : result === "failed" ? "Copy failed" : "",
|
|
1521
|
+
onFocus: (e) => {
|
|
1522
|
+
e.currentTarget.select();
|
|
1523
|
+
onFocus?.(e);
|
|
1524
|
+
},
|
|
1525
|
+
onClick: (e) => {
|
|
1526
|
+
e.currentTarget.select();
|
|
1527
|
+
onClick?.(e);
|
|
1528
|
+
},
|
|
1529
|
+
actions: props.disabled ? null : /* @__PURE__ */ jsx(Button, {
|
|
1530
|
+
icon: result === "copied" ? /* @__PURE__ */ jsx(Check, {}) : /* @__PURE__ */ jsx(Copy, {}),
|
|
1531
|
+
"aria-label": "Copy",
|
|
1532
|
+
intent: "ghost",
|
|
1533
|
+
size,
|
|
1534
|
+
className: compactButton[size],
|
|
1535
|
+
onClick: copy
|
|
1536
|
+
})
|
|
1537
|
+
});
|
|
1538
|
+
}
|
|
1539
|
+
//#endregion
|
|
1540
|
+
//#region src/components/input/input.tsx
|
|
1541
|
+
function Input({ type = "text", ...props }) {
|
|
1542
|
+
return /* @__PURE__ */ jsx(FieldFrame, {
|
|
1543
|
+
...props,
|
|
1544
|
+
type
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
//#endregion
|
|
1548
|
+
//#region src/components/input/password-input.tsx
|
|
1549
|
+
function PasswordInput({ autoComplete = "current-password", size = "md", ...props }) {
|
|
1550
|
+
const inputRef = useRef(null);
|
|
1551
|
+
const [visible, setVisible] = useState(false);
|
|
1552
|
+
return /* @__PURE__ */ jsx(FieldFrame, {
|
|
1553
|
+
...props,
|
|
1554
|
+
size,
|
|
1555
|
+
type: visible ? "text" : "password",
|
|
1556
|
+
autoComplete,
|
|
1557
|
+
inputRef,
|
|
1558
|
+
actions: props.disabled ? null : /* @__PURE__ */ jsx(Button, {
|
|
1559
|
+
icon: visible ? /* @__PURE__ */ jsx(EyeOff, {}) : /* @__PURE__ */ jsx(Eye, {}),
|
|
1560
|
+
"aria-label": "Show password",
|
|
1561
|
+
"aria-pressed": visible,
|
|
1562
|
+
intent: "ghost",
|
|
1563
|
+
size,
|
|
1564
|
+
className: compactButton[size],
|
|
1565
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
1566
|
+
onClick: () => setVisible((v) => !v)
|
|
1567
|
+
})
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1570
|
+
//#endregion
|
|
1571
|
+
//#region src/components/input/search-input.tsx
|
|
1572
|
+
function setNativeValue(input, value) {
|
|
1573
|
+
Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set?.call(input, value);
|
|
1574
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
1575
|
+
}
|
|
1576
|
+
function SearchInput({ onClear, onChange, onKeyDown, inputClassName, size = "md", ...props }) {
|
|
1577
|
+
const inputRef = useRef(null);
|
|
1578
|
+
const controlled = props.value !== void 0;
|
|
1579
|
+
const [filled, setFilled] = useState(() => Boolean(props.defaultValue));
|
|
1580
|
+
const canClear = (controlled ? String(props.value) !== "" : filled) && !props.disabled && !props.readOnly;
|
|
1581
|
+
const clear = () => {
|
|
1582
|
+
const input = inputRef.current;
|
|
1583
|
+
if (!input) return;
|
|
1584
|
+
setNativeValue(input, "");
|
|
1585
|
+
onClear?.();
|
|
1586
|
+
input.focus();
|
|
1587
|
+
};
|
|
1588
|
+
return /* @__PURE__ */ jsx(FieldFrame, {
|
|
1589
|
+
...props,
|
|
1590
|
+
size,
|
|
1591
|
+
type: "search",
|
|
1592
|
+
inputRef,
|
|
1593
|
+
start: /* @__PURE__ */ jsx(Search, {}),
|
|
1594
|
+
inputClassName: cn("[&::-webkit-search-cancel-button]:appearance-none [&::-webkit-search-decoration]:appearance-none", inputClassName),
|
|
1595
|
+
onChange: (e) => {
|
|
1596
|
+
if (!controlled) setFilled(e.currentTarget.value !== "");
|
|
1597
|
+
onChange?.(e);
|
|
1598
|
+
},
|
|
1599
|
+
onKeyDown: (e) => {
|
|
1600
|
+
onKeyDown?.(e);
|
|
1601
|
+
if (e.key !== "Escape" || e.defaultPrevented || e.currentTarget.value === "") return;
|
|
1602
|
+
if (props.readOnly) return;
|
|
1603
|
+
e.preventDefault();
|
|
1604
|
+
e.stopPropagation();
|
|
1605
|
+
clear();
|
|
1606
|
+
},
|
|
1607
|
+
actions: canClear ? /* @__PURE__ */ jsx(Button, {
|
|
1608
|
+
icon: /* @__PURE__ */ jsx(X, {}),
|
|
1609
|
+
"aria-label": "Clear search",
|
|
1610
|
+
intent: "ghost",
|
|
1611
|
+
size,
|
|
1612
|
+
className: compactButton[size],
|
|
1613
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
1614
|
+
onClick: clear
|
|
1615
|
+
}) : null
|
|
1616
|
+
});
|
|
1617
|
+
}
|
|
1618
|
+
//#endregion
|
|
1619
|
+
//#region src/components/meter/meter.tsx
|
|
1620
|
+
const toneClasses = {
|
|
1621
|
+
neutral: "bg-neutral-border",
|
|
1622
|
+
info: "bg-info",
|
|
1623
|
+
success: "bg-success",
|
|
1624
|
+
warning: "bg-warning",
|
|
1625
|
+
danger: "bg-danger"
|
|
1626
|
+
};
|
|
1627
|
+
/**
|
|
1628
|
+
* Stacked proportion bar with a legend. The bar is decorative; the legend list carries the
|
|
1629
|
+
* numbers for assistive tech, so nothing is conveyed by color alone.
|
|
1630
|
+
*/
|
|
1631
|
+
function Meter({ label, summary, segments, className, ...props }) {
|
|
1632
|
+
const labelId = useId();
|
|
1633
|
+
const shown = segments.filter((s) => s.value > 0);
|
|
1634
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1635
|
+
...props,
|
|
1636
|
+
"data-slot": "meter",
|
|
1637
|
+
className: cn("text-xs", className),
|
|
1638
|
+
children: [
|
|
1639
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1640
|
+
className: "flex justify-between gap-3",
|
|
1641
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
1642
|
+
id: labelId,
|
|
1643
|
+
className: "font-medium",
|
|
1644
|
+
children: label
|
|
1645
|
+
}), summary && /* @__PURE__ */ jsx("span", {
|
|
1646
|
+
className: "text-fg-muted",
|
|
1647
|
+
children: summary
|
|
1648
|
+
})]
|
|
1649
|
+
}),
|
|
1650
|
+
/* @__PURE__ */ jsx("div", {
|
|
1651
|
+
"aria-hidden": true,
|
|
1652
|
+
className: "mt-2 flex h-2 gap-0.5 overflow-hidden rounded-full bg-surface-muted",
|
|
1653
|
+
children: shown.map((s) => /* @__PURE__ */ jsx("span", {
|
|
1654
|
+
className: cn("min-w-1", toneClasses[s.tone]),
|
|
1655
|
+
style: { flex: s.value }
|
|
1656
|
+
}, s.label))
|
|
1657
|
+
}),
|
|
1658
|
+
shown.length > 0 && /* @__PURE__ */ jsx("ul", {
|
|
1659
|
+
"aria-labelledby": labelId,
|
|
1660
|
+
className: "mt-2 flex flex-wrap gap-x-3.5 gap-y-1 text-fg-muted",
|
|
1661
|
+
children: shown.map((s) => /* @__PURE__ */ jsxs("li", {
|
|
1662
|
+
className: "flex items-center gap-1.5",
|
|
1663
|
+
children: [
|
|
1664
|
+
/* @__PURE__ */ jsx("span", {
|
|
1665
|
+
"aria-hidden": true,
|
|
1666
|
+
className: cn("size-2 rounded-xs", toneClasses[s.tone])
|
|
1667
|
+
}),
|
|
1668
|
+
/* @__PURE__ */ jsx("span", {
|
|
1669
|
+
className: "font-medium text-fg",
|
|
1670
|
+
children: s.value
|
|
1671
|
+
}),
|
|
1672
|
+
s.label
|
|
1673
|
+
]
|
|
1674
|
+
}, s.label))
|
|
1675
|
+
})
|
|
1676
|
+
]
|
|
1677
|
+
});
|
|
1678
|
+
}
|
|
1679
|
+
//#endregion
|
|
1680
|
+
//#region src/components/select/select.tsx
|
|
1681
|
+
const selectPadding = {
|
|
1682
|
+
sm: "px-3",
|
|
1683
|
+
md: "px-3",
|
|
1684
|
+
lg: "px-4"
|
|
1685
|
+
};
|
|
1686
|
+
const iconStartPadding = {
|
|
1687
|
+
sm: "ps-8",
|
|
1688
|
+
md: "ps-[34px]",
|
|
1689
|
+
lg: "ps-[38px]"
|
|
1690
|
+
};
|
|
1691
|
+
const chevronEndPadding = {
|
|
1692
|
+
sm: "pe-8",
|
|
1693
|
+
md: "pe-[34px]",
|
|
1694
|
+
lg: "pe-[38px]"
|
|
1695
|
+
};
|
|
1696
|
+
const labelRestingWidth = {
|
|
1697
|
+
sm: "max-w-[calc(100%-40px)]",
|
|
1698
|
+
md: "max-w-[calc(100%-42px)]",
|
|
1699
|
+
lg: "max-w-[calc(100%-50px)]"
|
|
1700
|
+
};
|
|
1701
|
+
const isGroup = (item) => "options" in item;
|
|
1702
|
+
const pickerClasses = cn("supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[appearance:base-select]", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:[appearance:base-select]", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker-icon]:hidden", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:[position-area:block-end_span-inline-end]", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:[position-try-fallbacks:flip-block]", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:mt-1", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:min-w-[anchor-size(width)]", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:max-h-[max(10rem,calc(100%-12px))]", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:overflow-y-auto", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:rounded-xl", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:border", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:border-popover-border", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:bg-popover-bg", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:p-1", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:shadow-popover", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:text-fg", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:origin-top", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:opacity-0", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:scale-96", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:transition-[opacity,scale,display,overlay]", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:transition-discrete", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:duration-80", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:ease-out", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:open:[&::picker(select)]:opacity-100", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:open:[&::picker(select)]:scale-100", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:open:[&::picker(select)]:duration-120", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:open:[&::picker(select)]:starting:opacity-0", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:open:[&::picker(select)]:starting:scale-96", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:motion-reduce:[&::picker(select)]:transition-none");
|
|
1703
|
+
function optionClasses(size, mono) {
|
|
1704
|
+
return cn("bg-popover-bg text-fg checked:font-medium disabled:text-fg-subtle", textSize[size], mono && "font-mono", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:flex", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:cursor-default", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:items-center", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:gap-2", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:rounded-lg", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:bg-transparent", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:px-2", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:py-1.5", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:not-disabled:hover:bg-popover-highlight", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:focus-visible:bg-popover-highlight", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:focus-visible:outline-hidden", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:disabled:text-fg", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:disabled:opacity-50", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::checkmark]:order-1", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::checkmark]:ms-auto");
|
|
1705
|
+
}
|
|
1706
|
+
function openPicker(select) {
|
|
1707
|
+
select.focus();
|
|
1708
|
+
try {
|
|
1709
|
+
select.showPicker?.();
|
|
1710
|
+
} catch {}
|
|
1711
|
+
}
|
|
1712
|
+
/**
|
|
1713
|
+
* Single-choice select: a real `<select>` (native keyboard, typeahead, forms, validation and
|
|
1714
|
+
* autofill), customized through `appearance: base-select` where supported. Same frame as Input.
|
|
1715
|
+
*/
|
|
1716
|
+
function Select({ options, placeholder, helperText, error, size = "md", start, mono = false, className, selectClassName, label, floating = false, id, required, disabled, value, defaultValue, onChange, ref, "aria-describedby": describedByProp, "aria-invalid": ariaInvalid, ...props }) {
|
|
1717
|
+
const selectRef = useRef(null);
|
|
1718
|
+
const setRef = useMergedRef(selectRef, ref);
|
|
1719
|
+
const controlled = value !== void 0;
|
|
1720
|
+
const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue ?? "");
|
|
1721
|
+
const current = controlled ? value : uncontrolledValue;
|
|
1722
|
+
useEffect(() => {
|
|
1723
|
+
const form = selectRef.current?.form;
|
|
1724
|
+
if (!form) return;
|
|
1725
|
+
let timer;
|
|
1726
|
+
const onReset = () => {
|
|
1727
|
+
timer = setTimeout(() => {
|
|
1728
|
+
if (selectRef.current) setUncontrolledValue(selectRef.current.value);
|
|
1729
|
+
});
|
|
1730
|
+
};
|
|
1731
|
+
form.addEventListener("reset", onReset);
|
|
1732
|
+
return () => {
|
|
1733
|
+
form.removeEventListener("reset", onReset);
|
|
1734
|
+
clearTimeout(timer);
|
|
1735
|
+
};
|
|
1736
|
+
}, [props.form]);
|
|
1737
|
+
const leading = options.flatMap((item) => isGroup(item) ? item.options : [item]).find((option) => option.value === current)?.icon ?? start;
|
|
1738
|
+
const hasLeading = isPresent(leading);
|
|
1739
|
+
const optionClass = optionClasses(size, mono);
|
|
1740
|
+
const renderOption = (option) => /* @__PURE__ */ jsxs("option", {
|
|
1741
|
+
value: option.value,
|
|
1742
|
+
disabled: option.disabled,
|
|
1743
|
+
className: optionClass,
|
|
1744
|
+
children: [isPresent(option.icon) && /* @__PURE__ */ jsx("span", {
|
|
1745
|
+
"aria-hidden": true,
|
|
1746
|
+
className: "flex shrink-0 text-fg-muted [&_svg]:size-4 [&_svg]:shrink-0",
|
|
1747
|
+
children: option.icon
|
|
1748
|
+
}), /* @__PURE__ */ jsx("span", { children: option.label })]
|
|
1749
|
+
}, option.value);
|
|
1750
|
+
return /* @__PURE__ */ jsx(FieldShell, {
|
|
1751
|
+
label,
|
|
1752
|
+
floating,
|
|
1753
|
+
helperText,
|
|
1754
|
+
error,
|
|
1755
|
+
size,
|
|
1756
|
+
id,
|
|
1757
|
+
required,
|
|
1758
|
+
disabled,
|
|
1759
|
+
"aria-describedby": describedByProp,
|
|
1760
|
+
hasStart: hasLeading,
|
|
1761
|
+
className,
|
|
1762
|
+
controlRef: selectRef,
|
|
1763
|
+
activate: openPicker,
|
|
1764
|
+
children: (field) => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1765
|
+
field.floatingLabel(labelRestingWidth[size]),
|
|
1766
|
+
/* @__PURE__ */ jsxs("select", {
|
|
1767
|
+
...props,
|
|
1768
|
+
ref: setRef,
|
|
1769
|
+
id: field.id,
|
|
1770
|
+
required,
|
|
1771
|
+
disabled,
|
|
1772
|
+
...controlled ? { value } : { defaultValue: defaultValue ?? "" },
|
|
1773
|
+
onChange: (e) => {
|
|
1774
|
+
if (!controlled) setUncontrolledValue(e.currentTarget.value);
|
|
1775
|
+
onChange?.(e);
|
|
1776
|
+
},
|
|
1777
|
+
"aria-invalid": field.invalid ? true : ariaInvalid,
|
|
1778
|
+
"aria-describedby": field.describedBy,
|
|
1779
|
+
className: cn("size-full min-w-0 appearance-none items-center truncate rounded-[inherit] bg-transparent text-fg outline-none", "pointer-coarse:text-base disabled:cursor-not-allowed", "[&:has(option[value=\"\"]:checked)]:text-input-placeholder", textSize[size], selectPadding[size], hasLeading && iconStartPadding[size], chevronEndPadding[size], mono && "font-mono", pickerClasses, selectClassName),
|
|
1780
|
+
children: [/* @__PURE__ */ jsx("option", {
|
|
1781
|
+
value: "",
|
|
1782
|
+
disabled: true,
|
|
1783
|
+
hidden: true,
|
|
1784
|
+
children: field.floating ? "" : placeholder ?? ""
|
|
1785
|
+
}), options.map((item) => isGroup(item) ? /* @__PURE__ */ jsx("optgroup", {
|
|
1786
|
+
label: item.label,
|
|
1787
|
+
className: cn("bg-popover-bg font-sans text-fg", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:text-xs", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:font-medium", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:text-fg-muted", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:not-first-of-type:mt-1"),
|
|
1788
|
+
children: item.options.map(renderOption)
|
|
1789
|
+
}, `group:${item.label}`) : renderOption(item))]
|
|
1790
|
+
}),
|
|
1791
|
+
hasLeading && /* @__PURE__ */ jsx("span", {
|
|
1792
|
+
"aria-hidden": true,
|
|
1793
|
+
className: cn("pointer-events-none absolute inset-y-0 inset-s-0 flex items-center text-fg-muted [&_svg]:shrink-0", iconSize[size], startSlotPadding[size]),
|
|
1794
|
+
children: leading
|
|
1795
|
+
}),
|
|
1796
|
+
/* @__PURE__ */ jsx("span", {
|
|
1797
|
+
"aria-hidden": true,
|
|
1798
|
+
className: cn("pointer-events-none absolute inset-y-0 inset-e-0 flex items-center text-fg-muted", iconSize[size], endSlotPadding[size]),
|
|
1799
|
+
children: /* @__PURE__ */ jsx(ChevronDown, { className: "transition-transform duration-150 ease-out group-has-[select:open]/control:rotate-180 motion-reduce:transition-none" })
|
|
1800
|
+
})
|
|
1801
|
+
] })
|
|
1802
|
+
});
|
|
1803
|
+
}
|
|
1804
|
+
//#endregion
|
|
1805
|
+
//#region src/lib/slot.tsx
|
|
1806
|
+
function setRef(ref, node) {
|
|
1807
|
+
if (typeof ref === "function") return ref(node);
|
|
1808
|
+
if (ref) ref.current = node;
|
|
1809
|
+
}
|
|
1810
|
+
function composeRefs(...refs) {
|
|
1811
|
+
return (node) => {
|
|
1812
|
+
const cleanups = refs.map((ref) => setRef(ref, node));
|
|
1813
|
+
return () => {
|
|
1814
|
+
for (const [i, ref] of refs.entries()) {
|
|
1815
|
+
const cleanup = cleanups[i];
|
|
1816
|
+
if (typeof cleanup === "function") cleanup();
|
|
1817
|
+
else setRef(ref, null);
|
|
1818
|
+
}
|
|
1819
|
+
};
|
|
1820
|
+
};
|
|
1821
|
+
}
|
|
1822
|
+
function mergeProps(slot, child) {
|
|
1823
|
+
const merged = {
|
|
1824
|
+
...slot,
|
|
1825
|
+
...child
|
|
1826
|
+
};
|
|
1827
|
+
for (const key of Object.keys(slot)) {
|
|
1828
|
+
const own = slot[key];
|
|
1829
|
+
const theirs = child[key];
|
|
1830
|
+
if (/^on[A-Z]/.test(key) && typeof own === "function" && typeof theirs === "function") merged[key] = (...args) => {
|
|
1831
|
+
theirs(...args);
|
|
1832
|
+
own(...args);
|
|
1833
|
+
};
|
|
1834
|
+
else if (key === "className") merged.className = cn(own, theirs);
|
|
1835
|
+
else if (key === "style") merged.style = {
|
|
1836
|
+
...own,
|
|
1837
|
+
...theirs
|
|
1838
|
+
};
|
|
1839
|
+
}
|
|
1840
|
+
return merged;
|
|
1841
|
+
}
|
|
1842
|
+
/** Applies `wrap` to an element's children, or to what a render-prop child returns. */
|
|
1843
|
+
function wrapChildren(children, wrap) {
|
|
1844
|
+
if (typeof children !== "function") return wrap(children);
|
|
1845
|
+
return (...args) => wrap(children(...args));
|
|
1846
|
+
}
|
|
1847
|
+
/** Renders its only child with these props merged in (Radix Slot style). */
|
|
1848
|
+
function Slot({ children, ref, ...props }) {
|
|
1849
|
+
const child = Children.only(children);
|
|
1850
|
+
const childProps = isValidElement(child) ? child.props : {};
|
|
1851
|
+
const childRef = childProps.ref;
|
|
1852
|
+
const composedRef = useMemo(() => composeRefs(ref, childRef), [ref, childRef]);
|
|
1853
|
+
if (!isValidElement(child)) return null;
|
|
1854
|
+
return cloneElement(child, {
|
|
1855
|
+
...mergeProps(props, childProps),
|
|
1856
|
+
ref: composedRef
|
|
1857
|
+
});
|
|
1858
|
+
}
|
|
1859
|
+
//#endregion
|
|
1860
|
+
//#region src/components/tabs/shared.tsx
|
|
1861
|
+
const listBase = [
|
|
1862
|
+
"relative isolate max-w-full overflow-x-auto overscroll-x-contain scroll-px-6",
|
|
1863
|
+
"[mask-image:linear-gradient(to_right,transparent,#000_var(--tabs-fade-left,0px),#000_calc(100%-var(--tabs-fade-right,0px)),transparent)]",
|
|
1864
|
+
"[scrollbar-width:none] [&::-webkit-scrollbar]:hidden",
|
|
1865
|
+
"before:pointer-events-none before:absolute before:left-0 before:-z-1 before:opacity-0",
|
|
1866
|
+
"before:w-(--tabs-indicator-w) data-indicator:before:opacity-100",
|
|
1867
|
+
"before:duration-200 before:ease-[cubic-bezier(0.16,1,0.3,1)]",
|
|
1868
|
+
"data-instant:before:transition-none data-instant:after:transition-none"
|
|
1869
|
+
];
|
|
1870
|
+
const listVariant = {
|
|
1871
|
+
line: [
|
|
1872
|
+
"flex items-center shadow-[inset_0_-1px_0_var(--color-border-strong)]",
|
|
1873
|
+
"before:bottom-0 before:h-0.5 before:bg-fg before:[translate:var(--tabs-indicator-x)_0]",
|
|
1874
|
+
"before:transition-[translate,width]",
|
|
1875
|
+
"forced-colors:before:bg-[Highlight] forced-colors:before:[forced-color-adjust:none]",
|
|
1876
|
+
"after:pointer-events-none after:absolute after:top-0 after:left-0 after:-z-1 after:rounded-lg",
|
|
1877
|
+
"after:bg-fg/7 after:opacity-0 data-hover:after:opacity-100",
|
|
1878
|
+
"after:h-(--tabs-hover-h) after:w-(--tabs-hover-w)",
|
|
1879
|
+
"after:[translate:var(--tabs-hover-x)_var(--tabs-hover-y)]",
|
|
1880
|
+
"after:transition-[translate,width,height,opacity] after:duration-150",
|
|
1881
|
+
"after:ease-[cubic-bezier(0.33,1,0.68,1)]",
|
|
1882
|
+
"data-hover-enter:after:transition-opacity"
|
|
1883
|
+
],
|
|
1884
|
+
segmented: [
|
|
1885
|
+
"inline-flex w-fit rounded-md p-1 shadow-[inset_0_0_0_1px_var(--color-ring)]",
|
|
1886
|
+
"before:top-0 before:h-(--tabs-indicator-h) before:rounded-sm before:bg-fg/8",
|
|
1887
|
+
"before:[translate:var(--tabs-indicator-x)_var(--tabs-indicator-y)]",
|
|
1888
|
+
"before:transition-[translate,width,height]",
|
|
1889
|
+
"forced-colors:before:border-2 forced-colors:before:border-[Highlight]"
|
|
1890
|
+
]
|
|
1891
|
+
};
|
|
1892
|
+
const listSize = {
|
|
1893
|
+
line: {
|
|
1894
|
+
sm: "h-10",
|
|
1895
|
+
md: "h-12"
|
|
1896
|
+
},
|
|
1897
|
+
segmented: {
|
|
1898
|
+
sm: "h-8",
|
|
1899
|
+
md: "h-9"
|
|
1900
|
+
}
|
|
1901
|
+
};
|
|
1902
|
+
function listClasses(variant, size) {
|
|
1903
|
+
return [
|
|
1904
|
+
...listBase,
|
|
1905
|
+
...listVariant[variant],
|
|
1906
|
+
listSize[variant][size],
|
|
1907
|
+
variant === "segmented" ? "pointer-coarse:h-13" : "pointer-coarse:h-12"
|
|
1908
|
+
];
|
|
1909
|
+
}
|
|
1910
|
+
const itemBase = [
|
|
1911
|
+
"relative inline-flex shrink-0 cursor-pointer items-center justify-center",
|
|
1912
|
+
"text-sm whitespace-nowrap text-fg-muted no-underline select-none",
|
|
1913
|
+
"touch-manipulation [-webkit-tap-highlight-color:transparent]",
|
|
1914
|
+
"transition-colors duration-150 ease-[cubic-bezier(0.4,0,0.2,1)]",
|
|
1915
|
+
"focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-focus",
|
|
1916
|
+
"pointer-coarse:min-h-11 pointer-coarse:min-w-11",
|
|
1917
|
+
"not-data-disabled:not-aria-disabled:hover:text-fg data-selected:text-fg aria-[current=page]:text-fg",
|
|
1918
|
+
"data-disabled:cursor-not-allowed data-disabled:opacity-50",
|
|
1919
|
+
"aria-disabled:cursor-not-allowed aria-disabled:opacity-50",
|
|
1920
|
+
"forced-colors:data-disabled:text-[GrayText] forced-colors:data-disabled:opacity-100",
|
|
1921
|
+
"forced-colors:aria-disabled:text-[GrayText] forced-colors:aria-disabled:opacity-100"
|
|
1922
|
+
];
|
|
1923
|
+
const itemVariant = {
|
|
1924
|
+
line: ["rounded-lg data-selected:font-medium aria-[current=page]:font-medium"],
|
|
1925
|
+
segmented: ["h-full rounded-sm font-medium"]
|
|
1926
|
+
};
|
|
1927
|
+
const itemSize = {
|
|
1928
|
+
line: {
|
|
1929
|
+
sm: "h-7 px-2.5",
|
|
1930
|
+
md: "h-8 px-3"
|
|
1931
|
+
},
|
|
1932
|
+
segmented: {
|
|
1933
|
+
sm: "px-2.5",
|
|
1934
|
+
md: "px-3"
|
|
1935
|
+
}
|
|
1936
|
+
};
|
|
1937
|
+
function itemClasses$1(variant, size) {
|
|
1938
|
+
return [
|
|
1939
|
+
...itemBase,
|
|
1940
|
+
...itemVariant[variant],
|
|
1941
|
+
itemSize[variant][size]
|
|
1942
|
+
];
|
|
1943
|
+
}
|
|
1944
|
+
/** Icon, label and badge, laid out the same in both components. */
|
|
1945
|
+
function TabContent({ icon, badge, children }) {
|
|
1946
|
+
return /* @__PURE__ */ jsxs("span", {
|
|
1947
|
+
"data-slot": "tab-content",
|
|
1948
|
+
className: "inline-flex items-center gap-2",
|
|
1949
|
+
children: [
|
|
1950
|
+
icon != null && icon !== false && /* @__PURE__ */ jsx("span", {
|
|
1951
|
+
"aria-hidden": true,
|
|
1952
|
+
"data-slot": "tab-icon",
|
|
1953
|
+
className: "inline-flex shrink-0 [&_svg]:size-4",
|
|
1954
|
+
children: icon
|
|
1955
|
+
}),
|
|
1956
|
+
/* @__PURE__ */ jsxs("span", {
|
|
1957
|
+
className: "grid justify-items-center",
|
|
1958
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
1959
|
+
className: "col-start-1 row-start-1",
|
|
1960
|
+
children
|
|
1961
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
1962
|
+
"aria-hidden": true,
|
|
1963
|
+
"data-slot": "tab-label-sizer",
|
|
1964
|
+
className: "invisible col-start-1 row-start-1 font-medium",
|
|
1965
|
+
children
|
|
1966
|
+
})]
|
|
1967
|
+
}),
|
|
1968
|
+
badge != null && badge !== false && /* @__PURE__ */ jsxs(Fragment, { children: [" ", /* @__PURE__ */ jsx("span", {
|
|
1969
|
+
"data-slot": "tab-badge",
|
|
1970
|
+
className: "inline-flex",
|
|
1971
|
+
children: badge
|
|
1972
|
+
})] })
|
|
1973
|
+
]
|
|
1974
|
+
});
|
|
1975
|
+
}
|
|
1976
|
+
function boxIn(list, el) {
|
|
1977
|
+
const lr = list.getBoundingClientRect();
|
|
1978
|
+
const r = el.getBoundingClientRect();
|
|
1979
|
+
const scale = list.offsetWidth ? lr.width / list.offsetWidth : 1;
|
|
1980
|
+
return {
|
|
1981
|
+
x: (r.left - lr.left) / scale - list.clientLeft + list.scrollLeft,
|
|
1982
|
+
y: (r.top - lr.top) / scale - list.clientTop + list.scrollTop,
|
|
1983
|
+
w: r.width / scale,
|
|
1984
|
+
h: r.height / scale
|
|
1985
|
+
};
|
|
1986
|
+
}
|
|
1987
|
+
function setBox(list, name, { x, y, w, h }) {
|
|
1988
|
+
list.style.setProperty(`--tabs-${name}-x`, `${x}px`);
|
|
1989
|
+
list.style.setProperty(`--tabs-${name}-y`, `${y}px`);
|
|
1990
|
+
list.style.setProperty(`--tabs-${name}-w`, `${w}px`);
|
|
1991
|
+
list.style.setProperty(`--tabs-${name}-h`, `${h}px`);
|
|
1992
|
+
}
|
|
1993
|
+
const isSelected = (item) => item.dataset.selected !== void 0 || item.getAttribute("aria-current") === "page";
|
|
1994
|
+
const isDisabled = (item) => item.dataset.disabled !== void 0 || item.getAttribute("aria-disabled") === "true";
|
|
1995
|
+
/** This list's own items; a nested tab list keeps its items to itself. */
|
|
1996
|
+
function ownItems(list) {
|
|
1997
|
+
return [...list.querySelectorAll("[data-slot=\"tab\"]")].filter((item) => item.closest("[data-slot=\"tab-list\"]") === list);
|
|
1998
|
+
}
|
|
1999
|
+
const LINE_BAR_OUTSET = 2;
|
|
2000
|
+
const EDGE_FADE = 24;
|
|
2001
|
+
function setEdgeFade(list) {
|
|
2002
|
+
const max = list.scrollWidth - list.clientWidth;
|
|
2003
|
+
const left = getComputedStyle(list).direction === "rtl" ? max + list.scrollLeft : list.scrollLeft;
|
|
2004
|
+
const fade = (distance) => Math.min(Math.max(Math.round(distance), 0), EDGE_FADE);
|
|
2005
|
+
list.style.setProperty("--tabs-fade-left", `${fade(left)}px`);
|
|
2006
|
+
list.style.setProperty("--tabs-fade-right", `${fade(max - left)}px`);
|
|
2007
|
+
}
|
|
2008
|
+
function useTabListMotion(variant, { revealSelected = false } = {}) {
|
|
2009
|
+
const listRef = useRef(null);
|
|
2010
|
+
const hovered = useRef(null);
|
|
2011
|
+
useLayoutEffect(() => {
|
|
2012
|
+
const list = listRef.current;
|
|
2013
|
+
if (!list) return;
|
|
2014
|
+
let revealed;
|
|
2015
|
+
const place = (instant) => {
|
|
2016
|
+
setEdgeFade(list);
|
|
2017
|
+
const item = ownItems(list).find(isSelected);
|
|
2018
|
+
if (!item) {
|
|
2019
|
+
delete list.dataset.indicator;
|
|
2020
|
+
return;
|
|
2021
|
+
}
|
|
2022
|
+
if (revealSelected && item !== revealed) {
|
|
2023
|
+
revealed = item;
|
|
2024
|
+
revealInList(list, item);
|
|
2025
|
+
}
|
|
2026
|
+
const content = item.querySelector("[data-slot=\"tab-content\"]");
|
|
2027
|
+
const box = boxIn(list, variant === "line" && content ? content : item);
|
|
2028
|
+
if (variant === "line") {
|
|
2029
|
+
box.x -= LINE_BAR_OUTSET;
|
|
2030
|
+
box.w += 4;
|
|
2031
|
+
}
|
|
2032
|
+
if (instant || list.dataset.indicator === void 0) {
|
|
2033
|
+
list.dataset.instant = "";
|
|
2034
|
+
setBox(list, "indicator", box);
|
|
2035
|
+
list.dataset.indicator = "";
|
|
2036
|
+
list.offsetWidth;
|
|
2037
|
+
delete list.dataset.instant;
|
|
2038
|
+
} else setBox(list, "indicator", box);
|
|
2039
|
+
};
|
|
2040
|
+
place(true);
|
|
2041
|
+
const onScroll = () => setEdgeFade(list);
|
|
2042
|
+
list.addEventListener("scroll", onScroll, { passive: true });
|
|
2043
|
+
const resizes = typeof ResizeObserver === "undefined" ? void 0 : new ResizeObserver(() => place(true));
|
|
2044
|
+
resizes?.observe(list);
|
|
2045
|
+
for (const item of ownItems(list)) resizes?.observe(item);
|
|
2046
|
+
const mutations = new MutationObserver(() => {
|
|
2047
|
+
place(false);
|
|
2048
|
+
for (const item of ownItems(list)) resizes?.observe(item);
|
|
2049
|
+
});
|
|
2050
|
+
mutations.observe(list, {
|
|
2051
|
+
subtree: true,
|
|
2052
|
+
childList: true,
|
|
2053
|
+
attributes: true,
|
|
2054
|
+
attributeFilter: ["data-selected", "aria-current"]
|
|
2055
|
+
});
|
|
2056
|
+
return () => {
|
|
2057
|
+
list.removeEventListener("scroll", onScroll);
|
|
2058
|
+
mutations.disconnect();
|
|
2059
|
+
resizes?.disconnect();
|
|
2060
|
+
};
|
|
2061
|
+
}, [variant, revealSelected]);
|
|
2062
|
+
const onPointerMove = (e) => {
|
|
2063
|
+
const list = listRef.current;
|
|
2064
|
+
if (variant !== "line" || !list || e.pointerType && e.pointerType !== "mouse") return;
|
|
2065
|
+
const item = e.target.closest("[data-slot=\"tab\"]");
|
|
2066
|
+
if (!item || isDisabled(item) || !ownItems(list).includes(item)) return;
|
|
2067
|
+
const entering = list.dataset.hover === void 0;
|
|
2068
|
+
if (item === hovered.current && !entering) return;
|
|
2069
|
+
hovered.current = item;
|
|
2070
|
+
if (entering) list.dataset.hoverEnter = "";
|
|
2071
|
+
else delete list.dataset.hoverEnter;
|
|
2072
|
+
setBox(list, "hover", boxIn(list, item));
|
|
2073
|
+
list.dataset.hover = "";
|
|
2074
|
+
};
|
|
2075
|
+
const onPointerLeave = () => {
|
|
2076
|
+
const list = listRef.current;
|
|
2077
|
+
if (!list) return;
|
|
2078
|
+
hovered.current = null;
|
|
2079
|
+
delete list.dataset.hover;
|
|
2080
|
+
};
|
|
2081
|
+
return {
|
|
2082
|
+
listRef,
|
|
2083
|
+
onPointerMove,
|
|
2084
|
+
onPointerLeave
|
|
2085
|
+
};
|
|
2086
|
+
}
|
|
2087
|
+
function revealInList(list, el) {
|
|
2088
|
+
const pad = Number.parseFloat(getComputedStyle(list).scrollPaddingInlineStart) || 0;
|
|
2089
|
+
const start = el.getBoundingClientRect().left - list.getBoundingClientRect().left;
|
|
2090
|
+
const end = start + el.offsetWidth;
|
|
2091
|
+
if (start < pad) list.scrollLeft += start - pad;
|
|
2092
|
+
else if (end > list.clientWidth - pad) list.scrollLeft += end - list.clientWidth + pad;
|
|
2093
|
+
}
|
|
2094
|
+
function useRevealWhenSelected(ref, selected) {
|
|
2095
|
+
useLayoutEffect(() => {
|
|
2096
|
+
const el = ref.current;
|
|
2097
|
+
const list = el?.closest("[data-slot=\"tab-list\"]");
|
|
2098
|
+
if (selected && el && list) revealInList(list, el);
|
|
2099
|
+
}, [ref, selected]);
|
|
2100
|
+
}
|
|
2101
|
+
function revealOnFocus(el) {
|
|
2102
|
+
el.scrollIntoView?.({
|
|
2103
|
+
block: "nearest",
|
|
2104
|
+
inline: "nearest"
|
|
2105
|
+
});
|
|
2106
|
+
}
|
|
2107
|
+
//#endregion
|
|
2108
|
+
//#region src/components/sidebar/sidebar.tsx
|
|
2109
|
+
/** At and above this width the sidebar is part of the layout; below it, a drawer. */
|
|
2110
|
+
const WIDE_QUERY = "(min-width: 960px)";
|
|
2111
|
+
const SidebarContext = createContext(null);
|
|
2112
|
+
function useSidebarContext(part) {
|
|
2113
|
+
const context = useContext(SidebarContext);
|
|
2114
|
+
if (!context) throw new Error(`${part} must be rendered inside <SidebarProvider>`);
|
|
2115
|
+
return context;
|
|
2116
|
+
}
|
|
2117
|
+
const PanelContext = createContext(null);
|
|
2118
|
+
function usePanelContext(part) {
|
|
2119
|
+
const context = useContext(PanelContext);
|
|
2120
|
+
if (!context) throw new Error(`${part} must be rendered inside <Sidebar>`);
|
|
2121
|
+
return context;
|
|
2122
|
+
}
|
|
2123
|
+
/**
|
|
2124
|
+
* Drawer open state shared by Sidebar and SidebarTrigger, which usually sit in different parts of
|
|
2125
|
+
* the app shell. One per sidebar; no shortcuts, no persistence.
|
|
2126
|
+
*/
|
|
2127
|
+
function SidebarProvider({ children }) {
|
|
2128
|
+
const [open, setOpen] = useState(false);
|
|
2129
|
+
const drawerId = useId();
|
|
2130
|
+
useEffect(() => {
|
|
2131
|
+
if (!open || typeof window.matchMedia !== "function") return;
|
|
2132
|
+
const wide = window.matchMedia(WIDE_QUERY);
|
|
2133
|
+
const onChange = () => {
|
|
2134
|
+
if (wide.matches) setOpen(false);
|
|
2135
|
+
};
|
|
2136
|
+
wide.addEventListener("change", onChange);
|
|
2137
|
+
return () => wide.removeEventListener("change", onChange);
|
|
2138
|
+
}, [open]);
|
|
2139
|
+
return /* @__PURE__ */ jsx(SidebarContext, {
|
|
2140
|
+
value: {
|
|
2141
|
+
open,
|
|
2142
|
+
setOpen,
|
|
2143
|
+
drawerId
|
|
2144
|
+
},
|
|
2145
|
+
children
|
|
2146
|
+
});
|
|
2147
|
+
}
|
|
2148
|
+
/**
|
|
2149
|
+
* App navigation. From 960px up it is docked at the start of the layout: 240px, full height, only
|
|
2150
|
+
* the body scrolls. Below 960px it is hidden and SidebarTrigger opens the same content in a drawer.
|
|
2151
|
+
* Needs a SidebarProvider around both.
|
|
2152
|
+
*/
|
|
2153
|
+
function Sidebar({ className, children, ...props }) {
|
|
2154
|
+
const { open, setOpen, drawerId } = useSidebarContext("Sidebar");
|
|
2155
|
+
const label = props["aria-label"] ? { "aria-label": props["aria-label"] } : { "aria-labelledby": props["aria-labelledby"] };
|
|
2156
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(PanelContext, {
|
|
2157
|
+
value: { drawer: false },
|
|
2158
|
+
children: /* @__PURE__ */ jsx("nav", {
|
|
2159
|
+
...props,
|
|
2160
|
+
"data-slot": "sidebar",
|
|
2161
|
+
className: cn("sticky top-0 hidden h-dvh w-60 shrink-0 flex-col border-e border-border-strong bg-bg", "min-[960px]:flex", className),
|
|
2162
|
+
children
|
|
2163
|
+
})
|
|
2164
|
+
}), /* @__PURE__ */ jsx(Drawer, {
|
|
2165
|
+
...label,
|
|
2166
|
+
id: drawerId,
|
|
2167
|
+
open,
|
|
2168
|
+
onOpenChange: setOpen,
|
|
2169
|
+
side: "left",
|
|
2170
|
+
size: "sm",
|
|
2171
|
+
children: /* @__PURE__ */ jsx(PanelContext, {
|
|
2172
|
+
value: { drawer: true },
|
|
2173
|
+
children: /* @__PURE__ */ jsx("nav", {
|
|
2174
|
+
...label,
|
|
2175
|
+
"data-slot": "sidebar",
|
|
2176
|
+
"data-drawer": "",
|
|
2177
|
+
className: "flex min-h-0 flex-1 flex-col",
|
|
2178
|
+
children
|
|
2179
|
+
})
|
|
2180
|
+
})
|
|
2181
|
+
})] });
|
|
2182
|
+
}
|
|
2183
|
+
/** Menu button that opens the drawer; only shown below 960px. Focus returns here on close. */
|
|
2184
|
+
function SidebarTrigger({ "aria-label": ariaLabel = "Open navigation", className, onClick, ...props }) {
|
|
2185
|
+
const { open, setOpen, drawerId } = useSidebarContext("SidebarTrigger");
|
|
2186
|
+
return /* @__PURE__ */ jsx(Button, {
|
|
2187
|
+
...props,
|
|
2188
|
+
intent: "ghost",
|
|
2189
|
+
icon: /* @__PURE__ */ jsx(Menu, {}),
|
|
2190
|
+
"aria-label": ariaLabel,
|
|
2191
|
+
"aria-expanded": open,
|
|
2192
|
+
"aria-controls": drawerId,
|
|
2193
|
+
tooltip: false,
|
|
2194
|
+
className: cn("min-[960px]:hidden", className),
|
|
2195
|
+
onClick: (e) => {
|
|
2196
|
+
onClick?.(e);
|
|
2197
|
+
if (!e.defaultPrevented) setOpen(true);
|
|
2198
|
+
}
|
|
2199
|
+
});
|
|
2200
|
+
}
|
|
2201
|
+
/** Brand row, pinned above the body. In the drawer it also holds the close button. */
|
|
2202
|
+
function SidebarHeader({ className, children, ...props }) {
|
|
2203
|
+
const { drawer } = usePanelContext("SidebarHeader");
|
|
2204
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2205
|
+
...props,
|
|
2206
|
+
"data-slot": "sidebar-header",
|
|
2207
|
+
className: cn("flex h-14 shrink-0 items-center gap-2 px-3", className),
|
|
2208
|
+
children: [children, drawer && /* @__PURE__ */ jsx(DrawerClose, {
|
|
2209
|
+
"aria-label": "Close navigation",
|
|
2210
|
+
className: "ms-auto shrink-0"
|
|
2211
|
+
})]
|
|
2212
|
+
});
|
|
2213
|
+
}
|
|
2214
|
+
function useHoverPill() {
|
|
2215
|
+
const ref = useRef(null);
|
|
2216
|
+
const hovered = useRef(null);
|
|
2217
|
+
const onPointerMove = (e) => {
|
|
2218
|
+
const body = ref.current;
|
|
2219
|
+
if (!body || e.pointerType && e.pointerType !== "mouse") return;
|
|
2220
|
+
const item = e.target.closest("[data-slot=\"sidebar-item\"]");
|
|
2221
|
+
if (!item || item.closest("[data-slot=\"sidebar-body\"]") !== body) return;
|
|
2222
|
+
const entering = body.dataset.hover === void 0;
|
|
2223
|
+
if (item === hovered.current && !entering) return;
|
|
2224
|
+
hovered.current = item;
|
|
2225
|
+
if (entering) body.dataset.hoverEnter = "";
|
|
2226
|
+
else delete body.dataset.hoverEnter;
|
|
2227
|
+
const { x, y, w, h } = boxIn(body, item);
|
|
2228
|
+
body.style.setProperty("--sidebar-hover-x", `${x}px`);
|
|
2229
|
+
body.style.setProperty("--sidebar-hover-y", `${y}px`);
|
|
2230
|
+
body.style.setProperty("--sidebar-hover-w", `${w}px`);
|
|
2231
|
+
body.style.setProperty("--sidebar-hover-h", `${h}px`);
|
|
2232
|
+
body.dataset.hover = "";
|
|
2233
|
+
};
|
|
2234
|
+
const onPointerLeave = () => {
|
|
2235
|
+
const body = ref.current;
|
|
2236
|
+
if (!body) return;
|
|
2237
|
+
hovered.current = null;
|
|
2238
|
+
delete body.dataset.hover;
|
|
2239
|
+
};
|
|
2240
|
+
return {
|
|
2241
|
+
ref,
|
|
2242
|
+
onPointerMove,
|
|
2243
|
+
onPointerLeave
|
|
2244
|
+
};
|
|
2245
|
+
}
|
|
2246
|
+
const bodyClasses = [
|
|
2247
|
+
"relative isolate flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto overscroll-contain px-3 py-2",
|
|
2248
|
+
"after:pointer-events-none after:absolute after:top-0 after:left-0 after:-z-1 after:rounded-md",
|
|
2249
|
+
"after:bg-fg/5 after:opacity-0 data-hover:after:opacity-100",
|
|
2250
|
+
"after:h-(--sidebar-hover-h) after:w-(--sidebar-hover-w)",
|
|
2251
|
+
"after:[translate:var(--sidebar-hover-x)_var(--sidebar-hover-y)]",
|
|
2252
|
+
"after:transition-[translate,width,height,opacity] after:duration-150",
|
|
2253
|
+
"after:ease-[cubic-bezier(0.33,1,0.68,1)]",
|
|
2254
|
+
"data-hover-enter:after:transition-opacity"
|
|
2255
|
+
];
|
|
2256
|
+
/** The groups, between header and footer; the only part that scrolls. */
|
|
2257
|
+
function SidebarBody({ className, ref, onPointerMove, onPointerLeave, ...props }) {
|
|
2258
|
+
const hover = useHoverPill();
|
|
2259
|
+
const mergedRef = useMergedRef(hover.ref, ref);
|
|
2260
|
+
return /* @__PURE__ */ jsx("div", {
|
|
2261
|
+
...props,
|
|
2262
|
+
ref: mergedRef,
|
|
2263
|
+
"data-slot": "sidebar-body",
|
|
2264
|
+
className: cn(bodyClasses, className),
|
|
2265
|
+
onPointerMove: (e) => {
|
|
2266
|
+
onPointerMove?.(e);
|
|
2267
|
+
hover.onPointerMove(e);
|
|
2268
|
+
},
|
|
2269
|
+
onPointerLeave: (e) => {
|
|
2270
|
+
onPointerLeave?.(e);
|
|
2271
|
+
hover.onPointerLeave();
|
|
2272
|
+
}
|
|
2273
|
+
});
|
|
2274
|
+
}
|
|
2275
|
+
/** Pinned below the body. */
|
|
2276
|
+
function SidebarFooter({ className, ...props }) {
|
|
2277
|
+
return /* @__PURE__ */ jsx("div", {
|
|
2278
|
+
...props,
|
|
2279
|
+
"data-slot": "sidebar-footer",
|
|
2280
|
+
className: cn("shrink-0 p-3", className)
|
|
2281
|
+
});
|
|
2282
|
+
}
|
|
2283
|
+
function SidebarGroup({ label, className, children, ...props }) {
|
|
2284
|
+
const labelId = useId();
|
|
2285
|
+
const hasLabel = label != null && label !== false && label !== "";
|
|
2286
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2287
|
+
...props,
|
|
2288
|
+
"data-slot": "sidebar-group",
|
|
2289
|
+
className: cn("flex flex-col", className),
|
|
2290
|
+
children: [hasLabel && /* @__PURE__ */ jsx("div", {
|
|
2291
|
+
id: labelId,
|
|
2292
|
+
"data-slot": "sidebar-group-label",
|
|
2293
|
+
className: "px-2 pt-2 pb-1 text-xs/4 text-fg-muted",
|
|
2294
|
+
children: label
|
|
2295
|
+
}), /* @__PURE__ */ jsx("ul", {
|
|
2296
|
+
"aria-labelledby": hasLabel ? labelId : void 0,
|
|
2297
|
+
className: "flex flex-col gap-px",
|
|
2298
|
+
children
|
|
2299
|
+
})]
|
|
2300
|
+
});
|
|
2301
|
+
}
|
|
2302
|
+
const itemClasses = [
|
|
2303
|
+
"flex h-9 w-full items-center gap-2 rounded-md ps-2.5 pe-2",
|
|
2304
|
+
"text-sm leading-5 font-medium text-fg-muted no-underline select-none",
|
|
2305
|
+
"touch-manipulation [-webkit-tap-highlight-color:transparent]",
|
|
2306
|
+
"transition-[color,background-color] duration-150 ease-[cubic-bezier(0.4,0,0.2,1)]",
|
|
2307
|
+
"focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-focus",
|
|
2308
|
+
"hover:text-fg aria-[current=page]:bg-fg/8 aria-[current=page]:text-fg",
|
|
2309
|
+
"active:bg-fg/8 aria-[current=page]:active:bg-fg/12",
|
|
2310
|
+
"forced-colors:aria-[current=page]:bg-[Highlight] forced-colors:aria-[current=page]:text-[HighlightText]",
|
|
2311
|
+
"forced-colors:aria-[current=page]:[forced-color-adjust:none]"
|
|
2312
|
+
];
|
|
2313
|
+
/** Wraps runs of plain text in the truncating label span; icons and badges pass through. */
|
|
2314
|
+
function withLabel(children) {
|
|
2315
|
+
const out = [];
|
|
2316
|
+
let text = [];
|
|
2317
|
+
const flush = () => {
|
|
2318
|
+
if (text.length === 0) return;
|
|
2319
|
+
out.push(/* @__PURE__ */ jsx("span", {
|
|
2320
|
+
className: "min-w-0 flex-1 truncate",
|
|
2321
|
+
children: text
|
|
2322
|
+
}, `label-${out.length}`));
|
|
2323
|
+
text = [];
|
|
2324
|
+
};
|
|
2325
|
+
for (const child of Children.toArray(children)) if (typeof child === "string" || typeof child === "number") text.push(child);
|
|
2326
|
+
else {
|
|
2327
|
+
flush();
|
|
2328
|
+
out.push(child);
|
|
2329
|
+
}
|
|
2330
|
+
flush();
|
|
2331
|
+
return out;
|
|
2332
|
+
}
|
|
2333
|
+
/**
|
|
2334
|
+
* A sidebar link. Plain `<a>` by default; with `asChild`, your router's Link. Mark the current one
|
|
2335
|
+
* aria-current="page". Inside the drawer, a click the link handles in place (preventDefault, as
|
|
2336
|
+
* router links do) closes the drawer; a modified click opens a new tab and leaves it open.
|
|
2337
|
+
*/
|
|
2338
|
+
function SidebarItem({ asChild = false, className, children, ...props }) {
|
|
2339
|
+
const { setOpen } = useSidebarContext("SidebarItem");
|
|
2340
|
+
const { drawer } = usePanelContext("SidebarItem");
|
|
2341
|
+
const itemProps = {
|
|
2342
|
+
...props,
|
|
2343
|
+
"data-slot": "sidebar-item",
|
|
2344
|
+
className: cn(itemClasses, className)
|
|
2345
|
+
};
|
|
2346
|
+
let item;
|
|
2347
|
+
if (asChild) {
|
|
2348
|
+
const child = Children.only(children);
|
|
2349
|
+
item = isValidElement(child) ? /* @__PURE__ */ jsx(Slot, {
|
|
2350
|
+
...itemProps,
|
|
2351
|
+
children: cloneElement(child, { children: wrapChildren(child.props.children, withLabel) })
|
|
2352
|
+
}) : null;
|
|
2353
|
+
} else item = /* @__PURE__ */ jsx("a", {
|
|
2354
|
+
...itemProps,
|
|
2355
|
+
children: withLabel(children)
|
|
2356
|
+
});
|
|
2357
|
+
return /* @__PURE__ */ jsx("li", {
|
|
2358
|
+
className: "flex",
|
|
2359
|
+
onClick: (e) => {
|
|
2360
|
+
if (drawer && e.defaultPrevented) setOpen(false);
|
|
2361
|
+
},
|
|
2362
|
+
children: item
|
|
2363
|
+
});
|
|
2364
|
+
}
|
|
2365
|
+
/** Decorative 16px leading icon for a SidebarItem (hidden from assistive tech); takes the text color. */
|
|
2366
|
+
function SidebarIcon({ className, ...props }) {
|
|
2367
|
+
return /* @__PURE__ */ jsx("span", {
|
|
2368
|
+
"aria-hidden": true,
|
|
2369
|
+
...props,
|
|
2370
|
+
"data-slot": "sidebar-item-icon",
|
|
2371
|
+
className: cn("flex shrink-0 [&_svg]:size-4", className)
|
|
2372
|
+
});
|
|
2373
|
+
}
|
|
2374
|
+
/** Count at the end of a SidebarItem, as a small neutral Badge; part of the name ("Projects 8"). */
|
|
2375
|
+
function SidebarBadge({ className, ...props }) {
|
|
2376
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [" ", /* @__PURE__ */ jsx(Badge, {
|
|
2377
|
+
size: "sm",
|
|
2378
|
+
...props,
|
|
2379
|
+
className: cn("shrink-0", className)
|
|
2380
|
+
})] });
|
|
2381
|
+
}
|
|
2382
|
+
//#endregion
|
|
2383
|
+
//#region src/components/switch/switch.tsx
|
|
2384
|
+
const sizeClasses = {
|
|
2385
|
+
sm: {
|
|
2386
|
+
track: "h-3.5 w-7 [--switch-thumb:10px] [--switch-travel:14px] [--switch-stretch:2px]",
|
|
2387
|
+
offset: "mt-[5px]",
|
|
2388
|
+
icon: "[&_svg]:size-2"
|
|
2389
|
+
},
|
|
2390
|
+
md: {
|
|
2391
|
+
track: "h-5 w-9 [--switch-thumb:16px] [--switch-travel:16px] [--switch-stretch:4px]",
|
|
2392
|
+
offset: "mt-0.5",
|
|
2393
|
+
icon: "[&_svg]:size-3"
|
|
2394
|
+
}
|
|
2395
|
+
};
|
|
2396
|
+
function Switch({ label, description, error, size = "md", loading = false, labelPosition = "end", className, inputClassName, disabled, onChange, "aria-describedby": describedBy, "aria-invalid": ariaInvalid, ...props }) {
|
|
2397
|
+
const s = sizeClasses[size];
|
|
2398
|
+
const pressable = !disabled && !loading;
|
|
2399
|
+
const handleChange = (e) => {
|
|
2400
|
+
if (loading) {
|
|
2401
|
+
e.currentTarget.checked = !e.currentTarget.checked;
|
|
2402
|
+
return;
|
|
2403
|
+
}
|
|
2404
|
+
onChange?.(e);
|
|
2405
|
+
};
|
|
2406
|
+
return /* @__PURE__ */ jsx(ChoiceRow, {
|
|
2407
|
+
slot: "switch",
|
|
2408
|
+
label,
|
|
2409
|
+
description,
|
|
2410
|
+
error,
|
|
2411
|
+
labelPosition,
|
|
2412
|
+
gap: "gap-3",
|
|
2413
|
+
controlOffset: s.offset,
|
|
2414
|
+
className,
|
|
2415
|
+
"aria-describedby": describedBy,
|
|
2416
|
+
control: (row) => /* @__PURE__ */ jsxs("span", {
|
|
2417
|
+
className: cn("relative inline-flex", s.track),
|
|
2418
|
+
children: [/* @__PURE__ */ jsx("input", {
|
|
2419
|
+
...props,
|
|
2420
|
+
type: "checkbox",
|
|
2421
|
+
role: "switch",
|
|
2422
|
+
disabled,
|
|
2423
|
+
"aria-busy": loading || void 0,
|
|
2424
|
+
"aria-labelledby": props["aria-labelledby"] ?? row.labelledBy,
|
|
2425
|
+
"aria-describedby": row.describedBy,
|
|
2426
|
+
"aria-invalid": row.invalid ? true : ariaInvalid,
|
|
2427
|
+
onChange: handleChange,
|
|
2428
|
+
className: cn("peer m-0 size-full cursor-pointer appearance-none rounded-full border border-transparent outline-hidden", "disabled:cursor-not-allowed aria-busy:cursor-progress", "bg-(--switch-track) [box-shadow:0_0_0_1px_var(--switch-ring),0_0_0_4px_var(--switch-halo)]", "transition-[background-color,border-color,box-shadow] duration-150 ease-out", "[--switch-track:var(--color-input-border)] checked:[--switch-track:var(--color-control-checked)]", pressable && "not-checked:hover:[--switch-track:var(--color-input-border-hover)]", row.invalid ? ["[--switch-ring:var(--color-danger)] [--switch-halo:var(--color-input-halo-invalid)]", "focus-visible:[--switch-halo:var(--color-input-halo-invalid-focus)]"] : [
|
|
2429
|
+
"[--switch-ring:transparent] [--switch-halo:transparent]",
|
|
2430
|
+
"focus-visible:[--switch-ring:var(--color-input-focus)] focus-visible:[--switch-halo:var(--color-input-halo)]",
|
|
2431
|
+
"not-forced-colors:checked:focus-visible:outline-control-checked"
|
|
2432
|
+
], "checked:focus-visible:[--switch-ring:transparent] checked:focus-visible:[--switch-halo:transparent]", "checked:focus-visible:outline-solid checked:focus-visible:outline-2 checked:focus-visible:outline-offset-2", row.invalid && "not-forced-colors:checked:focus-visible:outline-danger", "forced-colors:forced-color-adjust-none forced-colors:[box-shadow:none]", "forced-colors:border-[CanvasText] forced-colors:bg-[Canvas]", "forced-colors:checked:border-[Highlight] forced-colors:checked:bg-[Highlight]", "forced-colors:disabled:border-[GrayText] forced-colors:disabled:checked:bg-[GrayText]", "forced-colors:focus-visible:outline-[CanvasText]", inputClassName)
|
|
2433
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
2434
|
+
"aria-hidden": true,
|
|
2435
|
+
className: cn("pointer-events-none absolute inset-s-0.5 top-0.5 grid place-items-center rounded-full", "h-(--switch-thumb) w-(--switch-thumb-w) bg-control-checked-fg text-control-checked shadow-switch-thumb", "[--switch-dir:1] rtl:[--switch-dir:-1] [translate:calc(var(--switch-x)*var(--switch-dir))_0]", "[--switch-x:0px] [--switch-thumb-w:var(--switch-thumb)] peer-checked:[--switch-x:var(--switch-travel)]", "transition-[translate,width] duration-150 ease-in-out", pressable && ["peer-active:[--switch-thumb-w:calc(var(--switch-thumb)+var(--switch-stretch))]", "peer-checked:peer-active:[--switch-x:calc(var(--switch-travel)-var(--switch-stretch))]"], "peer-disabled:shadow-none", "forced-colors:forced-color-adjust-none forced-colors:shadow-none", "forced-colors:bg-[CanvasText] forced-colors:text-[Canvas]", "forced-colors:peer-checked:bg-[HighlightText] forced-colors:peer-checked:text-[Highlight]", "forced-colors:peer-disabled:bg-[GrayText] forced-colors:peer-disabled:peer-checked:bg-[Canvas]", s.icon),
|
|
2436
|
+
children: loading && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(LoaderCircle, { className: "animate-spin motion-reduce:hidden" }), /* @__PURE__ */ jsx(CircleDashed, { className: "hidden motion-reduce:block" })] })
|
|
2437
|
+
})]
|
|
2438
|
+
})
|
|
2439
|
+
});
|
|
2440
|
+
}
|
|
2441
|
+
//#endregion
|
|
2442
|
+
//#region src/components/tabs/tab-nav.tsx
|
|
2443
|
+
const TabNavContext = createContext(null);
|
|
2444
|
+
/**
|
|
2445
|
+
* Tabs that change the URL: a `<nav>` of links, the current one marked (by the router, usually)
|
|
2446
|
+
* with aria-current="page". Same look as Tabs, but link semantics: Tab moves between links, Enter
|
|
2447
|
+
* follows one, and each can be opened in a new tab. For in-page panels, use Tabs.
|
|
2448
|
+
*/
|
|
2449
|
+
function TabNav({ variant = "line", size = "md", className, listClassName, children, ...props }) {
|
|
2450
|
+
const { listRef, onPointerMove, onPointerLeave } = useTabListMotion(variant, { revealSelected: true });
|
|
2451
|
+
return /* @__PURE__ */ jsx(TabNavContext.Provider, {
|
|
2452
|
+
value: {
|
|
2453
|
+
variant,
|
|
2454
|
+
size
|
|
2455
|
+
},
|
|
2456
|
+
children: /* @__PURE__ */ jsx("nav", {
|
|
2457
|
+
...props,
|
|
2458
|
+
"data-slot": "tab-nav",
|
|
2459
|
+
className,
|
|
2460
|
+
children: /* @__PURE__ */ jsx("ul", {
|
|
2461
|
+
ref: listRef,
|
|
2462
|
+
"data-slot": "tab-list",
|
|
2463
|
+
"data-variant": variant,
|
|
2464
|
+
className: cn(listClasses(variant, size), listClassName),
|
|
2465
|
+
onPointerMove,
|
|
2466
|
+
onPointerLeave,
|
|
2467
|
+
children
|
|
2468
|
+
})
|
|
2469
|
+
})
|
|
2470
|
+
});
|
|
2471
|
+
}
|
|
2472
|
+
/**
|
|
2473
|
+
* A link styled as a tab. Plain `<a>` by default; with `asChild`, your router's Link (for an index
|
|
2474
|
+
* route, make it current only on an exact match, or it stays current on every child route).
|
|
2475
|
+
*/
|
|
2476
|
+
function TabNavLink({ asChild = false, icon, badge, className, onFocus, children, ...props }) {
|
|
2477
|
+
const context = useContext(TabNavContext);
|
|
2478
|
+
if (!context) throw new Error("TabNavLink must be rendered inside <TabNav>");
|
|
2479
|
+
const itemProps = {
|
|
2480
|
+
...props,
|
|
2481
|
+
"data-slot": "tab",
|
|
2482
|
+
className: cn(itemClasses$1(context.variant, context.size), className),
|
|
2483
|
+
onFocus: (e) => {
|
|
2484
|
+
onFocus?.(e);
|
|
2485
|
+
revealOnFocus(e.currentTarget);
|
|
2486
|
+
}
|
|
2487
|
+
};
|
|
2488
|
+
const content = (label) => /* @__PURE__ */ jsx(TabContent, {
|
|
2489
|
+
icon,
|
|
2490
|
+
badge,
|
|
2491
|
+
children: label
|
|
2492
|
+
});
|
|
2493
|
+
let item;
|
|
2494
|
+
if (asChild) {
|
|
2495
|
+
const child = Children.only(children);
|
|
2496
|
+
item = isValidElement(child) ? /* @__PURE__ */ jsx(Slot, {
|
|
2497
|
+
...itemProps,
|
|
2498
|
+
children: cloneElement(child, { children: wrapChildren(child.props.children, content) })
|
|
2499
|
+
}) : null;
|
|
2500
|
+
} else item = /* @__PURE__ */ jsx("a", {
|
|
2501
|
+
...itemProps,
|
|
2502
|
+
children: content(children)
|
|
2503
|
+
});
|
|
2504
|
+
return /* @__PURE__ */ jsx("li", {
|
|
2505
|
+
className: "flex shrink-0",
|
|
2506
|
+
children: item
|
|
2507
|
+
});
|
|
2508
|
+
}
|
|
2509
|
+
//#endregion
|
|
2510
|
+
//#region src/components/tabs/tabs.tsx
|
|
2511
|
+
const TabsContext = createContext(null);
|
|
2512
|
+
function useTabsContext(part) {
|
|
2513
|
+
const context = useContext(TabsContext);
|
|
2514
|
+
if (!context) throw new Error(`${part} must be rendered inside <Tabs>`);
|
|
2515
|
+
return context;
|
|
2516
|
+
}
|
|
2517
|
+
const partId = (baseId, part, value) => `${baseId}-${part}-${value.replace(/\s+/g, "-")}`;
|
|
2518
|
+
/**
|
|
2519
|
+
* In-page tabs (WAI-ARIA APG tabs pattern): a tablist that switches panels without changing the
|
|
2520
|
+
* URL. For tabs that navigate between routes, use TabNav.
|
|
2521
|
+
*/
|
|
2522
|
+
function Tabs({ value, defaultValue, onValueChange, activationMode = "automatic", variant = "line", size = "md", "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, children, ...props }) {
|
|
2523
|
+
const id = useId();
|
|
2524
|
+
const [inner, setInner] = useState(defaultValue);
|
|
2525
|
+
const controlled = value !== void 0;
|
|
2526
|
+
const selected = controlled ? value : inner;
|
|
2527
|
+
const select = (next) => {
|
|
2528
|
+
if (next === selected) return;
|
|
2529
|
+
if (!controlled) setInner(next);
|
|
2530
|
+
onValueChange?.(next);
|
|
2531
|
+
};
|
|
2532
|
+
return /* @__PURE__ */ jsx(TabsContext.Provider, {
|
|
2533
|
+
value: {
|
|
2534
|
+
baseId: id,
|
|
2535
|
+
selected,
|
|
2536
|
+
select,
|
|
2537
|
+
activationMode,
|
|
2538
|
+
variant,
|
|
2539
|
+
size,
|
|
2540
|
+
label: {
|
|
2541
|
+
"aria-label": ariaLabel,
|
|
2542
|
+
"aria-labelledby": ariaLabelledby
|
|
2543
|
+
}
|
|
2544
|
+
},
|
|
2545
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
2546
|
+
...props,
|
|
2547
|
+
"data-slot": "tabs",
|
|
2548
|
+
children
|
|
2549
|
+
})
|
|
2550
|
+
});
|
|
2551
|
+
}
|
|
2552
|
+
const keyMoves = {
|
|
2553
|
+
ArrowRight: (i, last, rtl) => rtl ? i === 0 ? last : i - 1 : i === last ? 0 : i + 1,
|
|
2554
|
+
ArrowLeft: (i, last, rtl) => rtl ? i === last ? 0 : i + 1 : i === 0 ? last : i - 1,
|
|
2555
|
+
Home: () => 0,
|
|
2556
|
+
End: (_, last) => last
|
|
2557
|
+
};
|
|
2558
|
+
function TabList({ className, ref, onKeyDown, onPointerMove, onPointerLeave, children, ...props }) {
|
|
2559
|
+
const { select, activationMode, variant, size, label } = useTabsContext("TabList");
|
|
2560
|
+
const motion = useTabListMotion(variant);
|
|
2561
|
+
const mergedRef = useMergedRef(motion.listRef, ref);
|
|
2562
|
+
const handleKeyDown = (e) => {
|
|
2563
|
+
onKeyDown?.(e);
|
|
2564
|
+
const move = keyMoves[e.key];
|
|
2565
|
+
if (e.defaultPrevented || !move || e.altKey || e.ctrlKey || e.metaKey) return;
|
|
2566
|
+
const list = e.currentTarget;
|
|
2567
|
+
const tabs = [...list.querySelectorAll("[role=\"tab\"]:not(:disabled)")].filter((tab) => tab.closest("[role=\"tablist\"]") === list);
|
|
2568
|
+
const index = tabs.indexOf(e.target);
|
|
2569
|
+
if (index === -1) return;
|
|
2570
|
+
e.preventDefault();
|
|
2571
|
+
const rtl = getComputedStyle(list).direction === "rtl";
|
|
2572
|
+
const next = tabs[move(index, tabs.length - 1, rtl)];
|
|
2573
|
+
next.focus();
|
|
2574
|
+
if (activationMode === "automatic" && next.dataset.value !== void 0) select(next.dataset.value);
|
|
2575
|
+
};
|
|
2576
|
+
return /* @__PURE__ */ jsx("div", {
|
|
2577
|
+
...label,
|
|
2578
|
+
...props,
|
|
2579
|
+
ref: mergedRef,
|
|
2580
|
+
role: "tablist",
|
|
2581
|
+
"data-slot": "tab-list",
|
|
2582
|
+
"data-variant": variant,
|
|
2583
|
+
className: cn(listClasses(variant, size), className),
|
|
2584
|
+
onKeyDown: handleKeyDown,
|
|
2585
|
+
onPointerMove: (e) => {
|
|
2586
|
+
onPointerMove?.(e);
|
|
2587
|
+
motion.onPointerMove(e);
|
|
2588
|
+
},
|
|
2589
|
+
onPointerLeave: (e) => {
|
|
2590
|
+
onPointerLeave?.(e);
|
|
2591
|
+
motion.onPointerLeave();
|
|
2592
|
+
},
|
|
2593
|
+
children
|
|
2594
|
+
});
|
|
2595
|
+
}
|
|
2596
|
+
function Tab({ value, icon, badge, disabled = false, className, ref, onClick, onFocus, children, ...props }) {
|
|
2597
|
+
const context = useTabsContext("Tab");
|
|
2598
|
+
const isSelected = context.selected === value;
|
|
2599
|
+
const ownRef = useRef(null);
|
|
2600
|
+
const mergedRef = useMergedRef(ownRef, ref);
|
|
2601
|
+
useRevealWhenSelected(ownRef, isSelected);
|
|
2602
|
+
return /* @__PURE__ */ jsx("button", {
|
|
2603
|
+
...props,
|
|
2604
|
+
ref: mergedRef,
|
|
2605
|
+
type: "button",
|
|
2606
|
+
role: "tab",
|
|
2607
|
+
id: partId(context.baseId, "tab", value),
|
|
2608
|
+
"aria-controls": partId(context.baseId, "panel", value),
|
|
2609
|
+
"aria-selected": isSelected,
|
|
2610
|
+
tabIndex: isSelected ? 0 : -1,
|
|
2611
|
+
disabled,
|
|
2612
|
+
"data-slot": "tab",
|
|
2613
|
+
"data-value": value,
|
|
2614
|
+
"data-selected": isSelected || void 0,
|
|
2615
|
+
"data-disabled": disabled || void 0,
|
|
2616
|
+
className: cn(itemClasses$1(context.variant, context.size), className),
|
|
2617
|
+
onClick: (e) => {
|
|
2618
|
+
onClick?.(e);
|
|
2619
|
+
if (!e.defaultPrevented) context.select(value);
|
|
2620
|
+
},
|
|
2621
|
+
onFocus: (e) => {
|
|
2622
|
+
onFocus?.(e);
|
|
2623
|
+
revealOnFocus(e.currentTarget);
|
|
2624
|
+
},
|
|
2625
|
+
children: /* @__PURE__ */ jsx(TabContent, {
|
|
2626
|
+
icon,
|
|
2627
|
+
badge,
|
|
2628
|
+
children
|
|
2629
|
+
})
|
|
2630
|
+
});
|
|
2631
|
+
}
|
|
2632
|
+
function TabPanel({ value, keepMounted = false, className, ...props }) {
|
|
2633
|
+
const { baseId, selected } = useTabsContext("TabPanel");
|
|
2634
|
+
const isSelected = selected === value;
|
|
2635
|
+
if (!isSelected && !keepMounted) return null;
|
|
2636
|
+
return /* @__PURE__ */ jsx("div", {
|
|
2637
|
+
tabIndex: 0,
|
|
2638
|
+
...props,
|
|
2639
|
+
role: "tabpanel",
|
|
2640
|
+
id: partId(baseId, "panel", value),
|
|
2641
|
+
"aria-labelledby": partId(baseId, "tab", value),
|
|
2642
|
+
hidden: !isSelected,
|
|
2643
|
+
"data-slot": "tab-panel",
|
|
2644
|
+
className: cn("focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus", className)
|
|
2645
|
+
});
|
|
2646
|
+
}
|
|
2647
|
+
//#endregion
|
|
2648
|
+
//#region src/components/theme-switcher/theme-switcher.tsx
|
|
2649
|
+
const systemThemeQuery = "(prefers-color-scheme: dark)";
|
|
2650
|
+
function subscribeSystemTheme(onChange) {
|
|
2651
|
+
const query = window.matchMedia?.(systemThemeQuery);
|
|
2652
|
+
query?.addEventListener("change", onChange);
|
|
2653
|
+
return () => query?.removeEventListener("change", onChange);
|
|
2654
|
+
}
|
|
2655
|
+
const systemIsDark = () => window.matchMedia?.(systemThemeQuery).matches ?? false;
|
|
2656
|
+
const serverIsDark = () => false;
|
|
2657
|
+
/** Toggle the visible theme in one click, including when the initial choice follows the system. */
|
|
2658
|
+
function ThemeSwitcher({ value, onChange, onClick, className, "aria-label": ariaLabel, ...props }) {
|
|
2659
|
+
const systemDark = useSyncExternalStore(subscribeSystemTheme, systemIsDark, serverIsDark);
|
|
2660
|
+
const dark = value === "system" ? systemDark : value === "dark";
|
|
2661
|
+
const next = dark ? "light" : "dark";
|
|
2662
|
+
const label = ariaLabel ?? (dark ? "Switch to light theme" : "Switch to dark theme");
|
|
2663
|
+
return /* @__PURE__ */ jsx(Button, {
|
|
2664
|
+
...props,
|
|
2665
|
+
intent: "ghost",
|
|
2666
|
+
icon: dark ? /* @__PURE__ */ jsx(Sun, {}) : /* @__PURE__ */ jsx(Moon, {}),
|
|
2667
|
+
"aria-label": label,
|
|
2668
|
+
title: label,
|
|
2669
|
+
tooltip: false,
|
|
2670
|
+
"data-slot": "theme-switcher",
|
|
2671
|
+
className: cn("size-11 shrink-0 text-fg-muted hover:text-fg [&_svg]:size-5", className),
|
|
2672
|
+
onClick: (event) => {
|
|
2673
|
+
onClick?.(event);
|
|
2674
|
+
if (!event.defaultPrevented) onChange(next);
|
|
2675
|
+
}
|
|
2676
|
+
});
|
|
2677
|
+
}
|
|
2678
|
+
//#endregion
|
|
2679
|
+
//#region src/components/theme-switcher/use-theme.ts
|
|
2680
|
+
const validTheme = (value) => value === "light" || value === "dark" ? value : "system";
|
|
2681
|
+
/** Persisted color theme; `system` clears data-theme so prefers-color-scheme decides. */
|
|
2682
|
+
function useTheme({ storageKey = "swell-ui-theme" } = {}) {
|
|
2683
|
+
const [theme, setTheme] = useState(() => {
|
|
2684
|
+
try {
|
|
2685
|
+
return validTheme(localStorage.getItem(storageKey));
|
|
2686
|
+
} catch {
|
|
2687
|
+
return "system";
|
|
2688
|
+
}
|
|
2689
|
+
});
|
|
2690
|
+
useEffect(() => {
|
|
2691
|
+
if (theme === "system") delete document.documentElement.dataset.theme;
|
|
2692
|
+
else document.documentElement.dataset.theme = theme;
|
|
2693
|
+
try {
|
|
2694
|
+
localStorage.setItem(storageKey, theme);
|
|
2695
|
+
} catch {}
|
|
2696
|
+
}, [theme, storageKey]);
|
|
2697
|
+
return {
|
|
2698
|
+
theme,
|
|
2699
|
+
setTheme
|
|
2700
|
+
};
|
|
2701
|
+
}
|
|
2702
|
+
//#endregion
|
|
2703
|
+
//#region src/components/toast/store.ts
|
|
2704
|
+
function isActionButton(action) {
|
|
2705
|
+
return typeof action === "object" && action !== null && !isValidElement(action) && !(Symbol.iterator in action) && "label" in action;
|
|
2706
|
+
}
|
|
2707
|
+
function resolveDuration(record) {
|
|
2708
|
+
if (record.duration !== void 0) return record.duration;
|
|
2709
|
+
if (record.type === "danger" || record.type === "loading") return Number.POSITIVE_INFINITY;
|
|
2710
|
+
return record.action != null && record.action !== false ? 1e4 : 5e3;
|
|
2711
|
+
}
|
|
2712
|
+
let toasts = [];
|
|
2713
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
2714
|
+
let nextId = 0;
|
|
2715
|
+
let nextKey = 0;
|
|
2716
|
+
function emit(next) {
|
|
2717
|
+
toasts = next;
|
|
2718
|
+
for (const listener of listeners) listener();
|
|
2719
|
+
}
|
|
2720
|
+
function subscribe(listener) {
|
|
2721
|
+
listeners.add(listener);
|
|
2722
|
+
return () => {
|
|
2723
|
+
listeners.delete(listener);
|
|
2724
|
+
};
|
|
2725
|
+
}
|
|
2726
|
+
function getSnapshot() {
|
|
2727
|
+
return toasts;
|
|
2728
|
+
}
|
|
2729
|
+
const serverSnapshot = [];
|
|
2730
|
+
function getServerSnapshot() {
|
|
2731
|
+
return serverSnapshot;
|
|
2732
|
+
}
|
|
2733
|
+
function findLive(id) {
|
|
2734
|
+
return toasts.find((t) => t.id === id && !t.removed);
|
|
2735
|
+
}
|
|
2736
|
+
function create(options) {
|
|
2737
|
+
const id = options.id ?? `toast-${++nextId}`;
|
|
2738
|
+
const { id: _id, type = "neutral", dismissible = true, ...rest } = options;
|
|
2739
|
+
const live = findLive(id);
|
|
2740
|
+
if (live) {
|
|
2741
|
+
const next = {
|
|
2742
|
+
...rest,
|
|
2743
|
+
id,
|
|
2744
|
+
type,
|
|
2745
|
+
dismissible,
|
|
2746
|
+
key: live.key,
|
|
2747
|
+
version: live.version + 1,
|
|
2748
|
+
removed: false
|
|
2749
|
+
};
|
|
2750
|
+
emit(toasts.map((t) => t === live ? next : t));
|
|
2751
|
+
return id;
|
|
2752
|
+
}
|
|
2753
|
+
emit([{
|
|
2754
|
+
...rest,
|
|
2755
|
+
id,
|
|
2756
|
+
type,
|
|
2757
|
+
dismissible,
|
|
2758
|
+
key: ++nextKey,
|
|
2759
|
+
version: 0,
|
|
2760
|
+
removed: false
|
|
2761
|
+
}, ...toasts]);
|
|
2762
|
+
return id;
|
|
2763
|
+
}
|
|
2764
|
+
function update(id, patch) {
|
|
2765
|
+
const live = findLive(id);
|
|
2766
|
+
if (!live) return;
|
|
2767
|
+
const { type = live.type, dismissible = live.dismissible, ...rest } = patch;
|
|
2768
|
+
emit(toasts.map((t) => t === live ? {
|
|
2769
|
+
...live,
|
|
2770
|
+
...rest,
|
|
2771
|
+
type,
|
|
2772
|
+
dismissible,
|
|
2773
|
+
version: live.version + 1
|
|
2774
|
+
} : t));
|
|
2775
|
+
}
|
|
2776
|
+
function dismiss(id, swipeOut) {
|
|
2777
|
+
const hit = (t) => !t.removed && (id === void 0 || t.id === id);
|
|
2778
|
+
if (!toasts.some(hit)) return;
|
|
2779
|
+
if (listeners.size === 0) {
|
|
2780
|
+
emit(toasts.filter((t) => !hit(t)));
|
|
2781
|
+
return;
|
|
2782
|
+
}
|
|
2783
|
+
emit(toasts.map((t) => hit(t) ? {
|
|
2784
|
+
...t,
|
|
2785
|
+
removed: true,
|
|
2786
|
+
swipeOut
|
|
2787
|
+
} : t));
|
|
2788
|
+
}
|
|
2789
|
+
/** Called by the Toaster once a dismissed toast's exit has finished. */
|
|
2790
|
+
function removeToast(key) {
|
|
2791
|
+
if (toasts.some((t) => t.key === key)) emit(toasts.filter((t) => t.key !== key));
|
|
2792
|
+
}
|
|
2793
|
+
function isStepOptions(step) {
|
|
2794
|
+
return typeof step === "object" && step !== null && !isValidElement(step) && !(Symbol.iterator in step) && "title" in step;
|
|
2795
|
+
}
|
|
2796
|
+
function stepPatch(step, type) {
|
|
2797
|
+
const base = {
|
|
2798
|
+
type,
|
|
2799
|
+
description: void 0,
|
|
2800
|
+
action: void 0,
|
|
2801
|
+
duration: void 0
|
|
2802
|
+
};
|
|
2803
|
+
return isStepOptions(step) ? {
|
|
2804
|
+
...base,
|
|
2805
|
+
...step
|
|
2806
|
+
} : {
|
|
2807
|
+
...base,
|
|
2808
|
+
title: step
|
|
2809
|
+
};
|
|
2810
|
+
}
|
|
2811
|
+
function shortcut(type) {
|
|
2812
|
+
return (title, options) => create({
|
|
2813
|
+
...options,
|
|
2814
|
+
title,
|
|
2815
|
+
type
|
|
2816
|
+
});
|
|
2817
|
+
}
|
|
2818
|
+
/**
|
|
2819
|
+
* Imperative toasts, callable from anywhere (event handlers, mutations, loaders) once a
|
|
2820
|
+
* `<Toaster />` is mounted. Every call returns the toast's id. `toasterId` routes a toast to a
|
|
2821
|
+
* `<Toaster id>` instead.
|
|
2822
|
+
*/
|
|
2823
|
+
const toast = Object.assign((options) => create(options), {
|
|
2824
|
+
success: shortcut("success"),
|
|
2825
|
+
info: shortcut("info"),
|
|
2826
|
+
warning: shortcut("warning"),
|
|
2827
|
+
/** Renders as the `danger` type. Stays until dismissed. */
|
|
2828
|
+
error: shortcut("danger"),
|
|
2829
|
+
/** Spinner; stays until updated or dismissed. */
|
|
2830
|
+
loading: shortcut("loading"),
|
|
2831
|
+
/** Merges into a toast still on screen and restarts its timer. No-op for unknown ids. */
|
|
2832
|
+
update,
|
|
2833
|
+
/** Plays the exit of one toast, or of all of them without an id. */
|
|
2834
|
+
dismiss: (id) => dismiss(id),
|
|
2835
|
+
/**
|
|
2836
|
+
* Loading toast that turns into success or danger in place when the promise settles. Steps
|
|
2837
|
+
* are a title or full options; success/error may be functions of the result / the error.
|
|
2838
|
+
*/
|
|
2839
|
+
promise(promise, steps) {
|
|
2840
|
+
const id = create({
|
|
2841
|
+
id: steps.id,
|
|
2842
|
+
toasterId: steps.toasterId,
|
|
2843
|
+
...stepPatch(steps.loading, "loading")
|
|
2844
|
+
});
|
|
2845
|
+
const settle = (step, type) => update(id, stepPatch(step, type));
|
|
2846
|
+
(typeof promise === "function" ? promise() : promise).then((data) => settle(typeof steps.success === "function" ? steps.success(data) : steps.success, "success"), (error) => settle(typeof steps.error === "function" ? steps.error(error) : steps.error, "danger"));
|
|
2847
|
+
return id;
|
|
2848
|
+
}
|
|
2849
|
+
});
|
|
2850
|
+
//#endregion
|
|
2851
|
+
//#region src/components/toast/toaster.tsx
|
|
2852
|
+
const GAP = 14;
|
|
2853
|
+
const SCALE_STEP = .05;
|
|
2854
|
+
const SWIPE_DISTANCE = 45;
|
|
2855
|
+
const SWIPE_VELOCITY = .11;
|
|
2856
|
+
const EXIT_FALLBACK_MS = 600;
|
|
2857
|
+
const ENTER_MS = 400;
|
|
2858
|
+
const MOBILE_QUERY = "(width <= 600px)";
|
|
2859
|
+
const MOBILE_VISIBLE = 2;
|
|
2860
|
+
const CARD_BORDERS = 2;
|
|
2861
|
+
const owners = [];
|
|
2862
|
+
const ownerListeners = /* @__PURE__ */ new Set();
|
|
2863
|
+
function subscribeOwners(listener) {
|
|
2864
|
+
ownerListeners.add(listener);
|
|
2865
|
+
return () => {
|
|
2866
|
+
ownerListeners.delete(listener);
|
|
2867
|
+
};
|
|
2868
|
+
}
|
|
2869
|
+
function useIsActiveToaster(id) {
|
|
2870
|
+
const [owner] = useState(() => Symbol("toaster"));
|
|
2871
|
+
useLayoutEffect(() => {
|
|
2872
|
+
const entry = {
|
|
2873
|
+
owner,
|
|
2874
|
+
id
|
|
2875
|
+
};
|
|
2876
|
+
owners.push(entry);
|
|
2877
|
+
for (const listener of ownerListeners) listener();
|
|
2878
|
+
return () => {
|
|
2879
|
+
owners.splice(owners.indexOf(entry), 1);
|
|
2880
|
+
for (const listener of ownerListeners) listener();
|
|
2881
|
+
};
|
|
2882
|
+
}, [owner, id]);
|
|
2883
|
+
return useSyncExternalStore(subscribeOwners, () => owners.find((entry) => entry.id === id)?.owner === owner, () => false);
|
|
2884
|
+
}
|
|
2885
|
+
function useMediaQuery(query) {
|
|
2886
|
+
return useSyncExternalStore((onChange) => {
|
|
2887
|
+
if (typeof window.matchMedia !== "function") return () => {};
|
|
2888
|
+
const mql = window.matchMedia(query);
|
|
2889
|
+
mql.addEventListener("change", onChange);
|
|
2890
|
+
return () => mql.removeEventListener("change", onChange);
|
|
2891
|
+
}, () => typeof window.matchMedia === "function" && window.matchMedia(query).matches, () => false);
|
|
2892
|
+
}
|
|
2893
|
+
function useDocumentHidden() {
|
|
2894
|
+
return useSyncExternalStore((onChange) => {
|
|
2895
|
+
document.addEventListener("visibilitychange", onChange);
|
|
2896
|
+
return () => document.removeEventListener("visibilitychange", onChange);
|
|
2897
|
+
}, () => document.visibilityState === "hidden", () => false);
|
|
2898
|
+
}
|
|
2899
|
+
function isFocusVisible(el) {
|
|
2900
|
+
if (!el) return false;
|
|
2901
|
+
try {
|
|
2902
|
+
return el.matches(":focus-visible");
|
|
2903
|
+
} catch {
|
|
2904
|
+
return true;
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2907
|
+
function showPopover(el) {
|
|
2908
|
+
if (!el?.isConnected || typeof el.showPopover !== "function") return;
|
|
2909
|
+
let open = false;
|
|
2910
|
+
try {
|
|
2911
|
+
open = el.matches(":popover-open");
|
|
2912
|
+
} catch {}
|
|
2913
|
+
try {
|
|
2914
|
+
if (!open) el.showPopover();
|
|
2915
|
+
} catch {}
|
|
2916
|
+
}
|
|
2917
|
+
function modalDialogs() {
|
|
2918
|
+
try {
|
|
2919
|
+
return [...document.querySelectorAll("dialog:modal")].filter((dialog) => !dialog.hasAttribute("data-closing"));
|
|
2920
|
+
} catch {
|
|
2921
|
+
return [];
|
|
2922
|
+
}
|
|
2923
|
+
}
|
|
2924
|
+
function useToastHost(enabled) {
|
|
2925
|
+
const [host, setHost] = useState(null);
|
|
2926
|
+
useLayoutEffect(() => {
|
|
2927
|
+
if (!enabled) return;
|
|
2928
|
+
const el = document.createElement("div");
|
|
2929
|
+
el.dataset.slot = "toast-host";
|
|
2930
|
+
el.style.display = "contents";
|
|
2931
|
+
document.body.append(el);
|
|
2932
|
+
setHost(el);
|
|
2933
|
+
return () => {
|
|
2934
|
+
el.remove();
|
|
2935
|
+
setHost(null);
|
|
2936
|
+
};
|
|
2937
|
+
}, [enabled]);
|
|
2938
|
+
return host;
|
|
2939
|
+
}
|
|
2940
|
+
function useFollowModalDialogs(host, viewportRef) {
|
|
2941
|
+
useLayoutEffect(() => {
|
|
2942
|
+
const stack = [];
|
|
2943
|
+
const relocate = () => {
|
|
2944
|
+
const open = modalDialogs();
|
|
2945
|
+
for (let i = stack.length - 1; i >= 0; i--) if (!open.includes(stack[i])) stack.splice(i, 1);
|
|
2946
|
+
for (const dialog of open) if (!stack.includes(dialog)) stack.push(dialog);
|
|
2947
|
+
const target = stack.at(-1) ?? document.body;
|
|
2948
|
+
const viewport = viewportRef.current;
|
|
2949
|
+
if (host.parentElement !== target) {
|
|
2950
|
+
viewport?.setAttribute("data-moving", "");
|
|
2951
|
+
target.append(host);
|
|
2952
|
+
showPopover(viewport);
|
|
2953
|
+
viewport?.offsetWidth;
|
|
2954
|
+
viewport?.removeAttribute("data-moving");
|
|
2955
|
+
} else showPopover(viewport);
|
|
2956
|
+
};
|
|
2957
|
+
relocate();
|
|
2958
|
+
const observer = new MutationObserver(relocate);
|
|
2959
|
+
observer.observe(document.documentElement, {
|
|
2960
|
+
subtree: true,
|
|
2961
|
+
attributes: true,
|
|
2962
|
+
attributeFilter: ["open", "data-closing"]
|
|
2963
|
+
});
|
|
2964
|
+
return () => observer.disconnect();
|
|
2965
|
+
}, [host, viewportRef]);
|
|
2966
|
+
}
|
|
2967
|
+
/**
|
|
2968
|
+
* The single place toasts render: mount one near the app root (inside the router, so a `<Link>`
|
|
2969
|
+
* action has its context). Toasts themselves come from `toast()` and friends.
|
|
2970
|
+
*/
|
|
2971
|
+
function Toaster(props) {
|
|
2972
|
+
const host = useToastHost(useIsActiveToaster(props.id));
|
|
2973
|
+
if (!host) return null;
|
|
2974
|
+
return createPortal(/* @__PURE__ */ jsx(Viewport, {
|
|
2975
|
+
...props,
|
|
2976
|
+
host
|
|
2977
|
+
}), host);
|
|
2978
|
+
}
|
|
2979
|
+
const positionClasses = {
|
|
2980
|
+
"bottom-right": "top-auto bottom-6 left-auto right-6 max-[601px]:bottom-4",
|
|
2981
|
+
"bottom-left": "top-auto bottom-6 left-6 right-auto max-[601px]:bottom-4",
|
|
2982
|
+
"bottom-center": "top-auto bottom-6 left-1/2 right-auto -translate-x-1/2 max-[601px]:bottom-4",
|
|
2983
|
+
"top-right": "top-6 bottom-auto left-auto right-6 max-[601px]:top-4",
|
|
2984
|
+
"top-left": "top-6 bottom-auto left-6 right-auto max-[601px]:top-4",
|
|
2985
|
+
"top-center": "top-6 bottom-auto left-1/2 right-auto -translate-x-1/2 max-[601px]:top-4"
|
|
2986
|
+
};
|
|
2987
|
+
function Viewport({ id, position = "bottom-right", visibleToasts = 3, host }) {
|
|
2988
|
+
const all = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
2989
|
+
const toasts = useMemo(() => all.filter((t) => t.toasterId === id), [all, id]);
|
|
2990
|
+
const viewportRef = useRef(null);
|
|
2991
|
+
const statusRef = useRef(null);
|
|
2992
|
+
const alertRef = useRef(null);
|
|
2993
|
+
const returnFocus = useRef(null);
|
|
2994
|
+
const [heights, setHeights] = useState({});
|
|
2995
|
+
const [hovered, setHovered] = useState(false);
|
|
2996
|
+
const [focusWithin, setFocusWithin] = useState(false);
|
|
2997
|
+
const [interacting, setInteracting] = useState(false);
|
|
2998
|
+
const mobile = useMediaQuery(MOBILE_QUERY);
|
|
2999
|
+
const documentHidden = useDocumentHidden();
|
|
3000
|
+
useFollowModalDialogs(host, viewportRef);
|
|
3001
|
+
const live = toasts.filter((t) => !t.removed);
|
|
3002
|
+
const limit = Math.max(1, mobile ? Math.min(visibleToasts, MOBILE_VISIBLE) : visibleToasts);
|
|
3003
|
+
const expanded = live.length > 0 && (hovered || focusWithin);
|
|
3004
|
+
const paused = expanded || interacting || documentHidden;
|
|
3005
|
+
const frontHeight = live.length ? heights[live[0].key] : void 0;
|
|
3006
|
+
const layouts = /* @__PURE__ */ new Map();
|
|
3007
|
+
let before = 0;
|
|
3008
|
+
live.forEach((t, index) => {
|
|
3009
|
+
layouts.set(t.key, {
|
|
3010
|
+
index,
|
|
3011
|
+
offset: before + index * GAP,
|
|
3012
|
+
visible: index < limit
|
|
3013
|
+
});
|
|
3014
|
+
before += heights[t.key] ?? 0;
|
|
3015
|
+
});
|
|
3016
|
+
const onHeight = useCallback((key, height) => {
|
|
3017
|
+
setHeights((prev) => {
|
|
3018
|
+
if (prev[key] === height) return prev;
|
|
3019
|
+
const next = { ...prev };
|
|
3020
|
+
if (height === void 0) delete next[key];
|
|
3021
|
+
else next[key] = height;
|
|
3022
|
+
return next;
|
|
3023
|
+
});
|
|
3024
|
+
}, []);
|
|
3025
|
+
const pending = useRef({
|
|
3026
|
+
polite: [],
|
|
3027
|
+
assertive: [],
|
|
3028
|
+
frame: 0
|
|
3029
|
+
});
|
|
3030
|
+
const announce = useCallback((text, assertive) => {
|
|
3031
|
+
const queue = pending.current;
|
|
3032
|
+
const list = assertive ? queue.assertive : queue.polite;
|
|
3033
|
+
const region = assertive ? alertRef.current : statusRef.current;
|
|
3034
|
+
if (!region || list.includes(text)) return;
|
|
3035
|
+
list.push(text);
|
|
3036
|
+
region.textContent = "";
|
|
3037
|
+
if (queue.frame) return;
|
|
3038
|
+
queue.frame = requestAnimationFrame(() => {
|
|
3039
|
+
queue.frame = 0;
|
|
3040
|
+
if (statusRef.current && queue.polite.length) statusRef.current.textContent = queue.polite.join(" ");
|
|
3041
|
+
if (alertRef.current && queue.assertive.length) alertRef.current.textContent = queue.assertive.join(" ");
|
|
3042
|
+
queue.polite = [];
|
|
3043
|
+
queue.assertive = [];
|
|
3044
|
+
});
|
|
3045
|
+
}, []);
|
|
3046
|
+
useEffect(() => {
|
|
3047
|
+
const queue = pending.current;
|
|
3048
|
+
return () => cancelAnimationFrame(queue.frame);
|
|
3049
|
+
}, []);
|
|
3050
|
+
const restoreFocus = useCallback(() => {
|
|
3051
|
+
const target = returnFocus.current;
|
|
3052
|
+
returnFocus.current = null;
|
|
3053
|
+
if (target?.isConnected && target !== document.body) target.focus({ preventScroll: true });
|
|
3054
|
+
if (viewportRef.current?.contains(document.activeElement)) document.activeElement.blur();
|
|
3055
|
+
}, []);
|
|
3056
|
+
const onFocusLost = useCallback((index) => {
|
|
3057
|
+
const viewport = viewportRef.current;
|
|
3058
|
+
if (viewport && isFocusVisible(document.activeElement)) {
|
|
3059
|
+
const cards = [...viewport.querySelectorAll("li[data-slot=\"toast\"]:not([data-removed]):not([inert])")].sort((a, b) => Number(a.dataset.index) - Number(b.dataset.index));
|
|
3060
|
+
const next = cards.find((card) => Number(card.dataset.index) >= index) ?? cards.at(-1);
|
|
3061
|
+
if (next) {
|
|
3062
|
+
next.focus({ preventScroll: true });
|
|
3063
|
+
return;
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
restoreFocus();
|
|
3067
|
+
}, [restoreFocus]);
|
|
3068
|
+
useEffect(() => {
|
|
3069
|
+
const onKeyDown = (event) => {
|
|
3070
|
+
const f6 = event.key === "F6" && !event.altKey && !event.ctrlKey && !event.metaKey;
|
|
3071
|
+
const target = event.target instanceof HTMLElement ? event.target : null;
|
|
3072
|
+
const typing = !!target && (target.isContentEditable || target.matches("input, textarea, select"));
|
|
3073
|
+
const altT = event.code === "KeyT" && event.altKey && !event.ctrlKey && !event.metaKey && !typing;
|
|
3074
|
+
const viewport = viewportRef.current;
|
|
3075
|
+
if (!f6 && !altT || event.defaultPrevented || !viewport) return;
|
|
3076
|
+
if (viewport.contains(document.activeElement)) {
|
|
3077
|
+
event.preventDefault();
|
|
3078
|
+
restoreFocus();
|
|
3079
|
+
return;
|
|
3080
|
+
}
|
|
3081
|
+
const newest = viewport.querySelector("li[data-slot=\"toast\"][data-index=\"0\"]:not([data-removed])");
|
|
3082
|
+
if (!newest) return;
|
|
3083
|
+
event.preventDefault();
|
|
3084
|
+
returnFocus.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
3085
|
+
newest.focus({ preventScroll: true });
|
|
3086
|
+
};
|
|
3087
|
+
document.addEventListener("keydown", onKeyDown);
|
|
3088
|
+
return () => document.removeEventListener("keydown", onKeyDown);
|
|
3089
|
+
}, [restoreFocus]);
|
|
3090
|
+
const handleKeyDown = (event) => {
|
|
3091
|
+
if (event.key !== "Escape") return;
|
|
3092
|
+
const card = event.target.closest("li[data-slot=\"toast\"]");
|
|
3093
|
+
if (!card || card.dataset.dismissible === void 0 || !card.dataset.toastId) return;
|
|
3094
|
+
event.preventDefault();
|
|
3095
|
+
event.stopPropagation();
|
|
3096
|
+
dismiss(card.dataset.toastId);
|
|
3097
|
+
};
|
|
3098
|
+
const handleFocus = (event) => {
|
|
3099
|
+
const from = event.relatedTarget;
|
|
3100
|
+
if (!event.currentTarget.contains(from) && returnFocus.current === null) returnFocus.current = from instanceof HTMLElement ? from : null;
|
|
3101
|
+
setFocusWithin(isFocusVisible(event.target));
|
|
3102
|
+
};
|
|
3103
|
+
const handleBlur = (event) => {
|
|
3104
|
+
if (event.currentTarget.contains(event.relatedTarget)) return;
|
|
3105
|
+
setFocusWithin(false);
|
|
3106
|
+
returnFocus.current = null;
|
|
3107
|
+
};
|
|
3108
|
+
const handlePointerEnter = (event) => {
|
|
3109
|
+
if (event.pointerType !== "touch") setHovered(true);
|
|
3110
|
+
};
|
|
3111
|
+
const handlePointerLeave = (event) => {
|
|
3112
|
+
if (event.pointerType !== "touch") setHovered(false);
|
|
3113
|
+
};
|
|
3114
|
+
const [y, x] = position.split("-");
|
|
3115
|
+
const directions = x === "center" ? [y] : [y, x];
|
|
3116
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3117
|
+
/* @__PURE__ */ jsx("section", {
|
|
3118
|
+
ref: viewportRef,
|
|
3119
|
+
popover: "manual",
|
|
3120
|
+
"aria-label": "Notifications (F6)",
|
|
3121
|
+
tabIndex: -1,
|
|
3122
|
+
"data-slot": "toast-viewport",
|
|
3123
|
+
"data-position": position,
|
|
3124
|
+
"data-expanded": expanded || void 0,
|
|
3125
|
+
onKeyDown: handleKeyDown,
|
|
3126
|
+
onFocus: handleFocus,
|
|
3127
|
+
onBlur: handleBlur,
|
|
3128
|
+
onPointerDown: () => setInteracting(true),
|
|
3129
|
+
onPointerUp: () => setInteracting(false),
|
|
3130
|
+
onPointerCancel: () => setInteracting(false),
|
|
3131
|
+
className: cn("group/viewport fixed m-0 h-auto w-95 overflow-visible border-0 bg-transparent p-0 text-fg outline-none", "max-[601px]:inset-x-4 max-[601px]:w-auto max-[601px]:translate-x-0", positionClasses[position]),
|
|
3132
|
+
children: /* @__PURE__ */ jsx("ol", {
|
|
3133
|
+
className: "relative m-0 list-none p-0",
|
|
3134
|
+
onPointerEnter: handlePointerEnter,
|
|
3135
|
+
onPointerLeave: handlePointerLeave,
|
|
3136
|
+
children: toasts.map((record) => /* @__PURE__ */ jsx(ToastCard, {
|
|
3137
|
+
record,
|
|
3138
|
+
layout: layouts.get(record.key),
|
|
3139
|
+
total: live.length,
|
|
3140
|
+
expanded,
|
|
3141
|
+
paused,
|
|
3142
|
+
frontHeight,
|
|
3143
|
+
ownHeight: heights[record.key],
|
|
3144
|
+
edge: y,
|
|
3145
|
+
directions,
|
|
3146
|
+
onHeight,
|
|
3147
|
+
onFocusLost,
|
|
3148
|
+
announce
|
|
3149
|
+
}, record.key))
|
|
3150
|
+
})
|
|
3151
|
+
}),
|
|
3152
|
+
/* @__PURE__ */ jsx("div", {
|
|
3153
|
+
ref: statusRef,
|
|
3154
|
+
role: "status",
|
|
3155
|
+
"data-slot": "toast-announcer",
|
|
3156
|
+
className: "sr-only"
|
|
3157
|
+
}),
|
|
3158
|
+
/* @__PURE__ */ jsx("div", {
|
|
3159
|
+
ref: alertRef,
|
|
3160
|
+
role: "alert",
|
|
3161
|
+
"data-slot": "toast-announcer",
|
|
3162
|
+
className: "sr-only"
|
|
3163
|
+
})
|
|
3164
|
+
] });
|
|
3165
|
+
}
|
|
3166
|
+
const typeClasses = {
|
|
3167
|
+
neutral: "border-popover-border bg-popover-bg text-fg",
|
|
3168
|
+
loading: "border-popover-border bg-popover-bg text-fg",
|
|
3169
|
+
success: "border-success-border bg-success-subtle-bg text-success-subtle-fg",
|
|
3170
|
+
info: "border-info-border bg-info-subtle-bg text-info-subtle-fg",
|
|
3171
|
+
warning: "border-warning-border bg-warning-subtle-bg text-warning-subtle-fg",
|
|
3172
|
+
danger: "border-danger-border bg-danger-subtle-bg text-danger-subtle-fg"
|
|
3173
|
+
};
|
|
3174
|
+
const icons = [
|
|
3175
|
+
[
|
|
3176
|
+
"success",
|
|
3177
|
+
"text-success",
|
|
3178
|
+
/* @__PURE__ */ jsx(CircleCheck, {}, "success")
|
|
3179
|
+
],
|
|
3180
|
+
[
|
|
3181
|
+
"info",
|
|
3182
|
+
"text-info",
|
|
3183
|
+
/* @__PURE__ */ jsx(Info, {}, "info")
|
|
3184
|
+
],
|
|
3185
|
+
[
|
|
3186
|
+
"warning",
|
|
3187
|
+
"text-warning",
|
|
3188
|
+
/* @__PURE__ */ jsx(TriangleAlert, {}, "warning")
|
|
3189
|
+
],
|
|
3190
|
+
[
|
|
3191
|
+
"danger",
|
|
3192
|
+
"text-danger",
|
|
3193
|
+
/* @__PURE__ */ jsx(CircleX, {}, "danger")
|
|
3194
|
+
]
|
|
3195
|
+
];
|
|
3196
|
+
const iconLayer = "inline-flex opacity-0 transition-opacity duration-200 data-active:opacity-100";
|
|
3197
|
+
function ToastIcon({ type }) {
|
|
3198
|
+
const loading = type === "loading";
|
|
3199
|
+
return /* @__PURE__ */ jsxs("span", {
|
|
3200
|
+
"aria-hidden": true,
|
|
3201
|
+
"data-slot": "toast-icon",
|
|
3202
|
+
className: cn("mt-0.5 grid size-4 shrink-0 [&_svg]:size-4 *:col-start-1 *:row-start-1", type === "neutral" && "hidden"),
|
|
3203
|
+
children: [icons.map(([iconType, color, icon]) => /* @__PURE__ */ jsx("span", {
|
|
3204
|
+
"data-active": iconType === type || void 0,
|
|
3205
|
+
className: cn(iconLayer, color),
|
|
3206
|
+
children: icon
|
|
3207
|
+
}, iconType)), /* @__PURE__ */ jsxs("span", {
|
|
3208
|
+
"data-active": loading || void 0,
|
|
3209
|
+
className: cn(iconLayer, "text-fg-muted"),
|
|
3210
|
+
children: [/* @__PURE__ */ jsx(LoaderCircle, { className: cn(loading && "animate-spin", "motion-reduce:hidden") }), /* @__PURE__ */ jsx(CircleDashed, { className: "hidden motion-reduce:block" })]
|
|
3211
|
+
})]
|
|
3212
|
+
});
|
|
3213
|
+
}
|
|
3214
|
+
const enterFrom = {
|
|
3215
|
+
bottom: "starting:[translate:0_100%]",
|
|
3216
|
+
top: "starting:[translate:0_-100%]"
|
|
3217
|
+
};
|
|
3218
|
+
function ToastCard({ record, layout, total, expanded, paused, frontHeight, ownHeight, edge, directions, onHeight, onFocusLost, announce }) {
|
|
3219
|
+
const { id, key, type, title, description, action, dismissible, version, removed, swipeOut } = record;
|
|
3220
|
+
const liRef = useRef(null);
|
|
3221
|
+
const bodyRef = useRef(null);
|
|
3222
|
+
const titleRef = useRef(null);
|
|
3223
|
+
const descriptionRef = useRef(null);
|
|
3224
|
+
const titleId = useId();
|
|
3225
|
+
const descriptionId = useId();
|
|
3226
|
+
const [entered, setEntered] = useState(false);
|
|
3227
|
+
const [kept, setKept] = useState(layout ?? {
|
|
3228
|
+
index: 0,
|
|
3229
|
+
offset: 0,
|
|
3230
|
+
visible: true
|
|
3231
|
+
});
|
|
3232
|
+
if (layout && (layout.index !== kept.index || layout.offset !== kept.offset || layout.visible !== kept.visible)) setKept(layout);
|
|
3233
|
+
const { index, offset, visible } = layout ?? kept;
|
|
3234
|
+
const stacked = !expanded && index > 0;
|
|
3235
|
+
const sign = edge === "bottom" ? -1 : 1;
|
|
3236
|
+
const height = stacked ? frontHeight : ownHeight;
|
|
3237
|
+
useLayoutEffect(() => {
|
|
3238
|
+
const body = bodyRef.current;
|
|
3239
|
+
if (!body) return;
|
|
3240
|
+
const measure = () => onHeight(key, body.offsetHeight + CARD_BORDERS);
|
|
3241
|
+
measure();
|
|
3242
|
+
const observer = typeof ResizeObserver === "undefined" ? void 0 : new ResizeObserver(measure);
|
|
3243
|
+
observer?.observe(body);
|
|
3244
|
+
return () => {
|
|
3245
|
+
observer?.disconnect();
|
|
3246
|
+
onHeight(key, void 0);
|
|
3247
|
+
};
|
|
3248
|
+
}, [key, onHeight]);
|
|
3249
|
+
useEffect(() => {
|
|
3250
|
+
const timer = setTimeout(() => setEntered(true), ENTER_MS);
|
|
3251
|
+
return () => clearTimeout(timer);
|
|
3252
|
+
}, []);
|
|
3253
|
+
useLayoutEffect(() => {
|
|
3254
|
+
const titleText = titleRef.current?.textContent?.trim() ?? "";
|
|
3255
|
+
const descriptionText = descriptionRef.current?.textContent?.trim() ?? "";
|
|
3256
|
+
const text = titleText && descriptionText && !/[.!?…:]$/.test(titleText) ? `${titleText}. ${descriptionText}` : `${titleText} ${descriptionText}`.trim();
|
|
3257
|
+
if (text) announce(text, type === "danger");
|
|
3258
|
+
}, [
|
|
3259
|
+
version,
|
|
3260
|
+
type,
|
|
3261
|
+
announce
|
|
3262
|
+
]);
|
|
3263
|
+
const duration = resolveDuration(record);
|
|
3264
|
+
const timer = useRef({
|
|
3265
|
+
version: -1,
|
|
3266
|
+
duration: 0,
|
|
3267
|
+
remaining: 0
|
|
3268
|
+
});
|
|
3269
|
+
const holding = paused || !visible;
|
|
3270
|
+
useEffect(() => {
|
|
3271
|
+
const t = timer.current;
|
|
3272
|
+
if (t.version !== version || t.duration !== duration) {
|
|
3273
|
+
t.version = version;
|
|
3274
|
+
t.duration = duration;
|
|
3275
|
+
t.remaining = duration;
|
|
3276
|
+
}
|
|
3277
|
+
if (holding || removed || !Number.isFinite(t.remaining)) return;
|
|
3278
|
+
const start = Date.now();
|
|
3279
|
+
const handle = setTimeout(() => dismiss(id), Math.max(0, t.remaining));
|
|
3280
|
+
return () => {
|
|
3281
|
+
clearTimeout(handle);
|
|
3282
|
+
t.remaining -= Date.now() - start;
|
|
3283
|
+
};
|
|
3284
|
+
}, [
|
|
3285
|
+
id,
|
|
3286
|
+
version,
|
|
3287
|
+
duration,
|
|
3288
|
+
holding,
|
|
3289
|
+
removed
|
|
3290
|
+
]);
|
|
3291
|
+
useLayoutEffect(() => {
|
|
3292
|
+
if (!removed) return;
|
|
3293
|
+
const li = liRef.current;
|
|
3294
|
+
if (li?.contains(document.activeElement)) onFocusLost(index);
|
|
3295
|
+
let done = false;
|
|
3296
|
+
const finish = () => {
|
|
3297
|
+
if (done) return;
|
|
3298
|
+
done = true;
|
|
3299
|
+
removeToast(key);
|
|
3300
|
+
};
|
|
3301
|
+
const fallback = setTimeout(finish, EXIT_FALLBACK_MS);
|
|
3302
|
+
const animations = typeof li?.getAnimations === "function" ? li.getAnimations() : void 0;
|
|
3303
|
+
if (animations) Promise.allSettled(animations.map((a) => a.finished)).then(finish);
|
|
3304
|
+
return () => {
|
|
3305
|
+
done = true;
|
|
3306
|
+
clearTimeout(fallback);
|
|
3307
|
+
};
|
|
3308
|
+
}, [
|
|
3309
|
+
removed,
|
|
3310
|
+
key,
|
|
3311
|
+
index,
|
|
3312
|
+
onFocusLost
|
|
3313
|
+
]);
|
|
3314
|
+
const drag = useRef(null);
|
|
3315
|
+
const swipeable = dismissible && !removed;
|
|
3316
|
+
const setSwipe = (li, dx, dy) => {
|
|
3317
|
+
li.style.setProperty("--toast-swipe-x", `${dx}px`);
|
|
3318
|
+
li.style.setProperty("--toast-swipe-y", `${dy}px`);
|
|
3319
|
+
};
|
|
3320
|
+
const handlePointerDown = (event) => {
|
|
3321
|
+
if (!swipeable || event.button !== 0) return;
|
|
3322
|
+
if (event.target.closest("button, a, input, textarea, select, [data-no-swipe]")) return;
|
|
3323
|
+
drag.current = {
|
|
3324
|
+
x: event.clientX,
|
|
3325
|
+
y: event.clientY,
|
|
3326
|
+
time: Date.now(),
|
|
3327
|
+
amount: 0
|
|
3328
|
+
};
|
|
3329
|
+
event.currentTarget.setPointerCapture?.(event.pointerId);
|
|
3330
|
+
};
|
|
3331
|
+
const handlePointerMove = (event) => {
|
|
3332
|
+
const d = drag.current;
|
|
3333
|
+
const li = event.currentTarget;
|
|
3334
|
+
if (!d) return;
|
|
3335
|
+
const dx = event.clientX - d.x;
|
|
3336
|
+
const dy = event.clientY - d.y;
|
|
3337
|
+
if (!d.axis) {
|
|
3338
|
+
if (Math.abs(dx) <= 1 && Math.abs(dy) <= 1) return;
|
|
3339
|
+
if (window.getSelection()?.toString()) {
|
|
3340
|
+
drag.current = null;
|
|
3341
|
+
return;
|
|
3342
|
+
}
|
|
3343
|
+
d.axis = Math.abs(dx) > Math.abs(dy) ? "x" : "y";
|
|
3344
|
+
li.setAttribute("data-swiping", "");
|
|
3345
|
+
}
|
|
3346
|
+
const delta = d.axis === "x" ? dx : dy;
|
|
3347
|
+
const toward = d.axis === "x" ? delta > 0 ? "right" : "left" : delta > 0 ? "bottom" : "top";
|
|
3348
|
+
const allowed = directions.includes(toward);
|
|
3349
|
+
d.amount = allowed ? delta : delta / (1.5 + Math.abs(delta) / 20);
|
|
3350
|
+
d.direction = allowed ? toward : void 0;
|
|
3351
|
+
if (d.axis === "x") setSwipe(li, d.amount, 0);
|
|
3352
|
+
else setSwipe(li, 0, d.amount);
|
|
3353
|
+
};
|
|
3354
|
+
const endDrag = (event, cancelled) => {
|
|
3355
|
+
const d = drag.current;
|
|
3356
|
+
drag.current = null;
|
|
3357
|
+
const li = event.currentTarget;
|
|
3358
|
+
if (!d?.axis) return;
|
|
3359
|
+
li.removeAttribute("data-swiping");
|
|
3360
|
+
const velocity = Math.abs(d.amount) / Math.max(1, Date.now() - d.time);
|
|
3361
|
+
if (!cancelled && d.direction && (Math.abs(d.amount) >= SWIPE_DISTANCE || velocity > SWIPE_VELOCITY)) {
|
|
3362
|
+
dismiss(id, d.direction);
|
|
3363
|
+
return;
|
|
3364
|
+
}
|
|
3365
|
+
setSwipe(li, 0, 0);
|
|
3366
|
+
};
|
|
3367
|
+
const touchAction = !swipeable ? void 0 : directions.length === 1 ? "touch-pan-x" : "touch-none";
|
|
3368
|
+
const style = {
|
|
3369
|
+
"--toast-y": `${sign * (expanded ? offset : index * GAP)}px`,
|
|
3370
|
+
"--toast-scale": expanded ? 1 : Math.max(0, 1 - index * SCALE_STEP),
|
|
3371
|
+
zIndex: total - index
|
|
3372
|
+
};
|
|
3373
|
+
const hasAction = action != null && action !== false;
|
|
3374
|
+
return /* @__PURE__ */ jsx("li", {
|
|
3375
|
+
ref: liRef,
|
|
3376
|
+
tabIndex: 0,
|
|
3377
|
+
"aria-labelledby": titleId,
|
|
3378
|
+
"aria-describedby": description != null && description !== false ? descriptionId : void 0,
|
|
3379
|
+
inert: removed || !visible,
|
|
3380
|
+
"data-slot": "toast",
|
|
3381
|
+
"data-toast-id": id,
|
|
3382
|
+
"data-type": type,
|
|
3383
|
+
"data-index": index,
|
|
3384
|
+
"data-front": index === 0 || void 0,
|
|
3385
|
+
"data-stacked": stacked || void 0,
|
|
3386
|
+
"data-visible": visible,
|
|
3387
|
+
"data-removed": removed || void 0,
|
|
3388
|
+
"data-swipe-out": removed ? swipeOut : void 0,
|
|
3389
|
+
"data-dismissible": dismissible || void 0,
|
|
3390
|
+
"data-entered": entered || void 0,
|
|
3391
|
+
style,
|
|
3392
|
+
onPointerDown: handlePointerDown,
|
|
3393
|
+
onPointerMove: handlePointerMove,
|
|
3394
|
+
onPointerUp: (event) => endDrag(event, false),
|
|
3395
|
+
onPointerCancel: (event) => endDrag(event, true),
|
|
3396
|
+
className: cn("group/toast absolute inset-x-0 rounded-xl outline-none select-none", edge === "bottom" ? "bottom-0 origin-top" : "top-0 origin-bottom", "transform-[translateY(var(--toast-y))_scale(var(--toast-scale))]", "[translate:var(--toast-swipe-x,0px)_var(--toast-swipe-y,0px)]", "transition-[transform,translate,scale,opacity] duration-350 ease-[cubic-bezier(0.25,0.75,0.6,0.98)]", enterFrom[edge], "starting:opacity-0", "after:absolute after:inset-x-0 after:h-3.75", edge === "bottom" ? "after:bottom-full" : "after:top-full", "data-[visible=false]:pointer-events-none data-[visible=false]:opacity-0", "data-removed:pointer-events-none data-removed:scale-99 data-removed:opacity-0 data-removed:duration-150", "data-removed:data-swipe-out:duration-200 data-removed:data-swipe-out:ease-out", "data-[swipe-out=right]:[translate:calc(100%+24px)_0]", "data-[swipe-out=left]:[translate:calc(-100%-24px)_0]", "data-[swipe-out=bottom]:[translate:0_calc(100%+24px)]", "data-[swipe-out=top]:[translate:0_calc(-100%-24px)]", "data-swiping:transition-none", "group-data-moving/viewport:data-entered:transition-none", "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus", touchAction),
|
|
3397
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
3398
|
+
"data-slot": "toast-card",
|
|
3399
|
+
style: { height },
|
|
3400
|
+
className: cn("overflow-hidden rounded-[inherit] border shadow-popover", "forced-colors:border-[CanvasText]", "[transition-property:height,background-color,border-color,color]", "duration-[350ms,200ms,200ms,200ms] ease-[cubic-bezier(0.25,0.75,0.6,0.98)]", typeClasses[type]),
|
|
3401
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
3402
|
+
ref: bodyRef,
|
|
3403
|
+
"data-slot": "toast-body",
|
|
3404
|
+
className: cn("relative p-4 transition-opacity duration-200", "group-data-stacked/toast:pointer-events-none group-data-stacked/toast:opacity-0"),
|
|
3405
|
+
children: [
|
|
3406
|
+
/* @__PURE__ */ jsxs("div", {
|
|
3407
|
+
className: cn("flex items-start gap-3", dismissible && "pe-9.5"),
|
|
3408
|
+
children: [/* @__PURE__ */ jsx(ToastIcon, { type }), /* @__PURE__ */ jsxs("div", {
|
|
3409
|
+
className: "min-w-0 flex-1",
|
|
3410
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
3411
|
+
ref: titleRef,
|
|
3412
|
+
id: titleId,
|
|
3413
|
+
"data-slot": "toast-title",
|
|
3414
|
+
className: "text-sm/5 font-medium",
|
|
3415
|
+
children: title
|
|
3416
|
+
}), description != null && description !== false && /* @__PURE__ */ jsx("div", {
|
|
3417
|
+
ref: descriptionRef,
|
|
3418
|
+
id: descriptionId,
|
|
3419
|
+
"data-slot": "toast-description",
|
|
3420
|
+
className: "mt-0.5 text-sm/5 text-fg-muted",
|
|
3421
|
+
children: description
|
|
3422
|
+
})]
|
|
3423
|
+
})]
|
|
3424
|
+
}),
|
|
3425
|
+
hasAction && /* @__PURE__ */ jsx("div", {
|
|
3426
|
+
"data-slot": "toast-action",
|
|
3427
|
+
className: "mt-3 flex justify-end gap-2",
|
|
3428
|
+
children: isActionButton(action) ? /* @__PURE__ */ jsx(Button, {
|
|
3429
|
+
intent: "secondary",
|
|
3430
|
+
size: "sm",
|
|
3431
|
+
onClick: (event) => {
|
|
3432
|
+
action.onClick?.(event);
|
|
3433
|
+
if (!event.defaultPrevented) dismiss(id);
|
|
3434
|
+
},
|
|
3435
|
+
children: action.label
|
|
3436
|
+
}) : action
|
|
3437
|
+
}),
|
|
3438
|
+
dismissible && /* @__PURE__ */ jsx(Button, {
|
|
3439
|
+
intent: "ghost",
|
|
3440
|
+
size: "sm",
|
|
3441
|
+
icon: /* @__PURE__ */ jsx(X, {}),
|
|
3442
|
+
"aria-label": "Close",
|
|
3443
|
+
tooltip: false,
|
|
3444
|
+
className: "absolute top-2.25 right-2 text-current hover:not-aria-disabled:bg-current/10",
|
|
3445
|
+
onClick: () => dismiss(id)
|
|
3446
|
+
})
|
|
3447
|
+
]
|
|
3448
|
+
})
|
|
3449
|
+
})
|
|
3450
|
+
});
|
|
3451
|
+
}
|
|
3452
|
+
//#endregion
|
|
3453
|
+
export { Badge, Button, Card, CardContent, CardHeader, Checkbox, CheckboxGroup, CopyInput, Dialog, DialogBody, DialogFooter, DialogHeader, Drawer, DrawerBody, DrawerClose, DrawerFooter, DrawerHeader, Input, Meter, PasswordInput, SearchInput, Select, Sidebar, SidebarBadge, SidebarBody, SidebarFooter, SidebarGroup, SidebarHeader, SidebarIcon, SidebarItem, SidebarProvider, SidebarTrigger, Switch, Tab, TabList, TabNav, TabNavLink, TabPanel, Tabs, ThemeSwitcher, Toaster, Tooltip, cn, toast, useTheme };
|