@fragments-sdk/cli 0.2.2

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 (259) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +106 -0
  3. package/dist/bin.d.ts +1 -0
  4. package/dist/bin.js +4783 -0
  5. package/dist/bin.js.map +1 -0
  6. package/dist/chunk-4FDQSGKX.js +786 -0
  7. package/dist/chunk-4FDQSGKX.js.map +1 -0
  8. package/dist/chunk-7H2MMGYG.js +369 -0
  9. package/dist/chunk-7H2MMGYG.js.map +1 -0
  10. package/dist/chunk-BSCG3IP7.js +619 -0
  11. package/dist/chunk-BSCG3IP7.js.map +1 -0
  12. package/dist/chunk-LY2CFFPY.js +898 -0
  13. package/dist/chunk-LY2CFFPY.js.map +1 -0
  14. package/dist/chunk-MUZ6CM66.js +6636 -0
  15. package/dist/chunk-MUZ6CM66.js.map +1 -0
  16. package/dist/chunk-OAENNG3G.js +1489 -0
  17. package/dist/chunk-OAENNG3G.js.map +1 -0
  18. package/dist/chunk-XHNKNI6J.js +235 -0
  19. package/dist/chunk-XHNKNI6J.js.map +1 -0
  20. package/dist/core-DWKLGY4N.js +68 -0
  21. package/dist/core-DWKLGY4N.js.map +1 -0
  22. package/dist/generate-4LQNJ7SX.js +249 -0
  23. package/dist/generate-4LQNJ7SX.js.map +1 -0
  24. package/dist/index.d.ts +775 -0
  25. package/dist/index.js +41 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/init-EMVI47QG.js +416 -0
  28. package/dist/init-EMVI47QG.js.map +1 -0
  29. package/dist/mcp-bin.d.ts +1 -0
  30. package/dist/mcp-bin.js +1117 -0
  31. package/dist/mcp-bin.js.map +1 -0
  32. package/dist/scan-4YPRF7FV.js +12 -0
  33. package/dist/scan-4YPRF7FV.js.map +1 -0
  34. package/dist/service-QSZMZJBJ.js +208 -0
  35. package/dist/service-QSZMZJBJ.js.map +1 -0
  36. package/dist/static-viewer-MIPGZ4Z7.js +12 -0
  37. package/dist/static-viewer-MIPGZ4Z7.js.map +1 -0
  38. package/dist/test-SQ5ZHXWU.js +1067 -0
  39. package/dist/test-SQ5ZHXWU.js.map +1 -0
  40. package/dist/tokens-HSGMYK64.js +173 -0
  41. package/dist/tokens-HSGMYK64.js.map +1 -0
  42. package/dist/viewer-YRF4SQE4.js +11101 -0
  43. package/dist/viewer-YRF4SQE4.js.map +1 -0
  44. package/package.json +107 -0
  45. package/src/ai.ts +266 -0
  46. package/src/analyze.ts +265 -0
  47. package/src/bin.ts +916 -0
  48. package/src/build.ts +248 -0
  49. package/src/commands/a11y.ts +302 -0
  50. package/src/commands/add.ts +313 -0
  51. package/src/commands/audit.ts +195 -0
  52. package/src/commands/baseline.ts +221 -0
  53. package/src/commands/build.ts +144 -0
  54. package/src/commands/compare.ts +337 -0
  55. package/src/commands/context.ts +107 -0
  56. package/src/commands/dev.ts +107 -0
  57. package/src/commands/enhance.ts +858 -0
  58. package/src/commands/generate.ts +391 -0
  59. package/src/commands/init.ts +531 -0
  60. package/src/commands/link/figma.ts +645 -0
  61. package/src/commands/link/index.ts +10 -0
  62. package/src/commands/link/storybook.ts +267 -0
  63. package/src/commands/list.ts +49 -0
  64. package/src/commands/metrics.ts +114 -0
  65. package/src/commands/reset.ts +242 -0
  66. package/src/commands/scan.ts +537 -0
  67. package/src/commands/storygen.ts +207 -0
  68. package/src/commands/tokens.ts +251 -0
  69. package/src/commands/validate.ts +93 -0
  70. package/src/commands/verify.ts +215 -0
  71. package/src/core/composition.test.ts +262 -0
  72. package/src/core/composition.ts +255 -0
  73. package/src/core/config.ts +84 -0
  74. package/src/core/constants.ts +111 -0
  75. package/src/core/context.ts +380 -0
  76. package/src/core/defineSegment.ts +137 -0
  77. package/src/core/discovery.ts +337 -0
  78. package/src/core/figma.ts +263 -0
  79. package/src/core/fragment-types.ts +214 -0
  80. package/src/core/generators/context.ts +389 -0
  81. package/src/core/generators/index.ts +23 -0
  82. package/src/core/generators/registry.ts +364 -0
  83. package/src/core/generators/typescript-extractor.ts +374 -0
  84. package/src/core/importAnalyzer.ts +217 -0
  85. package/src/core/index.ts +149 -0
  86. package/src/core/loader.ts +155 -0
  87. package/src/core/node.ts +63 -0
  88. package/src/core/parser.ts +551 -0
  89. package/src/core/previewLoader.ts +172 -0
  90. package/src/core/schema/fragment.schema.json +189 -0
  91. package/src/core/schema/registry.schema.json +137 -0
  92. package/src/core/schema.ts +182 -0
  93. package/src/core/storyAdapter.test.ts +571 -0
  94. package/src/core/storyAdapter.ts +761 -0
  95. package/src/core/token-types.ts +287 -0
  96. package/src/core/types.ts +754 -0
  97. package/src/diff.ts +323 -0
  98. package/src/index.ts +43 -0
  99. package/src/mcp/__tests__/projectFields.test.ts +130 -0
  100. package/src/mcp/bin.ts +36 -0
  101. package/src/mcp/index.ts +8 -0
  102. package/src/mcp/server.ts +1310 -0
  103. package/src/mcp/utils.ts +54 -0
  104. package/src/mcp-bin.ts +36 -0
  105. package/src/migrate/__tests__/argTypes/argTypes.test.ts +189 -0
  106. package/src/migrate/__tests__/args/args.test.ts +452 -0
  107. package/src/migrate/__tests__/meta/meta.test.ts +198 -0
  108. package/src/migrate/__tests__/stories/stories.test.ts +278 -0
  109. package/src/migrate/__tests__/utils/utils.test.ts +371 -0
  110. package/src/migrate/__tests__/values/values.test.ts +303 -0
  111. package/src/migrate/bin.ts +108 -0
  112. package/src/migrate/converter.ts +658 -0
  113. package/src/migrate/detect.ts +196 -0
  114. package/src/migrate/index.ts +45 -0
  115. package/src/migrate/migrate.ts +163 -0
  116. package/src/migrate/parser.ts +1136 -0
  117. package/src/migrate/report.ts +624 -0
  118. package/src/migrate/types.ts +169 -0
  119. package/src/screenshot.ts +249 -0
  120. package/src/service/__tests__/ast-utils.test.ts +426 -0
  121. package/src/service/__tests__/enhance-scanner.test.ts +200 -0
  122. package/src/service/__tests__/figma/figma.test.ts +652 -0
  123. package/src/service/__tests__/metrics-store.test.ts +409 -0
  124. package/src/service/__tests__/patch-generator.test.ts +186 -0
  125. package/src/service/__tests__/props-extractor.test.ts +365 -0
  126. package/src/service/__tests__/token-registry.test.ts +267 -0
  127. package/src/service/analytics.ts +659 -0
  128. package/src/service/ast-utils.ts +444 -0
  129. package/src/service/browser-pool.ts +339 -0
  130. package/src/service/capture.ts +267 -0
  131. package/src/service/diff.ts +279 -0
  132. package/src/service/enhance/aggregator.ts +489 -0
  133. package/src/service/enhance/cache.ts +275 -0
  134. package/src/service/enhance/codebase-scanner.ts +357 -0
  135. package/src/service/enhance/context-generator.ts +529 -0
  136. package/src/service/enhance/doc-extractor.ts +523 -0
  137. package/src/service/enhance/index.ts +131 -0
  138. package/src/service/enhance/props-extractor.ts +665 -0
  139. package/src/service/enhance/scanner.ts +445 -0
  140. package/src/service/enhance/storybook-parser.ts +552 -0
  141. package/src/service/enhance/types.ts +346 -0
  142. package/src/service/enhance/variant-renderer.ts +479 -0
  143. package/src/service/figma.ts +1008 -0
  144. package/src/service/index.ts +249 -0
  145. package/src/service/metrics-store.ts +333 -0
  146. package/src/service/patch-generator.ts +349 -0
  147. package/src/service/report.ts +854 -0
  148. package/src/service/storage.ts +401 -0
  149. package/src/service/token-fixes.ts +281 -0
  150. package/src/service/token-parser.ts +504 -0
  151. package/src/service/token-registry.ts +721 -0
  152. package/src/service/utils.ts +172 -0
  153. package/src/setup.ts +241 -0
  154. package/src/shared/command-wrapper.ts +81 -0
  155. package/src/shared/dev-server-client.ts +199 -0
  156. package/src/shared/index.ts +8 -0
  157. package/src/shared/segment-loader.ts +59 -0
  158. package/src/shared/types.ts +147 -0
  159. package/src/static-viewer.ts +715 -0
  160. package/src/test/discovery.ts +172 -0
  161. package/src/test/index.ts +281 -0
  162. package/src/test/reporters/console.ts +194 -0
  163. package/src/test/reporters/json.ts +190 -0
  164. package/src/test/reporters/junit.ts +186 -0
  165. package/src/test/runner.ts +598 -0
  166. package/src/test/types.ts +245 -0
  167. package/src/test/watch.ts +200 -0
  168. package/src/validators.ts +152 -0
  169. package/src/viewer/__tests__/jsx-parser.test.ts +502 -0
  170. package/src/viewer/__tests__/render-utils.test.ts +232 -0
  171. package/src/viewer/__tests__/style-utils.test.ts +404 -0
  172. package/src/viewer/bin.ts +86 -0
  173. package/src/viewer/cli/health.ts +256 -0
  174. package/src/viewer/cli/index.ts +33 -0
  175. package/src/viewer/cli/scan.ts +124 -0
  176. package/src/viewer/cli/utils.ts +174 -0
  177. package/src/viewer/components/AccessibilityPanel.tsx +1404 -0
  178. package/src/viewer/components/ActionCapture.tsx +172 -0
  179. package/src/viewer/components/ActionsPanel.tsx +371 -0
  180. package/src/viewer/components/App.tsx +638 -0
  181. package/src/viewer/components/BottomPanel.tsx +224 -0
  182. package/src/viewer/components/CodePanel.tsx +589 -0
  183. package/src/viewer/components/CommandPalette.tsx +336 -0
  184. package/src/viewer/components/ComponentGraph.tsx +394 -0
  185. package/src/viewer/components/ComponentHeader.tsx +85 -0
  186. package/src/viewer/components/ContractPanel.tsx +234 -0
  187. package/src/viewer/components/ErrorBoundary.tsx +85 -0
  188. package/src/viewer/components/FigmaEmbed.tsx +231 -0
  189. package/src/viewer/components/FragmentEditor.tsx +485 -0
  190. package/src/viewer/components/HealthDashboard.tsx +452 -0
  191. package/src/viewer/components/HmrStatusIndicator.tsx +71 -0
  192. package/src/viewer/components/Icons.tsx +417 -0
  193. package/src/viewer/components/InteractionsPanel.tsx +720 -0
  194. package/src/viewer/components/IsolatedPreviewFrame.tsx +321 -0
  195. package/src/viewer/components/IsolatedRender.tsx +111 -0
  196. package/src/viewer/components/KeyboardShortcutsHelp.tsx +89 -0
  197. package/src/viewer/components/LandingPage.tsx +441 -0
  198. package/src/viewer/components/Layout.tsx +22 -0
  199. package/src/viewer/components/LeftSidebar.tsx +391 -0
  200. package/src/viewer/components/MultiViewportPreview.tsx +429 -0
  201. package/src/viewer/components/PreviewArea.tsx +404 -0
  202. package/src/viewer/components/PreviewFrameHost.tsx +310 -0
  203. package/src/viewer/components/PreviewPane.tsx +150 -0
  204. package/src/viewer/components/PreviewToolbar.tsx +176 -0
  205. package/src/viewer/components/PropsEditor.tsx +512 -0
  206. package/src/viewer/components/PropsTable.tsx +98 -0
  207. package/src/viewer/components/RelationsSection.tsx +57 -0
  208. package/src/viewer/components/ResizablePanel.tsx +328 -0
  209. package/src/viewer/components/RightSidebar.tsx +118 -0
  210. package/src/viewer/components/ScreenshotButton.tsx +90 -0
  211. package/src/viewer/components/Sidebar.tsx +169 -0
  212. package/src/viewer/components/SkeletonLoader.tsx +156 -0
  213. package/src/viewer/components/StoryRenderer.tsx +128 -0
  214. package/src/viewer/components/ThemeProvider.tsx +96 -0
  215. package/src/viewer/components/Toast.tsx +67 -0
  216. package/src/viewer/components/TokenStylePanel.tsx +708 -0
  217. package/src/viewer/components/UsageSection.tsx +95 -0
  218. package/src/viewer/components/VariantMatrix.tsx +350 -0
  219. package/src/viewer/components/VariantRenderer.tsx +131 -0
  220. package/src/viewer/components/VariantTabs.tsx +84 -0
  221. package/src/viewer/components/ViewportSelector.tsx +165 -0
  222. package/src/viewer/components/_future/CreatePage.tsx +836 -0
  223. package/src/viewer/composition-renderer.ts +381 -0
  224. package/src/viewer/constants/index.ts +1 -0
  225. package/src/viewer/constants/ui.ts +185 -0
  226. package/src/viewer/entry.tsx +299 -0
  227. package/src/viewer/hooks/index.ts +2 -0
  228. package/src/viewer/hooks/useA11yCache.ts +383 -0
  229. package/src/viewer/hooks/useA11yService.ts +498 -0
  230. package/src/viewer/hooks/useActions.ts +138 -0
  231. package/src/viewer/hooks/useAppState.ts +124 -0
  232. package/src/viewer/hooks/useFigmaIntegration.ts +132 -0
  233. package/src/viewer/hooks/useHmrStatus.ts +109 -0
  234. package/src/viewer/hooks/useKeyboardShortcuts.ts +222 -0
  235. package/src/viewer/hooks/usePreviewBridge.ts +347 -0
  236. package/src/viewer/hooks/useScrollSpy.ts +78 -0
  237. package/src/viewer/hooks/useUrlState.ts +330 -0
  238. package/src/viewer/hooks/useViewSettings.ts +125 -0
  239. package/src/viewer/index.html +28 -0
  240. package/src/viewer/index.ts +14 -0
  241. package/src/viewer/intelligence/healthReport.ts +505 -0
  242. package/src/viewer/intelligence/styleDrift.ts +340 -0
  243. package/src/viewer/intelligence/usageScanner.ts +309 -0
  244. package/src/viewer/jsx-parser.ts +485 -0
  245. package/src/viewer/postcss.config.js +6 -0
  246. package/src/viewer/preview-frame-entry.tsx +25 -0
  247. package/src/viewer/preview-frame.html +109 -0
  248. package/src/viewer/render-template.html +68 -0
  249. package/src/viewer/render-utils.ts +170 -0
  250. package/src/viewer/server.ts +276 -0
  251. package/src/viewer/style-utils.ts +414 -0
  252. package/src/viewer/styles/globals.css +355 -0
  253. package/src/viewer/tailwind.config.js +37 -0
  254. package/src/viewer/types/a11y.ts +197 -0
  255. package/src/viewer/utils/a11y-fixes.ts +471 -0
  256. package/src/viewer/utils/actionExport.ts +372 -0
  257. package/src/viewer/utils/colorSchemes.ts +201 -0
  258. package/src/viewer/utils/detectRelationships.ts +256 -0
  259. package/src/viewer/vite-plugin.ts +2143 -0
