@fragments-sdk/ui 0.9.7 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +32 -24
  2. package/dist/assets/ui.css +304 -0
  3. package/dist/blocks/BlogEditor.block.d.ts +3 -0
  4. package/dist/blocks/BlogEditor.block.d.ts.map +1 -0
  5. package/dist/components/Editor/Editor.module.scss.cjs +57 -0
  6. package/dist/components/Editor/Editor.module.scss.cjs.map +1 -0
  7. package/dist/components/Editor/Editor.module.scss.js +57 -0
  8. package/dist/components/Editor/Editor.module.scss.js.map +1 -0
  9. package/dist/components/Editor/index.cjs +548 -0
  10. package/dist/components/Editor/index.cjs.map +1 -0
  11. package/dist/components/Editor/index.d.ts +107 -0
  12. package/dist/components/Editor/index.d.ts.map +1 -0
  13. package/dist/components/Editor/index.js +531 -0
  14. package/dist/components/Editor/index.js.map +1 -0
  15. package/dist/components/Sidebar/index.cjs +6 -11
  16. package/dist/components/Sidebar/index.cjs.map +1 -1
  17. package/dist/components/Sidebar/index.d.ts.map +1 -1
  18. package/dist/components/Sidebar/index.js +6 -11
  19. package/dist/components/Sidebar/index.js.map +1 -1
  20. package/dist/components/Theme/index.cjs +86 -1
  21. package/dist/components/Theme/index.cjs.map +1 -1
  22. package/dist/components/Theme/index.d.ts +44 -1
  23. package/dist/components/Theme/index.d.ts.map +1 -1
  24. package/dist/components/Theme/index.js +86 -1
  25. package/dist/components/Theme/index.js.map +1 -1
  26. package/dist/index.cjs +24 -0
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.d.ts +3 -1
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +25 -1
  31. package/dist/index.js.map +1 -1
  32. package/dist/utils/keyboard-shortcuts.cjs +295 -0
  33. package/dist/utils/keyboard-shortcuts.cjs.map +1 -0
  34. package/dist/utils/keyboard-shortcuts.d.ts +293 -0
  35. package/dist/utils/keyboard-shortcuts.d.ts.map +1 -0
  36. package/dist/utils/keyboard-shortcuts.js +295 -0
  37. package/dist/utils/keyboard-shortcuts.js.map +1 -0
  38. package/fragments.json +1 -1
  39. package/package.json +32 -3
  40. package/src/blocks/BlogEditor.block.ts +34 -0
  41. package/src/components/Editor/Editor.fragment.tsx +322 -0
  42. package/src/components/Editor/Editor.module.scss +333 -0
  43. package/src/components/Editor/Editor.test.tsx +174 -0
  44. package/src/components/Editor/index.tsx +815 -0
  45. package/src/components/Sidebar/index.tsx +7 -14
  46. package/src/components/Theme/index.tsx +168 -1
  47. package/src/index.ts +49 -0
  48. package/src/tokens/_seeds.scss +20 -0
  49. package/src/utils/keyboard-shortcuts.test.ts +357 -0
  50. package/src/utils/keyboard-shortcuts.ts +502 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keyboard-shortcuts.cjs","sources":["../../src/utils/keyboard-shortcuts.ts"],"sourcesContent":["'use client';\n\n// ============================================\n// Keyboard Shortcuts Registry\n// ============================================\n//\n// Central source of truth for all keyboard shortcuts in @fragments-sdk/ui.\n// Import from here instead of hardcoding key combinations in components.\n//\n// This prevents conflicts (e.g., Sidebar Ctrl+B vs Editor Ctrl+B) and\n// makes shortcuts discoverable for documentation and customization.\n//\n// ADDING A SHORTCUT:\n// 1. Add an entry to KEYBOARD_SHORTCUTS below\n// 2. Import the constant in your component\n// 3. Use useKeyboardShortcut() or matchesShortcut() in your component\n//\n// CUSTOMIZING SHORTCUTS (consumer API):\n// configureShortcuts({ SIDEBAR_TOGGLE: { key: '\\\\', label: 'Ctrl+\\\\' } })\n// configureShortcuts({ SIDEBAR_TOGGLE: null }) // disable\n//\n\nimport { useEffect, useRef, type RefObject } from 'react';\n\n// ============================================\n// Types\n// ============================================\n\nexport interface KeyboardShortcut {\n /** The key to match (e.g., 'b', 'k', 'Escape'). Case-insensitive. */\n key: string;\n /** Require Ctrl (Windows/Linux) or Cmd (macOS) */\n meta?: boolean;\n /** Require Shift */\n shift?: boolean;\n /** Require Alt/Option */\n alt?: boolean;\n /** Human-readable label for display (e.g., \"Ctrl+B\", \"⌘B\") */\n label: string;\n /** Which component owns this shortcut */\n component: string;\n /** What the shortcut does */\n description: string;\n /**\n * Scope controls when the shortcut is active:\n * - 'global': Listens on document (e.g., sidebar toggle)\n * - 'component': Only active when component is focused/mounted\n */\n scope: 'global' | 'component';\n}\n\n// ============================================\n// Shortcut Definitions\n// ============================================\n\nexport const KEYBOARD_SHORTCUTS = {\n // ----- Sidebar -----\n SIDEBAR_TOGGLE: {\n key: 'b',\n meta: true,\n label: 'Ctrl+B',\n component: 'Sidebar',\n description: 'Toggle sidebar collapse/expand',\n scope: 'global',\n },\n SIDEBAR_CLOSE_MOBILE: {\n key: 'Escape',\n label: 'Escape',\n component: 'Sidebar',\n description: 'Close mobile sidebar drawer',\n scope: 'global',\n },\n\n // ----- Editor (TipTap handles these natively, metadata for display) -----\n EDITOR_BOLD: {\n key: 'b',\n meta: true,\n label: 'Ctrl+B',\n component: 'Editor',\n description: 'Toggle bold formatting',\n scope: 'component',\n },\n EDITOR_ITALIC: {\n key: 'i',\n meta: true,\n label: 'Ctrl+I',\n component: 'Editor',\n description: 'Toggle italic formatting',\n scope: 'component',\n },\n EDITOR_STRIKETHROUGH: {\n key: 's',\n meta: true,\n shift: true,\n label: 'Ctrl+Shift+S',\n component: 'Editor',\n description: 'Toggle strikethrough formatting',\n scope: 'component',\n },\n EDITOR_LINK: {\n key: 'k',\n meta: true,\n label: 'Ctrl+K',\n component: 'Editor',\n description: 'Insert or edit link',\n scope: 'component',\n },\n EDITOR_CODE: {\n key: 'e',\n meta: true,\n label: 'Ctrl+E',\n component: 'Editor',\n description: 'Toggle inline code',\n scope: 'component',\n },\n EDITOR_BULLET_LIST: {\n key: '8',\n meta: true,\n shift: true,\n label: 'Ctrl+Shift+8',\n component: 'Editor',\n description: 'Toggle bullet list',\n scope: 'component',\n },\n EDITOR_ORDERED_LIST: {\n key: '7',\n meta: true,\n shift: true,\n label: 'Ctrl+Shift+7',\n component: 'Editor',\n description: 'Toggle ordered list',\n scope: 'component',\n },\n EDITOR_HEADING1: {\n key: '1',\n meta: true,\n alt: true,\n label: 'Ctrl+Alt+1',\n component: 'Editor',\n description: 'Toggle heading level 1',\n scope: 'component',\n },\n EDITOR_HEADING2: {\n key: '2',\n meta: true,\n alt: true,\n label: 'Ctrl+Alt+2',\n component: 'Editor',\n description: 'Toggle heading level 2',\n scope: 'component',\n },\n EDITOR_HEADING3: {\n key: '3',\n meta: true,\n alt: true,\n label: 'Ctrl+Alt+3',\n component: 'Editor',\n description: 'Toggle heading level 3',\n scope: 'component',\n },\n EDITOR_BLOCKQUOTE: {\n key: 'b',\n meta: true,\n shift: true,\n label: 'Ctrl+Shift+B',\n component: 'Editor',\n description: 'Toggle blockquote',\n scope: 'component',\n },\n EDITOR_UNDO: {\n key: 'z',\n meta: true,\n label: 'Ctrl+Z',\n component: 'Editor',\n description: 'Undo last action',\n scope: 'component',\n },\n EDITOR_REDO: {\n key: 'z',\n meta: true,\n shift: true,\n label: 'Ctrl+Shift+Z',\n component: 'Editor',\n description: 'Redo last undone action',\n scope: 'component',\n },\n\n // ----- Prompt -----\n PROMPT_SUBMIT: {\n key: 'Enter',\n label: 'Enter',\n component: 'Prompt',\n description: 'Submit prompt (when submitOnEnter is true)',\n scope: 'component',\n },\n\n // ----- NavigationMenu -----\n NAV_TOGGLE: {\n key: 'Enter',\n label: 'Enter',\n component: 'NavigationMenu',\n description: 'Toggle menu item open/closed',\n scope: 'component',\n },\n NAV_CLOSE: {\n key: 'Escape',\n label: 'Escape',\n component: 'NavigationMenu',\n description: 'Close menu and return focus to trigger',\n scope: 'component',\n },\n\n // ----- Command -----\n COMMAND_SELECT: {\n key: 'Enter',\n label: 'Enter',\n component: 'Command',\n description: 'Select active command item',\n scope: 'component',\n },\n\n // ----- Collapsible -----\n COLLAPSIBLE_TOGGLE: {\n key: 'Enter',\n label: 'Enter',\n component: 'Collapsible',\n description: 'Toggle collapsible open/closed',\n scope: 'component',\n },\n} as const satisfies Record<string, KeyboardShortcut>;\n\nexport type ShortcutName = keyof typeof KEYBOARD_SHORTCUTS;\n\n// ============================================\n// Helpers\n// ============================================\n\n/**\n * Check if a KeyboardEvent matches a shortcut definition.\n *\n * Usage:\n * ```ts\n * import { KEYBOARD_SHORTCUTS, matchesShortcut } from '../../utils/keyboard-shortcuts';\n *\n * const handleKeyDown = (e: KeyboardEvent) => {\n * if (matchesShortcut(e, KEYBOARD_SHORTCUTS.SIDEBAR_TOGGLE)) {\n * e.preventDefault();\n * toggleSidebar();\n * }\n * };\n * ```\n */\nexport function matchesShortcut(event: KeyboardEvent, shortcut: KeyboardShortcut): boolean {\n if (shortcut.meta && !(event.metaKey || event.ctrlKey)) return false;\n if (shortcut.shift && !event.shiftKey) return false;\n if (shortcut.alt && !event.altKey) return false;\n\n // For modifier-only checks, ensure no extra modifiers are pressed\n if (!shortcut.meta && (event.metaKey || event.ctrlKey)) return false;\n if (!shortcut.shift && event.shiftKey) return false;\n if (!shortcut.alt && event.altKey) return false;\n\n return event.key.toLowerCase() === shortcut.key.toLowerCase();\n}\n\n/**\n * Get a human-readable shortcut label, adapting for macOS vs other platforms.\n * Returns \"⌘B\" on Mac, \"Ctrl+B\" elsewhere.\n */\nexport function getShortcutLabel(shortcut: KeyboardShortcut): string {\n if (typeof navigator === 'undefined') return shortcut.label;\n\n const isMac = navigator.platform?.includes('Mac') || navigator.userAgent?.includes('Mac');\n if (!isMac) return shortcut.label;\n\n const parts: string[] = [];\n if (shortcut.meta) parts.push('⌘');\n if (shortcut.shift) parts.push('⇧');\n if (shortcut.alt) parts.push('⌥');\n parts.push(shortcut.key.toUpperCase());\n return parts.join('');\n}\n\n/**\n * Find all shortcuts that conflict with a given shortcut (same key combo, different component).\n * Useful for debugging and documentation.\n */\nexport function findConflicts(name: ShortcutName): KeyboardShortcut[] {\n const target = KEYBOARD_SHORTCUTS[name] as KeyboardShortcut;\n return (Object.entries(KEYBOARD_SHORTCUTS) as [string, KeyboardShortcut][])\n .filter(([key, shortcut]) => {\n if (key === name) return false;\n return (\n shortcut.key.toLowerCase() === target.key.toLowerCase() &&\n !!shortcut.meta === !!target.meta &&\n !!shortcut.shift === !!target.shift &&\n !!shortcut.alt === !!target.alt\n );\n })\n .map(([, shortcut]) => shortcut);\n}\n\n/**\n * Get all registered shortcuts, optionally filtered by component or scope.\n */\nexport function getShortcuts(filter?: { component?: string; scope?: 'global' | 'component' }): KeyboardShortcut[] {\n return (Object.values(KEYBOARD_SHORTCUTS) as KeyboardShortcut[]).filter((shortcut) => {\n if (filter?.component && shortcut.component !== filter.component) return false;\n if (filter?.scope && shortcut.scope !== filter.scope) return false;\n return true;\n });\n}\n\n// ============================================\n// Editable Element Detection\n// ============================================\n\n/** Text-like input types where typing shortcuts should not fire global handlers */\nconst TEXT_INPUT_TYPES = new Set([\n 'text', 'search', 'url', 'tel', 'email', 'password', 'number',\n]);\n\n/**\n * Check if an element is an editable area (input, textarea, contenteditable, role=\"textbox\").\n * Global shortcuts should skip firing when the user is typing in one of these.\n */\nexport function isEditableElement(element: Element | null): boolean {\n if (!element || !('tagName' in element)) return false;\n\n const tag = element.tagName;\n\n // <textarea>\n if (tag === 'TEXTAREA') return true;\n\n // <input> with text-like type\n if (tag === 'INPUT') {\n const type = (element as HTMLInputElement).type?.toLowerCase() || 'text';\n return TEXT_INPUT_TYPES.has(type);\n }\n\n // contenteditable=\"true\" or contenteditable=\"\"\n const htmlEl = element as HTMLElement;\n if (htmlEl.isContentEditable) return true;\n // Fallback: check attribute directly (isContentEditable can be unreliable for detached elements)\n const ceAttr = htmlEl.contentEditable;\n if (ceAttr === 'true' || ceAttr === '') return true;\n\n // role=\"textbox\" (TipTap uses this)\n if (typeof element.getAttribute === 'function' && element.getAttribute('role') === 'textbox') return true;\n\n // Check ancestors for contenteditable (e.g., a <p> inside a [contenteditable] div)\n // Walk up manually because jsdom's closest doesn't reliably match property-set contentEditable\n let ancestor = element.parentElement;\n while (ancestor) {\n if ((ancestor as HTMLElement).isContentEditable) return true;\n const ancestorCe = (ancestor as HTMLElement).contentEditable;\n if (ancestorCe === 'true' || ancestorCe === '') return true;\n ancestor = ancestor.parentElement;\n }\n\n return false;\n}\n\n// ============================================\n// Shortcut Override API\n// ============================================\n\n/** Module-level override store. null = disabled, Partial<KeyboardShortcut> = merged with default. */\nconst shortcutOverrides = new Map<ShortcutName, Partial<KeyboardShortcut> | null>();\n\n/**\n * Configure shortcut overrides at app startup. Mirrors the `configureTheme()` API.\n *\n * - Partial overrides merge with the default (e.g., `{ key: '\\\\' }` keeps label/component/etc.)\n * - `null` disables a shortcut entirely\n * - Omitted keys are left unchanged\n * - Multiple calls merge additively (last write wins per key)\n *\n * @example\n * ```ts\n * import { configureShortcuts } from '@fragments-sdk/ui';\n *\n * // Remap sidebar toggle to Ctrl+\\\n * configureShortcuts({ SIDEBAR_TOGGLE: { key: '\\\\', label: 'Ctrl+\\\\' } });\n *\n * // Disable sidebar toggle entirely\n * configureShortcuts({ SIDEBAR_TOGGLE: null });\n * ```\n */\nexport function configureShortcuts(\n overrides: Partial<Record<ShortcutName, Partial<KeyboardShortcut> | null>>\n): void {\n for (const [name, value] of Object.entries(overrides) as [ShortcutName, Partial<KeyboardShortcut> | null][]) {\n if (!(name in KEYBOARD_SHORTCUTS)) continue;\n if (value === undefined) continue;\n shortcutOverrides.set(name, value);\n }\n}\n\n/**\n * Resolve a shortcut by name, applying any overrides.\n * Returns `null` if the shortcut has been disabled via `configureShortcuts({ name: null })`.\n */\nexport function getResolvedShortcut(name: ShortcutName): KeyboardShortcut | null {\n const override = shortcutOverrides.get(name);\n\n // Explicitly disabled\n if (override === null) return null;\n\n const defaultShortcut = KEYBOARD_SHORTCUTS[name] as KeyboardShortcut;\n\n // No override — return default\n if (override === undefined) return defaultShortcut;\n\n // Merge override into default\n return { ...defaultShortcut, ...override };\n}\n\n/**\n * Clear all shortcut overrides. Primarily useful for tests.\n */\nexport function resetShortcutOverrides(): void {\n shortcutOverrides.clear();\n}\n\n// ============================================\n// useKeyboardShortcut Hook\n// ============================================\n\nexport interface UseKeyboardShortcutOptions {\n /** Shortcut name from KEYBOARD_SHORTCUTS */\n name: ShortcutName;\n /** Handler called when the shortcut fires */\n handler: () => void;\n /** Whether the shortcut is active (default: true) */\n enabled?: boolean;\n /**\n * Override the shortcut's scope for this registration:\n * - 'global': listens on `document`, skips editable elements\n * - 'component': listens on `ref` element only\n * If omitted, uses the scope from the shortcut definition.\n */\n scope?: 'global' | 'component';\n /** Required when scope is 'component' — the element to attach the listener to */\n ref?: RefObject<Element | null>;\n}\n\n/**\n * Register a keyboard shortcut handler with automatic scope and editable-area handling.\n *\n * For global shortcuts, automatically skips when focus is in an editable element\n * (input, textarea, contenteditable, role=\"textbox\") so component-scoped shortcuts\n * like Editor's Ctrl+B take precedence over Sidebar's Ctrl+B.\n *\n * Respects `configureShortcuts()` overrides — if a shortcut is remapped or disabled,\n * the hook automatically adapts.\n *\n * @example\n * ```tsx\n * useKeyboardShortcut({\n * name: 'SIDEBAR_TOGGLE',\n * handler: toggleSidebar,\n * enabled: enableKeyboardShortcut && collapsible !== 'none',\n * });\n * ```\n */\nexport function useKeyboardShortcut({\n name,\n handler,\n enabled = true,\n scope: scopeOverride,\n ref,\n}: UseKeyboardShortcutOptions): void {\n // Use a ref for handler to avoid re-subscribing on every render\n const handlerRef = useRef(handler);\n handlerRef.current = handler;\n\n useEffect(() => {\n if (!enabled) return;\n\n const resolved = getResolvedShortcut(name);\n if (!resolved) return; // disabled via configureShortcuts\n\n const effectiveScope = scopeOverride ?? resolved.scope;\n\n const handleKeyDown = (e: KeyboardEvent) => {\n if (!matchesShortcut(e, resolved)) return;\n\n // Global shortcuts skip editable elements\n if (effectiveScope === 'global' && isEditableElement(e.target as Element)) return;\n\n e.preventDefault();\n handlerRef.current();\n };\n\n const target = effectiveScope === 'component' ? ref?.current : document;\n if (!target) return;\n\n target.addEventListener('keydown', handleKeyDown as EventListener);\n return () => target.removeEventListener('keydown', handleKeyDown as EventListener);\n }, [name, enabled, scopeOverride, ref]);\n}\n"],"names":["useRef","useEffect"],"mappings":"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"}
