chati-dev 4.3.1 → 4.4.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 (147) hide show
  1. package/README.md +18 -13
  2. package/bin/chati.js +12 -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 +58 -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/orchestrator/chati-router.js +1 -1
  40. package/framework/orchestrator/chati-update.md +19 -3
  41. package/framework/orchestrator/chati.md +19 -1
  42. package/framework/quality-gates/planning-gate.md +2 -0
  43. package/framework/scaffold/motion-premium/scaffold.yaml +2 -1
  44. package/framework/scaffold/motion-premium-3d/components/webgl/WebGLContext.tsx.template +1 -1
  45. package/framework/scaffold/saas-dashboard/README.md +58 -0
  46. package/framework/scaffold/saas-dashboard/app/dashboard/page.tsx.template +546 -0
  47. package/framework/scaffold/saas-dashboard/app/globals.css.template +746 -0
  48. package/framework/scaffold/saas-dashboard/app/layout.tsx.template +192 -0
  49. package/framework/scaffold/saas-dashboard/app/shell.css.template +1070 -0
  50. package/framework/scaffold/saas-dashboard/components/data/Chart.tsx.template +725 -0
  51. package/framework/scaffold/saas-dashboard/components/data/DataTable.tsx.template +623 -0
  52. package/framework/scaffold/saas-dashboard/components/data/DataTable.types.ts.template +104 -0
  53. package/framework/scaffold/saas-dashboard/components/data/DataTablePagination.tsx.template +234 -0
  54. package/framework/scaffold/saas-dashboard/components/data/SkeletonBlock.tsx.template +111 -0
  55. package/framework/scaffold/saas-dashboard/components/data/StatCard.tsx.template +393 -0
  56. package/framework/scaffold/saas-dashboard/components/data/StatCardGrid.tsx.template +121 -0
  57. package/framework/scaffold/saas-dashboard/components/feedback/EmptyState.tsx.template +281 -0
  58. package/framework/scaffold/saas-dashboard/components/interaction/CommandPalette.tsx.template +767 -0
  59. package/framework/scaffold/saas-dashboard/components/interaction/Field.tsx.template +351 -0
  60. package/framework/scaffold/saas-dashboard/components/interaction/Form.tsx.template +424 -0
  61. package/framework/scaffold/saas-dashboard/components/primitives/Badge.tsx.template +193 -0
  62. package/framework/scaffold/saas-dashboard/components/primitives/Button.tsx.template +261 -0
  63. package/framework/scaffold/saas-dashboard/components/primitives/Card.tsx.template +287 -0
  64. package/framework/scaffold/saas-dashboard/components/primitives/Input.tsx.template +310 -0
  65. package/framework/scaffold/saas-dashboard/components/primitives/Spinner.tsx.template +135 -0
  66. package/framework/scaffold/saas-dashboard/components/primitives/index.ts.template +60 -0
  67. package/framework/scaffold/saas-dashboard/components/shell/AppShell.tsx.template +174 -0
  68. package/framework/scaffold/saas-dashboard/components/shell/Breadcrumb.tsx.template +186 -0
  69. package/framework/scaffold/saas-dashboard/components/shell/Container.tsx.template +86 -0
  70. package/framework/scaffold/saas-dashboard/components/shell/NavItem.tsx.template +147 -0
  71. package/framework/scaffold/saas-dashboard/components/shell/Sidebar.tsx.template +378 -0
  72. package/framework/scaffold/saas-dashboard/components/shell/SidebarContext.tsx.template +169 -0
  73. package/framework/scaffold/saas-dashboard/components/shell/ThemeToggle.tsx.template +128 -0
  74. package/framework/scaffold/saas-dashboard/components/shell/Topbar.tsx.template +301 -0
  75. package/framework/scaffold/saas-dashboard/lib/brand.ts.template +324 -0
  76. package/framework/scaffold/saas-dashboard/scaffold.yaml +225 -0
  77. package/framework/schemas/session.schema.json +5 -0
  78. package/framework/schemas/task.schema.json +9 -0
  79. package/framework/scripts/visual-qa.js +87 -1
  80. package/framework/tasks/orchestrator-health.md +4 -4
  81. package/framework/tasks/qa-impl-verdict.md +11 -5
  82. package/framework/templates/qa-gate-tmpl.yaml +24 -11
  83. package/package.json +3 -3
  84. package/src/config/context-file-generator.js +0 -6
  85. package/src/dashboard/renderer.js +0 -36
  86. package/src/executors/runner.js +204 -0
  87. package/src/installer/core.js +2 -1
  88. package/src/installer/templates.js +5 -6
  89. package/src/intelligence/registry-manager.js +1 -1
  90. package/src/license/client.js +9 -3
  91. package/src/license/commands.js +12 -2
  92. package/src/license/machine-id.js +42 -1
  93. package/src/memory/gotchas.js +58 -40
  94. package/src/memory/magic-docs.js +1 -1
  95. package/src/memory/session-digest.js +9 -4
  96. package/src/orchestrator/cli.js +174 -14
  97. package/src/orchestrator/doctor.js +98 -16
  98. package/src/orchestrator/pipeline-manager.js +44 -22
  99. package/src/orchestrator/session-manager.js +56 -8
  100. package/src/telemetry/sender.js +7 -19
  101. package/src/terminal/run-team.js +3 -3
  102. package/src/upgrade/tracked-files-detector.js +34 -16
  103. package/src/utils/feature-flags.js +1 -1
  104. package/src/utils/flatten-entities.js +4 -40
  105. package/src/utils/schema-validator.js +0 -14
  106. package/src/wizard/i18n.js +3 -3
  107. package/src/api/index.js +0 -120
  108. package/src/autonomy/autonomous-gate.js +0 -294
  109. package/src/autonomy/build-loop.js +0 -281
  110. package/src/autonomy/build-state.js +0 -286
  111. package/src/autonomy/cause-analyzer.js +0 -177
  112. package/src/autonomy/escalation.js +0 -214
  113. package/src/autonomy/index.js +0 -51
  114. package/src/autonomy/mode-manager.js +0 -225
  115. package/src/autonomy/mode-suggester.js +0 -283
  116. package/src/autonomy/progress-reporter.js +0 -275
  117. package/src/autonomy/safety-net.js +0 -370
  118. package/src/config/agent-customizer.js +0 -231
  119. package/src/decision/analyzer.js +0 -291
  120. package/src/decision/engine.js +0 -250
  121. package/src/decision/index.js +0 -38
  122. package/src/decision/registry-healer.js +0 -468
  123. package/src/decision/registry-updater.js +0 -339
  124. package/src/extensions/loader.js +0 -145
  125. package/src/extensions/registry.js +0 -134
  126. package/src/gates/circuit-breaker.js +0 -151
  127. package/src/gates/g1-planning-complete.js +0 -154
  128. package/src/gates/g2-qa-planning.js +0 -156
  129. package/src/gates/g3-implementation.js +0 -215
  130. package/src/gates/g4-qa-implementation.js +0 -240
  131. package/src/gates/g5-deploy-ready.js +0 -181
  132. package/src/gates/gate-base.js +0 -185
  133. package/src/gates/index.js +0 -46
  134. package/src/health/auto-fix.js +0 -216
  135. package/src/health/engine.js +0 -246
  136. package/src/merger/semantic-merger.js +0 -292
  137. package/src/preview/detector.js +0 -238
  138. package/src/preview/index.js +0 -20
  139. package/src/preview/launcher.js +0 -235
  140. package/src/preview/log-buffer.js +0 -103
  141. package/src/quality/metrics-collector.js +0 -281
  142. package/src/quality/test-runner.js +0 -366
  143. package/src/tasks/executor.js +0 -195
  144. package/src/tasks/index.js +0 -4
  145. package/src/tasks/loader.js +0 -210
  146. package/src/tasks/router.js +0 -182
  147. 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;