@nubitio/ui 0.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.
package/dist/index.cjs ADDED
@@ -0,0 +1,2396 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region \0rolldown/runtime.js
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
+ key = keys[i];
12
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
13
+ get: ((k) => from[k]).bind(null, key),
14
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
15
+ });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
20
+ value: mod,
21
+ enumerable: true
22
+ }) : target, mod));
23
+ //#endregion
24
+ let react = require("react");
25
+ react = __toESM(react, 1);
26
+ let react_jsx_runtime = require("react/jsx-runtime");
27
+ let react_dom = require("react-dom");
28
+ //#region packages/ui/Avatar.tsx
29
+ /**
30
+ * 8 semantic hues aligned with the Fluent / @nubitio design palette.
31
+ * Expressed as HSL so the lightness can be adjusted for dark mode via CSS.
32
+ * The color is set as --avatar-hue on the element and consumed in Avatar.scss.
33
+ */
34
+ const AVATAR_HUES = [
35
+ 218,
36
+ 148,
37
+ 262,
38
+ 192,
39
+ 36,
40
+ 203,
41
+ 22,
42
+ 340
43
+ ];
44
+ const NAMED_SIZES = {
45
+ xs: 20,
46
+ sm: 24,
47
+ md: 32,
48
+ lg: 40,
49
+ xl: 48
50
+ };
51
+ function getAvatarInitials(owner = "") {
52
+ const normalized = owner.trim();
53
+ if (!normalized) return "NA";
54
+ const words = normalized.split(/\s+/);
55
+ if (words.length >= 2) return `${words[0][0]}${words[1][0]}`.toUpperCase();
56
+ return normalized.slice(0, 2).toUpperCase();
57
+ }
58
+ function getAvatarHue(owner = "") {
59
+ let hash = 0;
60
+ for (let i = 0; i < owner.length; i++) hash = hash * 31 + owner.charCodeAt(i) & 268435455;
61
+ return AVATAR_HUES[Math.abs(hash) % AVATAR_HUES.length];
62
+ }
63
+ const Avatar = ({ owner = "", size, variant = "md", shape = "circle", alt, className }) => {
64
+ const px = size ?? NAMED_SIZES[variant];
65
+ const initials = getAvatarInitials(owner);
66
+ const hue = getAvatarHue(owner);
67
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
68
+ className: [
69
+ "nb-avatar",
70
+ shape === "square" && "nb-avatar--square",
71
+ className
72
+ ].filter(Boolean).join(" "),
73
+ role: "img",
74
+ "aria-label": alt ?? (owner || "Avatar"),
75
+ style: {
76
+ width: px,
77
+ height: px,
78
+ fontSize: Math.round(px * .38),
79
+ "--avatar-hue": hue
80
+ },
81
+ children: initials
82
+ });
83
+ };
84
+ //#endregion
85
+ //#region packages/ui/Button.tsx
86
+ const cx$4 = (...values) => values.filter(Boolean).join(" ");
87
+ const Button = ({ variant = "secondary", size = "md", fullWidth = false, icon, loading = false, className, children, disabled, ...props }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
88
+ ...props,
89
+ type: props.type ?? "button",
90
+ disabled: disabled || loading,
91
+ "aria-busy": loading || void 0,
92
+ className: cx$4("nb-button", `nb-button--${variant}`, size === "sm" && "nb-button--sm", fullWidth && "nb-button--full", className),
93
+ children: [loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
94
+ className: "nb-button-spinner",
95
+ "aria-hidden": "true"
96
+ }) : icon ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
97
+ className: icon,
98
+ "aria-hidden": "true"
99
+ }) : null, children]
100
+ });
101
+ const IconButton = ({ icon, label, variant = "default", className, ...props }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
102
+ ...props,
103
+ type: props.type ?? "button",
104
+ "aria-label": props["aria-label"] ?? label,
105
+ title: props.title ?? label,
106
+ className: cx$4("nb-icon-button", variant === "danger" && "nb-icon-button--danger", className),
107
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
108
+ className: icon,
109
+ "aria-hidden": "true"
110
+ })
111
+ });
112
+ //#endregion
113
+ //#region packages/ui/UiStrings.tsx
114
+ const EN_UI_STRINGS = {
115
+ close: "Close",
116
+ closeDialog: "Close dialog",
117
+ closePanel: "Close panel",
118
+ clearDate: "Clear date",
119
+ clearDateRange: "Clear date range",
120
+ openCalendar: "Open calendar",
121
+ selectDate: "Select date",
122
+ selectDateRange: "Select date range",
123
+ previousMonth: "Previous month",
124
+ nextMonth: "Next month",
125
+ previousYear: "Previous year",
126
+ nextYear: "Next year",
127
+ selectMonthAndYear: "Select month and year",
128
+ selectYear: "Select year",
129
+ clear: "Clear",
130
+ today: "Today",
131
+ cancel: "Cancel",
132
+ confirm: "Confirm",
133
+ back: "Back",
134
+ previousYears: "Previous years",
135
+ nextYears: "Next years"
136
+ };
137
+ const ES_UI_STRINGS = {
138
+ close: "Cerrar",
139
+ closeDialog: "Cerrar diálogo",
140
+ closePanel: "Cerrar panel",
141
+ clearDate: "Limpiar fecha",
142
+ clearDateRange: "Limpiar rango",
143
+ openCalendar: "Abrir calendario",
144
+ selectDate: "Seleccionar fecha",
145
+ selectDateRange: "Seleccionar rango de fechas",
146
+ previousMonth: "Mes anterior",
147
+ nextMonth: "Mes siguiente",
148
+ previousYear: "Año anterior",
149
+ nextYear: "Año siguiente",
150
+ selectMonthAndYear: "Seleccionar mes y año",
151
+ selectYear: "Seleccionar año",
152
+ clear: "Limpiar",
153
+ today: "Hoy",
154
+ cancel: "Cancelar",
155
+ confirm: "Confirmar",
156
+ back: "Volver",
157
+ previousYears: "Años anteriores",
158
+ nextYears: "Años siguientes"
159
+ };
160
+ const UiStringsContext = (0, react.createContext)(EN_UI_STRINGS);
161
+ const UiStringsProvider = ({ strings, children }) => {
162
+ const value = (0, react.useMemo)(() => ({
163
+ ...EN_UI_STRINGS,
164
+ ...strings
165
+ }), [strings]);
166
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UiStringsContext.Provider, {
167
+ value,
168
+ children
169
+ });
170
+ };
171
+ const useUiStrings = () => (0, react.useContext)(UiStringsContext);
172
+ //#endregion
173
+ //#region packages/ui/AppDialog.tsx
174
+ const formatSize = (value) => {
175
+ if (value === void 0) return void 0;
176
+ return typeof value === "number" ? `${value}px` : value;
177
+ };
178
+ const joinClassNames = (...values) => values.filter(Boolean).join(" ");
179
+ const AppDialog = ({ visible, open, title, onClose, children, footer, width, maxWidth, height, fullScreen = false, showCloseButton = true, closeLabel, scrimLabel, closeOnOutsideClick = true, closeOnEscape = true, restoreFocus = true, keepMounted = false, rootClassName, className, headerClassName, titleClassName, bodyClassName, footerClassName, role = "dialog" }) => {
180
+ const strings = useUiStrings();
181
+ const resolvedCloseLabel = closeLabel ?? strings.close;
182
+ const resolvedScrimLabel = scrimLabel ?? strings.closeDialog;
183
+ const isVisible = visible ?? open ?? false;
184
+ const titleId = (0, react.useId)();
185
+ const dialogRef = (0, react.useRef)(null);
186
+ const lastFocusedElementRef = (0, react.useRef)(null);
187
+ const restoreFocusRef = (0, react.useRef)(restoreFocus);
188
+ (0, react.useEffect)(() => {
189
+ restoreFocusRef.current = restoreFocus;
190
+ }, [restoreFocus]);
191
+ (0, react.useEffect)(() => {
192
+ if (!isVisible) return;
193
+ lastFocusedElementRef.current = document.activeElement;
194
+ window.setTimeout(() => {
195
+ (dialogRef.current?.querySelector("button:not(:disabled), [href], input:not(:disabled), select:not(:disabled), textarea:not(:disabled), [tabindex]:not([tabindex=\"-1\"])"))?.focus();
196
+ });
197
+ return () => {
198
+ if (restoreFocusRef.current) lastFocusedElementRef.current?.focus?.();
199
+ };
200
+ }, [isVisible]);
201
+ (0, react.useEffect)(() => {
202
+ if (!isVisible || !closeOnEscape) return;
203
+ const handleKeyDown = (event) => {
204
+ if (event.key === "Escape") {
205
+ event.preventDefault();
206
+ onClose();
207
+ }
208
+ };
209
+ document.addEventListener("keydown", handleKeyDown);
210
+ return () => document.removeEventListener("keydown", handleKeyDown);
211
+ }, [
212
+ closeOnEscape,
213
+ onClose,
214
+ isVisible
215
+ ]);
216
+ if (!isVisible && !keepMounted) return null;
217
+ return (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
218
+ className: joinClassNames("nb-dialog-root", rootClassName),
219
+ "aria-hidden": !isVisible,
220
+ style: {
221
+ pointerEvents: isVisible ? void 0 : "none",
222
+ visibility: isVisible ? void 0 : "hidden"
223
+ },
224
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
225
+ className: "nb-dialog__scrim",
226
+ type: "button",
227
+ "aria-label": resolvedScrimLabel,
228
+ onClick: closeOnOutsideClick ? onClose : void 0
229
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
230
+ className: joinClassNames("nb-dialog", fullScreen && "nb-dialog--fullscreen", className),
231
+ role,
232
+ "aria-modal": "true",
233
+ "aria-labelledby": titleId,
234
+ ref: dialogRef,
235
+ onKeyDownCapture: (event) => {
236
+ if (!closeOnEscape && event.key === "Escape") {
237
+ event.preventDefault();
238
+ event.stopPropagation();
239
+ }
240
+ },
241
+ style: {
242
+ width: formatSize(width),
243
+ maxWidth: formatSize(maxWidth),
244
+ height: formatSize(height)
245
+ },
246
+ children: [
247
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
248
+ className: joinClassNames("nb-dialog__header", headerClassName),
249
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
250
+ className: joinClassNames("nb-dialog__title", titleClassName),
251
+ id: titleId,
252
+ children: title
253
+ }), showCloseButton && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconButton, {
254
+ className: "nb-dialog__close",
255
+ icon: "ph ph-x",
256
+ label: resolvedCloseLabel,
257
+ onClick: onClose
258
+ })]
259
+ }),
260
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
261
+ className: joinClassNames("nb-dialog__body", bodyClassName),
262
+ children
263
+ }),
264
+ footer && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
265
+ className: joinClassNames("nb-dialog__footer", footerClassName),
266
+ children: footer
267
+ })
268
+ ]
269
+ })]
270
+ }), document.body);
271
+ };
272
+ //#endregion
273
+ //#region packages/ui/ConfirmDialog.tsx
274
+ function ConfirmDialog({ open, title, message, onConfirm, onCancel, confirmLabel, cancelLabel, variant = "warning" }) {
275
+ const strings = useUiStrings();
276
+ const resolvedConfirmLabel = confirmLabel ?? strings.confirm;
277
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AppDialog, {
278
+ open,
279
+ title,
280
+ onClose: onCancel,
281
+ maxWidth: 420,
282
+ role: "alertdialog",
283
+ footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
284
+ variant: "secondary",
285
+ onClick: onCancel,
286
+ autoFocus: true,
287
+ children: cancelLabel ?? strings.cancel
288
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
289
+ variant: variant === "danger" ? "danger" : "primary",
290
+ onClick: onConfirm,
291
+ children: resolvedConfirmLabel
292
+ })] }),
293
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
294
+ className: "nb-confirm-dialog__message",
295
+ children: message
296
+ })
297
+ });
298
+ }
299
+ //#endregion
300
+ //#region packages/ui/AppToolbar.tsx
301
+ const AppToolbar = ({ title, additionalToolbarContent, showRefreshButton = true, onRefresh, icon, refreshLabel = "Refresh", children }) => {
302
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
303
+ className: "view-wrapper view-wrapper-dashboard",
304
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
305
+ className: "nb-toolbar theme-dependent",
306
+ children: [
307
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
308
+ className: "nb-toolbar__start",
309
+ children: [icon && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
310
+ className: icon,
311
+ "aria-hidden": "true"
312
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
313
+ className: "toolbar-header",
314
+ children: title
315
+ })]
316
+ }),
317
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
318
+ className: "nb-toolbar__center",
319
+ children: additionalToolbarContent
320
+ }),
321
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
322
+ className: "nb-toolbar__end",
323
+ children: showRefreshButton && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconButton, {
324
+ icon: "ph ph-arrow-clockwise",
325
+ label: refreshLabel,
326
+ onClick: onRefresh
327
+ })
328
+ })
329
+ ]
330
+ }), children]
331
+ });
332
+ };
333
+ //#endregion
334
+ //#region packages/ui/Card.tsx
335
+ const Card = ({ title, description, children }) => {
336
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
337
+ className: "nb-card",
338
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
339
+ className: "nb-card__content content",
340
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
341
+ className: "header",
342
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
343
+ className: "title",
344
+ children: title
345
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
346
+ className: "description",
347
+ children: description
348
+ })]
349
+ }), children]
350
+ })
351
+ });
352
+ };
353
+ //#endregion
354
+ //#region packages/ui/FormControls.tsx
355
+ const cx$3 = (...values) => values.filter(Boolean).join(" ");
356
+ const FormField = ({ label, error, helpText, children, className, ...props }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
357
+ ...props,
358
+ className: cx$3("nb-form-field", !!error && "nb-form-field--error", className),
359
+ children: [
360
+ label && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
361
+ className: "nb-form-field__label",
362
+ children: label
363
+ }),
364
+ children,
365
+ error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
366
+ className: "nb-form-field__error",
367
+ role: "alert",
368
+ children: error
369
+ }) : helpText ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
370
+ className: "nb-form-field__help",
371
+ children: helpText
372
+ }) : null
373
+ ]
374
+ });
375
+ const TextField = (0, react.forwardRef)(function TextField({ className, invalid, ...props }, ref) {
376
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
377
+ ...props,
378
+ ref,
379
+ className: cx$3("nb-input", invalid && "nb-input--invalid", className)
380
+ });
381
+ });
382
+ const SelectField = (0, react.forwardRef)(function SelectField({ className, invalid, children, ...props }, ref) {
383
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
384
+ ...props,
385
+ ref,
386
+ className: cx$3("nb-input", "nb-select", invalid && "nb-input--invalid", className),
387
+ children
388
+ });
389
+ });
390
+ const TextAreaField = (0, react.forwardRef)(function TextAreaField({ className, invalid, ...props }, ref) {
391
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
392
+ ...props,
393
+ ref,
394
+ className: cx$3("nb-input", "nb-textarea", invalid && "nb-input--invalid", className)
395
+ });
396
+ });
397
+ //#endregion
398
+ //#region packages/ui/AppDropdown.tsx
399
+ function cx$2(...values) {
400
+ return values.filter(Boolean).join(" ");
401
+ }
402
+ function normalizeIcon(icon) {
403
+ if (!icon) return void 0;
404
+ if (icon.startsWith("ph ")) return icon;
405
+ if (icon.startsWith("ph-")) return `ph ${icon}`;
406
+ return `ph ph-${icon}`;
407
+ }
408
+ function getMenuStyle(containerRef, menuMinWidth = 72) {
409
+ if (!containerRef.current) return {
410
+ position: "fixed",
411
+ visibility: "hidden",
412
+ margin: 0
413
+ };
414
+ const rect = (containerRef.current.querySelector(".nb-dropdown__trigger") ?? containerRef.current).getBoundingClientRect();
415
+ const menuWidth = Math.max(rect.width, menuMinWidth);
416
+ const left = Math.max(8, Math.min(rect.left, window.innerWidth - menuWidth - 8));
417
+ const spaceBelow = window.innerHeight - rect.bottom;
418
+ const spaceAbove = rect.top;
419
+ if (spaceBelow < 250 && spaceAbove > spaceBelow) return {
420
+ position: "fixed",
421
+ left: `${left}px`,
422
+ width: `${menuWidth}px`,
423
+ bottom: `${window.innerHeight - rect.top + 4}px`,
424
+ top: "auto",
425
+ margin: 0,
426
+ zIndex: 9999,
427
+ visibility: "visible"
428
+ };
429
+ return {
430
+ position: "fixed",
431
+ left: `${left}px`,
432
+ width: `${menuWidth}px`,
433
+ top: `${rect.bottom + 4}px`,
434
+ bottom: "auto",
435
+ margin: 0,
436
+ zIndex: 9999,
437
+ visibility: "visible"
438
+ };
439
+ }
440
+ function AppDropdown({ id: idProp, label, icon, value, options, onChange, disabled = false, placeholder = "Seleccionar…", variant = "form", className, showFieldLabel = variant === "form", menuMinWidth, error, helpText }) {
441
+ const generatedId = (0, react.useId)();
442
+ const id = idProp ?? generatedId;
443
+ const [open, setOpen] = (0, react.useState)(false);
444
+ const containerRef = (0, react.useRef)(null);
445
+ const menuRef = (0, react.useRef)(null);
446
+ const [menuStyle, setMenuStyle] = (0, react.useState)({
447
+ position: "fixed",
448
+ visibility: "hidden",
449
+ margin: 0
450
+ });
451
+ const selected = options.find((option) => option.value === value);
452
+ const computeMenuStyle = (0, react.useCallback)(() => getMenuStyle(containerRef, menuMinWidth), [menuMinWidth]);
453
+ (0, react.useEffect)(() => {
454
+ if (!open) return;
455
+ const handler = (e) => {
456
+ const target = e.target;
457
+ if (!containerRef.current?.contains(target) && !menuRef.current?.contains(target)) setOpen(false);
458
+ };
459
+ document.addEventListener("mousedown", handler);
460
+ return () => document.removeEventListener("mousedown", handler);
461
+ }, [open]);
462
+ (0, react.useLayoutEffect)(() => {
463
+ if (!open) return;
464
+ const update = () => {
465
+ if (!containerRef.current) return;
466
+ const rect = containerRef.current.getBoundingClientRect();
467
+ if (rect.top >= window.innerHeight || rect.bottom <= 0) {
468
+ setOpen(false);
469
+ return;
470
+ }
471
+ setMenuStyle(computeMenuStyle());
472
+ };
473
+ update();
474
+ window.addEventListener("resize", update);
475
+ window.addEventListener("scroll", update, {
476
+ capture: true,
477
+ passive: true
478
+ });
479
+ return () => {
480
+ window.removeEventListener("resize", update);
481
+ window.removeEventListener("scroll", update, { capture: true });
482
+ };
483
+ }, [open, computeMenuStyle]);
484
+ const openMenu = (0, react.useCallback)(() => {
485
+ setMenuStyle(computeMenuStyle());
486
+ setOpen(true);
487
+ }, [computeMenuStyle]);
488
+ const control = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
489
+ ref: containerRef,
490
+ className: cx$2("nb-dropdown", `nb-dropdown--${variant}`, open && "nb-dropdown--open", disabled && "nb-dropdown--disabled", className),
491
+ role: "group",
492
+ "aria-labelledby": label ? `${id}-label` : void 0,
493
+ children: [
494
+ variant === "toolbar" && label && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
495
+ className: "nb-dropdown__toolbar-label",
496
+ id: `${id}-label`,
497
+ children: [icon && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
498
+ className: normalizeIcon(icon),
499
+ "aria-hidden": "true"
500
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label })]
501
+ }),
502
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
503
+ type: "button",
504
+ className: "nb-dropdown__trigger",
505
+ "aria-haspopup": "listbox",
506
+ "aria-expanded": open,
507
+ "aria-labelledby": label ? `${id}-label ${id}-value` : `${id}-value`,
508
+ disabled,
509
+ onClick: () => open ? setOpen(false) : openMenu(),
510
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
511
+ className: "nb-dropdown__value",
512
+ id: `${id}-value`,
513
+ children: selected?.selectedLabel ?? selected?.label ?? placeholder
514
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
515
+ className: "ph ph-caret-down nb-dropdown__caret",
516
+ "aria-hidden": "true"
517
+ })]
518
+ }),
519
+ open && !disabled && (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
520
+ ref: menuRef,
521
+ className: cx$2("nb-dropdown__menu", `nb-dropdown__menu--${variant}`),
522
+ role: "listbox",
523
+ "aria-labelledby": label ? `${id}-label` : void 0,
524
+ style: menuStyle,
525
+ children: options.map((option) => {
526
+ const isActive = option.value === value;
527
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
528
+ role: "presentation",
529
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
530
+ type: "button",
531
+ role: "option",
532
+ "aria-selected": isActive,
533
+ disabled: option.disabled,
534
+ className: cx$2("nb-dropdown__option", isActive && "nb-dropdown__option--active"),
535
+ onClick: () => {
536
+ if (option.disabled) return;
537
+ onChange(option.value);
538
+ setOpen(false);
539
+ },
540
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
541
+ className: "nb-dropdown__option-content",
542
+ children: [option.iconText && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
543
+ className: "nb-dropdown__option-icon",
544
+ "aria-hidden": "true",
545
+ children: option.iconText
546
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: option.label })]
547
+ })
548
+ })
549
+ }, option.value);
550
+ })
551
+ }), document.body)
552
+ ]
553
+ });
554
+ if (!showFieldLabel || variant === "toolbar") return control;
555
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
556
+ className: cx$2("nb-form-field", !!error && "nb-form-field--error"),
557
+ htmlFor: id,
558
+ children: [
559
+ label && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
560
+ className: "nb-form-field__label",
561
+ id: `${id}-label`,
562
+ children: label
563
+ }),
564
+ control,
565
+ error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
566
+ className: "nb-form-field__error",
567
+ role: "alert",
568
+ children: error
569
+ }) : helpText ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
570
+ className: "nb-form-field__help",
571
+ children: helpText
572
+ }) : null
573
+ ]
574
+ });
575
+ }
576
+ //#endregion
577
+ //#region packages/ui/theme/ThemeProvider.tsx
578
+ const ThemeContext = react.default.createContext(null);
579
+ function useTheme() {
580
+ const ctx = (0, react.useContext)(ThemeContext);
581
+ if (!ctx) throw new Error("useTheme must be used inside ThemeProvider");
582
+ return ctx;
583
+ }
584
+ const themeModes = [
585
+ "light",
586
+ "dark",
587
+ "auto"
588
+ ];
589
+ const themeValues = ["light", "dark"];
590
+ function getNextThemeMode(mode) {
591
+ return themeModes[(themeModes.indexOf(mode) + 1) % themeModes.length];
592
+ }
593
+ function getSystemTheme() {
594
+ return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
595
+ }
596
+ function resolveTheme(mode, systemTheme = getSystemTheme()) {
597
+ if (mode !== "auto") return mode;
598
+ return systemTheme;
599
+ }
600
+ const THEME_VERSION = typeof __THEME_VERSION__ !== "undefined" ? __THEME_VERSION__ : String(Date.now());
601
+ function setThemeToDOM(href) {
602
+ const versioned = `${href}?v=${THEME_VERSION}`;
603
+ return new Promise((resolve) => {
604
+ const id = "theme-link";
605
+ let link = document.getElementById(id);
606
+ if (link) {
607
+ if (link.getAttribute("data-theme-href") === href) {
608
+ resolve();
609
+ return;
610
+ }
611
+ link.href = versioned;
612
+ link.setAttribute("data-theme-href", href);
613
+ } else {
614
+ link = document.createElement("link");
615
+ link.id = id;
616
+ link.rel = "stylesheet";
617
+ link.href = versioned;
618
+ link.setAttribute("data-theme-href", href);
619
+ document.head.appendChild(link);
620
+ }
621
+ link.onload = () => resolve();
622
+ });
623
+ }
624
+ function applyThemeAttributes(theme, themePrefix) {
625
+ const root = document.documentElement;
626
+ root.dataset.theme = theme;
627
+ themeValues.forEach((value) => root.classList.remove(`${themePrefix}${value}`));
628
+ root.classList.add(`${themePrefix}${theme}`);
629
+ }
630
+ const ThemeProvider = ({ storageKey = "nb-theme", themePrefix = "nb-theme-", basePath = "/themes/", children }) => {
631
+ const getPersistedMode = () => {
632
+ const persisted = window.localStorage.getItem(storageKey);
633
+ return persisted && themeModes.includes(persisted) ? persisted : "auto";
634
+ };
635
+ const [mode, setMode] = (0, react.useState)(getPersistedMode);
636
+ const [systemTheme, setSystemTheme] = (0, react.useState)(getSystemTheme);
637
+ const [isLoaded, setIsLoaded] = (0, react.useState)(false);
638
+ const theme = (0, react.useMemo)(() => resolveTheme(mode, systemTheme), [mode, systemTheme]);
639
+ (0, react.useEffect)(() => {
640
+ applyThemeAttributes(theme, themePrefix);
641
+ setThemeToDOM(`${basePath}${themePrefix}${theme}.css`).then(() => setIsLoaded(true));
642
+ }, [
643
+ theme,
644
+ basePath,
645
+ themePrefix
646
+ ]);
647
+ (0, react.useEffect)(() => {
648
+ if (!isLoaded) return;
649
+ const meta = document.querySelector("meta[name=\"theme-color\"]");
650
+ if (meta) {
651
+ const baseBg = getComputedStyle(document.body).getPropertyValue("--base-bg").trim();
652
+ meta.setAttribute("content", baseBg || (theme === "dark" ? "#000000" : "#ffffff"));
653
+ }
654
+ }, [theme, isLoaded]);
655
+ (0, react.useEffect)(() => {
656
+ const mq = window.matchMedia("(prefers-color-scheme: dark)");
657
+ const apply = () => setSystemTheme(mq.matches ? "dark" : "light");
658
+ mq.addEventListener("change", apply);
659
+ return () => mq.removeEventListener("change", apply);
660
+ }, []);
661
+ const switchTheme = (0, react.useCallback)(() => {
662
+ setMode((prev) => {
663
+ const next = getNextThemeMode(prev);
664
+ window.localStorage.setItem(storageKey, next);
665
+ return next;
666
+ });
667
+ }, [storageKey]);
668
+ const value = (0, react.useMemo)(() => ({
669
+ theme,
670
+ mode,
671
+ switchTheme,
672
+ isLoaded
673
+ }), [
674
+ theme,
675
+ mode,
676
+ switchTheme,
677
+ isLoaded
678
+ ]);
679
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ThemeContext.Provider, {
680
+ value,
681
+ children
682
+ });
683
+ };
684
+ //#endregion
685
+ //#region packages/ui/theme/ThemeSwitcher.tsx
686
+ const iconByMode = {
687
+ light: "ph ph-sun",
688
+ dark: "ph ph-moon",
689
+ auto: "ph ph-desktop"
690
+ };
691
+ const labelByMode = {
692
+ light: "Theme: Light",
693
+ dark: "Theme: Dark",
694
+ auto: "Theme: System"
695
+ };
696
+ const ThemeSwitcher = ({ labelByMode: overrides } = {}) => {
697
+ const { mode, switchTheme } = useTheme();
698
+ const label = {
699
+ ...labelByMode,
700
+ ...overrides
701
+ }[mode];
702
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconButton, {
703
+ className: "theme-button",
704
+ icon: iconByMode[mode],
705
+ label,
706
+ onClick: switchTheme
707
+ }) });
708
+ };
709
+ //#endregion
710
+ //#region packages/ui/theme/DensityProvider.tsx
711
+ const DensityContext = (0, react.createContext)(null);
712
+ function useDensity() {
713
+ const ctx = (0, react.useContext)(DensityContext);
714
+ if (!ctx) throw new Error("useDensity must be used inside DensityProvider");
715
+ return ctx;
716
+ }
717
+ const STORAGE_KEY$1 = "app-density";
718
+ const DensityProvider = ({ children }) => {
719
+ const [density, setDensity] = (0, react.useState)(() => {
720
+ return localStorage.getItem(STORAGE_KEY$1) === "compact" ? "compact" : "normal";
721
+ });
722
+ (0, react.useEffect)(() => {
723
+ if (density === "compact") document.documentElement.dataset.density = "compact";
724
+ else delete document.documentElement.dataset.density;
725
+ localStorage.setItem(STORAGE_KEY$1, density);
726
+ }, [density]);
727
+ const toggleDensity = (0, react.useCallback)(() => {
728
+ setDensity((prev) => prev === "normal" ? "compact" : "normal");
729
+ }, []);
730
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DensityContext.Provider, {
731
+ value: {
732
+ density,
733
+ toggleDensity
734
+ },
735
+ children
736
+ });
737
+ };
738
+ //#endregion
739
+ //#region packages/ui/theme/useAccentColor.ts
740
+ const ACCENT_PRESETS = [
741
+ {
742
+ label: "Azure",
743
+ value: "#1a6fd4"
744
+ },
745
+ {
746
+ label: "Teal",
747
+ value: "#0d9488"
748
+ },
749
+ {
750
+ label: "Indigo",
751
+ value: "#4f46e5"
752
+ },
753
+ {
754
+ label: "Slate",
755
+ value: "#475569"
756
+ },
757
+ {
758
+ label: "Violet",
759
+ value: "#7c3aed"
760
+ },
761
+ {
762
+ label: "Rose",
763
+ value: "#e11d48"
764
+ },
765
+ {
766
+ label: "Amber",
767
+ value: "#d97706"
768
+ }
769
+ ];
770
+ const STORAGE_KEY = "app-accent-color";
771
+ const DEFAULT_ACCENT = "#1a6fd4";
772
+ function applyAccent(color) {
773
+ document.documentElement.style.setProperty("--accent-color", color);
774
+ }
775
+ function useAccentColor() {
776
+ const [accent, setAccent] = (0, react.useState)(() => {
777
+ return localStorage.getItem(STORAGE_KEY) ?? DEFAULT_ACCENT;
778
+ });
779
+ (0, react.useEffect)(() => {
780
+ if (localStorage.getItem(STORAGE_KEY)) applyAccent(accent);
781
+ }, [accent]);
782
+ return {
783
+ accent,
784
+ changeAccent: (0, react.useCallback)((color) => {
785
+ localStorage.setItem(STORAGE_KEY, color);
786
+ setAccent(color);
787
+ }, []),
788
+ presets: ACCENT_PRESETS
789
+ };
790
+ }
791
+ //#endregion
792
+ //#region packages/ui/SettingsPanel.tsx
793
+ const SettingsPanel = ({ onClose: _onClose }) => {
794
+ const { density, toggleDensity } = useDensity();
795
+ const { accent, changeAccent, presets } = useAccentColor();
796
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
797
+ className: "nb-settings-panel",
798
+ role: "menu",
799
+ "aria-label": "Display settings",
800
+ children: [
801
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
802
+ className: "nb-settings-panel__heading",
803
+ children: "Color de acento"
804
+ }),
805
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
806
+ className: "nb-settings-panel__swatches",
807
+ role: "group",
808
+ "aria-label": "Colores de acento",
809
+ children: presets.map((preset) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
810
+ className: "nb-settings-panel__swatch",
811
+ type: "button",
812
+ "aria-label": preset.label,
813
+ "aria-pressed": accent === preset.value,
814
+ title: preset.label,
815
+ style: {
816
+ background: preset.value,
817
+ color: preset.value
818
+ },
819
+ onClick: () => changeAccent(preset.value)
820
+ }, preset.value))
821
+ }),
822
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("hr", { className: "nb-settings-panel__divider" }),
823
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
824
+ className: "nb-settings-panel__heading",
825
+ children: "Densidad"
826
+ }),
827
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
828
+ className: "nb-settings-panel__density",
829
+ role: "group",
830
+ "aria-label": "Densidad de interfaz",
831
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
832
+ className: "nb-settings-panel__density-btn",
833
+ type: "button",
834
+ "aria-pressed": density === "normal",
835
+ onClick: () => density !== "normal" && toggleDensity(),
836
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
837
+ className: "ph ph-rows",
838
+ "aria-hidden": "true"
839
+ }), "Normal"]
840
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
841
+ className: "nb-settings-panel__density-btn",
842
+ type: "button",
843
+ "aria-pressed": density === "compact",
844
+ onClick: () => density !== "compact" && toggleDensity(),
845
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
846
+ className: "ph ph-rows-plus-bottom",
847
+ "aria-hidden": "true"
848
+ }), "Compact"]
849
+ })]
850
+ })
851
+ ]
852
+ });
853
+ };
854
+ //#endregion
855
+ //#region packages/ui/Badge.tsx
856
+ const Badge = ({ variant = "primary", size = "md", outlined = false, pill = true, dot = false, children, className }) => {
857
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
858
+ className: [
859
+ "nb-badge",
860
+ `nb-badge--${variant}`,
861
+ size === "sm" && "nb-badge--sm",
862
+ outlined && "nb-badge--outlined",
863
+ pill && "nb-badge--pill",
864
+ dot && "nb-badge--dot",
865
+ className
866
+ ].filter(Boolean).join(" "),
867
+ children: dot ? null : children
868
+ });
869
+ };
870
+ //#endregion
871
+ //#region packages/ui/Skeleton.tsx
872
+ const fmtSize = (v) => v === void 0 ? void 0 : typeof v === "number" ? `${v}px` : v;
873
+ const Skeleton = ({ variant = "rect", width, height, lines = 1, className }) => {
874
+ const base = [
875
+ "nb-skeleton",
876
+ `nb-skeleton--${variant}`,
877
+ className
878
+ ].filter(Boolean).join(" ");
879
+ if (variant === "text" && lines > 1) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
880
+ className: "nb-skeleton-lines",
881
+ children: Array.from({ length: lines }).map((_, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
882
+ className: base,
883
+ style: { width: i === lines - 1 ? "70%" : fmtSize(width) ?? "100%" },
884
+ "aria-hidden": "true"
885
+ }, i))
886
+ });
887
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
888
+ className: base,
889
+ style: {
890
+ width: fmtSize(width),
891
+ height: fmtSize(height)
892
+ },
893
+ "aria-hidden": "true"
894
+ });
895
+ };
896
+ //#endregion
897
+ //#region packages/ui/EmptyState.tsx
898
+ const DEFAULT_ICONS = {
899
+ default: "folder-open",
900
+ danger: "warning-circle",
901
+ warning: "warning",
902
+ success: "check-circle"
903
+ };
904
+ const EmptyState = ({ icon, variant = "default", title, description, action, size = "md", fill = false, className }) => {
905
+ const iconName = icon ?? DEFAULT_ICONS[variant];
906
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
907
+ className: [
908
+ "nb-empty-state",
909
+ `nb-empty-state--${variant}`,
910
+ `nb-empty-state--${size}`,
911
+ fill && "nb-empty-state--fill",
912
+ className
913
+ ].filter(Boolean).join(" "),
914
+ role: "status",
915
+ "aria-live": "polite",
916
+ children: [
917
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
918
+ className: "nb-empty-state__icon-wrap",
919
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
920
+ className: `ph ph-${iconName}`,
921
+ "aria-hidden": "true"
922
+ })
923
+ }),
924
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
925
+ className: "nb-empty-state__title",
926
+ children: title
927
+ }),
928
+ description && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
929
+ className: "nb-empty-state__description",
930
+ children: description
931
+ }),
932
+ action && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
933
+ className: "nb-empty-state__action",
934
+ children: action
935
+ })
936
+ ]
937
+ });
938
+ };
939
+ //#endregion
940
+ //#region packages/ui/Chip.tsx
941
+ /**
942
+ * Pill-shaped filter/tag chip. Stateless — caller controls `active`.
943
+ * Use for category filters, tag lists, and multi-select inputs.
944
+ */
945
+ const Chip = ({ label, active = false, count, icon, size = "md", disabled = false, onClick, className = "" }) => {
946
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
947
+ type: "button",
948
+ role: "checkbox",
949
+ "aria-checked": active,
950
+ "aria-pressed": active,
951
+ disabled,
952
+ className: `nb-chip${size !== "md" ? ` nb-chip--${size}` : ""}${active ? " is-active" : ""}${className ? ` ${className}` : ""}`,
953
+ onClick,
954
+ children: [
955
+ icon && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
956
+ className: icon,
957
+ "aria-hidden": "true"
958
+ }),
959
+ label,
960
+ count !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
961
+ className: "nb-chip__count",
962
+ children: count
963
+ })
964
+ ]
965
+ });
966
+ };
967
+ //#endregion
968
+ //#region packages/ui/Toggle.tsx
969
+ /**
970
+ * Accessible toggle switch. Always controlled — pass `checked` + `onChange`.
971
+ * Renders a visually-hidden `<input type="checkbox">` with a custom track.
972
+ *
973
+ * With `label`: wraps in `.nb-toggle-row` for a label ↔ toggle layout.
974
+ * Without `label`: renders bare `.nb-toggle`.
975
+ */
976
+ const Toggle = ({ checked, onChange, label, size = "md", disabled = false, "aria-label": ariaLabel, className = "" }) => {
977
+ const id = (0, react.useId)();
978
+ const input = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
979
+ className: `nb-toggle${size === "sm" ? " nb-toggle--sm" : ""}${className ? ` ${className}` : ""}`,
980
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
981
+ id,
982
+ type: "checkbox",
983
+ checked,
984
+ disabled,
985
+ "aria-label": !label ? ariaLabel : void 0,
986
+ onChange: (e) => onChange(e.target.checked)
987
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
988
+ className: "nb-toggle__track",
989
+ "aria-hidden": "true"
990
+ })]
991
+ });
992
+ if (!label) return input;
993
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
994
+ htmlFor: id,
995
+ className: "nb-toggle-row",
996
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label }), input]
997
+ });
998
+ };
999
+ //#endregion
1000
+ //#region packages/ui/ContextMenu.tsx
1001
+ /**
1002
+ * Three-dot overflow context menu.
1003
+ * Replaces the legacy `shared/ui/library/card-menu/CardMenu` component.
1004
+ *
1005
+ * Features over CardMenu:
1006
+ * - `danger` variant items
1007
+ * - `separator` support
1008
+ * - `icon` per item
1009
+ * - Custom trigger slot
1010
+ * - Left/right alignment
1011
+ * - Click-outside + Escape to close
1012
+ */
1013
+ const ContextMenu = ({ items, visible = true, trigger, triggerLabel = "Opciones", align = "right", className = "" }) => {
1014
+ const [open, setOpen] = (0, react.useState)(false);
1015
+ const containerRef = (0, react.useRef)(null);
1016
+ (0, react.useEffect)(() => {
1017
+ if (!open) return;
1018
+ const onMouseDown = (e) => {
1019
+ if (containerRef.current && !containerRef.current.contains(e.target)) setOpen(false);
1020
+ };
1021
+ const onKeyDown = (e) => {
1022
+ if (e.key === "Escape") setOpen(false);
1023
+ };
1024
+ document.addEventListener("mousedown", onMouseDown);
1025
+ document.addEventListener("keydown", onKeyDown);
1026
+ return () => {
1027
+ document.removeEventListener("mousedown", onMouseDown);
1028
+ document.removeEventListener("keydown", onKeyDown);
1029
+ };
1030
+ }, [open]);
1031
+ if (!visible) return null;
1032
+ const dropdownStyle = align === "left" ? {
1033
+ right: "auto",
1034
+ left: 0
1035
+ } : {};
1036
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1037
+ ref: containerRef,
1038
+ className: `nb-context-menu${className ? ` ${className}` : ""}`,
1039
+ children: [trigger ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1040
+ onClick: () => setOpen((v) => !v),
1041
+ children: trigger
1042
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconButton, {
1043
+ icon: "ph ph-dots-three",
1044
+ label: triggerLabel,
1045
+ "aria-haspopup": "menu",
1046
+ "aria-expanded": open,
1047
+ onClick: () => setOpen((v) => !v)
1048
+ }), open && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
1049
+ className: "nb-context-menu__dropdown",
1050
+ role: "menu",
1051
+ style: dropdownStyle,
1052
+ children: items.map((item, idx) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.default.Fragment, { children: [item.separator && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
1053
+ role: "none",
1054
+ "aria-hidden": "true",
1055
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "nb-context-menu__separator" })
1056
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
1057
+ role: "none",
1058
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1059
+ type: "button",
1060
+ role: "menuitem",
1061
+ disabled: item.disabled,
1062
+ "aria-disabled": item.disabled,
1063
+ className: `nb-context-menu__item${item.danger ? " nb-context-menu__item--danger" : ""}`,
1064
+ onClick: () => {
1065
+ setOpen(false);
1066
+ item.onClick?.();
1067
+ },
1068
+ children: [item.icon && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
1069
+ className: item.icon,
1070
+ "aria-hidden": "true"
1071
+ }), item.label]
1072
+ })
1073
+ })] }, idx))
1074
+ })]
1075
+ });
1076
+ };
1077
+ //#endregion
1078
+ //#region packages/ui/StatCard.tsx
1079
+ /**
1080
+ * General-purpose analytics / dashboard card.
1081
+ * Replaces the legacy `shared/ui/card-analytics/CardAnalytics` component.
1082
+ * Uses `ContextMenu` from `@nubitio/ui` for the overflow menu.
1083
+ */
1084
+ const StatCard = ({ title, headerExtra, menuVisible = true, menuItems = [{ label: "Configure" }, { label: "Remove" }], isLoading = false, className = "", children }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1085
+ className: `nb-stat-card${className ? ` ${className}` : ""}`,
1086
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1087
+ className: "nb-stat-card__header",
1088
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [title && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1089
+ className: "nb-stat-card__title",
1090
+ children: title
1091
+ }), headerExtra && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1092
+ className: "nb-stat-card__extra",
1093
+ children: headerExtra
1094
+ })] }), menuVisible && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1095
+ className: "nb-stat-card__menu",
1096
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ContextMenu, { items: menuItems })
1097
+ })]
1098
+ }), !isLoading && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1099
+ className: "nb-stat-card__body",
1100
+ children
1101
+ })]
1102
+ });
1103
+ //#endregion
1104
+ //#region packages/ui/CollapsibleSection.tsx
1105
+ /**
1106
+ * Accessible collapsible section with a labelled header trigger.
1107
+ *
1108
+ * - Uses `aria-expanded` and `aria-controls` for screen reader support.
1109
+ * - The body is conditionally rendered (not just hidden) to avoid mounting
1110
+ * heavy children before they are needed. Pass `keepMounted` if you need
1111
+ * the body always in the DOM.
1112
+ */
1113
+ const CollapsibleSection = ({ label, icon, open, onToggle, children, trailing, className, bodyClassName, "data-testid": testId }) => {
1114
+ const bodyId = (0, react.useId)();
1115
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1116
+ className: [
1117
+ "nb-collapsible",
1118
+ open && "nb-collapsible--open",
1119
+ className
1120
+ ].filter(Boolean).join(" "),
1121
+ "data-testid": testId,
1122
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1123
+ className: "nb-collapsible__header",
1124
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1125
+ type: "button",
1126
+ className: "nb-collapsible__trigger",
1127
+ "aria-expanded": open,
1128
+ "aria-controls": bodyId,
1129
+ onClick: onToggle,
1130
+ children: [
1131
+ icon && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
1132
+ className: icon,
1133
+ "aria-hidden": "true"
1134
+ }),
1135
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1136
+ className: "nb-collapsible__label",
1137
+ children: label
1138
+ }),
1139
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
1140
+ className: "ph ph-caret-down nb-collapsible__chevron",
1141
+ "aria-hidden": "true"
1142
+ })
1143
+ ]
1144
+ }), trailing && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1145
+ className: "nb-collapsible__trailing",
1146
+ children: trailing
1147
+ })]
1148
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1149
+ id: bodyId,
1150
+ className: ["nb-collapsible__body", bodyClassName].filter(Boolean).join(" "),
1151
+ role: "region",
1152
+ "aria-labelledby": void 0,
1153
+ hidden: !open,
1154
+ children
1155
+ })]
1156
+ });
1157
+ };
1158
+ //#endregion
1159
+ //#region packages/ui/DatePicker.tsx
1160
+ const cx$1 = (...values) => values.filter(Boolean).join(" ");
1161
+ const ISO_DATE_RE$1 = /^\d{4}-\d{2}-\d{2}$/;
1162
+ const getLocale$1 = (override) => override || document.documentElement.lang || navigator.language || "en-US";
1163
+ const pad$1 = (value) => String(value).padStart(2, "0");
1164
+ const toIsoDate$1 = (date) => `${date.getFullYear()}-${pad$1(date.getMonth() + 1)}-${pad$1(date.getDate())}`;
1165
+ const parseIsoDate$1 = (value) => {
1166
+ if (!value || !ISO_DATE_RE$1.test(value)) return null;
1167
+ const [year, month, day] = value.split("-").map(Number);
1168
+ const date = new Date(year, month - 1, day);
1169
+ if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) return null;
1170
+ return date;
1171
+ };
1172
+ const startOfMonth$1 = (date) => new Date(date.getFullYear(), date.getMonth(), 1);
1173
+ const addMonths$1 = (date, months) => new Date(date.getFullYear(), date.getMonth() + months, 1);
1174
+ const formatDisplayDate$1 = (value, locale) => {
1175
+ const date = parseIsoDate$1(value);
1176
+ if (!date) return "";
1177
+ return new Intl.DateTimeFormat(getLocale$1(locale), {
1178
+ day: "2-digit",
1179
+ month: "2-digit",
1180
+ year: "numeric"
1181
+ }).format(date);
1182
+ };
1183
+ const formatMonthLabel$1 = (date, locale) => new Intl.DateTimeFormat(getLocale$1(locale), {
1184
+ month: "long",
1185
+ year: "numeric"
1186
+ }).format(date);
1187
+ /**
1188
+ * Locale-aware first day of week.
1189
+ * Uses Intl.Locale.weekInfo (Chrome 99+, FF 126+, Safari 17+) with fallback heuristic.
1190
+ */
1191
+ const getFirstDayOfWeek = (locale) => {
1192
+ try {
1193
+ const info = new Intl.Locale(getLocale$1(locale)).weekInfo;
1194
+ if (info && typeof info.firstDay === "number") return info.firstDay === 7 ? 0 : info.firstDay;
1195
+ } catch {}
1196
+ const country = (getLocale$1(locale).split("-")[1] ?? "").toUpperCase();
1197
+ return [
1198
+ "US",
1199
+ "CA",
1200
+ "MX",
1201
+ "CN",
1202
+ "JP",
1203
+ "KR",
1204
+ "SA",
1205
+ "EG",
1206
+ "IL",
1207
+ "BR"
1208
+ ].includes(country) ? 0 : 1;
1209
+ };
1210
+ const buildCalendarDays$1 = (month, min, max, locale) => {
1211
+ const first = startOfMonth$1(month);
1212
+ const firstDayOfWeek = getFirstDayOfWeek(locale);
1213
+ const offset = (first.getDay() - firstDayOfWeek + 7) % 7;
1214
+ const minDate = parseIsoDate$1(min);
1215
+ const maxDate = parseIsoDate$1(max);
1216
+ const start = new Date(first);
1217
+ start.setDate(first.getDate() - offset);
1218
+ return Array.from({ length: 42 }, (_, i) => {
1219
+ const date = new Date(start);
1220
+ date.setDate(start.getDate() + i);
1221
+ return {
1222
+ date,
1223
+ iso: toIsoDate$1(date),
1224
+ inCurrentMonth: date.getMonth() === month.getMonth(),
1225
+ disabled: Boolean(minDate && date < minDate || maxDate && date > maxDate)
1226
+ };
1227
+ });
1228
+ };
1229
+ const isIsoDateDisabled = (iso, min, max) => {
1230
+ const date = parseIsoDate$1(iso);
1231
+ if (!date) return true;
1232
+ const minDate = parseIsoDate$1(min);
1233
+ const maxDate = parseIsoDate$1(max);
1234
+ return Boolean(minDate && date < minDate || maxDate && date > maxDate);
1235
+ };
1236
+ const weekDayLabels = (locale) => {
1237
+ const firstDayOfWeek = getFirstDayOfWeek(locale);
1238
+ const anchor = new Date(2026, 5, 7);
1239
+ const formatter = new Intl.DateTimeFormat(getLocale$1(locale), { weekday: "narrow" });
1240
+ return Array.from({ length: 7 }, (_, i) => {
1241
+ const date = new Date(anchor);
1242
+ date.setDate(anchor.getDate() + (firstDayOfWeek + i) % 7);
1243
+ return formatter.format(date);
1244
+ });
1245
+ };
1246
+ /** Short month labels (Jan, Feb, …) — capitalized for display. */
1247
+ const getMonthLabels = (locale) => {
1248
+ const formatter = new Intl.DateTimeFormat(getLocale$1(locale), { month: "short" });
1249
+ return Array.from({ length: 12 }, (_, i) => {
1250
+ const label = formatter.format(new Date(2024, i, 1));
1251
+ return label.charAt(0).toUpperCase() + label.slice(1).replace(".", "");
1252
+ });
1253
+ };
1254
+ /** Year range: 12-year block containing the given year. */
1255
+ const getYearRangeStart = (year) => Math.floor(year / 12) * 12;
1256
+ /**
1257
+ * Applies dd/mm/yyyy mask.
1258
+ * Auto-inserts trailing slash when adding the 2nd or 4th digit.
1259
+ * Does NOT add trailing slash when the user is deleting.
1260
+ */
1261
+ const applyDateMask = (rawValue, prevValue) => {
1262
+ const digits = rawValue.replace(/\D/g, "").slice(0, 8);
1263
+ const prevDigits = prevValue.replace(/\D/g, "");
1264
+ const isAdding = digits.length > prevDigits.length;
1265
+ if (digits.length === 0) return "";
1266
+ let result = digits.slice(0, 2);
1267
+ if (digits.length > 2) result += "/" + digits.slice(2, 4);
1268
+ if (digits.length > 4) result += "/" + digits.slice(4, 8);
1269
+ if (isAdding && (digits.length === 2 || digits.length === 4)) result += "/";
1270
+ return result;
1271
+ };
1272
+ /**
1273
+ * Parses dd/mm/yyyy or ISO yyyy-mm-dd to ISO string.
1274
+ * Returns null if not a valid date.
1275
+ */
1276
+ const parseDisplayDate = (text, locale) => {
1277
+ const match = text.match(/^(\d{1,2})[/-](\d{1,2})[/-](\d{4})$/);
1278
+ if (match) {
1279
+ const [, a, b, year] = match;
1280
+ const [dd, mm] = (getLocale$1(locale).split("-")[1] ?? "").toUpperCase() === "US" ? [b, a] : [a, b];
1281
+ const iso = `${year}-${mm.padStart(2, "0")}-${dd.padStart(2, "0")}`;
1282
+ return parseIsoDate$1(iso) ? iso : null;
1283
+ }
1284
+ if (ISO_DATE_RE$1.test(text)) return parseIsoDate$1(text) ? text : null;
1285
+ return null;
1286
+ };
1287
+ const DatePicker = (0, react.forwardRef)(function DatePicker({ id, name, value, placeholder = "dd/mm/yyyy", disabled = false, readOnly = false, required = false, invalid = false, clearable = true, locale, min, max, className, ariaLabel, onChange, onBlur }, ref) {
1288
+ const generatedId = (0, react.useId)();
1289
+ const inputId = id ?? generatedId;
1290
+ const strings = useUiStrings();
1291
+ const selectedDate = parseIsoDate$1(value);
1292
+ const resolvedLocale = getLocale$1(locale);
1293
+ const displayValue = formatDisplayDate$1(value, resolvedLocale);
1294
+ const [open, setOpen] = (0, react.useState)(false);
1295
+ const [viewMode, setViewMode] = (0, react.useState)("days");
1296
+ const [month, setMonth] = (0, react.useState)(() => startOfMonth$1(selectedDate ?? /* @__PURE__ */ new Date()));
1297
+ const [yearRangeStart, setYearRangeStart] = (0, react.useState)(() => getYearRangeStart((/* @__PURE__ */ new Date()).getFullYear()));
1298
+ const [panelStyle, setPanelStyle] = (0, react.useState)({});
1299
+ const [focusedIso, setFocusedIso] = (0, react.useState)(null);
1300
+ const [inputText, setInputText] = (0, react.useState)("");
1301
+ const [isEditing, setIsEditing] = (0, react.useState)(false);
1302
+ const rootRef = (0, react.useRef)(null);
1303
+ const inputRef = (0, react.useRef)(null);
1304
+ const triggerRef = (0, react.useRef)(null);
1305
+ const panelRef = (0, react.useRef)(null);
1306
+ const days = (0, react.useMemo)(() => buildCalendarDays$1(month, min, max, resolvedLocale), [
1307
+ max,
1308
+ min,
1309
+ month,
1310
+ resolvedLocale
1311
+ ]);
1312
+ const weekdayLabels = (0, react.useMemo)(() => weekDayLabels(resolvedLocale), [resolvedLocale]);
1313
+ const monthLabels = (0, react.useMemo)(() => getMonthLabels(resolvedLocale), [resolvedLocale]);
1314
+ const selectedIso = selectedDate ? toIsoDate$1(selectedDate) : "";
1315
+ (0, react.useEffect)(() => {
1316
+ if (selectedDate) setMonth(startOfMonth$1(selectedDate));
1317
+ }, [value]);
1318
+ (0, react.useEffect)(() => {
1319
+ if (!open) {
1320
+ setFocusedIso(null);
1321
+ setViewMode("days");
1322
+ return;
1323
+ }
1324
+ const updatePosition = () => {
1325
+ const rect = triggerRef.current?.getBoundingClientRect();
1326
+ if (!rect) return;
1327
+ const panelWidth = panelRef.current?.offsetWidth ?? 296;
1328
+ const left = Math.min(Math.max(8, rect.left), window.innerWidth - panelWidth - 8);
1329
+ const top = Math.min(rect.bottom + 6, window.innerHeight - 380);
1330
+ setPanelStyle({
1331
+ left,
1332
+ top: Math.max(8, top)
1333
+ });
1334
+ };
1335
+ updatePosition();
1336
+ window.addEventListener("resize", updatePosition);
1337
+ window.addEventListener("scroll", updatePosition, true);
1338
+ return () => {
1339
+ window.removeEventListener("resize", updatePosition);
1340
+ window.removeEventListener("scroll", updatePosition, true);
1341
+ };
1342
+ }, [open]);
1343
+ (0, react.useEffect)(() => {
1344
+ if (!open) return;
1345
+ const handlePointerDown = (e) => {
1346
+ const target = e.target;
1347
+ if (rootRef.current?.contains(target) || panelRef.current?.contains(target)) return;
1348
+ setOpen(false);
1349
+ };
1350
+ const handleKeyDown = (e) => {
1351
+ if (e.key === "Escape") {
1352
+ e.preventDefault();
1353
+ setOpen(false);
1354
+ inputRef.current?.focus();
1355
+ }
1356
+ };
1357
+ document.addEventListener("pointerdown", handlePointerDown);
1358
+ document.addEventListener("keydown", handleKeyDown);
1359
+ return () => {
1360
+ document.removeEventListener("pointerdown", handlePointerDown);
1361
+ document.removeEventListener("keydown", handleKeyDown);
1362
+ };
1363
+ }, [open]);
1364
+ (0, react.useEffect)(() => {
1365
+ if (!open) return;
1366
+ const initialIso = selectedDate ? toIsoDate$1(selectedDate) : toIsoDate$1(/* @__PURE__ */ new Date());
1367
+ setFocusedIso(initialIso);
1368
+ window.setTimeout(() => {
1369
+ panelRef.current?.querySelector(`[data-iso="${initialIso}"]`)?.focus();
1370
+ }, 0);
1371
+ }, [open]);
1372
+ const commitValue = (0, react.useCallback)((nextValue) => {
1373
+ onChange?.(nextValue);
1374
+ setOpen(false);
1375
+ setIsEditing(false);
1376
+ setInputText("");
1377
+ window.setTimeout(() => inputRef.current?.focus());
1378
+ }, [onChange]);
1379
+ const handleToday = (0, react.useCallback)(() => {
1380
+ const today = toIsoDate$1(/* @__PURE__ */ new Date());
1381
+ if (isIsoDateDisabled(today, min, max)) return;
1382
+ setMonth(startOfMonth$1(parseIsoDate$1(today)));
1383
+ commitValue(today);
1384
+ }, [
1385
+ min,
1386
+ max,
1387
+ commitValue
1388
+ ]);
1389
+ const handleClear = (0, react.useCallback)((e) => {
1390
+ e.preventDefault();
1391
+ commitValue("");
1392
+ }, [commitValue]);
1393
+ const handleInputFocus = () => {
1394
+ setIsEditing(true);
1395
+ setInputText(displayValue);
1396
+ window.setTimeout(() => inputRef.current?.select(), 0);
1397
+ };
1398
+ const handleInputChange = (e) => {
1399
+ if (readOnly || disabled) return;
1400
+ setInputText(applyDateMask(e.target.value, inputText));
1401
+ };
1402
+ const handleInputBlur = (e) => {
1403
+ const related = e.relatedTarget;
1404
+ if (rootRef.current?.contains(related) || panelRef.current?.contains(related)) return;
1405
+ setIsEditing(false);
1406
+ const parsed = parseDisplayDate(inputText, resolvedLocale);
1407
+ if (parsed !== null) {
1408
+ if (parsed !== value) onChange?.(parsed);
1409
+ } else if (inputText === "" && value) onChange?.("");
1410
+ setInputText("");
1411
+ onBlur?.(e);
1412
+ };
1413
+ const handleInputKeyDown = (e) => {
1414
+ if (e.key === "Enter") {
1415
+ e.preventDefault();
1416
+ const parsed = parseDisplayDate(inputText, resolvedLocale);
1417
+ if (parsed !== null) commitValue(parsed);
1418
+ else if (inputText === "") commitValue("");
1419
+ } else if (e.key === "ArrowDown" || e.key === "F4") {
1420
+ e.preventDefault();
1421
+ if (!readOnly && !disabled) setOpen(true);
1422
+ } else if (e.key === "Escape" && open) setOpen(false);
1423
+ };
1424
+ const handleCalendarPointerDown = (e) => {
1425
+ e.preventDefault();
1426
+ if (readOnly || disabled) return;
1427
+ setOpen((v) => !v);
1428
+ };
1429
+ const handleDayKeyDown = (0, react.useCallback)((e, iso) => {
1430
+ if (![
1431
+ "ArrowLeft",
1432
+ "ArrowRight",
1433
+ "ArrowUp",
1434
+ "ArrowDown",
1435
+ "Enter",
1436
+ " "
1437
+ ].includes(e.key)) return;
1438
+ e.preventDefault();
1439
+ if (e.key === "Enter" || e.key === " ") {
1440
+ const day = days.find((d) => d.iso === iso);
1441
+ if (day && !day.disabled) commitValue(iso);
1442
+ return;
1443
+ }
1444
+ const delta = {
1445
+ ArrowLeft: -1,
1446
+ ArrowRight: 1,
1447
+ ArrowUp: -7,
1448
+ ArrowDown: 7
1449
+ }[e.key] ?? 0;
1450
+ const current = parseIsoDate$1(iso);
1451
+ if (!current) return;
1452
+ const next = new Date(current);
1453
+ next.setDate(current.getDate() + delta);
1454
+ const nextIso = toIsoDate$1(next);
1455
+ if (next.getMonth() !== month.getMonth() || next.getFullYear() !== month.getFullYear()) setMonth(startOfMonth$1(next));
1456
+ setFocusedIso(nextIso);
1457
+ window.setTimeout(() => {
1458
+ panelRef.current?.querySelector(`[data-iso="${nextIso}"]`)?.focus();
1459
+ }, 0);
1460
+ }, [
1461
+ days,
1462
+ month,
1463
+ commitValue
1464
+ ]);
1465
+ const handleMonthSelect = (monthIndex) => {
1466
+ setMonth(new Date(month.getFullYear(), monthIndex, 1));
1467
+ setViewMode("days");
1468
+ };
1469
+ const handleYearSelect = (year) => {
1470
+ setMonth(new Date(year, month.getMonth(), 1));
1471
+ setViewMode("months");
1472
+ };
1473
+ const setInputRef = (node) => {
1474
+ inputRef.current = node;
1475
+ if (typeof ref === "function") ref(node);
1476
+ else if (ref) ref.current = node;
1477
+ };
1478
+ const todayIso = toIsoDate$1(/* @__PURE__ */ new Date());
1479
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1480
+ ref: rootRef,
1481
+ className: cx$1("nb-date-picker", className),
1482
+ children: [
1483
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1484
+ type: "hidden",
1485
+ name,
1486
+ value: value ?? "",
1487
+ required
1488
+ }),
1489
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1490
+ ref: triggerRef,
1491
+ className: cx$1("nb-date-picker__trigger", invalid && "nb-date-picker__trigger--invalid", readOnly && "nb-date-picker__trigger--readonly", open && "nb-date-picker__trigger--open", disabled && "nb-date-picker__trigger--disabled"),
1492
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1493
+ ref: setInputRef,
1494
+ id: inputId,
1495
+ type: "text",
1496
+ inputMode: "numeric",
1497
+ className: cx$1("nb-date-picker__input", !isEditing && !displayValue && "nb-date-picker__input--placeholder"),
1498
+ value: isEditing ? inputText : displayValue,
1499
+ placeholder,
1500
+ disabled,
1501
+ readOnly,
1502
+ "aria-label": ariaLabel,
1503
+ "aria-haspopup": "dialog",
1504
+ "aria-expanded": open,
1505
+ "aria-invalid": invalid || void 0,
1506
+ "aria-required": required || void 0,
1507
+ autoComplete: "off",
1508
+ onFocus: handleInputFocus,
1509
+ onBlur: handleInputBlur,
1510
+ onChange: handleInputChange,
1511
+ onKeyDown: handleInputKeyDown
1512
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1513
+ className: "nb-date-picker__icons",
1514
+ children: [clearable && displayValue && !disabled && !readOnly && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1515
+ role: "button",
1516
+ tabIndex: -1,
1517
+ className: "nb-date-picker__clear",
1518
+ "aria-label": strings.clearDate,
1519
+ onPointerDown: handleClear,
1520
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
1521
+ className: "ph ph-x",
1522
+ "aria-hidden": "true"
1523
+ })
1524
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1525
+ role: "button",
1526
+ tabIndex: -1,
1527
+ className: "nb-date-picker__calendar-btn",
1528
+ "aria-label": strings.openCalendar,
1529
+ onPointerDown: handleCalendarPointerDown,
1530
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
1531
+ className: "ph ph-calendar-blank",
1532
+ "aria-hidden": "true"
1533
+ })
1534
+ })]
1535
+ })]
1536
+ }),
1537
+ open && (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1538
+ ref: panelRef,
1539
+ className: "nb-date-picker__panel",
1540
+ role: "dialog",
1541
+ "aria-label": strings.selectDate,
1542
+ style: panelStyle,
1543
+ children: [
1544
+ viewMode === "days" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1545
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1546
+ className: "nb-date-picker__header",
1547
+ children: [
1548
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1549
+ type: "button",
1550
+ className: "nb-date-picker__nav",
1551
+ "aria-label": strings.previousMonth,
1552
+ onClick: () => setMonth((m) => addMonths$1(m, -1)),
1553
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
1554
+ className: "ph ph-caret-left",
1555
+ "aria-hidden": "true"
1556
+ })
1557
+ }),
1558
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1559
+ type: "button",
1560
+ className: "nb-date-picker__month nb-date-picker__month--clickable",
1561
+ "aria-label": strings.selectMonthAndYear,
1562
+ onClick: () => setViewMode("months"),
1563
+ children: [formatMonthLabel$1(month, resolvedLocale), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
1564
+ className: "ph ph-caret-down nb-date-picker__month-caret",
1565
+ "aria-hidden": "true"
1566
+ })]
1567
+ }),
1568
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1569
+ type: "button",
1570
+ className: "nb-date-picker__nav",
1571
+ "aria-label": strings.nextMonth,
1572
+ onClick: () => setMonth((m) => addMonths$1(m, 1)),
1573
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
1574
+ className: "ph ph-caret-right",
1575
+ "aria-hidden": "true"
1576
+ })
1577
+ })
1578
+ ]
1579
+ }),
1580
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1581
+ className: "nb-date-picker__weekdays",
1582
+ "aria-hidden": "true",
1583
+ children: weekdayLabels.map((label, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label }, `${label}-${i}`))
1584
+ }),
1585
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1586
+ className: "nb-date-picker__days",
1587
+ role: "grid",
1588
+ "aria-label": formatMonthLabel$1(month, resolvedLocale),
1589
+ children: days.map((day) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1590
+ "data-iso": day.iso,
1591
+ type: "button",
1592
+ className: cx$1("nb-date-picker__day", !day.inCurrentMonth && "nb-date-picker__day--muted", day.iso === selectedIso && "nb-date-picker__day--selected", day.iso === todayIso && "nb-date-picker__day--today"),
1593
+ disabled: day.disabled,
1594
+ role: "gridcell",
1595
+ "aria-selected": day.iso === selectedIso,
1596
+ tabIndex: day.iso === (focusedIso ?? selectedIso ?? todayIso) ? 0 : -1,
1597
+ onClick: () => commitValue(day.iso),
1598
+ onKeyDown: (e) => handleDayKeyDown(e, day.iso),
1599
+ children: day.date.getDate()
1600
+ }, day.iso))
1601
+ }),
1602
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1603
+ className: "nb-date-picker__footer",
1604
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1605
+ type: "button",
1606
+ className: "nb-date-picker__text-button",
1607
+ onClick: () => commitValue(""),
1608
+ children: strings.clear
1609
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1610
+ type: "button",
1611
+ className: "nb-date-picker__text-button",
1612
+ onClick: handleToday,
1613
+ children: strings.today
1614
+ })]
1615
+ })
1616
+ ] }),
1617
+ viewMode === "months" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1618
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1619
+ className: "nb-date-picker__header",
1620
+ children: [
1621
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1622
+ type: "button",
1623
+ className: "nb-date-picker__nav",
1624
+ "aria-label": strings.previousYear,
1625
+ onClick: () => setMonth((m) => new Date(m.getFullYear() - 1, m.getMonth(), 1)),
1626
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
1627
+ className: "ph ph-caret-left",
1628
+ "aria-hidden": "true"
1629
+ })
1630
+ }),
1631
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1632
+ type: "button",
1633
+ className: "nb-date-picker__month nb-date-picker__month--clickable",
1634
+ "aria-label": strings.selectYear,
1635
+ onClick: () => {
1636
+ setYearRangeStart(getYearRangeStart(month.getFullYear()));
1637
+ setViewMode("years");
1638
+ },
1639
+ children: [month.getFullYear(), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
1640
+ className: "ph ph-caret-down nb-date-picker__month-caret",
1641
+ "aria-hidden": "true"
1642
+ })]
1643
+ }),
1644
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1645
+ type: "button",
1646
+ className: "nb-date-picker__nav",
1647
+ "aria-label": strings.nextYear,
1648
+ onClick: () => setMonth((m) => new Date(m.getFullYear() + 1, m.getMonth(), 1)),
1649
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
1650
+ className: "ph ph-caret-right",
1651
+ "aria-hidden": "true"
1652
+ })
1653
+ })
1654
+ ]
1655
+ }),
1656
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1657
+ className: "nb-date-picker__months-grid",
1658
+ role: "grid",
1659
+ "aria-label": `${month.getFullYear()}`,
1660
+ children: monthLabels.map((label, i) => {
1661
+ const isSelected = selectedDate ? selectedDate.getMonth() === i && selectedDate.getFullYear() === month.getFullYear() : false;
1662
+ const isCurrent = (/* @__PURE__ */ new Date()).getMonth() === i && (/* @__PURE__ */ new Date()).getFullYear() === month.getFullYear();
1663
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1664
+ type: "button",
1665
+ className: cx$1("nb-date-picker__month-cell", isSelected && "nb-date-picker__month-cell--selected", isCurrent && "nb-date-picker__month-cell--current"),
1666
+ role: "gridcell",
1667
+ "aria-selected": isSelected,
1668
+ onClick: () => handleMonthSelect(i),
1669
+ children: label
1670
+ }, i);
1671
+ })
1672
+ }),
1673
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1674
+ className: "nb-date-picker__footer",
1675
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1676
+ type: "button",
1677
+ className: "nb-date-picker__text-button",
1678
+ onClick: () => setViewMode("days"),
1679
+ children: [
1680
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
1681
+ className: "ph ph-arrow-left",
1682
+ "aria-hidden": "true"
1683
+ }),
1684
+ " ",
1685
+ strings.back
1686
+ ]
1687
+ })
1688
+ })
1689
+ ] }),
1690
+ viewMode === "years" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1691
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1692
+ className: "nb-date-picker__header",
1693
+ children: [
1694
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1695
+ type: "button",
1696
+ className: "nb-date-picker__nav",
1697
+ "aria-label": strings.previousYears,
1698
+ onClick: () => setYearRangeStart((y) => y - 12),
1699
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
1700
+ className: "ph ph-caret-left",
1701
+ "aria-hidden": "true"
1702
+ })
1703
+ }),
1704
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1705
+ className: "nb-date-picker__month",
1706
+ children: [
1707
+ yearRangeStart,
1708
+ "–",
1709
+ yearRangeStart + 11
1710
+ ]
1711
+ }),
1712
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1713
+ type: "button",
1714
+ className: "nb-date-picker__nav",
1715
+ "aria-label": strings.nextYears,
1716
+ onClick: () => setYearRangeStart((y) => y + 12),
1717
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
1718
+ className: "ph ph-caret-right",
1719
+ "aria-hidden": "true"
1720
+ })
1721
+ })
1722
+ ]
1723
+ }),
1724
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1725
+ className: "nb-date-picker__years-grid",
1726
+ role: "grid",
1727
+ "aria-label": `${yearRangeStart}–${yearRangeStart + 11}`,
1728
+ children: Array.from({ length: 12 }, (_, i) => yearRangeStart + i).map((year) => {
1729
+ const isSelected = selectedDate ? selectedDate.getFullYear() === year : false;
1730
+ const isCurrent = (/* @__PURE__ */ new Date()).getFullYear() === year;
1731
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1732
+ type: "button",
1733
+ className: cx$1("nb-date-picker__year-cell", isSelected && "nb-date-picker__year-cell--selected", isCurrent && "nb-date-picker__year-cell--current"),
1734
+ role: "gridcell",
1735
+ "aria-selected": isSelected,
1736
+ onClick: () => handleYearSelect(year),
1737
+ children: year
1738
+ }, year);
1739
+ })
1740
+ }),
1741
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1742
+ className: "nb-date-picker__footer",
1743
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1744
+ type: "button",
1745
+ className: "nb-date-picker__text-button",
1746
+ onClick: () => setViewMode("months"),
1747
+ children: [
1748
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
1749
+ className: "ph ph-arrow-left",
1750
+ "aria-hidden": "true"
1751
+ }),
1752
+ " ",
1753
+ strings.back
1754
+ ]
1755
+ })
1756
+ })
1757
+ ] })
1758
+ ]
1759
+ }), document.body)
1760
+ ]
1761
+ });
1762
+ });
1763
+ //#endregion
1764
+ //#region packages/ui/DateRangePicker.tsx
1765
+ /**
1766
+ * DateRangePicker — enterprise date range selector.
1767
+ *
1768
+ * A single trigger with two text inputs (start + end) sharing one calendar
1769
+ * panel. Supports hover-preview, range highlighting, keyboard entry, min/max
1770
+ * constraints, and full density/theme token compliance.
1771
+ *
1772
+ * Usage:
1773
+ * <DateRangePicker
1774
+ * startValue="2026-06-01"
1775
+ * endValue="2026-06-30"
1776
+ * onChange={(start, end) => ...}
1777
+ * />
1778
+ */
1779
+ const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
1780
+ const pad = (v) => String(v).padStart(2, "0");
1781
+ const toIsoDate = (d) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
1782
+ const parseIsoDate = (value) => {
1783
+ if (!value || !ISO_DATE_RE.test(value)) return null;
1784
+ const [year, month, day] = value.split("-").map(Number);
1785
+ const d = new Date(year, month - 1, day);
1786
+ if (d.getFullYear() !== year || d.getMonth() !== month - 1 || d.getDate() !== day) return null;
1787
+ return d;
1788
+ };
1789
+ const cx = (...vals) => vals.filter(Boolean).join(" ");
1790
+ const getLocale = (override) => override || document.documentElement.lang || navigator.language || "en-US";
1791
+ const startOfMonth = (date) => new Date(date.getFullYear(), date.getMonth(), 1);
1792
+ const addMonths = (date, months) => new Date(date.getFullYear(), date.getMonth() + months, 1);
1793
+ const formatDisplayDate = (value, locale) => {
1794
+ const date = parseIsoDate(value);
1795
+ if (!date) return "";
1796
+ return new Intl.DateTimeFormat(getLocale(locale), {
1797
+ day: "2-digit",
1798
+ month: "2-digit",
1799
+ year: "numeric"
1800
+ }).format(date);
1801
+ };
1802
+ const formatMonthLabel = (date, locale) => new Intl.DateTimeFormat(getLocale(locale), {
1803
+ month: "long",
1804
+ year: "numeric"
1805
+ }).format(date);
1806
+ const buildCalendarDays = (month, min, max, locale) => {
1807
+ const first = startOfMonth(month);
1808
+ const firstDayOfWeek = getFirstDayOfWeek(locale);
1809
+ const offset = (first.getDay() - firstDayOfWeek + 7) % 7;
1810
+ const minDate = parseIsoDate(min);
1811
+ const maxDate = parseIsoDate(max);
1812
+ const start = new Date(first);
1813
+ start.setDate(first.getDate() - offset);
1814
+ return Array.from({ length: 42 }, (_, i) => {
1815
+ const date = new Date(start);
1816
+ date.setDate(start.getDate() + i);
1817
+ return {
1818
+ date,
1819
+ iso: toIsoDate(date),
1820
+ inCurrentMonth: date.getMonth() === month.getMonth(),
1821
+ disabled: Boolean(minDate && date < minDate || maxDate && date > maxDate)
1822
+ };
1823
+ });
1824
+ };
1825
+ const getDayRoleInRange = (iso, startIso, endIso) => {
1826
+ if (!startIso) return "none";
1827
+ if (!endIso || startIso === endIso) return iso === startIso ? "single" : "none";
1828
+ if (iso === startIso) return "start";
1829
+ if (iso === endIso) return "end";
1830
+ if (iso > startIso && iso < endIso) return "in-range";
1831
+ return "none";
1832
+ };
1833
+ function DateRangePicker({ startValue, endValue, placeholder = "dd/mm/yyyy", disabled = false, readOnly = false, required = false, invalid = false, locale, min, max, className, ariaLabel, onChange, onBlur }) {
1834
+ const startId = (0, react.useId)();
1835
+ const endId = (0, react.useId)();
1836
+ const strings = useUiStrings();
1837
+ const resolvedLocale = getLocale(locale);
1838
+ const startDisplay = formatDisplayDate(startValue, resolvedLocale);
1839
+ const endDisplay = formatDisplayDate(endValue, resolvedLocale);
1840
+ const startIso = parseIsoDate(startValue) ? startValue : null;
1841
+ const endIso = parseIsoDate(endValue) ? endValue : null;
1842
+ const [open, setOpen] = (0, react.useState)(false);
1843
+ const [phase, setPhase] = (0, react.useState)("idle");
1844
+ const [pendingStart, setPendingStart] = (0, react.useState)(null);
1845
+ const [hoverIso, setHoverIso] = (0, react.useState)(null);
1846
+ const [month, setMonth] = (0, react.useState)(() => startOfMonth(parseIsoDate(startValue) ?? /* @__PURE__ */ new Date()));
1847
+ const [panelStyle, setPanelStyle] = (0, react.useState)({});
1848
+ const [startText, setStartText] = (0, react.useState)("");
1849
+ const [endText, setEndText] = (0, react.useState)("");
1850
+ const [editingField, setEditingField] = (0, react.useState)(null);
1851
+ const rootRef = (0, react.useRef)(null);
1852
+ const triggerRef = (0, react.useRef)(null);
1853
+ const panelRef = (0, react.useRef)(null);
1854
+ const startInputRef = (0, react.useRef)(null);
1855
+ const endInputRef = (0, react.useRef)(null);
1856
+ const days = (0, react.useMemo)(() => buildCalendarDays(month, min, max, resolvedLocale), [
1857
+ month,
1858
+ min,
1859
+ max,
1860
+ resolvedLocale
1861
+ ]);
1862
+ const weekdayLbls = (0, react.useMemo)(() => weekDayLabels(resolvedLocale), [resolvedLocale]);
1863
+ const todayIso = toIsoDate(/* @__PURE__ */ new Date());
1864
+ (0, react.useEffect)(() => {
1865
+ const anchor = parseIsoDate(startValue) ?? parseIsoDate(endValue);
1866
+ if (anchor) setMonth(startOfMonth(anchor));
1867
+ }, [startValue]);
1868
+ (0, react.useEffect)(() => {
1869
+ if (!open) return;
1870
+ const update = () => {
1871
+ const rect = triggerRef.current?.getBoundingClientRect();
1872
+ if (!rect) return;
1873
+ const panelWidth = panelRef.current?.offsetWidth ?? 296;
1874
+ const left = Math.min(Math.max(8, rect.left), window.innerWidth - panelWidth - 8);
1875
+ const top = Math.min(rect.bottom + 6, window.innerHeight - 380);
1876
+ setPanelStyle({
1877
+ left,
1878
+ top: Math.max(8, top)
1879
+ });
1880
+ };
1881
+ update();
1882
+ window.addEventListener("resize", update);
1883
+ window.addEventListener("scroll", update, true);
1884
+ return () => {
1885
+ window.removeEventListener("resize", update);
1886
+ window.removeEventListener("scroll", update, true);
1887
+ };
1888
+ }, [open]);
1889
+ const closePanel = (0, react.useCallback)(() => {
1890
+ setOpen(false);
1891
+ setPhase("idle");
1892
+ setPendingStart(null);
1893
+ setHoverIso(null);
1894
+ }, []);
1895
+ (0, react.useEffect)(() => {
1896
+ if (!open) return;
1897
+ const handlePointerDown = (e) => {
1898
+ const target = e.target;
1899
+ if (rootRef.current?.contains(target) || panelRef.current?.contains(target)) return;
1900
+ closePanel();
1901
+ };
1902
+ const handleKeyDown = (e) => {
1903
+ if (e.key === "Escape") {
1904
+ e.preventDefault();
1905
+ closePanel();
1906
+ startInputRef.current?.focus();
1907
+ }
1908
+ };
1909
+ document.addEventListener("pointerdown", handlePointerDown);
1910
+ document.addEventListener("keydown", handleKeyDown);
1911
+ return () => {
1912
+ document.removeEventListener("pointerdown", handlePointerDown);
1913
+ document.removeEventListener("keydown", handleKeyDown);
1914
+ };
1915
+ }, [open, closePanel]);
1916
+ const commitRange = (0, react.useCallback)((start, end) => {
1917
+ const [a, b] = start <= end ? [start, end] : [end, start];
1918
+ onChange?.(a, b);
1919
+ closePanel();
1920
+ window.setTimeout(() => startInputRef.current?.focus());
1921
+ }, [onChange, closePanel]);
1922
+ const handleDayClick = (0, react.useCallback)((iso) => {
1923
+ if (phase === "idle") {
1924
+ setPendingStart(iso);
1925
+ setPhase("selecting-end");
1926
+ } else commitRange(pendingStart ?? iso, iso);
1927
+ }, [
1928
+ phase,
1929
+ pendingStart,
1930
+ commitRange
1931
+ ]);
1932
+ const getEffectiveRange = () => {
1933
+ if (phase === "selecting-end" && pendingStart) {
1934
+ const effectiveEnd = hoverIso ?? null;
1935
+ return {
1936
+ effectiveStart: pendingStart <= (effectiveEnd ?? pendingStart) ? pendingStart : effectiveEnd,
1937
+ effectiveEnd: pendingStart <= (effectiveEnd ?? pendingStart) ? effectiveEnd : pendingStart
1938
+ };
1939
+ }
1940
+ return {
1941
+ effectiveStart: startIso,
1942
+ effectiveEnd: endIso
1943
+ };
1944
+ };
1945
+ const { effectiveStart, effectiveEnd } = getEffectiveRange();
1946
+ const handleStartFocus = () => {
1947
+ setEditingField("start");
1948
+ setStartText(startDisplay);
1949
+ window.setTimeout(() => startInputRef.current?.select(), 0);
1950
+ };
1951
+ const handleEndFocus = () => {
1952
+ setEditingField("end");
1953
+ setEndText(endDisplay);
1954
+ window.setTimeout(() => endInputRef.current?.select(), 0);
1955
+ };
1956
+ const handleStartChange = (e) => {
1957
+ if (readOnly || disabled) return;
1958
+ setStartText(applyDateMask(e.target.value, startText));
1959
+ };
1960
+ const handleEndChange = (e) => {
1961
+ if (readOnly || disabled) return;
1962
+ setEndText(applyDateMask(e.target.value, endText));
1963
+ };
1964
+ const commitTextInput = (field, text) => {
1965
+ const parsed = parseDisplayDate(text, resolvedLocale);
1966
+ if (parsed !== null) {
1967
+ const newStart = field === "start" ? parsed : startIso ?? "";
1968
+ const newEnd = field === "end" ? parsed : endIso ?? "";
1969
+ if (newStart && newEnd) {
1970
+ const [a, b] = newStart <= newEnd ? [newStart, newEnd] : [newEnd, newStart];
1971
+ onChange?.(a, b);
1972
+ } else if (field === "start") onChange?.(parsed, endIso ?? "");
1973
+ else onChange?.(startIso ?? "", parsed);
1974
+ } else if (text === "") if (field === "start") onChange?.("", endIso ?? "");
1975
+ else onChange?.(startIso ?? "", "");
1976
+ };
1977
+ const handleStartBlur = (e) => {
1978
+ const related = e.relatedTarget;
1979
+ if (rootRef.current?.contains(related) || panelRef.current?.contains(related)) return;
1980
+ commitTextInput("start", startText);
1981
+ setEditingField(null);
1982
+ setStartText("");
1983
+ onBlur?.(e);
1984
+ };
1985
+ const handleEndBlur = (e) => {
1986
+ const related = e.relatedTarget;
1987
+ if (rootRef.current?.contains(related) || panelRef.current?.contains(related)) return;
1988
+ commitTextInput("end", endText);
1989
+ setEditingField(null);
1990
+ setEndText("");
1991
+ onBlur?.(e);
1992
+ };
1993
+ const handleStartKeyDown = (e) => {
1994
+ if (e.key === "Enter") {
1995
+ e.preventDefault();
1996
+ const parsed = parseDisplayDate(startText);
1997
+ if (parsed) onChange?.(parsed, endIso ?? "");
1998
+ endInputRef.current?.focus();
1999
+ } else if (e.key === "ArrowDown" || e.key === "F4") {
2000
+ e.preventDefault();
2001
+ if (!readOnly && !disabled) setOpen(true);
2002
+ }
2003
+ };
2004
+ const handleEndKeyDown = (e) => {
2005
+ if (e.key === "Enter") {
2006
+ e.preventDefault();
2007
+ const parsed = parseDisplayDate(endText);
2008
+ if (parsed) onChange?.(startIso ?? "", parsed);
2009
+ } else if (e.key === "ArrowDown" || e.key === "F4") {
2010
+ e.preventDefault();
2011
+ if (!readOnly && !disabled) setOpen(true);
2012
+ }
2013
+ };
2014
+ const handleCalendarPointerDown = (e) => {
2015
+ e.preventDefault();
2016
+ if (readOnly || disabled) return;
2017
+ setOpen((v) => !v);
2018
+ };
2019
+ const handleClear = (e) => {
2020
+ e.preventDefault();
2021
+ onChange?.("", "");
2022
+ closePanel();
2023
+ };
2024
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2025
+ ref: rootRef,
2026
+ className: cx("nb-date-range-picker", className),
2027
+ children: [
2028
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2029
+ ref: triggerRef,
2030
+ className: cx("nb-date-range-picker__trigger", invalid && "nb-date-range-picker__trigger--invalid", readOnly && "nb-date-range-picker__trigger--readonly", open && "nb-date-range-picker__trigger--open", disabled && "nb-date-range-picker__trigger--disabled"),
2031
+ "aria-label": ariaLabel,
2032
+ children: [
2033
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2034
+ ref: startInputRef,
2035
+ id: startId,
2036
+ type: "text",
2037
+ inputMode: "numeric",
2038
+ className: cx("nb-date-range-picker__input", !editingField && !startDisplay && "nb-date-range-picker__input--placeholder"),
2039
+ value: editingField === "start" ? startText : startDisplay,
2040
+ placeholder,
2041
+ disabled,
2042
+ readOnly,
2043
+ autoComplete: "off",
2044
+ "aria-label": `Fecha inicio${ariaLabel ? ` ${ariaLabel}` : ""}`,
2045
+ "aria-required": required || void 0,
2046
+ onFocus: handleStartFocus,
2047
+ onBlur: handleStartBlur,
2048
+ onChange: handleStartChange,
2049
+ onKeyDown: handleStartKeyDown
2050
+ }),
2051
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2052
+ className: "nb-date-range-picker__sep",
2053
+ "aria-hidden": "true",
2054
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", { className: "ph ph-arrow-right" })
2055
+ }),
2056
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2057
+ ref: endInputRef,
2058
+ id: endId,
2059
+ type: "text",
2060
+ inputMode: "numeric",
2061
+ className: cx("nb-date-range-picker__input", !editingField && !endDisplay && "nb-date-range-picker__input--placeholder"),
2062
+ value: editingField === "end" ? endText : endDisplay,
2063
+ placeholder,
2064
+ disabled,
2065
+ readOnly,
2066
+ autoComplete: "off",
2067
+ "aria-label": `Fecha fin${ariaLabel ? ` ${ariaLabel}` : ""}`,
2068
+ onFocus: handleEndFocus,
2069
+ onBlur: handleEndBlur,
2070
+ onChange: handleEndChange,
2071
+ onKeyDown: handleEndKeyDown
2072
+ }),
2073
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2074
+ className: "nb-date-range-picker__icons",
2075
+ children: [(startDisplay || endDisplay) && !disabled && !readOnly && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2076
+ role: "button",
2077
+ tabIndex: -1,
2078
+ className: "nb-date-range-picker__clear",
2079
+ "aria-label": strings.clearDateRange,
2080
+ onPointerDown: handleClear,
2081
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
2082
+ className: "ph ph-x",
2083
+ "aria-hidden": "true"
2084
+ })
2085
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2086
+ role: "button",
2087
+ tabIndex: -1,
2088
+ className: "nb-date-range-picker__calendar-btn",
2089
+ "aria-label": strings.openCalendar,
2090
+ onPointerDown: handleCalendarPointerDown,
2091
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
2092
+ className: "ph ph-calendar-blank",
2093
+ "aria-hidden": "true"
2094
+ })
2095
+ })]
2096
+ })
2097
+ ]
2098
+ }),
2099
+ phase === "selecting-end" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2100
+ className: "nb-date-range-picker__hint",
2101
+ role: "status",
2102
+ "aria-live": "polite",
2103
+ children: "Selecciona la fecha de fin"
2104
+ }),
2105
+ open && (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2106
+ ref: panelRef,
2107
+ className: "nb-date-range-picker__panel",
2108
+ role: "dialog",
2109
+ "aria-label": strings.selectDateRange,
2110
+ style: panelStyle,
2111
+ children: [
2112
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2113
+ className: "nb-date-range-picker__header",
2114
+ children: [
2115
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2116
+ type: "button",
2117
+ className: "nb-date-range-picker__nav",
2118
+ "aria-label": strings.previousMonth,
2119
+ onClick: () => setMonth((m) => addMonths(m, -1)),
2120
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
2121
+ className: "ph ph-caret-left",
2122
+ "aria-hidden": "true"
2123
+ })
2124
+ }),
2125
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2126
+ className: "nb-date-range-picker__month",
2127
+ "aria-live": "polite",
2128
+ children: formatMonthLabel(month, resolvedLocale)
2129
+ }),
2130
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2131
+ type: "button",
2132
+ className: "nb-date-range-picker__nav",
2133
+ "aria-label": strings.nextMonth,
2134
+ onClick: () => setMonth((m) => addMonths(m, 1)),
2135
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
2136
+ className: "ph ph-caret-right",
2137
+ "aria-hidden": "true"
2138
+ })
2139
+ })
2140
+ ]
2141
+ }),
2142
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2143
+ className: "nb-date-range-picker__weekdays",
2144
+ "aria-hidden": "true",
2145
+ children: weekdayLbls.map((label, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label }, `${label}-${i}`))
2146
+ }),
2147
+ phase === "selecting-end" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2148
+ className: "nb-date-range-picker__phase-banner",
2149
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
2150
+ className: "ph ph-info",
2151
+ "aria-hidden": "true"
2152
+ }), "Ahora selecciona la fecha de fin"]
2153
+ }),
2154
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2155
+ className: "nb-date-range-picker__days",
2156
+ role: "grid",
2157
+ "aria-label": formatMonthLabel(month, resolvedLocale),
2158
+ children: days.map((day) => {
2159
+ const role = getDayRoleInRange(day.iso, effectiveStart, effectiveEnd);
2160
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2161
+ type: "button",
2162
+ className: cx("nb-date-range-picker__day", !day.inCurrentMonth && "nb-date-range-picker__day--muted", day.iso === todayIso && "nb-date-range-picker__day--today", role === "start" && "nb-date-range-picker__day--range-start", role === "end" && "nb-date-range-picker__day--range-end", role === "in-range" && "nb-date-range-picker__day--in-range", role === "single" && "nb-date-range-picker__day--range-single", phase === "selecting-end" && "nb-date-range-picker__day--selecting"),
2163
+ disabled: day.disabled,
2164
+ role: "gridcell",
2165
+ "aria-selected": role !== "none",
2166
+ onMouseEnter: () => phase === "selecting-end" && setHoverIso(day.iso),
2167
+ onMouseLeave: () => phase === "selecting-end" && setHoverIso(null),
2168
+ onClick: () => !day.disabled && handleDayClick(day.iso),
2169
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2170
+ className: "nb-date-range-picker__day-inner",
2171
+ children: day.date.getDate()
2172
+ })
2173
+ }, day.iso);
2174
+ })
2175
+ }),
2176
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2177
+ className: "nb-date-range-picker__footer",
2178
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2179
+ type: "button",
2180
+ className: "nb-date-range-picker__text-button",
2181
+ onClick: () => {
2182
+ onChange?.("", "");
2183
+ closePanel();
2184
+ },
2185
+ children: strings.clear
2186
+ }), phase === "selecting-end" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2187
+ type: "button",
2188
+ className: "nb-date-range-picker__text-button",
2189
+ onClick: () => {
2190
+ setPendingStart(null);
2191
+ setPhase("idle");
2192
+ setHoverIso(null);
2193
+ },
2194
+ children: strings.cancel
2195
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2196
+ type: "button",
2197
+ className: "nb-date-range-picker__text-button",
2198
+ onClick: () => {
2199
+ const today = toIsoDate(/* @__PURE__ */ new Date());
2200
+ setMonth(startOfMonth(/* @__PURE__ */ new Date()));
2201
+ if (!startIso) onChange?.(today, endIso ?? "");
2202
+ else if (!endIso) onChange?.(startIso, today);
2203
+ else onChange?.(today, today);
2204
+ closePanel();
2205
+ },
2206
+ children: strings.today
2207
+ })]
2208
+ })
2209
+ ]
2210
+ }), document.body)
2211
+ ]
2212
+ });
2213
+ }
2214
+ //#endregion
2215
+ //#region packages/ui/useFloatingPanel.ts
2216
+ function useFloatingPanel(options = {}) {
2217
+ const [open, setOpenState] = (0, react.useState)(false);
2218
+ const containerRef = (0, react.useRef)(null);
2219
+ const onCloseRef = (0, react.useRef)(options.onClose);
2220
+ onCloseRef.current = options.onClose;
2221
+ const setOpen = (0, react.useCallback)((next) => {
2222
+ setOpenState(next);
2223
+ if (!next) onCloseRef.current?.();
2224
+ }, []);
2225
+ const toggle = (0, react.useCallback)(() => setOpen(!open), [open, setOpen]);
2226
+ (0, react.useEffect)(() => {
2227
+ if (!open) return;
2228
+ const handlePointerDown = (e) => {
2229
+ const container = containerRef.current;
2230
+ if (container && !container.contains(e.target)) setOpen(false);
2231
+ };
2232
+ const handleKeyDown = (e) => {
2233
+ if (e.key === "Escape") setOpen(false);
2234
+ };
2235
+ document.addEventListener("pointerdown", handlePointerDown);
2236
+ document.addEventListener("keydown", handleKeyDown);
2237
+ return () => {
2238
+ document.removeEventListener("pointerdown", handlePointerDown);
2239
+ document.removeEventListener("keydown", handleKeyDown);
2240
+ };
2241
+ }, [open, setOpen]);
2242
+ return {
2243
+ open,
2244
+ setOpen,
2245
+ toggle,
2246
+ containerRef
2247
+ };
2248
+ }
2249
+ //#endregion
2250
+ //#region packages/ui/Popover.tsx
2251
+ const Popover = ({ trigger, panel, align = "end", ariaLabel, className }) => {
2252
+ const { open, setOpen, toggle, containerRef } = useFloatingPanel();
2253
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2254
+ ref: containerRef,
2255
+ className: [
2256
+ "nb-popover",
2257
+ `nb-popover--${align}`,
2258
+ open && "nb-popover--open",
2259
+ className
2260
+ ].filter(Boolean).join(" "),
2261
+ children: [trigger({
2262
+ open,
2263
+ toggle
2264
+ }), open && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2265
+ className: "nb-popover__panel",
2266
+ role: "dialog",
2267
+ "aria-label": ariaLabel,
2268
+ children: panel({ close: () => setOpen(false) })
2269
+ })]
2270
+ });
2271
+ };
2272
+ //#endregion
2273
+ //#region packages/ui/Drawer.tsx
2274
+ const formatWidth = (value) => {
2275
+ if (value === void 0) return void 0;
2276
+ return typeof value === "number" ? `${value}px` : value;
2277
+ };
2278
+ const Drawer = ({ isOpen, onClose, title, width, side = "right", scrim = "subtle", children, footer, closeLabel, scrimLabel, className, "aria-label": ariaLabel }) => {
2279
+ const strings = useUiStrings();
2280
+ const resolvedCloseLabel = closeLabel ?? strings.close;
2281
+ const resolvedScrimLabel = scrimLabel ?? strings.closePanel;
2282
+ const titleId = (0, react.useId)();
2283
+ const panelRef = (0, react.useRef)(null);
2284
+ const lastFocusRef = (0, react.useRef)(null);
2285
+ (0, react.useEffect)(() => {
2286
+ if (!isOpen) return;
2287
+ lastFocusRef.current = document.activeElement;
2288
+ const timer = window.setTimeout(() => {
2289
+ (panelRef.current?.querySelector("button:not(:disabled), [href], input:not(:disabled), select:not(:disabled), textarea:not(:disabled), [tabindex]:not([tabindex=\"-1\"])"))?.focus();
2290
+ });
2291
+ return () => {
2292
+ clearTimeout(timer);
2293
+ lastFocusRef.current?.focus?.();
2294
+ };
2295
+ }, [isOpen]);
2296
+ (0, react.useEffect)(() => {
2297
+ if (!isOpen) return;
2298
+ const handler = (e) => {
2299
+ if (e.key === "Escape") {
2300
+ e.preventDefault();
2301
+ onClose();
2302
+ }
2303
+ };
2304
+ document.addEventListener("keydown", handler);
2305
+ return () => document.removeEventListener("keydown", handler);
2306
+ }, [isOpen, onClose]);
2307
+ return (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2308
+ className: [
2309
+ "nb-drawer-root",
2310
+ isOpen && "nb-drawer-root--open",
2311
+ `nb-drawer-root--${side}`
2312
+ ].filter(Boolean).join(" "),
2313
+ "aria-hidden": !isOpen,
2314
+ style: { pointerEvents: isOpen ? void 0 : "none" },
2315
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2316
+ className: `nb-drawer__scrim nb-drawer__scrim--${scrim}`,
2317
+ type: "button",
2318
+ "aria-label": resolvedScrimLabel,
2319
+ tabIndex: -1,
2320
+ onClick: onClose
2321
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("aside", {
2322
+ ref: panelRef,
2323
+ className: ["nb-drawer", className].filter(Boolean).join(" "),
2324
+ role: "dialog",
2325
+ "aria-modal": "false",
2326
+ "aria-labelledby": title ? titleId : void 0,
2327
+ "aria-label": !title ? ariaLabel : void 0,
2328
+ style: { width: formatWidth(width) },
2329
+ children: [
2330
+ title !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
2331
+ className: "nb-drawer__header",
2332
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
2333
+ className: "nb-drawer__title",
2334
+ id: titleId,
2335
+ children: title
2336
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconButton, {
2337
+ className: "nb-drawer__close",
2338
+ icon: "ph ph-x",
2339
+ label: resolvedCloseLabel,
2340
+ onClick: onClose
2341
+ })]
2342
+ }),
2343
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2344
+ className: "nb-drawer__body",
2345
+ children
2346
+ }),
2347
+ footer && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("footer", {
2348
+ className: "nb-drawer__footer",
2349
+ children: footer
2350
+ })
2351
+ ]
2352
+ })]
2353
+ }), document.body);
2354
+ };
2355
+ //#endregion
2356
+ exports.ACCENT_PRESETS = ACCENT_PRESETS;
2357
+ exports.AppDialog = AppDialog;
2358
+ exports.AppDropdown = AppDropdown;
2359
+ exports.AppToolbar = AppToolbar;
2360
+ exports.Avatar = Avatar;
2361
+ exports.Badge = Badge;
2362
+ exports.Button = Button;
2363
+ exports.Card = Card;
2364
+ exports.Chip = Chip;
2365
+ exports.CollapsibleSection = CollapsibleSection;
2366
+ exports.ConfirmDialog = ConfirmDialog;
2367
+ exports.ContextMenu = ContextMenu;
2368
+ exports.DatePicker = DatePicker;
2369
+ exports.DateRangePicker = DateRangePicker;
2370
+ exports.DensityContext = DensityContext;
2371
+ exports.DensityProvider = DensityProvider;
2372
+ exports.Drawer = Drawer;
2373
+ exports.EN_UI_STRINGS = EN_UI_STRINGS;
2374
+ exports.ES_UI_STRINGS = ES_UI_STRINGS;
2375
+ exports.EmptyState = EmptyState;
2376
+ exports.FormField = FormField;
2377
+ exports.IconButton = IconButton;
2378
+ exports.Popover = Popover;
2379
+ exports.SelectField = SelectField;
2380
+ exports.SettingsPanel = SettingsPanel;
2381
+ exports.Skeleton = Skeleton;
2382
+ exports.StatCard = StatCard;
2383
+ exports.TextAreaField = TextAreaField;
2384
+ exports.TextField = TextField;
2385
+ exports.ThemeContext = ThemeContext;
2386
+ exports.ThemeProvider = ThemeProvider;
2387
+ exports.ThemeSwitcher = ThemeSwitcher;
2388
+ exports.Toggle = Toggle;
2389
+ exports.UiStringsProvider = UiStringsProvider;
2390
+ exports.getAvatarHue = getAvatarHue;
2391
+ exports.getAvatarInitials = getAvatarInitials;
2392
+ exports.useAccentColor = useAccentColor;
2393
+ exports.useDensity = useDensity;
2394
+ exports.useFloatingPanel = useFloatingPanel;
2395
+ exports.useTheme = useTheme;
2396
+ exports.useUiStrings = useUiStrings;