danielgm-ui 0.4.0 → 0.4.2

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.
@@ -10,7 +10,8 @@ export interface DataColumn<T extends object> {
10
10
  sortable?: boolean;
11
11
  /** Participa en el buscador global. @defaultValue true */
12
12
  searchable?: boolean;
13
- align?: 'start' | 'end';
13
+ /** Alineación compartida por cabecera y celdas. @defaultValue 'start' */
14
+ align?: 'start' | 'center' | 'end';
14
15
  /** Ancho estable CSS (px si es número); las columnas sin ancho reparten el espacio restante. */
15
16
  width?: CSSProperties['width'];
16
17
  }
@@ -27,7 +28,12 @@ export interface DataTableProps<T extends object> {
27
28
  caption: string;
28
29
  /** Orden controlado. Usa [] para quitarlo. */
29
30
  sorting?: DataTableSort[];
31
+ /** Orden base: conserva aria-sort pero no muestra flecha. */
30
32
  defaultSorting?: DataTableSort[];
33
+ /** Habilita las cabeceras ordenables; cada columna puede desactivarlas. @defaultValue true */
34
+ sortable?: boolean;
35
+ /** Delega la ordenación al consumidor sin reordenar data. @defaultValue false */
36
+ manualSorting?: boolean;
31
37
  onSortingChange?: (sorting: DataTableSort[]) => void;
32
38
  /** Buscador de todas las columnas habilitadas. @defaultValue true */
33
39
  searchable?: boolean;
@@ -50,4 +56,4 @@ export interface DataTableProps<T extends object> {
50
56
  className?: string;
51
57
  }
52
58
  /** 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;
59
+ export declare function DataTable<T extends object>({ data, columns, getRowId, caption, sorting, sortable, manualSorting, defaultSorting, onSortingChange, searchable, searchPlaceholder, pageSize, page, onPageChange, selectable, selection, defaultSelection, onSelectionChange, loading, emptyMessage, locale, className, }: DataTableProps<T>): ReactElement;
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  /** Tabla HTML con operaciones locales. TanStack calcula filas filtradas y ordenadas sin mutar data. */
4
- import { ArrowUpIcon, ArrowDownIcon, SortIcon } from './internal/icons.js';
4
+ import { ArrowUpIcon, ArrowDownIcon } from './internal/icons.js';
5
5
  import { useEffect, useMemo, useRef, useState, } from 'react';
6
6
  import { useTable, tableFeatures, rowSortingFeature, columnFilteringFeature, globalFilteringFeature, createSortedRowModel, createFilteredRowModel, filterFn_includesString, functionalUpdate, } from '@tanstack/react-table';
7
7
  import { Button } from './button.js';
@@ -20,7 +20,7 @@ const features = tableFeatures({
20
20
  filterFns: { includesString: filterFn_includesString },
21
21
  });
22
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, }) {
23
+ export function DataTable({ data, columns, getRowId, caption, sorting, sortable = true, manualSorting = false, defaultSorting = [], onSortingChange, searchable = true, searchPlaceholder, pageSize = 10, page, onPageChange, selectable = false, selection, defaultSelection = [], onSelectionChange, loading = false, emptyMessage, locale = 'en', className, }) {
24
24
  const [sort, changeSort] = useControllable(sorting, defaultSorting, onSortingChange);
25
25
  const [currentPage, changePage] = useControllable(page, 1, onPageChange);
26
26
  const [selected, changeSelection] = useControllable(selection, defaultSelection, onSelectionChange ? (next) => onSelectionChange([...next]) : undefined);
@@ -55,6 +55,7 @@ export function DataTable({ data, columns, getRowId, caption, sorting, defaultSo
55
55
  changeSort(functionalUpdate(updater, sort));
56
56
  changePage(1);
57
57
  },
58
+ manualSorting,
58
59
  globalFilterFn: 'includesString',
59
60
  enableMultiSort: false,
60
61
  });
@@ -89,11 +90,27 @@ export function DataTable({ data, columns, getRowId, caption, sorting, defaultSo
89
90
  } }) })), columns.map((column) => {
90
91
  const instance = table.getColumn(column.id);
91
92
  const direction = instance.getIsSorted();
92
- return (_jsx("th", { scope: "col", style: { textAlign: column.align ?? 'start' }, "aria-sort": direction === 'asc'
93
+ const baseline = defaultSorting.find((item) => item.id === column.id);
94
+ const showArrow = Boolean(direction && !(baseline && baseline.desc === (direction === 'desc')));
95
+ return (_jsx("th", { scope: "col", "data-align": column.align ?? 'start', style: { textAlign: column.align ?? 'start' }, "aria-sort": direction === 'asc'
93
96
  ? 'ascending'
94
97
  : direction === 'desc'
95
98
  ? '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));
99
+ : undefined, children: sortable && instance.getCanSort() ? (_jsxs(Button, { variant: "ghost", size: "sm", className: "dgm-table-sort", "data-sorted": showArrow || undefined, disabled: loading, onClick: () => {
100
+ // El orden base alterna siempre con su inverso, también en números.
101
+ if (baseline) {
102
+ changeSort([
103
+ {
104
+ id: column.id,
105
+ desc: direction ? direction !== 'desc' : !baseline.desc,
106
+ },
107
+ ]);
108
+ changePage(1);
109
+ }
110
+ else {
111
+ instance.toggleSorting();
112
+ }
113
+ }, children: [_jsx("span", { className: "dgm-table-sort-label", children: column.header }), _jsx("span", { className: "dgm-table-sort-icon", "aria-hidden": "true", children: direction === 'desc' ? _jsx(ArrowDownIcon, {}) : _jsx(ArrowUpIcon, {}) })] })) : (column.header) }, column.id));
97
114
  })] }) }), _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
