danielgm-ui 0.4.5 → 0.4.6

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.
@@ -1,12 +1,12 @@
1
1
  /** Menú contextual sobre una zona concreta. Radix gestiona el anclaje al puntero y la navegación. */
2
2
  import { type ReactElement } from 'react';
3
3
  import type { DropdownMenuItem } from './dropdown-menu.js';
4
- /** Mismo contrato de acciones y separadores que DropdownMenu. */
4
+ /** Mismo contrato de acciones, categorías anidadas y separadores que DropdownMenu. */
5
5
  export type ContextMenuItem = DropdownMenuItem;
6
6
  export interface ContextMenuProps {
7
7
  /** Un único elemento DOM o componente que transmita ref y props; se conserva su aspecto. */
8
8
  children: ReactElement;
9
- /** Acciones y separadores con IDs únicos. Los atajos son leyendas, no se registran. */
9
+ /** Acciones, categorías y separadores con IDs únicos por nivel. Los atajos son leyendas. */
10
10
  items: readonly ContextMenuItem[];
11
11
  /** Apariencia de las opciones; no modifica el panel. @defaultValue 'default' */
12
12
  itemVariant?: 'default' | 'ghost';
@@ -16,7 +16,7 @@ export interface ContextMenuProps {
16
16
  disabled?: boolean;
17
17
  /** Notifica apertura y cierre. La posición y la apertura proceden del disparador. */
18
18
  onOpenChange?: (open: boolean) => void;
19
- /** Clase CSS del panel renderizado en el portal. */
19
+ /** Clase CSS de todos los paneles, incluidos los submenús, renderizados en portales. */
20
20
  className?: string;
21
21
  }
22
22
  /** Clic derecho, pulsación prolongada o Shift+F10. El clic izquierdo conserva la acción del hijo. */
@@ -6,6 +6,7 @@ import * as Primitive from '@radix-ui/react-context-menu';
6
6
  import { menuItemFocusProps } from './internal/menu-item-focus.js';
7
7
  import { restoreFocus } from './internal/focus.js';
8
8
  import { OverlayLayerProvider, useOverlayLayer } from './internal/overlay-layer.js';
9
+ import { MenuItems } from './internal/menu-items.js';
9
10
  import { cx } from './utils.js';
10
11
  /** Clic derecho, pulsación prolongada o Shift+F10. El clic izquierdo conserva la acción del hijo. */
11
12
  export function ContextMenu({ children, items, itemVariant = 'default', label, disabled = false, onOpenChange, className, }) {
@@ -47,5 +48,5 @@ export function ContextMenu({ children, items, itemVariant = 'default', label, d
47
48
  if (!outside.current && trigger.current?.isConnected)
48
49
  restoreFocus(trigger.current);
49
50
  outside.current = false;
50
- }, 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.variant ?? itemVariant) === 'ghost' && 'dgm-dropdown-item--ghost', 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)))] }) })] }) }));
51
+ }, children: [_jsx(Primitive.Label, { className: "dgm-dropdown-label", children: label }), _jsx(MenuItems, { primitive: Primitive, items: items, itemVariant: itemVariant, className: cx('dgm-context-menu', className) })] }) })] }) }));
51
52
  }
@@ -1,30 +1,37 @@
1
1
  /** Menú de acciones: Radix gestiona teclado, búsqueda por letras, portal y foco. */
2
2
  import { type ReactElement, type ReactNode } from 'react';
