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.
- package/LICENSE +21 -0
- package/README.md +372 -0
- package/TABLER-LICENSE.txt +21 -0
- package/dist/accordion.d.ts +49 -0
- package/dist/accordion.js +14 -0
- package/dist/aspect-ratio.d.ts +8 -0
- package/dist/aspect-ratio.js +8 -0
- package/dist/avatar.d.ts +27 -0
- package/dist/avatar.js +15 -0
- package/dist/breadcrumbs.d.ts +20 -0
- package/dist/breadcrumbs.js +5 -0
- package/dist/button.d.ts +44 -0
- package/dist/button.js +25 -0
- package/dist/carousel.d.ts +19 -0
- package/dist/carousel.js +133 -0
- package/dist/choice-group.d.ts +36 -0
- package/dist/choice-group.js +27 -0
- package/dist/combobox.d.ts +37 -0
- package/dist/combobox.js +57 -0
- package/dist/command-palette.d.ts +28 -0
- package/dist/command-palette.js +90 -0
- package/dist/container.d.ts +21 -0
- package/dist/container.js +10 -0
- package/dist/context-menu.d.ts +21 -0
- package/dist/context-menu.js +50 -0
- package/dist/custom-select.d.ts +45 -0
- package/dist/custom-select.js +26 -0
- package/dist/data-table.d.ts +53 -0
- package/dist/data-table.js +104 -0
- package/dist/date-picker.d.ts +43 -0
- package/dist/date-picker.js +142 -0
- package/dist/display.d.ts +63 -0
- package/dist/display.js +43 -0
- package/dist/drawer.d.ts +12 -0
- package/dist/drawer.js +29 -0
- package/dist/dropdown-menu.d.ts +47 -0
- package/dist/dropdown-menu.js +24 -0
- package/dist/empty-state.d.ts +13 -0
- package/dist/empty-state.js +8 -0
- package/dist/fields.d.ts +62 -0
- package/dist/fields.js +70 -0
- package/dist/file-upload.d.ts +33 -0
- package/dist/file-upload.js +111 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.js +48 -0
- package/dist/internal/field-open.d.ts +2 -0
- package/dist/internal/field-open.js +15 -0
- package/dist/internal/focus.d.ts +4 -0
- package/dist/internal/focus.js +27 -0
- package/dist/internal/icons.d.ts +29 -0
- package/dist/internal/icons.js +32 -0
- package/dist/internal/navigation-origin.d.ts +2 -0
- package/dist/internal/navigation-origin.js +23 -0
- package/dist/internal/overlay-layer.d.ts +12 -0
- package/dist/internal/overlay-layer.js +14 -0
- package/dist/internal/pagination.d.ts +6 -0
- package/dist/internal/pagination.js +36 -0
- package/dist/internal/scroll-hints.d.ts +8 -0
- package/dist/internal/scroll-hints.js +108 -0
- package/dist/internal/state.d.ts +2 -0
- package/dist/internal/state.js +13 -0
- package/dist/internal/trigger-label.d.ts +5 -0
- package/dist/internal/trigger-label.js +26 -0
- package/dist/loader.d.ts +27 -0
- package/dist/loader.js +13 -0
- package/dist/modal.d.ts +23 -0
- package/dist/modal.js +30 -0
- package/dist/multi-select.d.ts +23 -0
- package/dist/multi-select.js +63 -0
- package/dist/navigation-menu.d.ts +26 -0
- package/dist/navigation-menu.js +27 -0
- package/dist/number-input.d.ts +14 -0
- package/dist/number-input.js +37 -0
- package/dist/pagination.d.ts +24 -0
- package/dist/pagination.js +60 -0
- package/dist/popover.d.ts +30 -0
- package/dist/popover.js +27 -0
- package/dist/progress.d.ts +18 -0
- package/dist/progress.js +9 -0
- package/dist/radio-group.d.ts +46 -0
- package/dist/radio-group.js +16 -0
- package/dist/resizable-panels.d.ts +22 -0
- package/dist/resizable-panels.js +70 -0
- package/dist/skeleton.d.ts +17 -0
- package/dist/skeleton.js +9 -0
- package/dist/slider.d.ts +41 -0
- package/dist/slider.js +29 -0
- package/dist/stepper.d.ts +25 -0
- package/dist/stepper.js +25 -0
- package/dist/styles.css +3369 -0
- package/dist/tabs.d.ts +32 -0
- package/dist/tabs.js +8 -0
- package/dist/toast.d.ts +54 -0
- package/dist/toast.js +107 -0
- package/dist/toggle-group.d.ts +32 -0
- package/dist/toggle-group.js +16 -0
- package/dist/toggles.d.ts +33 -0
- package/dist/toggles.js +29 -0
- package/dist/tooltip.d.ts +26 -0
- package/dist/tooltip.js +16 -0
- package/dist/transition.d.ts +27 -0
- package/dist/transition.js +148 -0
- package/dist/tree-view.d.ts +22 -0
- package/dist/tree-view.js +128 -0
- package/dist/typography.d.ts +23 -0
- package/dist/typography.js +20 -0
- package/dist/utils.d.ts +13 -0
- package/dist/utils.js +17 -0
- package/package.json +73 -0
package/dist/button.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* Botón de acción de la librería.
|
|
5
|
+
* La frontera cliente se declara en este módulo, no en el barrel público.
|
|
6
|
+
* forwardRef permite entregar el botón real tanto en React 18 como en React 19.
|
|
7
|
+
*/
|
|
8
|
+
import { forwardRef } from 'react';
|
|
9
|
+
import { cx } from './utils.js';
|
|
10
|
+
/**
|
|
11
|
+
* Acción nativa con foco de teclado, soporte de formularios y ref al HTMLButtonElement.
|
|
12
|
+
* Se usa type="button" por defecto para evitar envíos accidentales de formularios.
|
|
13
|
+
* El consumidor puede indicar type="submit" y pasar onClick u otros eventos nativos.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* <Button type="submit" loading={saving}>Guardar</Button>
|
|
17
|
+
*/
|
|
18
|
+
export const Button = forwardRef(function Button({ variant = 'primary', size = 'md', loading = false, fullWidth = false, iconOnly = false, disabled, type = 'button', className, children, ...props }, ref) {
|
|
19
|
+
// Las props calculadas se aplican después de ...props: el estado de carga
|
|
20
|
+
// siempre bloquea la interacción. El nombre se conserva en el texto o los atributos ARIA.
|
|
21
|
+
return (_jsxs("button", { ...props, ref: ref, type: type, disabled: disabled || loading, "aria-busy": loading || undefined, className: cx('dgm-button', `dgm-button--${variant}`, `dgm-button--${size}`, fullWidth && !iconOnly && 'dgm-full', iconOnly && 'dgm-button--icon-only', className), children: [loading && _jsx("span", { className: "dgm-spinner", "aria-hidden": "true" }), iconOnly
|
|
22
|
+
? // El nombre lo proporciona el botón: ni el dibujo ni el spinner se anuncian.
|
|
23
|
+
!loading && (_jsx("span", { className: "dgm-button-icon", "aria-hidden": "true", children: children }))
|
|
24
|
+
: children] }));
|
|
25
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Carrusel de una diapositiva por vista. Scroll-snap nativo conserva gestos y rueda sin capturar el gesto vertical. */
|
|
2
|
+
import { type ReactElement, type ReactNode } from 'react';
|
|
3
|
+
export interface CarouselItem {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
content: ReactNode;
|
|
7
|
+
}
|
|
8
|
+
export interface CarouselProps {
|
|
9
|
+
label: string;
|
|
10
|
+
items: readonly CarouselItem[];
|
|
11
|
+
defaultIndex?: number;
|
|
12
|
+
onIndexChange?: (index: number) => void;
|
|
13
|
+
locale?: 'es' | 'en';
|
|
14
|
+
/** Muestra accesos directos a cada diapositiva. @defaultValue true */
|
|
15
|
+
showIndicators?: boolean;
|
|
16
|
+
className?: string;
|
|
17
|
+
}
|
|
18
|
+
/** Sin autoplay: el movimiento siempre lo inicia la persona. Respeta prefers-reduced-motion. */
|
|
19
|
+
export declare function Carousel({ label, items, defaultIndex, onIndexChange, locale, showIndicators, className, }: CarouselProps): ReactElement;
|
package/dist/carousel.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/** Carrusel de una diapositiva por vista. Scroll-snap nativo conserva gestos y rueda sin capturar el gesto vertical. */
|
|
4
|
+
import { useEffect, useId, useRef, useState } from 'react';
|
|
5
|
+
import { Button } from './button.js';
|
|
6
|
+
import { cx } from './utils.js';
|
|
7
|
+
/** Distancia real entre diapositivas: incluye el gap CSS sin duplicar su medida en JavaScript.
|
|
8
|
+
* El viewport es el contenedor posicionado, por lo que todos los offsets comparten origen.
|
|
9
|
+
*/
|
|
10
|
+
function slideOffset(viewport, index) {
|
|
11
|
+
const first = viewport.children[0];
|
|
12
|
+
const slide = viewport.children[index];
|
|
13
|
+
return first && slide ? slide.offsetLeft - first.offsetLeft : 0;
|
|
14
|
+
}
|
|
15
|
+
/** Sin autoplay: el movimiento siempre lo inicia la persona. Respeta prefers-reduced-motion. */
|
|
16
|
+
export function Carousel({ label, items, defaultIndex = 0, onIndexChange, locale = 'en', showIndicators = true, className, }) {
|
|
17
|
+
const initial = Math.max(0, Math.min(items.length - 1, defaultIndex));
|
|
18
|
+
const [index, setIndex] = useState(initial);
|
|
19
|
+
const active = Math.max(0, Math.min(items.length - 1, index));
|
|
20
|
+
const viewport = useRef(null);
|
|
21
|
+
const indexRef = useRef(initial);
|
|
22
|
+
// Mientras los controles desplazan el visor, el índice representa su destino.
|
|
23
|
+
// Los eventos scroll intermedios no deben volver a anunciar las tarjetas anteriores.
|
|
24
|
+
const target = useRef(null);
|
|
25
|
+
const settleTimer = useRef(undefined);
|
|
26
|
+
const callback = useRef(onIndexChange);
|
|
27
|
+
callback.current = onIndexChange;
|
|
28
|
+
const id = useId();
|
|
29
|
+
const es = locale === 'es';
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
const node = viewport.current;
|
|
32
|
+
if (!node)
|
|
33
|
+
return;
|
|
34
|
+
function resize() {
|
|
35
|
+
if (node) {
|
|
36
|
+
target.current = null;
|
|
37
|
+
node.scrollLeft = slideOffset(node, Math.max(0, Math.min(items.length - 1, indexRef.current)));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
resize();
|
|
41
|
+
const observer = new ResizeObserver(resize);
|
|
42
|
+
observer.observe(node);
|
|
43
|
+
// scrollend confirma el destino; el temporizador de scroll cubre navegadores sin ese evento.
|
|
44
|
+
const settle = () => {
|
|
45
|
+
target.current = null;
|
|
46
|
+
syncPosition(node);
|
|
47
|
+
};
|
|
48
|
+
node.addEventListener('scrollend', settle);
|
|
49
|
+
return () => {
|
|
50
|
+
observer.disconnect();
|
|
51
|
+
node.removeEventListener('scrollend', settle);
|
|
52
|
+
clearTimeout(settleTimer.current);
|
|
53
|
+
};
|
|
54
|
+
}, [items.length]);
|
|
55
|
+
function update(next) {
|
|
56
|
+
if (next !== indexRef.current) {
|
|
57
|
+
indexRef.current = next;
|
|
58
|
+
setIndex(next);
|
|
59
|
+
callback.current?.(next);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function go(next) {
|
|
63
|
+
const node = viewport.current;
|
|
64
|
+
if (!node || !items.length)
|
|
65
|
+
return;
|
|
66
|
+
const bounded = Math.max(0, Math.min(items.length - 1, next));
|
|
67
|
+
clearTimeout(settleTimer.current);
|
|
68
|
+
target.current = bounded;
|
|
69
|
+
update(bounded);
|
|
70
|
+
node.scrollTo({
|
|
71
|
+
left: slideOffset(node, bounded),
|
|
72
|
+
behavior: window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
73
|
+
? 'instant'
|
|
74
|
+
: 'smooth',
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
/** Un gesto del usuario puede interrumpir el desplazamiento programado. */
|
|
78
|
+
function interrupt() {
|
|
79
|
+
target.current = null;
|
|
80
|
+
clearTimeout(settleTimer.current);
|
|
81
|
+
}
|
|
82
|
+
function syncPosition(node) {
|
|
83
|
+
// scrollend puede ejecutarse entre el commit de nuevas tarjetas y la limpieza
|
|
84
|
+
// del efecto anterior. La lista real evita leer offsets de nodos retirados.
|
|
85
|
+
const slides = node.children;
|
|
86
|
+
const count = slides.length;
|
|
87
|
+
if (!node.clientWidth || !count)
|
|
88
|
+
return;
|
|
89
|
+
let nearest = 0;
|
|
90
|
+
let distance = Infinity;
|
|
91
|
+
// Una sola lectura del origen y del scroll por evento, sin conservar geometría
|
|
92
|
+
// que pueda quedar obsoleta al redimensionar, cambiar el gap o cargar contenido.
|
|
93
|
+
const origin = slides[0].offsetLeft;
|
|
94
|
+
const scrollLeft = node.scrollLeft;
|
|
95
|
+
for (let position = 0; position < count; position++) {
|
|
96
|
+
const candidate = Math.abs(scrollLeft - (slides[position].offsetLeft - origin));
|
|
97
|
+
if (candidate < distance) {
|
|
98
|
+
nearest = position;
|
|
99
|
+
distance = candidate;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
update(nearest);
|
|
103
|
+
}
|
|
104
|
+
return (_jsxs("section", { "aria-label": label, "aria-roledescription": es ? 'carrusel' : 'carousel', className: cx('dgm-carousel', className), children: [_jsx("div", { id: id, ref: viewport, className: "dgm-carousel-viewport", dir: "ltr", tabIndex: items.length ? 0 : -1, "aria-label": es ? 'Diapositivas' : 'Slides', onPointerDown: interrupt, onTouchStart: interrupt, onWheel: interrupt, onScroll: (event) => {
|
|
105
|
+
const node = event.currentTarget;
|
|
106
|
+
if (target.current === null)
|
|
107
|
+
syncPosition(node);
|
|
108
|
+
else if (Math.abs(node.scrollLeft - slideOffset(node, target.current)) < 1) {
|
|
109
|
+
target.current = null;
|
|
110
|
+
}
|
|
111
|
+
clearTimeout(settleTimer.current);
|
|
112
|
+
settleTimer.current = setTimeout(() => {
|
|
113
|
+
target.current = null;
|
|
114
|
+
syncPosition(node);
|
|
115
|
+
}, 160);
|
|
116
|
+
}, onKeyDown: (event) => {
|
|
117
|
+
if (event.target !== event.currentTarget)
|
|
118
|
+
return;
|
|
119
|
+
if (event.key === 'ArrowRight')
|
|
120
|
+
go(active + 1);
|
|
121
|
+
else if (event.key === 'ArrowLeft')
|
|
122
|
+
go(active - 1);
|
|
123
|
+
else if (event.key === 'Home')
|
|
124
|
+
go(0);
|
|
125
|
+
else if (event.key === 'End')
|
|
126
|
+
go(items.length - 1);
|
|
127
|
+
else
|
|
128
|
+
return;
|
|
129
|
+
event.preventDefault();
|
|
130
|
+
}, children: items.map((item, position) => (_jsx("div", { role: "group", "aria-roledescription": es ? 'diapositiva' : 'slide', "aria-label": `${position + 1} / ${items.length}: ${item.label}`, "aria-hidden": active !== position || undefined, ref: (element) => {
|
|
131
|
+
element?.toggleAttribute('inert', active !== position);
|
|
132
|
+
}, className: "dgm-carousel-slide", children: item.content }, item.id))) }), _jsxs("div", { className: "dgm-carousel-controls", children: [_jsx(Button, { variant: "outline", disabled: !items.length || active === 0, "aria-controls": id, onClick: () => go(active - 1), children: es ? 'Anterior' : 'Previous' }), _jsxs("span", { className: "dgm-description", role: "status", "aria-atomic": "true", children: [items.length ? active + 1 : 0, " / ", items.length] }), _jsx(Button, { variant: "outline", disabled: !items.length || active >= items.length - 1, "aria-controls": id, onClick: () => go(active + 1), children: es ? 'Siguiente' : 'Next' })] }), showIndicators && (_jsx("div", { className: "dgm-carousel-indicators", children: items.map((item, position) => (_jsx("button", { type: "button", "aria-label": `${es ? 'Ir a' : 'Go to'} ${item.label}`, "aria-current": active === position ? 'true' : undefined, "aria-controls": id, onClick: () => go(position), children: _jsx("span", {}) }, item.id))) }))] }));
|
|
133
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Familia de selección múltiple basada en Checkbox y Switch, con un fieldset común. */
|
|
2
|
+
import { type ReactElement, type ReactNode } from 'react';
|
|
3
|
+
export interface ChoiceGroupOption {
|
|
4
|
+
value: string;
|
|
5
|
+
label: ReactNode;
|
|
6
|
+
description?: ReactNode;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface CheckboxGroupProps {
|
|
10
|
+
/** Legend visible y nombre accesible del grupo. */
|
|
11
|
+
label: ReactNode;
|
|
12
|
+
/** Opciones con valores únicos y no vacíos. */
|
|
13
|
+
options: readonly ChoiceGroupOption[];
|
|
14
|
+
/** Valores controlados. */
|
|
15
|
+
value?: readonly string[];
|
|
16
|
+
/** Selección inicial no controlada. @defaultValue [] */
|
|
17
|
+
defaultValue?: readonly string[];
|
|
18
|
+
/** Notifica todos los valores marcados. */
|
|
19
|
+
onValueChange?: (value: string[]) => void;
|
|
20
|
+
/** Nombre compartido; FormData.getAll(name) devuelve los marcados. */
|
|
21
|
+
name?: string;
|
|
22
|
+
description?: ReactNode;
|
|
23
|
+
error?: ReactNode;
|
|
24
|
+
disabled?: boolean;
|
|
25
|
+
/** Exige al menos una opción habilitada seleccionada. */
|
|
26
|
+
required?: boolean;
|
|
27
|
+
/** @defaultValue 'vertical' */
|
|
28
|
+
orientation?: 'horizontal' | 'vertical';
|
|
29
|
+
id?: string;
|
|
30
|
+
className?: string;
|
|
31
|
+
}
|
|
32
|
+
export type SwitchGroupProps = CheckboxGroupProps;
|
|
33
|
+
/** Selección múltiple para formularios. En modo controlado, restablecer value en onReset. */
|
|
34
|
+
export declare function CheckboxGroup(props: CheckboxGroupProps): ReactElement;
|
|
35
|
+
/** Ajustes independientes con la misma gestión de grupo que CheckboxGroup. */
|
|
36
|
+
export declare function SwitchGroup(props: SwitchGroupProps): ReactElement;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/** Familia de selección múltiple basada en Checkbox y Switch, con un fieldset común. */
|
|
4
|
+
import { useId } from 'react';
|
|
5
|
+
import { Checkbox, Switch } from './toggles.js';
|
|
6
|
+
import { useControllable } from './internal/state.js';
|
|
7
|
+
import { cx, hasContent } from './utils.js';
|
|
8
|
+
function ChoiceGroup({ label, options, value, defaultValue = [], onValueChange, name, description, error, disabled, required, orientation = 'vertical', id, className, kind, }) {
|
|
9
|
+
const generatedId = useId();
|
|
10
|
+
const controlId = id ?? `dgm-choices-${generatedId}`;
|
|
11
|
+
const [selected, change] = useControllable(value, defaultValue, onValueChange ? (next) => onValueChange([...next]) : undefined);
|
|
12
|
+
const Control = kind === 'switch' ? Switch : Checkbox;
|
|
13
|
+
const describedBy = cx(hasContent(description) && `${controlId}-description`, hasContent(error) && `${controlId}-error`) || undefined;
|
|
14
|
+
const firstEnabled = options.find((option) => !option.disabled)?.value;
|
|
15
|
+
const missing = required && !options.some((option) => !option.disabled && selected.includes(option.value));
|
|
16
|
+
return (_jsxs("fieldset", { id: controlId, disabled: disabled, "aria-describedby": describedBy, "aria-invalid": hasContent(error) || undefined, className: cx('dgm-choice-group', `dgm-choice-group--${kind}`, className), children: [_jsxs("legend", { className: "dgm-label", children: [label, required && _jsx("span", { "aria-hidden": "true", children: " *" })] }), hasContent(description) && (_jsx("p", { className: "dgm-description", id: `${controlId}-description`, children: description })), _jsx("div", { className: "dgm-choice-group-options", "data-orientation": orientation, children: options.map((option, index) => (_jsx(Control, { id: `${controlId}-${index}`, name: name, value: option.value, checked: selected.includes(option.value), disabled: disabled || option.disabled, label: option.label, description: option.description, "aria-describedby": describedBy, "aria-invalid": hasContent(error) || undefined, required: Boolean(missing && option.value === firstEnabled), onChange: (event) => change(event.target.checked
|
|
17
|
+
? [...selected, option.value]
|
|
18
|
+
: selected.filter((item) => item !== option.value)) }, option.value))) }), hasContent(error) && (_jsx("p", { className: "dgm-error", id: `${controlId}-error`, children: error }))] }));
|
|
19
|
+
}
|
|
20
|
+
/** Selección múltiple para formularios. En modo controlado, restablecer value en onReset. */
|
|
21
|
+
export function CheckboxGroup(props) {
|
|
22
|
+
return _jsx(ChoiceGroup, { ...props, kind: "checkbox" });
|
|
23
|
+
}
|
|
24
|
+
/** Ajustes independientes con la misma gestión de grupo que CheckboxGroup. */
|
|
25
|
+
export function SwitchGroup(props) {
|
|
26
|
+
return _jsx(ChoiceGroup, { ...props, kind: "switch" });
|
|
27
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** Selector con búsqueda: Popover sitúa el panel y cmdk gestiona la lista con teclado. */
|
|
2
|
+
import { type ReactNode } from 'react';
|
|
3
|
+
import type { SelectOption } from './custom-select.js';
|
|
4
|
+
export interface ComboboxOption extends SelectOption {
|
|
5
|
+
/** Sinónimos adicionales que participan en la búsqueda. */
|
|
6
|
+
keywords?: readonly string[];
|
|
7
|
+
}
|
|
8
|
+
export interface ComboboxProps {
|
|
9
|
+
label: ReactNode;
|
|
10
|
+
options: readonly ComboboxOption[];
|
|
11
|
+
value?: string;
|
|
12
|
+
defaultValue?: string;
|
|
13
|
+
onValueChange?: (value: string) => void;
|
|
14
|
+
/** Texto sin selección. */
|
|
15
|
+
placeholder?: string;
|
|
16
|
+
/** Etiqueta y placeholder del buscador. */
|
|
17
|
+
searchPlaceholder?: string;
|
|
18
|
+
/** Mensaje cuando ninguna opción coincide. */
|
|
19
|
+
emptyMessage?: string;
|
|
20
|
+
description?: ReactNode;
|
|
21
|
+
error?: ReactNode;
|
|
22
|
+
name?: string;
|
|
23
|
+
required?: boolean;
|
|
24
|
+
disabled?: boolean;
|
|
25
|
+
/** Muestra un control para borrar la selección. @defaultValue false */
|
|
26
|
+
clearable?: boolean;
|
|
27
|
+
/** Idioma de los textos integrados. @defaultValue 'en' */
|
|
28
|
+
locale?: 'es' | 'en';
|
|
29
|
+
id?: string;
|
|
30
|
+
className?: string;
|
|
31
|
+
wrapperClassName?: string;
|
|
32
|
+
}
|
|
33
|
+
/** Selección única de un catálogo; no permite enviar texto libre como valor. */
|
|
34
|
+
export declare const Combobox: import("react").ForwardRefExoticComponent<ComboboxProps & import("react").RefAttributes<HTMLButtonElement>>;
|
|
35
|
+
/** Alias de Combobox: Autocomplete también selecciona valores existentes, no texto libre. */
|
|
36
|
+
export declare const Autocomplete: import("react").ForwardRefExoticComponent<ComboboxProps & import("react").RefAttributes<HTMLButtonElement>>;
|
|
37
|
+
export type AutocompleteProps = ComboboxProps;
|
package/dist/combobox.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/** Selector con búsqueda: Popover sitúa el panel y cmdk gestiona la lista con teclado. */
|
|
4
|
+
import { forwardRef, useId, useImperativeHandle, useRef, useState } from 'react';
|
|
5
|
+
import * as Popover from '@radix-ui/react-popover';
|
|
6
|
+
import { Command } from 'cmdk';
|
|
7
|
+
import { useControllable } from './internal/state.js';
|
|
8
|
+
import { restoreFocus } from './internal/focus.js';
|
|
9
|
+
import { preventTriggerLabelDismiss } from './internal/trigger-label.js';
|
|
10
|
+
import { CheckIcon, ChevronIcon } 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
|
+
import { OverlayLayerProvider, useOverlayLayer } from './internal/overlay-layer.js';
|
|
15
|
+
/** Selección única de un catálogo; no permite enviar texto libre como valor. */
|
|
16
|
+
export const Combobox = forwardRef(function Combobox({ label, options, value, defaultValue = '', onValueChange, placeholder, searchPlaceholder, emptyMessage, description, error, name, required, disabled, clearable = false, locale = 'en', id, className, wrapperClassName, }, ref) {
|
|
17
|
+
const [selected, change] = useControllable(value, defaultValue, onValueChange);
|
|
18
|
+
const [open, setOpen] = useFieldOpen(disabled);
|
|
19
|
+
const { depth, style: layerStyle } = useOverlayLayer();
|
|
20
|
+
const [invalid, setInvalid] = useState(false);
|
|
21
|
+
const trigger = useRef(null);
|
|
22
|
+
// Un clic fuera debe conservar el foco en su destino, como ocurre en Popover.
|
|
23
|
+
const interactedOutside = useRef(false);
|
|
24
|
+
useImperativeHandle(ref, () => trigger.current);
|
|
25
|
+
const generatedId = useId();
|
|
26
|
+
const controlId = id ?? `dgm-combo-${generatedId}`;
|
|
27
|
+
const es = locale === 'es';
|
|
28
|
+
const current = options.find((option) => option.value === selected);
|
|
29
|
+
const message = error || (invalid ? (es ? 'Selecciona una opción.' : 'Select an option.') : undefined);
|
|
30
|
+
const describedBy = cx(hasContent(description) && `${controlId}-description`, hasContent(message) && `${controlId}-error`) || undefined;
|
|
31
|
+
function choose(next) {
|
|
32
|
+
if (disabled)
|
|
33
|
+
return;
|
|
34
|
+
change(next);
|
|
35
|
+
setInvalid(false);
|
|
36
|
+
setOpen(false);
|
|
37
|
+
}
|
|
38
|
+
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: " *" })] }), _jsxs("select", { className: "dgm-form-value", "aria-hidden": "true", tabIndex: -1, name: name, value: current ? selected : '', required: required, disabled: disabled, onChange: (event) => choose(event.target.value), onInvalid: (event) => {
|
|
39
|
+
event.preventDefault();
|
|
40
|
+
setInvalid(true);
|
|
41
|
+
trigger.current?.focus({ preventScroll: true });
|
|
42
|
+
}, children: [_jsx("option", { value: "" }), options.map((option) => (_jsx("option", { value: option.value, disabled: option.disabled, children: option.label }, option.value)))] }), _jsx(OverlayLayerProvider, { depth: depth, children: _jsxs(Popover.Root, { open: open, onOpenChange: setOpen, children: [_jsxs(Popover.Trigger, { ref: trigger, id: controlId, disabled: disabled, "aria-labelledby": `${controlId}-label ${controlId}-value`, "aria-describedby": describedBy, "aria-invalid": hasContent(message) || undefined, className: cx('dgm-input', 'dgm-combobox-trigger', className), children: [_jsxs("span", { className: "dgm-custom-value", id: `${controlId}-value`, children: [current?.icon && (_jsx("span", { className: "dgm-option-icon", "aria-hidden": "true", children: current.icon })), current?.label ?? placeholder ?? (es ? 'Seleccionar…' : 'Select…')] }), _jsx("span", { className: "dgm-custom-chevron", children: _jsx(ChevronIcon, {}) })] }), _jsx(Popover.Portal, { children: _jsx(Popover.Content, { className: "dgm-combobox-panel", style: layerStyle, sideOffset: 6, collisionPadding: 12, "aria-labelledby": `${controlId}-label`, onInteractOutside: (event) => {
|
|
43
|
+
if (preventTriggerLabelDismiss(event, trigger.current))
|
|
44
|
+
return;
|
|
45
|
+
interactedOutside.current = true;
|
|
46
|
+
}, onCloseAutoFocus: (event) => {
|
|
47
|
+
event.preventDefault();
|
|
48
|
+
if (!interactedOutside.current)
|
|
49
|
+
restoreFocus(trigger.current);
|
|
50
|
+
interactedOutside.current = false;
|
|
51
|
+
}, children: _jsxs(Command, { defaultValue: selected, className: "dgm-command", label: es ? 'Opciones' : 'Options', loop: true, children: [_jsx(Command.Input, { className: "dgm-command-input", "aria-label": searchPlaceholder ?? (es ? 'Buscar opciones' : 'Search options'), placeholder: searchPlaceholder ?? (es ? 'Buscar…' : 'Search…') }), _jsx(ScrollHints, { children: _jsxs(Command.List, { className: "dgm-command-list", children: [_jsx(Command.Empty, { className: "dgm-command-empty", children: emptyMessage ?? (es ? 'Sin resultados.' : 'No results.') }), options.map((option) => (_jsxs(Command.Item, { value: option.value, keywords: [option.label, ...(option.keywords ?? [])], disabled: disabled || option.disabled, onSelect: () => choose(option.value), className: "dgm-command-item", children: [option.icon && (_jsx("span", { className: "dgm-option-icon", "aria-hidden": "true", children: option.icon })), _jsx("span", { children: option.label }), option.value === selected && (_jsx("span", { className: "dgm-command-check", "aria-hidden": "true", children: _jsx(CheckIcon, {}) }))] }, option.value)))] }) })] }) }) })] }) }), clearable && selected && (_jsx("button", { type: "button", className: "dgm-text-action", disabled: disabled, onClick: () => {
|
|
52
|
+
choose('');
|
|
53
|
+
trigger.current?.focus({ preventScroll: true });
|
|
54
|
+
}, children: es ? 'Borrar selección' : 'Clear selection' })), hasContent(description) && (_jsx("span", { className: "dgm-description", id: `${controlId}-description`, children: description })), hasContent(message) && (_jsx("span", { className: "dgm-error", id: `${controlId}-error`, children: message }))] }));
|
|
55
|
+
});
|
|
56
|
+
/** Alias de Combobox: Autocomplete también selecciona valores existentes, no texto libre. */
|
|
57
|
+
export const Autocomplete = Combobox;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Buscador de acciones en nuestro Modal. El atajo global es opcional y se limpia al desmontar. */
|
|
2
|
+
import { type ReactElement, type ReactNode } from 'react';
|
|
3
|
+
export interface CommandAction {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
/** Sinónimos para la búsqueda. */
|
|
7
|
+
keywords?: readonly string[];
|
|
8
|
+
/** Agrupa acciones bajo un título visible. */
|
|
9
|
+
group?: string;
|
|
10
|
+
icon?: ReactNode;
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
/** La aplicación decide la navegación o acción concreta. */
|
|
13
|
+
onSelect: () => void;
|
|
14
|
+
}
|
|
15
|
+
export interface CommandPaletteProps {
|
|
16
|
+
open: boolean;
|
|
17
|
+
onOpenChange: (open: boolean) => void;
|
|
18
|
+
actions: readonly CommandAction[];
|
|
19
|
+
title: string;
|
|
20
|
+
placeholder?: string;
|
|
21
|
+
emptyMessage?: string;
|
|
22
|
+
/** Ctrl+K / Cmd+K fuera de campos editables. Actívalo una sola vez por aplicación. @defaultValue false */
|
|
23
|
+
shortcut?: boolean;
|
|
24
|
+
locale?: 'es' | 'en';
|
|
25
|
+
closeLabel?: string;
|
|
26
|
+
className?: string;
|
|
27
|
+
}
|
|
28
|
+
export declare function CommandPalette({ open, onOpenChange, actions, title, placeholder, emptyMessage, shortcut, locale, closeLabel, className, }: CommandPaletteProps): ReactElement;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/** Buscador de acciones en nuestro Modal. El atajo global es opcional y se limpia al desmontar. */
|
|
4
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
5
|
+
import { Command } from 'cmdk';
|
|
6
|
+
import { Modal } from './modal.js';
|
|
7
|
+
import { cx } from './utils.js';
|
|
8
|
+
import { ScrollHints } from './internal/scroll-hints.js';
|
|
9
|
+
export function CommandPalette({ open, onOpenChange, actions, title, placeholder, emptyMessage, shortcut = false, locale = 'en', closeLabel, className, }) {
|
|
10
|
+
const searchInput = useRef(null);
|
|
11
|
+
const interacted = useRef(false);
|
|
12
|
+
const [active, setActive] = useState('');
|
|
13
|
+
const [inputMode, setInputMode] = useState('idle');
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (!open) {
|
|
16
|
+
interacted.current = false;
|
|
17
|
+
setActive('');
|
|
18
|
+
setInputMode('idle');
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
// Modal registra primero el disparador para devolverle el foco al cerrar.
|
|
22
|
+
// En el siguiente frame llevamos el foco al buscador sin alterar ese destino.
|
|
23
|
+
const frame = requestAnimationFrame(() => searchInput.current?.focus({ preventScroll: true }));
|
|
24
|
+
return () => cancelAnimationFrame(frame);
|
|
25
|
+
}, [open]);
|
|
26
|
+
const latest = useRef({ open, onOpenChange });
|
|
27
|
+
latest.current = { open, onOpenChange };
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (!shortcut)
|
|
30
|
+
return;
|
|
31
|
+
function keydown(event) {
|
|
32
|
+
const target = event.target;
|
|
33
|
+
if (event.defaultPrevented ||
|
|
34
|
+
event.repeat ||
|
|
35
|
+
event.altKey ||
|
|
36
|
+
event.shiftKey ||
|
|
37
|
+
!(event.ctrlKey || event.metaKey) ||
|
|
38
|
+
event.key.toLowerCase() !== 'k')
|
|
39
|
+
return;
|
|
40
|
+
if (target instanceof HTMLElement &&
|
|
41
|
+
(target.isContentEditable ||
|
|
42
|
+
target.closest('input,textarea,select,[contenteditable="true"]')))
|
|
43
|
+
return;
|
|
44
|
+
event.preventDefault();
|
|
45
|
+
latest.current.onOpenChange(!latest.current.open);
|
|
46
|
+
}
|
|
47
|
+
document.addEventListener('keydown', keydown);
|
|
48
|
+
return () => document.removeEventListener('keydown', keydown);
|
|
49
|
+
}, [shortcut]);
|
|
50
|
+
// Conserva el orden de grupos y acciones sin filtrar de nuevo toda la lista por grupo.
|
|
51
|
+
const groups = useMemo(() => {
|
|
52
|
+
const grouped = new Map();
|
|
53
|
+
for (const action of actions) {
|
|
54
|
+
const key = action.group ?? '';
|
|
55
|
+
const group = grouped.get(key);
|
|
56
|
+
if (group)
|
|
57
|
+
group.push(action);
|
|
58
|
+
else
|
|
59
|
+
grouped.set(key, [action]);
|
|
60
|
+
}
|
|
61
|
+
return [...grouped];
|
|
62
|
+
}, [actions]);
|
|
63
|
+
const es = locale === 'es';
|
|
64
|
+
return (_jsx(Modal, { open: open, onOpenChange: onOpenChange, title: title, closeLabel: closeLabel ?? (es ? 'Cerrar buscador' : 'Close command menu'), className: cx('dgm-command-modal', className), children: _jsxs(Command, { label: title, loop: true, className: "dgm-command dgm-command--palette", value: active, onValueChange: (value) => {
|
|
65
|
+
// cmdk propone la primera acción al montar. Solo aceptamos una opción
|
|
66
|
+
// después de una interacción; abrir el modal no equivale a seleccionarla.
|
|
67
|
+
if (interacted.current)
|
|
68
|
+
setActive(value);
|
|
69
|
+
}, "data-input-mode": inputMode, onPointerMoveCapture: () => {
|
|
70
|
+
interacted.current = true;
|
|
71
|
+
setInputMode('pointer');
|
|
72
|
+
}, onKeyDownCapture: (event) => {
|
|
73
|
+
const navigationKey = ['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key);
|
|
74
|
+
const vimKey = event.ctrlKey && ['n', 'j', 'p', 'k'].includes(event.key);
|
|
75
|
+
if (navigationKey || vimKey) {
|
|
76
|
+
interacted.current = true;
|
|
77
|
+
setInputMode('keyboard');
|
|
78
|
+
}
|
|
79
|
+
}, children: [_jsx(Command.Input, { ref: searchInput, onValueChange: () => {
|
|
80
|
+
interacted.current = true;
|
|
81
|
+
setInputMode('keyboard');
|
|
82
|
+
}, className: "dgm-command-input", "aria-label": placeholder ?? (es ? 'Buscar acciones' : 'Search actions'), placeholder: placeholder ?? (es ? 'Buscar acciones…' : 'Search actions…') }), _jsx(ScrollHints, { children: _jsxs(Command.List, { className: "dgm-command-list", children: [_jsx(Command.Empty, { className: "dgm-command-empty", children: emptyMessage ?? (es ? 'No hay acciones coincidentes.' : 'No matching actions.') }), groups.map(([group, groupActions]) => (_jsx(Command.Group, { heading: group || undefined, children: groupActions.map((action) => (_jsxs(Command.Item, { className: "dgm-command-item", value: action.id, keywords: [action.label, ...(action.keywords ?? [])], disabled: action.disabled, onPointerLeave: () => {
|
|
83
|
+
// El hover termina al salir de la fila, aunque el puntero siga en el modal.
|
|
84
|
+
if (inputMode === 'pointer')
|
|
85
|
+
setActive('');
|
|
86
|
+
}, onSelect: () => {
|
|
87
|
+
onOpenChange(false);
|
|
88
|
+
action.onSelect();
|
|
89
|
+
}, children: [action.icon && (_jsx("span", { className: "dgm-option-icon", "aria-hidden": "true", children: action.icon })), action.label] }, action.id))) }, group)))] }) })] }) }));
|
|
90
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Estructura de página estática: no necesita hooks, hidratación ni frontera `use client`. */
|
|
2
|
+
import type { HTMLAttributes, ReactElement } from 'react';
|
|
3
|
+
/** Elementos estructurales con atributos HTML comunes; no admite controles ni componentes arbitrarios. */
|
|
4
|
+
export type ContainerElement = 'div' | 'main' | 'section' | 'article' | 'header' | 'footer' | 'aside' | 'nav';
|
|
5
|
+
/** Anchos máximos exteriores, incluido el padding: 40, 48, 64 y 80 rem, o todo el padre. */
|
|
6
|
+
export type ContainerSize = 'sm' | 'md' | 'lg' | 'xl' | 'full';
|
|
7
|
+
/** Atributos HTML y opciones de estructura. children, className, style y aria-* se conservan. */
|
|
8
|
+
export interface ContainerProps extends HTMLAttributes<HTMLElement> {
|
|
9
|
+
/** Elemento semántico. Usa un único main visible por página. @defaultValue 'div' */
|
|
10
|
+
as?: ContainerElement;
|
|
11
|
+
/** Límite de ancho; siempre se adapta al espacio disponible en el padre. @defaultValue 'xl' */
|
|
12
|
+
size?: ContainerSize;
|
|
13
|
+
/** Espaciado lateral fluido de 1 a 2 rem según el viewport; false lo elimina. @defaultValue true */
|
|
14
|
+
padding?: boolean;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Centra y limita el contenido sin imponer color, altura ni distribución interna.
|
|
18
|
+
* No expone ref: es una pieza de composición estática, como Card.
|
|
19
|
+
* Para fondos a todo el ancho, coloca el fondo en una sección exterior y el Container dentro.
|
|
20
|
+
*/
|
|
21
|
+
export declare function Container({ as: Element, size, padding, className, ...props }: ContainerProps): ReactElement;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from './utils.js';
|
|
3
|
+
/**
|
|
4
|
+
* Centra y limita el contenido sin imponer color, altura ni distribución interna.
|
|
5
|
+
* No expone ref: es una pieza de composición estática, como Card.
|
|
6
|
+
* Para fondos a todo el ancho, coloca el fondo en una sección exterior y el Container dentro.
|
|
7
|
+
*/
|
|
8
|
+
export function Container({ as: Element = 'div', size = 'xl', padding = true, className, ...props }) {
|
|
9
|
+
return (_jsx(Element, { ...props, className: cx('dgm-container', `dgm-container--${size}`, !padding && 'dgm-container--flush', className) }));
|
|
10
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Menú contextual sobre una zona concreta. Radix gestiona el anclaje al puntero y la navegación. */
|
|
2
|
+
import { type ReactElement } from 'react';
|
|
3
|
+
import type { DropdownMenuItem } from './dropdown-menu.js';
|
|
4
|
+
/** Mismo contrato de acciones y separadores que DropdownMenu. */
|
|
5
|
+
export type ContextMenuItem = DropdownMenuItem;
|
|
6
|
+
export interface ContextMenuProps {
|
|
7
|
+
/** Un único elemento DOM o componente que transmita ref y props; se conserva su aspecto. */
|
|
8
|
+
children: ReactElement;
|
|
9
|
+
/** Acciones y separadores con IDs únicos. Los atajos son leyendas, no se registran. */
|
|
10
|
+
items: readonly ContextMenuItem[];
|
|
11
|
+
/** Encabezado visible y nombre accesible del menú. */
|
|
12
|
+
label: string;
|
|
13
|
+
/** Desactiva únicamente el menú personalizado y conserva el menú nativo. @defaultValue false */
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
/** Notifica apertura y cierre. La posición y la apertura proceden del disparador. */
|
|
16
|
+
onOpenChange?: (open: boolean) => void;
|
|
17
|
+
/** Clase CSS del panel renderizado en el portal. */
|
|
18
|
+
className?: string;
|
|
19
|
+
}
|
|
20
|
+
/** Clic derecho, pulsación prolongada o Shift+F10. El clic izquierdo conserva la acción del hijo. */
|
|
21
|
+
export declare function ContextMenu({ children, items, label, disabled, onOpenChange, className, }: ContextMenuProps): ReactElement;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/** Menú contextual sobre una zona concreta. Radix gestiona el anclaje al puntero y la navegación. */
|
|
4
|
+
import { useId, useRef, useState } from 'react';
|
|
5
|
+
import * as Primitive from '@radix-ui/react-context-menu';
|
|
6
|
+
import { restoreFocus } from './internal/focus.js';
|
|
7
|
+
import { OverlayLayerProvider, useOverlayLayer } from './internal/overlay-layer.js';
|
|
8
|
+
import { cx } from './utils.js';
|
|
9
|
+
/** Clic derecho, pulsación prolongada o Shift+F10. El clic izquierdo conserva la acción del hijo. */
|
|
10
|
+
export function ContextMenu({ children, items, label, disabled = false, onOpenChange, className, }) {
|
|
11
|
+
const { depth, style: layerStyle } = useOverlayLayer();
|
|
12
|
+
const trigger = useRef(null);
|
|
13
|
+
const outside = useRef(false);
|
|
14
|
+
const [open, setOpen] = useState(false);
|
|
15
|
+
const menuId = useId();
|
|
16
|
+
return (_jsx(OverlayLayerProvider, { depth: depth, children: _jsxs(Primitive.Root, { modal: false, onOpenChange: (next) => {
|
|
17
|
+
setOpen(next);
|
|
18
|
+
onOpenChange?.(next);
|
|
19
|
+
}, children: [_jsx(Primitive.Trigger, { asChild: true, ref: trigger, disabled: disabled, style: disabled ? { WebkitTouchCallout: 'default' } : undefined, tabIndex: disabled ? undefined : 0, "aria-haspopup": disabled ? undefined : 'menu', "aria-expanded": disabled ? undefined : open, "aria-controls": open ? menuId : undefined, className: "dgm-context-trigger", onKeyDown: (event) => {
|
|
20
|
+
if (disabled ||
|
|
21
|
+
event.defaultPrevented ||
|
|
22
|
+
event.target !== event.currentTarget ||
|
|
23
|
+
event.repeat)
|
|
24
|
+
return;
|
|
25
|
+
if (event.key !== 'ContextMenu' && !(event.shiftKey && event.key === 'F10'))
|
|
26
|
+
return;
|
|
27
|
+
event.preventDefault();
|
|
28
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
29
|
+
// Reutiliza el evento de apertura de Radix para situar su ancla virtual junto al elemento.
|
|
30
|
+
event.currentTarget.dispatchEvent(new MouseEvent('contextmenu', {
|
|
31
|
+
bubbles: true,
|
|
32
|
+
cancelable: true,
|
|
33
|
+
button: 2,
|
|
34
|
+
clientX: rect.left + 8,
|
|
35
|
+
clientY: rect.bottom,
|
|
36
|
+
}));
|
|
37
|
+
}, children: children }), _jsx(Primitive.Portal, { children: _jsxs(Primitive.Content, { style: layerStyle, id: menuId, "aria-label": label, collisionPadding: 12, className: cx('dgm-dropdown', 'dgm-context-menu', className), onContextMenu: (event) => {
|
|
38
|
+
// El panel ya es el menú contextual: evita superponer el del navegador
|
|
39
|
+
// o propagar el gesto a otra zona contextual a través del portal.
|
|
40
|
+
event.preventDefault();
|
|
41
|
+
event.stopPropagation();
|
|
42
|
+
}, onInteractOutside: () => {
|
|
43
|
+
outside.current = true;
|
|
44
|
+
}, onCloseAutoFocus: (event) => {
|
|
45
|
+
event.preventDefault();
|
|
46
|
+
if (!outside.current && trigger.current?.isConnected)
|
|
47
|
+
restoreFocus(trigger.current);
|
|
48
|
+
outside.current = false;
|
|
49
|
+
}, children: [_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)))] }) })] }) }));
|
|
50
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
/** Cada valor debe ser único y no vacío; label proporciona el texto accesible y de búsqueda. */
|
|
3
|
+
export interface SelectOption {
|
|
4
|
+
/** Identificador enviado al formulario y a onValueChange. */
|
|
5
|
+
value: string;
|
|
6
|
+
/** Texto visible, nombre accesible y clave para la búsqueda por letras. */
|
|
7
|
+
label: string;
|
|
8
|
+
/** Icono decorativo: puede ser SVG, un componente de iconos o texto. */
|
|
9
|
+
icon?: ReactNode;
|
|
10
|
+
/** Impide seleccionar esta opción con teclado o puntero. */
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
}
|
|
13
|
+
/** Selector recomendado para código nuevo. La API se limita a selección única. */
|
|
14
|
+
export interface CustomSelectProps {
|
|
15
|
+
/** Etiqueta visible obligatoria vinculada al disparador. */
|
|
16
|
+
label: ReactNode;
|
|
17
|
+
/** Catálogo ordenado de valores disponibles. */
|
|
18
|
+
options: readonly SelectOption[];
|
|
19
|
+
/** Valor controlado. No combinar con defaultValue. */
|
|
20
|
+
value?: string;
|
|
21
|
+
/** Valor inicial cuando el consumidor no controla el estado. */
|
|
22
|
+
defaultValue?: string;
|
|
23
|
+
/** Notifica el valor elegido; no entrega un evento de select nativo. */
|
|
24
|
+
onValueChange?: (value: string) => void;
|
|
25
|
+
/** Texto cuando no hay selección. Localizar en la aplicación consumidora. */
|
|
26
|
+
placeholder?: string;
|
|
27
|
+
/** Ayuda asociada mediante aria-describedby. */
|
|
28
|
+
description?: ReactNode;
|
|
29
|
+
/** Error visible que activa aria-invalid; no ejecuta validación de negocio. */
|
|
30
|
+
error?: ReactNode;
|
|
31
|
+
/** Impide cambiar el valor y cierra el desplegable si está abierto. @defaultValue false */
|
|
32
|
+
disabled?: boolean;
|
|
33
|
+
/** Exige una selección en el formulario. @defaultValue false */
|
|
34
|
+
required?: boolean;
|
|
35
|
+
/** Nombre usado por el control de formulario que mantiene Radix. */
|
|
36
|
+
name?: string;
|
|
37
|
+
/** ID del disparador. Se genera con useId si se omite. */
|
|
38
|
+
id?: string;
|
|
39
|
+
/** Clase del botón disparador. */
|
|
40
|
+
className?: string;
|
|
41
|
+
/** Clase del conjunto formado por etiqueta, control y mensajes. */
|
|
42
|
+
wrapperClassName?: string;
|
|
43
|
+
}
|
|
44
|
+
/** Desplegable animado renderizado en un portal para evitar recortes por overflow. */
|
|
45
|
+
export declare const CustomSelect: import("react").ForwardRefExoticComponent<CustomSelectProps & import("react").RefAttributes<HTMLButtonElement>>;
|