115
  ? [...new Set([...selected, row.id])]
99
116
  : 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
package/dist/slider.d.ts CHANGED
@@ -10,6 +10,8 @@ export interface SliderBaseProps {
10
10
  step?: number;
11
11
  disabled?: boolean;
12
12
  name?: string;
13
+ /** Visibilidad visual de los extremos en Slider y RangeSlider; conserva foco y accesibilidad. @defaultValue 'always' */
14
+ thumbVisibility?: 'always' | 'interaction';
13
15
  /** Valor visible y aria-valuetext; útil para porcentajes, unidades o precios. */
14
16
  formatValue?: (value: number) => string;
15
17
  /** Marcas informativas, sin interacción propia. Deben estar entre min y max. */
package/dist/slider.js CHANGED
@@ -1,19 +1,27 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  /** Radix mantiene teclado, gestos táctiles y valores de formulario; la librería aporta etiquetas y estilo. */
4
- import { useId } from 'react';
4
+ import { useId, useState, useEffect } from 'react';
5
5
  import * as Primitive from '@radix-ui/react-slider';
6
6
  import { useControllable } from './internal/state.js';
7
7
  import { cx, hasContent } from './utils.js';
8
8
  /** Infraestructura interna; cada API pública mantiene un número fijo de controles. */
9
- function SliderControl({ label, description, error, min = 0, max = 100, step = 1, disabled, name, formatValue = String, marks = [], className, id, values, change, commit, thumbLabels, }) {
9
+ function SliderControl({ label, description, error, min = 0, max = 100, step = 1, disabled, name, thumbVisibility = 'always', formatValue = String, marks = [], className, id, values, change, commit, thumbLabels, }) {
10
10
  const generated = useId();
11
+ const [activePointer, setActivePointer] = useState(null);
12
+ useEffect(() => {
13
+ if (disabled)
14
+ setActivePointer(null);
15
+ }, [disabled]);
11
16
  const controlId = id ?? `dgm-slider-${generated}`;
12
17
  if (![min, max, step].every(Number.isFinite) || min >= max || step <= 0) {
13
18
  throw new RangeError('Slider requires finite min < max and step > 0.');
14
19
  }
15
20
  const describedBy = cx(hasContent(description) && `${controlId}-description`, hasContent(error) && `${controlId}-error`) || undefined;
16
- return (_jsxs("div", { className: cx('dgm-field', 'dgm-slider-field', className), id: controlId, children: [_jsxs("div", { className: "dgm-slider-heading", children: [_jsx("span", { className: "dgm-label", id: `${controlId}-label`, children: label }), _jsx("span", { className: "dgm-description", "aria-hidden": "true", children: values.map(formatValue).join(' – ') })] }), _jsxs(Primitive.Root, { className: "dgm-slider", value: values, onValueChange: change, onValueCommit: commit, min: min, max: max, step: step, disabled: disabled, name: name, children: [_jsx(Primitive.Track, { className: "dgm-slider-track", children: _jsx(Primitive.Range, { className: "dgm-slider-range" }) }), values.map((value, index) => (_jsx(Primitive.Thumb, { name: name, className: "dgm-slider-thumb", "aria-labelledby": `${controlId}-label${thumbLabels ? ` ${controlId}-thumb-${index}` : ''}`, "aria-describedby": describedBy, "aria-invalid": hasContent(error) || undefined, "aria-valuetext": formatValue(value) }, index)))] }), thumbLabels?.map((text, index) => (_jsx("span", { id: `${controlId}-thumb-${index}`, className: "dgm-visually-hidden", children: text }, index))), marks.length > 0 && (_jsx("div", { className: "dgm-slider-marks", "aria-hidden": "true", children: marks
21
+ return (_jsxs("div", { className: cx('dgm-field', 'dgm-slider-field', className), id: controlId, children: [_jsxs("div", { className: "dgm-slider-heading", children: [_jsx("span", { className: "dgm-label", id: `${controlId}-label`, children: label }), _jsx("span", { className: "dgm-description", "aria-hidden": "true", children: values.map(formatValue).join(' – ') })] }), _jsxs(Primitive.Root, { className: "dgm-slider", "data-thumb-visibility": thumbVisibility, "data-dragging": !disabled && activePointer !== null ? '' : undefined, onPointerDownCapture: (event) => {
22
+ if (!disabled && event.button === 0)
23
+ setActivePointer(event.pointerId);
24
+ }, onPointerUpCapture: () => setActivePointer(null), onPointerCancelCapture: () => setActivePointer(null), onLostPointerCapture: () => setActivePointer(null), value: values, onValueChange: change, onValueCommit: commit, min: min, max: max, step: step, disabled: disabled, name: name, children: [_jsx(Primitive.Track, { className: "dgm-slider-track", children: _jsx(Primitive.Range, { className: "dgm-slider-range" }) }), values.map((value, index) => (_jsx(Primitive.Thumb, { name: name, className: "dgm-slider-thumb", "aria-labelledby": `${controlId}-label${thumbLabels ? ` ${controlId}-thumb-${index}` : ''}`, "aria-describedby": describedBy, "aria-invalid": hasContent(error) || undefined, "aria-valuetext": formatValue(value) }, index)))] }), thumbLabels?.map((text, index) => (_jsx("span", { id: `${controlId}-thumb-${index}`, className: "dgm-visually-hidden", children: text }, index))), marks.length > 0 && (_jsx("div", { className: "dgm-slider-marks", "aria-hidden": "true", children: marks
17
25
  .filter((mark) => mark.value >= min && mark.value <= max)
18
26
  .map((mark) => (_jsx("span", { style: { left: `${(100 * (mark.value - min)) / (max - min)}%` }, children: mark.label ?? formatValue(mark.value) }, mark.value))) })), hasContent(description) && (_jsx("span", { id: `${controlId}-description`, className: "dgm-description", children: description })), hasContent(error) && (_jsx("span", { id: `${controlId}-error`, className: "dgm-error", children: error }))] }));
19
27
  }
package/dist/styles.css CHANGED
@@ -2759,8 +2759,54 @@ button.dgm-step:hover:not(:disabled),
2759
2759
  font-weight: var(--dgm-font-weight-semibold);
2760
2760
  background: var(--dgm-subtle);
2761
2761
  }
2762
- .dgm-data-table th .dgm-button {
2763
- padding-inline: 0;
2762
+ .dgm-data-table th .dgm-table-sort {
2763
+ --dgm-table-sort-space: 1.125rem;
2764
+ display: inline-block;
2765
+ position: relative;
2766
+ max-width: 100%;
2767
+ padding-inline: var(--dgm-table-sort-space);
2768
+ text-align: inherit;
2769
+ white-space: normal;
2770
+ }
2771
+ .dgm-table-sort-label {
2772
+ position: relative;
2773
+ display: inline-block;
2774
+ transition: transform 240ms ease;
2775
+ }
2776
+ .dgm-table-sort-icon {
2777
+ position: absolute;
2778
+ inset-inline-end: 0.5625rem;
2779
+ top: 50%;
2780
+ width: 0.875rem;
2781
+ height: 0.875rem;
2782
+ opacity: 0;
2783
+ transform: translateY(-50%) scale(0.5);
2784
+ transition:
2785
+ opacity 240ms ease,
2786
+ transform 240ms ease;
2787
+ }
2788
+ .dgm-table-sort-icon svg {
2789
+ width: 100%;
2790
+ height: 100%;
2791
+ }
2792
+ .dgm-table-sort[data-sorted] .dgm-table-sort-icon {
2793
+ opacity: 1;
2794
+ transform: translateY(-50%) scale(1);
2795
+ }
2796
+ .dgm-data-table th .dgm-table-sort[data-sorted] .dgm-table-sort-label {
2797
+ transform: translateX(-0.5625rem);
2798
+ }
2799
+ .dgm-data-table th[data-align='start'] .dgm-table-sort {
2800
+ margin-inline-start: calc(-1 * var(--dgm-table-sort-space));
2801
+ }
2802
+ [dir='rtl'] .dgm-data-table th .dgm-table-sort[data-sorted] .dgm-table-sort-label {
2803
+ transform: translateX(0.5625rem);
2804
+ }
2805
+ @media (prefers-reduced-motion: reduce) {
2806
+ .dgm-table-sort-label,
2807
+ .dgm-table-sort-icon {
2808
+ transition: none;
2809
+ }
2764
2810
  }
2765
2811
  .dgm-data-table tr[data-selected] {
2766
2812
  background: var(--dgm-subtle);
@@ -3084,6 +3130,22 @@ button.dgm-step:disabled {
3084
3130
  box-shadow: 0 2px 5px #0002;
3085
3131
  cursor: grab;
3086
3132
  }
3133
+ /* Opacidad conserva la geometría, el área táctil y los controles accesibles. */
3134
+ .dgm-slider[data-thumb-visibility='interaction'] .dgm-slider-thumb {
3135
+ opacity: 0;
3136
+ transition: opacity 0.15s ease;
3137
+ }
3138
+ .dgm-slider[data-thumb-visibility='interaction']:not([data-disabled]):focus-within
3139
+ .dgm-slider-thumb,
3140
+ .dgm-slider[data-thumb-visibility='interaction']:not([data-disabled])[data-dragging]
3141
+ .dgm-slider-thumb {
3142
+ opacity: 1;
3143
+ }
3144
+ @media (hover: hover) {
3145
+ .dgm-slider[data-thumb-visibility='interaction']:not([data-disabled]):hover .dgm-slider-thumb {
3146
+ opacity: 1;
3147
+ }
3148
+ }
3087
3149
  .dgm-slider-thumb::after {
3088
3150
  content: '';
3089
3151
  position: absolute;
@@ -3529,7 +3591,8 @@ button.dgm-step:disabled {
3529
3591
  .dgm-navigation-content {
3530
3592
  animation: none;
3531
3593
  }
3532
- .dgm-toggle-item {
3594
+ .dgm-toggle-item,
3595
+ .dgm-slider[data-thumb-visibility='interaction'] .dgm-slider-thumb {
3533
3596
  transition: none;
3534
3597
  }
3535
3598
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "danielgm-ui",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Accessible React components for Next.js and the web.",
5
5
  "repository": {
6
6
  "type": "git",