@@ -0,0 +1,336 @@
1
+ /**
2
+ * Command Palette - Cmd+K Quick Switcher
3
+ *
4
+ * A keyboard-driven command palette for:
5
+ * - Quick navigation to components
6
+ * - Fuzzy search across all components and variants
7
+ * - Keyboard navigation (arrows, enter, escape)
8
+ */
9
+
10
+ import { useState, useEffect, useRef, useMemo, useCallback } from "react";
11
+ import clsx from "clsx";
12
+ import type { SegmentDefinition } from "../../core/index.js";
13
+ import { SearchIcon, ChevronRightIcon } from "./Icons.js";
14
+
15
+ interface CommandPaletteProps {
16
+ /** Whether the palette is open */
17
+ isOpen: boolean;
18
+ /** Callback to close the palette */
19
+ onClose: () => void;
20
+ /** All available segments */
21
+ segments: Array<{ path: string; segment: SegmentDefinition }>;
22
+ /** Callback when a component is selected */
23
+ onSelectComponent: (path: string) => void;
24
+ /** Callback when a variant is selected */
25
+ onSelectVariant: (path: string, variantIndex: number) => void;
26
+ }
27
+
28
+ interface SearchResult {
29
+ type: "component" | "variant";
30
+ path: string;
31
+ componentName: string;
32
+ variantName?: string;
33
+ variantIndex?: number;
34
+ category: string;
35
+ score: number;
36
+ }
37
+
38
+ export function CommandPalette({
39
+ isOpen,
40
+ onClose,
41
+ segments,
42
+ onSelectComponent,
43
+ onSelectVariant,
44
+ }: CommandPaletteProps) {
45
+ const [query, setQuery] = useState("");
46
+ const [selectedIndex, setSelectedIndex] = useState(0);
47
+ const inputRef = useRef<HTMLInputElement>(null);
48
+ const listRef = useRef<HTMLDivElement>(null);
49
+
50
+ // Build search results with fuzzy matching
51
+ const results = useMemo(() => {
52
+ const allResults: SearchResult[] = [];
53
+
54
+ for (const { path, segment } of segments) {
55
+ const componentName = segment.meta.name;
56
+ const category = segment.meta.category;
57
+
58
+ // Add component result
59
+ allResults.push({
60
+ type: "component",
61
+ path,
62
+ componentName,
63
+ category,
64
+ score: 0,
65
+ });
66
+
67
+ // Add variant results
68
+ if (segment.variants) {
69
+ for (let i = 0; i < segment.variants.length; i++) {
70
+ const variant = segment.variants[i];
71
+ allResults.push({
72
+ type: "variant",
73
+ path,
74
+ componentName,
75
+ variantName: variant.name,
76
+ variantIndex: i,
77
+ category,
78
+ score: 0,
79
+ });
80
+ }
81
+ }
82
+ }
83
+
84
+ if (!query.trim()) {
85
+ // No query - show all components sorted alphabetically
86
+ return allResults
87
+ .filter((r) => r.type === "component")
88
+ .sort((a, b) => a.componentName.localeCompare(b.componentName))
89
+ .slice(0, 20);
90
+ }
91
+
92
+ // Fuzzy match and score results
93
+ const lowerQuery = query.toLowerCase();
94
+ const scored = allResults
95
+ .map((result) => {
96
+ const searchText = result.variantName
97
+ ? `${result.componentName} ${result.variantName}`
98
+ : result.componentName;
99
+ const score = fuzzyScore(searchText.toLowerCase(), lowerQuery);
100
+ return { ...result, score };
101
+ })
102
+ .filter((r) => r.score > 0)
103
+ .sort((a, b) => b.score - a.score)
104
+ .slice(0, 20);
105
+
106
+ return scored;
107
+ }, [segments, query]);
108
+
109
+ // Reset selection when results change
110
+ useEffect(() => {
111
+ setSelectedIndex(0);
112
+ }, [results]);
113
+
114
+ // Focus input when opened
115
+ useEffect(() => {
116
+ if (isOpen) {
117
+ setQuery("");
118
+ setSelectedIndex(0);
119
+ setTimeout(() => inputRef.current?.focus(), 0);
120
+ }
121
+ }, [isOpen]);
122
+
123
+ // Scroll selected item into view
124
+ useEffect(() => {
125
+ if (listRef.current && results.length > 0) {
126
+ const selectedEl = listRef.current.children[selectedIndex] as HTMLElement;
127
+ if (selectedEl) {
128
+ selectedEl.scrollIntoView({ block: "nearest" });
129
+ }
130
+ }
131
+ }, [selectedIndex, results.length]);
132
+
133
+ const handleKeyDown = useCallback(
134
+ (e: React.KeyboardEvent) => {
135
+ switch (e.key) {
136
+ case "ArrowDown":
137
+ e.preventDefault();
138
+ setSelectedIndex((i) => Math.min(i + 1, results.length - 1));
139
+ break;
140
+ case "ArrowUp":
141
+ e.preventDefault();
142
+ setSelectedIndex((i) => Math.max(i - 1, 0));
143
+ break;
144
+ case "Enter":
145
+ e.preventDefault();
146
+ if (results[selectedIndex]) {
147
+ handleSelect(results[selectedIndex]);
148
+ }
149
+ break;
150
+ case "Escape":
151
+ e.preventDefault();
152
+ onClose();
153
+ break;
154
+ }
155
+ },
156
+ [results, selectedIndex, onClose]
157
+ );
158
+
159
+ const handleSelect = (result: SearchResult) => {
160
+ if (result.type === "variant" && result.variantIndex !== undefined) {
161
+ onSelectVariant(result.path, result.variantIndex);
162
+ } else {
163
+ onSelectComponent(result.path);
164
+ }
165
+ onClose();
166
+ };
167
+
168
+ if (!isOpen) return null;
169
+
170
+ return (
171
+ <div className="fixed inset-0 z-50 flex items-start justify-center pt-[15vh]">
172
+ {/* Backdrop */}
173
+ <div
174
+ className="absolute inset-0 bg-black/50 backdrop-blur-sm"
175
+ onClick={onClose}
176
+ />
177
+
178
+ {/* Palette */}
179
+ <div
180
+ className="relative w-full max-w-xl bg-[--bg-primary] rounded-xl shadow-2xl border border-[--border] overflow-hidden"
181
+ onKeyDown={handleKeyDown}
182
+ >
183
+ {/* Search input */}
184
+ <div className="flex items-center px-4 py-3 border-b-2 border-blue-500">
185
+ <SearchIcon className="w-5 h-5 text-blue-500 mr-3" />
186
+ <input
187
+ ref={inputRef}
188
+ type="text"
189
+ value={query}
190
+ onChange={(e) => setQuery(e.target.value)}
191
+ placeholder="Search components and variants..."
192
+ className="flex-1 bg-transparent text-primary placeholder-tertiary text-base focus:outline-none"
193
+ />
194
+ <kbd className="hidden sm:inline-flex items-center gap-1 px-2 py-0.5 text-xs text-tertiary bg-[--bg-secondary] rounded border border-[--border]">
195
+ esc
196
+ </kbd>
197
+ </div>
198
+
199
+ {/* Results */}
200
+ <div
201
+ ref={listRef}
202
+ className="max-h-80 overflow-y-auto py-2"
203
+ >
204
+ {results.length === 0 ? (
205
+ <div className="px-4 py-8 text-center text-tertiary">
206
+ {query ? (
207
+ <>No results for "{query}"</>
208
+ ) : (
209
+ <>Start typing to search...</>
210
+ )}
211
+ </div>
212
+ ) : (
213
+ results.map((result, index) => (
214
+ <button
215
+ key={`${result.path}-${result.variantName || "component"}`}
216
+ onClick={() => handleSelect(result)}
217
+ className={clsx(
218
+ "w-full px-4 py-2 flex items-center gap-3 text-left transition-colors",
219
+ index === selectedIndex
220
+ ? "bg-blue-500/10 text-primary"
221
+ : "text-secondary hover:bg-[--bg-hover]"
222
+ )}
223
+ >
224
+ {/* Icon/Badge */}
225
+ <div
226
+ className={clsx(
227
+ "flex-shrink-0 w-8 h-8 rounded-lg flex items-center justify-center text-xs font-medium",
228
+ result.type === "component"
229
+ ? "bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300"
230
+ : "bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300"
231
+ )}
232
+ >
233
+ {result.type === "component" ? "C" : "V"}
234
+ </div>
235
+
236
+ {/* Content */}
237
+ <div className="flex-1 min-w-0">
238
+ <div className="flex items-center gap-2">
239
+ <span className="font-medium truncate">
240
+ {result.componentName}
241
+ </span>
242
+ {result.variantName && (
243
+ <>
244
+ <ChevronRightIcon className="w-3 h-3 text-tertiary flex-shrink-0" />
245
+ <span className="text-secondary truncate">
246
+ {result.variantName}
247
+ </span>
248
+ </>
249
+ )}
250
+ </div>
251
+ <div className="text-xs text-tertiary truncate">
252
+ {result.category}
253
+ </div>
254
+ </div>
255
+
256
+ {/* Keyboard hint for selected */}
257
+ {index === selectedIndex && (
258
+ <kbd className="hidden sm:inline-flex items-center px-1.5 py-0.5 text-[10px] text-tertiary bg-[--bg-secondary] rounded border border-[--border]">
259
+ enter
260
+ </kbd>
261
+ )}
262
+ </button>
263
+ ))
264
+ )}
265
+ </div>
266
+
267
+ {/* Footer */}
268
+ <div className="px-4 py-2 border-t border-[--border] flex items-center justify-between text-xs text-tertiary">
269
+ <div className="flex items-center gap-3">
270
+ <span className="flex items-center gap-1">
271
+ <kbd className="px-1.5 py-0.5 bg-[--bg-secondary] rounded border border-[--border]">↑</kbd>
272
+ <kbd className="px-1.5 py-0.5 bg-[--bg-secondary] rounded border border-[--border]">↓</kbd>
273
+ to navigate
274
+ </span>
275
+ <span className="flex items-center gap-1">
276
+ <kbd className="px-1.5 py-0.5 bg-[--bg-secondary] rounded border border-[--border]">↵</kbd>
277
+ to select
278
+ </span>
279
+ </div>
280
+ <span>
281
+ {results.length} result{results.length !== 1 ? "s" : ""}
282
+ </span>
283
+ </div>
284
+ </div>
285
+ </div>
286
+ );
287
+ }
288
+
289
+ /**
290
+ * Simple fuzzy matching score
291
+ * Returns 0 for no match, higher values for better matches
292
+ */
293
+ function fuzzyScore(text: string, query: string): number {
294
+ if (!query) return 1;
295
+
296
+ let score = 0;
297
+ let queryIndex = 0;
298
+ let consecutiveBonus = 0;
299
+ let lastMatchIndex = -2;
300
+
301
+ for (let i = 0; i < text.length && queryIndex < query.length; i++) {
302
+ if (text[i] === query[queryIndex]) {
303
+ score += 1;
304
+
305
+ // Bonus for consecutive matches
306
+ if (i === lastMatchIndex + 1) {
307
+ consecutiveBonus += 2;
308
+ score += consecutiveBonus;
309
+ } else {
310
+ consecutiveBonus = 0;
311
+ }
312
+
313
+ // Bonus for matching at word start
314
+ if (i === 0 || text[i - 1] === " " || text[i - 1] === "-" || text[i - 1] === "_") {
315
+ score += 3;
316
+ }
317
+
318
+ lastMatchIndex = i;
319
+ queryIndex++;
320
+ }
321
+ }
322
+
323
+ // Only return score if all query characters were found
324
+ if (queryIndex < query.length) return 0;
325
+
326
+ // Bonus for shorter results (more relevant)
327
+ score += Math.max(0, 20 - text.length);
328
+
329
+ // Bonus for exact match
330
+ if (text === query) score += 50;
331
+
332
+ // Bonus for starts with
333
+ if (text.startsWith(query)) score += 30;
334
+
335
+ return score;
336
+ }