reusify-ui 2.0.1 → 2.1.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.
@@ -7,36 +7,108 @@ const reactDom = require("react-dom");
7
7
  function cn(...classes) {
8
8
  return classes.filter(Boolean).join(" ");
9
9
  }
10
+ const legacyVariants = {
11
+ primary: ["solid", void 0],
12
+ danger: ["solid", "danger"],
13
+ success: ["solid", "success"]
14
+ };
15
+ const semanticColors$2 = /* @__PURE__ */ new Set(["primary", "secondary", "success", "danger", "warning", "info"]);
16
+ const radii = { none: "0", sm: "6px", md: "10px", lg: "14px", xl: "18px", "2xl": "24px", full: "9999px" };
17
+ function resolveRadius(radius) {
18
+ if (radius === void 0) return void 0;
19
+ if (typeof radius === "number") return `${radius}px`;
20
+ return radii[radius] ?? radius;
21
+ }
10
22
  const Button = react.forwardRef(function Button2({
11
- variant = "primary",
23
+ variant = "solid",
24
+ color,
25
+ textColor,
26
+ radius,
27
+ justify = "center",
12
28
  size = "md",
13
29
  loading = false,
14
30
  fullWidth = false,
31
+ pill = false,
32
+ iconOnly = false,
15
33
  leftIcon,
16
34
  rightIcon,
17
35
  disabled,
36
+ href,
18
37
  type = "button",
19
38
  className,
39
+ style,
40
+ onPointerMove,
20
41
  children,
21
42
  ...rest
22
43
  }, ref) {
23
- return /* @__PURE__ */ jsxRuntime.jsxs(
44
+ const [kind, legacyColor] = legacyVariants[variant] ?? [variant, void 0];
45
+ const handlePointerMove = kind === "pixel" ? (e) => {
46
+ const el = e.currentTarget;
47
+ const rect = el.getBoundingClientRect();
48
+ el.style.setProperty("--ru-px", `${e.clientX - rect.left}px`);
49
+ el.style.setProperty("--ru-py", `${e.clientY - rect.top}px`);
50
+ onPointerMove == null ? void 0 : onPointerMove(e);
51
+ } : onPointerMove;
52
+ const resolvedColor = color ?? legacyColor ?? "primary";
53
+ const customColor = !semanticColors$2.has(resolvedColor);
54
+ const classes = cn(
55
+ "ru-btn",
56
+ `ru-btn--${kind}`,
57
+ `ru-btn--${size}`,
58
+ !customColor && `ru-c-${resolvedColor}`,
59
+ fullWidth && "ru-btn--full",
60
+ pill && "ru-btn--pill",
61
+ iconOnly && "ru-btn--icon",
62
+ justify !== "center" && `ru-btn--justify-${justify}`,
63
+ className
64
+ );
65
+ const inlineStyle = {
66
+ ...customColor && { "--_c": resolvedColor },
67
+ ...textColor && { "--_fg": textColor },
68
+ ...radius !== void 0 && { borderRadius: resolveRadius(radius) },
69
+ ...style
70
+ };
71
+ const styleProp = Object.keys(inlineStyle).length ? inlineStyle : void 0;
72
+ const content = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
73
+ loading ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ru-spinner", "aria-hidden": "true" }) : leftIcon,
74
+ children,
75
+ !loading && rightIcon && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ru-btn__end", children: rightIcon })
76
+ ] });
77
+ if (href !== void 0) {
78
+ const inactive = disabled || loading;
79
+ return /* @__PURE__ */ jsxRuntime.jsx(
80
+ "a",
81
+ {
82
+ ref,
83
+ href: inactive ? void 0 : href,
84
+ "aria-disabled": inactive || void 0,
85
+ "aria-busy": loading || void 0,
86
+ className: classes,
87
+ style: styleProp,
88
+ onPointerMove: handlePointerMove,
89
+ ...rest,
90
+ children: content
91
+ }
92
+ );
93
+ }
94
+ return /* @__PURE__ */ jsxRuntime.jsx(
24
95
  "button",
25
96
  {
26
97
  ref,
27
98
  type,
28
99
  disabled: disabled || loading,
29
100
  "aria-busy": loading || void 0,
30
- className: cn("ru-btn", `ru-btn--${variant}`, `ru-btn--${size}`, fullWidth && "ru-btn--full", className),
101
+ className: classes,
102
+ style: styleProp,
103
+ onPointerMove: handlePointerMove,
31
104
  ...rest,
32
- children: [
33
- loading ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ru-spinner", "aria-hidden": "true" }) : leftIcon,
34
- children,
35
- !loading && rightIcon
36
- ]
105
+ children: content
37
106
  }
38
107
  );
39
108
  });
109
+ function ButtonGroup({ attached = false, className, ...rest }) {
110
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { role: "group", className: cn("ru-btn-group", attached && "ru-btn-group--attached", className), ...rest });
111
+ }
40
112
  const base = {
41
113
  xmlns: "http://www.w3.org/2000/svg",
42
114
  viewBox: "0 0 24 24",
@@ -155,132 +227,6 @@ function Alert({ variant = "info", title, icon, onClose, className, children, ..
155
227
  }
156
228
  );
157
229
  }
158
- function Badge({ variant = "soft", color = "primary", size = "md", dot = false, className, children, ...rest }) {
159
- return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: cn("ru-badge", `ru-badge--${variant}`, `ru-badge--${size}`, `ru-c-${color}`, className), ...rest, children: [
160
- dot && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ru-badge__dot", "aria-hidden": "true" }),
161
- children
162
- ] });
163
- }
164
- function getInitials(name) {
165
- const parts = name.trim().split(/\s+/).filter(Boolean);
166
- if (parts.length === 0) return "";
167
- const first = parts[0][0];
168
- const last = parts.length > 1 ? parts[parts.length - 1][0] : "";
169
- return (first + last).toUpperCase();
170
- }
171
- function Avatar({ src, alt, name, size = "md", shape = "circle", className, ...rest }) {
172
- const [failed, setFailed] = react.useState(false);
173
- react.useEffect(() => setFailed(false), [src]);
174
- const showImage = src && !failed;
175
- const initials = name ? getInitials(name) : "";
176
- return /* @__PURE__ */ jsxRuntime.jsx(
177
- "span",
178
- {
179
- className: cn("ru-avatar", `ru-avatar--${size}`, shape === "square" && "ru-avatar--square", className),
180
- role: showImage ? void 0 : "img",
181
- "aria-label": showImage ? void 0 : alt ?? name ?? "Avatar",
182
- ...rest,
183
- children: showImage ? /* @__PURE__ */ jsxRuntime.jsx("img", { src, alt: alt ?? name ?? "", onError: () => setFailed(true) }) : initials ? /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: initials }) : /* @__PURE__ */ jsxRuntime.jsx(UserIcon, { className: "ru-avatar__icon" })
184
- }
185
- );
186
- }
187
- function AvatarGroup({ max, size = "md", className, children, ...rest }) {
188
- const avatars = react.Children.toArray(children).filter(react.isValidElement);
189
- const visible = max ? avatars.slice(0, max) : avatars;
190
- const extra = avatars.length - visible.length;
191
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("ru-avatar-group", className), ...rest, children: [
192
- visible.map((child) => react.cloneElement(child, { size: child.props.size ?? size })),
193
- extra > 0 && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: cn("ru-avatar", `ru-avatar--${size}`), "aria-label": `${extra} more`, children: [
194
- "+",
195
- extra
196
- ] })
197
- ] });
198
- }
199
- function Card({ variant = "outline", hoverable = false, className, ...rest }) {
200
- return /* @__PURE__ */ jsxRuntime.jsx(
201
- "div",
202
- {
203
- className: cn(
204
- "ru-card",
205
- variant === "elevated" && "ru-card--elevated",
206
- hoverable && "ru-card--hoverable",
207
- className
208
- ),
209
- ...rest
210
- }
211
- );
212
- }
213
- function CardHeader({ title, subtitle, action, className, children, ...rest }) {
214
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("ru-card__header", className), ...rest, children: [
215
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
216
- title && /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "ru-card__title", children: title }),
217
- subtitle && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "ru-card__subtitle", children: subtitle }),
218
- children
219
- ] }),
220
- action
221
- ] });
222
- }
223
- function CardBody({ className, ...rest }) {
224
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("ru-card__body", className), ...rest });
225
- }
226
- function CardFooter({ className, ...rest }) {
227
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("ru-card__footer", className), ...rest });
228
- }
229
- function Divider({ orientation = "horizontal", label, className, ...rest }) {
230
- if (label && orientation === "horizontal") {
231
- return /* @__PURE__ */ jsxRuntime.jsx("div", { role: "separator", className: cn("ru-divider", "ru-divider--label", className), ...rest, children: label });
232
- }
233
- return /* @__PURE__ */ jsxRuntime.jsx(
234
- "hr",
235
- {
236
- "aria-orientation": orientation,
237
- className: cn("ru-divider", orientation === "vertical" && "ru-divider--vertical", className),
238
- ...rest
239
- }
240
- );
241
- }
242
- function Table({
243
- columns,
244
- data,
245
- rowKey,
246
- striped = false,
247
- hoverable = false,
248
- size = "md",
249
- caption,
250
- emptyText = "No data",
251
- onRowClick,
252
- className
253
- }) {
254
- const getKey = (row, index) => {
255
- if (typeof rowKey === "function") return rowKey(row, index);
256
- if (rowKey !== void 0) return String(row[rowKey]);
257
- return index;
258
- };
259
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("ru-table-wrap", className), children: /* @__PURE__ */ jsxRuntime.jsxs(
260
- "table",
261
- {
262
- className: cn(
263
- "ru-table",
264
- `ru-table--${size}`,
265
- striped && "ru-table--striped",
266
- (hoverable || onRowClick) && "ru-table--hover"
267
- ),
268
- children: [
269
- caption && /* @__PURE__ */ jsxRuntime.jsx("caption", { children: caption }),
270
- /* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsx("tr", { children: columns.map((col) => /* @__PURE__ */ jsxRuntime.jsx("th", { scope: "col", style: { textAlign: col.align, width: col.width }, children: col.header }, col.key)) }) }),
271
- /* @__PURE__ */ jsxRuntime.jsx("tbody", { children: data.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("tr", { children: /* @__PURE__ */ jsxRuntime.jsx("td", { className: "ru-table__empty", colSpan: columns.length, children: emptyText }) }) : data.map((row, i) => /* @__PURE__ */ jsxRuntime.jsx(
272
- "tr",
273
- {
274
- onClick: onRowClick ? () => onRowClick(row, i) : void 0,
275
- style: onRowClick ? { cursor: "pointer" } : void 0,
276
- children: columns.map((col) => /* @__PURE__ */ jsxRuntime.jsx("td", { style: { textAlign: col.align }, children: col.render ? col.render(row, i) : row[col.key] }, col.key))
277
- },
278
- getKey(row, i)
279
- )) })
280
- ]
281
- }
282
- ) });
283
- }
284
230
  function useControllableState(value, defaultValue, onChange) {
285
231
  const [internal, setInternal] = react.useState(defaultValue);
286
232
  const isControlled = value !== void 0;
@@ -894,7 +840,7 @@ function RadioGroup({
894
840
  )) })
