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