3
- /** Una acción tiene un ID estable; los separadores no son seleccionables. */
3
+ /** Acciones, separadores o categorías anidadas con IDs estables y únicos por nivel. */
4
4
  export type DropdownMenuItem = {
5
5
  id: string;
6
6
  type: 'separator';
7
- } | {
8
- id: string;
7
+ } | (MenuEntry & {
9
8
  type?: 'item';
10
- /** Texto visible, accesible y usado para buscar escribiendo. */
9
+ /** Leyenda decorativa; no registra un atajo. */
10
+ shortcut?: string;
11
+ /** Ejecuta la acción; preventDefault mantiene el menú abierto. */
12
+ onSelect?: (event: Event) => void;
13
+ }) | (MenuEntry & {
14
+ type: 'submenu';
15
+ /** Opciones del panel lateral; admite más categorías. Hover no ejecuta acciones. */
16
+ items: readonly DropdownMenuItem[];
17
+ });
18
+ /** Presentación compartida por acciones y categorías. */
19
+ interface MenuEntry {
20
+ id: string;
21
+ /** Texto visible y accesible, también usado para buscar escribiendo. */
11
22
  label: string;
12
23
  /** Icono decorativo opcional. */
13
24
  icon?: ReactNode;
14
- /** Leyenda visual: no registra por sí misma un atajo de teclado. */
15
- shortcut?: string;
16
25
  disabled?: boolean;
17
- /** Apariencia de esta opción; hereda itemVariant del menú. */
26
+ /** Hereda itemVariant del menú, también en paneles anidados. */
18
27
  variant?: 'default' | 'ghost';
19
- /** Resalta acciones destructivas sin ejecutarlas automáticamente. */
28
+ /** Tono semántico destructivo. */
20
29
  danger?: boolean;
21
- /** Se ejecuta con puntero o teclado. preventDefault mantiene abierto el menú. */
22
- onSelect?: (event: Event) => void;
23
- };
30
+ }
24
31
  export interface DropdownMenuProps {
25
32
  /** Un único botón que acepte ref y props; Button es compatible. */
26
33
  children: ReactElement;
27
- /** Acciones y separadores en orden de lectura. IDs únicos. */
34
+ /** Acciones, categorías y separadores en orden de lectura. IDs únicos por nivel. */
28
35
  items: readonly DropdownMenuItem[];
29
36
  /** Apariencia de las opciones; no modifica el panel. @defaultValue 'default' */
30
37
  itemVariant?: 'default' | 'ghost';
@@ -44,8 +51,9 @@ export interface DropdownMenuProps {
44
51
  sideOffset?: number;
45
52
  /** Desactiva el disparador. @defaultValue false */
46
53
  disabled?: boolean;
47
- /** Clase del panel que se renderiza en el portal. */
54
+ /** Clase de los paneles, incluidos los submenús, renderizados en portales. */
48
55
  className?: string;
49
56
  }
50
57
  /** El menú no bloquea el scroll de la página y devuelve el foco sin desplazarla. */
51
58
  export declare function DropdownMenu({ children, items, itemVariant, label, open, defaultOpen, onOpenChange, side, align, sideOffset, disabled, className, }: DropdownMenuProps): ReactElement;
59
+ export {};
@@ -6,6 +6,7 @@ import * as Primitive from '@radix-ui/react-dropdown-menu';
6
6
  import { menuItemFocusProps } from './internal/menu-item-focus.js';
7
7
  import { restoreFocus } from './internal/focus.js';
8
8
  import { OverlayLayerProvider, useOverlayLayer } from './internal/overlay-layer.js';
9
+ import { MenuItems } from './internal/menu-items.js';
9
10
  import { cx } from './utils.js';
10
11
  /** El menú no bloquea el scroll de la página y devuelve el foco sin desplazarla. */
11
12
  export function DropdownMenu({ children, items, itemVariant = 'default', label, open, defaultOpen, onOpenChange, side = 'bottom', align = 'start', sideOffset = 6, disabled, className, }) {
@@ -21,5 +22,5 @@ export function DropdownMenu({ children, items, itemVariant = 'default', label,
21
22
  restoreFocus(trigger.current);
22
23
  }
23
24
  interactedOutside.current = false;
24
- }, 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.variant ?? itemVariant) === 'ghost' && 'dgm-dropdown-item--ghost', 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)))] }) })] }) }));
25
+ }, children: [label && _jsx(Primitive.Label, { className: "dgm-dropdown-label", children: label }), _jsx(MenuItems, { primitive: Primitive, items: items, itemVariant: itemVariant, className: className })] }) })] }) }));
25
26
  }
