@xex/ui 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +12 -2
- package/dist/chunk-3M3PAPT3.js +215 -0
- package/dist/font-picker.d.ts +16 -0
- package/dist/font-picker.js +325 -0
- package/dist/index.d.ts +390 -0
- package/dist/index.js +1458 -0
- package/package.json +64 -4
package/dist/index.js
ADDED
|
@@ -0,0 +1,1458 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Button,
|
|
3
|
+
LoadingDots,
|
|
4
|
+
Spinner,
|
|
5
|
+
ThemeProvider,
|
|
6
|
+
useTheme
|
|
7
|
+
} from "./chunk-3M3PAPT3.js";
|
|
8
|
+
|
|
9
|
+
// src/components/Badge.tsx
|
|
10
|
+
import clsx from "clsx";
|
|
11
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
12
|
+
var variants = {
|
|
13
|
+
default: { pill: "bg-brand-100 text-brand-700", dot: "bg-brand-500" },
|
|
14
|
+
success: { pill: "bg-emerald-100 text-emerald-700", dot: "bg-emerald-500" },
|
|
15
|
+
warning: { pill: "bg-amber-100 text-amber-700", dot: "bg-amber-500" },
|
|
16
|
+
danger: { pill: "bg-red-100 text-red-700", dot: "bg-red-500" },
|
|
17
|
+
info: { pill: "bg-sky-100 text-sky-700", dot: "bg-sky-500" },
|
|
18
|
+
purple: { pill: "bg-purple-100 text-purple-700", dot: "bg-purple-500" },
|
|
19
|
+
gray: { pill: "bg-gray-100 text-gray-700", dot: "bg-gray-500" }
|
|
20
|
+
};
|
|
21
|
+
function Badge({ variant = "default", dot = false, className, children }) {
|
|
22
|
+
const v = variants[variant];
|
|
23
|
+
return /* @__PURE__ */ jsxs("span", { className: clsx("inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-semibold", v.pill, className), children: [
|
|
24
|
+
dot && /* @__PURE__ */ jsx("span", { className: clsx("h-1.5 w-1.5 rounded-full", v.dot), "aria-hidden": "true" }),
|
|
25
|
+
children
|
|
26
|
+
] });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// src/components/Modal.tsx
|
|
30
|
+
import { useEffect as useEffect2, useId, useRef } from "react";
|
|
31
|
+
import clsx2 from "clsx";
|
|
32
|
+
import { XMarkIcon } from "@heroicons/react/24/outline";
|
|
33
|
+
|
|
34
|
+
// src/components/keyboard.ts
|
|
35
|
+
import { useEffect } from "react";
|
|
36
|
+
var FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
37
|
+
var focusablesIn = (root) => Array.from(root.querySelectorAll(FOCUSABLE)).filter((el) => el.getClientRects().length > 0);
|
|
38
|
+
function radioGroupKeyDown(e) {
|
|
39
|
+
const keys = ["ArrowRight", "ArrowDown", "ArrowLeft", "ArrowUp", "Home", "End"];
|
|
40
|
+
if (!keys.includes(e.key)) return;
|
|
41
|
+
const radios = Array.from(e.currentTarget.querySelectorAll('[role="radio"]:not([disabled])'));
|
|
42
|
+
const i = radios.findIndex((r) => r === document.activeElement || r.contains(document.activeElement));
|
|
43
|
+
if (i < 0) return;
|
|
44
|
+
e.preventDefault();
|
|
45
|
+
const last = radios.length - 1;
|
|
46
|
+
const next = e.key === "Home" ? 0 : e.key === "End" ? last : e.key === "ArrowRight" || e.key === "ArrowDown" ? i === last ? 0 : i + 1 : i === 0 ? last : i - 1;
|
|
47
|
+
radios[next].focus();
|
|
48
|
+
radios[next].click();
|
|
49
|
+
}
|
|
50
|
+
function useDialogFocus(open, panelRef, options = {}) {
|
|
51
|
+
const { initialFocus, lockScroll = true } = options;
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (!open) return;
|
|
54
|
+
const opener = document.activeElement;
|
|
55
|
+
const { overflow, paddingRight } = document.body.style;
|
|
56
|
+
if (lockScroll) {
|
|
57
|
+
const scrollbar = window.innerWidth - document.documentElement.clientWidth;
|
|
58
|
+
document.body.style.overflow = "hidden";
|
|
59
|
+
if (scrollbar > 0) document.body.style.paddingRight = `${scrollbar}px`;
|
|
60
|
+
}
|
|
61
|
+
const panel = panelRef.current;
|
|
62
|
+
if (panel) {
|
|
63
|
+
const target = panel.querySelector("[data-autofocus]") ?? initialFocus?.() ?? focusablesIn(panel)[0] ?? panel;
|
|
64
|
+
target.focus();
|
|
65
|
+
}
|
|
66
|
+
return () => {
|
|
67
|
+
if (lockScroll) {
|
|
68
|
+
document.body.style.overflow = overflow;
|
|
69
|
+
document.body.style.paddingRight = paddingRight;
|
|
70
|
+
}
|
|
71
|
+
if (opener && document.contains(opener)) opener.focus();
|
|
72
|
+
};
|
|
73
|
+
}, [open]);
|
|
74
|
+
return (e) => {
|
|
75
|
+
const panel = panelRef.current;
|
|
76
|
+
if (e.key !== "Tab" || !panel) return;
|
|
77
|
+
const items = focusablesIn(panel);
|
|
78
|
+
if (items.length === 0) {
|
|
79
|
+
e.preventDefault();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const first = items[0];
|
|
83
|
+
const last = items[items.length - 1];
|
|
84
|
+
const active = document.activeElement;
|
|
85
|
+
if (e.shiftKey && (active === first || active === panel)) {
|
|
86
|
+
e.preventDefault();
|
|
87
|
+
last.focus();
|
|
88
|
+
} else if (!e.shiftKey && active === last) {
|
|
89
|
+
e.preventDefault();
|
|
90
|
+
first.focus();
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// src/components/Modal.tsx
|
|
96
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
97
|
+
var sizes = {
|
|
98
|
+
sm: "max-w-sm",
|
|
99
|
+
md: "max-w-md",
|
|
100
|
+
lg: "max-w-lg",
|
|
101
|
+
xl: "max-w-xl"
|
|
102
|
+
};
|
|
103
|
+
function Modal({ open, onClose, title, footer, size = "md", ariaLabel, children }) {
|
|
104
|
+
const panelRef = useRef(null);
|
|
105
|
+
const bodyRef = useRef(null);
|
|
106
|
+
const titleId = useId();
|
|
107
|
+
const trapTab = useDialogFocus(open, panelRef, {
|
|
108
|
+
initialFocus: () => bodyRef.current && focusablesIn(bodyRef.current)[0]
|
|
109
|
+
});
|
|
110
|
+
useEffect2(() => {
|
|
111
|
+
if (!open) return;
|
|
112
|
+
const onKey = (e) => {
|
|
113
|
+
if (e.key === "Escape") onClose();
|
|
114
|
+
};
|
|
115
|
+
document.addEventListener("keydown", onKey);
|
|
116
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
117
|
+
}, [open, onClose]);
|
|
118
|
+
if (!open) return null;
|
|
119
|
+
return /* @__PURE__ */ jsxs2("div", { className: "fixed inset-0 z-50 flex items-center justify-center p-4", children: [
|
|
120
|
+
/* @__PURE__ */ jsx2("div", { className: "absolute inset-0 bg-black/40 backdrop-blur-sm", onClick: onClose, "aria-hidden": "true" }),
|
|
121
|
+
/* @__PURE__ */ jsxs2(
|
|
122
|
+
"div",
|
|
123
|
+
{
|
|
124
|
+
ref: panelRef,
|
|
125
|
+
role: "dialog",
|
|
126
|
+
"aria-modal": "true",
|
|
127
|
+
"aria-labelledby": title ? titleId : void 0,
|
|
128
|
+
"aria-label": title ? void 0 : ariaLabel,
|
|
129
|
+
tabIndex: -1,
|
|
130
|
+
onKeyDown: trapTab,
|
|
131
|
+
className: clsx2("relative flex max-h-[90vh] w-full flex-col rounded-2xl bg-white shadow-2xl focus:outline-none", sizes[size]),
|
|
132
|
+
children: [
|
|
133
|
+
title && /* @__PURE__ */ jsxs2("div", { className: "flex items-center justify-between border-b border-gray-100 px-6 py-4", children: [
|
|
134
|
+
/* @__PURE__ */ jsx2("h2", { id: titleId, className: "text-lg font-semibold text-gray-900", children: title }),
|
|
135
|
+
/* @__PURE__ */ jsx2(
|
|
136
|
+
"button",
|
|
137
|
+
{
|
|
138
|
+
type: "button",
|
|
139
|
+
onClick: onClose,
|
|
140
|
+
"aria-label": "Close",
|
|
141
|
+
className: "rounded-lg p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-600 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-400",
|
|
142
|
+
children: /* @__PURE__ */ jsx2(XMarkIcon, { className: "h-5 w-5" })
|
|
143
|
+
}
|
|
144
|
+
)
|
|
145
|
+
] }),
|
|
146
|
+
/* @__PURE__ */ jsx2("div", { ref: bodyRef, className: "flex-1 overflow-y-auto px-6 py-5", children }),
|
|
147
|
+
footer && /* @__PURE__ */ jsx2("div", { className: "flex justify-end gap-2 border-t border-gray-100 px-6 py-4", children: footer })
|
|
148
|
+
]
|
|
149
|
+
}
|
|
150
|
+
)
|
|
151
|
+
] });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// src/components/ConfirmDialog.tsx
|
|
155
|
+
import { ExclamationTriangleIcon } from "@heroicons/react/24/solid";
|
|
156
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
157
|
+
function ConfirmDialog({
|
|
158
|
+
open,
|
|
159
|
+
title,
|
|
160
|
+
message,
|
|
161
|
+
confirmLabel = "Delete",
|
|
162
|
+
cancelLabel = "Cancel",
|
|
163
|
+
loading,
|
|
164
|
+
onConfirm,
|
|
165
|
+
onCancel
|
|
166
|
+
}) {
|
|
167
|
+
return /* @__PURE__ */ jsxs3(Modal, { open, onClose: onCancel, size: "lg", ariaLabel: title, children: [
|
|
168
|
+
/* @__PURE__ */ jsxs3("div", { role: "alert", className: "flex items-start gap-3 rounded-xl border border-red-200 bg-red-50 p-4", children: [
|
|
169
|
+
/* @__PURE__ */ jsx3(ExclamationTriangleIcon, { className: "mt-0.5 h-6 w-6 shrink-0 text-red-600" }),
|
|
170
|
+
/* @__PURE__ */ jsxs3("div", { children: [
|
|
171
|
+
/* @__PURE__ */ jsx3("p", { className: "font-bold text-red-800", children: title }),
|
|
172
|
+
/* @__PURE__ */ jsx3("p", { className: "mt-1 text-sm text-red-700", children: message })
|
|
173
|
+
] })
|
|
174
|
+
] }),
|
|
175
|
+
/* @__PURE__ */ jsxs3("div", { className: "mt-5 flex justify-end gap-2", children: [
|
|
176
|
+
/* @__PURE__ */ jsx3(Button, { variant: "secondary", onClick: onCancel, "data-autofocus": true, children: cancelLabel }),
|
|
177
|
+
/* @__PURE__ */ jsx3(Button, { variant: "danger", loading, onClick: onConfirm, children: confirmLabel })
|
|
178
|
+
] })
|
|
179
|
+
] });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// src/components/Select.tsx
|
|
183
|
+
import { useId as useId2 } from "react";
|
|
184
|
+
import clsx3 from "clsx";
|
|
185
|
+
import { ChevronDownIcon } from "@heroicons/react/20/solid";
|
|
186
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
187
|
+
function Select({ label, options, placeholder, error, hint, className, id, disabled, ...rest }) {
|
|
188
|
+
const autoId = useId2();
|
|
189
|
+
const selectId = id ?? autoId;
|
|
190
|
+
return /* @__PURE__ */ jsxs4("div", { className, children: [
|
|
191
|
+
label && /* @__PURE__ */ jsx4("label", { htmlFor: selectId, className: "mb-1 block text-sm font-medium text-gray-700", children: label }),
|
|
192
|
+
/* @__PURE__ */ jsxs4("div", { className: "relative", children: [
|
|
193
|
+
/* @__PURE__ */ jsxs4(
|
|
194
|
+
"select",
|
|
195
|
+
{
|
|
196
|
+
id: selectId,
|
|
197
|
+
disabled,
|
|
198
|
+
"aria-invalid": !!error || void 0,
|
|
199
|
+
className: clsx3(
|
|
200
|
+
"w-full appearance-none rounded-lg border bg-white px-3 py-2 pr-9 text-sm text-gray-900",
|
|
201
|
+
"focus:outline-none focus:ring-2 focus:ring-brand-400 focus:border-brand-400",
|
|
202
|
+
"disabled:bg-gray-50 disabled:text-gray-400 disabled:cursor-not-allowed",
|
|
203
|
+
error ? "border-red-400" : "border-gray-300"
|
|
204
|
+
),
|
|
205
|
+
...rest,
|
|
206
|
+
children: [
|
|
207
|
+
placeholder !== void 0 && /* @__PURE__ */ jsx4("option", { value: "", children: placeholder }),
|
|
208
|
+
options.map((o) => /* @__PURE__ */ jsx4("option", { value: o.value, children: o.label }, o.value))
|
|
209
|
+
]
|
|
210
|
+
}
|
|
211
|
+
),
|
|
212
|
+
/* @__PURE__ */ jsx4(ChevronDownIcon, { className: "pointer-events-none absolute right-3 top-1/2 h-5 w-5 -translate-y-1/2 text-gray-400" })
|
|
213
|
+
] }),
|
|
214
|
+
error ? /* @__PURE__ */ jsx4("p", { className: "mt-1 text-xs text-red-600", children: error }) : hint ? /* @__PURE__ */ jsx4("p", { className: "mt-1 text-xs text-gray-500", children: hint }) : null
|
|
215
|
+
] });
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// src/components/Input.tsx
|
|
219
|
+
import { useId as useId3 } from "react";
|
|
220
|
+
import clsx4 from "clsx";
|
|
221
|
+
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
222
|
+
function Input({ label, error, hint, icon, className, id, ...rest }) {
|
|
223
|
+
const autoId = useId3();
|
|
224
|
+
const inputId = id ?? autoId;
|
|
225
|
+
return /* @__PURE__ */ jsxs5("div", { className, children: [
|
|
226
|
+
label && /* @__PURE__ */ jsx5("label", { htmlFor: inputId, className: "mb-1 block text-sm font-medium text-gray-700", children: label }),
|
|
227
|
+
/* @__PURE__ */ jsxs5("div", { className: "relative", children: [
|
|
228
|
+
icon && /* @__PURE__ */ jsx5("span", { className: "pointer-events-none absolute left-3 top-1/2 flex -translate-y-1/2 items-center text-gray-400 [&>svg]:h-5 [&>svg]:w-5", children: icon }),
|
|
229
|
+
/* @__PURE__ */ jsx5(
|
|
230
|
+
"input",
|
|
231
|
+
{
|
|
232
|
+
id: inputId,
|
|
233
|
+
"aria-invalid": !!error || void 0,
|
|
234
|
+
className: clsx4(
|
|
235
|
+
"w-full rounded-lg border bg-white px-3 py-2 text-sm text-gray-900 placeholder:text-gray-400",
|
|
236
|
+
"focus:outline-none focus:ring-2 focus:ring-brand-400 focus:border-brand-400",
|
|
237
|
+
"disabled:cursor-not-allowed disabled:bg-gray-50 disabled:text-gray-400",
|
|
238
|
+
icon && "pl-9",
|
|
239
|
+
error ? "border-red-400" : "border-gray-300"
|
|
240
|
+
),
|
|
241
|
+
...rest
|
|
242
|
+
}
|
|
243
|
+
)
|
|
244
|
+
] }),
|
|
245
|
+
error ? /* @__PURE__ */ jsx5("p", { className: "mt-1 text-xs text-red-600", children: error }) : hint ? /* @__PURE__ */ jsx5("p", { className: "mt-1 text-xs text-gray-500", children: hint }) : null
|
|
246
|
+
] });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// src/components/Avatar.tsx
|
|
250
|
+
import clsx5 from "clsx";
|
|
251
|
+
import { jsx as jsx6 } from "react/jsx-runtime";
|
|
252
|
+
var sizes2 = {
|
|
253
|
+
xs: "h-6 w-6 text-[10px]",
|
|
254
|
+
sm: "h-8 w-8 text-xs",
|
|
255
|
+
md: "h-10 w-10 text-sm",
|
|
256
|
+
lg: "h-12 w-12 text-base",
|
|
257
|
+
xl: "h-14 w-14 text-lg"
|
|
258
|
+
};
|
|
259
|
+
var AVATAR_COLORS = ["#6366f1", "#0ea5e9", "#10b981", "#f59e0b", "#ef4444", "#8b5cf6", "#ec4899", "#14b8a6"];
|
|
260
|
+
function initialsOf(name) {
|
|
261
|
+
const parts = name.trim().split(/\s+/).filter(Boolean);
|
|
262
|
+
if (parts.length === 0) return "?";
|
|
263
|
+
const first = parts[0][0];
|
|
264
|
+
const last = parts.length > 1 ? parts[parts.length - 1][0] : "";
|
|
265
|
+
return (first + last).toUpperCase();
|
|
266
|
+
}
|
|
267
|
+
function colorFor(seed) {
|
|
268
|
+
let h = 0;
|
|
269
|
+
for (const ch of seed) h = h * 31 + ch.charCodeAt(0) >>> 0;
|
|
270
|
+
return AVATAR_COLORS[h % AVATAR_COLORS.length];
|
|
271
|
+
}
|
|
272
|
+
function Avatar({ name, size = "md", color, className }) {
|
|
273
|
+
return /* @__PURE__ */ jsx6(
|
|
274
|
+
"span",
|
|
275
|
+
{
|
|
276
|
+
title: name,
|
|
277
|
+
style: { backgroundColor: color ?? colorFor(name) },
|
|
278
|
+
className: clsx5("inline-flex shrink-0 select-none items-center justify-center rounded-full font-bold text-white", sizes2[size], className),
|
|
279
|
+
children: initialsOf(name)
|
|
280
|
+
}
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// src/components/PaginationBar.tsx
|
|
285
|
+
import clsx6 from "clsx";
|
|
286
|
+
import { Fragment, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
287
|
+
function pageWindow(page, totalPages) {
|
|
288
|
+
if (totalPages <= 7) return Array.from({ length: totalPages }, (_, i) => i + 1);
|
|
289
|
+
const out = [1];
|
|
290
|
+
const start = Math.max(2, page - 1);
|
|
291
|
+
const end = Math.min(totalPages - 1, page + 1);
|
|
292
|
+
if (start > 2) out.push(null);
|
|
293
|
+
for (let p = start; p <= end; p++) out.push(p);
|
|
294
|
+
if (end < totalPages - 1) out.push(null);
|
|
295
|
+
out.push(totalPages);
|
|
296
|
+
return out;
|
|
297
|
+
}
|
|
298
|
+
var navBtn = "rounded-md px-2.5 py-1 text-sm text-gray-600 hover:bg-gray-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-400 disabled:opacity-40 disabled:hover:bg-transparent";
|
|
299
|
+
function PaginationBar({
|
|
300
|
+
page,
|
|
301
|
+
totalPages,
|
|
302
|
+
total,
|
|
303
|
+
pageSize,
|
|
304
|
+
onPageChange,
|
|
305
|
+
pageSizeOptions,
|
|
306
|
+
onPageSizeChange,
|
|
307
|
+
right,
|
|
308
|
+
placement
|
|
309
|
+
}) {
|
|
310
|
+
const from = total === 0 ? 0 : (page - 1) * pageSize + 1;
|
|
311
|
+
const to = Math.min(page * pageSize, total);
|
|
312
|
+
return /* @__PURE__ */ jsxs6(
|
|
313
|
+
"div",
|
|
314
|
+
{
|
|
315
|
+
className: clsx6(
|
|
316
|
+
"flex flex-wrap items-center justify-between gap-3 border-gray-100 px-4 py-2.5",
|
|
317
|
+
placement === "above" ? "border-b" : placement === "below" ? "border-t" : "border-y"
|
|
318
|
+
),
|
|
319
|
+
children: [
|
|
320
|
+
/* @__PURE__ */ jsx7("div", { children: totalPages > 1 && /* @__PURE__ */ jsxs6("nav", { "aria-label": placement ? `Pagination, ${placement} table` : "Pagination", className: "flex items-center gap-1", children: [
|
|
321
|
+
/* @__PURE__ */ jsx7("button", { type: "button", className: navBtn, disabled: page <= 1, onClick: () => onPageChange(page - 1), children: "\xAB Prev" }),
|
|
322
|
+
pageWindow(page, totalPages).map(
|
|
323
|
+
(p, i) => p === null ? /* @__PURE__ */ jsx7("span", { className: "px-1 text-gray-400", children: "\u2026" }, `gap-${i}`) : /* @__PURE__ */ jsx7(
|
|
324
|
+
"button",
|
|
325
|
+
{
|
|
326
|
+
type: "button",
|
|
327
|
+
"aria-current": p === page ? "page" : void 0,
|
|
328
|
+
onClick: () => onPageChange(p),
|
|
329
|
+
className: clsx6(
|
|
330
|
+
"min-w-[2rem] rounded-md px-2 py-1 text-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-400",
|
|
331
|
+
p === page ? "bg-brand-600 font-semibold text-white" : "text-gray-600 hover:bg-gray-100"
|
|
332
|
+
),
|
|
333
|
+
children: p
|
|
334
|
+
},
|
|
335
|
+
p
|
|
336
|
+
)
|
|
337
|
+
),
|
|
338
|
+
/* @__PURE__ */ jsx7("button", { type: "button", className: navBtn, disabled: page >= totalPages, onClick: () => onPageChange(page + 1), children: "Next \xBB" })
|
|
339
|
+
] }) }),
|
|
340
|
+
/* @__PURE__ */ jsxs6("div", { className: "ml-auto flex items-center gap-3 text-sm text-gray-500", children: [
|
|
341
|
+
right && /* @__PURE__ */ jsxs6(Fragment, { children: [
|
|
342
|
+
right,
|
|
343
|
+
/* @__PURE__ */ jsx7("span", { className: "text-gray-300", "aria-hidden": "true", children: "|" })
|
|
344
|
+
] }),
|
|
345
|
+
pageSizeOptions && onPageSizeChange && /* @__PURE__ */ jsxs6("label", { className: "flex items-center gap-2", children: [
|
|
346
|
+
"Rows per page",
|
|
347
|
+
/* @__PURE__ */ jsx7(
|
|
348
|
+
"select",
|
|
349
|
+
{
|
|
350
|
+
value: pageSize,
|
|
351
|
+
onChange: (e) => onPageSizeChange(Number(e.target.value)),
|
|
352
|
+
className: "rounded-md border border-gray-300 bg-white px-1.5 py-1 text-sm text-gray-700 focus:outline-none focus:ring-2 focus:ring-brand-400 focus:border-brand-400",
|
|
353
|
+
children: pageSizeOptions.map((n) => /* @__PURE__ */ jsx7("option", { value: n, children: n }, n))
|
|
354
|
+
}
|
|
355
|
+
)
|
|
356
|
+
] }),
|
|
357
|
+
/* @__PURE__ */ jsxs6("span", { className: "tabular-nums", children: [
|
|
358
|
+
from,
|
|
359
|
+
"\u2013",
|
|
360
|
+
to,
|
|
361
|
+
" of ",
|
|
362
|
+
total
|
|
363
|
+
] })
|
|
364
|
+
] })
|
|
365
|
+
]
|
|
366
|
+
}
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// src/components/SegmentedControl.tsx
|
|
371
|
+
import clsx7 from "clsx";
|
|
372
|
+
import { jsx as jsx8 } from "react/jsx-runtime";
|
|
373
|
+
function SegmentedControl({ options, value, onChange, label, className }) {
|
|
374
|
+
return /* @__PURE__ */ jsx8(
|
|
375
|
+
"div",
|
|
376
|
+
{
|
|
377
|
+
role: "radiogroup",
|
|
378
|
+
"aria-label": label,
|
|
379
|
+
onKeyDown: radioGroupKeyDown,
|
|
380
|
+
className: clsx7("inline-flex rounded-lg bg-gray-100 p-0.5", className),
|
|
381
|
+
children: options.map((o) => {
|
|
382
|
+
const selected = o.value === value;
|
|
383
|
+
return /* @__PURE__ */ jsx8(
|
|
384
|
+
"button",
|
|
385
|
+
{
|
|
386
|
+
type: "button",
|
|
387
|
+
role: "radio",
|
|
388
|
+
"aria-checked": selected,
|
|
389
|
+
tabIndex: selected ? 0 : -1,
|
|
390
|
+
onClick: () => onChange(o.value),
|
|
391
|
+
className: clsx7(
|
|
392
|
+
"rounded-md px-2.5 py-1 text-xs font-medium transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-400",
|
|
393
|
+
selected ? "bg-white text-gray-900 shadow-sm" : "text-gray-500 hover:text-gray-800"
|
|
394
|
+
),
|
|
395
|
+
children: o.label
|
|
396
|
+
},
|
|
397
|
+
o.value
|
|
398
|
+
);
|
|
399
|
+
})
|
|
400
|
+
}
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// src/components/DataTable.tsx
|
|
405
|
+
import { useEffect as useEffect4, useMemo, useRef as useRef3, useState as useState2 } from "react";
|
|
406
|
+
import clsx9 from "clsx";
|
|
407
|
+
import { ArrowsRightLeftIcon, ArrowsUpDownIcon, ChevronDownIcon as ChevronDownIcon3, ChevronUpDownIcon, ChevronUpIcon } from "@heroicons/react/16/solid";
|
|
408
|
+
import { MagnifyingGlassIcon, XMarkIcon as XMarkIcon2 } from "@heroicons/react/24/outline";
|
|
409
|
+
|
|
410
|
+
// src/components/MultiSelect.tsx
|
|
411
|
+
import { useEffect as useEffect3, useId as useId4, useRef as useRef2, useState } from "react";
|
|
412
|
+
import clsx8 from "clsx";
|
|
413
|
+
import { ChevronDownIcon as ChevronDownIcon2 } from "@heroicons/react/20/solid";
|
|
414
|
+
import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
415
|
+
function MultiSelect({ options, value, onChange, label, className }) {
|
|
416
|
+
const [open, setOpen] = useState(false);
|
|
417
|
+
const rootRef = useRef2(null);
|
|
418
|
+
const triggerRef = useRef2(null);
|
|
419
|
+
const panelId = useId4();
|
|
420
|
+
useEffect3(() => {
|
|
421
|
+
if (!open) return;
|
|
422
|
+
const onDown = (e) => {
|
|
423
|
+
if (!rootRef.current?.contains(e.target)) setOpen(false);
|
|
424
|
+
};
|
|
425
|
+
const onKey = (e) => {
|
|
426
|
+
if (e.key !== "Escape") return;
|
|
427
|
+
setOpen(false);
|
|
428
|
+
triggerRef.current?.focus();
|
|
429
|
+
};
|
|
430
|
+
document.addEventListener("mousedown", onDown);
|
|
431
|
+
document.addEventListener("keydown", onKey);
|
|
432
|
+
return () => {
|
|
433
|
+
document.removeEventListener("mousedown", onDown);
|
|
434
|
+
document.removeEventListener("keydown", onKey);
|
|
435
|
+
};
|
|
436
|
+
}, [open]);
|
|
437
|
+
const labels = options.filter((o) => value.includes(o.value)).map((o) => o.label);
|
|
438
|
+
const summary = labels.length === 0 ? label : labels.length <= 2 ? labels.join(", ") : `${labels.length} selected`;
|
|
439
|
+
const toggle = (v) => onChange(value.includes(v) ? value.filter((x) => x !== v) : [...value, v]);
|
|
440
|
+
return /* @__PURE__ */ jsxs7(
|
|
441
|
+
"div",
|
|
442
|
+
{
|
|
443
|
+
ref: rootRef,
|
|
444
|
+
className: clsx8("relative", className),
|
|
445
|
+
onBlur: (e) => {
|
|
446
|
+
if (open && e.relatedTarget && !rootRef.current?.contains(e.relatedTarget)) setOpen(false);
|
|
447
|
+
},
|
|
448
|
+
children: [
|
|
449
|
+
/* @__PURE__ */ jsxs7(
|
|
450
|
+
"button",
|
|
451
|
+
{
|
|
452
|
+
ref: triggerRef,
|
|
453
|
+
type: "button",
|
|
454
|
+
"aria-haspopup": "true",
|
|
455
|
+
"aria-expanded": open,
|
|
456
|
+
"aria-controls": open ? panelId : void 0,
|
|
457
|
+
"aria-label": labels.length ? `${label}: ${labels.join(", ")}` : label,
|
|
458
|
+
onClick: () => setOpen((o) => !o),
|
|
459
|
+
className: clsx8(
|
|
460
|
+
"flex w-full items-center justify-between gap-2 rounded-lg border bg-white px-3 py-2 text-left text-sm focus:outline-none focus:ring-2 focus:ring-brand-400 focus:border-brand-400",
|
|
461
|
+
value.length ? "border-brand-500 text-gray-900" : "border-gray-300 text-gray-500"
|
|
462
|
+
),
|
|
463
|
+
children: [
|
|
464
|
+
/* @__PURE__ */ jsx9("span", { className: "truncate", children: summary }),
|
|
465
|
+
/* @__PURE__ */ jsx9(ChevronDownIcon2, { className: "h-5 w-5 shrink-0 text-gray-400" })
|
|
466
|
+
]
|
|
467
|
+
}
|
|
468
|
+
),
|
|
469
|
+
open && /* @__PURE__ */ jsxs7(
|
|
470
|
+
"div",
|
|
471
|
+
{
|
|
472
|
+
id: panelId,
|
|
473
|
+
role: "group",
|
|
474
|
+
"aria-label": label,
|
|
475
|
+
onMouseDown: (e) => {
|
|
476
|
+
if (!(e.target instanceof HTMLInputElement)) e.preventDefault();
|
|
477
|
+
},
|
|
478
|
+
className: "absolute left-0 z-20 mt-1 max-h-64 min-w-full overflow-y-auto rounded-xl border border-gray-200 bg-white p-1 shadow-xl",
|
|
479
|
+
children: [
|
|
480
|
+
options.map((o) => /* @__PURE__ */ jsxs7("label", { className: "flex cursor-default items-center gap-2 rounded-md px-2.5 py-1.5 text-sm text-gray-800 hover:bg-gray-50", children: [
|
|
481
|
+
/* @__PURE__ */ jsx9("input", { type: "checkbox", className: "h-4 w-4 rounded", checked: value.includes(o.value), onChange: () => toggle(o.value) }),
|
|
482
|
+
/* @__PURE__ */ jsx9("span", { className: "flex-1 whitespace-nowrap", children: o.label }),
|
|
483
|
+
o.count !== void 0 && /* @__PURE__ */ jsx9("span", { className: "text-xs tabular-nums text-gray-400", children: o.count })
|
|
484
|
+
] }, o.value)),
|
|
485
|
+
value.length > 0 && /* @__PURE__ */ jsx9(
|
|
486
|
+
"button",
|
|
487
|
+
{
|
|
488
|
+
type: "button",
|
|
489
|
+
onClick: () => onChange([]),
|
|
490
|
+
className: "mt-1 w-full rounded-md border-t border-gray-100 px-2.5 py-1.5 text-left text-xs font-medium text-brand-700 hover:bg-gray-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-400",
|
|
491
|
+
children: "Clear selection"
|
|
492
|
+
}
|
|
493
|
+
)
|
|
494
|
+
]
|
|
495
|
+
}
|
|
496
|
+
)
|
|
497
|
+
]
|
|
498
|
+
}
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// src/components/DataTable.tsx
|
|
503
|
+
import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
504
|
+
var MIN_COL_PX = 64;
|
|
505
|
+
var MIN_TABLE_COL_PX = 96;
|
|
506
|
+
function applyResize(base, i, delta, tableWidth) {
|
|
507
|
+
const n = i + 1;
|
|
508
|
+
if (n >= base.length) return base;
|
|
509
|
+
const min = MIN_COL_PX / tableWidth;
|
|
510
|
+
const d = Math.min(Math.max(delta, min - base[i]), base[n] - min);
|
|
511
|
+
const next = [...base];
|
|
512
|
+
next[i] = base[i] + d;
|
|
513
|
+
next[n] = base[n] - d;
|
|
514
|
+
return next;
|
|
515
|
+
}
|
|
516
|
+
var collator = new Intl.Collator(void 0, { numeric: true, sensitivity: "base" });
|
|
517
|
+
function DataTable({
|
|
518
|
+
columns,
|
|
519
|
+
rows,
|
|
520
|
+
rowKey,
|
|
521
|
+
label,
|
|
522
|
+
searchable = true,
|
|
523
|
+
searchPlaceholder = "Search\u2026",
|
|
524
|
+
pageSizeOptions = [5, 10, 25],
|
|
525
|
+
defaultPageSize = pageSizeOptions[0],
|
|
526
|
+
initialSort,
|
|
527
|
+
emptyMessage = "No rows match your filters."
|
|
528
|
+
}) {
|
|
529
|
+
const [query, setQuery] = useState2("");
|
|
530
|
+
const [filters, setFilters] = useState2({});
|
|
531
|
+
const [sort, setSort] = useState2(initialSort ?? null);
|
|
532
|
+
const [page, setPage] = useState2(1);
|
|
533
|
+
const [pageSize, setPageSize] = useState2(defaultPageSize);
|
|
534
|
+
const [dense, setDense] = useState2(false);
|
|
535
|
+
const [custom, setCustom] = useState2(null);
|
|
536
|
+
const tableRef = useRef3(null);
|
|
537
|
+
const wrapperRef = useRef3(null);
|
|
538
|
+
const drag = useRef3(null);
|
|
539
|
+
const [grow, setGrow] = useState2(0);
|
|
540
|
+
const [overflowing, setOverflowing] = useState2(false);
|
|
541
|
+
const [resizing, setResizing] = useState2(null);
|
|
542
|
+
const weights = columns.map((c) => c.width ?? 1);
|
|
543
|
+
const weightSum = weights.reduce((a, b) => a + b, 0);
|
|
544
|
+
const defaults2 = weights.map((w) => w / weightSum);
|
|
545
|
+
const widths = custom && custom.length === columns.length ? custom : defaults2;
|
|
546
|
+
const customized = custom !== null && custom.length === columns.length || grow > 0;
|
|
547
|
+
useEffect4(() => {
|
|
548
|
+
const wrapper = wrapperRef.current;
|
|
549
|
+
const table = tableRef.current;
|
|
550
|
+
if (!wrapper || !table) return;
|
|
551
|
+
const check = () => setOverflowing(table.offsetWidth > wrapper.clientWidth + 1);
|
|
552
|
+
check();
|
|
553
|
+
const ro = new ResizeObserver(check);
|
|
554
|
+
ro.observe(wrapper);
|
|
555
|
+
ro.observe(table);
|
|
556
|
+
return () => ro.disconnect();
|
|
557
|
+
}, []);
|
|
558
|
+
const growLast = (base, tableWidth, startGrow, delta) => {
|
|
559
|
+
const last = base.length - 1;
|
|
560
|
+
const px = base.map((f) => f * tableWidth);
|
|
561
|
+
const d = Math.max(delta, MIN_COL_PX - px[last], -startGrow);
|
|
562
|
+
px[last] += d;
|
|
563
|
+
const total = tableWidth + d;
|
|
564
|
+
setCustom(px.map((p) => p / total));
|
|
565
|
+
setGrow(startGrow + d);
|
|
566
|
+
};
|
|
567
|
+
useEffect4(() => {
|
|
568
|
+
if (resizing === null) return;
|
|
569
|
+
const { cursor, userSelect } = document.body.style;
|
|
570
|
+
document.body.style.cursor = "col-resize";
|
|
571
|
+
document.body.style.userSelect = "none";
|
|
572
|
+
return () => {
|
|
573
|
+
document.body.style.cursor = cursor;
|
|
574
|
+
document.body.style.userSelect = userSelect;
|
|
575
|
+
};
|
|
576
|
+
}, [resizing]);
|
|
577
|
+
const onHandleDown = (e, i) => {
|
|
578
|
+
if (e.button !== 0 || !tableRef.current) return;
|
|
579
|
+
e.preventDefault();
|
|
580
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
581
|
+
drag.current = { i, startX: e.clientX, base: widths, tableWidth: tableRef.current.getBoundingClientRect().width, grow };
|
|
582
|
+
setResizing(i);
|
|
583
|
+
};
|
|
584
|
+
const onHandleMove = (e) => {
|
|
585
|
+
const d = drag.current;
|
|
586
|
+
if (!d) return;
|
|
587
|
+
if (d.i === d.base.length - 1) growLast(d.base, d.tableWidth, d.grow, e.clientX - d.startX);
|
|
588
|
+
else setCustom(applyResize(d.base, d.i, (e.clientX - d.startX) / d.tableWidth, d.tableWidth));
|
|
589
|
+
};
|
|
590
|
+
const onHandleUp = (e) => {
|
|
591
|
+
if (e.currentTarget.hasPointerCapture(e.pointerId)) e.currentTarget.releasePointerCapture(e.pointerId);
|
|
592
|
+
drag.current = null;
|
|
593
|
+
setResizing(null);
|
|
594
|
+
};
|
|
595
|
+
const onHandleKey = (e, i) => {
|
|
596
|
+
if (e.key !== "ArrowLeft" && e.key !== "ArrowRight") return;
|
|
597
|
+
if (!tableRef.current) return;
|
|
598
|
+
e.preventDefault();
|
|
599
|
+
const tableWidth = tableRef.current.getBoundingClientRect().width;
|
|
600
|
+
const px = (e.shiftKey ? 40 : 10) * (e.key === "ArrowRight" ? 1 : -1);
|
|
601
|
+
if (i === columns.length - 1) growLast(widths, tableWidth, grow, px);
|
|
602
|
+
else setCustom(applyResize(widths, i, px / tableWidth, tableWidth));
|
|
603
|
+
};
|
|
604
|
+
const resetWidths = () => {
|
|
605
|
+
setCustom(null);
|
|
606
|
+
setGrow(0);
|
|
607
|
+
};
|
|
608
|
+
const filterColumns = columns.filter((c) => c.filter);
|
|
609
|
+
const filterActive = query.trim() !== "" || Object.values(filters).some((v) => v.length > 0);
|
|
610
|
+
const filterOptions = useMemo(
|
|
611
|
+
() => Object.fromEntries(
|
|
612
|
+
filterColumns.map((c) => [
|
|
613
|
+
c.id,
|
|
614
|
+
[...rows.reduce((m, r) => m.set(String(c.accessor(r)), (m.get(String(c.accessor(r))) ?? 0) + 1), /* @__PURE__ */ new Map())].sort((a, b) => collator.compare(a[0], b[0])).map(([v, count]) => ({ value: v, label: v, count }))
|
|
615
|
+
])
|
|
616
|
+
),
|
|
617
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
618
|
+
[rows, columns]
|
|
619
|
+
);
|
|
620
|
+
const visible = useMemo(() => {
|
|
621
|
+
const q = query.trim().toLowerCase();
|
|
622
|
+
let out = rows.filter((row) => {
|
|
623
|
+
for (const c of filterColumns) {
|
|
624
|
+
const want = filters[c.id];
|
|
625
|
+
if (want?.length && !want.includes(String(c.accessor(row)))) return false;
|
|
626
|
+
}
|
|
627
|
+
return !q || columns.some((c) => String(c.accessor(row)).toLowerCase().includes(q));
|
|
628
|
+
});
|
|
629
|
+
const col = sort && columns.find((c) => c.id === sort.columnId);
|
|
630
|
+
if (col && sort) {
|
|
631
|
+
const dir = sort.direction === "asc" ? 1 : -1;
|
|
632
|
+
out = [...out].sort((a, b) => {
|
|
633
|
+
const x = col.accessor(a);
|
|
634
|
+
const y = col.accessor(b);
|
|
635
|
+
return (typeof x === "number" && typeof y === "number" ? x - y : collator.compare(String(x), String(y))) * dir;
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
return out;
|
|
639
|
+
}, [rows, columns, query, filters, sort]);
|
|
640
|
+
const totalPages = Math.max(1, Math.ceil(visible.length / pageSize));
|
|
641
|
+
const current = Math.min(page, totalPages);
|
|
642
|
+
const pageRows = visible.slice((current - 1) * pageSize, current * pageSize);
|
|
643
|
+
const reset = () => setPage(1);
|
|
644
|
+
const toggleSort = (id) => {
|
|
645
|
+
reset();
|
|
646
|
+
setSort((s) => s?.columnId !== id ? { columnId: id, direction: "asc" } : s.direction === "asc" ? { columnId: id, direction: "desc" } : null);
|
|
647
|
+
};
|
|
648
|
+
const clearAll = () => {
|
|
649
|
+
setQuery("");
|
|
650
|
+
setFilters({});
|
|
651
|
+
reset();
|
|
652
|
+
};
|
|
653
|
+
const sortColumn = sort && columns.find((c) => c.id === sort.columnId);
|
|
654
|
+
const bar = (placement) => /* @__PURE__ */ jsx10(
|
|
655
|
+
PaginationBar,
|
|
656
|
+
{
|
|
657
|
+
placement,
|
|
658
|
+
page: current,
|
|
659
|
+
totalPages,
|
|
660
|
+
total: visible.length,
|
|
661
|
+
pageSize,
|
|
662
|
+
onPageChange: setPage,
|
|
663
|
+
pageSizeOptions,
|
|
664
|
+
onPageSizeChange: (n) => {
|
|
665
|
+
setPageSize(n);
|
|
666
|
+
reset();
|
|
667
|
+
},
|
|
668
|
+
right: /* @__PURE__ */ jsx10(
|
|
669
|
+
SegmentedControl,
|
|
670
|
+
{
|
|
671
|
+
label: "Row density",
|
|
672
|
+
value: dense ? "compact" : "comfortable",
|
|
673
|
+
onChange: (v) => setDense(v === "compact"),
|
|
674
|
+
options: [
|
|
675
|
+
{ value: "comfortable", label: "Comfortable" },
|
|
676
|
+
{ value: "compact", label: "Compact" }
|
|
677
|
+
]
|
|
678
|
+
}
|
|
679
|
+
)
|
|
680
|
+
}
|
|
681
|
+
);
|
|
682
|
+
const pad = dense ? "px-4 py-1.5" : "px-4 py-3";
|
|
683
|
+
return /* @__PURE__ */ jsxs8("div", { className: "rounded-xl border border-gray-200", children: [
|
|
684
|
+
(searchable || filterColumns.length > 0 || sort || customized) && /* @__PURE__ */ jsxs8("div", { className: "flex flex-wrap items-end gap-3 rounded-t-xl border-b border-gray-100 p-3", children: [
|
|
685
|
+
searchable && /* @__PURE__ */ jsx10(
|
|
686
|
+
Input,
|
|
687
|
+
{
|
|
688
|
+
className: "w-full sm:w-64",
|
|
689
|
+
"aria-label": `Search ${label}`,
|
|
690
|
+
placeholder: searchPlaceholder,
|
|
691
|
+
icon: /* @__PURE__ */ jsx10(MagnifyingGlassIcon, {}),
|
|
692
|
+
value: query,
|
|
693
|
+
onChange: (e) => {
|
|
694
|
+
setQuery(e.target.value);
|
|
695
|
+
reset();
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
),
|
|
699
|
+
filterColumns.map((c) => /* @__PURE__ */ jsx10(
|
|
700
|
+
MultiSelect,
|
|
701
|
+
{
|
|
702
|
+
className: "w-full sm:w-44",
|
|
703
|
+
label: c.filterLabel ?? `All ${c.header.toLowerCase()}`,
|
|
704
|
+
value: filters[c.id] ?? [],
|
|
705
|
+
onChange: (v) => {
|
|
706
|
+
setFilters((f) => ({ ...f, [c.id]: v }));
|
|
707
|
+
reset();
|
|
708
|
+
},
|
|
709
|
+
options: filterOptions[c.id]
|
|
710
|
+
},
|
|
711
|
+
c.id
|
|
712
|
+
)),
|
|
713
|
+
filterActive && /* @__PURE__ */ jsx10(Button, { variant: "ghost", size: "sm", icon: /* @__PURE__ */ jsx10(XMarkIcon2, { className: "h-4 w-4" }), onClick: clearAll, children: "Clear filters" }),
|
|
714
|
+
sortColumn && sort && /* @__PURE__ */ jsxs8(
|
|
715
|
+
Button,
|
|
716
|
+
{
|
|
717
|
+
variant: "ghost",
|
|
718
|
+
size: "sm",
|
|
719
|
+
icon: /* @__PURE__ */ jsx10(ArrowsUpDownIcon, { className: "h-4 w-4" }),
|
|
720
|
+
onClick: () => {
|
|
721
|
+
setSort(null);
|
|
722
|
+
reset();
|
|
723
|
+
},
|
|
724
|
+
title: "Remove sorting (a third click on the sorted header does the same)",
|
|
725
|
+
children: [
|
|
726
|
+
"Sorted by ",
|
|
727
|
+
sortColumn.header,
|
|
728
|
+
" (",
|
|
729
|
+
sort.direction === "asc" ? "A\u2192Z" : "Z\u2192A",
|
|
730
|
+
") \xB7 Reset"
|
|
731
|
+
]
|
|
732
|
+
}
|
|
733
|
+
),
|
|
734
|
+
customized && /* @__PURE__ */ jsx10(
|
|
735
|
+
Button,
|
|
736
|
+
{
|
|
737
|
+
variant: "ghost",
|
|
738
|
+
size: "sm",
|
|
739
|
+
icon: /* @__PURE__ */ jsx10(ArrowsRightLeftIcon, { className: "h-4 w-4" }),
|
|
740
|
+
onClick: resetWidths,
|
|
741
|
+
title: "Restore the default column widths (double-clicking a column edge does the same)",
|
|
742
|
+
children: "Reset column widths"
|
|
743
|
+
}
|
|
744
|
+
),
|
|
745
|
+
/* @__PURE__ */ jsx10("p", { role: "status", className: "ml-auto self-center text-sm text-gray-500", children: visible.length === rows.length ? `${rows.length} rows` : `${visible.length} of ${rows.length} rows` })
|
|
746
|
+
] }),
|
|
747
|
+
bar("above"),
|
|
748
|
+
/* @__PURE__ */ jsx10("div", { ref: wrapperRef, className: "overflow-x-auto", children: /* @__PURE__ */ jsxs8(
|
|
749
|
+
"table",
|
|
750
|
+
{
|
|
751
|
+
ref: tableRef,
|
|
752
|
+
"aria-label": label,
|
|
753
|
+
className: "w-full table-fixed text-left text-sm",
|
|
754
|
+
style: { minWidth: columns.length * MIN_TABLE_COL_PX, ...grow > 0 && { width: `calc(max(100%, ${columns.length * MIN_TABLE_COL_PX}px) + ${grow}px)` } },
|
|
755
|
+
children: [
|
|
756
|
+
/* @__PURE__ */ jsx10("colgroup", { children: columns.map((c, i) => /* @__PURE__ */ jsx10("col", { style: { width: `${widths[i] * 100}%` } }, c.id)) }),
|
|
757
|
+
/* @__PURE__ */ jsx10("thead", { className: "bg-gray-50 text-xs uppercase tracking-wide text-gray-500", children: /* @__PURE__ */ jsx10("tr", { children: columns.map((c, i) => {
|
|
758
|
+
const sorted = sort?.columnId === c.id ? sort.direction : null;
|
|
759
|
+
const sortable = c.sortable !== false;
|
|
760
|
+
const Icon = sorted === "asc" ? ChevronUpIcon : sorted === "desc" ? ChevronDownIcon3 : ChevronUpDownIcon;
|
|
761
|
+
return /* @__PURE__ */ jsxs8(
|
|
762
|
+
"th",
|
|
763
|
+
{
|
|
764
|
+
scope: "col",
|
|
765
|
+
"aria-sort": sortable ? sorted === "asc" ? "ascending" : sorted === "desc" ? "descending" : "none" : void 0,
|
|
766
|
+
className: clsx9("relative px-4 py-2.5 font-semibold", c.align === "right" && "text-right"),
|
|
767
|
+
children: [
|
|
768
|
+
sortable ? /* @__PURE__ */ jsxs8(
|
|
769
|
+
"button",
|
|
770
|
+
{
|
|
771
|
+
type: "button",
|
|
772
|
+
onClick: () => toggleSort(c.id),
|
|
773
|
+
className: clsx9(
|
|
774
|
+
"-mx-1.5 inline-flex max-w-[calc(100%+0.75rem)] items-center gap-1 rounded px-1.5 py-0.5 uppercase tracking-wide hover:text-gray-900 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-400",
|
|
775
|
+
sorted ? "text-gray-900" : "text-gray-500"
|
|
776
|
+
),
|
|
777
|
+
children: [
|
|
778
|
+
/* @__PURE__ */ jsx10("span", { className: "truncate", children: c.header }),
|
|
779
|
+
/* @__PURE__ */ jsx10(Icon, { className: clsx9("h-4 w-4 shrink-0", sorted ? "text-brand-600" : "text-gray-400"), "aria-hidden": "true" })
|
|
780
|
+
]
|
|
781
|
+
}
|
|
782
|
+
) : /* @__PURE__ */ jsx10("span", { className: "block truncate", children: c.header }),
|
|
783
|
+
columns.length > 1 && (i < columns.length - 1 || overflowing || grow > 0) && /* @__PURE__ */ jsx10(
|
|
784
|
+
"span",
|
|
785
|
+
{
|
|
786
|
+
role: "separator",
|
|
787
|
+
"aria-orientation": "vertical",
|
|
788
|
+
"aria-label": i === columns.length - 1 ? `Resize ${c.header} column (widens the table)` : `Resize ${c.header} column`,
|
|
789
|
+
"aria-valuemin": 0,
|
|
790
|
+
"aria-valuemax": 100,
|
|
791
|
+
"aria-valuenow": Math.round(widths[i] * 100),
|
|
792
|
+
tabIndex: 0,
|
|
793
|
+
title: "Drag to resize \xB7 double-click to reset",
|
|
794
|
+
onPointerDown: (e) => onHandleDown(e, i),
|
|
795
|
+
onPointerMove: onHandleMove,
|
|
796
|
+
onPointerUp: onHandleUp,
|
|
797
|
+
onPointerCancel: onHandleUp,
|
|
798
|
+
onKeyDown: (e) => onHandleKey(e, i),
|
|
799
|
+
onDoubleClick: resetWidths,
|
|
800
|
+
className: clsx9(
|
|
801
|
+
"group absolute inset-y-1 z-10 flex w-3 cursor-col-resize touch-none select-none justify-center rounded",
|
|
802
|
+
i === columns.length - 1 ? "right-0" : "-right-1.5"
|
|
803
|
+
),
|
|
804
|
+
children: /* @__PURE__ */ jsx10(
|
|
805
|
+
"span",
|
|
806
|
+
{
|
|
807
|
+
className: clsx9(
|
|
808
|
+
"my-auto h-5 w-0.5 rounded-full transition-colors group-hover:bg-brand-500 group-focus-visible:bg-brand-500",
|
|
809
|
+
resizing === i ? "bg-brand-500" : "bg-gray-300"
|
|
810
|
+
)
|
|
811
|
+
}
|
|
812
|
+
)
|
|
813
|
+
}
|
|
814
|
+
)
|
|
815
|
+
]
|
|
816
|
+
},
|
|
817
|
+
c.id
|
|
818
|
+
);
|
|
819
|
+
}) }) }),
|
|
820
|
+
/* @__PURE__ */ jsxs8("tbody", { children: [
|
|
821
|
+
pageRows.map((row) => /* @__PURE__ */ jsx10("tr", { className: "border-t border-gray-100 hover:bg-gray-50", children: columns.map((c) => /* @__PURE__ */ jsx10("td", { className: clsx9(pad, "overflow-hidden text-gray-600", c.align === "right" && "text-right"), children: c.cell ? c.cell(row, { dense }) : /* @__PURE__ */ jsx10("span", { className: "block truncate", children: String(c.accessor(row)) }) }, c.id)) }, rowKey(row))),
|
|
822
|
+
pageRows.length === 0 && /* @__PURE__ */ jsx10("tr", { className: "border-t border-gray-100", children: /* @__PURE__ */ jsxs8("td", { colSpan: columns.length, className: "px-4 py-10 text-center text-sm text-gray-500", children: [
|
|
823
|
+
emptyMessage,
|
|
824
|
+
" ",
|
|
825
|
+
filterActive && /* @__PURE__ */ jsx10("button", { type: "button", onClick: clearAll, className: "font-medium text-brand-700 underline", children: "Clear filters" })
|
|
826
|
+
] }) })
|
|
827
|
+
] })
|
|
828
|
+
]
|
|
829
|
+
}
|
|
830
|
+
) }),
|
|
831
|
+
bar("below")
|
|
832
|
+
] });
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// src/components/Progress.tsx
|
|
836
|
+
import clsx10 from "clsx";
|
|
837
|
+
import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
838
|
+
var fills = {
|
|
839
|
+
brand: "bg-brand-600",
|
|
840
|
+
success: "bg-emerald-500",
|
|
841
|
+
warning: "bg-amber-500",
|
|
842
|
+
danger: "bg-red-500"
|
|
843
|
+
};
|
|
844
|
+
var strokes = {
|
|
845
|
+
brand: "text-brand-600",
|
|
846
|
+
success: "text-emerald-500",
|
|
847
|
+
warning: "text-amber-500",
|
|
848
|
+
danger: "text-red-500"
|
|
849
|
+
};
|
|
850
|
+
var heights = { sm: "h-1", md: "h-2", lg: "h-3" };
|
|
851
|
+
function ProgressBar({ value, label, showLabel = false, tone = "brand", size = "md", className }) {
|
|
852
|
+
const determinate = typeof value === "number";
|
|
853
|
+
const pct = determinate ? Math.min(100, Math.max(0, value)) : 0;
|
|
854
|
+
return /* @__PURE__ */ jsxs9("div", { className, children: [
|
|
855
|
+
showLabel && /* @__PURE__ */ jsxs9("div", { className: "mb-1 flex justify-between text-xs text-gray-600", children: [
|
|
856
|
+
/* @__PURE__ */ jsx11("span", { children: label }),
|
|
857
|
+
determinate && /* @__PURE__ */ jsxs9("span", { className: "tabular-nums", children: [
|
|
858
|
+
Math.round(pct),
|
|
859
|
+
"%"
|
|
860
|
+
] })
|
|
861
|
+
] }),
|
|
862
|
+
/* @__PURE__ */ jsx11(
|
|
863
|
+
"div",
|
|
864
|
+
{
|
|
865
|
+
role: "progressbar",
|
|
866
|
+
"aria-label": label,
|
|
867
|
+
"aria-valuemin": 0,
|
|
868
|
+
"aria-valuemax": 100,
|
|
869
|
+
"aria-valuenow": determinate ? Math.round(pct) : void 0,
|
|
870
|
+
className: clsx10("relative w-full overflow-hidden rounded-full bg-gray-200", heights[size]),
|
|
871
|
+
children: determinate ? /* @__PURE__ */ jsx11("div", { className: clsx10("h-full rounded-full transition-[width] duration-300 ease-out", fills[tone]), style: { width: `${pct}%` } }) : /* @__PURE__ */ jsx11("div", { className: clsx10("xex-indeterminate absolute inset-y-0 w-2/5 rounded-full", fills[tone]) })
|
|
872
|
+
}
|
|
873
|
+
)
|
|
874
|
+
] });
|
|
875
|
+
}
|
|
876
|
+
function ProgressRing({ value, label, size = 48, tone = "brand", showValue = true }) {
|
|
877
|
+
const determinate = typeof value === "number";
|
|
878
|
+
const pct = determinate ? Math.min(100, Math.max(0, value)) : 25;
|
|
879
|
+
const stroke = Math.max(3, Math.round(size / 10));
|
|
880
|
+
const r = (size - stroke) / 2;
|
|
881
|
+
const c = 2 * Math.PI * r;
|
|
882
|
+
return /* @__PURE__ */ jsxs9(
|
|
883
|
+
"div",
|
|
884
|
+
{
|
|
885
|
+
role: "progressbar",
|
|
886
|
+
"aria-label": label,
|
|
887
|
+
"aria-valuemin": 0,
|
|
888
|
+
"aria-valuemax": 100,
|
|
889
|
+
"aria-valuenow": determinate ? Math.round(pct) : void 0,
|
|
890
|
+
className: "relative inline-flex items-center justify-center",
|
|
891
|
+
style: { width: size, height: size },
|
|
892
|
+
children: [
|
|
893
|
+
/* @__PURE__ */ jsxs9("svg", { width: size, height: size, viewBox: `0 0 ${size} ${size}`, className: clsx10("-rotate-90", !determinate && "animate-spin"), "aria-hidden": "true", children: [
|
|
894
|
+
/* @__PURE__ */ jsx11("circle", { cx: size / 2, cy: size / 2, r, fill: "none", strokeWidth: stroke, className: "xex-ring-track" }),
|
|
895
|
+
/* @__PURE__ */ jsx11(
|
|
896
|
+
"circle",
|
|
897
|
+
{
|
|
898
|
+
cx: size / 2,
|
|
899
|
+
cy: size / 2,
|
|
900
|
+
r,
|
|
901
|
+
fill: "none",
|
|
902
|
+
strokeWidth: stroke,
|
|
903
|
+
strokeLinecap: "round",
|
|
904
|
+
stroke: "currentColor",
|
|
905
|
+
className: clsx10(strokes[tone], determinate && "transition-[stroke-dashoffset] duration-300 ease-out"),
|
|
906
|
+
strokeDasharray: c,
|
|
907
|
+
strokeDashoffset: c * (1 - pct / 100)
|
|
908
|
+
}
|
|
909
|
+
)
|
|
910
|
+
] }),
|
|
911
|
+
determinate && showValue && size >= 40 && /* @__PURE__ */ jsxs9("span", { className: "absolute text-xs font-semibold tabular-nums text-gray-700", children: [
|
|
912
|
+
Math.round(pct),
|
|
913
|
+
"%"
|
|
914
|
+
] })
|
|
915
|
+
]
|
|
916
|
+
}
|
|
917
|
+
);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// src/components/Skeleton.tsx
|
|
921
|
+
import clsx11 from "clsx";
|
|
922
|
+
import { jsx as jsx12 } from "react/jsx-runtime";
|
|
923
|
+
function Skeleton({ shape = "rect", className }) {
|
|
924
|
+
return /* @__PURE__ */ jsx12(
|
|
925
|
+
"span",
|
|
926
|
+
{
|
|
927
|
+
"aria-hidden": "true",
|
|
928
|
+
className: clsx11(
|
|
929
|
+
"block animate-pulse bg-gray-200 motion-reduce:animate-none",
|
|
930
|
+
shape === "circle" ? "rounded-full" : shape === "text" ? "h-3 w-full rounded" : "rounded-lg",
|
|
931
|
+
className
|
|
932
|
+
)
|
|
933
|
+
}
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
function SkeletonText({ lines = 3, className }) {
|
|
937
|
+
return /* @__PURE__ */ jsx12("div", { className: clsx11("space-y-2", className), "aria-hidden": "true", children: Array.from({ length: lines }, (_, i) => /* @__PURE__ */ jsx12(Skeleton, { shape: "text", className: i === lines - 1 && lines > 1 ? "w-2/3" : void 0 }, i)) });
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// src/components/LoadingOverlay.tsx
|
|
941
|
+
import { jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
942
|
+
function LoadingOverlay({ loading, label = "Loading\u2026", children, className }) {
|
|
943
|
+
return /* @__PURE__ */ jsxs10("div", { className: className ? `relative ${className}` : "relative", "aria-busy": loading || void 0, children: [
|
|
944
|
+
/* @__PURE__ */ jsx13("div", { inert: loading || void 0, children }),
|
|
945
|
+
loading && /* @__PURE__ */ jsxs10("div", { className: "xex-overlay absolute inset-0 z-10 flex items-center justify-center gap-3 rounded-[inherit] backdrop-blur-[1px]", children: [
|
|
946
|
+
/* @__PURE__ */ jsx13(Spinner, { size: "lg", label, className: "text-brand-600" }),
|
|
947
|
+
/* @__PURE__ */ jsx13("span", { className: "text-sm font-medium text-gray-700", "aria-hidden": "true", children: label })
|
|
948
|
+
] })
|
|
949
|
+
] });
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// src/components/FetchStatus.tsx
|
|
953
|
+
import { CheckCircleIcon, ExclamationCircleIcon } from "@heroicons/react/20/solid";
|
|
954
|
+
import { CloudIcon } from "@heroicons/react/24/outline";
|
|
955
|
+
import { jsx as jsx14, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
956
|
+
var defaults = {
|
|
957
|
+
idle: "Not loaded yet",
|
|
958
|
+
loading: "Fetching",
|
|
959
|
+
success: "Up to date",
|
|
960
|
+
error: "Couldn\u2019t load data"
|
|
961
|
+
};
|
|
962
|
+
function FetchStatus({ state, messages, onRetry }) {
|
|
963
|
+
const text = messages?.[state] ?? defaults[state];
|
|
964
|
+
return /* @__PURE__ */ jsxs11("p", { role: "status", "aria-live": "polite", className: "flex items-center gap-2 text-sm", children: [
|
|
965
|
+
state === "idle" && /* @__PURE__ */ jsx14(CloudIcon, { className: "h-5 w-5 text-gray-400" }),
|
|
966
|
+
state === "loading" && /* @__PURE__ */ jsx14(Spinner, { size: "md", decorative: true, className: "text-brand-600" }),
|
|
967
|
+
state === "success" && /* @__PURE__ */ jsx14(CheckCircleIcon, { className: "h-5 w-5 text-emerald-500" }),
|
|
968
|
+
state === "error" && /* @__PURE__ */ jsx14(ExclamationCircleIcon, { className: "h-5 w-5 text-red-500" }),
|
|
969
|
+
/* @__PURE__ */ jsx14("span", { className: state === "error" ? "text-red-700" : state === "idle" ? "text-gray-500" : "text-gray-700", children: text }),
|
|
970
|
+
state === "loading" && /* @__PURE__ */ jsx14(LoadingDots, { className: "text-gray-400" }),
|
|
971
|
+
state === "error" && onRetry && /* @__PURE__ */ jsx14(Button, { size: "xs", variant: "outline", onClick: onRetry, children: "Retry" })
|
|
972
|
+
] });
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// src/components/Tabs.tsx
|
|
976
|
+
import { useCallback, useId as useId5, useLayoutEffect, useRef as useRef4, useState as useState3 } from "react";
|
|
977
|
+
import clsx12 from "clsx";
|
|
978
|
+
import { jsx as jsx15, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
979
|
+
var sizes3 = {
|
|
980
|
+
sm: "px-3 py-1.5 text-xs gap-1.5",
|
|
981
|
+
md: "px-4 py-2.5 text-sm gap-2"
|
|
982
|
+
};
|
|
983
|
+
function Tabs({
|
|
984
|
+
items,
|
|
985
|
+
value,
|
|
986
|
+
defaultValue,
|
|
987
|
+
onChange,
|
|
988
|
+
variant = "underline",
|
|
989
|
+
size = "md",
|
|
990
|
+
activation = "auto",
|
|
991
|
+
keepMounted = false,
|
|
992
|
+
fullWidth = false,
|
|
993
|
+
label,
|
|
994
|
+
className
|
|
995
|
+
}) {
|
|
996
|
+
const uid = useId5();
|
|
997
|
+
const firstEnabled = items.find((i) => !i.disabled)?.id ?? items[0]?.id;
|
|
998
|
+
const [internal, setInternal] = useState3(defaultValue ?? firstEnabled);
|
|
999
|
+
const selected = value ?? internal;
|
|
1000
|
+
const select = (id) => {
|
|
1001
|
+
if (value === void 0) setInternal(id);
|
|
1002
|
+
onChange?.(id);
|
|
1003
|
+
};
|
|
1004
|
+
const tabRefs = useRef4({});
|
|
1005
|
+
const trackRef = useRef4(null);
|
|
1006
|
+
const [line, setLine] = useState3(null);
|
|
1007
|
+
const tabId = (id) => `${uid}-tab-${id}`;
|
|
1008
|
+
const panelId = (id) => `${uid}-panel-${id}`;
|
|
1009
|
+
const measure = useCallback(() => {
|
|
1010
|
+
const el = tabRefs.current[selected];
|
|
1011
|
+
if (el) setLine({ left: el.offsetLeft, width: el.offsetWidth });
|
|
1012
|
+
}, [selected]);
|
|
1013
|
+
useLayoutEffect(() => {
|
|
1014
|
+
measure();
|
|
1015
|
+
const track = trackRef.current;
|
|
1016
|
+
if (!track) return;
|
|
1017
|
+
const ro = new ResizeObserver(measure);
|
|
1018
|
+
ro.observe(track);
|
|
1019
|
+
return () => ro.disconnect();
|
|
1020
|
+
}, [measure, items.length]);
|
|
1021
|
+
const onKeyDown = (e) => {
|
|
1022
|
+
const enabled = items.filter((i) => !i.disabled);
|
|
1023
|
+
if (enabled.length === 0) return;
|
|
1024
|
+
const current = enabled.findIndex((i) => tabRefs.current[i.id] === document.activeElement);
|
|
1025
|
+
let next = -1;
|
|
1026
|
+
if (e.key === "ArrowRight") next = current < 0 ? 0 : (current + 1) % enabled.length;
|
|
1027
|
+
else if (e.key === "ArrowLeft") next = current < 0 ? enabled.length - 1 : (current - 1 + enabled.length) % enabled.length;
|
|
1028
|
+
else if (e.key === "Home") next = 0;
|
|
1029
|
+
else if (e.key === "End") next = enabled.length - 1;
|
|
1030
|
+
if (next < 0) return;
|
|
1031
|
+
e.preventDefault();
|
|
1032
|
+
const target = enabled[next];
|
|
1033
|
+
tabRefs.current[target.id]?.focus();
|
|
1034
|
+
tabRefs.current[target.id]?.scrollIntoView({ block: "nearest", inline: "nearest" });
|
|
1035
|
+
if (activation === "auto") select(target.id);
|
|
1036
|
+
};
|
|
1037
|
+
return /* @__PURE__ */ jsxs12("div", { className, children: [
|
|
1038
|
+
/* @__PURE__ */ jsx15("div", { className: clsx12(variant === "underline" && "border-b border-gray-200"), children: /* @__PURE__ */ jsx15("div", { className: "-m-1.5 scroll-px-2 overflow-x-auto p-1.5", children: /* @__PURE__ */ jsxs12(
|
|
1039
|
+
"div",
|
|
1040
|
+
{
|
|
1041
|
+
ref: trackRef,
|
|
1042
|
+
role: "tablist",
|
|
1043
|
+
"aria-label": label,
|
|
1044
|
+
onKeyDown,
|
|
1045
|
+
className: clsx12("relative flex", variant === "pills" && "gap-1", fullWidth ? "w-full" : "w-max min-w-full"),
|
|
1046
|
+
children: [
|
|
1047
|
+
items.map((item) => {
|
|
1048
|
+
const isSelected = item.id === selected;
|
|
1049
|
+
return /* @__PURE__ */ jsxs12(
|
|
1050
|
+
"button",
|
|
1051
|
+
{
|
|
1052
|
+
ref: (node) => {
|
|
1053
|
+
tabRefs.current[item.id] = node;
|
|
1054
|
+
},
|
|
1055
|
+
id: tabId(item.id),
|
|
1056
|
+
type: "button",
|
|
1057
|
+
role: "tab",
|
|
1058
|
+
"aria-selected": isSelected,
|
|
1059
|
+
"aria-controls": item.content !== void 0 && (isSelected || keepMounted) ? panelId(item.id) : void 0,
|
|
1060
|
+
tabIndex: isSelected ? 0 : -1,
|
|
1061
|
+
disabled: item.disabled,
|
|
1062
|
+
onClick: () => select(item.id),
|
|
1063
|
+
className: clsx12(
|
|
1064
|
+
"inline-flex shrink-0 items-center justify-center whitespace-nowrap font-medium transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-400 disabled:cursor-not-allowed disabled:opacity-50",
|
|
1065
|
+
sizes3[size],
|
|
1066
|
+
fullWidth && "flex-1",
|
|
1067
|
+
variant === "underline" ? clsx12("rounded-t-lg", isSelected ? "text-brand-700" : "text-gray-500 hover:text-gray-800") : clsx12("rounded-full", isSelected ? "bg-brand-600 text-white" : "text-gray-600 hover:bg-gray-100")
|
|
1068
|
+
),
|
|
1069
|
+
children: [
|
|
1070
|
+
item.icon,
|
|
1071
|
+
item.label,
|
|
1072
|
+
item.badge !== void 0 && /* @__PURE__ */ jsx15(
|
|
1073
|
+
"span",
|
|
1074
|
+
{
|
|
1075
|
+
className: clsx12(
|
|
1076
|
+
"rounded-full px-1.5 py-px text-[11px] font-semibold tabular-nums",
|
|
1077
|
+
isSelected ? variant === "pills" ? "bg-white/25 text-white" : "bg-brand-100 text-brand-700" : "bg-gray-100 text-gray-600"
|
|
1078
|
+
),
|
|
1079
|
+
children: item.badge
|
|
1080
|
+
}
|
|
1081
|
+
)
|
|
1082
|
+
]
|
|
1083
|
+
},
|
|
1084
|
+
item.id
|
|
1085
|
+
);
|
|
1086
|
+
}),
|
|
1087
|
+
variant === "underline" && line && /* @__PURE__ */ jsx15(
|
|
1088
|
+
"span",
|
|
1089
|
+
{
|
|
1090
|
+
"aria-hidden": "true",
|
|
1091
|
+
className: "pointer-events-none absolute -bottom-px left-0 h-0.5 rounded-full bg-brand-600 transition-[transform,width] duration-200 ease-out motion-reduce:transition-none",
|
|
1092
|
+
style: { width: line.width, transform: `translateX(${line.left}px)` }
|
|
1093
|
+
}
|
|
1094
|
+
)
|
|
1095
|
+
]
|
|
1096
|
+
}
|
|
1097
|
+
) }) }),
|
|
1098
|
+
items.map((item) => {
|
|
1099
|
+
const isSelected = item.id === selected;
|
|
1100
|
+
if (item.content === void 0 || !isSelected && !keepMounted) return null;
|
|
1101
|
+
return /* @__PURE__ */ jsx15("div", { id: panelId(item.id), role: "tabpanel", "aria-labelledby": tabId(item.id), hidden: !isSelected, tabIndex: 0, className: "pt-4", children: item.content }, item.id);
|
|
1102
|
+
})
|
|
1103
|
+
] });
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// src/layout/Header.tsx
|
|
1107
|
+
import { Bars3Icon } from "@heroicons/react/24/outline";
|
|
1108
|
+
import { jsx as jsx16, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
1109
|
+
function Header({ title, subtitle, actions, onMenu }) {
|
|
1110
|
+
return /* @__PURE__ */ jsxs13("header", { className: "sticky top-0 z-30 flex flex-wrap items-center justify-between gap-3 border-b border-gray-200 bg-white px-4 py-4 sm:px-6", children: [
|
|
1111
|
+
/* @__PURE__ */ jsxs13("div", { className: "flex min-w-0 items-center gap-3", children: [
|
|
1112
|
+
onMenu && /* @__PURE__ */ jsx16(
|
|
1113
|
+
"button",
|
|
1114
|
+
{
|
|
1115
|
+
type: "button",
|
|
1116
|
+
onClick: onMenu,
|
|
1117
|
+
"aria-label": "Open navigation",
|
|
1118
|
+
className: "rounded-lg p-1.5 text-gray-600 hover:bg-gray-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-400 lg:hidden",
|
|
1119
|
+
children: /* @__PURE__ */ jsx16(Bars3Icon, { className: "h-6 w-6" })
|
|
1120
|
+
}
|
|
1121
|
+
),
|
|
1122
|
+
/* @__PURE__ */ jsxs13("div", { className: "min-w-0", children: [
|
|
1123
|
+
/* @__PURE__ */ jsx16("h1", { className: "truncate text-xl font-bold text-gray-900", children: title }),
|
|
1124
|
+
subtitle && /* @__PURE__ */ jsx16("p", { className: "text-sm text-gray-500", children: subtitle })
|
|
1125
|
+
] })
|
|
1126
|
+
] }),
|
|
1127
|
+
actions && /* @__PURE__ */ jsx16("div", { className: "flex items-center gap-2", children: actions })
|
|
1128
|
+
] });
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
// src/layout/PageGuide.tsx
|
|
1132
|
+
import { useCallback as useCallback2, useLayoutEffect as useLayoutEffect2, useRef as useRef5, useState as useState4 } from "react";
|
|
1133
|
+
import clsx13 from "clsx";
|
|
1134
|
+
import { jsx as jsx17, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
1135
|
+
var OFFSET = 96;
|
|
1136
|
+
var reducedMotion = () => window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
1137
|
+
function anchors(ids) {
|
|
1138
|
+
const max = Math.max(0, document.documentElement.scrollHeight - window.innerHeight);
|
|
1139
|
+
const ys = ids.map((id) => {
|
|
1140
|
+
const el = document.getElementById(id);
|
|
1141
|
+
return el ? Math.max(0, el.getBoundingClientRect().top + window.scrollY - OFFSET) : 0;
|
|
1142
|
+
});
|
|
1143
|
+
for (let i = 1; i < ys.length; i++) ys[i] = Math.max(ys[i], ys[i - 1]);
|
|
1144
|
+
const k = ys.findIndex((y) => y >= max - 1);
|
|
1145
|
+
if (k >= 0) {
|
|
1146
|
+
const base = k > 0 ? ys[k - 1] : 0;
|
|
1147
|
+
for (let i = k; i < ys.length; i++) ys[i] = base + (max - base) * (i - k + 1) / (ys.length - k);
|
|
1148
|
+
}
|
|
1149
|
+
return ys;
|
|
1150
|
+
}
|
|
1151
|
+
function posAt(ys, y) {
|
|
1152
|
+
const n = ys.length;
|
|
1153
|
+
if (n === 0 || y <= ys[0]) return 0;
|
|
1154
|
+
let i = n - 1;
|
|
1155
|
+
while (i > 0 && ys[i] > y) i--;
|
|
1156
|
+
if (i >= n - 1) return n - 1;
|
|
1157
|
+
const span = ys[i + 1] - ys[i];
|
|
1158
|
+
return span > 0 ? i + (y - ys[i]) / span : i;
|
|
1159
|
+
}
|
|
1160
|
+
function scrollYFor(ys, pos) {
|
|
1161
|
+
const i = Math.min(Math.floor(pos), ys.length - 1);
|
|
1162
|
+
if (i >= ys.length - 1) return ys[ys.length - 1];
|
|
1163
|
+
return ys[i] + (ys[i + 1] - ys[i]) * (pos - i);
|
|
1164
|
+
}
|
|
1165
|
+
function PageGuide({ sections }) {
|
|
1166
|
+
const ids = sections.map((s) => s.id);
|
|
1167
|
+
const idsKey = ids.join("|");
|
|
1168
|
+
const scrollerRef = useRef5(null);
|
|
1169
|
+
const trackRef = useRef5(null);
|
|
1170
|
+
const markerRef = useRef5(null);
|
|
1171
|
+
const itemRefs = useRef5([]);
|
|
1172
|
+
const [active, setActive] = useState4(0);
|
|
1173
|
+
const [dragging, setDragging] = useState4(false);
|
|
1174
|
+
const frame = useRef5(0);
|
|
1175
|
+
const update = useCallback2(() => {
|
|
1176
|
+
const items = itemRefs.current.slice(0, idsKey.split("|").length);
|
|
1177
|
+
const marker = markerRef.current;
|
|
1178
|
+
const scroller = scrollerRef.current;
|
|
1179
|
+
if (!marker || !scroller || items.length === 0 || items.some((el) => !el)) return;
|
|
1180
|
+
const pos = posAt(anchors(idsKey.split("|")), window.scrollY);
|
|
1181
|
+
const i = Math.min(Math.floor(pos), items.length - 1);
|
|
1182
|
+
const next = items[Math.min(i + 1, items.length - 1)];
|
|
1183
|
+
const top = items[i].offsetTop + (next.offsetTop - items[i].offsetTop) * (pos - i);
|
|
1184
|
+
const height = items[i].offsetHeight;
|
|
1185
|
+
marker.style.height = `${height}px`;
|
|
1186
|
+
marker.style.transform = `translateY(${top}px)`;
|
|
1187
|
+
if (top < scroller.scrollTop + 8) scroller.scrollTop = Math.max(0, top - 8);
|
|
1188
|
+
else if (top + height > scroller.scrollTop + scroller.clientHeight - 8) scroller.scrollTop = top + height - scroller.clientHeight + 8;
|
|
1189
|
+
setActive(Math.round(pos));
|
|
1190
|
+
}, [idsKey]);
|
|
1191
|
+
useLayoutEffect2(() => {
|
|
1192
|
+
const schedule = () => {
|
|
1193
|
+
cancelAnimationFrame(frame.current);
|
|
1194
|
+
frame.current = requestAnimationFrame(update);
|
|
1195
|
+
};
|
|
1196
|
+
update();
|
|
1197
|
+
window.addEventListener("scroll", schedule, { passive: true });
|
|
1198
|
+
window.addEventListener("resize", schedule);
|
|
1199
|
+
const ro = new ResizeObserver(schedule);
|
|
1200
|
+
ro.observe(document.body);
|
|
1201
|
+
return () => {
|
|
1202
|
+
cancelAnimationFrame(frame.current);
|
|
1203
|
+
window.removeEventListener("scroll", schedule);
|
|
1204
|
+
window.removeEventListener("resize", schedule);
|
|
1205
|
+
ro.disconnect();
|
|
1206
|
+
};
|
|
1207
|
+
}, [update]);
|
|
1208
|
+
const goTo = (index, focusTarget = false) => {
|
|
1209
|
+
const ys = anchors(ids);
|
|
1210
|
+
const i = Math.min(Math.max(index, 0), ids.length - 1);
|
|
1211
|
+
window.scrollTo({ top: ys[i], behavior: reducedMotion() ? "auto" : "smooth" });
|
|
1212
|
+
if (focusTarget) document.getElementById(ids[i])?.focus({ preventScroll: true });
|
|
1213
|
+
};
|
|
1214
|
+
const scrubTo = (clientY, smooth) => {
|
|
1215
|
+
const items = itemRefs.current.slice(0, ids.length);
|
|
1216
|
+
const track = trackRef.current;
|
|
1217
|
+
if (!track || items.some((el) => !el)) return;
|
|
1218
|
+
const y = clientY - track.getBoundingClientRect().top;
|
|
1219
|
+
const centres = items.map((el) => el.offsetTop + el.offsetHeight / 2);
|
|
1220
|
+
let pos = 0;
|
|
1221
|
+
if (y >= centres[centres.length - 1]) pos = centres.length - 1;
|
|
1222
|
+
else if (y > centres[0]) {
|
|
1223
|
+
let i = 0;
|
|
1224
|
+
while (centres[i + 1] < y) i++;
|
|
1225
|
+
pos = i + (y - centres[i]) / (centres[i + 1] - centres[i]);
|
|
1226
|
+
}
|
|
1227
|
+
window.scrollTo({ top: scrollYFor(anchors(ids), pos), behavior: smooth && !reducedMotion() ? "smooth" : "auto" });
|
|
1228
|
+
};
|
|
1229
|
+
const onPointerDown = (e) => {
|
|
1230
|
+
if (e.button !== 0) return;
|
|
1231
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
1232
|
+
setDragging(true);
|
|
1233
|
+
scrubTo(e.clientY, true);
|
|
1234
|
+
};
|
|
1235
|
+
const onPointerMove = (e) => {
|
|
1236
|
+
if (dragging) scrubTo(e.clientY, false);
|
|
1237
|
+
};
|
|
1238
|
+
const endDrag = (e) => {
|
|
1239
|
+
if (e.currentTarget.hasPointerCapture(e.pointerId)) e.currentTarget.releasePointerCapture(e.pointerId);
|
|
1240
|
+
setDragging(false);
|
|
1241
|
+
};
|
|
1242
|
+
return /* @__PURE__ */ jsxs14("aside", { "aria-label": "On this page", className: "sticky top-24 hidden w-52 shrink-0 self-start xl:block", children: [
|
|
1243
|
+
/* @__PURE__ */ jsx17("p", { className: "mb-2 px-3 text-xs font-semibold uppercase tracking-wide text-gray-500", children: "On this page" }),
|
|
1244
|
+
/* @__PURE__ */ jsx17("div", { ref: scrollerRef, className: "-m-1.5 max-h-[calc(100vh-10rem)] overflow-y-auto overscroll-contain p-1.5", children: /* @__PURE__ */ jsxs14("div", { ref: trackRef, className: "group relative", children: [
|
|
1245
|
+
/* @__PURE__ */ jsx17(
|
|
1246
|
+
"div",
|
|
1247
|
+
{
|
|
1248
|
+
"aria-hidden": "true",
|
|
1249
|
+
onPointerDown,
|
|
1250
|
+
onPointerMove,
|
|
1251
|
+
onPointerUp: endDrag,
|
|
1252
|
+
onPointerCancel: endDrag,
|
|
1253
|
+
className: clsx13("absolute -left-2 bottom-0 top-0 z-10 w-5 touch-none select-none", dragging ? "cursor-grabbing" : "cursor-grab")
|
|
1254
|
+
}
|
|
1255
|
+
),
|
|
1256
|
+
/* @__PURE__ */ jsx17("span", { "aria-hidden": "true", className: "absolute bottom-0 left-0 top-0 w-px bg-gray-200" }),
|
|
1257
|
+
/* @__PURE__ */ jsx17(
|
|
1258
|
+
"span",
|
|
1259
|
+
{
|
|
1260
|
+
ref: markerRef,
|
|
1261
|
+
"aria-hidden": "true",
|
|
1262
|
+
className: clsx13(
|
|
1263
|
+
"pointer-events-none absolute left-0 top-0 rounded-full bg-brand-600 transition-[width,margin] duration-150",
|
|
1264
|
+
dragging ? "-ml-px w-1" : "w-0.5 group-hover:-ml-px group-hover:w-1"
|
|
1265
|
+
)
|
|
1266
|
+
}
|
|
1267
|
+
),
|
|
1268
|
+
/* @__PURE__ */ jsx17("ul", { className: "space-y-0.5 pl-0", children: sections.map((s, i) => /* @__PURE__ */ jsx17("li", { children: /* @__PURE__ */ jsx17(
|
|
1269
|
+
"a",
|
|
1270
|
+
{
|
|
1271
|
+
ref: (node) => {
|
|
1272
|
+
itemRefs.current[i] = node;
|
|
1273
|
+
},
|
|
1274
|
+
href: `#${s.id}`,
|
|
1275
|
+
"aria-current": i === active ? "location" : void 0,
|
|
1276
|
+
onClick: (e) => {
|
|
1277
|
+
e.preventDefault();
|
|
1278
|
+
goTo(i, e.detail === 0);
|
|
1279
|
+
},
|
|
1280
|
+
className: clsx13(
|
|
1281
|
+
"block py-1.5 pl-3 pr-2 text-sm transition-colors",
|
|
1282
|
+
i === active ? "font-medium text-brand-700" : "text-gray-500 hover:text-gray-900"
|
|
1283
|
+
),
|
|
1284
|
+
children: s.label
|
|
1285
|
+
}
|
|
1286
|
+
) }, s.id)) })
|
|
1287
|
+
] }) })
|
|
1288
|
+
] });
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
// src/layout/Section.tsx
|
|
1292
|
+
import { jsx as jsx18, jsxs as jsxs15 } from "react/jsx-runtime";
|
|
1293
|
+
function Section({ id, title, description, children }) {
|
|
1294
|
+
return /* @__PURE__ */ jsxs15("section", { id, tabIndex: -1, className: "card scroll-mt-24 p-6 focus:outline-none", children: [
|
|
1295
|
+
/* @__PURE__ */ jsx18("h2", { className: "text-base font-semibold text-gray-900", children: title }),
|
|
1296
|
+
description && /* @__PURE__ */ jsx18("p", { className: "mt-0.5 text-sm text-gray-500", children: description }),
|
|
1297
|
+
/* @__PURE__ */ jsx18("div", { className: "mt-5", children })
|
|
1298
|
+
] });
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
// src/layout/Sidebar.tsx
|
|
1302
|
+
import { useEffect as useEffect5, useRef as useRef6 } from "react";
|
|
1303
|
+
import clsx14 from "clsx";
|
|
1304
|
+
import { XMarkIcon as XMarkIcon3 } from "@heroicons/react/24/outline";
|
|
1305
|
+
import { Fragment as Fragment2, jsx as jsx19, jsxs as jsxs16 } from "react/jsx-runtime";
|
|
1306
|
+
function Sidebar({ items, active, onSelect, open, onClose }) {
|
|
1307
|
+
const asideRef = useRef6(null);
|
|
1308
|
+
const trapTab = useDialogFocus(open, asideRef, {
|
|
1309
|
+
initialFocus: () => asideRef.current?.querySelector('[aria-current="page"]')
|
|
1310
|
+
});
|
|
1311
|
+
useEffect5(() => {
|
|
1312
|
+
if (!open) return;
|
|
1313
|
+
const mq = window.matchMedia("(min-width: 1024px)");
|
|
1314
|
+
const onChange = () => mq.matches && onClose();
|
|
1315
|
+
mq.addEventListener("change", onChange);
|
|
1316
|
+
return () => mq.removeEventListener("change", onChange);
|
|
1317
|
+
}, [open, onClose]);
|
|
1318
|
+
useEffect5(() => {
|
|
1319
|
+
if (!open) return;
|
|
1320
|
+
const onKey = (e) => e.key === "Escape" && onClose();
|
|
1321
|
+
document.addEventListener("keydown", onKey);
|
|
1322
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
1323
|
+
}, [open, onClose]);
|
|
1324
|
+
return /* @__PURE__ */ jsxs16(Fragment2, { children: [
|
|
1325
|
+
open && /* @__PURE__ */ jsx19("div", { className: "fixed inset-0 z-40 bg-black/40 backdrop-blur-sm lg:hidden", onClick: onClose, "aria-hidden": "true" }),
|
|
1326
|
+
/* @__PURE__ */ jsxs16(
|
|
1327
|
+
"aside",
|
|
1328
|
+
{
|
|
1329
|
+
ref: asideRef,
|
|
1330
|
+
"aria-label": "Main navigation",
|
|
1331
|
+
onKeyDown: open ? trapTab : void 0,
|
|
1332
|
+
className: clsx14(
|
|
1333
|
+
"fixed inset-y-0 left-0 z-50 flex w-64 flex-col border-r border-gray-200 bg-white duration-200",
|
|
1334
|
+
"lg:sticky lg:top-0 lg:z-auto lg:h-screen lg:w-60 lg:shrink-0 lg:translate-x-0",
|
|
1335
|
+
// Closed on small screens: invisible = out of the tab order. It switches off only after
|
|
1336
|
+
// the slide-out finishes, but switches on immediately so focus can move in on open.
|
|
1337
|
+
open ? "visible translate-x-0 shadow-2xl transition-transform" : "invisible -translate-x-full transition-[transform,visibility] lg:visible"
|
|
1338
|
+
),
|
|
1339
|
+
children: [
|
|
1340
|
+
/* @__PURE__ */ jsxs16("div", { className: "flex items-center justify-between px-5 py-5", children: [
|
|
1341
|
+
/* @__PURE__ */ jsxs16("div", { className: "flex items-center gap-2", children: [
|
|
1342
|
+
/* @__PURE__ */ jsx19("span", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-brand-600 text-sm font-bold text-white", children: "X" }),
|
|
1343
|
+
/* @__PURE__ */ jsx19("span", { className: "text-lg font-bold tracking-tight text-gray-900", children: "XeX" })
|
|
1344
|
+
] }),
|
|
1345
|
+
/* @__PURE__ */ jsx19("button", { type: "button", onClick: onClose, "aria-label": "Close navigation", className: "rounded-lg p-1 text-gray-400 hover:bg-gray-100 lg:hidden", children: /* @__PURE__ */ jsx19(XMarkIcon3, { className: "h-5 w-5" }) })
|
|
1346
|
+
] }),
|
|
1347
|
+
/* @__PURE__ */ jsx19("nav", { className: "flex-1 space-y-1 overflow-y-auto px-3 pb-4 pt-2", children: items.map(({ id, label, icon: Icon }) => /* @__PURE__ */ jsxs16(
|
|
1348
|
+
"a",
|
|
1349
|
+
{
|
|
1350
|
+
href: `#/${id}`,
|
|
1351
|
+
"aria-current": id === active ? "page" : void 0,
|
|
1352
|
+
onClick: (e) => {
|
|
1353
|
+
e.preventDefault();
|
|
1354
|
+
onSelect(id);
|
|
1355
|
+
},
|
|
1356
|
+
className: id === active ? "nav-item-active" : "nav-item-inactive",
|
|
1357
|
+
children: [
|
|
1358
|
+
/* @__PURE__ */ jsx19(Icon, { className: "h-5 w-5" }),
|
|
1359
|
+
label
|
|
1360
|
+
]
|
|
1361
|
+
},
|
|
1362
|
+
id
|
|
1363
|
+
)) })
|
|
1364
|
+
]
|
|
1365
|
+
}
|
|
1366
|
+
)
|
|
1367
|
+
] });
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
// src/theme/ThemeSwitcher.tsx
|
|
1371
|
+
import clsx15 from "clsx";
|
|
1372
|
+
import { CheckIcon, MoonIcon, SunIcon } from "@heroicons/react/24/solid";
|
|
1373
|
+
import { jsx as jsx20, jsxs as jsxs17 } from "react/jsx-runtime";
|
|
1374
|
+
function ThemeSwitcher({ showLabels = false }) {
|
|
1375
|
+
const { theme, themes, setThemeId, mode, toggleMode } = useTheme();
|
|
1376
|
+
return /* @__PURE__ */ jsxs17("div", { className: "flex flex-wrap items-center gap-3", children: [
|
|
1377
|
+
/* @__PURE__ */ jsx20("div", { className: "flex flex-wrap items-center gap-2", role: "radiogroup", "aria-label": "Color theme", onKeyDown: radioGroupKeyDown, children: themes.map((t) => {
|
|
1378
|
+
const selected = t.id === theme.id;
|
|
1379
|
+
return /* @__PURE__ */ jsxs17(
|
|
1380
|
+
"button",
|
|
1381
|
+
{
|
|
1382
|
+
type: "button",
|
|
1383
|
+
role: "radio",
|
|
1384
|
+
"aria-checked": selected,
|
|
1385
|
+
tabIndex: selected ? 0 : -1,
|
|
1386
|
+
title: t.name,
|
|
1387
|
+
onClick: () => setThemeId(t.id),
|
|
1388
|
+
className: clsx15(
|
|
1389
|
+
"flex items-center gap-2 rounded-full border p-1 transition focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-400",
|
|
1390
|
+
showLabels && "pr-3",
|
|
1391
|
+
selected ? "border-gray-400" : "border-transparent hover:border-gray-300"
|
|
1392
|
+
),
|
|
1393
|
+
children: [
|
|
1394
|
+
/* @__PURE__ */ jsx20(
|
|
1395
|
+
"span",
|
|
1396
|
+
{
|
|
1397
|
+
className: "flex h-6 w-6 items-center justify-center rounded-full text-white",
|
|
1398
|
+
style: { background: `linear-gradient(135deg, ${t.shades[400]}, ${t.shades[700]})` },
|
|
1399
|
+
children: selected && /* @__PURE__ */ jsx20(CheckIcon, { className: "h-4 w-4" })
|
|
1400
|
+
}
|
|
1401
|
+
),
|
|
1402
|
+
showLabels && /* @__PURE__ */ jsx20("span", { className: "text-sm font-medium text-gray-700", children: t.name })
|
|
1403
|
+
]
|
|
1404
|
+
},
|
|
1405
|
+
t.id
|
|
1406
|
+
);
|
|
1407
|
+
}) }),
|
|
1408
|
+
/* @__PURE__ */ jsxs17(
|
|
1409
|
+
"button",
|
|
1410
|
+
{
|
|
1411
|
+
type: "button",
|
|
1412
|
+
onClick: toggleMode,
|
|
1413
|
+
"aria-label": `Switch to ${mode === "dark" ? "light" : "dark"} mode`,
|
|
1414
|
+
className: "flex items-center gap-1.5 rounded-lg border border-gray-300 bg-white px-2.5 py-1.5 text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-400",
|
|
1415
|
+
children: [
|
|
1416
|
+
mode === "dark" ? /* @__PURE__ */ jsx20(SunIcon, { className: "h-4 w-4" }) : /* @__PURE__ */ jsx20(MoonIcon, { className: "h-4 w-4" }),
|
|
1417
|
+
mode === "dark" ? "Light" : "Dark"
|
|
1418
|
+
]
|
|
1419
|
+
}
|
|
1420
|
+
)
|
|
1421
|
+
] });
|
|
1422
|
+
}
|
|
1423
|
+
export {
|
|
1424
|
+
AVATAR_COLORS,
|
|
1425
|
+
Avatar,
|
|
1426
|
+
Badge,
|
|
1427
|
+
Button,
|
|
1428
|
+
ConfirmDialog,
|
|
1429
|
+
DataTable,
|
|
1430
|
+
FetchStatus,
|
|
1431
|
+
Header,
|
|
1432
|
+
Input,
|
|
1433
|
+
LoadingDots,
|
|
1434
|
+
LoadingOverlay,
|
|
1435
|
+
Modal,
|
|
1436
|
+
MultiSelect,
|
|
1437
|
+
PageGuide,
|
|
1438
|
+
PaginationBar,
|
|
1439
|
+
ProgressBar,
|
|
1440
|
+
ProgressRing,
|
|
1441
|
+
Section,
|
|
1442
|
+
SegmentedControl,
|
|
1443
|
+
Select,
|
|
1444
|
+
Sidebar,
|
|
1445
|
+
Skeleton,
|
|
1446
|
+
SkeletonText,
|
|
1447
|
+
Spinner,
|
|
1448
|
+
Tabs,
|
|
1449
|
+
ThemeProvider,
|
|
1450
|
+
ThemeSwitcher,
|
|
1451
|
+
colorFor,
|
|
1452
|
+
focusablesIn,
|
|
1453
|
+
initialsOf,
|
|
1454
|
+
pageWindow,
|
|
1455
|
+
radioGroupKeyDown,
|
|
1456
|
+
useDialogFocus,
|
|
1457
|
+
useTheme
|
|
1458
|
+
};
|