danielgm-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.
Files changed (109) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +372 -0
  3. package/TABLER-LICENSE.txt +21 -0
  4. package/dist/accordion.d.ts +49 -0
  5. package/dist/accordion.js +14 -0
  6. package/dist/aspect-ratio.d.ts +8 -0
  7. package/dist/aspect-ratio.js +8 -0
  8. package/dist/avatar.d.ts +27 -0
  9. package/dist/avatar.js +15 -0
  10. package/dist/breadcrumbs.d.ts +20 -0
  11. package/dist/breadcrumbs.js +5 -0
  12. package/dist/button.d.ts +44 -0
  13. package/dist/button.js +25 -0
  14. package/dist/carousel.d.ts +19 -0
  15. package/dist/carousel.js +133 -0
  16. package/dist/choice-group.d.ts +36 -0
  17. package/dist/choice-group.js +27 -0
  18. package/dist/combobox.d.ts +37 -0
  19. package/dist/combobox.js +57 -0
  20. package/dist/command-palette.d.ts +28 -0
  21. package/dist/command-palette.js +90 -0
  22. package/dist/container.d.ts +21 -0
  23. package/dist/container.js +10 -0
  24. package/dist/context-menu.d.ts +21 -0
  25. package/dist/context-menu.js +50 -0
  26. package/dist/custom-select.d.ts +45 -0
  27. package/dist/custom-select.js +26 -0
  28. package/dist/data-table.d.ts +53 -0
  29. package/dist/data-table.js +104 -0
  30. package/dist/date-picker.d.ts +43 -0
  31. package/dist/date-picker.js +142 -0
  32. package/dist/display.d.ts +63 -0
  33. package/dist/display.js +43 -0
  34. package/dist/drawer.d.ts +12 -0
  35. package/dist/drawer.js +29 -0
  36. package/dist/dropdown-menu.d.ts +47 -0
  37. package/dist/dropdown-menu.js +24 -0
  38. package/dist/empty-state.d.ts +13 -0
  39. package/dist/empty-state.js +8 -0
  40. package/dist/fields.d.ts +62 -0
  41. package/dist/fields.js +70 -0
  42. package/dist/file-upload.d.ts +33 -0
  43. package/dist/file-upload.js +111 -0
  44. package/dist/index.d.ts +48 -0
  45. package/dist/index.js +48 -0
  46. package/dist/internal/field-open.d.ts +2 -0
  47. package/dist/internal/field-open.js +15 -0
  48. package/dist/internal/focus.d.ts +4 -0
  49. package/dist/internal/focus.js +27 -0
  50. package/dist/internal/icons.d.ts +29 -0
  51. package/dist/internal/icons.js +32 -0
  52. package/dist/internal/navigation-origin.d.ts +2 -0
  53. package/dist/internal/navigation-origin.js +23 -0
  54. package/dist/internal/overlay-layer.d.ts +12 -0
  55. package/dist/internal/overlay-layer.js +14 -0
  56. package/dist/internal/pagination.d.ts +6 -0
  57. package/dist/internal/pagination.js +36 -0
  58. package/dist/internal/scroll-hints.d.ts +8 -0
  59. package/dist/internal/scroll-hints.js +108 -0
  60. package/dist/internal/state.d.ts +2 -0
  61. package/dist/internal/state.js +13 -0
  62. package/dist/internal/trigger-label.d.ts +5 -0
  63. package/dist/internal/trigger-label.js +26 -0
  64. package/dist/loader.d.ts +27 -0
  65. package/dist/loader.js +13 -0
  66. package/dist/modal.d.ts +23 -0
  67. package/dist/modal.js +30 -0
  68. package/dist/multi-select.d.ts +23 -0
  69. package/dist/multi-select.js +63 -0
  70. package/dist/navigation-menu.d.ts +26 -0
  71. package/dist/navigation-menu.js +27 -0
  72. package/dist/number-input.d.ts +14 -0
  73. package/dist/number-input.js +37 -0
  74. package/dist/pagination.d.ts +24 -0
  75. package/dist/pagination.js +60 -0
  76. package/dist/popover.d.ts +30 -0
  77. package/dist/popover.js +27 -0
  78. package/dist/progress.d.ts +18 -0
  79. package/dist/progress.js +9 -0
  80. package/dist/radio-group.d.ts +46 -0
  81. package/dist/radio-group.js +16 -0
  82. package/dist/resizable-panels.d.ts +22 -0
  83. package/dist/resizable-panels.js +70 -0
  84. package/dist/skeleton.d.ts +17 -0
  85. package/dist/skeleton.js +9 -0
  86. package/dist/slider.d.ts +41 -0
  87. package/dist/slider.js +29 -0
  88. package/dist/stepper.d.ts +25 -0
  89. package/dist/stepper.js +25 -0
  90. package/dist/styles.css +3369 -0
  91. package/dist/tabs.d.ts +32 -0
  92. package/dist/tabs.js +8 -0
  93. package/dist/toast.d.ts +54 -0
  94. package/dist/toast.js +107 -0
  95. package/dist/toggle-group.d.ts +32 -0
  96. package/dist/toggle-group.js +16 -0
  97. package/dist/toggles.d.ts +33 -0
  98. package/dist/toggles.js +29 -0
  99. package/dist/tooltip.d.ts +26 -0
  100. package/dist/tooltip.js +16 -0
  101. package/dist/transition.d.ts +27 -0
  102. package/dist/transition.js +148 -0
  103. package/dist/tree-view.d.ts +22 -0
  104. package/dist/tree-view.js +128 -0
  105. package/dist/typography.d.ts +23 -0
  106. package/dist/typography.js +20 -0
  107. package/dist/utils.d.ts +13 -0
  108. package/dist/utils.js +17 -0
  109. package/package.json +73 -0
