reusify-ui 2.1.0 → 2.3.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.
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { jsx, jsxs, Fragment } from "react/jsx-runtime";
3
- import { forwardRef, useRef, useEffect, useState, useCallback, useImperativeHandle, useId, useMemo, Children, isValidElement, Fragment as Fragment$1, cloneElement, useContext, createContext } from "react";
3
+ import { forwardRef, useState, useRef, useEffect, useLayoutEffect, useCallback, useImperativeHandle, useId, useMemo, Children, isValidElement, Fragment as Fragment$1, cloneElement, useContext, createContext } from "react";
4
4
  import { createPortal } from "react-dom";
5
5
  function cn(...classes) {
6
6
  return classes.filter(Boolean).join(" ");
@@ -10,7 +10,7 @@ const legacyVariants = {
10
10
  danger: ["solid", "danger"],
11
11
  success: ["solid", "success"]
12
12
  };
13
- const semanticColors$2 = /* @__PURE__ */ new Set(["primary", "secondary", "success", "danger", "warning", "info"]);
13
+ const semanticColors$5 = /* @__PURE__ */ new Set(["primary", "secondary", "success", "danger", "warning", "info"]);
14
14
  const radii = { none: "0", sm: "6px", md: "10px", lg: "14px", xl: "18px", "2xl": "24px", full: "9999px" };
15
15
  function resolveRadius(radius) {
16
16
  if (radius === void 0) return void 0;
@@ -48,7 +48,7 @@ const Button = forwardRef(function Button2({
48
48
  onPointerMove == null ? void 0 : onPointerMove(e);
49
49
  } : onPointerMove;
50
50
  const resolvedColor = color ?? legacyColor ?? "primary";
51
- const customColor = !semanticColors$2.has(resolvedColor);
51
+ const customColor = !semanticColors$5.has(resolvedColor);
52
52
  const classes = cn(
53
53
  "ru-btn",
54
54
  `ru-btn--${kind}`,
@@ -143,6 +143,16 @@ const UserIcon = (p) => /* @__PURE__ */ jsxs("svg", { ...base, ...p, children: [
143
143
  /* @__PURE__ */ jsx("circle", { cx: "12", cy: "8", r: "4" }),
144
144
  /* @__PURE__ */ jsx("path", { d: "M4 21a8 8 0 0 1 16 0" })
145
145
  ] });
146
+ const UploadCloudIcon = (p) => /* @__PURE__ */ jsxs("svg", { ...base, ...p, children: [
147
+ /* @__PURE__ */ jsx("path", { d: "M4 14.9A7 7 0 1 1 15.7 8h1.8a4.5 4.5 0 0 1 2.5 8.2" }),
148
+ /* @__PURE__ */ jsx("path", { d: "M12 12v9M16 16l-4-4-4 4" })
149
+ ] });
150
+ const FileIcon = (p) => /* @__PURE__ */ jsxs("svg", { ...base, ...p, children: [
151
+ /* @__PURE__ */ jsx("path", { d: "M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z" }),
152
+ /* @__PURE__ */ jsx("path", { d: "M14 2v4a2 2 0 0 0 2 2h4" })
153
+ ] });
154
+ const TrendUpIcon = (p) => /* @__PURE__ */ jsx("svg", { ...base, ...p, children: /* @__PURE__ */ jsx("path", { d: "M12 19V5M5 12l7-7 7 7" }) });
155
+ const TrendDownIcon = (p) => /* @__PURE__ */ jsx("svg", { ...base, ...p, children: /* @__PURE__ */ jsx("path", { d: "M12 5v14M19 12l-7 7-7-7" }) });
146
156
  const statusIcons = {
147
157
  info: InfoIcon,
148
158
  success: SuccessIcon,
@@ -158,11 +168,17 @@ function Spinner({ size = "md", color = "primary", label = "Loading", className,
158
168
  label && /* @__PURE__ */ jsx("span", { className: "ru-sr-only", children: label })
159
169
  ] });
160
170
  }
171
+ const progressColors = /* @__PURE__ */ new Set(["primary", "secondary", "success", "danger", "warning", "info"]);
161
172
  function Progress({
162
173
  value,
163
174
  max = 100,
164
175
  size = "md",
165
176
  color = "primary",
177
+ variant = "bar",
178
+ ticks = 24,
179
+ glow = false,
180
+ icon,
181
+ endLabel,
166
182
  label,
167
183
  showValue = false,
168
184
  className,
@@ -170,7 +186,86 @@ function Progress({
170
186
  }) {
171
187
  const indeterminate = value === void 0;
172
188
  const percent = indeterminate ? 0 : Math.min(100, Math.max(0, value / max * 100));
173
- return /* @__PURE__ */ jsxs("div", { className: cn("ru-progress", indeterminate && "ru-progress--indeterminate", className), ...rest, children: [
189
+ const semantic = progressColors.has(color);
190
+ const glowColor = glow === true ? semantic ? `var(--ru-${color})` : color : glow || void 0;
191
+ const meterStyle = { ...!semantic && { "--_c": color }, ...glowColor && { "--_glow": glowColor } };
192
+ if (variant === "capsule") {
193
+ return /* @__PURE__ */ jsxs("div", { className: cn("ru-progress", "ru-progress--capsule", `ru-progress--${size}`, glowColor && "ru-progress--glow", className), ...rest, children: [
194
+ label && /* @__PURE__ */ jsx("div", { className: "ru-progress__label", children: /* @__PURE__ */ jsx("span", { children: label }) }),
195
+ /* @__PURE__ */ jsxs(
196
+ "div",
197
+ {
198
+ role: "progressbar",
199
+ "aria-valuemin": 0,
200
+ "aria-valuemax": max,
201
+ "aria-valuenow": value ?? 0,
202
+ "aria-label": typeof label === "string" ? label : void 0,
203
+ className: cn("ru-progress__capsule", semantic && `ru-c-${color}`),
204
+ style: meterStyle,
205
+ children: [
206
+ /* @__PURE__ */ jsx("div", { className: "ru-progress__capsule-fill", style: { width: `max(var(--_h), ${percent}%)` }, children: icon ?? (showValue && /* @__PURE__ */ jsxs("span", { className: "ru-progress__capsule-value", children: [
207
+ Math.round(percent),
208
+ "%"
209
+ ] })) }),
210
+ endLabel && /* @__PURE__ */ jsx("span", { className: "ru-progress__capsule-end", children: endLabel })
211
+ ]
212
+ }
213
+ )
214
+ ] });
215
+ }
216
+ if (variant === "bars") {
217
+ const total = Math.max(2, ticks);
218
+ const lit = Math.round(percent / 100 * total);
219
+ return /* @__PURE__ */ jsxs("div", { className: cn("ru-progress", "ru-progress--bars", `ru-progress--${size}`, glowColor && "ru-progress--glow", className), ...rest, children: [
220
+ (label || showValue) && /* @__PURE__ */ jsxs("div", { className: "ru-progress__label", children: [
221
+ /* @__PURE__ */ jsx("span", { children: label }),
222
+ showValue && /* @__PURE__ */ jsxs("span", { className: "ru-progress__value ru-progress__value--accent", children: [
223
+ Math.round(percent),
224
+ "%"
225
+ ] })
226
+ ] }),
227
+ /* @__PURE__ */ jsx(
228
+ "div",
229
+ {
230
+ role: "progressbar",
231
+ "aria-valuemin": 0,
232
+ "aria-valuemax": max,
233
+ "aria-valuenow": value ?? 0,
234
+ "aria-label": typeof label === "string" ? label : void 0,
235
+ className: cn("ru-progress__bars", semantic && `ru-c-${color}`),
236
+ style: meterStyle,
237
+ children: Array.from({ length: total }, (_, i) => /* @__PURE__ */ jsx("span", { className: cn(i < lit && "is-on", i === lit - 1 && percent > 0 && percent < 100 && "is-marker") }, i))
238
+ }
239
+ )
240
+ ] });
241
+ }
242
+ if (variant === "ticks") {
243
+ const remaining = percent >= 100 ? 0 : Math.max(1, Math.round(ticks * (1 - percent / 100)));
244
+ return /* @__PURE__ */ jsxs("div", { className: cn("ru-progress", "ru-progress--ticks", `ru-progress--${size}`, glowColor && "ru-progress--glow", className), ...rest, children: [
245
+ label && /* @__PURE__ */ jsx("div", { className: "ru-progress__label", children: /* @__PURE__ */ jsx("span", { children: label }) }),
246
+ /* @__PURE__ */ jsxs(
247
+ "div",
248
+ {
249
+ role: "progressbar",
250
+ "aria-valuemin": 0,
251
+ "aria-valuemax": max,
252
+ "aria-valuenow": value ?? 0,
253
+ "aria-label": typeof label === "string" ? label : void 0,
254
+ className: cn("ru-progress__meter", semantic && `ru-c-${color}`),
255
+ style: meterStyle,
256
+ children: [
257
+ percent > 0 && /* @__PURE__ */ jsx("div", { className: "ru-progress__fill", style: { width: `${percent}%` }, children: showValue && /* @__PURE__ */ jsxs("span", { className: "ru-progress__fill-value", children: [
258
+ Math.round(percent),
259
+ "%"
260
+ ] }) }),
261
+ percent > 0 && percent < 100 && /* @__PURE__ */ jsx("span", { className: "ru-progress__marker", "aria-hidden": "true" }),
262
+ remaining > 0 && /* @__PURE__ */ jsx("div", { className: "ru-progress__ticks", "aria-hidden": "true", children: Array.from({ length: remaining }, (_, i) => /* @__PURE__ */ jsx("span", {}, i)) })
263
+ ]
264
+ }
265
+ )
266
+ ] });
267
+ }
268
+ return /* @__PURE__ */ jsxs("div", { className: cn("ru-progress", indeterminate && "ru-progress--indeterminate", glowColor && "ru-progress--glow", className), ...rest, children: [
174
269
  (label || showValue) && /* @__PURE__ */ jsxs("div", { className: "ru-progress__label", children: [
175
270
  /* @__PURE__ */ jsx("span", { children: label }),
176
271
  showValue && !indeterminate && /* @__PURE__ */ jsxs("span", { className: "ru-progress__value", children: [
@@ -186,7 +281,8 @@ function Progress({
186
281
  "aria-valuemax": max,
187
282
  "aria-valuenow": indeterminate ? void 0 : value,
188
283
  "aria-label": typeof label === "string" ? label : void 0,
189
- className: cn("ru-progress__track", `ru-progress__track--${size}`, `ru-c-${color}`),
284
+ className: cn("ru-progress__track", `ru-progress__track--${size}`, semantic && `ru-c-${color}`),
285
+ style: meterStyle,
190
286
  children: /* @__PURE__ */ jsx("div", { className: "ru-progress__bar", style: indeterminate ? void 0 : { width: `${percent}%` } })
191
287
  }
192
288
  )
@@ -225,6 +321,98 @@ function Alert({ variant = "info", title, icon, onClose, className, children, ..
225
321
  }
226
322
  );
227
323
  }
324
+ const useBrowserLayoutEffect = typeof document === "undefined" ? useEffect : useLayoutEffect;
325
+ const TOOLTIP_EDGE = 8;
326
+ const TOOLTIP_COMFORT = 16;
327
+ const ARROW_INSET = 14;
328
+ const oppositeSide = { top: "bottom", bottom: "top", left: "right", right: "left" };
329
+ const AUTO_ORDER = ["top", "bottom", "right", "left"];
330
+ const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
331
+ function visibleArea(el) {
332
+ const area = {
333
+ top: TOOLTIP_EDGE,
334
+ left: TOOLTIP_EDGE,
335
+ right: document.documentElement.clientWidth - TOOLTIP_EDGE,
336
+ bottom: window.innerHeight - TOOLTIP_EDGE
337
+ };
338
+ for (let node = el; node && node !== document.body; node = node.parentElement) {
339
+ const { overflowX, overflowY, position } = getComputedStyle(node);
340
+ if (node !== el) {
341
+ const rect = node.getBoundingClientRect();
342
+ if (overflowX !== "visible") {
343
+ area.left = Math.max(area.left, rect.left + TOOLTIP_EDGE);
344
+ area.right = Math.min(area.right, rect.right - TOOLTIP_EDGE);
345
+ }
346
+ if (overflowY !== "visible") {
347
+ area.top = Math.max(area.top, rect.top + TOOLTIP_EDGE);
348
+ area.bottom = Math.min(area.bottom, rect.bottom - TOOLTIP_EDGE);
349
+ }
350
+ }
351
+ if (position === "fixed") break;
352
+ }
353
+ return area;
354
+ }
355
+ function slideAlong(start, size, low, high, triggerLow, triggerHigh) {
356
+ const nearLow = start < low + TOOLTIP_COMFORT;
357
+ const nearHigh = start + size > high - TOOLTIP_COMFORT;
358
+ if (!nearLow && !nearHigh) return 0;
359
+ const clamped = clamp(start, low, high - size);
360
+ if (nearLow && nearHigh) return clamped - start;
361
+ const snapped = nearHigh ? triggerHigh - size : triggerLow;
362
+ const canSnap = snapped >= low - TOOLTIP_EDGE && snapped + size <= high + TOOLTIP_EDGE;
363
+ return (canSnap ? snapped : clamped) - start;
364
+ }
365
+ const samePosition = (a, b) => a && Object.keys(b).every((key) => a[key] === b[key]);
366
+ function placeTooltip(trigger, bubble, placement) {
367
+ const t = trigger.getBoundingClientRect();
368
+ const w = bubble.offsetWidth;
369
+ const h = bubble.offsetHeight;
370
+ const gap = parseFloat(getComputedStyle(bubble).getPropertyValue("--_gap")) || 10;
371
+ const area = visibleArea(trigger);
372
+ const room = { top: t.top - area.top, bottom: area.bottom - t.bottom, left: t.left - area.left, right: area.right - t.right };
373
+ const needs = (side2) => (side2 === "top" || side2 === "bottom" ? h : w) + gap;
374
+ const fits = (s) => room[s] >= needs(s);
375
+ const aligned = (s) => s === "top" || s === "bottom" ? slideAlong(t.left + t.width / 2 - w / 2, w, area.left, area.right, t.left, t.right) === 0 : slideAlong(t.top + t.height / 2 - h / 2, h, area.top, area.bottom, t.top, t.bottom) === 0;
376
+ const roomiest = AUTO_ORDER.reduce((best, s) => room[s] > room[best] ? s : best);
377
+ let side = placement === "auto" ? AUTO_ORDER.find((s) => fits(s) && aligned(s)) ?? AUTO_ORDER.find(fits) ?? roomiest : placement;
378
+ if (room[side] < needs(side) && room[oppositeSide[side]] > room[side]) side = oppositeSide[side];
379
+ if (room[side] < needs(side) && (side === "left" || side === "right")) {
380
+ const vertical = room.bottom >= room.top ? "bottom" : "top";
381
+ if (room[vertical] >= needs(vertical)) side = vertical;
382
+ }
383
+ if (side === "top" || side === "bottom") {
384
+ const start2 = t.left + t.width / 2 - w / 2;
385
+ const shift2 = slideAlong(start2, w, area.left, area.right, t.left, t.right);
386
+ return { side, shiftX: shift2, shiftY: 0, arrowX: clamp(-shift2, ARROW_INSET - w / 2, w / 2 - ARROW_INSET), arrowY: 0 };
387
+ }
388
+ const start = t.top + t.height / 2 - h / 2;
389
+ const shift = slideAlong(start, h, area.top, area.bottom, t.top, t.bottom);
390
+ return { side, shiftX: 0, shiftY: shift, arrowX: 0, arrowY: clamp(-shift, ARROW_INSET - h / 2, h / 2 - ARROW_INSET) };
391
+ }
392
+ function usePlacement(open, triggerRef, bubbleRef, placement = "top") {
393
+ const [position, setPosition] = useState(null);
394
+ const update = () => {
395
+ if (!triggerRef.current || !bubbleRef.current) return;
396
+ const next = placeTooltip(triggerRef.current, bubbleRef.current, placement);
397
+ setPosition((prev) => samePosition(prev, next) ? prev : next);
398
+ };
399
+ const updateRef = useRef(update);
400
+ updateRef.current = update;
401
+ useBrowserLayoutEffect(() => {
402
+ if (open) updateRef.current();
403
+ });
404
+ useEffect(() => {
405
+ if (!open) return void 0;
406
+ const onChange = () => updateRef.current();
407
+ window.addEventListener("scroll", onChange, true);
408
+ window.addEventListener("resize", onChange);
409
+ return () => {
410
+ window.removeEventListener("scroll", onChange, true);
411
+ window.removeEventListener("resize", onChange);
412
+ };
413
+ }, [open]);
414
+ return position;
415
+ }
228
416
  function useControllableState(value, defaultValue, onChange) {
229
417
  const [internal, setInternal] = useState(defaultValue);
230
418
  const isControlled = value !== void 0;
@@ -1084,18 +1272,22 @@ function resolveAvatarColor(color, seed) {
1084
1272
  }
1085
1273
  function AvatarTooltip({ content, description, accent, children }) {
1086
1274
  const [open, setOpen] = useState(false);
1275
+ const rootRef = useRef(null);
1087
1276
  const bubbleRef = useRef(null);
1277
+ const position = usePlacement(open, rootRef, bubbleRef, "top");
1278
+ const sliding = Boolean(position == null ? void 0 : position.shiftX);
1088
1279
  const tiltToward = (e) => {
1089
1280
  const bubble = bubbleRef.current;
1090
1281
  if (!bubble) return;
1091
1282
  const rect = e.currentTarget.getBoundingClientRect();
1092
1283
  const ratio = Math.max(-1, Math.min(1, (e.clientX - (rect.left + rect.width / 2)) / (rect.width / 2)));
1093
- bubble.style.setProperty("--_shift", `${ratio * 10}px`);
1284
+ bubble.style.setProperty("--_shift", sliding ? "0px" : `${ratio * 10}px`);
1094
1285
  bubble.style.setProperty("--_tilt", `${ratio * 12}deg`);
1095
1286
  };
1096
1287
  return /* @__PURE__ */ jsxs(
1097
1288
  "span",
1098
1289
  {
1290
+ ref: rootRef,
1099
1291
  className: "ru-avatar-tip",
1100
1292
  onMouseEnter: () => setOpen(true),
1101
1293
  onMouseLeave: () => setOpen(false),
@@ -1105,10 +1297,23 @@ function AvatarTooltip({ content, description, accent, children }) {
1105
1297
  children: [
1106
1298
  children,
1107
1299
  open && // The avatar's accessible name already includes this text, so the bubble is visual only.
1108
- /* @__PURE__ */ jsx("span", { ref: bubbleRef, className: "ru-avatar-tip__bubble", "aria-hidden": "true", style: accent ? { "--_accent": accent } : void 0, children: /* @__PURE__ */ jsxs("span", { className: "ru-avatar-tip__inner", children: [
1109
- /* @__PURE__ */ jsx("span", { className: "ru-avatar-tip__name", children: content }),
1110
- description && /* @__PURE__ */ jsx("span", { className: "ru-avatar-tip__desc", children: description })
1111
- ] }) })
1300
+ /* @__PURE__ */ jsx(
1301
+ "span",
1302
+ {
1303
+ ref: bubbleRef,
1304
+ className: cn("ru-avatar-tip__bubble", (position == null ? void 0 : position.side) === "bottom" && "ru-avatar-tip__bubble--bottom"),
1305
+ "aria-hidden": "true",
1306
+ style: {
1307
+ ...accent && { "--_accent": accent },
1308
+ ...position && { "--_sx": `${position.shiftX}px`, "--_ax": `${position.arrowX}px` }
1309
+ },
1310
+ children: /* @__PURE__ */ jsxs("span", { className: "ru-avatar-tip__inner", children: [
1311
+ /* @__PURE__ */ jsx("span", { className: "ru-avatar-tip__dots" }),
1312
+ /* @__PURE__ */ jsx("span", { className: "ru-avatar-tip__name", children: content }),
1313
+ description && /* @__PURE__ */ jsx("span", { className: "ru-avatar-tip__desc", children: description })
1314
+ ] })
1315
+ }
1316
+ )
1112
1317
  ]
1113
1318
  }
1114
1319
  );
@@ -1268,6 +1473,7 @@ function Divider({
1268
1473
  "ru-divider",
1269
1474
  variant !== "solid" && `ru-divider--${variant}`,
1270
1475
  vertical && "ru-divider--vertical",
1476
+ vertical && length !== void 0 && "ru-divider--sized",
1271
1477
  labelled && "ru-divider--label",
1272
1478
  labelled && labelPosition !== "center" && `ru-divider--label-${labelPosition}`,
1273
1479
  !vertical && align !== "center" && `ru-divider--align-${align}`,
@@ -1398,7 +1604,7 @@ function Table({
1398
1604
  ] })
1399
1605
  ] });
1400
1606
  }
1401
- const EXIT_MS = 200;
1607
+ const EXIT_MS$1 = 200;
1402
1608
  function usePresence(open) {
1403
1609
  const [render, setRender] = useState(open);
1404
1610
  const [closing, setClosing] = useState(false);
@@ -1413,7 +1619,7 @@ function usePresence(open) {
1413
1619
  const timer = setTimeout(() => {
1414
1620
  setRender(false);
1415
1621
  setClosing(false);
1416
- }, EXIT_MS);
1622
+ }, EXIT_MS$1);
1417
1623
  return () => clearTimeout(timer);
1418
1624
  }, [open]);
1419
1625
  return { render, closing };
@@ -1446,7 +1652,7 @@ function Modal({
1446
1652
  const titleId = useId();
1447
1653
  const descId = useId();
1448
1654
  const { render, closing } = usePresence(open);
1449
- useOverlay(open && mounted, panelRef, onClose, closeOnEsc);
1655
+ useOverlay(open && mounted && render, panelRef, onClose, closeOnEsc);
1450
1656
  if (!render || !mounted) return null;
1451
1657
  const onOverlayMouseDown = (e) => {
1452
1658
  if (!closing && closeOnOverlayClick && e.target === e.currentTarget) onClose();
@@ -1495,7 +1701,7 @@ function Drawer({
1495
1701
  const titleId = useId();
1496
1702
  const descId = useId();
1497
1703
  const { render, closing } = usePresence(open);
1498
- useOverlay(open && mounted, panelRef, onClose, closeOnEsc);
1704
+ useOverlay(open && mounted && render, panelRef, onClose, closeOnEsc);
1499
1705
  if (!render || !mounted) return null;
1500
1706
  const resolvedSize = typeof size === "number" ? `${size}px` : drawerSizes[size] ?? size;
1501
1707
  return createPortal(
@@ -1551,13 +1757,24 @@ function Tooltip({
1551
1757
  textColor,
1552
1758
  open: openProp,
1553
1759
  disabled,
1760
+ onPlacementChange,
1554
1761
  className
1555
1762
  }) {
1556
1763
  const [open, setOpen] = useState(false);
1557
1764
  const timer = useRef(void 0);
1765
+ const rootRef = useRef(null);
1766
+ const bubbleRef = useRef(null);
1558
1767
  const id = useId();
1559
1768
  const visible = (openProp ?? open) && !disabled && content != null && content !== "";
1560
1769
  useEffect(() => () => clearTimeout(timer.current), []);
1770
+ const position = usePlacement(visible, rootRef, bubbleRef, placement);
1771
+ const side = position == null ? void 0 : position.side;
1772
+ const onPlacementChangeRef = useRef(onPlacementChange);
1773
+ onPlacementChangeRef.current = onPlacementChange;
1774
+ useEffect(() => {
1775
+ var _a;
1776
+ if (side) (_a = onPlacementChangeRef.current) == null ? void 0 : _a.call(onPlacementChangeRef, side);
1777
+ }, [side]);
1561
1778
  const show = () => {
1562
1779
  clearTimeout(timer.current);
1563
1780
  timer.current = setTimeout(() => setOpen(true), delay);
@@ -1572,6 +1789,7 @@ function Tooltip({
1572
1789
  return /* @__PURE__ */ jsxs(
1573
1790
  "span",
1574
1791
  {
1792
+ ref: rootRef,
1575
1793
  className: cn("ru-tooltip", className),
1576
1794
  onMouseEnter: show,
1577
1795
  onMouseLeave: hide,
@@ -1583,15 +1801,25 @@ function Tooltip({
1583
1801
  visible && /* @__PURE__ */ jsx(
1584
1802
  "span",
1585
1803
  {
1804
+ ref: bubbleRef,
1586
1805
  role: "tooltip",
1587
1806
  id,
1588
1807
  className: cn(
1589
1808
  "ru-tooltip__bubble",
1590
- `ru-tooltip__bubble--${placement}`,
1809
+ `ru-tooltip__bubble--${side ?? (placement === "auto" ? "top" : placement)}`,
1591
1810
  size !== "md" && `ru-tooltip__bubble--${size}`,
1592
1811
  !arrow && "ru-tooltip__bubble--no-arrow"
1593
1812
  ),
1594
- style: color || textColor ? { "--ru-tooltip-bg": color, "--ru-tooltip-text": textColor } : void 0,
1813
+ style: {
1814
+ ...color && { "--ru-tooltip-bg": color },
1815
+ ...textColor && { "--ru-tooltip-text": textColor },
1816
+ ...position && {
1817
+ "--_sx": `${position.shiftX}px`,
1818
+ "--_sy": `${position.shiftY}px`,
1819
+ "--_ax": `${position.arrowX}px`,
1820
+ "--_ay": `${position.arrowY}px`
1821
+ }
1822
+ },
1595
1823
  children: content
1596
1824
  }
1597
1825
  )
@@ -1706,12 +1934,23 @@ function Dropdown({ trigger, items, align = "start", closeOnSelect = true, class
1706
1934
  ] });
1707
1935
  }
1708
1936
  const ToastContext = createContext(null);
1709
- function ToastProvider({ children, position = "bottom-right", duration = 4e3, max = 5 }) {
1937
+ const EXIT_MS = 200;
1938
+ function ToastProvider({
1939
+ children,
1940
+ position = "bottom-right",
1941
+ duration = 4e3,
1942
+ max = 5,
1943
+ appearance = "default",
1944
+ animation = "slide",
1945
+ progress = false
1946
+ }) {
1710
1947
  const [toasts, setToasts] = useState([]);
1711
1948
  const mounted = useMounted();
1712
1949
  const counter = useRef(0);
1713
1950
  const dismiss = useCallback((id) => {
1714
- setToasts((list) => id ? list.filter((t) => t.id !== id) : []);
1951
+ const matches = (t) => !id || t.id === id;
1952
+ setToasts((list) => list.map((t) => matches(t) ? { ...t, closing: true } : t));
1953
+ setTimeout(() => setToasts((list) => list.filter((t) => !(matches(t) && t.closing))), EXIT_MS);
1715
1954
  }, []);
1716
1955
  const api = useMemo(() => {
1717
1956
  const show = (options) => {
@@ -1731,25 +1970,61 @@ function ToastProvider({ children, position = "bottom-right", duration = 4e3, ma
1731
1970
  return /* @__PURE__ */ jsxs(ToastContext.Provider, { value: api, children: [
1732
1971
  children,
1733
1972
  mounted && createPortal(
1734
- /* @__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)) }),
1973
+ // One stack per position in use, so toasts can override the provider's position.
1974
+ [.../* @__PURE__ */ new Set([position, ...toasts.map((t) => t.position ?? position)])].map((spot) => /* @__PURE__ */ jsx("ol", { className: cn("ru-toaster", `ru-toaster--${spot}`), "aria-live": "polite", "aria-label": "Notifications", children: toasts.filter((t) => (t.position ?? position) === spot).map((t) => /* @__PURE__ */ jsx(
1975
+ ToastView,
1976
+ {
1977
+ toast: t,
1978
+ duration: t.duration ?? duration,
1979
+ appearance: t.appearance ?? appearance,
1980
+ animation: t.animation ?? animation,
1981
+ progress: t.progress ?? progress,
1982
+ onDismiss: dismiss
1983
+ },
1984
+ t.id
1985
+ )) }, spot)),
1735
1986
  document.body
1736
1987
  )
1737
1988
  ] });
1738
1989
  }
1739
- function ToastView({ toast, duration, onDismiss }) {
1990
+ function ToastView({ toast, duration, appearance, animation, progress, onDismiss }) {
1740
1991
  const [paused, setPaused] = useState(false);
1741
- const { id, variant = "default", title, description, action } = toast;
1992
+ const { id, variant = "default", title, description, action, closing } = toast;
1993
+ const ref = useRef(null);
1994
+ const remaining = useRef(duration);
1995
+ const timed = Number.isFinite(duration) && duration > 0;
1742
1996
  useEffect(() => {
1743
- if (paused || !Number.isFinite(duration) || duration <= 0) return;
1744
- const timer = setTimeout(() => onDismiss(id), duration);
1745
- return () => clearTimeout(timer);
1746
- }, [id, duration, paused, onDismiss]);
1997
+ remaining.current = duration;
1998
+ }, [id, duration]);
1999
+ useEffect(() => {
2000
+ if (paused || closing || !timed) return void 0;
2001
+ const startedAt = Date.now();
2002
+ const timer = setTimeout(() => onDismiss(id), remaining.current);
2003
+ return () => {
2004
+ clearTimeout(timer);
2005
+ remaining.current -= Date.now() - startedAt;
2006
+ };
2007
+ }, [id, duration, paused, closing, timed, onDismiss]);
2008
+ useLayoutEffect(() => {
2009
+ if (closing && ref.current) ref.current.style.setProperty("--_h", `${ref.current.offsetHeight}px`);
2010
+ }, [closing]);
1747
2011
  const Icon = variant === "default" ? null : statusIcons[variant];
2012
+ const countdown = progress && timed;
2013
+ const traced = countdown && appearance === "accent";
2014
+ const countdownStyle = { animationDuration: `${duration}ms`, animationPlayState: paused ? "paused" : "running" };
1748
2015
  return /* @__PURE__ */ jsxs(
1749
2016
  "li",
1750
2017
  {
2018
+ ref,
1751
2019
  role: "status",
1752
- className: cn("ru-toast", variant !== "default" && `ru-c-${variant}`),
2020
+ className: cn(
2021
+ "ru-toast",
2022
+ `ru-toast--${appearance}`,
2023
+ `ru-toast--in-${animation}`,
2024
+ traced && "ru-toast--traced",
2025
+ closing && "ru-toast--closing",
2026
+ variant !== "default" && `ru-c-${variant}`
2027
+ ),
1753
2028
  onMouseEnter: () => setPaused(true),
1754
2029
  onMouseLeave: () => setPaused(false),
1755
2030
  children: [
@@ -1759,7 +2034,9 @@ function ToastView({ toast, duration, onDismiss }) {
1759
2034
  description && /* @__PURE__ */ jsx("div", { className: "ru-toast__desc", children: description }),
1760
2035
  action && /* @__PURE__ */ jsx("div", { className: "ru-toast__action", children: action })
1761
2036
  ] }),
1762
- /* @__PURE__ */ jsx(CloseButton, { onClick: () => onDismiss(id), label: "Dismiss notification" })
2037
+ /* @__PURE__ */ jsx(CloseButton, { onClick: () => onDismiss(id), label: "Dismiss notification" }),
2038
+ traced && /* @__PURE__ */ jsx("svg", { className: "ru-toast__trace", "aria-hidden": "true", children: /* @__PURE__ */ jsx("rect", { width: "100%", height: "100%", rx: "13", pathLength: "1", style: countdownStyle }) }),
2039
+ countdown && !traced && /* @__PURE__ */ jsx("span", { className: "ru-toast__progress", "aria-hidden": "true", style: countdownStyle })
1763
2040
  ]
1764
2041
  }
1765
2042
  );
@@ -1860,7 +2137,18 @@ function SpotlightCard({ color, className, style, onMouseMove, children, ...rest
1860
2137
  }
1861
2138
  );
1862
2139
  }
1863
- const semanticColors$1 = /* @__PURE__ */ new Set(["primary", "secondary", "success", "danger", "warning", "info"]);
2140
+ function DotText({ as: Tag = "span", dot = 4, color, glow = false, className, style, children, ...rest }) {
2141
+ return /* @__PURE__ */ jsx(
2142
+ Tag,
2143
+ {
2144
+ className: cn("ru-dot-text", glow && "ru-dot-text--glow", className),
2145
+ style: { "--_d": `${dot}px`, ...color && { color }, ...style },
2146
+ ...rest,
2147
+ children
2148
+ }
2149
+ );
2150
+ }
2151
+ const semanticColors$4 = /* @__PURE__ */ new Set(["primary", "secondary", "success", "danger", "warning", "info"]);
1864
2152
  function Pager({ onPrev, onNext, prevLabel, nextLabel }) {
1865
2153
  if (!onPrev && !onNext) return null;
1866
2154
  return /* @__PURE__ */ jsxs("div", { className: "ru-frame-card__pager", children: [
@@ -1898,7 +2186,7 @@ function FrameCard({
1898
2186
  {
1899
2187
  className: cn("ru-frame-card", onFrame && "ru-frame-card--title-frame", className),
1900
2188
  style: {
1901
- "--_fc": semanticColors$1.has(color) ? `var(--ru-${color})` : color,
2189
+ "--_fc": semanticColors$4.has(color) ? `var(--ru-${color})` : color,
1902
2190
  ...textColor && { "--_fc-on": textColor, "--_fc-ink": textColor },
1903
2191
  ...style
1904
2192
  },
@@ -1937,7 +2225,7 @@ function FrameCard({
1937
2225
  }
1938
2226
  const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
1939
2227
  const WEEKDAYS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
1940
- const semanticColors = /* @__PURE__ */ new Set(["primary", "secondary", "success", "danger", "warning", "info"]);
2228
+ const semanticColors$3 = /* @__PURE__ */ new Set(["primary", "secondary", "success", "danger", "warning", "info"]);
1941
2229
  const dayKey = (d) => d.getFullYear() * 1e4 + d.getMonth() * 100 + d.getDate();
1942
2230
  const sameDay = (a, b) => Boolean(a && b) && dayKey(a) === dayKey(b);
1943
2231
  function Calendar({
@@ -1997,7 +2285,7 @@ function Calendar({
1997
2285
  next.focus();
1998
2286
  }
1999
2287
  };
2000
- const ink = color ? semanticColors.has(color) ? `var(--ru-${color})` : color : void 0;
2288
+ const ink = color ? semanticColors$3.has(color) ? `var(--ru-${color})` : color : void 0;
2001
2289
  return /* @__PURE__ */ jsxs("div", { className: cn("ru-calendar", className), style: ink ? { "--_cc": ink, ...style } : style, ...rest, children: [
2002
2290
  /* @__PURE__ */ jsxs("div", { className: "ru-calendar__head", children: [
2003
2291
  /* @__PURE__ */ jsx("button", { type: "button", className: "ru-calendar__nav", "aria-label": "Previous month", onClick: () => shift(-1), children: /* @__PURE__ */ jsx(ChevronLeftIcon, {}) }),
@@ -2052,6 +2340,638 @@ function Calendar({
2052
2340
  }) })
2053
2341
  ] });
2054
2342
  }
2343
+ const normalize = (options) => options.map((o) => typeof o === "object" ? o : { label: String(o), value: o });
2344
+ function SegmentedControl({
2345
+ options,
2346
+ value,
2347
+ defaultValue,
2348
+ onChange,
2349
+ size = "md",
2350
+ fullWidth = false,
2351
+ pill = false,
2352
+ disabled = false,
2353
+ className,
2354
+ style,
2355
+ ...rest
2356
+ }) {
2357
+ var _a;
2358
+ const items = normalize(options);
2359
+ const firstEnabled = (_a = items.find((i) => !i.disabled)) == null ? void 0 : _a.value;
2360
+ const [active, setActive] = useControllableState(value, defaultValue ?? firstEnabled, onChange);
2361
+ const rootRef = useRef(null);
2362
+ const [thumb, setThumb] = useState(null);
2363
+ const activeIndex = items.findIndex((i) => i.value === active);
2364
+ useEffect(() => {
2365
+ const root = rootRef.current;
2366
+ if (!root) return void 0;
2367
+ const measure = () => {
2368
+ const el = root.querySelectorAll('[role="radio"]')[activeIndex];
2369
+ setThumb(el ? { x: el.offsetLeft, w: el.offsetWidth } : null);
2370
+ };
2371
+ measure();
2372
+ if (typeof ResizeObserver === "undefined") return void 0;
2373
+ const ro = new ResizeObserver(measure);
2374
+ ro.observe(root);
2375
+ return () => ro.disconnect();
2376
+ }, [activeIndex, items.length]);
2377
+ const move = (from, step) => {
2378
+ var _a2;
2379
+ const buttons = rootRef.current.querySelectorAll('[role="radio"]');
2380
+ for (let n = 1; n <= items.length; n++) {
2381
+ const i = (from + step * n + items.length) % items.length;
2382
+ if (!items[i].disabled) {
2383
+ setActive(items[i].value);
2384
+ (_a2 = buttons[i]) == null ? void 0 : _a2.focus();
2385
+ return;
2386
+ }
2387
+ }
2388
+ };
2389
+ const onKeyDown = (e, index) => {
2390
+ if (e.key === "ArrowRight" || e.key === "ArrowDown") move(index, 1);
2391
+ else if (e.key === "ArrowLeft" || e.key === "ArrowUp") move(index, -1);
2392
+ else return;
2393
+ e.preventDefault();
2394
+ };
2395
+ return /* @__PURE__ */ jsxs(
2396
+ "div",
2397
+ {
2398
+ ref: rootRef,
2399
+ role: "radiogroup",
2400
+ "aria-disabled": disabled || void 0,
2401
+ className: cn(
2402
+ "ru-segmented",
2403
+ `ru-segmented--${size}`,
2404
+ fullWidth && "ru-segmented--full",
2405
+ pill && "ru-segmented--pill",
2406
+ thumb && "ru-segmented--ready",
2407
+ className
2408
+ ),
2409
+ style: thumb ? { "--_x": `${thumb.x}px`, "--_w": `${thumb.w}px`, ...style } : style,
2410
+ ...rest,
2411
+ children: [
2412
+ thumb && activeIndex >= 0 && /* @__PURE__ */ jsx("span", { className: "ru-segmented__thumb", "aria-hidden": "true" }),
2413
+ items.map((item, i) => {
2414
+ const selected = i === activeIndex;
2415
+ return /* @__PURE__ */ jsxs(
2416
+ "button",
2417
+ {
2418
+ type: "button",
2419
+ role: "radio",
2420
+ "aria-checked": selected,
2421
+ tabIndex: selected || activeIndex < 0 && i === 0 ? 0 : -1,
2422
+ disabled: disabled || item.disabled,
2423
+ className: "ru-segmented__item",
2424
+ onClick: () => setActive(item.value),
2425
+ onKeyDown: (e) => onKeyDown(e, i),
2426
+ children: [
2427
+ item.icon,
2428
+ item.label
2429
+ ]
2430
+ },
2431
+ String(item.value)
2432
+ );
2433
+ })
2434
+ ]
2435
+ }
2436
+ );
2437
+ }
2438
+ const semanticColors$2 = /* @__PURE__ */ new Set(["primary", "secondary", "success", "danger", "warning", "info"]);
2439
+ function ProgressRing({
2440
+ value = 0,
2441
+ max = 100,
2442
+ size = 48,
2443
+ thickness = 4,
2444
+ color = "primary",
2445
+ trackColor,
2446
+ showValue = false,
2447
+ label,
2448
+ className,
2449
+ style,
2450
+ children,
2451
+ ...rest
2452
+ }) {
2453
+ const percent = Math.min(100, Math.max(0, value / max * 100));
2454
+ const r = (size - thickness) / 2;
2455
+ const circumference = 2 * Math.PI * r;
2456
+ const ink = semanticColors$2.has(color) ? `var(--ru-${color})` : color;
2457
+ return /* @__PURE__ */ jsxs(
2458
+ "div",
2459
+ {
2460
+ role: "progressbar",
2461
+ "aria-valuemin": 0,
2462
+ "aria-valuemax": max,
2463
+ "aria-valuenow": value,
2464
+ "aria-label": label,
2465
+ className: cn("ru-ring", className),
2466
+ style: { width: size, height: size, "--_c": ink, ...trackColor && { "--_track": trackColor }, ...style },
2467
+ ...rest,
2468
+ children: [
2469
+ /* @__PURE__ */ jsxs("svg", { width: size, height: size, viewBox: `0 0 ${size} ${size}`, "aria-hidden": "true", children: [
2470
+ /* @__PURE__ */ jsx("circle", { className: "ru-ring__track", cx: size / 2, cy: size / 2, r, strokeWidth: thickness }),
2471
+ /* @__PURE__ */ jsx(
2472
+ "circle",
2473
+ {
2474
+ className: "ru-ring__bar",
2475
+ cx: size / 2,
2476
+ cy: size / 2,
2477
+ r,
2478
+ strokeWidth: thickness,
2479
+ strokeDasharray: circumference,
2480
+ strokeDashoffset: circumference * (1 - percent / 100)
2481
+ }
2482
+ )
2483
+ ] }),
2484
+ (children !== void 0 || showValue) && /* @__PURE__ */ jsx("span", { className: "ru-ring__label", style: { fontSize: Math.max(10, Math.round(size * 0.24)) }, children: children ?? `${Math.round(percent)}%` })
2485
+ ]
2486
+ }
2487
+ );
2488
+ }
2489
+ const semanticColors$1 = /* @__PURE__ */ new Set(["primary", "secondary", "success", "danger", "warning", "info"]);
2490
+ function Stat({
2491
+ label,
2492
+ value,
2493
+ trend,
2494
+ trendLabel,
2495
+ invertTrend = false,
2496
+ helpText,
2497
+ size = "md",
2498
+ align = "start",
2499
+ className,
2500
+ ...rest
2501
+ }) {
2502
+ const hasTrend = typeof trend === "number";
2503
+ const up = hasTrend && trend >= 0;
2504
+ const good = up !== invertTrend;
2505
+ const TrendIcon = up ? TrendUpIcon : TrendDownIcon;
2506
+ return /* @__PURE__ */ jsxs("div", { className: cn("ru-stat", `ru-stat--${size}`, align === "end" && "ru-stat--end", className), ...rest, children: [
2507
+ /* @__PURE__ */ jsxs("div", { className: "ru-stat__top", children: [
2508
+ label && /* @__PURE__ */ jsx("span", { className: "ru-stat__label", children: label }),
2509
+ hasTrend && /* @__PURE__ */ jsxs("span", { className: cn("ru-stat__trend", good ? "ru-stat__trend--good" : "ru-stat__trend--bad"), children: [
2510
+ /* @__PURE__ */ jsx("span", { className: "ru-stat__trend-icon", children: /* @__PURE__ */ jsx(TrendIcon, {}) }),
2511
+ trendLabel ?? `${Math.abs(trend).toLocaleString()}%`,
2512
+ /* @__PURE__ */ jsx("span", { className: "ru-sr-only", children: up ? " increase" : " decrease" })
2513
+ ] })
2514
+ ] }),
2515
+ /* @__PURE__ */ jsx("div", { className: "ru-stat__value", children: value }),
2516
+ helpText && /* @__PURE__ */ jsx("div", { className: "ru-stat__help", children: helpText })
2517
+ ] });
2518
+ }
2519
+ function DateTile({ date, color = "primary", size = "md", className, style, ...rest }) {
2520
+ const d = date instanceof Date ? date : new Date(date);
2521
+ const ink = semanticColors$1.has(color) ? `var(--ru-${color})` : color;
2522
+ return /* @__PURE__ */ jsxs(
2523
+ "time",
2524
+ {
2525
+ dateTime: d.toISOString().slice(0, 10),
2526
+ "aria-label": d.toLocaleDateString("en-US", { weekday: "long", month: "long", day: "numeric", year: "numeric" }),
2527
+ className: cn("ru-date-tile", `ru-date-tile--${size}`, className),
2528
+ style: { "--_c": ink, ...style },
2529
+ ...rest,
2530
+ children: [
2531
+ /* @__PURE__ */ jsx("span", { className: "ru-date-tile__month", "aria-hidden": "true", children: d.toLocaleDateString("en-US", { month: "short" }) }),
2532
+ /* @__PURE__ */ jsx("span", { className: "ru-date-tile__day", "aria-hidden": "true", children: d.getDate() })
2533
+ ]
2534
+ }
2535
+ );
2536
+ }
2537
+ function matchesAccept(file, accept) {
2538
+ if (!accept) return true;
2539
+ return accept.split(",").some((rule) => {
2540
+ const r = rule.trim().toLowerCase();
2541
+ if (!r) return false;
2542
+ if (r.startsWith(".")) return file.name.toLowerCase().endsWith(r);
2543
+ if (r.endsWith("/*")) return file.type.toLowerCase().startsWith(r.slice(0, -1));
2544
+ return file.type.toLowerCase() === r;
2545
+ });
2546
+ }
2547
+ function formatBytes(bytes) {
2548
+ if (typeof bytes !== "number") return bytes;
2549
+ if (bytes < 1024) return `${bytes} B`;
2550
+ const units = ["KB", "MB", "GB"];
2551
+ let n = bytes / 1024;
2552
+ let i = 0;
2553
+ while (n >= 1024 && i < units.length - 1) {
2554
+ n /= 1024;
2555
+ i++;
2556
+ }
2557
+ return `${Number(n.toFixed(1))}${units[i]}`;
2558
+ }
2559
+ function Dropzone({
2560
+ onFiles,
2561
+ onReject,
2562
+ accept,
2563
+ multiple = true,
2564
+ maxSize,
2565
+ disabled = false,
2566
+ icon,
2567
+ title = "Drag & drop files here",
2568
+ description,
2569
+ className,
2570
+ children,
2571
+ ...rest
2572
+ }) {
2573
+ const inputRef = useRef(null);
2574
+ const [dragging, setDragging] = useState(false);
2575
+ const handle = (list) => {
2576
+ const files = Array.from(list ?? []);
2577
+ const accepted = [];
2578
+ const rejected = [];
2579
+ for (const file of multiple ? files : files.slice(0, 1)) {
2580
+ if (!matchesAccept(file, accept)) rejected.push({ file, reason: "type" });
2581
+ else if (maxSize && file.size > maxSize) rejected.push({ file, reason: "size" });
2582
+ else accepted.push(file);
2583
+ }
2584
+ if (accepted.length) onFiles == null ? void 0 : onFiles(accepted);
2585
+ if (rejected.length) onReject == null ? void 0 : onReject(rejected);
2586
+ };
2587
+ const open = () => {
2588
+ var _a;
2589
+ return !disabled && ((_a = inputRef.current) == null ? void 0 : _a.click());
2590
+ };
2591
+ return /* @__PURE__ */ jsxs(
2592
+ "div",
2593
+ {
2594
+ role: "button",
2595
+ tabIndex: disabled ? -1 : 0,
2596
+ "aria-disabled": disabled || void 0,
2597
+ className: cn("ru-dropzone", dragging && "ru-dropzone--active", disabled && "ru-dropzone--disabled", className),
2598
+ onClick: open,
2599
+ onKeyDown: (e) => {
2600
+ if (e.key === "Enter" || e.key === " ") {
2601
+ e.preventDefault();
2602
+ open();
2603
+ }
2604
+ },
2605
+ onDragEnter: (e) => {
2606
+ e.preventDefault();
2607
+ if (!disabled) setDragging(true);
2608
+ },
2609
+ onDragOver: (e) => e.preventDefault(),
2610
+ onDragLeave: (e) => {
2611
+ if (!e.currentTarget.contains(e.relatedTarget)) setDragging(false);
2612
+ },
2613
+ onDrop: (e) => {
2614
+ e.preventDefault();
2615
+ setDragging(false);
2616
+ if (!disabled) handle(e.dataTransfer.files);
2617
+ },
2618
+ ...rest,
2619
+ children: [
2620
+ /* @__PURE__ */ jsx(
2621
+ "input",
2622
+ {
2623
+ ref: inputRef,
2624
+ type: "file",
2625
+ hidden: true,
2626
+ accept,
2627
+ multiple,
2628
+ disabled,
2629
+ onChange: (e) => {
2630
+ handle(e.target.files);
2631
+ e.target.value = "";
2632
+ }
2633
+ }
2634
+ ),
2635
+ children ?? /* @__PURE__ */ jsxs(Fragment, { children: [
2636
+ /* @__PURE__ */ jsx("span", { className: "ru-dropzone__icon", children: icon ?? /* @__PURE__ */ jsx(UploadCloudIcon, {}) }),
2637
+ /* @__PURE__ */ jsx("span", { className: "ru-dropzone__title", children: title }),
2638
+ /* @__PURE__ */ jsx("span", { className: "ru-dropzone__desc", children: description ?? /* @__PURE__ */ jsxs(Fragment, { children: [
2639
+ "or ",
2640
+ /* @__PURE__ */ jsx("span", { className: "ru-dropzone__browse", children: "browse" }),
2641
+ " to choose files",
2642
+ maxSize ? ` (max ${formatBytes(maxSize)})` : ""
2643
+ ] }) })
2644
+ ] })
2645
+ ]
2646
+ }
2647
+ );
2648
+ }
2649
+ const statusText = { uploading: "Uploading…", complete: "Completed", error: "Failed" };
2650
+ function FileItem({
2651
+ name,
2652
+ size,
2653
+ status = "complete",
2654
+ progress = 0,
2655
+ icon,
2656
+ error,
2657
+ onRemove,
2658
+ action,
2659
+ removeLabel,
2660
+ className,
2661
+ ...rest
2662
+ }) {
2663
+ const uploading = status === "uploading";
2664
+ return /* @__PURE__ */ jsxs("div", { className: cn("ru-file-item", `ru-file-item--${status}`, className), ...rest, children: [
2665
+ /* @__PURE__ */ jsx("span", { className: "ru-file-item__icon", children: icon ?? /* @__PURE__ */ jsx(FileIcon, {}) }),
2666
+ /* @__PURE__ */ jsxs("div", { className: "ru-file-item__body", children: [
2667
+ /* @__PURE__ */ jsx("div", { className: "ru-file-item__name", title: name, children: name }),
2668
+ /* @__PURE__ */ jsxs("div", { className: "ru-file-item__meta", children: [
2669
+ size !== void 0 && /* @__PURE__ */ jsx("span", { children: formatBytes(size) }),
2670
+ size !== void 0 && /* @__PURE__ */ jsx("span", { className: "ru-file-item__dot", "aria-hidden": "true" }),
2671
+ /* @__PURE__ */ jsx("span", { className: "ru-file-item__status", children: status === "error" && error ? error : statusText[status] })
2672
+ ] }),
2673
+ uploading && /* @__PURE__ */ jsx(
2674
+ "div",
2675
+ {
2676
+ className: "ru-file-item__bar",
2677
+ role: "progressbar",
2678
+ "aria-valuemin": 0,
2679
+ "aria-valuemax": 100,
2680
+ "aria-valuenow": progress,
2681
+ "aria-label": `Uploading ${name}`,
2682
+ children: /* @__PURE__ */ jsx("span", { style: { width: `${progress}%` } })
2683
+ }
2684
+ )
2685
+ ] }),
2686
+ uploading && /* @__PURE__ */ jsxs("span", { className: "ru-file-item__percent", children: [
2687
+ Math.round(progress),
2688
+ "%"
2689
+ ] }),
2690
+ action,
2691
+ onRemove && /* @__PURE__ */ jsx("button", { type: "button", className: "ru-file-item__remove", "aria-label": removeLabel ?? `Remove ${name}`, onClick: onRemove, children: /* @__PURE__ */ jsx(CloseIcon, {}) })
2692
+ ] });
2693
+ }
2694
+ const UndoIcon = () => /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
2695
+ /* @__PURE__ */ jsx("path", { d: "M9 14 4 9l5-5" }),
2696
+ /* @__PURE__ */ jsx("path", { d: "M4 9h10.5a5.5 5.5 0 0 1 0 11H11" })
2697
+ ] });
2698
+ const EraserIcon = () => /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
2699
+ /* @__PURE__ */ jsx("path", { d: "m7 21-4.3-4.3a1 1 0 0 1 0-1.4l10-10a1 1 0 0 1 1.4 0l5.6 5.6a1 1 0 0 1 0 1.4L11 21" }),
2700
+ /* @__PURE__ */ jsx("path", { d: "M22 21H7M5 11l9 9" })
2701
+ ] });
2702
+ const SignaturePad = forwardRef(function SignaturePad2({
2703
+ onChange,
2704
+ strokeColor,
2705
+ strokeWidth = 2.5,
2706
+ height = 180,
2707
+ placeholder = "Sign here",
2708
+ showControls = true,
2709
+ undoLabel = "Undo",
2710
+ clearLabel = "Clear",
2711
+ actions,
2712
+ disabled = false,
2713
+ className,
2714
+ style,
2715
+ ...rest
2716
+ }, ref) {
2717
+ const canvasRef = useRef(null);
2718
+ const strokes = useRef([]);
2719
+ const drawing = useRef(null);
2720
+ const [count, setCount] = useState(0);
2721
+ const ink = useCallback(() => strokeColor ?? getComputedStyle(canvasRef.current).color, [strokeColor]);
2722
+ const drawStroke = useCallback(
2723
+ (ctx, points) => {
2724
+ ctx.strokeStyle = ink();
2725
+ ctx.fillStyle = ctx.strokeStyle;
2726
+ if (points.length === 1) {
2727
+ ctx.beginPath();
2728
+ ctx.arc(points[0].x, points[0].y, strokeWidth / 2, 0, Math.PI * 2);
2729
+ ctx.fill();
2730
+ return;
2731
+ }
2732
+ for (let i = 1; i < points.length; i++) {
2733
+ const a = points[i - 1];
2734
+ const b = points[i];
2735
+ const mid = { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
2736
+ ctx.lineWidth = b.w;
2737
+ ctx.beginPath();
2738
+ ctx.moveTo(a.mx ?? a.x, a.my ?? a.y);
2739
+ ctx.quadraticCurveTo(a.x, a.y, mid.x, mid.y);
2740
+ ctx.stroke();
2741
+ b.mx = mid.x;
2742
+ b.my = mid.y;
2743
+ }
2744
+ },
2745
+ [ink, strokeWidth]
2746
+ );
2747
+ const redraw = useCallback(() => {
2748
+ const canvas = canvasRef.current;
2749
+ if (!canvas) return;
2750
+ const ctx = canvas.getContext("2d");
2751
+ if (!ctx) return;
2752
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
2753
+ ctx.lineCap = "round";
2754
+ ctx.lineJoin = "round";
2755
+ strokes.current.forEach((s) => drawStroke(ctx, s.map(({ x, y, w }) => ({ x, y, w }))));
2756
+ }, [drawStroke]);
2757
+ useEffect(() => {
2758
+ const canvas = canvasRef.current;
2759
+ if (!canvas) return void 0;
2760
+ const fit = () => {
2761
+ var _a;
2762
+ const dpr = window.devicePixelRatio || 1;
2763
+ const { width, height: h } = canvas.getBoundingClientRect();
2764
+ canvas.width = Math.max(1, Math.round(width * dpr));
2765
+ canvas.height = Math.max(1, Math.round(h * dpr));
2766
+ (_a = canvas.getContext("2d")) == null ? void 0 : _a.setTransform(dpr, 0, 0, dpr, 0, 0);
2767
+ redraw();
2768
+ };
2769
+ fit();
2770
+ if (typeof ResizeObserver === "undefined") return void 0;
2771
+ const ro = new ResizeObserver(fit);
2772
+ ro.observe(canvas);
2773
+ return () => ro.disconnect();
2774
+ }, [redraw]);
2775
+ const emit = () => onChange == null ? void 0 : onChange(strokes.current.length ? canvasRef.current.toDataURL("image/png") : null);
2776
+ const point = (e) => {
2777
+ const rect = canvasRef.current.getBoundingClientRect();
2778
+ return { x: e.clientX - rect.left, y: e.clientY - rect.top, t: e.timeStamp };
2779
+ };
2780
+ const onPointerDown = (e) => {
2781
+ if (disabled || e.pointerType === "mouse" && e.button !== 0) return;
2782
+ e.currentTarget.setPointerCapture(e.pointerId);
2783
+ const p = { ...point(e), w: strokeWidth };
2784
+ drawing.current = [p];
2785
+ const ctx = canvasRef.current.getContext("2d");
2786
+ ctx.lineCap = "round";
2787
+ ctx.lineJoin = "round";
2788
+ drawStroke(ctx, [p]);
2789
+ };
2790
+ const onPointerMove = (e) => {
2791
+ const stroke = drawing.current;
2792
+ if (!stroke) return;
2793
+ const prev = stroke[stroke.length - 1];
2794
+ const p = point(e);
2795
+ const dist = Math.hypot(p.x - prev.x, p.y - prev.y);
2796
+ if (dist < 1.5) return;
2797
+ const speed = dist / Math.max(1, p.t - prev.t);
2798
+ const target = strokeWidth * Math.max(0.45, Math.min(1.35, 1.4 - speed * 0.45));
2799
+ p.w = prev.w * 0.6 + target * 0.4;
2800
+ stroke.push(p);
2801
+ drawStroke(canvasRef.current.getContext("2d"), stroke.slice(-3));
2802
+ };
2803
+ const onPointerUp = () => {
2804
+ if (!drawing.current) return;
2805
+ strokes.current = [...strokes.current, drawing.current];
2806
+ drawing.current = null;
2807
+ setCount(strokes.current.length);
2808
+ emit();
2809
+ };
2810
+ const undo = () => {
2811
+ strokes.current = strokes.current.slice(0, -1);
2812
+ setCount(strokes.current.length);
2813
+ redraw();
2814
+ emit();
2815
+ };
2816
+ const clear = () => {
2817
+ strokes.current = [];
2818
+ setCount(0);
2819
+ redraw();
2820
+ emit();
2821
+ };
2822
+ useImperativeHandle(ref, () => ({
2823
+ clear,
2824
+ undo,
2825
+ isEmpty: () => strokes.current.length === 0,
2826
+ toDataURL: (type = "image/png") => canvasRef.current.toDataURL(type)
2827
+ }));
2828
+ return /* @__PURE__ */ jsxs("div", { className: cn("ru-signature", disabled && "ru-signature--disabled", className), style, ...rest, children: [
2829
+ /* @__PURE__ */ jsxs("div", { className: "ru-signature__canvas-wrap", style: { height }, children: [
2830
+ count === 0 && placeholder && /* @__PURE__ */ jsx("span", { className: "ru-signature__placeholder", "aria-hidden": "true", children: placeholder }),
2831
+ /* @__PURE__ */ jsx(
2832
+ "canvas",
2833
+ {
2834
+ ref: canvasRef,
2835
+ className: "ru-signature__canvas",
2836
+ role: "img",
2837
+ "aria-label": count ? "Signature drawn" : "Signature pad, empty",
2838
+ onPointerDown,
2839
+ onPointerMove,
2840
+ onPointerUp,
2841
+ onPointerCancel: onPointerUp
2842
+ }
2843
+ ),
2844
+ /* @__PURE__ */ jsx("span", { className: "ru-signature__baseline", "aria-hidden": "true" })
2845
+ ] }),
2846
+ (showControls || actions) && /* @__PURE__ */ jsxs("div", { className: "ru-signature__bar", children: [
2847
+ showControls && /* @__PURE__ */ jsxs(Fragment, { children: [
2848
+ /* @__PURE__ */ jsxs("button", { type: "button", className: "ru-signature__btn", onClick: undo, disabled: disabled || count === 0, children: [
2849
+ /* @__PURE__ */ jsx(UndoIcon, {}),
2850
+ undoLabel
2851
+ ] }),
2852
+ /* @__PURE__ */ jsx("span", { className: "ru-signature__sep", "aria-hidden": "true" }),
2853
+ /* @__PURE__ */ jsxs("button", { type: "button", className: "ru-signature__btn", onClick: clear, disabled: disabled || count === 0, children: [
2854
+ /* @__PURE__ */ jsx(EraserIcon, {}),
2855
+ clearLabel
2856
+ ] })
2857
+ ] }),
2858
+ actions && /* @__PURE__ */ jsx("div", { className: "ru-signature__actions", children: actions })
2859
+ ] })
2860
+ ] });
2861
+ });
2862
+ const statusLabels = {
2863
+ running: "In progress",
2864
+ queued: "Queued",
2865
+ pending: "Not started",
2866
+ partial: "Partly done",
2867
+ complete: "Complete",
2868
+ error: "Failed"
2869
+ };
2870
+ function StatusIcon({ status = "pending", progress = 50, size = 20, label, className, style, ...rest }) {
2871
+ const r = 8;
2872
+ const c = 2 * Math.PI * r;
2873
+ return /* @__PURE__ */ jsx(
2874
+ "span",
2875
+ {
2876
+ role: "img",
2877
+ "aria-label": label ?? statusLabels[status] ?? status,
2878
+ className: cn("ru-status-icon", `ru-status-icon--${status}`, className),
2879
+ style: { width: size, height: size, ...style },
2880
+ ...rest,
2881
+ children: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 20 20", width: size, height: size, "aria-hidden": "true", children: status === "complete" || status === "error" ? /* @__PURE__ */ jsxs(Fragment, { children: [
2882
+ /* @__PURE__ */ jsx("circle", { cx: "10", cy: "10", r: "10", className: "ru-status-icon__fill" }),
2883
+ status === "complete" ? /* @__PURE__ */ jsx("path", { d: "m6 10.2 2.6 2.6L14 7.4", className: "ru-status-icon__glyph" }) : /* @__PURE__ */ jsx("path", { d: "m7 7 6 6M13 7l-6 6", className: "ru-status-icon__glyph" })
2884
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
2885
+ /* @__PURE__ */ jsx(
2886
+ "circle",
2887
+ {
2888
+ cx: "10",
2889
+ cy: "10",
2890
+ r,
2891
+ className: "ru-status-icon__ring",
2892
+ strokeDasharray: status === "running" || status === "queued" ? "2.6 2.4" : void 0
2893
+ }
2894
+ ),
2895
+ status === "partial" && /* @__PURE__ */ jsx(
2896
+ "circle",
2897
+ {
2898
+ cx: "10",
2899
+ cy: "10",
2900
+ r,
2901
+ className: "ru-status-icon__arc",
2902
+ strokeDasharray: c,
2903
+ strokeDashoffset: c * (1 - Math.min(100, Math.max(0, progress)) / 100)
2904
+ }
2905
+ )
2906
+ ] }) })
2907
+ }
2908
+ );
2909
+ }
2910
+ function StepList({ items, connector = true, dividers = true, className, ...rest }) {
2911
+ return /* @__PURE__ */ jsx("ol", { className: cn("ru-step-list", !dividers && "ru-step-list--plain", className), ...rest, children: items.map((item, i) => /* @__PURE__ */ jsxs(
2912
+ "li",
2913
+ {
2914
+ className: cn("ru-step-list__item", `ru-step-list__item--${item.status ?? "pending"}`),
2915
+ "aria-current": item.status === "running" ? "step" : void 0,
2916
+ children: [
2917
+ /* @__PURE__ */ jsxs("span", { className: "ru-step-list__rail", children: [
2918
+ /* @__PURE__ */ jsx("span", { className: "ru-step-list__icon", children: /* @__PURE__ */ jsx(StatusIcon, { status: item.status, progress: item.progress, size: 22 }) }),
2919
+ connector && i < items.length - 1 && /* @__PURE__ */ jsx("span", { className: "ru-step-list__line", "aria-hidden": "true" })
2920
+ ] }),
2921
+ /* @__PURE__ */ jsxs("div", { className: "ru-step-list__body", children: [
2922
+ /* @__PURE__ */ jsx("div", { className: "ru-step-list__title", children: item.title }),
2923
+ item.description && /* @__PURE__ */ jsx("div", { className: "ru-step-list__desc", children: item.description })
2924
+ ] }),
2925
+ item.meta && /* @__PURE__ */ jsx("div", { className: "ru-step-list__meta", children: item.meta })
2926
+ ]
2927
+ },
2928
+ item.key ?? i
2929
+ )) });
2930
+ }
2931
+ function StackedProgress({ segments, max, size = "md", showLegend = true, gap = 4, label, className, ...rest }) {
2932
+ const total = max ?? segments.reduce((sum, s) => sum + s.value, 0);
2933
+ const summary = segments.map((s) => `${s.label} ${Math.round(s.value / total * 100)}%`).join(", ");
2934
+ return /* @__PURE__ */ jsxs("div", { className: cn("ru-stacked", `ru-stacked--${size}`, className), ...rest, children: [
2935
+ (label || showLegend) && /* @__PURE__ */ jsxs("div", { className: "ru-stacked__head", children: [
2936
+ label && /* @__PURE__ */ jsx("span", { className: "ru-stacked__label", children: label }),
2937
+ showLegend && /* @__PURE__ */ jsx("ul", { className: "ru-stacked__legend", children: segments.map((s) => /* @__PURE__ */ jsxs("li", { children: [
2938
+ /* @__PURE__ */ jsx("span", { className: "ru-stacked__dot", style: { background: s.color } }),
2939
+ s.label
2940
+ ] }, s.label)) })
2941
+ ] }),
2942
+ /* @__PURE__ */ jsxs("div", { className: "ru-stacked__bar", role: "img", "aria-label": summary, style: { gap }, children: [
2943
+ segments.map((s) => /* @__PURE__ */ jsx("span", { className: "ru-stacked__seg", title: `${s.label}: ${s.value}`, style: { flexGrow: s.value, background: s.color } }, s.label)),
2944
+ max !== void 0 && total > segments.reduce((sum, s) => sum + s.value, 0) && /* @__PURE__ */ jsx("span", { className: "ru-stacked__rest", style: { flexGrow: total - segments.reduce((sum, s) => sum + s.value, 0) } })
2945
+ ] })
2946
+ ] });
2947
+ }
2948
+ const semanticColors = /* @__PURE__ */ new Set(["primary", "secondary", "success", "danger", "warning", "info"]);
2949
+ function IconTile({
2950
+ color = "primary",
2951
+ size = 48,
2952
+ radius,
2953
+ badge,
2954
+ glow = true,
2955
+ label,
2956
+ className,
2957
+ style,
2958
+ children,
2959
+ ...rest
2960
+ }) {
2961
+ const ink = semanticColors.has(color) ? `var(--ru-${color})` : color;
2962
+ return /* @__PURE__ */ jsxs(
2963
+ "span",
2964
+ {
2965
+ className: cn("ru-icon-tile", glow && "ru-icon-tile--glow", className),
2966
+ style: { "--_c": ink, "--_s": `${size}px`, ...radius !== void 0 && { "--_r": typeof radius === "number" ? `${radius}px` : radius }, ...style },
2967
+ ...rest,
2968
+ children: [
2969
+ /* @__PURE__ */ jsx("span", { className: "ru-icon-tile__face", role: label ? "img" : void 0, "aria-label": label, "aria-hidden": label ? void 0 : true, children }),
2970
+ badge && /* @__PURE__ */ jsx("span", { className: "ru-icon-tile__badge", "aria-hidden": "true", children: badge })
2971
+ ]
2972
+ }
2973
+ );
2974
+ }
2055
2975
  export {
2056
2976
  Accordion,
2057
2977
  Alert,
@@ -2067,10 +2987,15 @@ export {
2067
2987
  CardFooter,
2068
2988
  CardHeader,
2069
2989
  Checkbox,
2990
+ DateTile,
2070
2991
  Divider,
2992
+ DotText,
2071
2993
  Drawer,
2072
2994
  Dropdown,
2995
+ Dropzone,
2996
+ FileItem,
2073
2997
  FrameCard,
2998
+ IconTile,
2074
2999
  Input,
2075
3000
  Kbd,
2076
3001
  Modal,
@@ -2078,12 +3003,19 @@ export {
2078
3003
  Pagination,
2079
3004
  Pattern,
2080
3005
  Progress,
3006
+ ProgressRing,
2081
3007
  Radio,
2082
3008
  RadioGroup,
3009
+ SegmentedControl,
2083
3010
  Select,
3011
+ SignaturePad,
2084
3012
  Skeleton,
2085
3013
  Spinner,
2086
3014
  SpotlightCard,
3015
+ StackedProgress,
3016
+ Stat,
3017
+ StatusIcon,
3018
+ StepList,
2087
3019
  Switch,
2088
3020
  Table,
2089
3021
  Tabs,
@@ -2091,6 +3023,7 @@ export {
2091
3023
  ToastProvider,
2092
3024
  Tooltip,
2093
3025
  cn,
3026
+ formatBytes,
2094
3027
  useClickOutside,
2095
3028
  useControllableState,
2096
3029
  useDisclosure,