reusify-ui 1.0.7 → 2.0.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.
@@ -0,0 +1,1506 @@
1
+ "use client";
2
+ import { jsxs, jsx, Fragment as Fragment$1 } from "react/jsx-runtime";
3
+ import { forwardRef, useState, useEffect, Children, isValidElement, cloneElement, useRef, useCallback, useImperativeHandle, useId, useMemo, Fragment, useContext, createContext } from "react";
4
+ import { createPortal } from "react-dom";
5
+ function cn(...classes) {
6
+ return classes.filter(Boolean).join(" ");
7
+ }
8
+ const Button = forwardRef(function Button2({
9
+ variant = "primary",
10
+ size = "md",
11
+ loading = false,
12
+ fullWidth = false,
13
+ leftIcon,
14
+ rightIcon,
15
+ disabled,
16
+ type = "button",
17
+ className,
18
+ children,
19
+ ...rest
20
+ }, ref) {
21
+ return /* @__PURE__ */ jsxs(
22
+ "button",
23
+ {
24
+ ref,
25
+ type,
26
+ disabled: disabled || loading,
27
+ "aria-busy": loading || void 0,
28
+ className: cn("ru-btn", `ru-btn--${variant}`, `ru-btn--${size}`, fullWidth && "ru-btn--full", className),
29
+ ...rest,
30
+ children: [
31
+ loading ? /* @__PURE__ */ jsx("span", { className: "ru-spinner", "aria-hidden": "true" }) : leftIcon,
32
+ children,
33
+ !loading && rightIcon
34
+ ]
35
+ }
36
+ );
37
+ });
38
+ const base = {
39
+ xmlns: "http://www.w3.org/2000/svg",
40
+ viewBox: "0 0 24 24",
41
+ fill: "none",
42
+ stroke: "currentColor",
43
+ strokeWidth: 2,
44
+ strokeLinecap: "round",
45
+ strokeLinejoin: "round",
46
+ "aria-hidden": true,
47
+ focusable: "false"
48
+ };
49
+ const CloseIcon = (p) => /* @__PURE__ */ jsx("svg", { ...base, ...p, children: /* @__PURE__ */ jsx("path", { d: "M18 6 6 18M6 6l12 12" }) });
50
+ const InfoIcon = (p) => /* @__PURE__ */ jsxs("svg", { ...base, ...p, children: [
51
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" }),
52
+ /* @__PURE__ */ jsx("path", { d: "M12 16v-4M12 8h.01" })
53
+ ] });
54
+ const SuccessIcon = (p) => /* @__PURE__ */ jsxs("svg", { ...base, ...p, children: [
55
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" }),
56
+ /* @__PURE__ */ jsx("path", { d: "m9 12 2 2 4-4" })
57
+ ] });
58
+ const WarningIcon = (p) => /* @__PURE__ */ jsxs("svg", { ...base, ...p, children: [
59
+ /* @__PURE__ */ jsx("path", { d: "m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z" }),
60
+ /* @__PURE__ */ jsx("path", { d: "M12 9v4M12 17h.01" })
61
+ ] });
62
+ const DangerIcon = (p) => /* @__PURE__ */ jsxs("svg", { ...base, ...p, children: [
63
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" }),
64
+ /* @__PURE__ */ jsx("path", { d: "m15 9-6 6M9 9l6 6" })
65
+ ] });
66
+ const ChevronDownIcon = (p) => /* @__PURE__ */ jsx("svg", { ...base, ...p, children: /* @__PURE__ */ jsx("path", { d: "m6 9 6 6 6-6" }) });
67
+ const ChevronLeftIcon = (p) => /* @__PURE__ */ jsx("svg", { ...base, ...p, children: /* @__PURE__ */ jsx("path", { d: "m15 18-6-6 6-6" }) });
68
+ const ChevronRightIcon = (p) => /* @__PURE__ */ jsx("svg", { ...base, ...p, children: /* @__PURE__ */ jsx("path", { d: "m9 18 6-6-6-6" }) });
69
+ const CheckIcon = (p) => /* @__PURE__ */ jsx("svg", { ...base, ...p, children: /* @__PURE__ */ jsx("path", { d: "M20 6 9 17l-5-5" }) });
70
+ const UserIcon = (p) => /* @__PURE__ */ jsxs("svg", { ...base, ...p, children: [
71
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "8", r: "4" }),
72
+ /* @__PURE__ */ jsx("path", { d: "M4 21a8 8 0 0 1 16 0" })
73
+ ] });
74
+ const statusIcons = {
75
+ info: InfoIcon,
76
+ success: SuccessIcon,
77
+ warning: WarningIcon,
78
+ danger: DangerIcon
79
+ };
80
+ function CloseButton({ onClick, label = "Close" }) {
81
+ return /* @__PURE__ */ jsx("button", { type: "button", className: "ru-close", "aria-label": label, onClick, children: /* @__PURE__ */ jsx(CloseIcon, {}) });
82
+ }
83
+ function Spinner({ size = "md", color = "primary", label = "Loading", className, ...rest }) {
84
+ return /* @__PURE__ */ jsxs("span", { role: "status", className: cn("ru-spinner-wrap", className), ...rest, children: [
85
+ /* @__PURE__ */ jsx("span", { className: cn("ru-spinner", `ru-spinner--${size}`, `ru-c-${color}`), "aria-hidden": "true" }),
86
+ label && /* @__PURE__ */ jsx("span", { className: "ru-sr-only", children: label })
87
+ ] });
88
+ }
89
+ function Progress({
90
+ value,
91
+ max = 100,
92
+ size = "md",
93
+ color = "primary",
94
+ label,
95
+ showValue = false,
96
+ className,
97
+ ...rest
98
+ }) {
99
+ const indeterminate = value === void 0;
100
+ const percent = indeterminate ? 0 : Math.min(100, Math.max(0, value / max * 100));
101
+ return /* @__PURE__ */ jsxs("div", { className: cn("ru-progress", indeterminate && "ru-progress--indeterminate", className), ...rest, children: [
102
+ (label || showValue) && /* @__PURE__ */ jsxs("div", { className: "ru-progress__label", children: [
103
+ /* @__PURE__ */ jsx("span", { children: label }),
104
+ showValue && !indeterminate && /* @__PURE__ */ jsxs("span", { className: "ru-progress__value", children: [
105
+ Math.round(percent),
106
+ "%"
107
+ ] })
108
+ ] }),
109
+ /* @__PURE__ */ jsx(
110
+ "div",
111
+ {
112
+ role: "progressbar",
113
+ "aria-valuemin": 0,
114
+ "aria-valuemax": max,
115
+ "aria-valuenow": indeterminate ? void 0 : value,
116
+ "aria-label": typeof label === "string" ? label : void 0,
117
+ className: cn("ru-progress__track", `ru-progress__track--${size}`, `ru-c-${color}`),
118
+ children: /* @__PURE__ */ jsx("div", { className: "ru-progress__bar", style: indeterminate ? void 0 : { width: `${percent}%` } })
119
+ }
120
+ )
121
+ ] });
122
+ }
123
+ function Skeleton({ variant = "text", width, height, lines = 1, className, style, ...rest }) {
124
+ const block = (key, lineWidth) => /* @__PURE__ */ jsx(
125
+ "span",
126
+ {
127
+ "aria-hidden": "true",
128
+ className: cn("ru-skeleton", `ru-skeleton--${variant}`, className),
129
+ style: { width: lineWidth ?? width, height: variant === "circle" ? height ?? width : height, ...style },
130
+ ...rest
131
+ },
132
+ key
133
+ );
134
+ if (variant !== "text" || lines <= 1) return block();
135
+ return /* @__PURE__ */ jsx("span", { className: "ru-skeleton-group", children: Array.from({ length: lines }, (_, i) => block(i, i === lines - 1 ? "60%" : width)) });
136
+ }
137
+ function Alert({ variant = "info", title, icon, onClose, className, children, ...rest }) {
138
+ const Icon = statusIcons[variant];
139
+ return /* @__PURE__ */ jsxs(
140
+ "div",
141
+ {
142
+ role: variant === "danger" || variant === "warning" ? "alert" : "status",
143
+ className: cn("ru-alert", `ru-c-${variant}`, className),
144
+ ...rest,
145
+ children: [
146
+ icon !== false && /* @__PURE__ */ jsx("span", { className: "ru-alert__icon", children: icon ?? /* @__PURE__ */ jsx(Icon, {}) }),
147
+ /* @__PURE__ */ jsxs("div", { className: "ru-alert__content", children: [
148
+ title && /* @__PURE__ */ jsx("p", { className: "ru-alert__title", children: title }),
149
+ children && /* @__PURE__ */ jsx("div", { className: "ru-alert__desc", children })
150
+ ] }),
151
+ onClose && /* @__PURE__ */ jsx(CloseButton, { onClick: onClose, label: "Dismiss" })
152
+ ]
153
+ }
154
+ );
155
+ }
156
+ function Badge({ variant = "soft", color = "primary", size = "md", dot = false, className, children, ...rest }) {
157
+ return /* @__PURE__ */ jsxs("span", { className: cn("ru-badge", `ru-badge--${variant}`, `ru-badge--${size}`, `ru-c-${color}`, className), ...rest, children: [
158
+ dot && /* @__PURE__ */ jsx("span", { className: "ru-badge__dot", "aria-hidden": "true" }),
159
+ children
160
+ ] });
161
+ }
162
+ function getInitials(name) {
163
+ const parts = name.trim().split(/\s+/).filter(Boolean);
164
+ if (parts.length === 0) return "";
165
+ const first = parts[0][0];
166
+ const last = parts.length > 1 ? parts[parts.length - 1][0] : "";
167
+ return (first + last).toUpperCase();
168
+ }
169
+ function Avatar({ src, alt, name, size = "md", shape = "circle", className, ...rest }) {
170
+ const [failed, setFailed] = useState(false);
171
+ useEffect(() => setFailed(false), [src]);
172
+ const showImage = src && !failed;
173
+ const initials = name ? getInitials(name) : "";
174
+ return /* @__PURE__ */ jsx(
175
+ "span",
176
+ {
177
+ className: cn("ru-avatar", `ru-avatar--${size}`, shape === "square" && "ru-avatar--square", className),
178
+ role: showImage ? void 0 : "img",
179
+ "aria-label": showImage ? void 0 : alt ?? name ?? "Avatar",
180
+ ...rest,
181
+ children: showImage ? /* @__PURE__ */ jsx("img", { src, alt: alt ?? name ?? "", onError: () => setFailed(true) }) : initials ? /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: initials }) : /* @__PURE__ */ jsx(UserIcon, { className: "ru-avatar__icon" })
182
+ }
183
+ );
184
+ }
185
+ function AvatarGroup({ max, size = "md", className, children, ...rest }) {
186
+ const avatars = Children.toArray(children).filter(isValidElement);
187
+ const visible = max ? avatars.slice(0, max) : avatars;
188
+ const extra = avatars.length - visible.length;
189
+ return /* @__PURE__ */ jsxs("div", { className: cn("ru-avatar-group", className), ...rest, children: [
190
+ visible.map((child) => cloneElement(child, { size: child.props.size ?? size })),
191
+ extra > 0 && /* @__PURE__ */ jsxs("span", { className: cn("ru-avatar", `ru-avatar--${size}`), "aria-label": `${extra} more`, children: [
192
+ "+",
193
+ extra
194
+ ] })
195
+ ] });
196
+ }
197
+ function Card({ variant = "outline", hoverable = false, className, ...rest }) {
198
+ return /* @__PURE__ */ jsx(
199
+ "div",
200
+ {
201
+ className: cn(
202
+ "ru-card",
203
+ variant === "elevated" && "ru-card--elevated",
204
+ hoverable && "ru-card--hoverable",
205
+ className
206
+ ),
207
+ ...rest
208
+ }
209
+ );
210
+ }
211
+ function CardHeader({ title, subtitle, action, className, children, ...rest }) {
212
+ return /* @__PURE__ */ jsxs("div", { className: cn("ru-card__header", className), ...rest, children: [
213
+ /* @__PURE__ */ jsxs("div", { children: [
214
+ title && /* @__PURE__ */ jsx("h3", { className: "ru-card__title", children: title }),
215
+ subtitle && /* @__PURE__ */ jsx("p", { className: "ru-card__subtitle", children: subtitle }),
216
+ children
217
+ ] }),
218
+ action
219
+ ] });
220
+ }
221
+ function CardBody({ className, ...rest }) {
222
+ return /* @__PURE__ */ jsx("div", { className: cn("ru-card__body", className), ...rest });
223
+ }
224
+ function CardFooter({ className, ...rest }) {
225
+ return /* @__PURE__ */ jsx("div", { className: cn("ru-card__footer", className), ...rest });
226
+ }
227
+ function Divider({ orientation = "horizontal", label, className, ...rest }) {
228
+ if (label && orientation === "horizontal") {
229
+ return /* @__PURE__ */ jsx("div", { role: "separator", className: cn("ru-divider", "ru-divider--label", className), ...rest, children: label });
230
+ }
231
+ return /* @__PURE__ */ jsx(
232
+ "hr",
233
+ {
234
+ "aria-orientation": orientation,
235
+ className: cn("ru-divider", orientation === "vertical" && "ru-divider--vertical", className),
236
+ ...rest
237
+ }
238
+ );
239
+ }
240
+ function Table({
241
+ columns,
242
+ data,
243
+ rowKey,
244
+ striped = false,
245
+ hoverable = false,
246
+ size = "md",
247
+ caption,
248
+ emptyText = "No data",
249
+ onRowClick,
250
+ className
251
+ }) {
252
+ const getKey = (row, index) => {
253
+ if (typeof rowKey === "function") return rowKey(row, index);
254
+ if (rowKey !== void 0) return String(row[rowKey]);
255
+ return index;
256
+ };
257
+ return /* @__PURE__ */ jsx("div", { className: cn("ru-table-wrap", className), children: /* @__PURE__ */ jsxs(
258
+ "table",
259
+ {
260
+ className: cn(
261
+ "ru-table",
262
+ `ru-table--${size}`,
263
+ striped && "ru-table--striped",
264
+ (hoverable || onRowClick) && "ru-table--hover"
265
+ ),
266
+ children: [
267
+ caption && /* @__PURE__ */ jsx("caption", { children: caption }),
268
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", { children: columns.map((col) => /* @__PURE__ */ jsx("th", { scope: "col", style: { textAlign: col.align, width: col.width }, children: col.header }, col.key)) }) }),
269
+ /* @__PURE__ */ jsx("tbody", { children: data.length === 0 ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx("td", { className: "ru-table__empty", colSpan: columns.length, children: emptyText }) }) : data.map((row, i) => /* @__PURE__ */ jsx(
270
+ "tr",
271
+ {
272
+ onClick: onRowClick ? () => onRowClick(row, i) : void 0,
273
+ style: onRowClick ? { cursor: "pointer" } : void 0,
274
+ children: columns.map((col) => /* @__PURE__ */ jsx("td", { style: { textAlign: col.align }, children: col.render ? col.render(row, i) : row[col.key] }, col.key))
275
+ },
276
+ getKey(row, i)
277
+ )) })
278
+ ]
279
+ }
280
+ ) });
281
+ }
282
+ function useControllableState(value, defaultValue, onChange) {
283
+ const [internal, setInternal] = useState(defaultValue);
284
+ const isControlled = value !== void 0;
285
+ const onChangeRef = useRef(onChange);
286
+ onChangeRef.current = onChange;
287
+ const setValue = useCallback(
288
+ (next) => {
289
+ var _a;
290
+ if (!isControlled) setInternal(next);
291
+ (_a = onChangeRef.current) == null ? void 0 : _a.call(onChangeRef, next);
292
+ },
293
+ [isControlled]
294
+ );
295
+ return [isControlled ? value : internal, setValue];
296
+ }
297
+ function useDisclosure(initialOpen = false) {
298
+ const [isOpen, setIsOpen] = useState(initialOpen);
299
+ const open = useCallback(() => setIsOpen(true), []);
300
+ const close = useCallback(() => setIsOpen(false), []);
301
+ const toggle = useCallback(() => setIsOpen((v) => !v), []);
302
+ return { isOpen, open, close, toggle, setIsOpen };
303
+ }
304
+ function useClickOutside(ref, handler, enabled = true) {
305
+ const handlerRef = useRef(handler);
306
+ handlerRef.current = handler;
307
+ useEffect(() => {
308
+ if (!enabled) return;
309
+ const listener = (event) => {
310
+ const el = ref.current;
311
+ if (!el || el.contains(event.target)) return;
312
+ handlerRef.current();
313
+ };
314
+ document.addEventListener("pointerdown", listener);
315
+ return () => document.removeEventListener("pointerdown", listener);
316
+ }, [ref, enabled]);
317
+ }
318
+ function useMounted() {
319
+ const [mounted, setMounted] = useState(false);
320
+ useEffect(() => setMounted(true), []);
321
+ return mounted;
322
+ }
323
+ const FOCUSABLE = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
324
+ function useOverlay(open, panelRef, onClose, closeOnEsc) {
325
+ const onCloseRef = useRef(onClose);
326
+ onCloseRef.current = onClose;
327
+ const closeOnEscRef = useRef(closeOnEsc);
328
+ closeOnEscRef.current = closeOnEsc;
329
+ useEffect(() => {
330
+ var _a;
331
+ if (!open) return;
332
+ const panel = panelRef.current;
333
+ const previouslyFocused = document.activeElement;
334
+ const previousOverflow = document.body.style.overflow;
335
+ document.body.style.overflow = "hidden";
336
+ (_a = (panel == null ? void 0 : panel.querySelector("[data-autofocus]")) ?? panel) == null ? void 0 : _a.focus();
337
+ const onKeyDown = (event) => {
338
+ var _a2;
339
+ if (event.key === "Escape" && closeOnEscRef.current) {
340
+ (_a2 = onCloseRef.current) == null ? void 0 : _a2.call(onCloseRef);
341
+ return;
342
+ }
343
+ if (event.key !== "Tab" || !panel) return;
344
+ const items = Array.from(panel.querySelectorAll(FOCUSABLE));
345
+ if (items.length === 0) {
346
+ event.preventDefault();
347
+ return;
348
+ }
349
+ const first = items[0];
350
+ const last = items[items.length - 1];
351
+ const active = document.activeElement;
352
+ if (event.shiftKey && (active === first || active === panel)) {
353
+ event.preventDefault();
354
+ last.focus();
355
+ } else if (!event.shiftKey && active === last) {
356
+ event.preventDefault();
357
+ first.focus();
358
+ }
359
+ };
360
+ document.addEventListener("keydown", onKeyDown);
361
+ return () => {
362
+ var _a2;
363
+ document.removeEventListener("keydown", onKeyDown);
364
+ document.body.style.overflow = previousOverflow;
365
+ (_a2 = previouslyFocused == null ? void 0 : previouslyFocused.focus) == null ? void 0 : _a2.call(previouslyFocused);
366
+ };
367
+ }, [open, panelRef]);
368
+ }
369
+ function Field({
370
+ id,
371
+ helpId,
372
+ label,
373
+ helperText,
374
+ error,
375
+ size = "md",
376
+ fullWidth,
377
+ required,
378
+ disabled,
379
+ className,
380
+ wrapClassName,
381
+ children
382
+ }) {
383
+ const message = typeof error === "string" ? error : helperText;
384
+ return /* @__PURE__ */ jsxs("div", { className: cn("ru-field", fullWidth && "ru-field--full", className), children: [
385
+ label && /* @__PURE__ */ jsxs("label", { className: "ru-label", htmlFor: id, children: [
386
+ label,
387
+ required && /* @__PURE__ */ jsx("span", { className: "ru-label__req", "aria-hidden": "true", children: "*" })
388
+ ] }),
389
+ /* @__PURE__ */ jsx(
390
+ "div",
391
+ {
392
+ className: cn(
393
+ "ru-input-wrap",
394
+ `ru-input-wrap--${size}`,
395
+ error && "ru-input-wrap--error",
396
+ disabled && "ru-input-wrap--disabled",
397
+ wrapClassName
398
+ ),
399
+ children
400
+ }
401
+ ),
402
+ message && /* @__PURE__ */ jsx("p", { id: helpId, className: cn("ru-help", error && "ru-help--error"), children: message })
403
+ ] });
404
+ }
405
+ function useFieldIds(id, error, helperText) {
406
+ const autoId = useId();
407
+ const fieldId = id ?? autoId;
408
+ const helpId = `${fieldId}-help`;
409
+ const hasMessage = typeof error === "string" || Boolean(helperText);
410
+ return { fieldId, helpId, describedBy: hasMessage ? helpId : void 0 };
411
+ }
412
+ const Input = forwardRef(function Input2({ label, helperText, error, size, fullWidth, leftIcon, rightIcon, containerClassName, className, id, ...rest }, ref) {
413
+ const { fieldId, helpId, describedBy } = useFieldIds(id, error, helperText);
414
+ return /* @__PURE__ */ jsxs(
415
+ Field,
416
+ {
417
+ id: fieldId,
418
+ helpId,
419
+ label,
420
+ helperText,
421
+ error,
422
+ size,
423
+ fullWidth,
424
+ required: rest.required,
425
+ disabled: rest.disabled,
426
+ className: containerClassName,
427
+ children: [
428
+ leftIcon && /* @__PURE__ */ jsx("span", { className: "ru-input-icon", children: leftIcon }),
429
+ /* @__PURE__ */ jsx(
430
+ "input",
431
+ {
432
+ ref,
433
+ id: fieldId,
434
+ "aria-invalid": error ? true : void 0,
435
+ "aria-describedby": describedBy,
436
+ className: cn("ru-input", className),
437
+ ...rest
438
+ }
439
+ ),
440
+ rightIcon && /* @__PURE__ */ jsx("span", { className: "ru-input-icon", children: rightIcon })
441
+ ]
442
+ }
443
+ );
444
+ });
445
+ const Textarea = forwardRef(function Textarea2({ label, helperText, error, fullWidth, containerClassName, className, id, rows = 4, ...rest }, ref) {
446
+ const { fieldId, helpId, describedBy } = useFieldIds(id, error, helperText);
447
+ return /* @__PURE__ */ jsx(
448
+ Field,
449
+ {
450
+ id: fieldId,
451
+ helpId,
452
+ label,
453
+ helperText,
454
+ error,
455
+ fullWidth,
456
+ required: rest.required,
457
+ disabled: rest.disabled,
458
+ className: containerClassName,
459
+ wrapClassName: "ru-input-wrap--textarea",
460
+ children: /* @__PURE__ */ jsx(
461
+ "textarea",
462
+ {
463
+ ref,
464
+ id: fieldId,
465
+ rows,
466
+ "aria-invalid": error ? true : void 0,
467
+ "aria-describedby": describedBy,
468
+ className: cn("ru-input", className),
469
+ ...rest
470
+ }
471
+ )
472
+ }
473
+ );
474
+ });
475
+ const NativeSelect = forwardRef(function NativeSelect2({
476
+ label,
477
+ helperText,
478
+ error,
479
+ size,
480
+ fullWidth,
481
+ options,
482
+ placeholder,
483
+ containerClassName,
484
+ className,
485
+ id,
486
+ children,
487
+ value,
488
+ defaultValue,
489
+ ...rest
490
+ }, ref) {
491
+ const { fieldId, helpId, describedBy } = useFieldIds(id, error, helperText);
492
+ const valueProps = value !== void 0 ? { value } : { defaultValue: defaultValue ?? (placeholder ? "" : void 0) };
493
+ return /* @__PURE__ */ jsxs(
494
+ Field,
495
+ {
496
+ id: fieldId,
497
+ helpId,
498
+ label,
499
+ helperText,
500
+ error,
501
+ size,
502
+ fullWidth,
503
+ required: rest.required,
504
+ disabled: rest.disabled,
505
+ className: containerClassName,
506
+ children: [
507
+ /* @__PURE__ */ jsxs(
508
+ "select",
509
+ {
510
+ ref,
511
+ id: fieldId,
512
+ "aria-invalid": error ? true : void 0,
513
+ "aria-describedby": describedBy,
514
+ className: cn("ru-input", "ru-native-select", className),
515
+ ...valueProps,
516
+ ...rest,
517
+ children: [
518
+ placeholder && /* @__PURE__ */ jsx("option", { value: "", disabled: true, children: placeholder }),
519
+ options == null ? void 0 : options.map((opt) => /* @__PURE__ */ jsx("option", { value: opt.value, disabled: opt.disabled, children: opt.label }, opt.value)),
520
+ children
521
+ ]
522
+ }
523
+ ),
524
+ /* @__PURE__ */ jsx(ChevronDownIcon, { className: "ru-native-select__chevron" })
525
+ ]
526
+ }
527
+ );
528
+ });
529
+ function childrenToOptions(children) {
530
+ const result = [];
531
+ Children.forEach(children, (child) => {
532
+ if (!isValidElement(child)) return;
533
+ if (child.type === "option") {
534
+ const { value, children: label, disabled } = child.props;
535
+ result.push({ label, value: value ?? String(label), disabled });
536
+ } else if (child.type === "optgroup") {
537
+ result.push({ label: child.props.label, options: childrenToOptions(child.props.children) });
538
+ } else if (child.type === Fragment) {
539
+ result.push(...childrenToOptions(child.props.children));
540
+ }
541
+ });
542
+ return result;
543
+ }
544
+ function flattenOptions(options) {
545
+ const items = [];
546
+ let index = 0;
547
+ for (const opt of options) {
548
+ if (opt.options) {
549
+ items.push({ type: "group", label: opt.label, key: `group-${items.length}` });
550
+ for (const child of opt.options) items.push({ ...child, type: "option", index: index++ });
551
+ } else {
552
+ items.push({ ...opt, type: "option", index: index++ });
553
+ }
554
+ }
555
+ return items;
556
+ }
557
+ const LIST_MAX_HEIGHT = 288;
558
+ const Select = forwardRef(function Select2({
559
+ label,
560
+ helperText,
561
+ error,
562
+ size = "md",
563
+ fullWidth,
564
+ options = [],
565
+ placeholder = "Select…",
566
+ value,
567
+ defaultValue,
568
+ onChange,
569
+ disabled,
570
+ required,
571
+ name,
572
+ id,
573
+ emptyText = "No options",
574
+ containerClassName,
575
+ className,
576
+ children,
577
+ ...rest
578
+ }, ref) {
579
+ const { fieldId, helpId, describedBy } = useFieldIds(id, error, helperText);
580
+ const [selected, setSelected] = useControllableState(value, defaultValue ?? null, onChange);
581
+ const [open, setOpen] = useState(false);
582
+ const [active, setActive] = useState(-1);
583
+ const [position, setPosition] = useState(null);
584
+ const triggerRef = useRef(null);
585
+ const listRef = useRef(null);
586
+ const typeahead = useRef({ text: "", timer: void 0 });
587
+ const listId = useId();
588
+ const mounted = useMounted();
589
+ useImperativeHandle(ref, () => triggerRef.current);
590
+ const items = useMemo(() => flattenOptions([...options, ...childrenToOptions(children)]), [options, children]);
591
+ const selectable = useMemo(() => items.filter((item) => item.type === "option"), [items]);
592
+ const selectedItem = selectable.find((opt) => opt.value === selected);
593
+ const step = (from, dir) => {
594
+ let i = from;
595
+ for (let n = 0; n < selectable.length; n++) {
596
+ i = (i + dir + selectable.length) % selectable.length;
597
+ if (!selectable[i].disabled) return i;
598
+ }
599
+ return from;
600
+ };
601
+ const firstEnabled = () => step(-1, 1);
602
+ const lastEnabled = () => step(0, -1);
603
+ const openList = () => {
604
+ if (disabled) return;
605
+ const current = selectable.findIndex((opt) => opt.value === selected);
606
+ setActive(current >= 0 ? current : firstEnabled());
607
+ setOpen(true);
608
+ };
609
+ const choose = (opt) => {
610
+ var _a;
611
+ if (!opt || opt.disabled) return;
612
+ setSelected(opt.value);
613
+ setOpen(false);
614
+ (_a = triggerRef.current) == null ? void 0 : _a.focus();
615
+ };
616
+ const runTypeahead = (char) => {
617
+ const state = typeahead.current;
618
+ clearTimeout(state.timer);
619
+ state.text += char.toLowerCase();
620
+ state.timer = setTimeout(() => state.text = "", 500);
621
+ const start = open ? active : selectable.findIndex((opt) => opt.value === selected);
622
+ for (let n = 1; n <= selectable.length; n++) {
623
+ const i = (start + n + selectable.length) % selectable.length;
624
+ const opt = selectable[i];
625
+ if (!opt.disabled && String(opt.label).toLowerCase().startsWith(state.text)) {
626
+ setActive(i);
627
+ if (!open) setOpen(true);
628
+ return;
629
+ }
630
+ }
631
+ };
632
+ const onKeyDown = (e) => {
633
+ if (disabled) return;
634
+ const { key } = e;
635
+ if (!open) {
636
+ if (key === "ArrowDown" || key === "ArrowUp" || key === "Enter" || key === " ") {
637
+ e.preventDefault();
638
+ openList();
639
+ } else if (key.length === 1 && !e.metaKey && !e.ctrlKey && !e.altKey) {
640
+ runTypeahead(key);
641
+ }
642
+ return;
643
+ }
644
+ switch (key) {
645
+ case "ArrowDown":
646
+ e.preventDefault();
647
+ setActive((i) => step(i, 1));
648
+ break;
649
+ case "ArrowUp":
650
+ e.preventDefault();
651
+ setActive((i) => step(i, -1));
652
+ break;
653
+ case "Home":
654
+ e.preventDefault();
655
+ setActive(firstEnabled());
656
+ break;
657
+ case "End":
658
+ e.preventDefault();
659
+ setActive(lastEnabled());
660
+ break;
661
+ case "Enter":
662
+ case " ":
663
+ e.preventDefault();
664
+ if (key === " " && typeahead.current.text) runTypeahead(" ");
665
+ else choose(selectable[active]);
666
+ break;
667
+ case "Escape":
668
+ e.preventDefault();
669
+ e.stopPropagation();
670
+ setOpen(false);
671
+ break;
672
+ case "Tab":
673
+ setOpen(false);
674
+ break;
675
+ default:
676
+ if (key.length === 1 && !e.metaKey && !e.ctrlKey && !e.altKey) runTypeahead(key);
677
+ }
678
+ };
679
+ useEffect(() => {
680
+ if (!open) {
681
+ setPosition(null);
682
+ return;
683
+ }
684
+ const update = () => {
685
+ var _a;
686
+ const anchor = (_a = triggerRef.current) == null ? void 0 : _a.parentElement;
687
+ if (!anchor) return;
688
+ const rect = anchor.getBoundingClientRect();
689
+ const below = window.innerHeight - rect.bottom;
690
+ const above = rect.top;
691
+ const top = below < 240 && above > below;
692
+ setPosition({
693
+ left: rect.left,
694
+ width: rect.width,
695
+ top: top ? void 0 : rect.bottom + 6,
696
+ bottom: top ? window.innerHeight - rect.top + 6 : void 0,
697
+ maxHeight: Math.max(120, Math.min(LIST_MAX_HEIGHT, (top ? above : below) - 16)),
698
+ placement: top ? "top" : "bottom"
699
+ });
700
+ };
701
+ update();
702
+ window.addEventListener("resize", update);
703
+ window.addEventListener("scroll", update, true);
704
+ return () => {
705
+ window.removeEventListener("resize", update);
706
+ window.removeEventListener("scroll", update, true);
707
+ };
708
+ }, [open]);
709
+ useEffect(() => {
710
+ if (!open) return;
711
+ const onPointerDown = (e) => {
712
+ var _a, _b;
713
+ if (((_a = triggerRef.current) == null ? void 0 : _a.contains(e.target)) || ((_b = listRef.current) == null ? void 0 : _b.contains(e.target))) return;
714
+ setOpen(false);
715
+ };
716
+ document.addEventListener("pointerdown", onPointerDown);
717
+ return () => document.removeEventListener("pointerdown", onPointerDown);
718
+ }, [open]);
719
+ useEffect(() => {
720
+ var _a;
721
+ if (open && position && active >= 0) {
722
+ (_a = document.getElementById(`${listId}-${active}`)) == null ? void 0 : _a.scrollIntoView({ block: "nearest" });
723
+ }
724
+ }, [open, position, active, listId]);
725
+ useEffect(() => () => clearTimeout(typeahead.current.timer), []);
726
+ return /* @__PURE__ */ jsxs(
727
+ Field,
728
+ {
729
+ id: fieldId,
730
+ helpId,
731
+ label,
732
+ helperText,
733
+ error,
734
+ size,
735
+ fullWidth,
736
+ required,
737
+ disabled,
738
+ className: containerClassName,
739
+ wrapClassName: cn("ru-select-wrap", open && "ru-select-wrap--open"),
740
+ children: [
741
+ /* @__PURE__ */ jsxs(
742
+ "button",
743
+ {
744
+ ref: triggerRef,
745
+ id: fieldId,
746
+ type: "button",
747
+ role: "combobox",
748
+ "aria-haspopup": "listbox",
749
+ "aria-expanded": open,
750
+ "aria-controls": open ? listId : void 0,
751
+ "aria-activedescendant": open && active >= 0 ? `${listId}-${active}` : void 0,
752
+ "aria-invalid": error ? true : void 0,
753
+ "aria-required": required || void 0,
754
+ "aria-describedby": describedBy,
755
+ disabled,
756
+ className: cn("ru-select-trigger", className),
757
+ onClick: () => open ? setOpen(false) : openList(),
758
+ onKeyDown,
759
+ ...rest,
760
+ children: [
761
+ (selectedItem == null ? void 0 : selectedItem.icon) && /* @__PURE__ */ jsx("span", { className: "ru-select__icon", children: selectedItem.icon }),
762
+ /* @__PURE__ */ jsx("span", { className: cn("ru-select__value", !selectedItem && "ru-select__placeholder"), children: selectedItem ? selectedItem.label : placeholder }),
763
+ /* @__PURE__ */ jsx(ChevronDownIcon, { className: "ru-select__chevron" })
764
+ ]
765
+ }
766
+ ),
767
+ (name || required) && /* @__PURE__ */ jsx(
768
+ "input",
769
+ {
770
+ className: "ru-select__native",
771
+ tabIndex: -1,
772
+ "aria-hidden": "true",
773
+ name,
774
+ required,
775
+ value: selected ?? "",
776
+ onChange: () => {
777
+ },
778
+ onFocus: () => {
779
+ var _a;
780
+ return (_a = triggerRef.current) == null ? void 0 : _a.focus();
781
+ }
782
+ }
783
+ ),
784
+ open && position && mounted && createPortal(
785
+ /* @__PURE__ */ jsxs(
786
+ "ul",
787
+ {
788
+ ref: listRef,
789
+ id: listId,
790
+ role: "listbox",
791
+ "aria-labelledby": fieldId,
792
+ className: cn("ru-select__list", position.placement === "top" && "ru-select__list--top"),
793
+ style: {
794
+ left: position.left,
795
+ width: position.width,
796
+ top: position.top,
797
+ bottom: position.bottom,
798
+ maxHeight: position.maxHeight
799
+ },
800
+ onMouseDown: (e) => e.preventDefault(),
801
+ children: [
802
+ selectable.length === 0 && /* @__PURE__ */ jsx("li", { className: "ru-select__empty", children: emptyText }),
803
+ items.map(
804
+ (item) => item.type === "group" ? /* @__PURE__ */ jsx("li", { role: "presentation", className: "ru-select__group", children: item.label }, item.key) : /* @__PURE__ */ jsxs(
805
+ "li",
806
+ {
807
+ id: `${listId}-${item.index}`,
808
+ role: "option",
809
+ "aria-selected": item.value === selected,
810
+ "aria-disabled": item.disabled || void 0,
811
+ className: cn(
812
+ "ru-select__option",
813
+ item.index === active && "ru-select__option--active",
814
+ item.value === selected && "ru-select__option--selected",
815
+ item.disabled && "ru-select__option--disabled"
816
+ ),
817
+ onMouseMove: () => !item.disabled && item.index !== active && setActive(item.index),
818
+ onClick: () => choose(item),
819
+ children: [
820
+ item.icon && /* @__PURE__ */ jsx("span", { className: "ru-select__icon", children: item.icon }),
821
+ /* @__PURE__ */ jsxs("span", { className: "ru-select__text", children: [
822
+ /* @__PURE__ */ jsx("span", { className: "ru-select__label", children: item.label }),
823
+ item.description && /* @__PURE__ */ jsx("span", { className: "ru-select__desc", children: item.description })
824
+ ] }),
825
+ item.value === selected && /* @__PURE__ */ jsx(CheckIcon, { className: "ru-select__check" })
826
+ ]
827
+ },
828
+ item.value
829
+ )
830
+ )
831
+ ]
832
+ }
833
+ ),
834
+ document.body
835
+ )
836
+ ]
837
+ }
838
+ );
839
+ });
840
+ const Checkbox = forwardRef(function Checkbox2({ label, description, indeterminate = false, color = "primary", className, disabled, ...rest }, ref) {
841
+ const innerRef = useRef(null);
842
+ useImperativeHandle(ref, () => innerRef.current);
843
+ useEffect(() => {
844
+ if (innerRef.current) innerRef.current.indeterminate = indeterminate;
845
+ }, [indeterminate]);
846
+ return /* @__PURE__ */ jsxs("label", { className: cn("ru-check", `ru-c-${color}`, disabled && "ru-check--disabled", className), children: [
847
+ /* @__PURE__ */ jsx("input", { ref: innerRef, type: "checkbox", className: "ru-check__input", disabled, ...rest }),
848
+ (label || description) && /* @__PURE__ */ jsxs("span", { className: "ru-check__text", children: [
849
+ label,
850
+ description && /* @__PURE__ */ jsx("span", { className: "ru-check__desc", children: description })
851
+ ] })
852
+ ] });
853
+ });
854
+ const Radio = forwardRef(function Radio2({ label, description, color = "primary", className, disabled, ...rest }, ref) {
855
+ return /* @__PURE__ */ jsxs("label", { className: cn("ru-check", `ru-c-${color}`, disabled && "ru-check--disabled", className), children: [
856
+ /* @__PURE__ */ jsx("input", { ref, type: "radio", className: "ru-check__input ru-check__input--radio", disabled, ...rest }),
857
+ (label || description) && /* @__PURE__ */ jsxs("span", { className: "ru-check__text", children: [
858
+ label,
859
+ description && /* @__PURE__ */ jsx("span", { className: "ru-check__desc", children: description })
860
+ ] })
861
+ ] });
862
+ });
863
+ function RadioGroup({
864
+ options,
865
+ name,
866
+ value,
867
+ defaultValue,
868
+ onChange,
869
+ label,
870
+ orientation = "vertical",
871
+ color = "primary",
872
+ disabled,
873
+ className
874
+ }) {
875
+ const autoName = useId();
876
+ const [selected, setSelected] = useControllableState(value, defaultValue ?? "", onChange);
877
+ return /* @__PURE__ */ jsxs("fieldset", { className: cn("ru-radio-group", className), disabled, children: [
878
+ label && /* @__PURE__ */ jsx("legend", { className: "ru-label", children: label }),
879
+ /* @__PURE__ */ jsx("div", { className: cn("ru-radio-group__items", orientation === "horizontal" && "ru-radio-group__items--horizontal"), children: options.map((opt) => /* @__PURE__ */ jsx(
880
+ Radio,
881
+ {
882
+ name: name ?? autoName,
883
+ value: opt.value,
884
+ label: opt.label,
885
+ description: opt.description,
886
+ color,
887
+ disabled: disabled || opt.disabled,
888
+ checked: selected === opt.value,
889
+ onChange: () => setSelected(opt.value)
890
+ },
891
+ opt.value
892
+ )) })
893
+ ] });
894
+ }
895
+ const Switch = forwardRef(function Switch2({ checked, defaultChecked = false, onChange, label, size = "md", color = "primary", disabled, name, id, className }, ref) {
896
+ const [on, setOn] = useControllableState(checked, defaultChecked, onChange);
897
+ return /* @__PURE__ */ jsxs(
898
+ "label",
899
+ {
900
+ className: cn("ru-switch", `ru-switch--${size}`, `ru-c-${color}`, disabled && "ru-switch--disabled", className),
901
+ children: [
902
+ /* @__PURE__ */ jsx(
903
+ "button",
904
+ {
905
+ ref,
906
+ id,
907
+ type: "button",
908
+ role: "switch",
909
+ "aria-checked": on,
910
+ disabled,
911
+ className: "ru-switch__track",
912
+ onClick: () => setOn(!on),
913
+ children: /* @__PURE__ */ jsx("span", { className: "ru-switch__thumb" })
914
+ }
915
+ ),
916
+ label && /* @__PURE__ */ jsx("span", { children: label }),
917
+ name && /* @__PURE__ */ jsx("input", { type: "hidden", name, value: on ? "on" : "off" })
918
+ ]
919
+ }
920
+ );
921
+ });
922
+ function DialogContent({ titleId, descId, title, description, showCloseButton, onClose, children, footer }) {
923
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
924
+ (title || description || showCloseButton) && /* @__PURE__ */ jsxs("div", { className: "ru-modal__header", children: [
925
+ /* @__PURE__ */ jsxs("div", { children: [
926
+ title && /* @__PURE__ */ jsx("h2", { id: titleId, className: "ru-modal__title", children: title }),
927
+ description && /* @__PURE__ */ jsx("p", { id: descId, className: "ru-modal__desc", children: description })
928
+ ] }),
929
+ showCloseButton && /* @__PURE__ */ jsx(CloseButton, { onClick: onClose })
930
+ ] }),
931
+ /* @__PURE__ */ jsx("div", { className: "ru-modal__body", children }),
932
+ footer && /* @__PURE__ */ jsx("div", { className: "ru-modal__footer", children: footer })
933
+ ] });
934
+ }
935
+ function Modal({
936
+ open,
937
+ onClose,
938
+ size = "md",
939
+ closeOnOverlayClick = true,
940
+ closeOnEsc = true,
941
+ showCloseButton = true,
942
+ className,
943
+ ...content
944
+ }) {
945
+ const panelRef = useRef(null);
946
+ const mounted = useMounted();
947
+ const titleId = useId();
948
+ const descId = useId();
949
+ useOverlay(open && mounted, panelRef, onClose, closeOnEsc);
950
+ if (!open || !mounted) return null;
951
+ const onOverlayMouseDown = (e) => {
952
+ if (closeOnOverlayClick && e.target === e.currentTarget) onClose();
953
+ };
954
+ return createPortal(
955
+ /* @__PURE__ */ jsx("div", { className: "ru-overlay", onMouseDown: onOverlayMouseDown, children: /* @__PURE__ */ jsx(
956
+ "div",
957
+ {
958
+ ref: panelRef,
959
+ role: "dialog",
960
+ "aria-modal": "true",
961
+ "aria-labelledby": content.title ? titleId : void 0,
962
+ "aria-describedby": content.description ? descId : void 0,
963
+ tabIndex: -1,
964
+ className: cn("ru-modal", `ru-modal--${size}`, className),
965
+ children: /* @__PURE__ */ jsx(
966
+ DialogContent,
967
+ {
968
+ ...content,
969
+ titleId,
970
+ descId,
971
+ showCloseButton,
972
+ onClose
973
+ }
974
+ )
975
+ }
976
+ ) }),
977
+ document.body
978
+ );
979
+ }
980
+ const drawerSizes = { sm: "20rem", md: "24rem", lg: "32rem" };
981
+ function Drawer({
982
+ open,
983
+ onClose,
984
+ placement = "right",
985
+ size = "md",
986
+ closeOnOverlayClick = true,
987
+ closeOnEsc = true,
988
+ showCloseButton = true,
989
+ className,
990
+ ...content
991
+ }) {
992
+ const panelRef = useRef(null);
993
+ const mounted = useMounted();
994
+ const titleId = useId();
995
+ const descId = useId();
996
+ useOverlay(open && mounted, panelRef, onClose, closeOnEsc);
997
+ if (!open || !mounted) return null;
998
+ const resolvedSize = typeof size === "number" ? `${size}px` : drawerSizes[size] ?? size;
999
+ return createPortal(
1000
+ /* @__PURE__ */ jsx(
1001
+ "div",
1002
+ {
1003
+ className: "ru-overlay ru-overlay--drawer",
1004
+ onMouseDown: (e) => {
1005
+ if (closeOnOverlayClick && e.target === e.currentTarget) onClose();
1006
+ },
1007
+ children: /* @__PURE__ */ jsx(
1008
+ "div",
1009
+ {
1010
+ ref: panelRef,
1011
+ role: "dialog",
1012
+ "aria-modal": "true",
1013
+ "aria-labelledby": content.title ? titleId : void 0,
1014
+ "aria-describedby": content.description ? descId : void 0,
1015
+ tabIndex: -1,
1016
+ className: cn("ru-drawer", `ru-drawer--${placement}`, className),
1017
+ style: { "--ru-drawer-size": resolvedSize },
1018
+ children: /* @__PURE__ */ jsx(
1019
+ DialogContent,
1020
+ {
1021
+ ...content,
1022
+ titleId,
1023
+ descId,
1024
+ showCloseButton,
1025
+ onClose
1026
+ }
1027
+ )
1028
+ }
1029
+ )
1030
+ }
1031
+ ),
1032
+ document.body
1033
+ );
1034
+ }
1035
+ function Tooltip({ content, children, placement = "top", delay = 100, disabled, className }) {
1036
+ const [open, setOpen] = useState(false);
1037
+ const timer = useRef(void 0);
1038
+ const id = useId();
1039
+ const visible = open && !disabled && content != null && content !== "";
1040
+ useEffect(() => () => clearTimeout(timer.current), []);
1041
+ const show = () => {
1042
+ clearTimeout(timer.current);
1043
+ timer.current = setTimeout(() => setOpen(true), delay);
1044
+ };
1045
+ const hide = () => {
1046
+ clearTimeout(timer.current);
1047
+ setOpen(false);
1048
+ };
1049
+ const trigger = isValidElement(children) ? cloneElement(children, {
1050
+ "aria-describedby": visible ? id : void 0
1051
+ }) : children;
1052
+ return /* @__PURE__ */ jsxs(
1053
+ "span",
1054
+ {
1055
+ className: cn("ru-tooltip", className),
1056
+ onMouseEnter: show,
1057
+ onMouseLeave: hide,
1058
+ onFocus: show,
1059
+ onBlur: hide,
1060
+ onKeyDown: (e) => e.key === "Escape" && hide(),
1061
+ children: [
1062
+ trigger,
1063
+ visible && /* @__PURE__ */ jsx("span", { role: "tooltip", id, className: cn("ru-tooltip__bubble", `ru-tooltip__bubble--${placement}`), children: content })
1064
+ ]
1065
+ }
1066
+ );
1067
+ }
1068
+ function Dropdown({ trigger, items, align = "start", closeOnSelect = true, className }) {
1069
+ const [open, setOpen] = useState(false);
1070
+ const rootRef = useRef(null);
1071
+ const menuRef = useRef(null);
1072
+ const menuId = useId();
1073
+ useClickOutside(rootRef, () => setOpen(false), open);
1074
+ const getItems = () => {
1075
+ var _a;
1076
+ return Array.from(((_a = menuRef.current) == null ? void 0 : _a.querySelectorAll('[role="menuitem"]:not(:disabled)')) ?? []);
1077
+ };
1078
+ const focusItem = (index) => {
1079
+ const els = getItems();
1080
+ if (els.length) els[(index + els.length) % els.length].focus();
1081
+ };
1082
+ const closeAndFocusTrigger = () => {
1083
+ var _a, _b;
1084
+ setOpen(false);
1085
+ (_b = (_a = rootRef.current) == null ? void 0 : _a.querySelector("[aria-haspopup]")) == null ? void 0 : _b.focus();
1086
+ };
1087
+ useEffect(() => {
1088
+ if (open) focusItem(0);
1089
+ }, [open]);
1090
+ const onMenuKeyDown = (e) => {
1091
+ const els = getItems();
1092
+ const index = els.indexOf(document.activeElement);
1093
+ switch (e.key) {
1094
+ case "ArrowDown":
1095
+ e.preventDefault();
1096
+ focusItem(index + 1);
1097
+ break;
1098
+ case "ArrowUp":
1099
+ e.preventDefault();
1100
+ focusItem(index - 1);
1101
+ break;
1102
+ case "Home":
1103
+ e.preventDefault();
1104
+ focusItem(0);
1105
+ break;
1106
+ case "End":
1107
+ e.preventDefault();
1108
+ focusItem(-1);
1109
+ break;
1110
+ case "Escape":
1111
+ e.preventDefault();
1112
+ closeAndFocusTrigger();
1113
+ break;
1114
+ case "Tab":
1115
+ setOpen(false);
1116
+ break;
1117
+ }
1118
+ };
1119
+ const triggerProps = trigger.props;
1120
+ const triggerEl = cloneElement(trigger, {
1121
+ "aria-haspopup": "menu",
1122
+ "aria-expanded": open,
1123
+ "aria-controls": open ? menuId : void 0,
1124
+ onClick: (e) => {
1125
+ var _a;
1126
+ (_a = triggerProps.onClick) == null ? void 0 : _a.call(triggerProps, e);
1127
+ setOpen((v) => !v);
1128
+ },
1129
+ onKeyDown: (e) => {
1130
+ var _a;
1131
+ (_a = triggerProps.onKeyDown) == null ? void 0 : _a.call(triggerProps, e);
1132
+ if (e.key === "ArrowDown" && !open) {
1133
+ e.preventDefault();
1134
+ setOpen(true);
1135
+ }
1136
+ }
1137
+ });
1138
+ return /* @__PURE__ */ jsxs("div", { ref: rootRef, className: cn("ru-dropdown", className), children: [
1139
+ triggerEl,
1140
+ open && /* @__PURE__ */ jsx(
1141
+ "div",
1142
+ {
1143
+ ref: menuRef,
1144
+ id: menuId,
1145
+ role: "menu",
1146
+ className: cn("ru-dropdown__menu", align === "end" && "ru-dropdown__menu--end"),
1147
+ onKeyDown: onMenuKeyDown,
1148
+ children: items.map(
1149
+ (item, i) => item.divider ? /* @__PURE__ */ jsx("div", { role: "separator", className: "ru-dropdown__divider" }, item.key ?? i) : /* @__PURE__ */ jsxs(
1150
+ "button",
1151
+ {
1152
+ type: "button",
1153
+ role: "menuitem",
1154
+ tabIndex: -1,
1155
+ disabled: item.disabled,
1156
+ className: cn("ru-dropdown__item", item.danger && "ru-dropdown__item--danger"),
1157
+ onClick: () => {
1158
+ var _a;
1159
+ (_a = item.onClick) == null ? void 0 : _a.call(item);
1160
+ if (closeOnSelect) closeAndFocusTrigger();
1161
+ },
1162
+ children: [
1163
+ item.icon && /* @__PURE__ */ jsx("span", { className: "ru-dropdown__icon", children: item.icon }),
1164
+ item.label
1165
+ ]
1166
+ },
1167
+ item.key ?? i
1168
+ )
1169
+ )
1170
+ }
1171
+ )
1172
+ ] });
1173
+ }
1174
+ function Tabs({ items, value, defaultValue, onChange, variant = "line", fullWidth = false, className }) {
1175
+ var _a;
1176
+ const baseId = useId();
1177
+ const firstEnabled = ((_a = items.find((item) => !item.disabled)) == null ? void 0 : _a.value) ?? "";
1178
+ const [active, setActive] = useControllableState(value, defaultValue ?? firstEnabled, onChange);
1179
+ const tabRefs = useRef([]);
1180
+ const onKeyDown = (e, index) => {
1181
+ var _a2;
1182
+ const enabled = items.flatMap((item, i) => item.disabled ? [] : [i]);
1183
+ const pos = enabled.indexOf(index);
1184
+ let next;
1185
+ switch (e.key) {
1186
+ case "ArrowRight":
1187
+ next = enabled[(pos + 1) % enabled.length];
1188
+ break;
1189
+ case "ArrowLeft":
1190
+ next = enabled[(pos - 1 + enabled.length) % enabled.length];
1191
+ break;
1192
+ case "Home":
1193
+ next = enabled[0];
1194
+ break;
1195
+ case "End":
1196
+ next = enabled[enabled.length - 1];
1197
+ break;
1198
+ default:
1199
+ return;
1200
+ }
1201
+ e.preventDefault();
1202
+ (_a2 = tabRefs.current[next]) == null ? void 0 : _a2.focus();
1203
+ setActive(items[next].value);
1204
+ };
1205
+ const activeIndex = items.findIndex((item) => item.value === active);
1206
+ const activeItem = items[activeIndex];
1207
+ return /* @__PURE__ */ jsxs("div", { className: cn("ru-tabs", `ru-tabs--${variant}`, fullWidth && "ru-tabs--full", className), children: [
1208
+ /* @__PURE__ */ jsx("div", { role: "tablist", className: "ru-tabs__list", children: items.map((item, i) => {
1209
+ const selected = i === activeIndex;
1210
+ return /* @__PURE__ */ jsxs(
1211
+ "button",
1212
+ {
1213
+ ref: (el) => {
1214
+ tabRefs.current[i] = el;
1215
+ },
1216
+ type: "button",
1217
+ role: "tab",
1218
+ id: `${baseId}-tab-${i}`,
1219
+ "aria-selected": selected,
1220
+ "aria-controls": `${baseId}-panel-${i}`,
1221
+ tabIndex: selected ? 0 : -1,
1222
+ disabled: item.disabled,
1223
+ className: "ru-tabs__tab",
1224
+ onClick: () => setActive(item.value),
1225
+ onKeyDown: (e) => onKeyDown(e, i),
1226
+ children: [
1227
+ item.icon,
1228
+ item.label
1229
+ ]
1230
+ },
1231
+ item.value
1232
+ );
1233
+ }) }),
1234
+ activeItem && activeItem.content !== void 0 && /* @__PURE__ */ jsx(
1235
+ "div",
1236
+ {
1237
+ role: "tabpanel",
1238
+ id: `${baseId}-panel-${activeIndex}`,
1239
+ "aria-labelledby": `${baseId}-tab-${activeIndex}`,
1240
+ tabIndex: 0,
1241
+ className: "ru-tabs__panel",
1242
+ children: activeItem.content
1243
+ }
1244
+ )
1245
+ ] });
1246
+ }
1247
+ function Accordion({ items, multiple = false, value, defaultValue, onChange, className }) {
1248
+ const baseId = useId();
1249
+ const [openValues, setOpenValues] = useControllableState(value, defaultValue ?? [], onChange);
1250
+ const toggle = (key) => {
1251
+ const isOpen = openValues.includes(key);
1252
+ if (isOpen) setOpenValues(openValues.filter((v) => v !== key));
1253
+ else setOpenValues(multiple ? [...openValues, key] : [key]);
1254
+ };
1255
+ return /* @__PURE__ */ jsx("div", { className: cn("ru-accordion", className), children: items.map((item, i) => {
1256
+ const key = item.value ?? String(i);
1257
+ const isOpen = openValues.includes(key);
1258
+ const triggerId = `${baseId}-trigger-${i}`;
1259
+ const panelId = `${baseId}-panel-${i}`;
1260
+ return /* @__PURE__ */ jsxs("div", { className: "ru-accordion__item", children: [
1261
+ /* @__PURE__ */ jsx("h3", { className: "ru-accordion__heading", children: /* @__PURE__ */ jsxs(
1262
+ "button",
1263
+ {
1264
+ type: "button",
1265
+ id: triggerId,
1266
+ "aria-expanded": isOpen,
1267
+ "aria-controls": panelId,
1268
+ disabled: item.disabled,
1269
+ className: "ru-accordion__trigger",
1270
+ onClick: () => toggle(key),
1271
+ children: [
1272
+ /* @__PURE__ */ jsx("span", { children: item.title }),
1273
+ /* @__PURE__ */ jsx(ChevronDownIcon, { className: "ru-accordion__chevron" })
1274
+ ]
1275
+ }
1276
+ ) }),
1277
+ /* @__PURE__ */ jsx(
1278
+ "div",
1279
+ {
1280
+ id: panelId,
1281
+ role: "region",
1282
+ "aria-labelledby": triggerId,
1283
+ className: cn("ru-accordion__panel", isOpen && "ru-accordion__panel--open"),
1284
+ children: /* @__PURE__ */ jsx("div", { className: "ru-accordion__inner", children: /* @__PURE__ */ jsx("div", { className: "ru-accordion__content", children: item.content }) })
1285
+ }
1286
+ )
1287
+ ] }, key);
1288
+ }) });
1289
+ }
1290
+ function range(start, end) {
1291
+ return Array.from({ length: end - start + 1 }, (_, i) => start + i);
1292
+ }
1293
+ function getPageTokens(page, totalPages, siblings) {
1294
+ const slots = siblings * 2 + 5;
1295
+ if (totalPages <= slots) return range(1, totalPages);
1296
+ const left = Math.max(page - siblings, 1);
1297
+ const right = Math.min(page + siblings, totalPages);
1298
+ const showLeftEllipsis = left > 2;
1299
+ const showRightEllipsis = right < totalPages - 1;
1300
+ const edgeCount = 3 + siblings * 2;
1301
+ if (!showLeftEllipsis) return [...range(1, edgeCount), "end-ellipsis", totalPages];
1302
+ if (!showRightEllipsis) return [1, "start-ellipsis", ...range(totalPages - edgeCount + 1, totalPages)];
1303
+ return [1, "start-ellipsis", ...range(left, right), "end-ellipsis", totalPages];
1304
+ }
1305
+ function Pagination({ page, totalPages, onChange, siblingCount = 1, size = "md", className }) {
1306
+ if (totalPages < 1) return null;
1307
+ const go = (p) => {
1308
+ if (p >= 1 && p <= totalPages && p !== page) onChange(p);
1309
+ };
1310
+ return /* @__PURE__ */ jsx("nav", { "aria-label": "Pagination", className: cn("ru-pagination", `ru-pagination--${size}`, className), children: /* @__PURE__ */ jsxs("ul", { className: "ru-pagination__list", children: [
1311
+ /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
1312
+ "button",
1313
+ {
1314
+ type: "button",
1315
+ className: "ru-pagination__btn",
1316
+ "aria-label": "Previous page",
1317
+ disabled: page <= 1,
1318
+ onClick: () => go(page - 1),
1319
+ children: /* @__PURE__ */ jsx(ChevronLeftIcon, {})
1320
+ }
1321
+ ) }),
1322
+ getPageTokens(page, totalPages, siblingCount).map(
1323
+ (token) => typeof token === "number" ? /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
1324
+ "button",
1325
+ {
1326
+ type: "button",
1327
+ className: "ru-pagination__btn",
1328
+ "aria-label": `Page ${token}`,
1329
+ "aria-current": token === page ? "page" : void 0,
1330
+ onClick: () => go(token),
1331
+ children: token
1332
+ }
1333
+ ) }, token) : /* @__PURE__ */ jsx("li", { className: "ru-pagination__ellipsis", "aria-hidden": "true", children: "…" }, token)
1334
+ ),
1335
+ /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
1336
+ "button",
1337
+ {
1338
+ type: "button",
1339
+ className: "ru-pagination__btn",
1340
+ "aria-label": "Next page",
1341
+ disabled: page >= totalPages,
1342
+ onClick: () => go(page + 1),
1343
+ children: /* @__PURE__ */ jsx(ChevronRightIcon, {})
1344
+ }
1345
+ ) })
1346
+ ] }) });
1347
+ }
1348
+ function Breadcrumbs({ items, separator, className }) {
1349
+ return /* @__PURE__ */ jsx("nav", { "aria-label": "Breadcrumb", className: cn("ru-breadcrumbs", className), children: /* @__PURE__ */ jsx("ol", { children: items.map((item, i) => {
1350
+ const isLast = i === items.length - 1;
1351
+ let node = item.label;
1352
+ if (isLast) {
1353
+ node = /* @__PURE__ */ jsx("span", { className: "ru-breadcrumbs__current", "aria-current": "page", children: item.label });
1354
+ } else if (item.href) {
1355
+ node = /* @__PURE__ */ jsx("a", { className: "ru-breadcrumbs__link", href: item.href, onClick: item.onClick, children: item.label });
1356
+ } else if (item.onClick) {
1357
+ node = /* @__PURE__ */ jsx("button", { type: "button", className: "ru-breadcrumbs__link", onClick: item.onClick, children: item.label });
1358
+ }
1359
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1360
+ /* @__PURE__ */ jsx("li", { className: "ru-breadcrumbs__item", children: node }),
1361
+ !isLast && /* @__PURE__ */ jsx("li", { className: "ru-breadcrumbs__sep", "aria-hidden": "true", children: separator ?? /* @__PURE__ */ jsx(ChevronRightIcon, {}) })
1362
+ ] }, i);
1363
+ }) }) });
1364
+ }
1365
+ const ToastContext = createContext(null);
1366
+ function ToastProvider({ children, position = "bottom-right", duration = 4e3, max = 5 }) {
1367
+ const [toasts, setToasts] = useState([]);
1368
+ const mounted = useMounted();
1369
+ const counter = useRef(0);
1370
+ const dismiss = useCallback((id) => {
1371
+ setToasts((list) => id ? list.filter((t) => t.id !== id) : []);
1372
+ }, []);
1373
+ const api = useMemo(() => {
1374
+ const show = (options) => {
1375
+ const opts = typeof options === "string" ? { title: options } : options;
1376
+ const id = opts.id ?? `ru-toast-${++counter.current}`;
1377
+ setToasts((list) => [...list.filter((t) => t.id !== id), { ...opts, id }].slice(-max));
1378
+ return id;
1379
+ };
1380
+ const fn = show;
1381
+ fn.success = (title, o) => show({ ...o, title, variant: "success" });
1382
+ fn.error = (title, o) => show({ ...o, title, variant: "danger" });
1383
+ fn.warning = (title, o) => show({ ...o, title, variant: "warning" });
1384
+ fn.info = (title, o) => show({ ...o, title, variant: "info" });
1385
+ fn.dismiss = dismiss;
1386
+ return fn;
1387
+ }, [dismiss, max]);
1388
+ return /* @__PURE__ */ jsxs(ToastContext.Provider, { value: api, children: [
1389
+ children,
1390
+ mounted && createPortal(
1391
+ /* @__PURE__ */ jsx("ol", { className: cn("ru-toaster", `ru-toaster--${position}`), "aria-live": "polite", "aria-label": "Notifications", children: toasts.map((t) => /* @__PURE__ */ jsx(ToastView, { toast: t, duration: t.duration ?? duration, onDismiss: dismiss }, t.id)) }),
1392
+ document.body
1393
+ )
1394
+ ] });
1395
+ }
1396
+ function ToastView({ toast, duration, onDismiss }) {
1397
+ const [paused, setPaused] = useState(false);
1398
+ const { id, variant = "default", title, description, action } = toast;
1399
+ useEffect(() => {
1400
+ if (paused || !Number.isFinite(duration) || duration <= 0) return;
1401
+ const timer = setTimeout(() => onDismiss(id), duration);
1402
+ return () => clearTimeout(timer);
1403
+ }, [id, duration, paused, onDismiss]);
1404
+ const Icon = variant === "default" ? null : statusIcons[variant];
1405
+ return /* @__PURE__ */ jsxs(
1406
+ "li",
1407
+ {
1408
+ role: "status",
1409
+ className: cn("ru-toast", variant !== "default" && `ru-c-${variant}`),
1410
+ onMouseEnter: () => setPaused(true),
1411
+ onMouseLeave: () => setPaused(false),
1412
+ children: [
1413
+ Icon && /* @__PURE__ */ jsx("span", { className: "ru-toast__icon", children: /* @__PURE__ */ jsx(Icon, {}) }),
1414
+ /* @__PURE__ */ jsxs("div", { className: "ru-toast__content", children: [
1415
+ title && /* @__PURE__ */ jsx("div", { className: "ru-toast__title", children: title }),
1416
+ description && /* @__PURE__ */ jsx("div", { className: "ru-toast__desc", children: description }),
1417
+ action && /* @__PURE__ */ jsx("div", { className: "ru-toast__action", children: action })
1418
+ ] }),
1419
+ /* @__PURE__ */ jsx(CloseButton, { onClick: () => onDismiss(id), label: "Dismiss notification" })
1420
+ ]
1421
+ }
1422
+ );
1423
+ }
1424
+ function useToast() {
1425
+ const ctx = useContext(ToastContext);
1426
+ if (!ctx) throw new Error("useToast must be used within a <ToastProvider>.");
1427
+ return ctx;
1428
+ }
1429
+ function Kbd({ className, ...rest }) {
1430
+ return /* @__PURE__ */ jsx("kbd", { className: cn("ru-kbd", className), ...rest });
1431
+ }
1432
+ function Pattern({ variant = "grid", size = 24, fade = true, className, style, ...rest }) {
1433
+ return /* @__PURE__ */ jsx(
1434
+ "div",
1435
+ {
1436
+ "aria-hidden": "true",
1437
+ className: cn("ru-pattern", `ru-pattern--${variant}`, fade && "ru-pattern--fade", className),
1438
+ style: { "--ru-pattern-size": `${size}px`, ...style },
1439
+ ...rest
1440
+ }
1441
+ );
1442
+ }
1443
+ function SpotlightCard({ color, className, style, onMouseMove, children, ...rest }) {
1444
+ const ref = useRef(null);
1445
+ const handleMouseMove = (e) => {
1446
+ const el = ref.current;
1447
+ if (el) {
1448
+ const rect = el.getBoundingClientRect();
1449
+ el.style.setProperty("--ru-spot-x", `${e.clientX - rect.left}px`);
1450
+ el.style.setProperty("--ru-spot-y", `${e.clientY - rect.top}px`);
1451
+ }
1452
+ onMouseMove == null ? void 0 : onMouseMove(e);
1453
+ };
1454
+ return /* @__PURE__ */ jsx(
1455
+ "div",
1456
+ {
1457
+ ref,
1458
+ className: cn("ru-card", "ru-spotlight", className),
1459
+ style: color ? { "--ru-spot-color": color, ...style } : style,
1460
+ onMouseMove: handleMouseMove,
1461
+ ...rest,
1462
+ children
1463
+ }
1464
+ );
1465
+ }
1466
+ export {
1467
+ Accordion,
1468
+ Alert,
1469
+ Avatar,
1470
+ AvatarGroup,
1471
+ Badge,
1472
+ Breadcrumbs,
1473
+ Button,
1474
+ Card,
1475
+ CardBody,
1476
+ CardFooter,
1477
+ CardHeader,
1478
+ Checkbox,
1479
+ Divider,
1480
+ Drawer,
1481
+ Dropdown,
1482
+ Input,
1483
+ Kbd,
1484
+ Modal,
1485
+ NativeSelect,
1486
+ Pagination,
1487
+ Pattern,
1488
+ Progress,
1489
+ Radio,
1490
+ RadioGroup,
1491
+ Select,
1492
+ Skeleton,
1493
+ Spinner,
1494
+ SpotlightCard,
1495
+ Switch,
1496
+ Table,
1497
+ Tabs,
1498
+ Textarea,
1499
+ ToastProvider,
1500
+ Tooltip,
1501
+ cn,
1502
+ useClickOutside,
1503
+ useControllableState,
1504
+ useDisclosure,
1505
+ useToast
1506
+ };