@@ -0,0 +1,293 @@
1
+ import { type RefObject } from 'react';
2
+ export interface KeyboardShortcut {
3
+ /** The key to match (e.g., 'b', 'k', 'Escape'). Case-insensitive. */
4
+ key: string;
5
+ /** Require Ctrl (Windows/Linux) or Cmd (macOS) */
6
+ meta?: boolean;
7
+ /** Require Shift */
8
+ shift?: boolean;
9
+ /** Require Alt/Option */
10
+ alt?: boolean;
11
+ /** Human-readable label for display (e.g., "Ctrl+B", "⌘B") */
12
+ label: string;
13
+ /** Which component owns this shortcut */
14
+ component: string;
15
+ /** What the shortcut does */
16
+ description: string;
17
+ /**
18
+ * Scope controls when the shortcut is active:
19
+ * - 'global': Listens on document (e.g., sidebar toggle)
20
+ * - 'component': Only active when component is focused/mounted
21
+ */
22
+ scope: 'global' | 'component';
23
+ }
24
+ export declare const KEYBOARD_SHORTCUTS: {
25
+ readonly SIDEBAR_TOGGLE: {
26
+ readonly key: "b";
27
+ readonly meta: true;
28
+ readonly label: "Ctrl+B";
29
+ readonly component: "Sidebar";
30
+ readonly description: "Toggle sidebar collapse/expand";
31
+ readonly scope: "global";
32
+ };
33
+ readonly SIDEBAR_CLOSE_MOBILE: {
34
+ readonly key: "Escape";
35
+ readonly label: "Escape";
36
+ readonly component: "Sidebar";
37
+ readonly description: "Close mobile sidebar drawer";
38
+ readonly scope: "global";
39
+ };
40
+ readonly EDITOR_BOLD: {
41
+ readonly key: "b";
42
+ readonly meta: true;
43
+ readonly label: "Ctrl+B";
44
+ readonly component: "Editor";
45
+ readonly description: "Toggle bold formatting";
46
+ readonly scope: "component";
47
+ };
48
+ readonly EDITOR_ITALIC: {
49
+ readonly key: "i";
50
+ readonly meta: true;
51
+ readonly label: "Ctrl+I";
52
+ readonly component: "Editor";
53
+ readonly description: "Toggle italic formatting";
54
+ readonly scope: "component";
55
+ };
56
+ readonly EDITOR_STRIKETHROUGH: {
57
+ readonly key: "s";
58
+ readonly meta: true;
59
+ readonly shift: true;
60
+ readonly label: "Ctrl+Shift+S";
61
+ readonly component: "Editor";
62
+ readonly description: "Toggle strikethrough formatting";
63
+ readonly scope: "component";
64
+ };
65
+ readonly EDITOR_LINK: {
66
+ readonly key: "k";
67
+ readonly meta: true;
68
+ readonly label: "Ctrl+K";
69
+ readonly component: "Editor";
70
+ readonly description: "Insert or edit link";
71
+ readonly scope: "component";
72
+ };
73
+ readonly EDITOR_CODE: {
74
+ readonly key: "e";
75
+ readonly meta: true;
76
+ readonly label: "Ctrl+E";
77
+ readonly component: "Editor";
78
+ readonly description: "Toggle inline code";
79
+ readonly scope: "component";
80
+ };
81
+ readonly EDITOR_BULLET_LIST: {
82
+ readonly key: "8";
83
+ readonly meta: true;
84
+ readonly shift: true;
85
+ readonly label: "Ctrl+Shift+8";
86
+ readonly component: "Editor";
87
+ readonly description: "Toggle bullet list";
88
+ readonly scope: "component";
89
+ };
90
+ readonly EDITOR_ORDERED_LIST: {
91
+ readonly key: "7";
92
+ readonly meta: true;
93
+ readonly shift: true;
94
+ readonly label: "Ctrl+Shift+7";
95
+ readonly component: "Editor";
96
+ readonly description: "Toggle ordered list";
97
+ readonly scope: "component";
98
+ };
99
+ readonly EDITOR_HEADING1: {
100
+ readonly key: "1";
101
+ readonly meta: true;
102
+ readonly alt: true;
103
+ readonly label: "Ctrl+Alt+1";
104
+ readonly component: "Editor";
105
+ readonly description: "Toggle heading level 1";
106
+ readonly scope: "component";
107
+ };
108
+ readonly EDITOR_HEADING2: {
109
+ readonly key: "2";
110
+ readonly meta: true;
111
+ readonly alt: true;
112
+ readonly label: "Ctrl+Alt+2";
113
+ readonly component: "Editor";
114
+ readonly description: "Toggle heading level 2";
115
+ readonly scope: "component";
116
+ };
117
+ readonly EDITOR_HEADING3: {
118
+ readonly key: "3";
119
+ readonly meta: true;
120
+ readonly alt: true;
121
+ readonly label: "Ctrl+Alt+3";
122
+ readonly component: "Editor";
123
+ readonly description: "Toggle heading level 3";
124
+ readonly scope: "component";
125
+ };
126
+ readonly EDITOR_BLOCKQUOTE: {
127
+ readonly key: "b";
128
+ readonly meta: true;
129
+ readonly shift: true;
130
+ readonly label: "Ctrl+Shift+B";
131
+ readonly component: "Editor";
132
+ readonly description: "Toggle blockquote";
133
+ readonly scope: "component";
134
+ };
135
+ readonly EDITOR_UNDO: {
136
+ readonly key: "z";
137
+ readonly meta: true;
138
+ readonly label: "Ctrl+Z";
139
+ readonly component: "Editor";
140
+ readonly description: "Undo last action";
141
+ readonly scope: "component";
142
+ };
143
+ readonly EDITOR_REDO: {
144
+ readonly key: "z";
145
+ readonly meta: true;
146
+ readonly shift: true;
147
+ readonly label: "Ctrl+Shift+Z";
148
+ readonly component: "Editor";
149
+ readonly description: "Redo last undone action";
150
+ readonly scope: "component";
151
+ };
152
+ readonly PROMPT_SUBMIT: {
153
+ readonly key: "Enter";
154
+ readonly label: "Enter";
155
+ readonly component: "Prompt";
156
+ readonly description: "Submit prompt (when submitOnEnter is true)";
157
+ readonly scope: "component";
158
+ };
159
+ readonly NAV_TOGGLE: {
160
+ readonly key: "Enter";
161
+ readonly label: "Enter";
162
+ readonly component: "NavigationMenu";
163
+ readonly description: "Toggle menu item open/closed";
164
+ readonly scope: "component";
165
+ };
166
+ readonly NAV_CLOSE: {
167
+ readonly key: "Escape";
168
+ readonly label: "Escape";
169
+ readonly component: "NavigationMenu";
170
+ readonly description: "Close menu and return focus to trigger";
171
+ readonly scope: "component";
172
+ };
173
+ readonly COMMAND_SELECT: {
174
+ readonly key: "Enter";
175
+ readonly label: "Enter";
176
+ readonly component: "Command";
177
+ readonly description: "Select active command item";
178
+ readonly scope: "component";
179
+ };
180
+ readonly COLLAPSIBLE_TOGGLE: {
181
+ readonly key: "Enter";
182
+ readonly label: "Enter";
183
+ readonly component: "Collapsible";
184
+ readonly description: "Toggle collapsible open/closed";
185
+ readonly scope: "component";
186
+ };
187
+ };
188
+ export type ShortcutName = keyof typeof KEYBOARD_SHORTCUTS;
189
+ /**
190
+ * Check if a KeyboardEvent matches a shortcut definition.
191
+ *
192
+ * Usage:
193
+ * ```ts
194
+ * import { KEYBOARD_SHORTCUTS, matchesShortcut } from '../../utils/keyboard-shortcuts';
195
+ *
196
+ * const handleKeyDown = (e: KeyboardEvent) => {
197
+ * if (matchesShortcut(e, KEYBOARD_SHORTCUTS.SIDEBAR_TOGGLE)) {
198
+ * e.preventDefault();
199
+ * toggleSidebar();
200
+ * }
201
+ * };
202
+ * ```
203
+ */
204
+ export declare function matchesShortcut(event: KeyboardEvent, shortcut: KeyboardShortcut): boolean;
205
+ /**
206
+ * Get a human-readable shortcut label, adapting for macOS vs other platforms.
207
+ * Returns "⌘B" on Mac, "Ctrl+B" elsewhere.
208
+ */
209
+ export declare function getShortcutLabel(shortcut: KeyboardShortcut): string;
210
+ /**
211
+ * Find all shortcuts that conflict with a given shortcut (same key combo, different component).
212
+ * Useful for debugging and documentation.
213
+ */
214
+ export declare function findConflicts(name: ShortcutName): KeyboardShortcut[];
215
+ /**
216
+ * Get all registered shortcuts, optionally filtered by component or scope.
217
+ */
218
+ export declare function getShortcuts(filter?: {
219
+ component?: string;
220
+ scope?: 'global' | 'component';
221
+ }): KeyboardShortcut[];
222
+ /**
223
+ * Check if an element is an editable area (input, textarea, contenteditable, role="textbox").
224
+ * Global shortcuts should skip firing when the user is typing in one of these.
225
+ */
226
+ export declare function isEditableElement(element: Element | null): boolean;
227
+ /**
228
+ * Configure shortcut overrides at app startup. Mirrors the `configureTheme()` API.
229
+ *
230
+ * - Partial overrides merge with the default (e.g., `{ key: '\\' }` keeps label/component/etc.)
231
+ * - `null` disables a shortcut entirely
232
+ * - Omitted keys are left unchanged
233
+ * - Multiple calls merge additively (last write wins per key)
234
+ *
235
+ * @example
236
+ * ```ts
237
+ * import { configureShortcuts } from '@fragments-sdk/ui';
238
+ *
239
+ * // Remap sidebar toggle to Ctrl+\
240
+ * configureShortcuts({ SIDEBAR_TOGGLE: { key: '\\', label: 'Ctrl+\\' } });
241
+ *
242
+ * // Disable sidebar toggle entirely
243
+ * configureShortcuts({ SIDEBAR_TOGGLE: null });
244
+ * ```
245
+ */
246
+ export declare function configureShortcuts(overrides: Partial<Record<ShortcutName, Partial<KeyboardShortcut> | null>>): void;
247
+ /**
248
+ * Resolve a shortcut by name, applying any overrides.
249
+ * Returns `null` if the shortcut has been disabled via `configureShortcuts({ name: null })`.
250
+ */
251
+ export declare function getResolvedShortcut(name: ShortcutName): KeyboardShortcut | null;
252
+ /**
253
+ * Clear all shortcut overrides. Primarily useful for tests.
254
+ */
255
+ export declare function resetShortcutOverrides(): void;
256
+ export interface UseKeyboardShortcutOptions {
257
+ /** Shortcut name from KEYBOARD_SHORTCUTS */
258
+ name: ShortcutName;
259
+ /** Handler called when the shortcut fires */
260
+ handler: () => void;
261
+ /** Whether the shortcut is active (default: true) */
262
+ enabled?: boolean;
263
+ /**
264
+ * Override the shortcut's scope for this registration:
265
+ * - 'global': listens on `document`, skips editable elements
266
+ * - 'component': listens on `ref` element only
267
+ * If omitted, uses the scope from the shortcut definition.
268
+ */
269
+ scope?: 'global' | 'component';
270
+ /** Required when scope is 'component' — the element to attach the listener to */
271
+ ref?: RefObject<Element | null>;
272
+ }
273
+ /**
274
+ * Register a keyboard shortcut handler with automatic scope and editable-area handling.
275
+ *
276
+ * For global shortcuts, automatically skips when focus is in an editable element
277
+ * (input, textarea, contenteditable, role="textbox") so component-scoped shortcuts
278
+ * like Editor's Ctrl+B take precedence over Sidebar's Ctrl+B.
279
+ *
280
+ * Respects `configureShortcuts()` overrides — if a shortcut is remapped or disabled,
281
+ * the hook automatically adapts.
282
+ *
283
+ * @example
284
+ * ```tsx
285
+ * useKeyboardShortcut({
286
+ * name: 'SIDEBAR_TOGGLE',
287
+ * handler: toggleSidebar,
288
+ * enabled: enableKeyboardShortcut && collapsible !== 'none',
289
+ * });
290
+ * ```
291
+ */
292
+ export declare function useKeyboardShortcut({ name, handler, enabled, scope: scopeOverride, ref, }: UseKeyboardShortcutOptions): void;
293
+ //# sourceMappingURL=keyboard-shortcuts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keyboard-shortcuts.d.ts","sourceRoot":"","sources":["../../src/utils/keyboard-shortcuts.ts"],"names":[],"mappings":"AAsBA,OAAO,EAAqB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAM1D,MAAM,WAAW,gBAAgB;IAC/B,qEAAqE;IACrE,GAAG,EAAE,MAAM,CAAC;IACZ,kDAAkD;IAClD,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,oBAAoB;IACpB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,yBAAyB;IACzB,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,8DAA8D;IAC9D,KAAK,EAAE,MAAM,CAAC;IACd,yCAAyC;IACzC,SAAS,EAAE,MAAM,CAAC;IAClB,6BAA6B;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,KAAK,EAAE,QAAQ,GAAG,WAAW,CAAC;CAC/B;AAMD,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8KsB,CAAC;AAEtD,MAAM,MAAM,YAAY,GAAG,MAAM,OAAO,kBAAkB,CAAC;AAM3D;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,gBAAgB,GAAG,OAAO,CAWzF;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,gBAAgB,GAAG,MAAM,CAYnE;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,YAAY,GAAG,gBAAgB,EAAE,CAapE;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,MAAM,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,QAAQ,GAAG,WAAW,CAAA;CAAE,GAAG,gBAAgB,EAAE,CAMhH;AAWD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,GAAG,OAAO,CAmClE;AASD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,OAAO,CAAC,gBAAgB,CAAC,GAAG,IAAI,CAAC,CAAC,GACzE,IAAI,CAMN;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,YAAY,GAAG,gBAAgB,GAAG,IAAI,CAa/E;AAED;;GAEG;AACH,wBAAgB,sBAAsB,IAAI,IAAI,CAE7C;AAMD,MAAM,WAAW,0BAA0B;IACzC,4CAA4C;IAC5C,IAAI,EAAE,YAAY,CAAC;IACnB,6CAA6C;IAC7C,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,qDAAqD;IACrD,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;OAKG;IACH,KAAK,CAAC,EAAE,QAAQ,GAAG,WAAW,CAAC;IAC/B,iFAAiF;IACjF,GAAG,CAAC,EAAE,SAAS,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;CACjC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,IAAI,EACJ,OAAO,EACP,OAAc,EACd,KAAK,EAAE,aAAa,EACpB,GAAG,GACJ,EAAE,0BAA0B,GAAG,IAAI,CA6BnC"}
@@ -0,0 +1,295 @@
1
+ import { useRef, useEffect } from "react";
2
+ const KEYBOARD_SHORTCUTS = {
3
+ // ----- Sidebar -----
4
+ SIDEBAR_TOGGLE: {
5
+ key: "b",
6
+ meta: true,
7
+ label: "Ctrl+B",
8
+ component: "Sidebar",
9
+ description: "Toggle sidebar collapse/expand",
10
+ scope: "global"
11
+ },
12
+ SIDEBAR_CLOSE_MOBILE: {
13
+ key: "Escape",
14
+ label: "Escape",
15
+ component: "Sidebar",
16
+ description: "Close mobile sidebar drawer",
17
+ scope: "global"
18
+ },
19
+ // ----- Editor (TipTap handles these natively, metadata for display) -----
20
+ EDITOR_BOLD: {
21
+ key: "b",
22
+ meta: true,
23
+ label: "Ctrl+B",
24
+ component: "Editor",
25
+ description: "Toggle bold formatting",
26
+ scope: "component"
27
+ },
28
+ EDITOR_ITALIC: {
29
+ key: "i",
30
+ meta: true,
31
+ label: "Ctrl+I",
32
+ component: "Editor",
33
+ description: "Toggle italic formatting",
34
+ scope: "component"
35
+ },
36
+ EDITOR_STRIKETHROUGH: {
37
+ key: "s",
38
+ meta: true,
39
+ shift: true,
40
+ label: "Ctrl+Shift+S",
41
+ component: "Editor",
42
+ description: "Toggle strikethrough formatting",
43
+ scope: "component"
44
+ },
45
+ EDITOR_LINK: {
46
+ key: "k",
47
+ meta: true,
48
+ label: "Ctrl+K",
49
+ component: "Editor",
50
+ description: "Insert or edit link",
51
+ scope: "component"
52
+ },
53
+ EDITOR_CODE: {
54
+ key: "e",
55
+ meta: true,
56
+ label: "Ctrl+E",
57
+ component: "Editor",
58
+ description: "Toggle inline code",
59
+ scope: "component"
60
+ },
61
+ EDITOR_BULLET_LIST: {
62
+ key: "8",
63
+ meta: true,
64
+ shift: true,
65
+ label: "Ctrl+Shift+8",
66
+ component: "Editor",
67
+ description: "Toggle bullet list",
68
+ scope: "component"
69
+ },
70
+ EDITOR_ORDERED_LIST: {
71
+ key: "7",
72
+ meta: true,
73
+ shift: true,
74
+ label: "Ctrl+Shift+7",
75
+ component: "Editor",
76
+ description: "Toggle ordered list",
77
+ scope: "component"
78
+ },
79
+ EDITOR_HEADING1: {
80
+ key: "1",
81
+ meta: true,
82
+ alt: true,
83
+ label: "Ctrl+Alt+1",
84
+ component: "Editor",
85
+ description: "Toggle heading level 1",
86
+ scope: "component"
87
+ },
88
+ EDITOR_HEADING2: {
89
+ key: "2",
90
+ meta: true,
91
+ alt: true,
92
+ label: "Ctrl+Alt+2",
93
+ component: "Editor",
94
+ description: "Toggle heading level 2",
95
+ scope: "component"
96
+ },
97
+ EDITOR_HEADING3: {
98
+ key: "3",
99
+ meta: true,
100
+ alt: true,
101
+ label: "Ctrl+Alt+3",
102
+ component: "Editor",
103
+ description: "Toggle heading level 3",
104
+ scope: "component"
105
+ },
106
+ EDITOR_BLOCKQUOTE: {
107
+ key: "b",
108
+ meta: true,
109
+ shift: true,
110
+ label: "Ctrl+Shift+B",
111
+ component: "Editor",
112
+ description: "Toggle blockquote",
113
+ scope: "component"
114
+ },
115
+ EDITOR_UNDO: {
116
+ key: "z",
117
+ meta: true,
118
+ label: "Ctrl+Z",
119
+ component: "Editor",
120
+ description: "Undo last action",
121
+ scope: "component"
122
+ },
123
+ EDITOR_REDO: {
124
+ key: "z",
125
+ meta: true,
126
+ shift: true,
127
+ label: "Ctrl+Shift+Z",
128
+ component: "Editor",
129
+ description: "Redo last undone action",
130
+ scope: "component"
131
+ },
132
+ // ----- Prompt -----
133
+ PROMPT_SUBMIT: {
134
+ key: "Enter",
135
+ label: "Enter",
136
+ component: "Prompt",
137
+ description: "Submit prompt (when submitOnEnter is true)",
138
+ scope: "component"
139
+ },
140
+ // ----- NavigationMenu -----
141
+ NAV_TOGGLE: {
142
+ key: "Enter",
143
+ label: "Enter",
144
+ component: "NavigationMenu",
145
+ description: "Toggle menu item open/closed",
146
+ scope: "component"
147
+ },
148
+ NAV_CLOSE: {
149
+ key: "Escape",
150
+ label: "Escape",
151
+ component: "NavigationMenu",
152
+ description: "Close menu and return focus to trigger",
153
+ scope: "component"
154
+ },
155
+ // ----- Command -----
156
+ COMMAND_SELECT: {
157
+ key: "Enter",
158
+ label: "Enter",
159
+ component: "Command",
160
+ description: "Select active command item",
161
+ scope: "component"
162
+ },
163
+ // ----- Collapsible -----
164
+ COLLAPSIBLE_TOGGLE: {
165
+ key: "Enter",
166
+ label: "Enter",
167
+ component: "Collapsible",
168
+ description: "Toggle collapsible open/closed",
169
+ scope: "component"
170
+ }
171
+ };
172
+ function matchesShortcut(event, shortcut) {
173
+ if (shortcut.meta && !(event.metaKey || event.ctrlKey)) return false;
174
+ if (shortcut.shift && !event.shiftKey) return false;
175
+ if (shortcut.alt && !event.altKey) return false;
176
+ if (!shortcut.meta && (event.metaKey || event.ctrlKey)) return false;
177
+ if (!shortcut.shift && event.shiftKey) return false;
178
+ if (!shortcut.alt && event.altKey) return false;
179
+ return event.key.toLowerCase() === shortcut.key.toLowerCase();
180
+ }
181
+ function getShortcutLabel(shortcut) {
182
+ var _a, _b;
183
+ if (typeof navigator === "undefined") return shortcut.label;
184
+ const isMac = ((_a = navigator.platform) == null ? void 0 : _a.includes("Mac")) || ((_b = navigator.userAgent) == null ? void 0 : _b.includes("Mac"));
185
+ if (!isMac) return shortcut.label;
186
+ const parts = [];
187
+ if (shortcut.meta) parts.push("⌘");
188
+ if (shortcut.shift) parts.push("⇧");
189
+ if (shortcut.alt) parts.push("⌥");
190
+ parts.push(shortcut.key.toUpperCase());
191
+ return parts.join("");
192
+ }
193
+ function findConflicts(name) {
194
+ const target = KEYBOARD_SHORTCUTS[name];
195
+ return Object.entries(KEYBOARD_SHORTCUTS).filter(([key, shortcut]) => {
196
+ if (key === name) return false;
197
+ return shortcut.key.toLowerCase() === target.key.toLowerCase() && !!shortcut.meta === !!target.meta && !!shortcut.shift === !!target.shift && !!shortcut.alt === !!target.alt;
198
+ }).map(([, shortcut]) => shortcut);
199
+ }
200
+ function getShortcuts(filter) {
201
+ return Object.values(KEYBOARD_SHORTCUTS).filter((shortcut) => {
202
+ if ((filter == null ? void 0 : filter.component) && shortcut.component !== filter.component) return false;
203
+ if ((filter == null ? void 0 : filter.scope) && shortcut.scope !== filter.scope) return false;
204
+ return true;
205
+ });
206
+ }
207
+ const TEXT_INPUT_TYPES = /* @__PURE__ */ new Set([
208
+ "text",
209
+ "search",
210
+ "url",
211
+ "tel",
212
+ "email",
213
+ "password",
214
+ "number"
215
+ ]);
216
+ function isEditableElement(element) {
217
+ var _a;
218
+ if (!element || !("tagName" in element)) return false;
219
+ const tag = element.tagName;
220
+ if (tag === "TEXTAREA") return true;
221
+ if (tag === "INPUT") {
222
+ const type = ((_a = element.type) == null ? void 0 : _a.toLowerCase()) || "text";
223
+ return TEXT_INPUT_TYPES.has(type);
224
+ }
225
+ const htmlEl = element;
226
+ if (htmlEl.isContentEditable) return true;
227
+ const ceAttr = htmlEl.contentEditable;
228
+ if (ceAttr === "true" || ceAttr === "") return true;
229
+ if (typeof element.getAttribute === "function" && element.getAttribute("role") === "textbox") return true;
230
+ let ancestor = element.parentElement;
231
+ while (ancestor) {
232
+ if (ancestor.isContentEditable) return true;
233
+ const ancestorCe = ancestor.contentEditable;
234
+ if (ancestorCe === "true" || ancestorCe === "") return true;
235
+ ancestor = ancestor.parentElement;
236
+ }
237
+ return false;
238
+ }
239
+ const shortcutOverrides = /* @__PURE__ */ new Map();
240
+ function configureShortcuts(overrides) {
241
+ for (const [name, value] of Object.entries(overrides)) {
242
+ if (!(name in KEYBOARD_SHORTCUTS)) continue;
243
+ if (value === void 0) continue;
244
+ shortcutOverrides.set(name, value);
245
+ }
246
+ }
247
+ function getResolvedShortcut(name) {
248
+ const override = shortcutOverrides.get(name);
249
+ if (override === null) return null;
250
+ const defaultShortcut = KEYBOARD_SHORTCUTS[name];
251
+ if (override === void 0) return defaultShortcut;
252
+ return { ...defaultShortcut, ...override };
253
+ }
254
+ function resetShortcutOverrides() {
255
+ shortcutOverrides.clear();
256
+ }
257
+ function useKeyboardShortcut({
258
+ name,
259
+ handler,
260
+ enabled = true,
261
+ scope: scopeOverride,
262
+ ref
263
+ }) {
264
+ const handlerRef = useRef(handler);
265
+ handlerRef.current = handler;
266
+ useEffect(() => {
267
+ if (!enabled) return;
268
+ const resolved = getResolvedShortcut(name);
269
+ if (!resolved) return;
270
+ const effectiveScope = scopeOverride ?? resolved.scope;
271
+ const handleKeyDown = (e) => {
272
+ if (!matchesShortcut(e, resolved)) return;
273
+ if (effectiveScope === "global" && isEditableElement(e.target)) return;
274
+ e.preventDefault();
275
+ handlerRef.current();
276
+ };
277
+ const target = effectiveScope === "component" ? ref == null ? void 0 : ref.current : document;
278
+ if (!target) return;
279
+ target.addEventListener("keydown", handleKeyDown);
280
+ return () => target.removeEventListener("keydown", handleKeyDown);
281
+ }, [name, enabled, scopeOverride, ref]);
282
+ }
283
+ export {
284
+ KEYBOARD_SHORTCUTS,
285
+ configureShortcuts,
286
+ findConflicts,
287
+ getResolvedShortcut,
288
+ getShortcutLabel,
289
+ getShortcuts,
290
+ isEditableElement,
291
+ matchesShortcut,
292
+ resetShortcutOverrides,
293
+ useKeyboardShortcut
294
+ };
295
+ //# sourceMappingURL=keyboard-shortcuts.js.map