895
841
  ] });
896
842
  }
897
- const Switch = react.forwardRef(function Switch2({ checked, defaultChecked = false, onChange, label, size = "md", color = "primary", disabled, name, id, className }, ref) {
843
+ const Switch = react.forwardRef(function Switch2({ checked, defaultChecked = false, onChange, label, size = "md", color = "primary", disabled, name, id, className, ...rest }, ref) {
898
844
  const [on, setOn] = useControllableState(checked, defaultChecked, onChange);
899
845
  return /* @__PURE__ */ jsxRuntime.jsxs(
900
846
  "label",
@@ -912,6 +858,7 @@ const Switch = react.forwardRef(function Switch2({ checked, defaultChecked = fal
912
858
  disabled,
913
859
  className: "ru-switch__track",
914
860
  onClick: () => setOn(!on),
861
+ ...rest,
915
862
  children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ru-switch__thumb" })
916
863
  }
917
864
  ),
@@ -921,330 +868,78 @@ const Switch = react.forwardRef(function Switch2({ checked, defaultChecked = fal
921
868
  }
922
869
  );
923
870
  });
924
- function DialogContent({ titleId, descId, title, description, showCloseButton, onClose, children, footer }) {
925
- return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
926
- (title || description || showCloseButton) && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ru-modal__header", children: [
927
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
928
- title && /* @__PURE__ */ jsxRuntime.jsx("h2", { id: titleId, className: "ru-modal__title", children: title }),
929
- description && /* @__PURE__ */ jsxRuntime.jsx("p", { id: descId, className: "ru-modal__desc", children: description })
930
- ] }),
931
- showCloseButton && /* @__PURE__ */ jsxRuntime.jsx(CloseButton, { onClick: onClose })
932
- ] }),
933
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ru-modal__body", children }),
934
- footer && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ru-modal__footer", children: footer })
935
- ] });
936
- }
937
- function Modal({
938
- open,
939
- onClose,
940
- size = "md",
941
- closeOnOverlayClick = true,
942
- closeOnEsc = true,
943
- showCloseButton = true,
944
- className,
945
- ...content
946
- }) {
947
- const panelRef = react.useRef(null);
948
- const mounted = useMounted();
949
- const titleId = react.useId();
950
- const descId = react.useId();
951
- useOverlay(open && mounted, panelRef, onClose, closeOnEsc);
952
- if (!open || !mounted) return null;
953
- const onOverlayMouseDown = (e) => {
954
- if (closeOnOverlayClick && e.target === e.currentTarget) onClose();
871
+ function Tabs({ items, value, defaultValue, onChange, variant = "line", fullWidth = false, className }) {
872
+ var _a;
873
+ const baseId = react.useId();
874
+ const firstEnabled = ((_a = items.find((item) => !item.disabled)) == null ? void 0 : _a.value) ?? "";
875
+ const [active, setActive] = useControllableState(value, defaultValue ?? firstEnabled, onChange);
876
+ const tabRefs = react.useRef([]);
877
+ const onKeyDown = (e, index) => {
878
+ var _a2;
879
+ const enabled = items.flatMap((item, i) => item.disabled ? [] : [i]);
880
+ const pos = enabled.indexOf(index);
881
+ let next;
882
+ switch (e.key) {
883
+ case "ArrowRight":
884
+ next = enabled[(pos + 1) % enabled.length];
885
+ break;
886
+ case "ArrowLeft":
887
+ next = enabled[(pos - 1 + enabled.length) % enabled.length];
888
+ break;
889
+ case "Home":
890
+ next = enabled[0];
891
+ break;
892
+ case "End":
893
+ next = enabled[enabled.length - 1];
894
+ break;
895
+ default:
896
+ return;
897
+ }
898
+ e.preventDefault();
899
+ (_a2 = tabRefs.current[next]) == null ? void 0 : _a2.focus();
900
+ setActive(items[next].value);
955
901
  };
956
- return reactDom.createPortal(
957
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ru-overlay", onMouseDown: onOverlayMouseDown, children: /* @__PURE__ */ jsxRuntime.jsx(
902
+ const activeIndex = items.findIndex((item) => item.value === active);
903
+ const activeItem = items[activeIndex];
904
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("ru-tabs", `ru-tabs--${variant}`, fullWidth && "ru-tabs--full", className), children: [
905
+ /* @__PURE__ */ jsxRuntime.jsx("div", { role: "tablist", className: "ru-tabs__list", children: items.map((item, i) => {
906
+ const selected = i === activeIndex;
907
+ return /* @__PURE__ */ jsxRuntime.jsxs(
908
+ "button",
909
+ {
910
+ ref: (el) => {
911
+ tabRefs.current[i] = el;
912
+ },
913
+ type: "button",
914
+ role: "tab",
915
+ id: `${baseId}-tab-${i}`,
916
+ "aria-selected": selected,
917
+ "aria-controls": `${baseId}-panel-${i}`,
918
+ tabIndex: selected ? 0 : -1,
919
+ disabled: item.disabled,
920
+ className: "ru-tabs__tab",
921
+ onClick: () => setActive(item.value),
922
+ onKeyDown: (e) => onKeyDown(e, i),
923
+ children: [
924
+ item.icon,
925
+ item.label
926
+ ]
927
+ },
928
+ item.value
929
+ );
930
+ }) }),
931
+ activeItem && activeItem.content !== void 0 && /* @__PURE__ */ jsxRuntime.jsx(
958
932
  "div",
959
933
  {
960
- ref: panelRef,
961
- role: "dialog",
962
- "aria-modal": "true",
963
- "aria-labelledby": content.title ? titleId : void 0,
964
- "aria-describedby": content.description ? descId : void 0,
965
- tabIndex: -1,
966
- className: cn("ru-modal", `ru-modal--${size}`, className),
967
- children: /* @__PURE__ */ jsxRuntime.jsx(
968
- DialogContent,
969
- {
970
- ...content,
971
- titleId,
972
- descId,
973
- showCloseButton,
974
- onClose
975
- }
976
- )
934
+ role: "tabpanel",
935
+ id: `${baseId}-panel-${activeIndex}`,
936
+ "aria-labelledby": `${baseId}-tab-${activeIndex}`,
937
+ tabIndex: 0,
938
+ className: "ru-tabs__panel",
939
+ children: activeItem.content
977
940
  }
978
- ) }),
979
- document.body
980
- );
981
- }
982
- const drawerSizes = { sm: "20rem", md: "24rem", lg: "32rem" };
983
- function Drawer({
984
- open,
985
- onClose,
986
- placement = "right",
987
- size = "md",
988
- closeOnOverlayClick = true,
989
- closeOnEsc = true,
990
- showCloseButton = true,
991
- className,
992
- ...content
993
- }) {
994
- const panelRef = react.useRef(null);
995
- const mounted = useMounted();
996
- const titleId = react.useId();
997
- const descId = react.useId();
998
- useOverlay(open && mounted, panelRef, onClose, closeOnEsc);
999
- if (!open || !mounted) return null;
1000
- const resolvedSize = typeof size === "number" ? `${size}px` : drawerSizes[size] ?? size;
1001
- return reactDom.createPortal(
1002
- /* @__PURE__ */ jsxRuntime.jsx(
1003
- "div",
1004
- {
1005
- className: "ru-overlay ru-overlay--drawer",
1006
- onMouseDown: (e) => {
1007
- if (closeOnOverlayClick && e.target === e.currentTarget) onClose();
1008
- },
1009
- children: /* @__PURE__ */ jsxRuntime.jsx(
1010
- "div",
1011
- {
1012
- ref: panelRef,
1013
- role: "dialog",
1014
- "aria-modal": "true",
1015
- "aria-labelledby": content.title ? titleId : void 0,
1016
- "aria-describedby": content.description ? descId : void 0,
1017
- tabIndex: -1,
1018
- className: cn("ru-drawer", `ru-drawer--${placement}`, className),
1019
- style: { "--ru-drawer-size": resolvedSize },
1020
- children: /* @__PURE__ */ jsxRuntime.jsx(
1021
- DialogContent,
1022
- {
1023
- ...content,
1024
- titleId,
1025
- descId,
1026
- showCloseButton,
1027
- onClose
1028
- }
1029
- )
1030
- }
1031
- )
1032
- }
1033
- ),
1034
- document.body
1035
- );
1036
- }
1037
- function Tooltip({ content, children, placement = "top", delay = 100, disabled, className }) {
1038
- const [open, setOpen] = react.useState(false);
1039
- const timer = react.useRef(void 0);
1040
- const id = react.useId();
1041
- const visible = open && !disabled && content != null && content !== "";
1042
- react.useEffect(() => () => clearTimeout(timer.current), []);
1043
- const show = () => {
1044
- clearTimeout(timer.current);
1045
- timer.current = setTimeout(() => setOpen(true), delay);
1046
- };
1047
- const hide = () => {
1048
- clearTimeout(timer.current);
1049
- setOpen(false);
1050
- };
1051
- const trigger = react.isValidElement(children) ? react.cloneElement(children, {
1052
- "aria-describedby": visible ? id : void 0
1053
- }) : children;
1054
- return /* @__PURE__ */ jsxRuntime.jsxs(
1055
- "span",
1056
- {
1057
- className: cn("ru-tooltip", className),
1058
- onMouseEnter: show,
1059
- onMouseLeave: hide,
1060
- onFocus: show,
1061
- onBlur: hide,
1062
- onKeyDown: (e) => e.key === "Escape" && hide(),
1063
- children: [
1064
- trigger,
1065
- visible && /* @__PURE__ */ jsxRuntime.jsx("span", { role: "tooltip", id, className: cn("ru-tooltip__bubble", `ru-tooltip__bubble--${placement}`), children: content })
1066
- ]
1067
- }
1068
- );
1069
- }
1070
- function Dropdown({ trigger, items, align = "start", closeOnSelect = true, className }) {
1071
- const [open, setOpen] = react.useState(false);
1072
- const rootRef = react.useRef(null);
1073
- const menuRef = react.useRef(null);
1074
- const menuId = react.useId();
1075
- useClickOutside(rootRef, () => setOpen(false), open);
1076
- const getItems = () => {
1077
- var _a;
1078
- return Array.from(((_a = menuRef.current) == null ? void 0 : _a.querySelectorAll('[role="menuitem"]:not(:disabled)')) ?? []);
1079
- };
1080
- const focusItem = (index) => {
1081
- const els = getItems();
1082
- if (els.length) els[(index + els.length) % els.length].focus();
1083
- };
1084
- const closeAndFocusTrigger = () => {
1085
- var _a, _b;
1086
- setOpen(false);
1087
- (_b = (_a = rootRef.current) == null ? void 0 : _a.querySelector("[aria-haspopup]")) == null ? void 0 : _b.focus();
1088
- };
1089
- react.useEffect(() => {
1090
- if (open) focusItem(0);
1091
- }, [open]);
1092
- const onMenuKeyDown = (e) => {
1093
- const els = getItems();
1094
- const index = els.indexOf(document.activeElement);
1095
- switch (e.key) {
1096
- case "ArrowDown":
1097
- e.preventDefault();
1098
- focusItem(index + 1);
1099
- break;
1100
- case "ArrowUp":
1101
- e.preventDefault();
1102
- focusItem(index - 1);
1103
- break;
1104
- case "Home":
1105
- e.preventDefault();
1106
- focusItem(0);
1107
- break;
1108
- case "End":
1109
- e.preventDefault();
1110
- focusItem(-1);
1111
- break;
1112
- case "Escape":
1113
- e.preventDefault();
1114
- closeAndFocusTrigger();
1115
- break;
1116
- case "Tab":
1117
- setOpen(false);
1118
- break;
1119
- }
1120
- };
1121
- const triggerProps = trigger.props;
1122
- const triggerEl = react.cloneElement(trigger, {
1123
- "aria-haspopup": "menu",
1124
- "aria-expanded": open,
1125
- "aria-controls": open ? menuId : void 0,
1126
- onClick: (e) => {
1127
- var _a;
1128
- (_a = triggerProps.onClick) == null ? void 0 : _a.call(triggerProps, e);
1129
- setOpen((v) => !v);
1130
- },
1131
- onKeyDown: (e) => {
1132
- var _a;
1133
- (_a = triggerProps.onKeyDown) == null ? void 0 : _a.call(triggerProps, e);
1134
- if (e.key === "ArrowDown" && !open) {
1135
- e.preventDefault();
1136
- setOpen(true);
1137
- }
1138
- }
1139
- });
1140
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: rootRef, className: cn("ru-dropdown", className), children: [
1141
- triggerEl,
1142
- open && /* @__PURE__ */ jsxRuntime.jsx(
1143
- "div",
1144
- {
1145
- ref: menuRef,
1146
- id: menuId,
1147
- role: "menu",
1148
- className: cn("ru-dropdown__menu", align === "end" && "ru-dropdown__menu--end"),
1149
- onKeyDown: onMenuKeyDown,
1150
- children: items.map(
1151
- (item, i) => item.divider ? /* @__PURE__ */ jsxRuntime.jsx("div", { role: "separator", className: "ru-dropdown__divider" }, item.key ?? i) : /* @__PURE__ */ jsxRuntime.jsxs(
1152
- "button",
1153
- {
1154
- type: "button",
1155
- role: "menuitem",
1156
- tabIndex: -1,
1157
- disabled: item.disabled,
1158
- className: cn("ru-dropdown__item", item.danger && "ru-dropdown__item--danger"),
1159
- onClick: () => {
1160
- var _a;
1161
- (_a = item.onClick) == null ? void 0 : _a.call(item);
1162
- if (closeOnSelect) closeAndFocusTrigger();
1163
- },
1164
- children: [
1165
- item.icon && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ru-dropdown__icon", children: item.icon }),
1166
- item.label
1167
- ]
1168
- },
1169
- item.key ?? i
1170
- )
1171
- )
1172
- }
1173
- )
1174
- ] });
1175
- }
1176
- function Tabs({ items, value, defaultValue, onChange, variant = "line", fullWidth = false, className }) {
1177
- var _a;
1178
- const baseId = react.useId();
1179
- const firstEnabled = ((_a = items.find((item) => !item.disabled)) == null ? void 0 : _a.value) ?? "";
1180
- const [active, setActive] = useControllableState(value, defaultValue ?? firstEnabled, onChange);
1181
- const tabRefs = react.useRef([]);
1182
- const onKeyDown = (e, index) => {
1183
- var _a2;
1184
- const enabled = items.flatMap((item, i) => item.disabled ? [] : [i]);
1185
- const pos = enabled.indexOf(index);
1186
- let next;
1187
- switch (e.key) {
1188
- case "ArrowRight":
1189
- next = enabled[(pos + 1) % enabled.length];
1190
- break;
1191
- case "ArrowLeft":
1192
- next = enabled[(pos - 1 + enabled.length) % enabled.length];
1193
- break;
1194
- case "Home":
1195
- next = enabled[0];
1196
- break;
1197
- case "End":
1198
- next = enabled[enabled.length - 1];
1199
- break;
1200
- default:
1201
- return;
1202
- }
1203
- e.preventDefault();
1204
- (_a2 = tabRefs.current[next]) == null ? void 0 : _a2.focus();
1205
- setActive(items[next].value);
1206
- };
1207
- const activeIndex = items.findIndex((item) => item.value === active);
1208
- const activeItem = items[activeIndex];
1209
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("ru-tabs", `ru-tabs--${variant}`, fullWidth && "ru-tabs--full", className), children: [
1210
- /* @__PURE__ */ jsxRuntime.jsx("div", { role: "tablist", className: "ru-tabs__list", children: items.map((item, i) => {
1211
- const selected = i === activeIndex;
1212
- return /* @__PURE__ */ jsxRuntime.jsxs(
1213
- "button",
1214
- {
1215
- ref: (el) => {
1216
- tabRefs.current[i] = el;
1217
- },
1218
- type: "button",
1219
- role: "tab",
1220
- id: `${baseId}-tab-${i}`,
1221
- "aria-selected": selected,
1222
- "aria-controls": `${baseId}-panel-${i}`,
1223
- tabIndex: selected ? 0 : -1,
1224
- disabled: item.disabled,
1225
- className: "ru-tabs__tab",
1226
- onClick: () => setActive(item.value),
1227
- onKeyDown: (e) => onKeyDown(e, i),
1228
- children: [
1229
- item.icon,
1230
- item.label
1231
- ]
1232
- },
1233
- item.value
1234
- );
1235
- }) }),
1236
- activeItem && activeItem.content !== void 0 && /* @__PURE__ */ jsxRuntime.jsx(
1237
- "div",
1238
- {
1239
- role: "tabpanel",
1240
- id: `${baseId}-panel-${activeIndex}`,
1241
- "aria-labelledby": `${baseId}-tab-${activeIndex}`,
1242
- tabIndex: 0,
1243
- className: "ru-tabs__panel",
1244
- children: activeItem.content
1245
- }
1246
- )
1247
- ] });
941
+ )
942
+ ] });
1248
943
  }
