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,26 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ /** Selector de selección única. Radix aporta teclado, búsqueda por letras y gestión de foco. */
4
+ import { ChevronIcon, CheckIcon } from './internal/icons.js';
5
+ import { forwardRef, useId } from 'react';
6
+ import * as Primitive from '@radix-ui/react-select';
7
+ import { cx, hasContent } from './utils.js';
8
+ import { ScrollHints } from './internal/scroll-hints.js';
9
+ import { useFieldOpen } from './internal/field-open.js';
10
+ import { OverlayLayerProvider, useOverlayLayer } from './internal/overlay-layer.js';
11
+ /** Geometría centrada y tamaño fijo: evita que una fuente cambie la forma o el eje del icono. */
12
+ function Chevron() {
13
+ return _jsx(ChevronIcon, { className: "dgm-chevron-svg" });
14
+ }
15
+ /** Desplegable animado renderizado en un portal para evitar recortes por overflow. */
16
+ export const CustomSelect = forwardRef(function CustomSelect({ label, options, value, defaultValue, onValueChange, placeholder = 'Select…', description, error, disabled, required, name, id, className, wrapperClassName, }, ref) {
17
+ const [open, setOpen] = useFieldOpen(disabled);
18
+ const { depth, style: layerStyle } = useOverlayLayer();
19
+ const generatedId = useId();
20
+ const controlId = id ?? `dgm-custom-${generatedId}`;
21
+ const describedBy = cx(hasContent(description) && `${controlId}-description`, hasContent(error) && `${controlId}-error`) || undefined;
22
+ return (_jsxs("div", { className: cx('dgm-field', wrapperClassName), children: [_jsxs("label", { className: "dgm-label", id: `${controlId}-label`, htmlFor: controlId, children: [label, required && _jsx("span", { "aria-hidden": "true", children: " *" })] }), _jsx(OverlayLayerProvider, { depth: depth, children: _jsxs(Primitive.Root, { value: value, defaultValue: defaultValue, onValueChange: (next) => {
23
+ if (!disabled)
24
+ onValueChange?.(next);
25
+ }, open: open, onOpenChange: setOpen, disabled: disabled, required: required, name: name, children: [_jsxs(Primitive.Trigger, { ref: ref, id: controlId, "aria-labelledby": `${controlId}-label`, "aria-describedby": describedBy, "aria-invalid": hasContent(error) || undefined, className: cx('dgm-input', 'dgm-custom-trigger', className), children: [_jsx(Primitive.Value, { className: "dgm-custom-value", placeholder: placeholder }), _jsx(Primitive.Icon, { className: "dgm-custom-chevron", "aria-hidden": "true", children: _jsx(Chevron, {}) })] }), _jsx(Primitive.Portal, { children: _jsx(Primitive.Content, { position: "popper", sideOffset: 6, collisionPadding: 12, className: "dgm-custom-menu", style: layerStyle, children: _jsx(ScrollHints, { className: "dgm-custom-scroll-region", children: _jsx(Primitive.Viewport, { className: "dgm-custom-viewport", children: options.map((option) => (_jsxs(Primitive.Item, { value: option.value, disabled: disabled || option.disabled, textValue: option.label, className: "dgm-custom-option", children: [_jsx(Primitive.ItemText, { children: _jsxs("span", { className: "dgm-custom-value", children: [option.icon && (_jsx("span", { className: "dgm-option-icon", "aria-hidden": "true", children: option.icon })), option.label] }) }), _jsx(Primitive.ItemIndicator, { className: "dgm-custom-check", "aria-hidden": "true", children: _jsx(CheckIcon, {}) })] }, option.value))) }) }) }) })] }) }), hasContent(description) && (_jsx("span", { className: "dgm-description", id: `${controlId}-description`, children: description })), hasContent(error) && (_jsx("span", { className: "dgm-error", id: `${controlId}-error`, children: error }))] }));
26
+ });
@@ -0,0 +1,53 @@
1
+ import { type CSSProperties, type ReactElement, type ReactNode } from 'react';
2
+ export interface DataColumn<T extends object> {
3
+ /** Identificador único y estable. */
4
+ id: string;
5
+ header: string;
6
+ /** Valor primitivo para ordenar y buscar; cell solo afecta a la presentación. */
7
+ accessor: (row: T) => string | number | null | undefined;
8
+ cell?: (row: T) => ReactNode;
9
+ /** @defaultValue true */
10
+ sortable?: boolean;
11
+ /** Participa en el buscador global. @defaultValue true */
12
+ searchable?: boolean;
13
+ align?: 'start' | 'end';
14
+ /** Ancho estable CSS (px si es número); las columnas sin ancho reparten el espacio restante. */
15
+ width?: CSSProperties['width'];
16
+ }
17
+ export interface DataTableSort {
18
+ id: string;
19
+ desc: boolean;
20
+ }
21
+ export interface DataTableProps<T extends object> {
22
+ data: readonly T[];
23
+ columns: readonly DataColumn<T>[];
24
+ /** Identidad estable independiente de la posición al ordenar o paginar. */
25
+ getRowId: (row: T) => string;
26
+ /** Caption visible y nombre de la tabla. */
27
+ caption: string;
28
+ /** Orden controlado. Usa [] para quitarlo. */
29
+ sorting?: DataTableSort[];
30
+ defaultSorting?: DataTableSort[];
31
+ onSortingChange?: (sorting: DataTableSort[]) => void;
32
+ /** Buscador de todas las columnas habilitadas. @defaultValue true */
33
+ searchable?: boolean;
34
+ searchPlaceholder?: string;
35
+ /** Registros por página. @defaultValue 10 */
36
+ pageSize?: number;
37
+ /** Página controlada, comenzando en 1. */
38
+ page?: number;
39
+ onPageChange?: (page: number) => void;
40
+ /** Habilita selección por fila y de la página visible. @defaultValue false */
41
+ selectable?: boolean;
42
+ /** IDs seleccionados, opcionalmente controlados. */
43
+ selection?: readonly string[];
44
+ defaultSelection?: readonly string[];
45
+ onSelectionChange?: (ids: string[]) => void;
46
+ /** Muestra marcadores y desactiva operaciones mientras llegan los datos. */
47
+ loading?: boolean;
48
+ emptyMessage?: string;
49
+ locale?: 'es' | 'en';
50
+ className?: string;
51
+ }
52
+ /** Paginación y selección conservan IDs reales. El filtrado devuelve a la primera página. */
53
+ export declare function DataTable<T extends object>({ data, columns, getRowId, caption, sorting, defaultSorting, onSortingChange, searchable, searchPlaceholder, pageSize, page, onPageChange, selectable, selection, defaultSelection, onSelectionChange, loading, emptyMessage, locale, className, }: DataTableProps<T>): ReactElement;
@@ -0,0 +1,104 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ /** Tabla HTML con operaciones locales. TanStack calcula filas filtradas y ordenadas sin mutar data. */
4
+ import { ArrowUpIcon, ArrowDownIcon, SortIcon } from './internal/icons.js';
5
+ import { useEffect, useMemo, useRef, useState, } from 'react';
6
+ import { useTable, tableFeatures, rowSortingFeature, columnFilteringFeature, globalFilteringFeature, createSortedRowModel, createFilteredRowModel, filterFn_includesString, functionalUpdate, } from '@tanstack/react-table';
7
+ import { Button } from './button.js';
8
+ import { Checkbox } from './toggles.js';
9
+ import { Input } from './fields.js';
10
+ import { Pagination } from './pagination.js';
11
+ import { Skeleton } from './skeleton.js';
12
+ import { useControllable } from './internal/state.js';
13
+ import { cx } from './utils.js';
14
+ const features = tableFeatures({
15
+ rowSortingFeature,
16
+ columnFilteringFeature,
17
+ globalFilteringFeature,
18
+ sortedRowModel: createSortedRowModel(),
19
+ filteredRowModel: createFilteredRowModel(),
20
+ filterFns: { includesString: filterFn_includesString },
21
+ });
22
+ /** Paginación y selección conservan IDs reales. El filtrado devuelve a la primera página. */
23
+ export function DataTable({ data, columns, getRowId, caption, sorting, defaultSorting = [], onSortingChange, searchable = true, searchPlaceholder, pageSize = 10, page, onPageChange, selectable = false, selection, defaultSelection = [], onSelectionChange, loading = false, emptyMessage, locale = 'en', className, }) {
24
+ const [sort, changeSort] = useControllable(sorting, defaultSorting, onSortingChange);
25
+ const [currentPage, changePage] = useControllable(page, 1, onPageChange);
26
+ const [selected, changeSelection] = useControllable(selection, defaultSelection, onSelectionChange ? (next) => onSelectionChange([...next]) : undefined);
27
+ const [query, setQuery] = useState('');
28
+ // Mantiene el contrato público de arrays y evita recorrer la selección por fila.
29
+ const selectedIds = useMemo(() => new Set(selected), [selected]);
30
+ const selectPage = useRef(null);
31
+ const es = locale === 'es';
32
+ const collator = useMemo(() => new Intl.Collator(locale, { numeric: true, sensitivity: 'base' }), [locale]);
33
+ const definitions = useMemo(() => columns.map((column) => ({
34
+ id: column.id,
35
+ header: column.header,
36
+ accessorFn: column.accessor,
37
+ enableSorting: column.sortable !== false,
38
+ enableGlobalFilter: column.searchable !== false,
39
+ sortFn: (a, b, id) => {
40
+ const left = a.getValue(id);
41
+ const right = b.getValue(id);
42
+ return typeof left === 'number' && typeof right === 'number'
43
+ ? left - right
44
+ : collator.compare(String(left ?? ''), String(right ?? ''));
45
+ },
46
+ })), [columns, collator]);
47
+ const rowsData = useMemo(() => [...data], [data]);
48
+ const table = useTable({
49
+ features,
50
+ data: rowsData,
51
+ columns: definitions,
52
+ getRowId,
53
+ state: { sorting: sort, globalFilter: query },
54
+ onSortingChange: (updater) => {
55
+ changeSort(functionalUpdate(updater, sort));
56
+ changePage(1);
57
+ },
58
+ globalFilterFn: 'includesString',
59
+ enableMultiSort: false,
60
+ });
61
+ const filtered = table.getRowModel().rows;
62
+ const size = Number.isFinite(pageSize) ? Math.max(1, Math.floor(pageSize)) : 10;
63
+ const pageCount = Math.ceil(filtered.length / size);
64
+ const current = Math.max(1, Math.min(pageCount || 1, Number.isFinite(currentPage) ? currentPage : 1));
65
+ const visible = filtered.slice((current - 1) * size, current * size);
66
+ const visibleIds = visible.map((row) => row.id);
67
+ const allChecked = selectable && visibleIds.length > 0 && visibleIds.every((id) => selectedIds.has(id));
68
+ const someChecked = selectable && visibleIds.some((id) => selectedIds.has(id));
69
+ useEffect(() => {
70
+ if (selectPage.current)
71
+ selectPage.current.indeterminate = someChecked && !allChecked;
72
+ }, [someChecked, allChecked]);
73
+ // Si data se reduce, mostramos una página válida sin enviar callbacks durante el render.
74
+ useEffect(() => {
75
+ if (currentPage !== current)
76
+ changePage(current);
77
+ }, [current, currentPage]);
78
+ const selectedCount = useMemo(() => selectable
79
+ ? data.reduce((count, row) => count + Number(selectedIds.has(getRowId(row))), 0)
80
+ : 0, [data, getRowId, selectable, selectedIds]);
81
+ return (_jsxs("div", { className: cx('dgm-data-table', className), children: [searchable && (_jsx(Input, { label: es ? 'Buscar en la tabla' : 'Search table', placeholder: searchPlaceholder, value: query, disabled: loading, onChange: (event) => {
82
+ setQuery(event.target.value);
83
+ changePage(1);
84
+ } })), _jsx("div", { className: "dgm-table-scroll", role: "region", "aria-label": caption, tabIndex: 0, "aria-busy": loading, children: _jsxs("table", { style: { minWidth: `${columns.length * 10 + (selectable ? 3.5 : 0)}rem` }, children: [_jsx("caption", { children: caption }), _jsxs("colgroup", { children: [selectable && _jsx("col", { style: { width: '3.5rem' } }), columns.map((column) => (_jsx("col", { style: { width: column.width } }, column.id)))] }), _jsx("thead", { children: _jsxs("tr", { children: [selectable && (_jsx("th", { scope: "col", children: _jsx(Checkbox, { ref: selectPage, label: _jsx("span", { className: "dgm-visually-hidden", children: es ? 'Seleccionar página' : 'Select page' }), checked: allChecked, "aria-checked": someChecked && !allChecked ? 'mixed' : allChecked, disabled: loading || visibleIds.length === 0, onChange: (event) => {
85
+ const pageIds = new Set(visibleIds);
86
+ changeSelection(event.target.checked
87
+ ? [...new Set([...selected, ...visibleIds])]
88
+ : selected.filter((id) => !pageIds.has(id)));
89
+ } }) })), columns.map((column) => {
90
+ const instance = table.getColumn(column.id);
91
+ const direction = instance.getIsSorted();
92
+ return (_jsx("th", { scope: "col", style: { textAlign: column.align ?? 'start' }, "aria-sort": direction === 'asc'
93
+ ? 'ascending'
94
+ : direction === 'desc'
95
+ ? 'descending'
96
+ : undefined, children: column.sortable !== false ? (_jsxs(Button, { variant: "ghost", size: "sm", disabled: loading, onClick: instance.getToggleSortingHandler(), children: [column.header, _jsx("span", { "aria-hidden": "true", children: direction === 'asc' ? (_jsx(ArrowUpIcon, {})) : direction === 'desc' ? (_jsx(ArrowDownIcon, {})) : (_jsx(SortIcon, {})) })] })) : (column.header) }, column.id));
97
+ })] }) }), _jsx("tbody", { children: loading ? (Array.from({ length: Math.min(size, 5) }, (_, index) => (_jsx("tr", { children: _jsx("td", { colSpan: columns.length + (selectable ? 1 : 0), children: _jsx(Skeleton, { height: "1.5rem" }) }) }, index)))) : visible.length ? (visible.map((row) => (_jsxs("tr", { "data-selected": selectedIds.has(row.id) || undefined, children: [selectable && (_jsx("td", { children: _jsx(Checkbox, { checked: selectedIds.has(row.id), label: _jsxs("span", { className: "dgm-visually-hidden", children: [es ? 'Seleccionar fila' : 'Select row', " ", row.id] }), onChange: (event) => changeSelection(event.target.checked
98
+ ? [...new Set([...selected, row.id])]
99
+ : selected.filter((id) => id !== row.id)) }) })), columns.map((column) => (_jsx("td", { style: { textAlign: column.align ?? 'start' }, children: column.cell ? column.cell(row.original) : column.accessor(row.original) }, column.id)))] }, row.id)))) : (_jsx("tr", { children: _jsx("td", { colSpan: columns.length + (selectable ? 1 : 0), className: "dgm-table-empty", children: emptyMessage ?? (es ? 'No hay resultados.' : 'No results.') }) })) })] }) }), _jsxs("div", { className: "dgm-table-footer", children: [_jsx("p", { role: "status", children: loading
100
+ ? es
101
+ ? 'Cargando…'
102
+ : 'Loading…'
103
+ : `${filtered.length} ${es ? 'resultados' : 'results'}${selectable ? ` · ${selectedCount} ${es ? 'seleccionados' : 'selected'}` : ''}` }), _jsx(Pagination, { page: current, pageCount: pageCount, onPageChange: changePage, disabled: loading, locale: locale })] })] }));
104
+ }
@@ -0,0 +1,43 @@
1
+ import { type ReactElement, type ReactNode } from 'react';
2
+ export interface DatePickerBaseProps {
3
+ label: ReactNode;
4
+ description?: ReactNode;
5
+ error?: ReactNode;
6
+ /** Nombre de formulario. En rangos genera name.from y name.to. */
7
+ name?: string;
8
+ required?: boolean;
9
+ disabled?: boolean;
10
+ /** Límites inclusivos en formato YYYY-MM-DD. */
11
+ min?: string;
12
+ max?: string;
13
+ /** Días no disponibles, también en YYYY-MM-DD. */
14
+ disabledDates?: readonly string[];
15
+ /** Mes inicial si no hay selección (YYYY-MM-DD). */
16
+ defaultMonth?: string;
17
+ /** Traduce fechas, meses, días y nombres accesibles. @defaultValue 'en' */
18
+ locale?: 'es' | 'en';
19
+ placeholder?: string;
20
+ /** Muestra una acción para borrar la selección. @defaultValue true */
21
+ clearable?: boolean;
22
+ id?: string;
23
+ className?: string;
24
+ wrapperClassName?: string;
25
+ }
26
+ export interface DatePickerProps extends DatePickerBaseProps {
27
+ /** Fecha controlada; null significa sin selección. */
28
+ value?: string | null;
29
+ defaultValue?: string | null;
30
+ onValueChange?: (value: string | null) => void;
31
+ }
32
+ export interface DateRangeValue {
33
+ from: string;
34
+ to?: string;
35
+ }
36
+ export interface DateRangePickerProps extends DatePickerBaseProps {
37
+ /** Intervalo controlado. to queda ausente mientras se elige el segundo extremo. */
38
+ value?: DateRangeValue | null;
39
+ defaultValue?: DateRangeValue | null;
40
+ onValueChange?: (value: DateRangeValue | null) => void;
41
+ }
42
+ export declare function DatePicker({ value, defaultValue, onValueChange, ...props }: DatePickerProps): ReactElement;
43
+ export declare function DateRangePicker({ value, defaultValue, onValueChange, ...props }: DateRangePickerProps): ReactElement;
@@ -0,0 +1,142 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ /** Calendarios de fecha civil: la API usa YYYY-MM-DD, sin convertir a UTC ni añadir horas. */
4
+ import { CalendarIcon, ChevronIcon, ChevronUpIcon, ChevronLeftIcon, ChevronRightIcon, } from './internal/icons.js';
5
+ import { useId, useRef, useState } from 'react';
6
+ import * as Popover from '@radix-ui/react-popover';
7
+ import { DayPicker } from 'react-day-picker';
8
+ import { es as spanish, enUS } from 'react-day-picker/locale';
9
+ import { format } from 'date-fns';
10
+ import { useControllable } from './internal/state.js';
11
+ import { restoreFocus } from './internal/focus.js';
12
+ import { preventTriggerLabelDismiss } from './internal/trigger-label.js';
13
+ import { useFieldOpen } from './internal/field-open.js';
14
+ import { OverlayLayerProvider, useOverlayLayer } from './internal/overlay-layer.js';
15
+ import { cx, hasContent } from './utils.js';
16
+ /** Conserva la orientación de DayPicker y unifica sus controles con el resto de iconos. */
17
+ function CalendarChevron({ orientation = 'down', size = 18, className, style, }) {
18
+ const Icon = orientation === 'left'
19
+ ? ChevronLeftIcon
20
+ : orientation === 'right'
21
+ ? ChevronRightIcon
22
+ : orientation === 'up'
23
+ ? ChevronUpIcon
24
+ : ChevronIcon;
25
+ return _jsx(Icon, { size: size, className: className, style: style });
26
+ }
27
+ /** Valida la fecha antes de construirla; evita normalizar 31 de febrero a otro mes. */
28
+ function parseDay(value) {
29
+ if (!value || !/^\d{4}-\d{2}-\d{2}$/.test(value))
30
+ return;
31
+ const [year, month, day] = value.split('-').map(Number);
32
+ const date = new Date(0);
33
+ date.setFullYear(year, month - 1, day);
34
+ date.setHours(12, 0, 0, 0);
35
+ return date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day
36
+ ? date
37
+ : undefined;
38
+ }
39
+ function toDay(date) {
40
+ return format(date, 'yyyy-MM-dd');
41
+ }
42
+ /** Contenedor común: etiqueta, valor de formulario, portal y mensajes. */
43
+ function CalendarField({ props, from, to, range, onSelect, }) {
44
+ const { label, description, error, name, required, disabled, min, max, disabledDates = [], defaultMonth, locale = 'en', placeholder, clearable = true, id, className, wrapperClassName, } = props;
45
+ const [open, setOpen] = useFieldOpen(disabled);
46
+ const { depth, style: layerStyle } = useOverlayLayer();
47
+ const interactedOutside = useRef(false);
48
+ const [invalid, setInvalid] = useState(false);
49
+ const trigger = useRef(null);
50
+ const generatedId = useId();
51
+ const controlId = id ?? `dgm-date-${generatedId}`;
52
+ const isSpanish = locale === 'es';
53
+ const dateLocale = isSpanish ? spanish : enUS;
54
+ const first = parseDay(from);
55
+ const last = parseDay(to);
56
+ const minimum = parseDay(min);
57
+ const maximum = parseDay(max);
58
+ const blocked = [
59
+ ...(minimum ? [{ before: minimum }] : []),
60
+ ...(maximum ? [{ after: maximum }] : []),
61
+ ...disabledDates.flatMap((value) => {
62
+ const day = parseDay(value);
63
+ return day ? [day] : [];
64
+ }),
65
+ ];
66
+ const valid = (date) => Boolean(date &&
67
+ (!minimum || date >= minimum) &&
68
+ (!maximum || date <= maximum) &&
69
+ !disabledDates.includes(toDay(date)));
70
+ const complete = valid(first) &&
71
+ (!range ||
72
+ (valid(last) &&
73
+ last >= first &&
74
+ !disabledDates.some((value) => value >= from && value <= to)));
75
+ const display = first
76
+ ? format(first, 'PP', { locale: dateLocale }) +
77
+ (range
78
+ ? ' — ' + (last ? format(last, 'PP', { locale: dateLocale }) : isSpanish ? 'Fin' : 'End')
79
+ : '')
80
+ : (placeholder ?? (isSpanish ? 'Seleccionar fecha…' : 'Select date…'));
81
+ const message = error ||
82
+ (invalid
83
+ ? isSpanish
84
+ ? 'Selecciona una fecha o intervalo disponible.'
85
+ : 'Select an available date or range.'
86
+ : undefined);
87
+ const describedBy = cx(hasContent(description) && `${controlId}-description`, hasContent(message) && `${controlId}-error`) || undefined;
88
+ function choose(nextFrom, nextTo) {
89
+ if (disabled)
90
+ return;
91
+ onSelect(nextFrom, nextTo);
92
+ setInvalid(false);
93
+ if (!range || nextTo)
94
+ setOpen(false);
95
+ }
96
+ const shared = {
97
+ components: { Chevron: CalendarChevron },
98
+ locale: dateLocale,
99
+ autoFocus: true,
100
+ showOutsideDays: true,
101
+ defaultMonth: first ?? parseDay(defaultMonth) ?? minimum,
102
+ disabled: disabled || blocked,
103
+ startMonth: minimum,
104
+ endMonth: maximum,
105
+ className: 'dgm-calendar',
106
+ };
107
+ return (_jsxs("div", { className: cx('dgm-field', wrapperClassName), children: [_jsxs("label", { className: "dgm-label", htmlFor: controlId, id: `${controlId}-label`, children: [label, required && _jsx("span", { "aria-hidden": "true", children: " *" })] }), _jsx("input", { className: "dgm-form-value", "aria-hidden": "true", tabIndex: -1, name: range && name ? `${name}.from` : name, value: valid(first) ? (from ?? '') : '', required: required || Boolean(from), disabled: disabled, onChange: () => { }, onInvalid: (event) => {
108
+ event.preventDefault();
109
+ setInvalid(true);
110
+ trigger.current?.focus({ preventScroll: true });
111
+ } }), range && (_jsx("input", { className: "dgm-form-value", "aria-hidden": "true", tabIndex: -1, name: name ? `${name}.to` : undefined, value: complete ? (to ?? '') : '', required: required || Boolean(from || to), disabled: disabled, onChange: () => { }, onInvalid: (event) => {
112
+ event.preventDefault();
113
+ setInvalid(true);
114
+ trigger.current?.focus({ preventScroll: true });
115
+ } })), _jsx(OverlayLayerProvider, { depth: depth, children: _jsxs(Popover.Root, { open: open, onOpenChange: setOpen, children: [_jsxs(Popover.Trigger, { ref: trigger, id: controlId, disabled: disabled, className: cx('dgm-input', 'dgm-date-trigger', className), "aria-labelledby": `${controlId}-label ${controlId}-value`, "aria-describedby": describedBy, "aria-invalid": hasContent(message) || undefined, children: [_jsx("span", { id: `${controlId}-value`, children: display }), _jsx(CalendarIcon, {})] }), _jsx(Popover.Portal, { children: _jsx(Popover.Content, { className: "dgm-calendar-panel", style: layerStyle, sideOffset: 6, collisionPadding: 12, "aria-labelledby": `${controlId}-label`, onInteractOutside: (event) => {
116
+ if (preventTriggerLabelDismiss(event, trigger.current))
117
+ return;
118
+ interactedOutside.current = true;
119
+ }, onCloseAutoFocus: (event) => {
120
+ event.preventDefault();
121
+ if (!interactedOutside.current)
122
+ restoreFocus(trigger.current);
123
+ interactedOutside.current = false;
124
+ }, children: range ? (_jsx(DayPicker, { ...shared, mode: "range", excludeDisabled: true, selected: first ? { from: first, to: last } : undefined, onSelect: (selection, day) => {
125
+ // El primer clic inicia un intervalo; el segundo lo completa, incluso en el mismo día.
126
+ if (!first || last)
127
+ choose(toDay(day));
128
+ else
129
+ choose(selection?.from ? toDay(selection.from) : undefined, selection?.to ? toDay(selection.to) : undefined);
130
+ } })) : (_jsx(DayPicker, { ...shared, mode: "single", selected: first, onSelect: (selection) => choose(selection ? toDay(selection) : undefined) })) }) })] }) }), clearable && first && (_jsx("button", { type: "button", className: "dgm-text-action", disabled: disabled, onClick: () => {
131
+ choose();
132
+ trigger.current?.focus({ preventScroll: true });
133
+ }, children: isSpanish ? 'Borrar fecha' : 'Clear date' })), hasContent(description) && (_jsx("span", { className: "dgm-description", id: `${controlId}-description`, children: description })), hasContent(message) && (_jsx("span", { className: "dgm-error", id: `${controlId}-error`, children: message }))] }));
134
+ }
135
+ export function DatePicker({ value, defaultValue = null, onValueChange, ...props }) {
136
+ const [selected, change] = useControllable(value, defaultValue, onValueChange);
137
+ return (_jsx(CalendarField, { props: props, from: selected ?? undefined, range: false, onSelect: (from) => change(from ?? null) }));
138
+ }
139
+ export function DateRangePicker({ value, defaultValue = null, onValueChange, ...props }) {
140
+ const [selected, change] = useControllable(value, defaultValue, onValueChange);
141
+ return (_jsx(CalendarField, { props: props, from: selected?.from, to: selected?.to, range: true, onSelect: (from, to) => change(from ? { from, to } : null) }));
142
+ }
@@ -0,0 +1,63 @@
1
+ import type { HTMLAttributes, ReactNode, ReactElement } from 'react';
2
+ /** Intención visual compartida; el significado también debe expresarse mediante texto. */
3
+ export type Tone = 'neutral' | 'info' | 'success' | 'warning' | 'danger';
4
+ /** Atributos de span con un tono semántico opcional. */
5
+ export interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {
6
+ /**
7
+ * Paleta de la etiqueta.
8
+ * @defaultValue 'neutral'
9
+ */
10
+ tone?: Tone;
11
+ }
12
+ /** Etiqueta informativa. No introduce foco, eventos de botón ni un rol interactivo. */
13
+ export declare function Badge({ tone, className, ...props }: BadgeProps): ReactElement;
14
+ /** Atributos de div compartidos por Card y sus piezas de composición. */
15
+ export interface CardProps extends HTMLAttributes<HTMLDivElement> {
16
+ }
17
+ /** Contenedor visual. El consumidor decide el encabezado y las acciones que incluye. */
18
+ export declare function Card({ className, ...props }: CardProps): ReactElement;
19
+ /** Zona superior de Card; no genera automáticamente ningún h1, h2 o h3. */
20
+ export declare function CardHeader({ className, ...props }: CardProps): ReactElement;
21
+ /** Cuerpo de Card con espaciado coherente cuando sigue a CardHeader. */
22
+ export declare function CardContent({ className, ...props }: CardProps): ReactElement;
23
+ /** Pie con flex-wrap para que acciones y metadatos se adapten a anchos reducidos. */
24
+ export declare function CardFooter({ className, ...props }: CardProps): ReactElement;
25
+ /**
26
+ * Atributos de div. title se redefine como contenido React visible, no como tooltip HTML.
27
+ * Se pueden pasar role="alert" o role="status" cuando el mensaje deba anunciarse.
28
+ */
29
+ export interface AlertProps extends Omit<HTMLAttributes<HTMLDivElement>, 'title'> {
30
+ /** Anima la entrada al montar un aviso dinámico. Desactivado para contenido estático. @defaultValue false */
31
+ animate?: boolean;
32
+ /**
33
+ * Intención visual del aviso.
34
+ * @defaultValue 'info'
35
+ */
36
+ tone?: Exclude<Tone, 'neutral'>;
37
+ /** Encabezado visible opcional, independiente del contenido de children. */
38
+ title?: ReactNode;
39
+ }
40
+ /**
41
+ * Mensaje contextual estático por defecto: no impone un rol de región viva.
42
+ * El consumidor decide si un cambio dinámico es urgente (alert) o informativo (status).
43
+ * El símbolo es decorativo; el título y el contenido deben explicar el mensaje.
44
+ */
45
+ export declare function Alert({ animate, tone, title, children, className, ...props }: AlertProps): ReactElement;
46
+ /** Atributos de hr junto a su orientación y su finalidad semántica. */
47
+ export interface SeparatorProps extends HTMLAttributes<HTMLHRElement> {
48
+ /**
49
+ * Dirección de la línea; en vertical necesita un contenedor flex.
50
+ * @defaultValue 'horizontal'
51
+ */
52
+ orientation?: 'horizontal' | 'vertical';
53
+ /**
54
+ * Excluye la línea del árbol de accesibilidad.
55
+ * @defaultValue false
56
+ */
57
+ decorative?: boolean;
58
+ }
59
+ /**
60
+ * Separación temática nativa o línea decorativa, según decorative.
61
+ * role, aria-hidden y aria-orientation se derivan juntos para no contradecirse.
62
+ */
63
+ export declare function Separator({ orientation, decorative, className, ...props }: SeparatorProps): ReactElement;
@@ -0,0 +1,43 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Componentes de presentación sin hooks ni frontera `use client`.
4
+ * Se pueden componer desde Server Components de Next.js o renderizar con Astro.
5
+ * Los retornos ReactElement explícitos mantienen compatibilidad con los tipos de React 18.
6
+ */
7
+ import { SuccessIcon, InfoIcon, WarningIcon, DangerIcon } from './internal/icons.js';
8
+ import { cx, hasContent } from './utils.js';
9
+ /** Etiqueta informativa. No introduce foco, eventos de botón ni un rol interactivo. */
10
+ export function Badge({ tone = 'neutral', className, ...props }) {
11
+ return _jsx("span", { ...props, className: cx('dgm-badge', `dgm-tone--${tone}`, className) });
12
+ }
13
+ /** Contenedor visual. El consumidor decide el encabezado y las acciones que incluye. */
14
+ export function Card({ className, ...props }) {
15
+ return _jsx("div", { ...props, className: cx('dgm-card', className) });
16
+ }
17
+ /** Zona superior de Card; no genera automáticamente ningún h1, h2 o h3. */
18
+ export function CardHeader({ className, ...props }) {
19
+ return _jsx("div", { ...props, className: cx('dgm-card-header', className) });
20
+ }
21
+ /** Cuerpo de Card con espaciado coherente cuando sigue a CardHeader. */
22
+ export function CardContent({ className, ...props }) {
23
+ return _jsx("div", { ...props, className: cx('dgm-card-content', className) });
24
+ }
25
+ /** Pie con flex-wrap para que acciones y metadatos se adapten a anchos reducidos. */
26
+ export function CardFooter({ className, ...props }) {
27
+ return _jsx("div", { ...props, className: cx('dgm-card-footer', className) });
28
+ }
29
+ /**
30
+ * Mensaje contextual estático por defecto: no impone un rol de región viva.
31
+ * El consumidor decide si un cambio dinámico es urgente (alert) o informativo (status).
32
+ * El símbolo es decorativo; el título y el contenido deben explicar el mensaje.
33
+ */
34
+ export function Alert({ animate = false, tone = 'info', title, children, className, ...props }) {
35
+ return (_jsxs("div", { ...props, className: cx('dgm-alert', `dgm-tone--${tone}`, animate && 'dgm-alert--animated', className), children: [_jsx("span", { className: "dgm-alert-symbol", "aria-hidden": "true", children: tone === 'success' ? (_jsx(SuccessIcon, { size: 20 })) : tone === 'info' ? (_jsx(InfoIcon, { size: 20 })) : tone === 'warning' ? (_jsx(WarningIcon, { size: 20 })) : (_jsx(DangerIcon, { size: 20 })) }), _jsxs("div", { children: [hasContent(title) && _jsx("div", { className: "dgm-alert-title", children: title }), hasContent(children) && _jsx("div", { className: "dgm-alert-content", children: children })] })] }));
36
+ }
37
+ /**
38
+ * Separación temática nativa o línea decorativa, según decorative.
39
+ * role, aria-hidden y aria-orientation se derivan juntos para no contradecirse.
40
+ */
41
+ export function Separator({ orientation = 'horizontal', decorative = false, className, ...props }) {
42
+ return (_jsx("hr", { ...props, role: decorative ? 'none' : 'separator', "aria-hidden": decorative || undefined, "aria-orientation": decorative ? undefined : orientation, className: cx('dgm-separator', `dgm-separator--${orientation}`, className) }));
43
+ }
@@ -0,0 +1,12 @@
1
+ /** Diálogo anclado a un borde. Comparte el contrato de Modal y añade side. */
2
+ import { type ReactElement } from 'react';
3
+ import type { ModalProps } from './modal.js';
4
+ export interface DrawerProps extends ModalProps {
5
+ /** Borde por el que entra y sale. @defaultValue 'right' */
6
+ side?: 'left' | 'right' | 'top' | 'bottom';
7
+ }
8
+ /** Atrapa el foco, bloquea el fondo y mantiene la cabecera y las acciones visibles. */
9
+ export declare function Drawer({ open, onOpenChange, title, description, children, footer, side, closeLabel, closeOnOverlayClick, className, }: DrawerProps): ReactElement;
10
+ /** Alias de Drawer: Sheet comparte comportamiento y propiedades. */
11
+ export declare const Sheet: typeof Drawer;
12
+ export type SheetProps = DrawerProps;
package/dist/drawer.js ADDED
@@ -0,0 +1,29 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ /** Diálogo anclado a un borde. Comparte el contrato de Modal y añade side. */
4
+ import { useId, useRef } from 'react';
5
+ import * as Dialog from '@radix-ui/react-dialog';
6
+ import { restoreFocus } from './internal/focus.js';
7
+ import { CloseIcon } from './internal/icons.js';
8
+ import { OverlayLayerProvider, useOverlayLayer } from './internal/overlay-layer.js';
9
+ import { cx, hasContent } from './utils.js';
10
+ /** Atrapa el foco, bloquea el fondo y mantiene la cabecera y las acciones visibles. */
11
+ export function Drawer({ open, onOpenChange, title, description, children, footer, side = 'right', closeLabel = 'Close drawer', 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-drawer', `dgm-drawer--${side}`, className), "aria-describedby": hasContent(description) ? descriptionId : undefined, onOpenAutoFocus: () => {
16
+ returnFocus.current =
17
+ document.activeElement instanceof HTMLElement ? document.activeElement : null;
18
+ }, onCloseAutoFocus: (event) => {
19
+ if (returnFocus.current?.isConnected) {
20
+ event.preventDefault();
21
+ restoreFocus(returnFocus.current);
22
+ }
23
+ }, onPointerDownOutside: (event) => {
24
+ if (!closeOnOverlayClick)
25
+ event.preventDefault();
26
+ }, 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 })] })] }) }) }));
27
+ }
28
+ /** Alias de Drawer: Sheet comparte comportamiento y propiedades. */
29
+ export const Sheet = Drawer;
@@ -0,0 +1,47 @@
1
+ /** Menú de acciones: Radix gestiona teclado, búsqueda por letras, portal y foco. */
2
+ import { type ReactElement, type ReactNode } from 'react';
3
+ /** Una acción tiene un ID estable; los separadores no son seleccionables. */
4
+ export type DropdownMenuItem = {
5
+ id: string;
6
+ type: 'separator';
7
+ } | {
8
+ id: string;
9
+ type?: 'item';
10
+ /** Texto visible, accesible y usado para buscar escribiendo. */
11
+ label: string;
12
+ /** Icono decorativo opcional. */
13
+ icon?: ReactNode;
14
+ /** Leyenda visual: no registra por sí misma un atajo de teclado. */
15
+ shortcut?: string;
16
+ disabled?: boolean;
17
+ /** Resalta acciones destructivas sin ejecutarlas automáticamente. */
18
+ danger?: boolean;
19
+ /** Se ejecuta con puntero o teclado. preventDefault mantiene abierto el menú. */
20
+ onSelect?: (event: Event) => void;
21
+ };
22
+ export interface DropdownMenuProps {
23
+ /** Un único botón que acepte ref y props; Button es compatible. */
24
+ children: ReactElement;
25
+ /** Acciones y separadores en orden de lectura. IDs únicos. */
26
+ items: readonly DropdownMenuItem[];
27
+ /** Encabezado opcional del menú. */
28
+ label?: string;
29
+ /** Apertura controlada, junto con onOpenChange. */
30
+ open?: boolean;
31
+ /** Apertura inicial en modo no controlado. @defaultValue false */
32
+ defaultOpen?: boolean;
33
+ /** Notifica apertura y cierre, incluido Escape o selección. */
34
+ onOpenChange?: (open: boolean) => void;
35
+ /** Lado preferido; cambia si no hay espacio. @defaultValue 'bottom' */
36
+ side?: 'top' | 'right' | 'bottom' | 'left';
37
+ /** Alineación respecto al botón. @defaultValue 'start' */
38
+ align?: 'start' | 'center' | 'end';
39
+ /** Distancia al botón en píxeles. @defaultValue 6 */
40
+ sideOffset?: number;
41
+ /** Desactiva el disparador. @defaultValue false */
42
+ disabled?: boolean;
43
+ /** Clase del panel que se renderiza en el portal. */
44
+ className?: string;
45
+ }
46
+ /** El menú no bloquea el scroll de la página y devuelve el foco sin desplazarla. */
47
+ export declare function DropdownMenu({ children, items, label, open, defaultOpen, onOpenChange, side, align, sideOffset, disabled, className, }: DropdownMenuProps): ReactElement;
@@ -0,0 +1,24 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ /** Menú de acciones: Radix gestiona teclado, búsqueda por letras, portal y foco. */
4
+ import { useRef } from 'react';
5
+ import * as Primitive from '@radix-ui/react-dropdown-menu';
6
+ import { restoreFocus } from './internal/focus.js';
7
+ import { OverlayLayerProvider, useOverlayLayer } from './internal/overlay-layer.js';
8
+ import { cx } from './utils.js';
9
+ /** El menú no bloquea el scroll de la página y devuelve el foco sin desplazarla. */
10
+ export function DropdownMenu({ children, items, label, open, defaultOpen, onOpenChange, side = 'bottom', align = 'start', sideOffset = 6, disabled, className, }) {
11
+ const { depth, style: layerStyle } = useOverlayLayer();
12
+ const trigger = useRef(null);
13
+ const interactedOutside = useRef(false);
14
+ return (_jsx(OverlayLayerProvider, { depth: depth, children: _jsxs(Primitive.Root, { open: open, defaultOpen: defaultOpen, onOpenChange: onOpenChange, modal: false, 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, className: cx('dgm-dropdown', className), onInteractOutside: () => {
15
+ interactedOutside.current = true;
16
+ }, onCloseAutoFocus: (event) => {
17
+ // Un clic fuera conserva su destino; Escape o una acción vuelven al botón.
18
+ event.preventDefault();
19
+ if (!interactedOutside.current && trigger.current?.isConnected) {
20
+ restoreFocus(trigger.current);
21
+ }
22
+ interactedOutside.current = false;
23
+ }, children: [label && _jsx(Primitive.Label, { className: "dgm-dropdown-label", children: label }), items.map((item) => item.type === 'separator' ? (_jsx(Primitive.Separator, { className: "dgm-dropdown-separator" }, item.id)) : (_jsxs(Primitive.Item, { textValue: item.label, disabled: item.disabled, onSelect: item.onSelect, className: cx('dgm-dropdown-item', item.danger && 'dgm-dropdown-item--danger'), children: [item.icon && (_jsx("span", { className: "dgm-option-icon", "aria-hidden": "true", children: item.icon })), _jsx("span", { className: "dgm-dropdown-text", children: item.label }), item.shortcut && (_jsx("span", { className: "dgm-dropdown-shortcut", "aria-hidden": "true", children: item.shortcut }))] }, item.id)))] }) })] }) }));
24
+ }
@@ -0,0 +1,13 @@
1
+ /** Presentación estática compatible con Server Components; la aplicación aporta las acciones. */
2
+ import type { HTMLAttributes, ReactElement, ReactNode } from 'react';
3
+ export interface EmptyStateProps extends Omit<HTMLAttributes<HTMLDivElement>, 'title'> {
4
+ title: ReactNode;
5
+ description?: ReactNode;
6
+ /** Icono decorativo; lupa por defecto. Pasa null para ocultarlo. */
7
+ icon?: ReactNode;
8
+ action?: ReactNode;
9
+ /** Escoge el nivel que corresponda a la estructura de la página. */
10
+ headingLevel?: 2 | 3 | 4;
11
+ }
12
+ /** No impone un live region: utiliza role="status" solo si necesitas anunciar cambios dinámicos. */
13
+ export declare function EmptyState({ title, description, icon, action, headingLevel, className, children, ...props }: EmptyStateProps): ReactElement;
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cx } from './utils.js';
3
+ import { SearchIcon } from './internal/icons.js';
4
+ /** No impone un live region: utiliza role="status" solo si necesitas anunciar cambios dinámicos. */
5
+ export function EmptyState({ title, description, icon = _jsx(SearchIcon, { size: 24 }), action, headingLevel = 3, className, children, ...props }) {
6
+ const Heading = `h${headingLevel}`;
7
+ return (_jsxs("div", { ...props, className: cx('dgm-empty-state', className), children: [icon && (_jsx("div", { className: "dgm-empty-icon", "aria-hidden": "true", children: icon })), _jsx(Heading, { children: title }), description && _jsx("p", { className: "dgm-description", children: description }), children, action && _jsx("div", { className: "dgm-empty-action", children: action })] }));
8
+ }