chati-dev 4.3.1 → 4.4.1

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 (158) hide show
  1. package/README.md +18 -13
  2. package/bin/chati.js +28 -4
  3. package/framework/agents/build/dev.md +5 -1
  4. package/framework/agents/discover/greenfield-wu.md +16 -1
  5. package/framework/agents/plan/tasks.md +31 -0
  6. package/framework/agents/plan/ux-brand-architect.md +21 -2
  7. package/framework/agents/plan/ux-component-engineer.md +10 -0
  8. package/framework/agents/quality/qa-implementation.md +1 -1
  9. package/framework/agents/quality/qa-planning.md +1 -1
  10. package/framework/agents/quality/qa-visual.md +24 -21
  11. package/framework/config.yaml +5 -4
  12. package/framework/constitution.md +60 -11
  13. package/framework/context/governance.md +12 -10
  14. package/framework/context/root.md +2 -2
  15. package/framework/data/entity-registry.yaml +12 -4
  16. package/framework/data/qa-rubrics.yaml +335 -0
  17. package/framework/domains/constitution.yaml +3 -3
  18. package/framework/executors/json-validate.js +80 -0
  19. package/framework/executors/npm-script.js +72 -0
  20. package/framework/executors/path-check.js +67 -0
  21. package/framework/executors/registry.yaml +36 -0
  22. package/framework/hooks/advance-trigger.js +47 -3
  23. package/framework/hooks/constitution-guard.js +9 -4
  24. package/framework/hooks/git-push-authority.js +113 -0
  25. package/framework/hooks/license-guard.js +59 -9
  26. package/framework/hooks/model-governance.js +40 -17
  27. package/framework/hooks/prism-engine.js +78 -11
  28. package/framework/hooks/session-digest.js +12 -3
  29. package/framework/hooks/settings.json +7 -3
  30. package/framework/hooks/style-guard.js +52 -3
  31. package/framework/hooks/team-quality-gate.js +43 -5
  32. package/framework/i18n/en.yaml +3 -3
  33. package/framework/i18n/es.yaml +3 -3
  34. package/framework/i18n/fr.yaml +3 -3
  35. package/framework/i18n/pt.yaml +3 -3
  36. package/framework/intelligence/confidence.yaml +85 -6
  37. package/framework/intelligence/context-engine.md +9 -5
  38. package/framework/intelligence/decision-engine.md +1 -1
  39. package/framework/manifest.json +1351 -0
  40. package/framework/manifest.sig +1 -0
  41. package/framework/orchestrator/chati-router.js +1 -1
  42. package/framework/orchestrator/chati-update.md +19 -3
  43. package/framework/orchestrator/chati.md +24 -3
  44. package/framework/quality-gates/planning-gate.md +2 -0
  45. package/framework/scaffold/motion-premium/scaffold.yaml +2 -1
  46. package/framework/scaffold/motion-premium-3d/components/webgl/WebGLContext.tsx.template +1 -1
  47. package/framework/scaffold/saas-dashboard/README.md +58 -0
  48. package/framework/scaffold/saas-dashboard/app/dashboard/page.tsx.template +546 -0
  49. package/framework/scaffold/saas-dashboard/app/globals.css.template +746 -0
  50. package/framework/scaffold/saas-dashboard/app/layout.tsx.template +192 -0
  51. package/framework/scaffold/saas-dashboard/app/shell.css.template +1070 -0
  52. package/framework/scaffold/saas-dashboard/components/data/Chart.tsx.template +725 -0
  53. package/framework/scaffold/saas-dashboard/components/data/DataTable.tsx.template +623 -0
  54. package/framework/scaffold/saas-dashboard/components/data/DataTable.types.ts.template +104 -0
  55. package/framework/scaffold/saas-dashboard/components/data/DataTablePagination.tsx.template +234 -0
  56. package/framework/scaffold/saas-dashboard/components/data/SkeletonBlock.tsx.template +111 -0
  57. package/framework/scaffold/saas-dashboard/components/data/StatCard.tsx.template +393 -0
  58. package/framework/scaffold/saas-dashboard/components/data/StatCardGrid.tsx.template +121 -0
  59. package/framework/scaffold/saas-dashboard/components/feedback/EmptyState.tsx.template +281 -0
  60. package/framework/scaffold/saas-dashboard/components/interaction/CommandPalette.tsx.template +767 -0
  61. package/framework/scaffold/saas-dashboard/components/interaction/Field.tsx.template +351 -0
  62. package/framework/scaffold/saas-dashboard/components/interaction/Form.tsx.template +424 -0
  63. package/framework/scaffold/saas-dashboard/components/primitives/Badge.tsx.template +193 -0
  64. package/framework/scaffold/saas-dashboard/components/primitives/Button.tsx.template +261 -0
  65. package/framework/scaffold/saas-dashboard/components/primitives/Card.tsx.template +287 -0
  66. package/framework/scaffold/saas-dashboard/components/primitives/Input.tsx.template +310 -0
  67. package/framework/scaffold/saas-dashboard/components/primitives/Spinner.tsx.template +135 -0
  68. package/framework/scaffold/saas-dashboard/components/primitives/index.ts.template +60 -0
  69. package/framework/scaffold/saas-dashboard/components/shell/AppShell.tsx.template +174 -0
  70. package/framework/scaffold/saas-dashboard/components/shell/Breadcrumb.tsx.template +186 -0
  71. package/framework/scaffold/saas-dashboard/components/shell/Container.tsx.template +86 -0
  72. package/framework/scaffold/saas-dashboard/components/shell/NavItem.tsx.template +147 -0
  73. package/framework/scaffold/saas-dashboard/components/shell/Sidebar.tsx.template +378 -0
  74. package/framework/scaffold/saas-dashboard/components/shell/SidebarContext.tsx.template +169 -0
  75. package/framework/scaffold/saas-dashboard/components/shell/ThemeToggle.tsx.template +128 -0
  76. package/framework/scaffold/saas-dashboard/components/shell/Topbar.tsx.template +301 -0
  77. package/framework/scaffold/saas-dashboard/lib/brand.ts.template +324 -0
  78. package/framework/scaffold/saas-dashboard/scaffold.yaml +225 -0
  79. package/framework/schemas/session.schema.json +5 -0
  80. package/framework/schemas/task.schema.json +9 -0
  81. package/framework/scripts/visual-qa.js +87 -1
  82. package/framework/tasks/orchestrator-health.md +4 -4
  83. package/framework/tasks/qa-impl-verdict.md +11 -5
  84. package/framework/templates/qa-gate-tmpl.yaml +24 -11
  85. package/package.json +4 -4
  86. package/src/config/claude-settings-generator.js +9 -6
  87. package/src/config/context-file-generator.js +0 -6
  88. package/src/dashboard/renderer.js +0 -36
  89. package/src/executors/runner.js +204 -0
  90. package/src/installer/core.js +35 -4
  91. package/src/installer/manifest.js +30 -4
  92. package/src/installer/signing-public-key.pem +1 -1
  93. package/src/installer/templates.js +5 -6
  94. package/src/installer/validator.js +2 -1
  95. package/src/intelligence/registry-manager.js +3 -2
  96. package/src/license/client.js +9 -3
  97. package/src/license/commands.js +12 -2
  98. package/src/license/machine-id.js +42 -1
  99. package/src/memory/gotchas.js +58 -40
  100. package/src/memory/magic-docs.js +7 -2
  101. package/src/memory/session-digest.js +9 -4
  102. package/src/orchestrator/cli.js +195 -23
  103. package/src/orchestrator/deviation-handler.js +5 -3
  104. package/src/orchestrator/doctor.js +98 -16
  105. package/src/orchestrator/pipeline-manager.js +44 -22
  106. package/src/orchestrator/session-manager.js +56 -8
  107. package/src/telemetry/sender.js +7 -19
  108. package/src/terminal/run-parallel.js +14 -14
  109. package/src/terminal/run-team.js +3 -3
  110. package/src/upgrade/backup.js +22 -44
  111. package/src/upgrade/migrator.js +51 -12
  112. package/src/upgrade/tracked-files-detector.js +34 -16
  113. package/src/utils/constitution-meta.js +12 -0
  114. package/src/utils/feature-flags.js +26 -2
  115. package/src/utils/flatten-entities.js +4 -40
  116. package/src/utils/schema-validator.js +0 -14
  117. package/src/wizard/i18n.js +3 -3
  118. package/src/api/index.js +0 -120
  119. package/src/autonomy/autonomous-gate.js +0 -294
  120. package/src/autonomy/build-loop.js +0 -281
  121. package/src/autonomy/build-state.js +0 -286
  122. package/src/autonomy/cause-analyzer.js +0 -177
  123. package/src/autonomy/escalation.js +0 -214
  124. package/src/autonomy/index.js +0 -51
  125. package/src/autonomy/mode-manager.js +0 -225
  126. package/src/autonomy/mode-suggester.js +0 -283
  127. package/src/autonomy/progress-reporter.js +0 -275
  128. package/src/autonomy/safety-net.js +0 -370
  129. package/src/config/agent-customizer.js +0 -231
  130. package/src/decision/analyzer.js +0 -291
  131. package/src/decision/engine.js +0 -250
  132. package/src/decision/index.js +0 -38
  133. package/src/decision/registry-healer.js +0 -468
  134. package/src/decision/registry-updater.js +0 -339
  135. package/src/extensions/loader.js +0 -145
  136. package/src/extensions/registry.js +0 -134
  137. package/src/gates/circuit-breaker.js +0 -151
  138. package/src/gates/g1-planning-complete.js +0 -154
  139. package/src/gates/g2-qa-planning.js +0 -156
  140. package/src/gates/g3-implementation.js +0 -215
  141. package/src/gates/g4-qa-implementation.js +0 -240
  142. package/src/gates/g5-deploy-ready.js +0 -181
  143. package/src/gates/gate-base.js +0 -185
  144. package/src/gates/index.js +0 -46
  145. package/src/health/auto-fix.js +0 -216
  146. package/src/health/engine.js +0 -246
  147. package/src/merger/semantic-merger.js +0 -292
  148. package/src/preview/detector.js +0 -238
  149. package/src/preview/index.js +0 -20
  150. package/src/preview/launcher.js +0 -235
  151. package/src/preview/log-buffer.js +0 -103
  152. package/src/quality/metrics-collector.js +0 -281
  153. package/src/quality/test-runner.js +0 -366
  154. package/src/tasks/executor.js +0 -195
  155. package/src/tasks/index.js +0 -4
  156. package/src/tasks/loader.js +0 -210
  157. package/src/tasks/router.js +0 -182
  158. package/src/utils/event-bus.js +0 -126