1249
944
  function Accordion({ items, multiple = false, value, defaultValue, onChange, className }) {
1250
945
  const baseId = react.useId();
@@ -1364,31 +1059,679 @@ function Breadcrumbs({ items, separator, className }) {
1364
1059
  ] }, i);
1365
1060
  }) }) });
1366
1061
  }
1367
- const ToastContext = react.createContext(null);
1368
- function ToastProvider({ children, position = "bottom-right", duration = 4e3, max = 5 }) {
1369
- const [toasts, setToasts] = react.useState([]);
1370
- const mounted = useMounted();
1371
- const counter = react.useRef(0);
1372
- const dismiss = react.useCallback((id) => {
1373
- setToasts((list) => id ? list.filter((t) => t.id !== id) : []);
1374
- }, []);
1375
- const api = react.useMemo(() => {
1376
- const show = (options) => {
1377
- const opts = typeof options === "string" ? { title: options } : options;
1378
- const id = opts.id ?? `ru-toast-${++counter.current}`;
1379
- setToasts((list) => [...list.filter((t) => t.id !== id), { ...opts, id }].slice(-max));
1380
- return id;
1381
- };
1382
- const fn = show;
1383
- fn.success = (title, o) => show({ ...o, title, variant: "success" });
1384
- fn.error = (title, o) => show({ ...o, title, variant: "danger" });
1385
- fn.warning = (title, o) => show({ ...o, title, variant: "warning" });
1386
- fn.info = (title, o) => show({ ...o, title, variant: "info" });
1387
- fn.dismiss = dismiss;
1388
- return fn;
1389
- }, [dismiss, max]);
1390
- return /* @__PURE__ */ jsxRuntime.jsxs(ToastContext.Provider, { value: api, children: [
1391
- children,
1062
+ function Badge({ variant = "soft", color = "primary", size = "md", dot = false, className, children, ...rest }) {
1063
+ return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: cn("ru-badge", `ru-badge--${variant}`, `ru-badge--${size}`, `ru-c-${color}`, className), ...rest, children: [
1064
+ dot && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ru-badge__dot", "aria-hidden": "true" }),
1065
+ children
1066
+ ] });
1067
+ }
1068
+ function getInitials(name) {
1069
+ const parts = name.trim().split(/\s+/).filter(Boolean);
1070
+ if (parts.length === 0) return "";
1071
+ const first = parts[0][0];
1072
+ const last = parts.length > 1 ? parts[parts.length - 1][0] : "";
1073
+ return (first + last).toUpperCase();
1074
+ }
1075
+ const avatarSemanticColors = /* @__PURE__ */ new Set(["primary", "secondary", "success", "danger", "warning", "info"]);
1076
+ const avatarStatusLabels = { online: "Online", away: "Away", busy: "Busy", offline: "Offline" };
1077
+ function hueFromString(text) {
1078
+ let hash = 0;
1079
+ for (const ch of text) hash = (hash * 31 + ch.charCodeAt(0)) % 360;
1080
+ return hash;
1081
+ }
1082
+ function resolveAvatarColor(color, seed) {
1083
+ if (!color) return void 0;
1084
+ if (color === "auto") return `hsl(${hueFromString(seed)} 70% 52%)`;
1085
+ return avatarSemanticColors.has(color) ? `var(--ru-${color})` : color;
1086
+ }
1087
+ function AvatarTooltip({ content, description, accent, children }) {
1088
+ const [open, setOpen] = react.useState(false);
1089
+ const bubbleRef = react.useRef(null);
1090
+ const tiltToward = (e) => {
1091
+ const bubble = bubbleRef.current;
1092
+ if (!bubble) return;
1093
+ const rect = e.currentTarget.getBoundingClientRect();
1094
+ const ratio = Math.max(-1, Math.min(1, (e.clientX - (rect.left + rect.width / 2)) / (rect.width / 2)));
1095
+ bubble.style.setProperty("--_shift", `${ratio * 10}px`);
1096
+ bubble.style.setProperty("--_tilt", `${ratio * 12}deg`);
1097
+ };
1098
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1099
+ "span",
1100
+ {
1101
+ className: "ru-avatar-tip",
1102
+ onMouseEnter: () => setOpen(true),
1103
+ onMouseLeave: () => setOpen(false),
1104
+ onMouseMove: tiltToward,
1105
+ onFocus: () => setOpen(true),
1106
+ onBlur: () => setOpen(false),
1107
+ children: [
1108
+ children,
1109
+ open && // The avatar's accessible name already includes this text, so the bubble is visual only.
1110
+ /* @__PURE__ */ jsxRuntime.jsx("span", { ref: bubbleRef, className: "ru-avatar-tip__bubble", "aria-hidden": "true", style: accent ? { "--_accent": accent } : void 0, children: /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "ru-avatar-tip__inner", children: [
1111
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ru-avatar-tip__name", children: content }),
1112
+ description && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ru-avatar-tip__desc", children: description })
1113
+ ] }) })
1114
+ ]
1115
+ }
1116
+ );
1117
+ }
1118
+ function Avatar({
1119
+ src,
1120
+ alt,
1121
+ name,
1122
+ size = "md",
1123
+ shape = "circle",
1124
+ color,
1125
+ ring,
1126
+ status,
1127
+ statusPulse = false,
1128
+ badge,
1129
+ tooltip,
1130
+ description,
1131
+ tooltipAccent,
1132
+ className,
1133
+ style,
1134
+ ...rest
1135
+ }) {
1136
+ const [failed, setFailed] = react.useState(false);
1137
+ react.useEffect(() => setFailed(false), [src]);
1138
+ const showImage = src && !failed;
1139
+ const initials = name ? getInitials(name) : "";
1140
+ const numericSize = typeof size === "number";
1141
+ const tint = resolveAvatarColor(color, name ?? alt ?? "");
1142
+ const ringColor = typeof ring === "string" ? resolveAvatarColor(ring, name ?? "") : void 0;
1143
+ const vars = {
1144
+ ...numericSize && { width: size, height: size, fontSize: Math.round(size * 0.38) },
1145
+ ...tint && { "--_avc": tint },
1146
+ ...ringColor && { "--_ring": ringColor },
1147
+ ...style
1148
+ };
1149
+ const baseLabel = alt ?? name ?? "Avatar";
1150
+ const label = [baseLabel, description && `, ${description}`, status && ` (${avatarStatusLabels[status] ?? status})`, badge !== void 0 && badge !== null && `, ${badge} new`].filter(Boolean).join("");
1151
+ const avatar = /* @__PURE__ */ jsxRuntime.jsxs(
1152
+ "span",
1153
+ {
1154
+ className: cn(
1155
+ "ru-avatar",
1156
+ !numericSize && `ru-avatar--${size}`,
1157
+ shape !== "circle" && `ru-avatar--${shape}`,
1158
+ tint && "ru-avatar--tinted",
1159
+ ring && "ru-avatar--ring",
1160
+ className
1161
+ ),
1162
+ role: showImage ? void 0 : "img",
1163
+ "aria-label": showImage ? void 0 : label,
1164
+ style: Object.keys(vars).length ? vars : void 0,
1165
+ ...rest,
1166
+ children: [
1167
+ showImage ? /* @__PURE__ */ jsxRuntime.jsx("img", { src, alt: label === "Avatar" ? "" : label, onError: () => setFailed(true) }) : initials ? /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: initials }) : /* @__PURE__ */ jsxRuntime.jsx(UserIcon, { className: "ru-avatar__icon" }),
1168
+ badge !== void 0 && badge !== null && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ru-avatar__badge", "aria-hidden": "true", children: badge }),
1169
+ status && /* @__PURE__ */ jsxRuntime.jsx(
1170
+ "span",
1171
+ {
1172
+ className: cn("ru-avatar__status", `ru-avatar__status--${status}`, statusPulse && "ru-avatar__status--pulse"),
1173
+ "aria-hidden": "true"
1174
+ }
1175
+ )
1176
+ ]
1177
+ }
1178
+ );
1179
+ if (!tooltip) return avatar;
1180
+ return /* @__PURE__ */ jsxRuntime.jsx(AvatarTooltip, { content: tooltip === true ? baseLabel : tooltip, description, accent: tooltipAccent, children: avatar });
1181
+ }
1182
+ const avatarOverlap = { tight: "-0.75rem", normal: "-0.5rem", loose: "-0.25rem", none: "0.25rem" };
1183
+ function AvatarGroup({ max, size = "md", spacing, tooltips = false, className, style, children, ...rest }) {
1184
+ const avatars = react.Children.toArray(children).filter(react.isValidElement);
1185
+ const visible = max ? avatars.slice(0, max) : avatars;
1186
+ const extra = avatars.length - visible.length;
1187
+ const overlap = spacing === void 0 ? void 0 : avatarOverlap[spacing] ?? `-${typeof spacing === "number" ? `${spacing}px` : spacing}`;
1188
+ const vars = overlap ? { "--_overlap": overlap, ...style } : style;
1189
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("ru-avatar-group", className), style: vars, ...rest, children: [
1190
+ visible.map(
1191
+ (child) => react.cloneElement(child, {
1192
+ size: child.props.size ?? size,
1193
+ ...tooltips && child.props.tooltip === void 0 && { tooltip: true }
1194
+ })
1195
+ ),
1196
+ extra > 0 && /* @__PURE__ */ jsxRuntime.jsxs(
1197
+ "span",
1198
+ {
1199
+ className: cn("ru-avatar", typeof size === "number" ? void 0 : `ru-avatar--${size}`),
1200
+ style: typeof size === "number" ? { width: size, height: size, fontSize: Math.round(size * 0.34) } : void 0,
1201
+ role: "img",
1202
+ "aria-label": `${extra} more`,
1203
+ children: [
1204
+ "+",
1205
+ extra
1206
+ ]
1207
+ }
1208
+ )
1209
+ ] });
1210
+ }
1211
+ function Card({ variant = "outline", hoverable = false, className, ...rest }) {
1212
+ return /* @__PURE__ */ jsxRuntime.jsx(
1213
+ "div",
1214
+ {
1215
+ className: cn(
1216
+ "ru-card",
1217
+ variant === "elevated" && "ru-card--elevated",
1218
+ hoverable && "ru-card--hoverable",
1219
+ className
1220
+ ),
1221
+ ...rest
1222
+ }
1223
+ );
1224
+ }
1225
+ function CardHeader({ title, subtitle, action, className, children, ...rest }) {
1226
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("ru-card__header", className), ...rest, children: [
1227
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1228
+ title && /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "ru-card__title", children: title }),
1229
+ subtitle && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "ru-card__subtitle", children: subtitle }),
1230
+ children
1231
+ ] }),
1232
+ action
1233
+ ] });
1234
+ }
1235
+ function CardBody({ className, ...rest }) {
1236
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("ru-card__body", className), ...rest });
1237
+ }
1238
+ function CardFooter({ className, ...rest }) {
1239
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("ru-card__footer", className), ...rest });
1240
+ }
1241
+ const dividerSemanticColors = /* @__PURE__ */ new Set(["primary", "secondary", "success", "danger", "warning", "info"]);
1242
+ const dividerThickness = { sm: "1px", md: "2px", lg: "4px" };
1243
+ const dividerSpacing = { none: "0", sm: "0.5rem", md: "1rem", lg: "2rem", xl: "3rem" };
1244
+ const toLength = (v) => typeof v === "number" ? `${v}px` : v;
1245
+ function Divider({
1246
+ orientation = "horizontal",
1247
+ variant = "solid",
1248
+ size,
1249
+ color,
1250
+ length,
1251
+ align = "center",
1252
+ spacing,
1253
+ label,
1254
+ labelPosition = "center",
1255
+ className,
1256
+ style,
1257
+ ...rest
1258
+ }) {
1259
+ const vertical = orientation === "vertical";
1260
+ const labelled = Boolean(label) && !vertical;
1261
+ const vars = {
1262
+ ...size !== void 0 && { "--_t": dividerThickness[size] ?? toLength(size) },
1263
+ ...color && { "--_line": dividerSemanticColors.has(color) ? `var(--ru-${color})` : color },
1264
+ ...length !== void 0 && { "--_len": toLength(length) },
1265
+ ...spacing !== void 0 && { "--_gap": dividerSpacing[spacing] ?? toLength(spacing) },
1266
+ ...style
1267
+ };
1268
+ const styleProp = Object.keys(vars).length ? vars : void 0;
1269
+ const classes = cn(
1270
+ "ru-divider",
1271
+ variant !== "solid" && `ru-divider--${variant}`,
1272
+ vertical && "ru-divider--vertical",
1273
+ labelled && "ru-divider--label",
1274
+ labelled && labelPosition !== "center" && `ru-divider--label-${labelPosition}`,
1275
+ !vertical && align !== "center" && `ru-divider--align-${align}`,
1276
+ className
1277
+ );
1278
+ if (labelled) {
1279
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { role: "separator", "aria-orientation": "horizontal", className: classes, style: styleProp, ...rest, children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ru-divider__label", children: label }) });
1280
+ }
1281
+ return /* @__PURE__ */ jsxRuntime.jsx("hr", { "aria-orientation": orientation, className: classes, style: styleProp, ...rest });
1282
+ }
1283
+ function usePagination(pagination, dataLength) {
1284
+ const opts = pagination === true ? {} : pagination || {};
1285
+ const enabled = Boolean(pagination);
1286
+ const [pageSize, setPageSize] = useControllableState(
1287
+ opts.onPageSizeChange ? opts.pageSize : void 0,
1288
+ opts.pageSize ?? opts.defaultPageSize ?? 10,
1289
+ opts.onPageSizeChange
1290
+ );
1291
+ const [page, setPage] = useControllableState(opts.page, opts.defaultPage ?? 1, opts.onPageChange);
1292
+ const serverSide = opts.totalRows !== void 0;
1293
+ const totalRows = serverSide ? opts.totalRows : dataLength;
1294
+ const totalPages = Math.max(1, Math.ceil(totalRows / pageSize));
1295
+ const current = Math.min(Math.max(1, page), totalPages);
1296
+ const start = enabled ? (current - 1) * pageSize : 0;
1297
+ return {
1298
+ enabled,
1299
+ opts,
1300
+ serverSide,
1301
+ page: current,
1302
+ pageSize,
1303
+ totalRows,
1304
+ totalPages,
1305
+ start,
1306
+ setPage,
1307
+ changePageSize: (size) => {
1308
+ setPageSize(size);
1309
+ setPage(1);
1310
+ }
1311
+ };
1312
+ }
1313
+ function Table({
1314
+ columns,
1315
+ data,
1316
+ rowKey,
1317
+ striped = false,
1318
+ hoverable = false,
1319
+ size = "md",
1320
+ caption,
1321
+ emptyText = "No data",
1322
+ onRowClick,
1323
+ pagination,
1324
+ loading = false,
1325
+ className
1326
+ }) {
1327
+ const pager = usePagination(pagination, data.length);
1328
+ const rows = pager.enabled && !pager.serverSide ? data.slice(pager.start, pager.start + pager.pageSize) : data;
1329
+ const getKey = (row, index) => {
1330
+ if (typeof rowKey === "function") return rowKey(row, index);
1331
+ if (rowKey !== void 0) return String(row[rowKey]);
1332
+ return index;
1333
+ };
1334
+ const firstRow = pager.totalRows === 0 ? 0 : pager.start + 1;
1335
+ const lastRow = Math.min(pager.start + (pager.serverSide ? rows.length : pager.pageSize), pager.totalRows);
1336
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("ru-table-wrap", className), children: [
1337
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ru-table-scroll", children: /* @__PURE__ */ jsxRuntime.jsxs(
1338
+ "table",
1339
+ {
1340
+ className: cn(
1341
+ "ru-table",
1342
+ `ru-table--${size}`,
1343
+ striped && "ru-table--striped",
1344
+ (hoverable || onRowClick) && "ru-table--hover",
1345
+ loading && "ru-table--loading"
1346
+ ),
1347
+ "aria-busy": loading || void 0,
1348
+ children: [
1349
+ caption && /* @__PURE__ */ jsxRuntime.jsx("caption", { children: caption }),
1350
+ /* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsx("tr", { children: columns.map((col) => /* @__PURE__ */ jsxRuntime.jsx("th", { scope: "col", style: { textAlign: col.align, width: col.width }, children: col.header }, col.key)) }) }),
1351
+ /* @__PURE__ */ jsxRuntime.jsx("tbody", { children: rows.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("tr", { children: /* @__PURE__ */ jsxRuntime.jsx("td", { className: "ru-table__empty", colSpan: columns.length, children: loading ? "Loading…" : emptyText }) }) : rows.map((row, i) => {
1352
+ const index = pager.start + i;
1353
+ return /* @__PURE__ */ jsxRuntime.jsx(
1354
+ "tr",
1355
+ {
1356
+ onClick: onRowClick ? () => onRowClick(row, index) : void 0,
1357
+ style: onRowClick ? { cursor: "pointer" } : void 0,
1358
+ children: columns.map((col) => /* @__PURE__ */ jsxRuntime.jsx("td", { style: { textAlign: col.align }, children: col.render ? col.render(row, index) : row[col.key] }, col.key))
1359
+ },
1360
+ getKey(row, index)
1361
+ );
1362
+ }) })
1363
+ ]
1364
+ }
1365
+ ) }),
1366
+ pager.enabled && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ru-table__footer", children: [
1367
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "ru-table__info", "aria-live": "polite", children: pager.totalRows === 0 ? "No results" : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1368
+ "Showing ",
1369
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { children: firstRow }),
1370
+ "–",
1371
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { children: lastRow }),
1372
+ " of ",
1373
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { children: pager.totalRows })
1374
+ ] }) }),
1375
+ pager.opts.pageSizeOptions && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ru-table__size", children: [
1376
+ /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "Rows per page" }),
1377
+ /* @__PURE__ */ jsxRuntime.jsx(
1378
+ Select,
1379
+ {
1380
+ size: "sm",
1381
+ "aria-label": "Rows per page",
1382
+ value: pager.pageSize,
1383
+ onChange: pager.changePageSize,
1384
+ options: pager.opts.pageSizeOptions.map((n) => ({ label: String(n), value: n })),
1385
+ containerClassName: "ru-table__size-select"
1386
+ }
1387
+ )
1388
+ ] }),
1389
+ pager.totalPages > 1 && /* @__PURE__ */ jsxRuntime.jsx(
1390
+ Pagination,
1391
+ {
1392
+ size: "sm",
1393
+ page: pager.page,
1394
+ totalPages: pager.totalPages,
1395
+ onChange: pager.setPage,
1396
+ siblingCount: pager.opts.siblingCount,
1397
+ className: "ru-table__pagination"
1398
+ }
1399
+ )
1400
+ ] })
1401
+ ] });
1402
+ }
1403
+ const EXIT_MS = 200;
1404
+ function usePresence(open) {
1405
+ const [render, setRender] = react.useState(open);
1406
+ const [closing, setClosing] = react.useState(false);
1407
+ react.useEffect(() => {
1408
+ if (open) {
1409
+ setRender(true);
1410
+ setClosing(false);
1411
+ return void 0;
1412
+ }
1413
+ if (!render) return void 0;
1414
+ setClosing(true);
1415
+ const timer = setTimeout(() => {
1416
+ setRender(false);
1417
+ setClosing(false);
1418
+ }, EXIT_MS);
1419
+ return () => clearTimeout(timer);
1420
+ }, [open]);
1421
+ return { render, closing };
1422
+ }
1423
+ function DialogContent({ titleId, descId, title, description, showCloseButton, onClose, children, footer }) {
1424
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1425
+ (title || description || showCloseButton) && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ru-modal__header", children: [
1426
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1427
+ title && /* @__PURE__ */ jsxRuntime.jsx("h2", { id: titleId, className: "ru-modal__title", children: title }),
1428
+ description && /* @__PURE__ */ jsxRuntime.jsx("p", { id: descId, className: "ru-modal__desc", children: description })
1429
+ ] }),
1430
+ showCloseButton && /* @__PURE__ */ jsxRuntime.jsx(CloseButton, { onClick: onClose })
1431
+ ] }),
1432
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ru-modal__body", children }),
1433
+ footer && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ru-modal__footer", children: footer })
1434
+ ] });
1435
+ }
1436
+ function Modal({
1437
+ open,
1438
+ onClose,
1439
+ size = "md",
1440
+ closeOnOverlayClick = true,
1441
+ closeOnEsc = true,
1442
+ showCloseButton = true,
1443
+ className,
1444
+ ...content
1445
+ }) {
1446
+ const panelRef = react.useRef(null);
1447
+ const mounted = useMounted();
1448
+ const titleId = react.useId();
1449
+ const descId = react.useId();
1450
+ const { render, closing } = usePresence(open);
1451
+ useOverlay(open && mounted, panelRef, onClose, closeOnEsc);
1452
+ if (!render || !mounted) return null;
1453
+ const onOverlayMouseDown = (e) => {
1454
+ if (!closing && closeOnOverlayClick && e.target === e.currentTarget) onClose();
1455
+ };
1456
+ return reactDom.createPortal(
1457
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("ru-overlay", closing && "ru-overlay--closing"), onMouseDown: onOverlayMouseDown, children: /* @__PURE__ */ jsxRuntime.jsx(
1458
+ "div",
1459
+ {
1460
+ ref: panelRef,
1461
+ role: "dialog",
1462
+ "aria-modal": "true",
1463
+ "aria-labelledby": content.title ? titleId : void 0,
1464
+ "aria-describedby": content.description ? descId : void 0,
1465
+ tabIndex: -1,
1466
+ className: cn("ru-modal", `ru-modal--${size}`, closing && "ru-modal--closing", className),
1467
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1468
+ DialogContent,
1469
+ {
1470
+ ...content,
1471
+ titleId,
1472
+ descId,
1473
+ showCloseButton,
1474
+ onClose
1475
+ }
1476
+ )
1477
+ }
1478
+ ) }),
1479
+ document.body
1480
+ );
1481
+ }
1482
+ const drawerSizes = { sm: "20rem", md: "24rem", lg: "32rem" };
1483
+ function Drawer({
1484
+ open,
1485
+ onClose,
1486
+ placement = "right",
1487
+ size = "md",
1488
+ floating = false,
1489
+ closeOnOverlayClick = true,
1490
+ closeOnEsc = true,
1491
+ showCloseButton = true,
1492
+ className,
1493
+ ...content
1494
+ }) {
1495
+ const panelRef = react.useRef(null);
1496
+ const mounted = useMounted();
1497
+ const titleId = react.useId();
1498
+ const descId = react.useId();
1499
+ const { render, closing } = usePresence(open);
1500
+ useOverlay(open && mounted, panelRef, onClose, closeOnEsc);
1501
+ if (!render || !mounted) return null;
1502
+ const resolvedSize = typeof size === "number" ? `${size}px` : drawerSizes[size] ?? size;
1503
+ return reactDom.createPortal(
1504
+ /* @__PURE__ */ jsxRuntime.jsx(
1505
+ "div",
1506
+ {
1507
+ className: cn("ru-overlay", "ru-overlay--drawer", closing && "ru-overlay--closing"),
1508
+ onMouseDown: (e) => {
1509
+ if (!closing && closeOnOverlayClick && e.target === e.currentTarget) onClose();
1510
+ },
1511
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1512
+ "div",
1513
+ {
1514
+ ref: panelRef,
1515
+ role: "dialog",
1516
+ "aria-modal": "true",
1517
+ "aria-labelledby": content.title ? titleId : void 0,
1518
+ "aria-describedby": content.description ? descId : void 0,
1519
+ tabIndex: -1,
1520
+ className: cn(
1521
+ "ru-drawer",
1522
+ `ru-drawer--${placement}`,
1523
+ floating && "ru-drawer--floating",
1524
+ closing && "ru-drawer--closing",
1525
+ className
1526
+ ),
1527
+ style: { "--ru-drawer-size": resolvedSize },
1528
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1529
+ DialogContent,
1530
+ {
1531
+ ...content,
1532
+ titleId,
1533
+ descId,
1534
+ showCloseButton,
1535
+ onClose
1536
+ }
1537
+ )
1538
+ }
1539
+ )
1540
+ }
1541
+ ),
1542
+ document.body
1543
+ );
1544
+ }
1545
+ function Tooltip({
1546
+ content,
1547
+ children,
1548
+ placement = "top",
1549
+ delay = 100,
1550
+ size = "md",
1551
+ arrow = true,
1552
+ color,
1553
+ textColor,
1554
+ open: openProp,
1555
+ disabled,
1556
+ className
1557
+ }) {
1558
+ const [open, setOpen] = react.useState(false);
1559
+ const timer = react.useRef(void 0);
1560
+ const id = react.useId();
1561
+ const visible = (openProp ?? open) && !disabled && content != null && content !== "";
1562
+ react.useEffect(() => () => clearTimeout(timer.current), []);
1563
+ const show = () => {
1564
+ clearTimeout(timer.current);
1565
+ timer.current = setTimeout(() => setOpen(true), delay);
1566
+ };
1567
+ const hide = () => {
1568
+ clearTimeout(timer.current);
1569
+ setOpen(false);
1570
+ };
1571
+ const trigger = react.isValidElement(children) ? react.cloneElement(children, {
1572
+ "aria-describedby": visible ? id : void 0
1573
+ }) : children;
1574
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1575
+ "span",
1576
+ {
1577
+ className: cn("ru-tooltip", className),
1578
+ onMouseEnter: show,
1579
+ onMouseLeave: hide,
1580
+ onFocus: show,
1581
+ onBlur: hide,
1582
+ onKeyDown: (e) => e.key === "Escape" && hide(),
1583
+ children: [
1584
+ trigger,
1585
+ visible && /* @__PURE__ */ jsxRuntime.jsx(
1586
+ "span",
1587
+ {
1588
+ role: "tooltip",
1589
+ id,
1590
+ className: cn(
1591
+ "ru-tooltip__bubble",
1592
+ `ru-tooltip__bubble--${placement}`,
1593
+ size !== "md" && `ru-tooltip__bubble--${size}`,
1594
+ !arrow && "ru-tooltip__bubble--no-arrow"
1595
+ ),
1596
+ style: color || textColor ? { "--ru-tooltip-bg": color, "--ru-tooltip-text": textColor } : void 0,
1597
+ children: content
1598
+ }
1599
+ )
1600
+ ]
1601
+ }
1602
+ );
1603
+ }
1604
+ function Dropdown({ trigger, items, align = "start", closeOnSelect = true, className }) {
1605
+ const [open, setOpen] = react.useState(false);
1606
+ const rootRef = react.useRef(null);
1607
+ const menuRef = react.useRef(null);
1608
+ const menuId = react.useId();
1609
+ useClickOutside(rootRef, () => setOpen(false), open);
1610
+ const getItems = () => {
1611
+ var _a;
1612
+ return Array.from(((_a = menuRef.current) == null ? void 0 : _a.querySelectorAll('[role="menuitem"]:not(:disabled)')) ?? []);
1613
+ };
1614
+ const focusItem = (index) => {
1615
+ const els = getItems();
1616
+ if (els.length) els[(index + els.length) % els.length].focus();
1617
+ };
1618
+ const closeAndFocusTrigger = () => {
1619
+ var _a, _b;
1620
+ setOpen(false);
1621
+ (_b = (_a = rootRef.current) == null ? void 0 : _a.querySelector("[aria-haspopup]")) == null ? void 0 : _b.focus();
1622
+ };
1623
+ react.useEffect(() => {
1624
+ if (open) focusItem(0);
1625
+ }, [open]);
1626
+ const onMenuKeyDown = (e) => {
1627
+ const els = getItems();
1628
+ const index = els.indexOf(document.activeElement);
1629
+ switch (e.key) {
1630
+ case "ArrowDown":
1631
+ e.preventDefault();
1632
+ focusItem(index + 1);
1633
+ break;
1634
+ case "ArrowUp":
1635
+ e.preventDefault();
1636
+ focusItem(index - 1);
1637
+ break;
1638
+ case "Home":
1639
+ e.preventDefault();
1640
+ focusItem(0);
1641
+ break;
1642
+ case "End":
1643
+ e.preventDefault();
1644
+ focusItem(-1);
1645
+ break;
1646
+ case "Escape":
1647
+ e.preventDefault();
1648
+ closeAndFocusTrigger();
1649
+ break;
1650
+ case "Tab":
1651
+ setOpen(false);
1652
+ break;
1653
+ }
1654
+ };
1655
+ const triggerProps = trigger.props;
1656
+ const triggerEl = react.cloneElement(trigger, {
1657
+ "aria-haspopup": "menu",
1658
+ "aria-expanded": open,
1659
+ "aria-controls": open ? menuId : void 0,
1660
+ onClick: (e) => {
1661
+ var _a;
1662
+ (_a = triggerProps.onClick) == null ? void 0 : _a.call(triggerProps, e);
1663
+ setOpen((v) => !v);
1664
+ },
1665
+ onKeyDown: (e) => {
1666
+ var _a;
1667
+ (_a = triggerProps.onKeyDown) == null ? void 0 : _a.call(triggerProps, e);
1668
+ if (e.key === "ArrowDown" && !open) {
1669
+ e.preventDefault();
1670
+ setOpen(true);
1671
+ }
1672
+ }
1673
+ });
1674
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: rootRef, className: cn("ru-dropdown", className), children: [
1675
+ triggerEl,
1676
+ open && /* @__PURE__ */ jsxRuntime.jsx(
1677
+ "div",
1678
+ {
1679
+ ref: menuRef,
1680
+ id: menuId,
1681
+ role: "menu",
1682
+ className: cn("ru-dropdown__menu", align === "end" && "ru-dropdown__menu--end"),
1683
+ onKeyDown: onMenuKeyDown,
1684
+ children: items.map(
1685
+ (item, i) => item.divider ? /* @__PURE__ */ jsxRuntime.jsx("div", { role: "separator", className: "ru-dropdown__divider" }, item.key ?? i) : /* @__PURE__ */ jsxRuntime.jsxs(
1686
+ "button",
1687
+ {
1688
+ type: "button",
1689
+ role: "menuitem",
1690
+ tabIndex: -1,
1691
+ disabled: item.disabled,
1692
+ className: cn("ru-dropdown__item", item.danger && "ru-dropdown__item--danger"),
1693
+ onClick: () => {
1694
+ var _a;
1695
+ (_a = item.onClick) == null ? void 0 : _a.call(item);
1696
+ if (closeOnSelect) closeAndFocusTrigger();
1697
+ },
1698
+ children: [
1699
+ item.icon && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ru-dropdown__icon", children: item.icon }),
1700
+ item.label
1701
+ ]
1702
+ },
1703
+ item.key ?? i
1704
+ )
1705
+ )
1706
+ }
1707
+ )
1708
+ ] });
1709
+ }
1710
+ const ToastContext = react.createContext(null);
1711
+ function ToastProvider({ children, position = "bottom-right", duration = 4e3, max = 5 }) {
1712
+ const [toasts, setToasts] = react.useState([]);
1713
+ const mounted = useMounted();
1714
+ const counter = react.useRef(0);
1715
+ const dismiss = react.useCallback((id) => {
1716
+ setToasts((list) => id ? list.filter((t) => t.id !== id) : []);
1717
+ }, []);
1718
+ const api = react.useMemo(() => {
1719
+ const show = (options) => {
1720
+ const opts = typeof options === "string" ? { title: options } : options;
1721
+ const id = opts.id ?? `ru-toast-${++counter.current}`;
1722
+ setToasts((list) => [...list.filter((t) => t.id !== id), { ...opts, id }].slice(-max));
1723
+ return id;
1724
+ };
1725
+ const fn = show;
1726
+ fn.success = (title, o) => show({ ...o, title, variant: "success" });
1727
+ fn.error = (title, o) => show({ ...o, title, variant: "danger" });
1728
+ fn.warning = (title, o) => show({ ...o, title, variant: "warning" });
1729
+ fn.info = (title, o) => show({ ...o, title, variant: "info" });
1730
+ fn.dismiss = dismiss;
1731
+ return fn;
1732
+ }, [dismiss, max]);
1733
+ return /* @__PURE__ */ jsxRuntime.jsxs(ToastContext.Provider, { value: api, children: [
1734
+ children,
1392
1735
  mounted && reactDom.createPortal(
1393
1736
  /* @__PURE__ */ jsxRuntime.jsx("ol", { className: cn("ru-toaster", `ru-toaster--${position}`), "aria-live": "polite", "aria-label": "Notifications", children: toasts.map((t) => /* @__PURE__ */ jsxRuntime.jsx(ToastView, { toast: t, duration: t.duration ?? duration, onDismiss: dismiss }, t.id)) }),
1394
1737
  document.body
@@ -1431,14 +1774,68 @@ function useToast() {
1431
1774
  function Kbd({ className, ...rest }) {
1432
1775
  return /* @__PURE__ */ jsxRuntime.jsx("kbd", { className: cn("ru-kbd", className), ...rest });
1433
1776
  }
1434
- function Pattern({ variant = "grid", size = 24, fade = true, className, style, ...rest }) {
1435
- return /* @__PURE__ */ jsxRuntime.jsx(
1777
+ const patternSemanticColors = /* @__PURE__ */ new Set(["primary", "secondary", "success", "danger", "warning", "info"]);
1778
+ const toCssColor = (c) => patternSemanticColors.has(c) ? `var(--ru-${c})` : c;
1779
+ const toPx = (v) => typeof v === "number" ? `${v}px` : v;
1780
+ function Pattern({
1781
+ variant = "grid",
1782
+ size = 24,
1783
+ thickness,
1784
+ color,
1785
+ opacity,
1786
+ fade = true,
1787
+ animate,
1788
+ speed,
1789
+ spotlight = false,
1790
+ spotlightColor,
1791
+ spotlightSize,
1792
+ className,
1793
+ style,
1794
+ ...rest
1795
+ }) {
1796
+ const ref = react.useRef(null);
1797
+ react.useEffect(() => {
1798
+ const el = ref.current;
1799
+ const parent = el == null ? void 0 : el.parentElement;
1800
+ if (!spotlight || !parent) return void 0;
1801
+ const move = (e) => {
1802
+ const rect = el.getBoundingClientRect();
1803
+ el.style.setProperty("--ru-mx", `${e.clientX - rect.left}px`);
1804
+ el.style.setProperty("--ru-my", `${e.clientY - rect.top}px`);
1805
+ el.classList.add("ru-pattern--spot-on");
1806
+ };
1807
+ const leave = () => el.classList.remove("ru-pattern--spot-on");
1808
+ parent.addEventListener("pointermove", move);
1809
+ parent.addEventListener("pointerleave", leave);
1810
+ return () => {
1811
+ parent.removeEventListener("pointermove", move);
1812
+ parent.removeEventListener("pointerleave", leave);
1813
+ };
1814
+ }, [spotlight]);
1815
+ const fadeKind = fade === true ? "radial" : fade || null;
1816
+ const vars = {
1817
+ "--_ps": toPx(size),
1818
+ ...thickness !== void 0 && { "--_pt": toPx(thickness) },
1819
+ ...color && { "--_pc": toCssColor(color) },
1820
+ ...spotlightColor && { "--_spc": toCssColor(spotlightColor) },
1821
+ ...spotlightSize !== void 0 && { "--_sr": toPx(spotlightSize) },
1822
+ ...speed !== void 0 && { "--_speed": typeof speed === "number" ? `${speed}s` : speed },
1823
+ ...opacity !== void 0 && { opacity },
1824
+ ...style
1825
+ };
1826
+ const layerClass = `ru-pattern__layer ru-pattern__layer--${variant}`;
1827
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1436
1828
  "div",
1437
1829
  {
1830
+ ref,
1438
1831
  "aria-hidden": "true",
1439
- className: cn("ru-pattern", `ru-pattern--${variant}`, fade && "ru-pattern--fade", className),
1440
- style: { "--ru-pattern-size": `${size}px`, ...style },
1441
- ...rest
1832
+ className: cn("ru-pattern", fadeKind && `ru-pattern--fade-${fadeKind}`, animate && `ru-pattern--${animate}`, className),
1833
+ style: vars,
1834
+ ...rest,
1835
+ children: [
1836
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: layerClass }),
1837
+ spotlight && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ru-pattern__spot", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: layerClass }) })
1838
+ ]
1442
1839
  }
1443
1840
  );
1444
1841
  }
@@ -1465,6 +1862,198 @@ function SpotlightCard({ color, className, style, onMouseMove, children, ...rest
1465
1862
  }
1466
1863
  );
1467
1864
  }
1865
+ const semanticColors$1 = /* @__PURE__ */ new Set(["primary", "secondary", "success", "danger", "warning", "info"]);
1866
+ function Pager({ onPrev, onNext, prevLabel, nextLabel }) {
1867
+ if (!onPrev && !onNext) return null;
1868
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ru-frame-card__pager", children: [
1869
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", "aria-label": prevLabel, onClick: onPrev, disabled: !onPrev, children: /* @__PURE__ */ jsxRuntime.jsx(ChevronLeftIcon, {}) }),
1870
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", "aria-label": nextLabel, onClick: onNext, disabled: !onNext, children: /* @__PURE__ */ jsxRuntime.jsx(ChevronRightIcon, {}) })
1871
+ ] });
1872
+ }
1873
+ function FrameCard({
1874
+ title,
1875
+ color = "warning",
1876
+ textColor,
1877
+ titlePlacement = "panel",
1878
+ collapsible = false,
1879
+ open,
1880
+ defaultOpen = true,
1881
+ onOpenChange,
1882
+ headerAction,
1883
+ footer,
1884
+ onPrev,
1885
+ onNext,
1886
+ prevLabel = "Previous",
1887
+ nextLabel = "Next",
1888
+ className,
1889
+ style,
1890
+ children,
1891
+ ...rest
1892
+ }) {
1893
+ const [isOpen, setOpen] = useControllableState(open, defaultOpen, onOpenChange);
1894
+ const bodyId = react.useId();
1895
+ const onFrame = titlePlacement === "frame";
1896
+ const pager = /* @__PURE__ */ jsxRuntime.jsx(Pager, { onPrev, onNext, prevLabel, nextLabel });
1897
+ const hasPager = Boolean(onPrev || onNext);
1898
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1899
+ "section",
1900
+ {
1901
+ className: cn("ru-frame-card", onFrame && "ru-frame-card--title-frame", className),
1902
+ style: {
1903
+ "--_fc": semanticColors$1.has(color) ? `var(--ru-${color})` : color,
1904
+ ...textColor && { "--_fc-on": textColor, "--_fc-ink": textColor },
1905
+ ...style
1906
+ },
1907
+ ...rest,
1908
+ children: [
1909
+ onFrame && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ru-frame-card__frame-head", children: [
1910
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "ru-frame-card__title", children: title }),
1911
+ headerAction ?? pager
1912
+ ] }),
1913
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ru-frame-card__panel", children: [
1914
+ !onFrame && (title || headerAction || collapsible) && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ru-frame-card__head", children: [
1915
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "ru-frame-card__title", children: title }),
1916
+ headerAction,
1917
+ collapsible && /* @__PURE__ */ jsxRuntime.jsx(
1918
+ "button",
1919
+ {
1920
+ type: "button",
1921
+ className: "ru-frame-card__toggle",
1922
+ "aria-expanded": isOpen,
1923
+ "aria-controls": bodyId,
1924
+ "aria-label": isOpen ? "Collapse" : "Expand",
1925
+ onClick: () => setOpen(!isOpen),
1926
+ children: /* @__PURE__ */ jsxRuntime.jsx(ChevronDownIcon, {})
1927
+ }
1928
+ )
1929
+ ] }),
1930
+ /* @__PURE__ */ jsxRuntime.jsx("div", { id: bodyId, className: cn("ru-frame-card__collapse", !isOpen && "ru-frame-card__collapse--closed"), children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ru-frame-card__inner", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ru-frame-card__body", children }) }) })
1931
+ ] }),
1932
+ (footer || !onFrame && hasPager) && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ru-frame-card__foot", children: [
1933
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ru-frame-card__foot-content", children: footer }),
1934
+ !onFrame && pager
1935
+ ] })
1936
+ ]
1937
+ }
1938
+ );
1939
+ }
1940
+ const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
1941
+ const WEEKDAYS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
1942
+ const semanticColors = /* @__PURE__ */ new Set(["primary", "secondary", "success", "danger", "warning", "info"]);
1943
+ const dayKey = (d) => d.getFullYear() * 1e4 + d.getMonth() * 100 + d.getDate();
1944
+ const sameDay = (a, b) => Boolean(a && b) && dayKey(a) === dayKey(b);
1945
+ function Calendar({
1946
+ mode = "single",
1947
+ value,
1948
+ defaultValue,
1949
+ onChange,
1950
+ defaultMonth,
1951
+ weekStartsOn = 1,
1952
+ minYear,
1953
+ maxYear,
1954
+ color,
1955
+ today = /* @__PURE__ */ new Date(),
1956
+ className,
1957
+ style,
1958
+ ...rest
1959
+ }) {
1960
+ const emptyValue = mode === "range" ? { start: null, end: null } : null;
1961
+ const [selected, setSelected] = useControllableState(value, defaultValue ?? emptyValue, onChange);
1962
+ const anchor = defaultMonth ?? (mode === "range" ? selected == null ? void 0 : selected.start : selected) ?? today;
1963
+ const [view, setView] = react.useState({ y: anchor.getFullYear(), m: anchor.getMonth() });
1964
+ const gridRef = react.useRef(null);
1965
+ const shift = (delta) => setView((v) => {
1966
+ const d = new Date(v.y, v.m + delta, 1);
1967
+ return { y: d.getFullYear(), m: d.getMonth() };
1968
+ });
1969
+ const days = react.useMemo(() => {
1970
+ const offset = (new Date(view.y, view.m, 1).getDay() - weekStartsOn + 7) % 7;
1971
+ const inMonth = new Date(view.y, view.m + 1, 0).getDate();
1972
+ const cells = Math.ceil((offset + inMonth) / 7) * 7;
1973
+ return Array.from({ length: cells }, (_, i) => new Date(view.y, view.m, 1 - offset + i));
1974
+ }, [view, weekStartsOn]);
1975
+ const weekdays = Array.from({ length: 7 }, (_, i) => WEEKDAYS[(i + weekStartsOn) % 7]);
1976
+ const firstYear = minYear ?? view.y - 10;
1977
+ const lastYear = maxYear ?? view.y + 10;
1978
+ const years = Array.from({ length: lastYear - firstYear + 1 }, (_, i) => firstYear + i);
1979
+ const isSelected = (d) => {
1980
+ if (mode !== "range") return sameDay(d, selected);
1981
+ const { start, end } = selected ?? {};
1982
+ if (start && end) return dayKey(d) >= dayKey(start) && dayKey(d) <= dayKey(end);
1983
+ return sameDay(d, start);
1984
+ };
1985
+ const pick = (d) => {
1986
+ if (d.getMonth() !== view.m) setView({ y: d.getFullYear(), m: d.getMonth() });
1987
+ if (mode !== "range") return setSelected(d);
1988
+ const { start, end } = selected ?? {};
1989
+ if (!start || end) return setSelected({ start: d, end: null });
1990
+ return setSelected(dayKey(d) < dayKey(start) ? { start: d, end: start } : { start, end: d });
1991
+ };
1992
+ const onGridKeyDown = (e) => {
1993
+ const step = { ArrowLeft: -1, ArrowRight: 1, ArrowUp: -7, ArrowDown: 7 }[e.key];
1994
+ if (!step) return;
1995
+ const buttons = Array.from(gridRef.current.querySelectorAll("button"));
1996
+ const next = buttons[buttons.indexOf(document.activeElement) + step];
1997
+ if (next) {
1998
+ e.preventDefault();
1999
+ next.focus();
2000
+ }
2001
+ };
2002
+ const ink = color ? semanticColors.has(color) ? `var(--ru-${color})` : color : void 0;
2003
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("ru-calendar", className), style: ink ? { "--_cc": ink, ...style } : style, ...rest, children: [
2004
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ru-calendar__head", children: [
2005
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "ru-calendar__nav", "aria-label": "Previous month", onClick: () => shift(-1), children: /* @__PURE__ */ jsxRuntime.jsx(ChevronLeftIcon, {}) }),
2006
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ru-calendar__pickers", children: [
2007
+ /* @__PURE__ */ jsxRuntime.jsx(
2008
+ Select,
2009
+ {
2010
+ size: "sm",
2011
+ "aria-label": "Month",
2012
+ value: view.m,
2013
+ onChange: (m) => setView((v) => ({ ...v, m })),
2014
+ options: MONTHS.map((label, i) => ({ label, value: i })),
2015
+ containerClassName: "ru-calendar__picker"
2016
+ }
2017
+ ),
2018
+ /* @__PURE__ */ jsxRuntime.jsx(
2019
+ Select,
2020
+ {
2021
+ size: "sm",
2022
+ "aria-label": "Year",
2023
+ value: view.y,
2024
+ onChange: (y) => setView((v) => ({ ...v, y })),
2025
+ options: years.map((y) => ({ label: String(y), value: y })),
2026
+ containerClassName: "ru-calendar__picker ru-calendar__picker--year"
2027
+ }
2028
+ )
2029
+ ] }),
2030
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "ru-calendar__nav", "aria-label": "Next month", onClick: () => shift(1), children: /* @__PURE__ */ jsxRuntime.jsx(ChevronRightIcon, {}) })
2031
+ ] }),
2032
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ru-calendar__weekdays", "aria-hidden": "true", children: weekdays.map((w) => /* @__PURE__ */ jsxRuntime.jsx("span", { children: w }, w)) }),
2033
+ /* @__PURE__ */ jsxRuntime.jsx("div", { ref: gridRef, className: "ru-calendar__grid", onKeyDown: onGridKeyDown, children: days.map((d) => {
2034
+ const outside = d.getMonth() !== view.m;
2035
+ const selectedDay = isSelected(d);
2036
+ return /* @__PURE__ */ jsxRuntime.jsx(
2037
+ "button",
2038
+ {
2039
+ type: "button",
2040
+ className: cn(
2041
+ "ru-calendar__cell",
2042
+ outside && "ru-calendar__cell--outside",
2043
+ selectedDay && "ru-calendar__cell--selected",
2044
+ sameDay(d, today) && "ru-calendar__cell--today"
2045
+ ),
2046
+ "aria-label": d.toDateString(),
2047
+ "aria-pressed": selectedDay,
2048
+ "aria-current": sameDay(d, today) ? "date" : void 0,
2049
+ onClick: () => pick(d),
2050
+ children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ru-calendar__day", children: d.getDate() })
2051
+ },
2052
+ dayKey(d)
2053
+ );
2054
+ }) })
2055
+ ] });
2056
+ }
1468
2057
  exports.Accordion = Accordion;
1469
2058
  exports.Alert = Alert;
1470
2059
  exports.Avatar = Avatar;
@@ -1472,6 +2061,8 @@ exports.AvatarGroup = AvatarGroup;
1472
2061
  exports.Badge = Badge;
1473
2062
  exports.Breadcrumbs = Breadcrumbs;
1474
2063
  exports.Button = Button;
2064
+ exports.ButtonGroup = ButtonGroup;
2065
+ exports.Calendar = Calendar;
1475
2066
  exports.Card = Card;
1476
2067
  exports.CardBody = CardBody;
1477
2068
  exports.CardFooter = CardFooter;
@@ -1480,6 +2071,7 @@ exports.Checkbox = Checkbox;
1480
2071
  exports.Divider = Divider;
1481
2072
  exports.Drawer = Drawer;
1482
2073
  exports.Dropdown = Dropdown;
2074
+ exports.FrameCard = FrameCard;
1483
2075
  exports.Input = Input;
1484
2076
  exports.Kbd = Kbd;
1485
2077
  exports.Modal = Modal;