@@ -0,0 +1,108 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { useEffect, useRef, useState } from 'react';
4
+ import { cx } from '../utils.js';
5
+ /** Decoración interna: el primer hijo conserva el scroll, su semántica y su foco.
6
+ * Observamos tamaño y contenido porque buscar, traducir o cargar opciones puede
7
+ * cambiar el desbordamiento sin que el usuario desplace la lista. */
8
+ export function ScrollHints({ children, className }) {
9
+ const region = useRef(null);
10
+ const [above, setAbove] = useState(false);
11
+ const [below, setBelow] = useState(false);
12
+ useEffect(() => {
13
+ const root = region.current;
14
+ if (!root)
15
+ return;
16
+ let viewport = null;
17
+ const observed = new Set();
18
+ const update = () => {
19
+ if (!viewport)
20
+ return;
21
+ const maximum = viewport.scrollHeight - viewport.clientHeight;
22
+ setAbove(maximum > 1 && viewport.scrollTop > 1);
23
+ setBelow(maximum > 1 && viewport.scrollTop < maximum - 1);
24
+ };
25
+ const measure = () => {
26
+ if (!viewport)
27
+ return;
28
+ // La máscara deja opaca la barra real, incluso si está a la izquierda (RTL).
29
+ // Con barras superpuestas o sin desbordamiento no reservamos ninguna franja.
30
+ const style = getComputedStyle(viewport);
31
+ const left = Number.parseFloat(style.borderLeftWidth) || 0;
32
+ const right = Number.parseFloat(style.borderRightWidth) || 0;
33
+ const width = Math.max(0, viewport.offsetWidth - viewport.clientWidth - left - right);
34
+ const onLeft = viewport.clientLeft > left;
35
+ const scrollbarLeft = `${onLeft ? width : 0}px`;
36
+ const scrollbarRight = `${onLeft ? 0 : width}px`;
37
+ if (root.style.getPropertyValue('--dgm-scrollbar-left') !== scrollbarLeft) {
38
+ root.style.setProperty('--dgm-scrollbar-left', scrollbarLeft);
39
+ }
40
+ if (root.style.getPropertyValue('--dgm-scrollbar-right') !== scrollbarRight) {
41
+ root.style.setProperty('--dgm-scrollbar-right', scrollbarRight);
42
+ }
43
+ update();
44
+ };
45
+ const resize = new ResizeObserver(measure);
46
+ const observeSizes = () => {
47
+ const nodes = new Set(viewport ? [viewport, ...viewport.children] : []);
48
+ for (const node of observed) {
49
+ if (!nodes.has(node)) {
50
+ resize.unobserve(node);
51
+ observed.delete(node);
52
+ }
53
+ }
54
+ for (const node of nodes) {
55
+ if (!observed.has(node)) {
56
+ resize.observe(node);
57
+ observed.add(node);
58
+ }
59
+ }
60
+ };
61
+ const bindViewport = () => {
62
+ // Radix puede insertar un <style> antes del viewport. La identidad del nodo,
63
+ // no la de children/JSX, determina la duración de estas suscripciones.
64
+ const candidate = root.querySelector(':scope > :not(style):not(script)');
65
+ const next = candidate instanceof HTMLElement ? candidate : null;
66
+ if (next === viewport)
67
+ return false;
68
+ viewport?.removeEventListener('scroll', update);
69
+ viewport = next;
70
+ viewport?.addEventListener('scroll', update, { passive: true });
71
+ observeSizes();
72
+ if (!viewport) {
73
+ setAbove(false);
74
+ setBelow(false);
75
+ root.style.removeProperty('--dgm-scrollbar-left');
76
+ root.style.removeProperty('--dgm-scrollbar-right');
77
+ }
78
+ return true;
79
+ };
80
+ bindViewport();
81
+ measure();
82
+ const content = new MutationObserver((records) => {
83
+ // Las variables de la máscara pertenecen a root: no se realimentan como
84
+ // mutaciones del contenido. Un cambio de texto/estilo solo necesita medir.
85
+ if (!records.some((record) => record.target !== root || record.type === 'childList'))
86
+ return;
87
+ const rebound = bindViewport();
88
+ if (!rebound && records.some((record) => record.type === 'childList'))
89
+ observeSizes();
90
+ measure();
91
+ });
92
+ content.observe(root, {
93
+ childList: true,
94
+ subtree: true,
95
+ characterData: true,
96
+ attributes: true,
97
+ attributeFilter: ['hidden', 'style'],
98
+ });
99
+ return () => {
100
+ resize.disconnect();
101
+ content.disconnect();
102
+ viewport?.removeEventListener('scroll', update);
103
+ viewport = null;
104
+ observed.clear();
105
+ };
106
+ }, []);
107
+ return (_jsx("div", { ref: region, className: cx('dgm-scroll-region', className), "data-more-above": above || undefined, "data-more-below": below || undefined, children: children }));
108
+ }
@@ -0,0 +1,2 @@
1
+ /** Estado local o controlado. No cambiar de modo durante la vida del componente. */
2
+ export declare function useControllable<T>(value: T | undefined, initial: T, onChange?: (next: T) => void): readonly [T, (next: T) => void];
@@ -0,0 +1,13 @@
1
+ 'use client';
2
+ import { useState } from 'react';
3
+ /** Estado local o controlado. No cambiar de modo durante la vida del componente. */
4
+ export function useControllable(value, initial, onChange) {
5
+ const [local, setLocal] = useState(initial);
6
+ const current = value === undefined ? local : value;
7
+ function change(next) {
8
+ if (value === undefined)
9
+ setLocal(next);
10
+ onChange?.(next);
11
+ }
12
+ return [current, change];
13
+ }
@@ -0,0 +1,5 @@
1
+ /** Un label reenvía su clic al botón después del pointerdown. Cerrar el panel
2
+ * durante ese pointerdown haría que el clic nativo lo abriese de nuevo. */
3
+ export declare function preventTriggerLabelDismiss(event: CustomEvent<{
4
+ originalEvent: PointerEvent | FocusEvent;
5
+ }>, trigger: HTMLButtonElement | null): boolean;
@@ -0,0 +1,26 @@
1
+ /** Un label reenvía su clic al botón después del pointerdown. Cerrar el panel
2
+ * durante ese pointerdown haría que el clic nativo lo abriese de nuevo. */
3
+ export function preventTriggerLabelDismiss(event, trigger) {
4
+ if (!trigger || trigger.disabled)
5
+ return false;
6
+ const target = event.target;
7
+ if (!(target instanceof Element) || trigger.contains(target))
8
+ return false;
9
+ const label = target.closest('label');
10
+ if (!label || label.control !== trigger)
11
+ return false;
12
+ // Un enlace o control dentro del label tiene su propia acción: no activa
13
+ // el botón asociado y debe conservar el cierre exterior y su destino de foco.
14
+ const interactive = target.closest('a[href], button, input, select, textarea, summary, [contenteditable="true"]');
15
+ if (interactive && label.contains(interactive))
16
+ return false;
17
+ const original = event.detail.originalEvent;
18
+ if (original.type === 'pointerdown') {
19
+ const pointer = original;
20
+ if (pointer.button !== 0)
21
+ return false;
22
+ }
23
+ // Cancela solo el descarte de Radix, no el clic nativo ni su traslado de foco.
24
+ event.preventDefault();
25
+ return true;
26
+ }
@@ -0,0 +1,27 @@
1
+ /** Indicador de actividad con animaciones CSS; compatible con renderizado en servidor. */
2
+ import type { CSSProperties, HTMLAttributes, ReactElement } from 'react';
3
+ export interface LoaderProps extends Omit<HTMLAttributes<HTMLSpanElement>, 'children' | 'color'> {
4
+ /** Estilo visual de la actividad. @defaultValue 'spinner' */
5
+ variant?: 'spinner' | 'dots' | 'bars' | 'pulse';
6
+ /** Tamaño de la figura, independiente de su etiqueta. @defaultValue 'md' */
7
+ size?: 'sm' | 'md' | 'lg' | 'xl';
8
+ /** Color CSS; admite currentColor y variables del tema. @defaultValue 'var(--dgm-primary)' */
9
+ color?: CSSProperties['color'];
10
+ /** Texto de carga. Si se omite, se usa la traducción de locale. */
11
+ label?: string;
12
+ /** Muestra la etiqueta también visualmente. @defaultValue false */
13
+ showLabel?: boolean;
14
+ /** Desactiva el anuncio si el contenedor ya comunica la carga. @defaultValue false */
15
+ decorative?: boolean;
16
+ /** Activa el movimiento, respetando siempre prefers-reduced-motion. @defaultValue true */
17
+ animate?: boolean;
18
+ /** Idioma de la etiqueta predeterminada. @defaultValue 'en' */
19
+ locale?: 'es' | 'en';
20
+ }
21
+ /**
22
+ * La figura es decorativa; solo el texto participa en el anuncio de estado.
23
+ * No indica un porcentaje ni controla peticiones. La aplicación decide cuándo
24
+ * mostrarlo y cuándo sustituirlo por el contenido. Usa decorative si ya existe
25
+ * otro anuncio de carga, especialmente al combinar varios indicadores.
26
+ */
27
+ export declare function Loader({ variant, size, color, label, showLabel, decorative, animate, locale, className, style, ...props }: LoaderProps): ReactElement;
package/dist/loader.js ADDED
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cx } from './utils.js';
3
+ /**
4
+ * La figura es decorativa; solo el texto participa en el anuncio de estado.
5
+ * No indica un porcentaje ni controla peticiones. La aplicación decide cuándo
6
+ * mostrarlo y cuándo sustituirlo por el contenido. Usa decorative si ya existe
7
+ * otro anuncio de carga, especialmente al combinar varios indicadores.
8
+ */
9
+ export function Loader({ variant = 'spinner', size = 'md', color = 'var(--dgm-primary)', label, showLabel = false, decorative = false, animate = true, locale = 'en', className, style, ...props }) {
10
+ const text = label ?? (locale === 'es' ? 'Cargando…' : 'Loading…');
11
+ return (_jsxs("span", { ...props, role: decorative ? undefined : 'status', "aria-live": decorative ? undefined : 'polite', "aria-atomic": decorative ? undefined : true, "aria-hidden": decorative || undefined, className: cx('dgm-loader', `dgm-loader--${variant}`, `dgm-loader--${size}`, !animate && 'dgm-loader--static', className), style: { color, ...style }, children: [_jsx("span", { className: "dgm-loader-figure", "aria-hidden": "true", children: variant !== 'spinner' &&
12
+ [0, 1, 2].map((index) => (_jsx("span", { className: "dgm-loader-piece", style: { '--dgm-loader-index': index } }, index))) }), _jsx("span", { className: showLabel ? 'dgm-loader-label' : 'dgm-loader-label-hidden', children: text })] }));
13
+ }
@@ -0,0 +1,23 @@
1
+ import { type ReactNode, type ReactElement } from 'react';
2
+ export interface ModalProps {
3
+ /** La aplicación controla la apertura. */
4
+ open: boolean;
5
+ /** Se llama al pulsar Escape, la X o el fondo (si está permitido). */
6
+ onOpenChange: (open: boolean) => void;
7
+ /** Título visible y nombre accesible obligatorio. */
8
+ title: ReactNode;
9
+ /** Resumen opcional enlazado con aria-describedby. */
10
+ description?: ReactNode;
11
+ /** Cuerpo desplazable: formularios, texto u otros componentes. */
12
+ children: ReactNode;
13
+ /** Acciones que permanecen fuera del área desplazable. */
14
+ footer?: ReactNode;
15
+ /** Etiqueta accesible localizada del botón X. @defaultValue 'Close dialog' */
16
+ closeLabel?: string;
17
+ /** Permite cerrar al pulsar fuera. Escape y X siguen disponibles. @defaultValue true */
18
+ closeOnOverlayClick?: boolean;
19
+ /** Clase añadida al panel, sin alterar el overlay. */
20
+ className?: string;
21
+ }
22
+ /** Portal con cabecera fija y cuerpo desplazable para contenidos largos y pantallas pequeñas. */
23
+ export declare function Modal({ open, onOpenChange, title, description, children, footer, closeLabel, closeOnOverlayClick, className, }: ModalProps): ReactElement;
package/dist/modal.js ADDED
@@ -0,0 +1,30 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ /** Modal controlado. Radix conserva el foco dentro, bloquea el fondo y gestiona Escape. */
4
+ import { CloseIcon } from './internal/icons.js';
5
+ import { useId, useRef } from 'react';
6
+ import * as Dialog from '@radix-ui/react-dialog';
7
+ import { restoreFocus } from './internal/focus.js';
8
+ import { OverlayLayerProvider, useOverlayLayer } from './internal/overlay-layer.js';
9
+ import { cx, hasContent } from './utils.js';
10
+ /** Portal con cabecera fija y cuerpo desplazable para contenidos largos y pantallas pequeñas. */
11
+ export function Modal({ open, onOpenChange, title, description, children, footer, closeLabel = 'Close dialog', closeOnOverlayClick = true, className, }) {
12
+ const { depth, style: layerStyle } = useOverlayLayer();
13
+ const returnFocus = useRef(null);
14
+ const descriptionId = useId();
15
+ return (_jsx(OverlayLayerProvider, { depth: depth, children: _jsx(Dialog.Root, { open: open, onOpenChange: onOpenChange, children: _jsxs(Dialog.Portal, { children: [_jsx(Dialog.Overlay, { className: "dgm-modal-overlay", style: layerStyle }), _jsxs(Dialog.Content, { style: layerStyle, className: cx('dgm-modal', className), "aria-describedby": hasContent(description) ? descriptionId : undefined, onOpenAutoFocus: () => {
16
+ // Se captura antes de que Radix enfoque el contenido, incluso sin Dialog.Trigger.
17
+ returnFocus.current =
18
+ document.activeElement instanceof HTMLElement ? document.activeElement : null;
19
+ }, onCloseAutoFocus: (event) => {
20
+ if (returnFocus.current?.isConnected) {
21
+ event.preventDefault();
22
+ // Restaurar el foco no debe desplazar la página hacia el disparador,
23
+ // especialmente si está dentro de una cabecera sticky.
24
+ restoreFocus(returnFocus.current);
25
+ }
26
+ }, onPointerDownOutside: (event) => {
27
+ if (!closeOnOverlayClick)
28
+ event.preventDefault();
29
+ }, children: [_jsxs("header", { className: "dgm-modal-header", children: [_jsx(Dialog.Title, { className: "dgm-modal-title", children: title }), hasContent(description) && (_jsx(Dialog.Description, { id: descriptionId, className: "dgm-description", children: description })), _jsx(Dialog.Close, { className: "dgm-icon-close", "aria-label": closeLabel, children: _jsx(CloseIcon, {}) })] }), _jsx("div", { className: "dgm-modal-body", children: children }), hasContent(footer) && _jsx("footer", { className: "dgm-modal-footer", children: footer })] })] }) }) }));
30
+ }
@@ -0,0 +1,23 @@
1
+ /** Selector múltiple con búsqueda. El panel es un diálogo con checkboxes, no un listbox de selección única. */
2
+ import { type ReactElement, type ReactNode } from 'react';
3
+ import type { ComboboxOption } from './combobox.js';
4
+ export interface MultiSelectProps {
5
+ label: ReactNode;
6
+ options: readonly ComboboxOption[];
7
+ value?: string[];
8
+ defaultValue?: string[];
9
+ onValueChange?: (value: string[]) => void;
10
+ name?: string;
11
+ required?: boolean;
12
+ disabled?: boolean;
13
+ description?: ReactNode;
14
+ error?: ReactNode;
15
+ placeholder?: string;
16
+ searchPlaceholder?: string;
17
+ emptyMessage?: string;
18
+ locale?: 'es' | 'en';
19
+ id?: string;
20
+ className?: string;
21
+ }
22
+ /** Los valores son IDs del catálogo. FormData.getAll(name) devuelve todos los seleccionados habilitados. */
23
+ export declare function MultiSelect({ label, options, value, defaultValue, onValueChange, name, required, disabled, description, error, placeholder, searchPlaceholder, emptyMessage, locale, id, className, }: MultiSelectProps): ReactElement;
@@ -0,0 +1,63 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ /** Selector múltiple con búsqueda. El panel es un diálogo con checkboxes, no un listbox de selección única. */
4
+ import { useEffect, useId, useMemo, useRef, useState, } from 'react';
5
+ import { Popover } from './popover.js';
6
+ import { Button } from './button.js';
7
+ import { Input } from './fields.js';
8
+ import { Checkbox } from './toggles.js';
9
+ import { useControllable } from './internal/state.js';
10
+ import { ChevronIcon, CloseIcon } from './internal/icons.js';
11
+ import { cx, hasContent } from './utils.js';
12
+ import { ScrollHints } from './internal/scroll-hints.js';
13
+ import { useFieldOpen } from './internal/field-open.js';
14
+ /** Los valores son IDs del catálogo. FormData.getAll(name) devuelve todos los seleccionados habilitados. */
15
+ export function MultiSelect({ label, options, value, defaultValue = [], onValueChange, name, required, disabled, description, error, placeholder, searchPlaceholder, emptyMessage, locale = 'en', id, className, }) {
16
+ const [selected, change] = useControllable(value, defaultValue, onValueChange);
17
+ const [query, setQuery] = useState('');
18
+ const [open, setOpen] = useFieldOpen(disabled);
19
+ // Todos los cierres, incluida la desactivación externa, descartan la búsqueda.
20
+ useEffect(() => {
21
+ if (!open)
22
+ setQuery('');
23
+ }, [open]);
24
+ const [invalid, setInvalid] = useState(false);
25
+ const trigger = useRef(null);
26
+ const generated = useId();
27
+ const fieldId = id ?? `dgm-multi-${generated}`;
28
+ const es = locale === 'es';
29
+ const selectedValues = useMemo(() => new Set(selected), [selected]);
30
+ const current = useMemo(() => options.filter((option) => selectedValues.has(option.value)), [options, selectedValues]);
31
+ const normalizedQuery = query.trim().toLocaleLowerCase(locale);
32
+ const filtered = useMemo(() => normalizedQuery
33
+ ? options.filter((option) => [option.label, ...(option.keywords ?? [])]
34
+ .join(' ')
35
+ .toLocaleLowerCase(locale)
36
+ .includes(normalizedQuery))
37
+ : options, [options, locale, normalizedQuery]);
38
+ const message = error ||
39
+ (invalid
40
+ ? es
41
+ ? 'Selecciona al menos una opción.'
42
+ : 'Select at least one option.'
43
+ : undefined);
44
+ const describedBy = cx(hasContent(description) && `${fieldId}-description`, hasContent(message) && `${fieldId}-error`) || undefined;
45
+ function update(next) {
46
+ if (disabled)
47
+ return;
48
+ change(next);
49
+ setInvalid(false);
50
+ }
51
+ return (_jsxs("div", { className: cx('dgm-field', 'dgm-multi-select', className), children: [_jsxs("label", { className: "dgm-label", htmlFor: fieldId, children: [label, required && _jsx("span", { "aria-hidden": "true", children: " *" })] }), _jsx("select", { multiple: true, className: "dgm-form-value", "aria-hidden": "true", tabIndex: -1, name: name, required: required, disabled: disabled, value: current.filter((option) => !option.disabled).map((option) => option.value), onChange: (event) => update(Array.from(event.target.selectedOptions, (option) => option.value)), onInvalid: (event) => {
52
+ event.preventDefault();
53
+ setInvalid(true);
54
+ trigger.current?.focus({ preventScroll: true });
55
+ }, children: options.map((option) => (_jsx("option", { value: option.value, disabled: option.disabled, children: option.label }, option.value))) }), _jsx(Popover, { title: label, open: open, onOpenChange: setOpen, disabled: disabled, closeLabel: es ? 'Cerrar selector' : 'Close selector', className: "dgm-multi-panel", content: _jsxs(_Fragment, { children: [_jsx(Input, { label: es ? 'Buscar opciones' : 'Search options', placeholder: searchPlaceholder, type: "search", value: query, onChange: (event) => setQuery(event.target.value) }), _jsx(ScrollHints, { className: "dgm-multi-scroll-region", children: _jsx("div", { className: "dgm-multi-options", children: filtered.length ? (filtered.map((option) => (_jsx(Checkbox, { disabled: disabled || option.disabled, checked: selectedValues.has(option.value), label: _jsxs("span", { className: "dgm-option-label", children: [option.icon && _jsx("span", { "aria-hidden": "true", children: option.icon }), option.label] }), onChange: (event) => update(event.target.checked
56
+ ? [...selected, option.value]
57
+ : selected.filter((item) => item !== option.value)) }, option.value)))) : (_jsx("p", { role: "status", className: "dgm-description", children: emptyMessage ?? (es ? 'Sin resultados.' : 'No results.') })) }) }), _jsx(Button, { variant: "secondary", fullWidth: true, onClick: () => setOpen(false), children: es ? 'Listo' : 'Done' })] }), children: _jsxs("button", { ref: trigger, type: "button", id: fieldId, disabled: disabled, "aria-describedby": describedBy, "aria-invalid": hasContent(message) || undefined, className: "dgm-input dgm-multi-trigger", children: [_jsx("span", { children: current.length
58
+ ? `${current.length} ${es ? 'seleccionadas' : 'selected'}`
59
+ : (placeholder ?? (es ? 'Seleccionar opciones' : 'Select options')) }), _jsx(ChevronIcon, {})] }) }), current.length > 0 && (_jsx("ul", { className: "dgm-multi-tags", "aria-label": es ? 'Opciones seleccionadas' : 'Selected options', children: current.map((option) => (_jsxs("li", { children: [option.label, _jsx("button", { type: "button", disabled: disabled || option.disabled, "aria-label": `${es ? 'Quitar' : 'Remove'} ${option.label}`, onClick: () => {
60
+ update(selected.filter((item) => item !== option.value));
61
+ trigger.current?.focus({ preventScroll: true });
62
+ }, children: _jsx(CloseIcon, {}) })] }, option.value))) })), hasContent(description) && (_jsx("span", { id: `${fieldId}-description`, className: "dgm-description", children: description })), hasContent(message) && (_jsx("span", { id: `${fieldId}-error`, className: "dgm-error", children: message }))] }));
63
+ }
@@ -0,0 +1,26 @@
1
+ /** Navegación de destinos, no menú de acciones. Cada submenú conserva enlaces HTML auténticos. */
2
+ import { type ReactElement, type ReactNode } from 'react';
3
+ export interface NavigationLink {
4
+ id: string;
5
+ label: string;
6
+ href: string;
7
+ description?: string;
8
+ icon?: ReactNode;
9
+ }
10
+ export type NavigationMenuItem = NavigationLink | {
11
+ id: string;
12
+ label: string;
13
+ items: readonly NavigationLink[];
14
+ };
15
+ export interface NavigationMenuProps {
16
+ label: string;
17
+ items: readonly NavigationMenuItem[];
18
+ /** href activo exacto; el consumidor conoce la ruta actual. */
19
+ currentHref?: string;
20
+ /** Adaptador para routers: devuelve un único enlace que acepte props y ref. */
21
+ renderLink?: (item: NavigationLink) => ReactElement;
22
+ delayDuration?: number;
23
+ className?: string;
24
+ }
25
+ /** En móvil los elementos se distribuyen en filas y los paneles permanecen dentro del ancho disponible. */
26
+ export declare function NavigationMenu({ label, items, currentHref, renderLink, delayDuration, className, }: NavigationMenuProps): ReactElement;
@@ -0,0 +1,27 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ /** Navegación de destinos, no menú de acciones. Cada submenú conserva enlaces HTML auténticos. */
4
+ import { useCallback, useRef } from 'react';
5
+ import * as Primitive from '@radix-ui/react-navigation-menu';
6
+ import { ChevronIcon } from './internal/icons.js';
7
+ import { cx } from './utils.js';
8
+ import { observeNavigationOrigin } from './internal/navigation-origin.js';
9
+ /** Cada panel conserva su ancla, también al cambiar entre varios submenús. */
10
+ function NavigationPanel({ label, children }) {
11
+ const trigger = useRef(null);
12
+ const cleanup = useRef(undefined);
13
+ const contentRef = useCallback((panel) => {
14
+ cleanup.current?.();
15
+ cleanup.current =
16
+ panel && trigger.current ? observeNavigationOrigin(panel, trigger.current) : undefined;
17
+ }, []);
18
+ return (_jsxs(_Fragment, { children: [_jsxs(Primitive.Trigger, { ref: trigger, className: "dgm-navigation-trigger", children: [label, _jsx(ChevronIcon, {})] }), _jsx(Primitive.Content, { ref: contentRef, className: "dgm-navigation-content", children: children })] }));
19
+ }
20
+ /** En móvil los elementos se distribuyen en filas y los paneles permanecen dentro del ancho disponible. */
21
+ export function NavigationMenu({ label, items, currentHref, renderLink, delayDuration = 150, className, }) {
22
+ function link(item) {
23
+ const active = currentHref === item.href;
24
+ return (_jsx(Primitive.Link, { asChild: true, active: active, "aria-current": active ? 'page' : undefined, className: "dgm-navigation-link", children: renderLink ? (renderLink(item)) : (_jsxs("a", { href: item.href, children: [item.icon && _jsx("span", { "aria-hidden": "true", children: item.icon }), _jsxs("span", { children: [item.label, item.description && _jsx("small", { children: item.description })] })] })) }, item.id));
25
+ }
26
+ return (_jsx(Primitive.Root, { "aria-label": label, delayDuration: delayDuration, className: cx('dgm-navigation', className), children: _jsx(Primitive.List, { className: "dgm-navigation-list", children: items.map((item) => (_jsx(Primitive.Item, { className: "dgm-navigation-item", children: 'items' in item ? (_jsx(NavigationPanel, { label: item.label, children: _jsx("ul", { children: item.items.map((child) => (_jsx("li", { children: link(child) }, child.id))) }) })) : (link(item)) }, item.id))) }) }));
27
+ }
@@ -0,0 +1,14 @@
1
+ import { type ChangeEvent } from 'react';
2
+ import { type InputProps } from './fields.js';
3
+ export interface NumberInputProps extends Omit<InputProps, 'type' | 'value' | 'defaultValue' | 'onChange' | 'step'> {
4
+ /** undefined activa el modo no controlado; '' representa un campo vacío controlado. */
5
+ value?: number | '';
6
+ defaultValue?: number;
7
+ onValueChange?: (value: number | '') => void;
8
+ onChange?: (event: ChangeEvent<HTMLInputElement>) => void;
9
+ /** Incremento positivo. @defaultValue 1 */
10
+ step?: number;
11
+ locale?: 'es' | 'en';
12
+ }
13
+ /** Conserva min/max, required, reset nativo, ref y validación del navegador. */
14
+ export declare const NumberInput: import("react").ForwardRefExoticComponent<NumberInputProps & import("react").RefAttributes<HTMLInputElement>>;
@@ -0,0 +1,37 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ /** Campo numérico nativo. El navegador aplica stepUp/stepDown sin errores de suma decimal. */
4
+ import { MinusIcon, PlusIcon } from './internal/icons.js';
5
+ import { forwardRef, useRef, useImperativeHandle } from 'react';
6
+ import { Input } from './fields.js';
7
+ import { Button } from './button.js';
8
+ /** Conserva min/max, required, reset nativo, ref y validación del navegador. */
9
+ export const NumberInput = forwardRef(function NumberInput({ onValueChange, onChange, step = 1, disabled, readOnly, locale = 'en', ...props }, ref) {
10
+ const input = useRef(null);
11
+ useImperativeHandle(ref, () => input.current);
12
+ if (!Number.isFinite(step) || step <= 0)
13
+ throw new RangeError('NumberInput step must be positive.');
14
+ function increment(direction) {
15
+ const node = input.current;
16
+ if (!node || disabled || readOnly)
17
+ return;
18
+ const previous = node.value;
19
+ if (direction < 0)
20
+ node.stepDown();
21
+ else
22
+ node.stepUp();
23
+ const next = node.value;
24
+ if (next !== previous) {
25
+ // Restaurar el valor anterior hace que React detecte el cambio nativo y entregue onChange real.
26
+ node.value = previous;
27
+ const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set;
28
+ setter.call(node, next);
29
+ node.dispatchEvent(new Event('input', { bubbles: true }));
30
+ }
31
+ node.focus({ preventScroll: true });
32
+ }
33
+ return (_jsxs("div", { className: "dgm-number-field", children: [_jsx(Input, { ...props, type: "number", step: step, disabled: disabled, readOnly: readOnly, ref: input, onChange: (event) => {
34
+ onChange?.(event);
35
+ onValueChange?.(Number.isFinite(event.target.valueAsNumber) ? event.target.valueAsNumber : '');
36
+ } }), _jsxs("div", { className: "dgm-number-actions", children: [_jsx(Button, { variant: "outline", iconOnly: true, disabled: disabled || readOnly, "aria-label": locale === 'es' ? 'Disminuir' : 'Decrease', onClick: () => increment(-1), children: _jsx(MinusIcon, {}) }), _jsx(Button, { variant: "outline", iconOnly: true, disabled: disabled || readOnly, "aria-label": locale === 'es' ? 'Aumentar' : 'Increase', onClick: () => increment(1), children: _jsx(PlusIcon, {}) })] })] }));
37
+ });
@@ -0,0 +1,24 @@
1
+ import { type ReactElement } from 'react';
2
+ export interface PaginationProps {
3
+ /** Página actual, comenzando en 1. */
4
+ page: number;
5
+ /** Número total de páginas. 0 representa una colección vacía. */
6
+ pageCount: number;
7
+ /** Solicita la página elegida; no obtiene datos de red. */
8
+ onPageChange: (page: number) => void;
9
+ /** Vecinos alrededor de la página actual. @defaultValue 1 */
10
+ siblings?: number;
11
+ /** Desactiva todos los controles, por ejemplo durante una petición. */
12
+ disabled?: boolean;
13
+ /** Etiquetas integradas. @defaultValue 'en' */
14
+ locale?: 'es' | 'en';
15
+ /** Nombre de la región de navegación. */
16
+ label?: string;
17
+ /** Tamaño de los botones. @defaultValue 'sm' */
18
+ size?: 'sm' | 'md' | 'lg';
19
+ /** Muestra tantas páginas como quepan en el contenedor; sustituye siblings. @defaultValue false */
20
+ fullWidth?: boolean;
21
+ className?: string;
22
+ }
23
+ /** Número acotado de botones incluso con miles de páginas; los saltos no son interactivos. */
24
+ export declare function Pagination({ page, pageCount, onPageChange, siblings, disabled, locale, label, size, fullWidth, className, }: PaginationProps): ReactElement;
@@ -0,0 +1,60 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ /** Navegación numerada; el consumidor decide qué contenido corresponde a cada página. */
4
+ import { ChevronLeftIcon, ChevronRightIcon } from './internal/icons.js';
5
+ import { useEffect, useRef, useState } from 'react';
6
+ import { fitPaginationPages } from './internal/pagination.js';
7
+ import { Button } from './button.js';
8
+ import { cx } from './utils.js';
9
+ /** Número acotado de botones incluso con miles de páginas; los saltos no son interactivos. */
10
+ export function Pagination({ page, pageCount, onPageChange, siblings = 1, disabled, locale = 'en', label, size = 'sm', fullWidth = false, className, }) {
11
+ const navigation = useRef(null);
12
+ const previousButton = useRef(null);
13
+ const lastPageButton = useRef(null);
14
+ const [slots, setSlots] = useState(null);
15
+ const count = Number.isFinite(pageCount) ? Math.max(0, Math.floor(pageCount)) : 0;
16
+ const current = Math.max(1, Math.min(count || 1, Number.isFinite(page) ? Math.floor(page) : 1));
17
+ const radius = Number.isFinite(siblings) ? Math.max(0, Math.min(3, Math.floor(siblings))) : 1;
18
+ const pages = new Set([1, count]);
19
+ for (let value = Math.max(1, current - radius); value <= Math.min(count, current + radius); value++)
20
+ pages.add(value);
21
+ useEffect(() => {
22
+ if (!fullWidth) {
23
+ setSlots(null);
24
+ return;
25
+ }
26
+ const nav = navigation.current;
27
+ const arrow = previousButton.current;
28
+ const number = lastPageButton.current;
29
+ if (!nav || !arrow || !number)
30
+ return;
31
+ const measure = () => {
32
+ const style = getComputedStyle(nav);
33
+ const gap = parseFloat(style.columnGap) || 0;
34
+ const available = nav.clientWidth -
35
+ (parseFloat(style.paddingLeft) || 0) -
36
+ (parseFloat(style.paddingRight) || 0);
37
+ const numberWidth = number.getBoundingClientRect().width;
38
+ const arrowWidth = arrow.getBoundingClientRect().width;
39
+ if (available <= 0 || numberWidth <= 0)
40
+ return;
41
+ // Dos flechas, una separación inicial y una celda + separación por posición.
42
+ setSlots(Math.max(1, Math.floor((available - 2 * arrowWidth - gap) / (numberWidth + gap))));
43
+ };
44
+ measure();
45
+ const observer = new ResizeObserver(measure);
46
+ // También observamos los controles: un cambio de fuente o tamaño altera su ancho.
47
+ observer.observe(nav);
48
+ observer.observe(arrow);
49
+ observer.observe(number);
50
+ return () => observer.disconnect();
51
+ }, [fullWidth, size, count, current, slots]);
52
+ const ordered = fullWidth && slots !== null
53
+ ? fitPaginationPages(current, count, slots)
54
+ : [...pages].filter((value) => value > 0 && value <= count).sort((a, b) => a - b);
55
+ const es = locale === 'es';
56
+ return (_jsxs("nav", { ref: navigation, style: { '--dgm-page-digits': String(count).length }, "aria-label": label ?? (es ? 'Paginación' : 'Pagination'), className: cx('dgm-pagination', `dgm-pagination--${size}`, fullWidth && 'dgm-pagination--full', className), children: [_jsx(Button, { ref: previousButton, iconOnly: true, size: size, variant: "outline", disabled: disabled || current <= 1 || count === 0, onClick: () => onPageChange(current - 1), "aria-label": es ? 'Página anterior' : 'Previous page', children: _jsx(ChevronLeftIcon, {}) }), ordered.map((value, index) => (_jsxs("span", { className: "dgm-pagination-part", children: [index > 0 && value - ordered[index - 1] > 1 && (_jsx("span", { className: "dgm-pagination-gap", "aria-hidden": "true", children: "\u2026" })), _jsx(Button, { ref: value === ordered[ordered.length - 1] ? lastPageButton : undefined, size: size, variant: value === current ? 'primary' : 'ghost', "aria-label": `${es ? 'Página' : 'Page'} ${value}`, "aria-current": value === current ? 'page' : undefined, disabled: disabled, onClick: () => {
57
+ if (value !== current)
58
+ onPageChange(value);
59
+ }, children: value })] }, value))), _jsx(Button, { iconOnly: true, size: size, variant: "outline", disabled: disabled || current >= count || count === 0, onClick: () => onPageChange(current + 1), "aria-label": es ? 'Página siguiente' : 'Next page', children: _jsx(ChevronRightIcon, {}) })] }));
60
+ }
@@ -0,0 +1,30 @@
1
+ /** Panel contextual interactivo. Radix coordina foco, colisiones y cierre por Escape. */
2
+ import { type ReactElement, type ReactNode } from 'react';
3
+ export interface PopoverProps {
4
+ /** Un único botón que acepte props y ref. */
5
+ children: ReactElement;
6
+ /** Título visible y nombre accesible del panel. */
7
+ title: ReactNode;
8
+ /** Contenido interactivo o informativo. */
9
+ content: ReactNode;
10
+ /** Apertura controlada. */
11
+ open?: boolean;
12
+ /** Apertura inicial no controlada. @defaultValue false */
13
+ defaultOpen?: boolean;
14
+ /** Notifica los cambios de apertura. */
15
+ onOpenChange?: (open: boolean) => void;
16
+ /** Lado preferido, con detección de colisiones. @defaultValue 'bottom' */
17
+ side?: 'top' | 'right' | 'bottom' | 'left';
18
+ /** Alineación del panel. @defaultValue 'start' */
19
+ align?: 'start' | 'center' | 'end';
20
+ /** Separación en píxeles. @defaultValue 8 */
21
+ sideOffset?: number;
22
+ /** Desactiva el disparador. */
23
+ disabled?: boolean;
24
+ /** Nombre accesible localizado de la X. @defaultValue 'Close popover' */
25
+ closeLabel?: string;
26
+ /** Clase del panel renderizado en el portal. */
27
+ className?: string;
28
+ }
29
+ /** El panel no bloquea la página; un clic fuera conserva su destino de foco. */
30
+ export declare function Popover({ children, title, content, open, defaultOpen, onOpenChange, side, align, sideOffset, disabled, closeLabel, className, }: PopoverProps): ReactElement;
@@ -0,0 +1,27 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ /** Panel contextual interactivo. Radix coordina foco, colisiones y cierre por Escape. */
4
+ import { useId, useRef } from 'react';
5
+ import * as Primitive from '@radix-ui/react-popover';
6
+ import { restoreFocus } from './internal/focus.js';
7
+ import { preventTriggerLabelDismiss } from './internal/trigger-label.js';
8
+ import { CloseIcon } from './internal/icons.js';
9
+ import { OverlayLayerProvider, useOverlayLayer } from './internal/overlay-layer.js';
10
+ import { cx } from './utils.js';
11
+ /** El panel no bloquea la página; un clic fuera conserva su destino de foco. */
12
+ export function Popover({ children, title, content, open, defaultOpen, onOpenChange, side = 'bottom', align = 'start', sideOffset = 8, disabled, closeLabel = 'Close popover', className, }) {
13
+ const { depth, style: layerStyle } = useOverlayLayer();
14
+ const titleId = useId();
15
+ const trigger = useRef(null);
16
+ const outside = useRef(false);
17
+ return (_jsx(OverlayLayerProvider, { depth: depth, children: _jsxs(Primitive.Root, { open: open, defaultOpen: defaultOpen, onOpenChange: onOpenChange, children: [_jsx(Primitive.Trigger, { asChild: true, ref: trigger, disabled: disabled, children: children }), _jsx(Primitive.Portal, { children: _jsxs(Primitive.Content, { style: layerStyle, side: side, align: align, sideOffset: sideOffset, collisionPadding: 12, "aria-labelledby": titleId, className: cx('dgm-popover', className), onInteractOutside: (event) => {
18
+ if (preventTriggerLabelDismiss(event, trigger.current))
19
+ return;
20
+ outside.current = true;
21
+ }, onCloseAutoFocus: (event) => {
22
+ event.preventDefault();
23
+ if (!outside.current)
24
+ restoreFocus(trigger.current);
25
+ outside.current = false;
26
+ }, children: [_jsx("div", { className: "dgm-popover-heading", id: titleId, children: title }), _jsx(Primitive.Close, { className: "dgm-icon-close", "aria-label": closeLabel, children: _jsx(CloseIcon, {}) }), _jsx("div", { className: "dgm-popover-body", children: content })] }) })] }) }));
27
+ }
@@ -0,0 +1,18 @@
1
+ /** Progreso nativo: omitir value representa una duración desconocida. Sin hidratación. */
2
+ import type { ReactElement } from 'react';
3
+ export interface ProgressProps {
4
+ /** Nombre accesible obligatorio. */
5
+ label: string;
6
+ /** Avance actual; se limita a 0..max. undefined es indeterminado. */
7
+ value?: number;
8
+ /** Límite superior positivo. @defaultValue 100 */
9
+ max?: number;
10
+ /** Muestra también el porcentaje en texto. @defaultValue false */
11
+ showValue?: boolean;
12
+ /** Explicación accesible más útil que el número, por ejemplo «2 de 5 archivos». */
13
+ valueText?: string;
14
+ /** @defaultValue 'md' */
15
+ size?: 'sm' | 'md' | 'lg';
16
+ className?: string;
17
+ }
18
+ export declare function Progress({ label, value, max, showValue, valueText, size, className, }: ProgressProps): ReactElement;