@@ -0,0 +1,10 @@
1
+ import type * as DropdownPrimitive from '@radix-ui/react-dropdown-menu';
2
+ import type { DropdownMenuItem } from '../dropdown-menu.js';
3
+ type MenuPrimitive = Pick<typeof DropdownPrimitive, 'Item' | 'Separator' | 'Sub' | 'SubTrigger' | 'SubContent' | 'Portal'>;
4
+ export declare function MenuItems({ primitive: Primitive, items, itemVariant, className, }: {
5
+ primitive: MenuPrimitive;
6
+ items: readonly DropdownMenuItem[];
7
+ itemVariant: 'default' | 'ghost';
8
+ className?: string;
9
+ }): import("react").JSX.Element;
10
+ export {};
@@ -0,0 +1,46 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /** Mantiene idéntica la composición de ambos menús sin reemplazar la intención de puntero de Radix. */
3
+ import { useEffect, useRef, useState } from 'react';
4
+ import { cx } from '../utils.js';
5
+ import { menuItemFocusProps } from './menu-item-focus.js';
6
+ import { useOverlayLayer } from './overlay-layer.js';
7
+ export function MenuItems({ primitive: Primitive, items, itemVariant, className, }) {
8
+ return (_jsx(_Fragment, { children: items.map((item) => {
9
+ if (item.type === 'separator')
10
+ return _jsx(Primitive.Separator, { className: "dgm-dropdown-separator" }, item.id);
11
+ const itemClass = cx('dgm-dropdown-item', (item.variant ?? itemVariant) === 'ghost' && 'dgm-dropdown-item--ghost', item.danger && 'dgm-dropdown-item--danger');
12
+ const content = (_jsxs(_Fragment, { 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.type === 'submenu' ? (_jsx("svg", { className: "dgm-dropdown-chevron", width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", "aria-hidden": "true", children: _jsx("path", { d: "m9 6 6 6-6 6" }) })) : (item.shortcut && (_jsx("span", { className: "dgm-dropdown-shortcut", "aria-hidden": "true", children: item.shortcut })))] }));
13
+ if (item.type !== 'submenu')
14
+ return (_jsx(Primitive.Item, { textValue: item.label, disabled: item.disabled, onSelect: item.onSelect, className: itemClass, children: content }, item.id));
15
+ return (_jsx(Submenu, { primitive: Primitive, item: item, itemVariant: itemVariant, className: className, itemClass: itemClass, children: content }, item.id));
16
+ }) }));
17
+ }
18
+ /** Solo Escape se adapta: Radix cierra toda la raíz por defecto; aquí conserva los niveles padres. */
19
+ function Submenu({ primitive: Primitive, item, itemVariant, className, itemClass, children, }) {
20
+ const [open, setOpen] = useState(false);
21
+ const trigger = useRef(null);
22
+ const leaveTimer = useRef(undefined);
23
+ const cancelLeave = () => clearTimeout(leaveTimer.current);
24
+ // Radix conserva su corredor de intención entre paneles; el cierre exterior espera
25
+ // a que el puntero pueda entrar en cualquiera de las dos superficies.
26
+ const scheduleLeave = (event) => {
27
+ if (event.pointerType !== 'mouse')
28
+ return;
29
+ cancelLeave();
30
+ leaveTimer.current = setTimeout(() => setOpen(false), 400);
31
+ };
32
+ useEffect(() => cancelLeave, []);
33
+ const { style } = useOverlayLayer();
34
+ return (_jsxs(Primitive.Sub, { open: open, onOpenChange: (next) => {
35
+ cancelLeave();
36
+ setOpen(next);
37
+ }, children: [_jsx(Primitive.SubTrigger, { ref: trigger, onPointerEnter: cancelLeave, onPointerLeave: scheduleLeave, textValue: item.label, disabled: item.disabled, className: itemClass, children: children }), _jsx(Primitive.Portal, { children: _jsx(Primitive.SubContent, { ...menuItemFocusProps, onPointerEnter: cancelLeave, onPointerLeave: scheduleLeave, style: style, sideOffset: 6, collisionPadding: 12, className: cx('dgm-dropdown', className), onEscapeKeyDown: (event) => {
38
+ event.preventDefault();
39
+ cancelLeave();
40
+ setOpen(false);
41
+ trigger.current?.focus({ preventScroll: true });
42
+ }, onContextMenu: (event) => {
43
+ event.preventDefault();
44
+ event.stopPropagation();
45
+ }, children: _jsx(MenuItems, { primitive: Primitive, items: item.items, itemVariant: itemVariant, className: className }) }) })] }));
46
+ }
package/dist/styles.css CHANGED
@@ -1651,8 +1651,8 @@ button.dgm-step:hover:not(:disabled),
1651
1651
  .dgm-dropdown {
1652
1652
  box-sizing: border-box;
1653
1653
  z-index: calc(var(--dgm-layer-base) + var(--dgm-layer-depth, 0) * var(--dgm-layer-step) + 1);
1654
- min-width: min(12rem, calc(100vw - 1.5rem));
1655
- max-width: min(24rem, calc(100vw - 1.5rem));
1654
+ min-width: min(12rem, var(--radix-popper-available-width, calc(100vw - 1.5rem)));
1655
+ max-width: min(24rem, calc(100vw - 1.5rem), var(--radix-popper-available-width));
1656
1656
  max-height: var(--radix-dropdown-menu-content-available-height);
1657
1657
  overflow-y: auto;
1658
1658
  padding: var(--dgm-list-padding);
@@ -2191,6 +2191,14 @@ button.dgm-step:hover:not(:disabled),
2191
2191
  }
2192
2192
  }
2193
2193
 
2194
+ /* La flecha no reduce el texto de categorías largas ni cambia sus métricas al abrir. */
2195
+ .dgm-dropdown-chevron {
2196
+ flex-shrink: 0;
2197
+ }
2198
+ .dgm-dropdown-chevron:dir(rtl) {
2199
+ transform: rotate(180deg);
2200
+ }
2201
+
2194
2202
  /* Componentes compuestos: esta hoja se distribuye unida a styles.css, sin imports adicionales. */
2195
2203
  .dgm-visually-hidden,
2196
2204
  .dgm-form-value {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "danielgm-ui",
3
- "version": "0.4.5",
3
+ "version": "0.4.6",
4
4
  "description": "Accessible React components for Next.js and the web.",
5
5
  "repository": {
6
6
  "type": "git",