@griddo/ax 12.9.0 → 12.9.1-rc.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.
@@ -7,7 +7,7 @@ import Field from "../Field";
7
7
 
8
8
  import * as S from "./style";
9
9
 
10
- export const TemplateManager = (props: IProps): JSX.Element => {
10
+ export const TemplateManager = (props: IProps) => {
11
11
  const {
12
12
  template,
13
13
  selectedTab,
@@ -52,13 +52,8 @@ export const TemplateManager = (props: IProps): JSX.Element => {
52
52
  const isComputedField = Object.prototype.hasOwnProperty.call(field, "computed");
53
53
 
54
54
  const addedModules: string[] = modulesDataPacks.reduce((acc: string[], current: any) => {
55
- if (
56
- current.sectionList &&
57
- current.sectionList[template.component] &&
58
- current.sectionList[template.component].includes(key) &&
59
- !acc.includes(current.id)
60
- ) {
61
- return [...acc, current.id];
55
+ if (current.sectionList?.[template.component]?.includes(key) && !acc.includes(current.id)) {
56
+ acc.push(current.id);
62
57
  }
63
58
  return acc;
64
59
  }, []);
@@ -84,57 +79,56 @@ export const TemplateManager = (props: IProps): JSX.Element => {
84
79
  return (
85
80
  <>
86
81
  {isConfig && templateFields && !isForm && <S.Title>Template Options</S.Title>}
87
- {templateFields &&
88
- templateFields
89
- .filter((templateField: ISchemaField) => {
90
- const isHidden = templateField.hidden || (isForm && !templateField.overwrite);
91
- return !isHidden;
92
- })
93
- .map((templateField: ISchemaField, index: number) => {
94
- const {
95
- whiteList,
96
- categories,
97
- key,
98
- fieldObjKey,
99
- mappedField,
100
- currentContent,
101
- handleUpdate,
102
- error,
103
- readonly,
104
- disabledField,
105
- } = getFieldProps(templateField);
82
+ {templateFields
83
+ ?.filter((templateField: ISchemaField) => {
84
+ const isHidden = templateField.hidden || (isForm && !templateField.overwrite);
85
+ return !isHidden;
86
+ })
87
+ .map((templateField: ISchemaField, index: number) => {
88
+ const {
89
+ whiteList,
90
+ categories,
91
+ key,
92
+ fieldObjKey,
93
+ mappedField,
94
+ currentContent,
95
+ handleUpdate,
96
+ error,
97
+ readonly,
98
+ disabledField,
99
+ } = getFieldProps(templateField);
106
100
 
107
- return (
108
- <Field
109
- whiteList={whiteList}
110
- key={`${objKey}.${key}${index}`}
111
- objKey={fieldObjKey}
112
- field={mappedField}
113
- selectedContent={currentContent}
114
- goTo={goTo}
115
- updateValue={handleUpdate}
116
- pages={pages}
117
- actions={actions}
118
- site={site}
119
- disabled={disabled || disabledField}
120
- activatedModules={activatedModules}
121
- isTemplateActivated={isTemplateActivated}
122
- categories={categories}
123
- error={error}
124
- deleteError={deleteError}
125
- errors={errors}
126
- theme={theme}
127
- moduleCopy={moduleCopy}
128
- availableDataPacks={availableDataPacks}
129
- template={template}
130
- setHistoryPush={setHistoryPush}
131
- lang={lang}
132
- readonly={readonly}
133
- scrollEditorID={scrollEditorID}
134
- languages={languages}
135
- />
136
- );
137
- })}
101
+ return (
102
+ <Field
103
+ whiteList={whiteList}
104
+ key={`${objKey}.${key}${index}`}
105
+ objKey={fieldObjKey}
106
+ field={mappedField}
107
+ selectedContent={currentContent}
108
+ goTo={goTo}
109
+ updateValue={handleUpdate}
110
+ pages={pages}
111
+ actions={actions}
112
+ site={site}
113
+ disabled={disabled || disabledField}
114
+ activatedModules={activatedModules}
115
+ isTemplateActivated={isTemplateActivated}
116
+ categories={categories}
117
+ error={error}
118
+ deleteError={deleteError}
119
+ errors={errors}
120
+ theme={theme}
121
+ moduleCopy={moduleCopy}
122
+ availableDataPacks={availableDataPacks}
123
+ template={template}
124
+ setHistoryPush={setHistoryPush}
125
+ lang={lang}
126
+ readonly={readonly}
127
+ scrollEditorID={scrollEditorID}
128
+ languages={languages}
129
+ />
130
+ );
131
+ })}
138
132
  </>
139
133
  );
140
134
  };
@@ -0,0 +1,515 @@
1
+ # Keyboard Shortcuts
2
+
3
+ Sistema declarativo de atajos de teclado para Griddo AX. Registra shortcuts en componentes React con cleanup automático, soporte de secuencias (chords) y resolución de conflictos por prioridad.
4
+
5
+ ```
6
+ import { useShortcuts, shortcut } from "@ax/hooks";
7
+ ```
8
+
9
+ ---
10
+
11
+ ## Tabla de contenidos
12
+
13
+ 1. [Inicio rápido](#inicio-rápido)
14
+ 2. [Conceptos clave](#conceptos-clave)
15
+ 3. [API base: `useShortcuts` + `shortcut`](#api-base)
16
+ 4. [Presets](#presets)
17
+ 5. [Global shortcuts (safety net)](#global-shortcuts)
18
+ 6. [Prioridad](#prioridad)
19
+ 7. [Secuencias de teclas (chords)](#secuencias-de-teclas-chords)
20
+ 8. [Scoped shortcuts (ref)](#scoped-shortcuts)
21
+ 9. [Iframe bridge](#iframe-bridge)
22
+ 10. [Comportamiento en inputs](#comportamiento-en-inputs)
23
+ 11. [Arquitectura](#arquitectura)
24
+ 12. [Referencia de archivos](#referencia-de-archivos)
25
+ 13. [Recetas](#recetas)
26
+
27
+ ---
28
+
29
+ ## Inicio rápido
30
+
31
+ ```tsx
32
+ import { useSaveShortcut } from "@ax/hooks";
33
+
34
+ function MyEditor() {
35
+ const canSave = /* ... */;
36
+
37
+ useSaveShortcut(() => {
38
+ saveDocument();
39
+ }, canSave);
40
+
41
+ return <div>...</div>;
42
+ }
43
+ ```
44
+
45
+ Eso es todo. `mod+s` queda registrado, funciona dentro de inputs, y se desregistra automáticamente al desmontar el componente.
46
+
47
+ ---
48
+
49
+ ## Conceptos clave
50
+
51
+ ### `mod` = Cmd (Mac) / Ctrl (Windows/Linux)
52
+
53
+ Usa `"mod"` en vez de `"ctrl"` o `"meta"`. El engine detecta la plataforma automáticamente.
54
+
55
+ ### Cleanup automático
56
+
57
+ Los shortcuts se desregistran cuando el componente se desmonta o cuando cambian las dependencias del hook. No necesitas hacer cleanup manual.
58
+
59
+ ### Prioridad
60
+
61
+ Cuando dos shortcuts compiten por la misma combinación de teclas, gana el de mayor prioridad. Si un handler retorna `false`, el engine pasa al siguiente en la cola.
62
+
63
+ ---
64
+
65
+ ## API base
66
+
67
+ ### `shortcut(id, keys, handler, overrides?)`
68
+
69
+ Crea un objeto `ShortcutConfig`. No registra nada por sí solo — es solo un builder.
70
+
71
+ ```tsx
72
+ shortcut("save", "mod+s", () => handleSave());
73
+ shortcut("delete", "del", () => handleDelete());
74
+ shortcut("find", "mod+shift+f", () => openSearch());
75
+ shortcut("goto-list", "g l", () => navigateToList()); // chord: pulsa g, luego l
76
+ ```
77
+
78
+ **Parámetros:**
79
+
80
+ | Param | Tipo | Descripción |
81
+ | ----------- | ----------------------------------------- | -------------------------------------------------------- |
82
+ | `id` | `string` | Identificador único (ej. `"save"`, `"close:modal"`) |
83
+ | `keys` | `string` | Combinación de teclas (ver [sintaxis](#sintaxis-de-keys)) |
84
+ | `handler` | `(event: KeyboardEvent) => void \| false` | Callback. Retorna `false` para no hacer `preventDefault` |
85
+ | `overrides` | `object` (opcional) | Ver tabla abajo |
86
+
87
+ **Overrides:**
88
+
89
+ | Campo | Tipo | Default | Descripción |
90
+ | -------------- | --------- | ----------- | -------------------------------------------------------- |
91
+ | `allowInInput` | `boolean` | `false` | Si `true`, funciona dentro de `<input>`, `<textarea>`, etc |
92
+ | `priority` | `number` | `0` | Mayor = más prioridad en conflictos |
93
+ | `enabled` | `boolean` | `true` | `false` desactiva sin desregistrar |
94
+ | `description` | `string` | `undefined` | Texto descriptivo (para debug o UI de shortcuts) |
95
+
96
+ ### Sintaxis de keys
97
+
98
+ ```
99
+ "mod+s" → Cmd+S / Ctrl+S
100
+ "shift+alt+n" → Shift+Alt+N
101
+ "escape" → Escape
102
+ "del" → Delete
103
+ "mod+shift+z" → Cmd+Shift+Z / Ctrl+Shift+Z
104
+ "g l" → Chord: pulsa G, luego L (ver sección de chords)
105
+ ```
106
+
107
+ Modificadores disponibles: `mod`, `shift`, `alt`.
108
+
109
+ ---
110
+
111
+ ### `useShortcuts(shortcuts, options?)`
112
+
113
+ Registra un array de shortcuts en el engine. Es la API de bajo nivel.
114
+
115
+ ```tsx
116
+ useShortcuts([
117
+ shortcut("bold", "mod+b", () => toggleBold()),
118
+ shortcut("italic", "mod+i", () => toggleItalic()),
119
+ shortcut("underline", "mod+u", () => toggleUnderline()),
120
+ ]);
121
+ ```
122
+
123
+ **Options:**
124
+
125
+ | Campo | Tipo | Default | Descripción |
126
+ | --------- | ----------------------- | ---------- | ------------------------------------------------- |
127
+ | `enabled` | `boolean` | `true` | Desregistra todos los shortcuts cuando es `false` |
128
+ | `ref` | `RefObject<HTMLElement>` | `window` | Elemento donde escuchar keydown (ver [ref](#scoped-shortcuts)) |
129
+
130
+ > **Nota sobre `enabled`:** Cuando es `false`, los shortcuts se **desregistran por completo**.
131
+ > Esto significa que el browser default no se previene. Si necesitas bloquear el browser
132
+ > default incluso cuando el shortcut no está activo (ej. `mod+s` → Save As), usa
133
+ > `useGlobalShortcuts()` en el root (ver [Global shortcuts](#global-shortcuts)).
134
+
135
+ ---
136
+
137
+ ## Presets
138
+
139
+ Hooks preconfigurados para shortcuts comunes. Encapsulan la combinación de teclas, el id y las opciones — solo pasas el handler.
140
+
141
+ ### `useSaveShortcut(handler, enabled?)`
142
+
143
+ ```tsx
144
+ useSaveShortcut(() => {
145
+ isPublished ? publishChanges() : saveDraft();
146
+ }, canSave);
147
+ ```
148
+
149
+ - Tecla: `mod+s`
150
+ - `allowInInput`: siempre `true`
151
+ - Id: `"save"`
152
+
153
+ ### `useEscapeShortcut(handler, enabled?)`
154
+
155
+ ```tsx
156
+ useEscapeShortcut(() => {
157
+ closePanel();
158
+ }, isPanelOpen);
159
+ ```
160
+
161
+ - Tecla: `Escape`
162
+ - `allowInInput`: siempre `true`
163
+ - Id: `"escape"`
164
+
165
+ ### `useUndoRedoShortcuts(onUndo, onRedo, enabled?)`
166
+
167
+ ```tsx
168
+ useUndoRedoShortcuts(
169
+ () => dispatch(undo()),
170
+ () => dispatch(redo()),
171
+ isEditing,
172
+ );
173
+ ```
174
+
175
+ - Teclas: `mod+z` / `mod+shift+z`
176
+ - `allowInInput`: siempre `true`
177
+ - Ids: `"undo"`, `"redo"`
178
+
179
+ ---
180
+
181
+ ## Global shortcuts
182
+
183
+ `useGlobalShortcuts()` es un safety net que se monta **una vez en el root de la app**
184
+ (`App/index.tsx`). Registra shortcuts con prioridad baja (`-1`) que solo hacen
185
+ `preventDefault` para bloquear defaults del navegador.
186
+
187
+ ```tsx
188
+ // App/index.tsx — ya está configurado
189
+ const App = (props: IProps) => {
190
+ useGlobalShortcuts();
191
+ // ...
192
+ };
193
+ ```
194
+
195
+ ### Cómo funciona
196
+
197
+ ```
198
+ ┌─────────────────────────────────────────────────────────┐
199
+ │ Usuario pulsa Cmd+S │
200
+ │ │
201
+ │ ┌─ ShortcutEngine (prioridad descendente) ───────────┐ │
202
+ │ │ │ │
203
+ │ │ 1. useSaveShortcut (priority: 0) ← si está │ │
204
+ │ │ montado, ejecuta el handler del componente │ │
205
+ │ │ │ │
206
+ │ │ 2. useGlobalShortcuts (priority: -1) ← fallback │ │
207
+ │ │ si ningún componente lo maneja, solo hace │ │
208
+ │ │ preventDefault (bloquea Save As) │ │
209
+ │ │ │ │
210
+ │ └─────────────────────────────────────────────────────┘ │
211
+ └─────────────────────────────────────────────────────────┘
212
+ ```
213
+
214
+ ### Escenarios
215
+
216
+ | Ruta/estado | `mod+s` lo maneja... | Resultado |
217
+ | ---------------------- | ------------------------ | ------------------------ |
218
+ | PageEditor montado | `useSaveShortcut` (p:0) | Guarda la página |
219
+ | PageEditor con `enabled=false` | `useGlobalShortcuts` (p:-1) | Bloquea Save As, no hace nada más |
220
+ | Cualquier otra ruta | `useGlobalShortcuts` (p:-1) | Bloquea Save As |
221
+
222
+ ---
223
+
224
+ ## Prioridad
225
+
226
+ El engine despacha shortcuts por **prioridad descendente**. El primero que matchea gana y hace `preventDefault` + `stopPropagation`. Si su handler retorna `false`, el engine continúa al siguiente.
227
+
228
+ ```tsx
229
+ // Este shortcut SIEMPRE gana sobre otro "mod+s" con priority < 10
230
+ shortcut("save:critical", "mod+s", handler, { priority: 10 });
231
+ ```
232
+
233
+ **Valores de referencia:**
234
+
235
+ | Nivel | Valor | Uso |
236
+ | ---------- | ----- | ---------------------------------------- |
237
+ | Global | `-1` | Safety nets (ej. `useGlobalShortcuts`) |
238
+ | Default | `0` | Shortcuts normales de componente |
239
+ | Override | `> 0` | Modales, overlays que deben ganar siempre |
240
+
241
+ ### Ejemplo: modal que captura Escape
242
+
243
+ ```tsx
244
+ function ConfirmModal({ isOpen, onClose }) {
245
+ // priority: 10 — gana sobre cualquier otro Escape
246
+ useShortcuts(
247
+ [shortcut("close:confirm-modal", "escape", () => onClose(), { priority: 10 })],
248
+ { enabled: isOpen },
249
+ );
250
+
251
+ return isOpen ? <div>...</div> : null;
252
+ }
253
+ ```
254
+
255
+ ---
256
+
257
+ ## Secuencias de teclas (chords)
258
+
259
+ Un chord es una secuencia de teclas que se pulsan una tras otra (no simultáneamente). Se definen separando combinaciones con espacio:
260
+
261
+ ```tsx
262
+ shortcut("goto:list", "g l", () => navigateTo("/list"));
263
+ // Pulsa G, luego L (en menos de 1 segundo)
264
+
265
+ shortcut("goto:settings", "g s", () => navigateTo("/settings"));
266
+ // Pulsa G, luego S
267
+ ```
268
+
269
+ ### Cómo funciona
270
+
271
+ 1. Pulsas `G` → el engine detecta que hay un chord en progreso y hace `preventDefault`
272
+ 2. Tienes **1 segundo** para pulsar `L`
273
+ 3. Si pulsas `L` a tiempo → se ejecuta el handler
274
+ 4. Si pulsas otra tecla o pasa el timeout → el chord se cancela
275
+
276
+ ### Chords con modificadores
277
+
278
+ ```tsx
279
+ shortcut("debug:panel", "mod+k mod+d", () => openDebugPanel());
280
+ // Cmd+K, luego Cmd+D
281
+ ```
282
+
283
+ ---
284
+
285
+ ## Scoped shortcuts
286
+
287
+ Por defecto, los shortcuts escuchan en `window`. Puedes limitarlos a un elemento específico con `ref`:
288
+
289
+ ```tsx
290
+ function TextEditor() {
291
+ const editorRef = useRef<HTMLDivElement>(null);
292
+
293
+ useShortcuts(
294
+ [
295
+ shortcut("bold", "mod+b", () => toggleBold()),
296
+ shortcut("italic", "mod+i", () => toggleItalic()),
297
+ ],
298
+ { ref: editorRef },
299
+ );
300
+
301
+ return <div ref={editorRef} tabIndex={0}>...</div>;
302
+ }
303
+ ```
304
+
305
+ Los shortcuts con ref solo se disparan cuando el evento se origina dentro de ese elemento. Gracias a `stopPropagation`, si el scoped shortcut matchea, no llega a los shortcuts globales en `window`.
306
+
307
+ ---
308
+
309
+ ## Iframe bridge
310
+
311
+ Los eventos `keydown` dentro de un `<iframe>` no llegan al `window` del padre — el browser los confina al documento del iframe. Esto significa que shortcuts como `mod+s` no se disparan si el foco está en el iframe de preview.
312
+
313
+ Para resolverlo, el sistema usa un puente de dos hooks que conectan el iframe con el engine del padre vía `postMessage`:
314
+
315
+ ### `useForwardShortcutKeys()` (lado iframe)
316
+
317
+ Se usa dentro del iframe (ej. `FramePreview`). Escucha `keydown` y reenvía los eventos relevantes al padre.
318
+
319
+ ```tsx
320
+ // FramePreview/index.tsx
321
+ import { useForwardShortcutKeys } from "@ax/hooks";
322
+
323
+ const FramePreview = () => {
324
+ useForwardShortcutKeys();
325
+ // ...
326
+ };
327
+ ```
328
+
329
+ **Qué reenvía:** Eventos con `metaKey`/`ctrlKey` o `Escape`.
330
+
331
+ **Qué no reenvía:** Shortcuts de clipboard (`mod+c`, `mod+v`, `mod+x`, `mod+a`) para que funcionen normalmente en el iframe.
332
+
333
+ ### `useIframeShortcutBridge()` (lado padre)
334
+
335
+ Se usa una vez en el root de la app, junto a `useGlobalShortcuts()`. Escucha los mensajes del iframe y re-despacha un `KeyboardEvent` sintético en `window`, donde el `ShortcutEngine` lo procesa normalmente.
336
+
337
+ ```tsx
338
+ // App/index.tsx — ya está configurado
339
+ const App = (props: IProps) => {
340
+ useGlobalShortcuts();
341
+ useIframeShortcutBridge();
342
+ // ...
343
+ };
344
+ ```
345
+
346
+ ### Flujo
347
+
348
+ ```
349
+ ┌─────────────────────────────────────────────────────────────┐
350
+ │ Usuario pulsa Cmd+S dentro del iframe │
351
+ │ │
352
+ │ ┌─ Iframe (FramePreview) ──────────────────────────────┐ │
353
+ │ │ useForwardShortcutKeys() │ │
354
+ │ │ → captura keydown │ │
355
+ │ │ → preventDefault (bloquea Save As en el iframe) │ │
356
+ │ │ → postMessage({ type: "shortcut-keydown", ... }) │ │
357
+ │ └───────────────────────────────────────────────────────┘ │
358
+ │ │ │
359
+ │ ▼ │
360
+ │ ┌─ Parent (App) ────────────────────────────────────────┐ │
361
+ │ │ useIframeShortcutBridge() │ │
362
+ │ │ → recibe message │ │
363
+ │ │ → window.dispatchEvent(new KeyboardEvent(...)) │ │
364
+ │ │ │ │
365
+ │ │ ShortcutEngine │ │
366
+ │ │ → procesa el evento como cualquier otro keydown │ │
367
+ │ │ → useSaveShortcut handler se ejecuta │ │
368
+ │ └───────────────────────────────────────────────────────┘ │
369
+ └─────────────────────────────────────────────────────────────┘
370
+ ```
371
+
372
+ ---
373
+
374
+ ## Comportamiento en inputs
375
+
376
+ Por defecto, los shortcuts **no se disparan** cuando el foco está en:
377
+
378
+ - `<input>`
379
+ - `<textarea>`
380
+ - `<select>`
381
+ - Elementos con `contentEditable`
382
+
383
+ **Excepciones:**
384
+
385
+ 1. **`allowInInput: true`** — El shortcut funciona en cualquier contexto. Todos los presets lo activan.
386
+ 2. **`Escape`** — Siempre funciona en inputs, incluso sin `allowInInput`. Es una decisión de UX: Escape se usa para cerrar/cancelar y nunca produce texto.
387
+
388
+ ```tsx
389
+ // Funciona siempre, incluso dentro de un input
390
+ shortcut("save", "mod+s", handler, { allowInInput: true });
391
+
392
+ // Solo funciona fuera de inputs
393
+ shortcut("delete", "del", handler);
394
+
395
+ // Escape siempre funciona (caso especial del engine)
396
+ shortcut("close", "escape", handler);
397
+ ```
398
+
399
+ ---
400
+
401
+ ## Arquitectura
402
+
403
+ ```
404
+ ┌──────────────────────────────────────────────────────────────┐
405
+ │ Componente React │
406
+ │ │
407
+ │ useShortcuts([...]) ← Hook (registra/desregistra) │
408
+ │ │ │
409
+ │ │ usa ref-based handlers para evitar stale closures │
410
+ │ ▼ │
411
+ │ shortcutEngine.register(config, target) │
412
+ │ │ │
413
+ │ │ un solo listener `keydown` por target │
414
+ │ ▼ │
415
+ │ ┌─ ShortcutEngine ──────────────────────────────────────┐ │
416
+ │ │ │ │
417
+ │ │ keydown event │ │
418
+ │ │ → recopila shortcuts del target │ │
419
+ │ │ → ordena por prioridad (desc) │ │
420
+ │ │ → itera: el primero que matchea, gana │ │
421
+ │ │ → execute(): handler() + preventDefault + stopProp │ │
422
+ │ │ → si handler retorna false: continúa al siguiente │ │
423
+ │ │ │ │
424
+ │ └────────────────────────────────────────────────────────┘ │
425
+ └──────────────────────────────────────────────────────────────┘
426
+ ```
427
+
428
+ ### Un solo listener por target
429
+
430
+ El engine **no** crea un `addEventListener` por shortcut. Mantiene un solo listener por cada target (`window`, o un ref de elemento). Cuando llega un `keydown`, recorre los shortcuts registrados en ese target ordenados por prioridad.
431
+
432
+ Esto garantiza que:
433
+
434
+ - La resolución de prioridad funciona correctamente
435
+ - No hay race conditions entre listeners independientes
436
+ - El cleanup es eficiente (el listener se remueve solo cuando el último shortcut del target se desregistra)
437
+
438
+ ---
439
+
440
+ ## Referencia de archivos
441
+
442
+ | Archivo | Responsabilidad |
443
+ | ------------------ | ------------------------------------------------------------ |
444
+ | `types.ts` | Tipos: `KeyCombo`, `ModifierKey`, `ShortcutConfig`, `UseShortcutsOptions` |
445
+ | `engine.ts` | `ShortcutEngine` (singleton), `parseShortcut`, matching de teclas |
446
+ | `useShortcuts.ts` | Hook base `useShortcuts` + builder `shortcut()` |
447
+ | `presets.ts` | Hooks preconfigurados: `useSaveShortcut`, `useEscapeShortcut`, `useUndoRedoShortcuts`, `useGlobalShortcuts`, `useIframeShortcutBridge`, `useForwardShortcutKeys` — lado iframe del bridge |
448
+ | `index.ts` | Barrel export |
449
+
450
+ ---
451
+
452
+ ## Recetas
453
+
454
+ ### Shortcut condicional
455
+
456
+ ```tsx
457
+ const isAdmin = usePermission("admin");
458
+
459
+ useShortcuts([
460
+ shortcut("admin:panel", "mod+shift+a", () => openAdminPanel()),
461
+ ], { enabled: isAdmin });
462
+ ```
463
+
464
+ ### Múltiples shortcuts en un componente
465
+
466
+ ```tsx
467
+ useShortcuts([
468
+ shortcut("save", "mod+s", handleSave, { allowInInput: true }),
469
+ shortcut("close", "escape", handleClose),
470
+ shortcut("search", "mod+k", handleSearch, { allowInInput: true }),
471
+ shortcut("delete", "del", handleDelete),
472
+ ]);
473
+ ```
474
+
475
+ ### Shortcut que no previene el default del browser
476
+
477
+ Si necesitas que el browser siga procesando la tecla después de tu handler, retorna `false`:
478
+
479
+ ```tsx
480
+ shortcut("log-typing", "mod+a", (event) => {
481
+ analytics.track("select-all");
482
+ return false; // deja que el browser haga Select All
483
+ });
484
+ ```
485
+
486
+ ### Combinar preset + shortcuts custom
487
+
488
+ ```tsx
489
+ function PageEditor() {
490
+ useSaveShortcut(() => savePage(), canSave);
491
+ useEscapeShortcut(() => closePanel(), isPanelOpen);
492
+
493
+ // Shortcuts adicionales específicos de este componente
494
+ useShortcuts([
495
+ shortcut("preview", "mod+shift+p", () => togglePreview()),
496
+ shortcut("publish", "mod+shift+enter", () => publish()),
497
+ ]);
498
+ }
499
+ ```
500
+
501
+ ### Modal con Escape de alta prioridad
502
+
503
+ ```tsx
504
+ function DeleteConfirmModal({ isOpen, onConfirm, onCancel }) {
505
+ useShortcuts(
506
+ [
507
+ shortcut("cancel:delete", "escape", () => onCancel(), { priority: 10 }),
508
+ shortcut("confirm:delete", "enter", () => onConfirm(), { priority: 10 }),
509
+ ],
510
+ { enabled: isOpen },
511
+ );
512
+
513
+ // ...
514
+ }
515
+ ```