@@ -0,0 +1,767 @@
1
+ /**
2
+ * components/interaction/CommandPalette.tsx
3
+ * SaaS Dashboard Preset -- D_interaction phase
4
+ *
5
+ * WHY THIS EXISTS
6
+ * ---------------
7
+ * A command palette is the power-user navigation contract. It lets a user
8
+ * navigate to any route, trigger any action, and search any entity without
9
+ * lifting their hands from the keyboard. Without it, dense dashboard UIs
10
+ * force too many pointer round-trips; WITH it, experienced users can operate
11
+ * entirely via keyboard.
12
+ *
13
+ * ARCHITECTURE DECISIONS
14
+ * ----------------------
15
+ * 1. Hand-rolled, no `cmdk` package: cmdk is excellent but adds a non-trivial
16
+ * peer-dep surface. This implementation mirrors cmdk semantics (Command,
17
+ * CommandInput, CommandGroup, CommandItem) purely in React + framer-motion,
18
+ * keeping the preset self-contained. Any team that prefers cmdk can drop
19
+ * this file and re-wrap in ~30 lines.
20
+ *
21
+ * 2. Portal rendering via React createPortal: the palette must visually escape
22
+ * any overflow:hidden ancestor (sidebar, main scroll area). Portaling to
23
+ * document.body is the only reliable solution; React 18's concurrent portal
24
+ * maintains the context tree so SidebarContext and theme tokens remain live.
25
+ *
26
+ * 3. Overlay blur for depth: a semi-opaque backdrop with backdrop-filter:blur
27
+ * gives the palette "above everything" depth without a hard black overlay
28
+ * that would feel oppressive in an already-dark UI.
29
+ *
30
+ * 4. Fuzzy filter: a simple substring match across label + keywords. For
31
+ * production, swap the `filterItems` function with a Fuse.js call or a
32
+ * server-side search endpoint -- the interface is identical.
33
+ *
34
+ * 5. Focus trap: while open, Tab and Shift+Tab cycle through the input and
35
+ * results. Escape closes. Arrow Up/Down navigate results. Enter activates.
36
+ * All keyboard affordances are listed in the sr-only description so screen
37
+ * reader users know the contract on first open.
38
+ *
39
+ * 6. Reduced-motion: framer-motion variants collapse to opacity-only when
40
+ * prefers-reduced-motion is detected via the useReducedMotion hook.
41
+ *
42
+ * ANIMATION RATIONALE
43
+ * -------------------
44
+ * Entering: scale 0.96 -> 1 + opacity 0 -> 1, ease-out (decelerate to rest).
45
+ * Exiting: scale 1 -> 0.96 + opacity 1 -> 0, ease-in (accelerate away).
46
+ * Duration: 200ms enter (duration-normal), 150ms exit (duration-fast).
47
+ * Backdrop: opacity only, duration-normal.
48
+ *
49
+ * TOKEN USAGE
50
+ * -----------
51
+ * All colors: CSS custom properties from globals.css semantic layer.
52
+ * All durations: CSS custom properties (mirrored in DURATION from brand.ts).
53
+ * No raw hex, no raw millisecond literals.
54
+ */
55
+
56
+ 'use client';
57
+
58
+ import React, {
59
+ useState,
60
+ useEffect,
61
+ useCallback,
62
+ useRef,
63
+ useId,
64
+ createContext,
65
+ useContext,
66
+ type KeyboardEvent,
67
+ } from 'react';
68
+ import { createPortal } from 'react-dom';
69
+ import { AnimatePresence, motion, useReducedMotion } from 'framer-motion';
70
+ import {
71
+ Search,
72
+ LayoutDashboard,
73
+ Users,
74
+ BarChart2,
75
+ Settings,
76
+ FileText,
77
+ Bell,
78
+ LogOut,
79
+ ChevronRight,
80
+ Zap,
81
+ } from 'lucide-react';
82
+ import { DURATION, EASING } from '@/lib/brand';
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // Types
86
+ // ---------------------------------------------------------------------------
87
+
88
+ export interface CommandItem {
89
+ id: string;
90
+ label: string;
91
+ /** Short description shown below label */
92
+ description?: string;
93
+ /** Lucide React node, rendered at 16px */
94
+ icon?: React.ReactNode;
95
+ /** Group this item belongs to */
96
+ group: string;
97
+ /** Extra tokens for fuzzy matching that don't appear in UI */
98
+ keywords?: string[];
99
+ /** Called on selection */
100
+ onSelect: () => void;
101
+ /** Optional keyboard shortcut hint (display only) */
102
+ shortcut?: string[];
103
+ }
104
+
105
+ export interface CommandGroup {
106
+ id: string;
107
+ label: string;
108
+ items: CommandItem[];
109
+ }
110
+
111
+ // ---------------------------------------------------------------------------
112
+ // Context (allows Topbar / anywhere to open the palette)
113
+ // ---------------------------------------------------------------------------
114
+
115
+ interface CommandPaletteContextValue {
116
+ open: () => void;
117
+ close: () => void;
118
+ isOpen: boolean;
119
+ }
120
+
121
+ const CommandPaletteContext = createContext<CommandPaletteContextValue>({
122
+ open: () => undefined,
123
+ close: () => undefined,
124
+ isOpen: false,
125
+ });
126
+
127
+ export function useCommandPalette(): CommandPaletteContextValue {
128
+ return useContext(CommandPaletteContext);
129
+ }
130
+
131
+ // ---------------------------------------------------------------------------
132
+ // Fuzzy filter
133
+ // ---------------------------------------------------------------------------
134
+
135
+ function filterItems(items: CommandItem[], query: string): CommandItem[] {
136
+ if (!query.trim()) return items;
137
+ const q = query.toLowerCase();
138
+ return items.filter((item) => {
139
+ const haystack = [
140
+ item.label,
141
+ item.description ?? '',
142
+ ...(item.keywords ?? []),
143
+ ]
144
+ .join(' ')
145
+ .toLowerCase();
146
+ return haystack.includes(q);
147
+ });
148
+ }
149
+
150
+ function groupByGroup(
151
+ items: CommandItem[],
152
+ allGroups: CommandGroup[],
153
+ ): CommandGroup[] {
154
+ const grouped = new Map<string, CommandItem[]>();
155
+ for (const item of items) {
156
+ const arr = grouped.get(item.group) ?? [];
157
+ arr.push(item);
158
+ grouped.set(item.group, arr);
159
+ }
160
+ // Preserve original group ordering
161
+ return allGroups
162
+ .map((g) => ({ ...g, items: grouped.get(g.id) ?? [] }))
163
+ .filter((g) => g.items.length > 0);
164
+ }
165
+
166
+ // ---------------------------------------------------------------------------
167
+ // Default command set (demo data; replace or extend per project)
168
+ // ---------------------------------------------------------------------------
169
+
170
+ const DEFAULT_GROUPS: CommandGroup[] = [
171
+ {
172
+ id: 'navigation',
173
+ label: 'Navigation',
174
+ items: [
175
+ {
176
+ id: 'nav-dashboard',
177
+ label: 'Dashboard',
178
+ description: 'Overview and KPIs',
179
+ icon: <LayoutDashboard size={16} aria-hidden="true" />,
180
+ group: 'navigation',
181
+ keywords: ['home', 'overview', 'metrics'],
182
+ onSelect: () => { window.location.href = '/dashboard'; },
183
+ shortcut: ['G', 'D'],
184
+ },
185
+ {
186
+ id: 'nav-users',
187
+ label: 'Users',
188
+ description: 'Manage team members',
189
+ icon: <Users size={16} aria-hidden="true" />,
190
+ group: 'navigation',
191
+ keywords: ['people', 'team', 'members', 'accounts'],
192
+ onSelect: () => { window.location.href = '/dashboard/users'; },
193
+ shortcut: ['G', 'U'],
194
+ },
195
+ {
196
+ id: 'nav-analytics',
197
+ label: 'Analytics',
198
+ description: 'Charts and reports',
199
+ icon: <BarChart2 size={16} aria-hidden="true" />,
200
+ group: 'navigation',
201
+ keywords: ['charts', 'reports', 'data', 'graphs'],
202
+ onSelect: () => { window.location.href = '/dashboard/analytics'; },
203
+ shortcut: ['G', 'A'],
204
+ },
205
+ ],
206
+ },
207
+ {
208
+ id: 'actions',
209
+ label: 'Quick Actions',
210
+ items: [
211
+ {
212
+ id: 'action-new-report',
213
+ label: 'New Report',
214
+ description: 'Create a custom analytics report',
215
+ icon: <FileText size={16} aria-hidden="true" />,
216
+ group: 'actions',
217
+ keywords: ['create', 'report', 'analytics', 'export'],
218
+ onSelect: () => { /* trigger report modal */ },
219
+ },
220
+ {
221
+ id: 'action-invite',
222
+ label: 'Invite Team Member',
223
+ description: 'Send an invitation email',
224
+ icon: <Zap size={16} aria-hidden="true" />,
225
+ group: 'actions',
226
+ keywords: ['invite', 'add user', 'team', 'email'],
227
+ onSelect: () => { /* trigger invite modal */ },
228
+ },
229
+ {
230
+ id: 'action-notifications',
231
+ label: 'Notifications',
232
+ description: 'View recent alerts',
233
+ icon: <Bell size={16} aria-hidden="true" />,
234
+ group: 'actions',
235
+ keywords: ['alerts', 'updates', 'messages'],
236
+ onSelect: () => { window.location.href = '/dashboard/notifications'; },
237
+ },
238
+ ],
239
+ },
240
+ {
241
+ id: 'settings',
242
+ label: 'Settings',
243
+ items: [
244
+ {
245
+ id: 'settings-general',
246
+ label: 'General Settings',
247
+ description: 'Account and workspace preferences',
248
+ icon: <Settings size={16} aria-hidden="true" />,
249
+ group: 'settings',
250
+ keywords: ['preferences', 'account', 'workspace', 'config'],
251
+ onSelect: () => { window.location.href = '/dashboard/settings'; },
252
+ },
253
+ {
254
+ id: 'settings-logout',
255
+ label: 'Sign Out',
256
+ description: 'End your current session',
257
+ icon: <LogOut size={16} aria-hidden="true" />,
258
+ group: 'settings',
259
+ keywords: ['logout', 'signout', 'exit', 'session'],
260
+ onSelect: () => { /* trigger sign-out */ },
261
+ },
262
+ ],
263
+ },
264
+ ];
265
+
266
+ const ALL_ITEMS: CommandItem[] = DEFAULT_GROUPS.flatMap((g) => g.items);
267
+
268
+ // ---------------------------------------------------------------------------
269
+ // CommandPaletteProvider (wraps layout)
270
+ // ---------------------------------------------------------------------------
271
+
272
+ export function CommandPaletteProvider({
273
+ children,
274
+ }: {
275
+ children: React.ReactNode;
276
+ }) {
277
+ const [isOpen, setIsOpen] = useState(false);
278
+
279
+ const open = useCallback(() => setIsOpen(true), []);
280
+ const close = useCallback(() => setIsOpen(false), []);
281
+
282
+ // Global Cmd+K / Ctrl+K handler
283
+ useEffect(() => {
284
+ function handleKeyDown(e: globalThis.KeyboardEvent) {
285
+ if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
286
+ e.preventDefault();
287
+ setIsOpen((prev) => !prev);
288
+ }
289
+ }
290
+ window.addEventListener('keydown', handleKeyDown);
291
+ return () => window.removeEventListener('keydown', handleKeyDown);
292
+ }, []);
293
+
294
+ return (
295
+ <CommandPaletteContext.Provider value={{ open, close, isOpen }}>
296
+ {children}
297
+ <AnimatePresence>
298
+ {isOpen && <CommandPaletteImpl onClose={close} />}
299
+ </AnimatePresence>
300
+ </CommandPaletteContext.Provider>
301
+ );
302
+ }
303
+
304
+ // ---------------------------------------------------------------------------
305
+ // Inner implementation (rendered in portal)
306
+ // ---------------------------------------------------------------------------
307
+
308
+ function CommandPaletteImpl({ onClose }: { onClose: () => void }) {
309
+ const [query, setQuery] = useState('');
310
+ const [activeIndex, setActiveIndex] = useState(0);
311
+ const inputRef = useRef<HTMLInputElement>(null);
312
+ const listRef = useRef<HTMLUListElement>(null);
313
+ const shouldReduceMotion = useReducedMotion();
314
+ const descriptionId = useId();
315
+ const inputId = useId();
316
+
317
+ const filtered = filterItems(ALL_ITEMS, query);
318
+ const visibleGroups = groupByGroup(filtered, DEFAULT_GROUPS);
319
+
320
+ // Flatten for arrow-key navigation
321
+ const flatItems = visibleGroups.flatMap((g) => g.items);
322
+
323
+ // Reset active index when results change
324
+ useEffect(() => {
325
+ setActiveIndex(0);
326
+ }, [query]);
327
+
328
+ // Focus input on open
329
+ useEffect(() => {
330
+ inputRef.current?.focus();
331
+ }, []);
332
+
333
+ // Scroll active item into view
334
+ useEffect(() => {
335
+ const activeEl = listRef.current?.querySelector(
336
+ '[data-active="true"]',
337
+ ) as HTMLElement | null;
338
+ activeEl?.scrollIntoView({ block: 'nearest' });
339
+ }, [activeIndex]);
340
+
341
+ function handleKeyDown(e: KeyboardEvent<HTMLDivElement>) {
342
+ switch (e.key) {
343
+ case 'ArrowDown':
344
+ e.preventDefault();
345
+ setActiveIndex((i) => Math.min(i + 1, flatItems.length - 1));
346
+ break;
347
+ case 'ArrowUp':
348
+ e.preventDefault();
349
+ setActiveIndex((i) => Math.max(i - 1, 0));
350
+ break;
351
+ case 'Enter':
352
+ e.preventDefault();
353
+ if (flatItems[activeIndex]) {
354
+ flatItems[activeIndex].onSelect();
355
+ onClose();
356
+ }
357
+ break;
358
+ case 'Escape':
359
+ e.preventDefault();
360
+ onClose();
361
+ break;
362
+ }
363
+ }
364
+
365
+ // Framer-motion variants
366
+ const overlayVariants = {
367
+ hidden: { opacity: 0 },
368
+ visible: {
369
+ opacity: 1,
370
+ transition: {
371
+ duration: shouldReduceMotion ? 0 : DURATION.normal / 1000,
372
+ ease: EASING.out,
373
+ },
374
+ },
375
+ exit: {
376
+ opacity: 0,
377
+ transition: {
378
+ duration: shouldReduceMotion ? 0 : DURATION.fast / 1000,
379
+ ease: EASING.in,
380
+ },
381
+ },
382
+ };
383
+
384
+ const panelVariants = {
385
+ hidden: { opacity: 0, scale: 0.96, y: -8 },
386
+ visible: {
387
+ opacity: 1,
388
+ scale: 1,
389
+ y: 0,
390
+ transition: {
391
+ duration: shouldReduceMotion ? 0 : DURATION.normal / 1000,
392
+ ease: EASING.out,
393
+ },
394
+ },
395
+ exit: {
396
+ opacity: 0,
397
+ scale: 0.96,
398
+ y: -8,
399
+ transition: {
400
+ duration: shouldReduceMotion ? 0 : DURATION.fast / 1000,
401
+ ease: EASING.in,
402
+ },
403
+ },
404
+ };
405
+
406
+ const palette = (
407
+ <>
408
+ {/* Backdrop */}
409
+ <motion.div
410
+ aria-hidden="true"
411
+ variants={overlayVariants}
412
+ initial="hidden"
413
+ animate="visible"
414
+ exit="exit"
415
+ onClick={onClose}
416
+ style={{
417
+ position: 'fixed',
418
+ inset: 0,
419
+ backgroundColor: 'var(--color-overlay-scrim)',
420
+ backdropFilter: 'blur(12px)',
421
+ WebkitBackdropFilter: 'blur(12px)',
422
+ zIndex: 'var(--z-command)',
423
+ }}
424
+ />
425
+
426
+ {/* Panel */}
427
+ <motion.div
428
+ role="dialog"
429
+ aria-modal="true"
430
+ aria-label="Command palette"
431
+ aria-describedby={descriptionId}
432
+ variants={panelVariants}
433
+ initial="hidden"
434
+ animate="visible"
435
+ exit="exit"
436
+ onKeyDown={handleKeyDown}
437
+ style={{
438
+ position: 'fixed',
439
+ top: '15%',
440
+ left: '50%',
441
+ transform: 'translateX(-50%)',
442
+ width: 'min(560px, calc(100vw - 2rem))',
443
+ zIndex: 'calc(var(--z-command) + 1)',
444
+ backgroundColor: 'var(--color-bg-overlay)',
445
+ border: '1px solid var(--color-border-default)',
446
+ borderRadius: 'var(--radius-lg)',
447
+ boxShadow: 'var(--shadow-3)',
448
+ overflow: 'hidden',
449
+ display: 'flex',
450
+ flexDirection: 'column',
451
+ maxHeight: 'min(560px, 70vh)',
452
+ }}
453
+ >
454
+ {/* SR-only description */}
455
+ <p id={descriptionId} className="sr-only">
456
+ Type to search. Use arrow keys to navigate results. Press Enter to
457
+ select. Press Escape to close.
458
+ </p>
459
+
460
+ {/* Search input row */}
461
+ <div
462
+ style={{
463
+ display: 'flex',
464
+ alignItems: 'center',
465
+ gap: 'var(--space-3)',
466
+ padding: 'var(--space-4) var(--space-5)',
467
+ borderBottom: '1px solid var(--color-border-default)',
468
+ flexShrink: 0,
469
+ }}
470
+ >
471
+ <Search
472
+ size={18}
473
+ aria-hidden="true"
474
+ style={{ color: 'var(--color-text-muted)', flexShrink: 0 }}
475
+ />
476
+ <input
477
+ ref={inputRef}
478
+ id={inputId}
479
+ type="text"
480
+ role="combobox"
481
+ aria-expanded={filtered.length > 0}
482
+ aria-autocomplete="list"
483
+ aria-controls="command-results"
484
+ aria-activedescendant={
485
+ flatItems[activeIndex]
486
+ ? `cmd-item-${flatItems[activeIndex].id}`
487
+ : undefined
488
+ }
489
+ value={query}
490
+ onChange={(e) => setQuery(e.target.value)}
491
+ placeholder="Search commands, pages, actions..."
492
+ style={{
493
+ flex: 1,
494
+ background: 'transparent',
495
+ border: 'none',
496
+ outline: 'none',
497
+ fontSize: 'var(--type-body)',
498
+ lineHeight: 'var(--type-body-lh)',
499
+ color: 'var(--color-text-primary)',
500
+ }}
501
+ />
502
+ <kbd
503
+ aria-label="Escape to close"
504
+ style={{
505
+ display: 'inline-flex',
506
+ alignItems: 'center',
507
+ justifyContent: 'center',
508
+ height: '20px',
509
+ minWidth: '20px',
510
+ padding: '0 var(--space-1)',
511
+ borderRadius: 'var(--radius-sharp)',
512
+ border: '1px solid var(--color-border-default)',
513
+ backgroundColor: 'var(--color-bg-sunken)',
514
+ fontSize: 'var(--type-caption)',
515
+ color: 'var(--color-text-muted)',
516
+ fontFamily: 'var(--font-mono)',
517
+ lineHeight: '1',
518
+ flexShrink: 0,
519
+ }}
520
+ >
521
+ Esc
522
+ </kbd>
523
+ </div>
524
+
525
+ {/* Results */}
526
+ <ul
527
+ ref={listRef}
528
+ id="command-results"
529
+ role="listbox"
530
+ aria-label="Search results"
531
+ style={{
532
+ overflowY: 'auto',
533
+ flex: 1,
534
+ padding: 'var(--space-2) 0',
535
+ listStyle: 'none',
536
+ margin: 0,
537
+ }}
538
+ >
539
+ {visibleGroups.length === 0 && (
540
+ <li
541
+ role="option"
542
+ aria-selected="false"
543
+ style={{
544
+ padding: 'var(--space-8) var(--space-5)',
545
+ textAlign: 'center',
546
+ fontSize: 'var(--type-small)',
547
+ color: 'var(--color-text-muted)',
548
+ }}
549
+ >
550
+ No results for &ldquo;{query}&rdquo;
551
+ </li>
552
+ )}
553
+
554
+ {visibleGroups.map((group) => {
555
+ let itemIndex = 0;
556
+ // Compute offset for this group in flatItems
557
+ const groupOffset = flatItems.findIndex((fi) =>
558
+ group.items.some((gi) => gi.id === fi.id),
559
+ );
560
+
561
+ return (
562
+ <li key={group.id} role="presentation">
563
+ {/* Group label */}
564
+ <div
565
+ role="presentation"
566
+ style={{
567
+ padding: 'var(--space-1) var(--space-5)',
568
+ fontSize: 'var(--type-caption)',
569
+ lineHeight: 'var(--type-caption-lh)',
570
+ letterSpacing: '0.06em',
571
+ textTransform: 'uppercase',
572
+ color: 'var(--color-text-muted)',
573
+ fontWeight: 600,
574
+ userSelect: 'none',
575
+ marginTop: 'var(--space-1)',
576
+ }}
577
+ >
578
+ {group.label}
579
+ </div>
580
+
581
+ {/* Group items */}
582
+ <ul role="presentation" style={{ listStyle: 'none', margin: 0, padding: 0 }}>
583
+ {group.items.map((item) => {
584
+ const flatIdx = groupOffset + (itemIndex++);
585
+ const isActive = flatIdx === activeIndex;
586
+
587
+ return (
588
+ <li
589
+ key={item.id}
590
+ id={`cmd-item-${item.id}`}
591
+ role="option"
592
+ aria-selected={isActive}
593
+ data-active={isActive ? 'true' : 'false'}
594
+ onClick={() => {
595
+ item.onSelect();
596
+ onClose();
597
+ }}
598
+ onMouseMove={() => setActiveIndex(flatIdx)}
599
+ style={{
600
+ display: 'flex',
601
+ alignItems: 'center',
602
+ gap: 'var(--space-3)',
603
+ padding: 'var(--space-2) var(--space-5)',
604
+ cursor: 'pointer',
605
+ backgroundColor: isActive
606
+ ? 'var(--color-bg-highlight)'
607
+ : 'transparent',
608
+ transition: `background-color var(--duration-instant) var(--ease-out)`,
609
+ minHeight: '36px',
610
+ borderRadius: 0,
611
+ }}
612
+ >
613
+ {/* Icon */}
614
+ {item.icon && (
615
+ <span
616
+ aria-hidden="true"
617
+ style={{ color: isActive ? 'var(--color-text-accent)' : 'var(--color-text-muted)', flexShrink: 0, display: 'flex' }}
618
+ >
619
+ {item.icon}
620
+ </span>
621
+ )}
622
+
623
+ {/* Label + description */}
624
+ <span style={{ flex: 1, minWidth: 0 }}>
625
+ <span
626
+ style={{
627
+ display: 'block',
628
+ fontSize: 'var(--type-small)',
629
+ lineHeight: 'var(--type-small-lh)',
630
+ color: isActive
631
+ ? 'var(--color-text-primary)'
632
+ : 'var(--color-text-secondary)',
633
+ fontWeight: isActive ? 500 : 400,
634
+ overflow: 'hidden',
635
+ textOverflow: 'ellipsis',
636
+ whiteSpace: 'nowrap',
637
+ }}
638
+ >
639
+ {item.label}
640
+ </span>
641
+ {item.description && (
642
+ <span
643
+ style={{
644
+ display: 'block',
645
+ fontSize: 'var(--type-caption)',
646
+ lineHeight: 'var(--type-caption-lh)',
647
+ color: 'var(--color-text-muted)',
648
+ overflow: 'hidden',
649
+ textOverflow: 'ellipsis',
650
+ whiteSpace: 'nowrap',
651
+ }}
652
+ >
653
+ {item.description}
654
+ </span>
655
+ )}
656
+ </span>
657
+
658
+ {/* Shortcut hint */}
659
+ {item.shortcut && (
660
+ <span
661
+ aria-hidden="true"
662
+ style={{ display: 'flex', gap: 'var(--space-1)', flexShrink: 0 }}
663
+ >
664
+ {item.shortcut.map((key) => (
665
+ <kbd
666
+ key={key}
667
+ style={{
668
+ display: 'inline-flex',
669
+ alignItems: 'center',
670
+ justifyContent: 'center',
671
+ height: '18px',
672
+ minWidth: '18px',
673
+ padding: '0 var(--space-1)',
674
+ borderRadius: 'var(--radius-sharp)',
675
+ border: '1px solid var(--color-border-default)',
676
+ backgroundColor: 'var(--color-bg-sunken)',
677
+ fontSize: '11px',
678
+ color: 'var(--color-text-muted)',
679
+ fontFamily: 'var(--font-mono)',
680
+ }}
681
+ >
682
+ {key}
683
+ </kbd>
684
+ ))}
685
+ </span>
686
+ )}
687
+
688
+ {/* Active indicator chevron */}
689
+ {isActive && (
690
+ <ChevronRight
691
+ size={14}
692
+ aria-hidden="true"
693
+ style={{ color: 'var(--color-text-accent)', flexShrink: 0 }}
694
+ />
695
+ )}
696
+ </li>
697
+ );
698
+ })}
699
+ </ul>
700
+ </li>
701
+ );
702
+ })}
703
+ </ul>
704
+
705
+ {/* Footer hint */}
706
+ <div
707
+ aria-hidden="true"
708
+ style={{
709
+ display: 'flex',
710
+ alignItems: 'center',
711
+ gap: 'var(--space-4)',
712
+ padding: 'var(--space-2) var(--space-5)',
713
+ borderTop: '1px solid var(--color-border-default)',
714
+ flexShrink: 0,
715
+ }}
716
+ >
717
+ {[
718
+ { keys: ['arrowup', 'arrowdown'], label: 'Navigate' },
719
+ { keys: ['enter'], label: 'Select' },
720
+ { keys: ['esc'], label: 'Close' },
721
+ ].map(({ keys, label }) => (
722
+ <span
723
+ key={label}
724
+ style={{
725
+ display: 'flex',
726
+ alignItems: 'center',
727
+ gap: 'var(--space-1)',
728
+ fontSize: 'var(--type-caption)',
729
+ color: 'var(--color-text-muted)',
730
+ }}
731
+ >
732
+ {keys.map((k) => (
733
+ <kbd
734
+ key={k}
735
+ style={{
736
+ display: 'inline-flex',
737
+ alignItems: 'center',
738
+ justifyContent: 'center',
739
+ height: '16px',
740
+ minWidth: '16px',
741
+ padding: '0 3px',
742
+ borderRadius: 'var(--radius-sharp)',
743
+ border: '1px solid var(--color-border-default)',
744
+ backgroundColor: 'var(--color-bg-sunken)',
745
+ fontSize: '10px',
746
+ fontFamily: 'var(--font-mono)',
747
+ color: 'var(--color-text-muted)',
748
+ }}
749
+ >
750
+ {k === 'arrowup' ? '↑' : k === 'arrowdown' ? '↓' : k === 'enter' ? '⏎' : k}
751
+ </kbd>
752
+ ))}
753
+ <span>{label}</span>
754
+ </span>
755
+ ))}
756
+ </div>
757
+ </motion.div>
758
+ </>
759
+ );
760
+
761
+ // Portal to body to escape any overflow/stacking context
762
+ return typeof document !== 'undefined'
763
+ ? createPortal(palette, document.body)
764
+ : null;
765
+ }
766
+
767
+ export default CommandPaletteProvider;