@fragments-sdk/cli 0.7.11 → 0.7.12

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fragments-sdk/cli",
3
- "version": "0.7.11",
3
+ "version": "0.7.12",
4
4
  "license": "FSL-1.1-MIT",
5
5
  "description": "CLI, MCP server, and dev tools for Fragments design system",
6
6
  "author": "Conan McNicholl",
@@ -270,11 +270,6 @@ export function App({ fragments }: AppProps) {
270
270
  }
271
271
  }, [copyUrl, success, uiActions]);
272
272
 
273
- const focusSearchInput = useCallback(() => {
274
- searchInputRef.current?.focus();
275
- searchInputRef.current?.select();
276
- }, []);
277
-
278
273
  // Sorted fragment paths for keyboard navigation
279
274
  const sortedFragmentPaths = useMemo(() => {
280
275
  return [...fragments]
@@ -328,7 +323,7 @@ export function App({ fragments }: AppProps) {
328
323
  toggleResponsive: () => uiActions.setMultiViewport(!uiState.showMultiViewport),
329
324
  copyLink: handleCopyLink,
330
325
  showHelp: uiActions.toggleShortcutsHelp,
331
- openSearch: focusSearchInput,
326
+ openSearch: () => uiActions.setCommandPalette(true),
332
327
  escape: () => {
333
328
  if (document.activeElement === searchInputRef.current) {
334
329
  if (searchQuery) {
@@ -614,7 +609,6 @@ function HeaderSearch({ value, onChange, inputRef }: HeaderSearchProps) {
614
609
  placeholder="Search components"
615
610
  aria-label="Search components"
616
611
  size="sm"
617
- shortcut="⌘K"
618
612
  style={{ width: '240px' }}
619
613
  />
620
614
  </Header.Search>
@@ -723,11 +717,12 @@ function PreviewAside({
723
717
  </a>
724
718
  {variants.map((variant, index) => {
725
719
  const active = index === focusedVariantIndex;
720
+ const anchorId = getVariantSectionId(fragment.meta.name, variant.name);
726
721
 
727
722
  return (
728
723
  <a
729
724
  key={variant.name}
730
- href="#preview-canvas"
725
+ href={`#${anchorId}`}
731
726
  style={getLinkStyle(active)}
732
727
  onClick={(event) => {
733
728
  event.preventDefault();
@@ -336,10 +336,16 @@ export function CommandPalette({
336
336
  function fuzzyScore(text: string, query: string): number {
337
337
  if (!query) return 1;
338
338
 
339
+ // Require substring match for short queries to avoid scattered single-char noise
340
+ if (query.length <= 3 && !text.includes(query)) {
341
+ return 0;
342
+ }
343
+
339
344
  let score = 0;
340
345
  let queryIndex = 0;
341
346
  let consecutiveBonus = 0;
342
347
  let lastMatchIndex = -2;
348
+ let maxGap = 0;
343
349
 
344
350
  for (let i = 0; i < text.length && queryIndex < query.length; i++) {
345
351
  if (text[i] === query[queryIndex]) {
@@ -351,6 +357,11 @@ function fuzzyScore(text: string, query: string): number {
351
357
  score += consecutiveBonus;
352
358
  } else {
353
359
  consecutiveBonus = 0;
360
+ // Track max gap between matches
361
+ if (lastMatchIndex >= 0) {
362
+ const gap = i - lastMatchIndex;
363
+ if (gap > maxGap) maxGap = gap;
364
+ }
354
365
  }
355
366
 
356
367
  // Bonus for matching at word start
@@ -366,6 +377,11 @@ function fuzzyScore(text: string, query: string): number {
366
377
  // Only return score if all query characters were found
367
378
  if (queryIndex < query.length) return 0;
368
379
 
380
+ // Penalize large gaps between matched characters
381
+ if (maxGap > 5) {
382
+ score -= maxGap;
383
+ }
384
+
369
385
  // Bonus for shorter results (more relevant)
370
386
  score += Math.max(0, 20 - text.length);
371
387
 
@@ -375,5 +391,5 @@ function fuzzyScore(text: string, query: string): number {
375
391
  // Bonus for starts with
376
392
  if (text.startsWith(query)) score += 30;
377
393
 
378
- return score;
394
+ return Math.max(score, 0) || 0;
379
395
  }
@@ -162,91 +162,88 @@ export const IsolatedPreviewFrame = memo(function IsolatedPreviewFrame({
162
162
  onMouseLeave={() => setIsHovered(false)}
163
163
  >
164
164
  {/* Skeleton loading overlay (initial load) */}
165
- <div
166
- style={{
167
- position: 'absolute',
168
- inset: 0,
169
- zIndex: 10,
170
- transition: 'opacity 150ms',
171
- opacity: showSkeleton ? 1 : 0,
172
- pointerEvents: showSkeleton ? 'auto' : 'none',
173
- background: 'rgba(255, 255, 255, 0.95)',
174
- }}
175
- >
176
- <PreviewSkeleton />
177
- </div>
165
+ {showSkeleton && (
166
+ <div
167
+ style={{
168
+ position: 'absolute',
169
+ inset: 0,
170
+ zIndex: 10,
171
+ background: 'var(--bg-primary, rgba(255, 255, 255, 0.95))',
172
+ }}
173
+ >
174
+ <PreviewSkeleton />
175
+ </div>
176
+ )}
178
177
 
179
178
  {/* Spinner overlay (subsequent renders) */}
180
- <div
181
- style={{
182
- position: 'absolute',
183
- inset: 0,
184
- zIndex: 10,
185
- display: 'flex',
186
- alignItems: 'center',
187
- justifyContent: 'center',
188
- transition: 'opacity 150ms',
189
- opacity: showSpinner ? 1 : 0,
190
- pointerEvents: showSpinner ? 'auto' : 'none',
191
- background: 'rgba(255, 255, 255, 0.8)',
192
- }}
193
- >
194
- <div style={{ display: 'flex', alignItems: 'center', gap: 8, color: '#6b7280', fontSize: 14 }}>
195
- <LoadingSpinner />
196
- <span>Rendering...</span>
179
+ {showSpinner && (
180
+ <div
181
+ style={{
182
+ position: 'absolute',
183
+ inset: 0,
184
+ zIndex: 10,
185
+ display: 'flex',
186
+ alignItems: 'center',
187
+ justifyContent: 'center',
188
+ background: 'color-mix(in srgb, var(--bg-primary, white) 80%, transparent)',
189
+ }}
190
+ >
191
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8, color: 'var(--text-tertiary, #6b7280)', fontSize: 14 }}>
192
+ <LoadingSpinner />
193
+ <span>Rendering...</span>
194
+ </div>
197
195
  </div>
198
- </div>
196
+ )}
199
197
 
200
198
  {/* Error overlay */}
201
- <div
202
- style={{
203
- position: 'absolute',
204
- inset: 0,
205
- zIndex: 10,
206
- display: 'flex',
207
- alignItems: 'center',
208
- justifyContent: 'center',
209
- padding: '16px',
210
- transition: 'opacity 150ms',
211
- opacity: frameError && !isLoading ? 1 : 0,
212
- pointerEvents: frameError && !isLoading ? 'auto' : 'none',
213
- background: 'rgba(254, 242, 242, 0.95)',
214
- }}
215
- >
199
+ {frameError && !isLoading && (
216
200
  <div
217
201
  style={{
218
- background: 'white',
219
- border: '1px solid #fecaca',
220
- borderRadius: 8,
221
- padding: 16,
222
- maxWidth: 400,
202
+ position: 'absolute',
203
+ inset: 0,
204
+ zIndex: 10,
205
+ display: 'flex',
206
+ alignItems: 'center',
207
+ justifyContent: 'center',
208
+ padding: '16px',
209
+ background: 'rgba(254, 242, 242, 0.95)',
223
210
  }}
224
211
  >
225
- <div style={{ color: '#dc2626', fontWeight: 500, marginBottom: 8 }}>
226
- Preview Error
227
- </div>
228
- <div style={{ color: '#991b1b', fontSize: 13, marginBottom: retryCount < MAX_RETRIES ? 12 : 0 }}>
229
- {frameError}
212
+ <div
213
+ style={{
214
+ background: 'white',
215
+ border: '1px solid #fecaca',
216
+ borderRadius: 8,
217
+ padding: 16,
218
+ maxWidth: 400,
219
+ }}
220
+ >
221
+ <div style={{ color: '#dc2626', fontWeight: 500, marginBottom: 8 }}>
222
+ Preview Error
223
+ </div>
224
+ <div style={{ color: '#991b1b', fontSize: 13, marginBottom: retryCount < MAX_RETRIES ? 12 : 0 }}>
225
+ {frameError}
226
+ </div>
227
+ {retryCount < MAX_RETRIES && (
228
+ <button
229
+ onClick={handleRetry}
230
+ style={{
231
+ padding: '6px 12px',
232
+ fontSize: 13,
233
+ fontWeight: 500,
234
+ color: 'white',
235
+ background: '#dc2626',
236
+ border: 'none',
237
+ borderRadius: 6,
238
+ cursor: 'pointer',
239
+ }}
240
+ >
241
+ Retry ({MAX_RETRIES - retryCount} remaining)
242
+ </button>
243
+ )}
230
244
  </div>
231
- {retryCount < MAX_RETRIES && (
232
- <button
233
- onClick={handleRetry}
234
- style={{
235
- padding: '6px 12px',
236
- fontSize: 13,
237
- fontWeight: 500,
238
- color: 'white',
239
- background: '#dc2626',
240
- border: 'none',
241
- borderRadius: 6,
242
- cursor: 'pointer',
243
- }}
244
- >
245
- Retry ({MAX_RETRIES - retryCount} remaining)
246
- </button>
247
- )}
248
245
  </div>
249
- </div>
246
+ )}
250
247
 
251
248
  {/* The iframe */}
252
249
  <iframe
@@ -14,16 +14,26 @@ function fuzzyMatch(text: string, pattern: string): FuzzyMatch | null {
14
14
  const textLower = text.toLowerCase();
15
15
  const patternLower = pattern.toLowerCase();
16
16
 
17
+ // Require substring match for short queries (<=3 chars) to avoid scattered single-char noise
18
+ if (patternLower.length <= 3 && !textLower.includes(patternLower)) {
19
+ return null;
20
+ }
21
+
17
22
  const indices: number[] = [];
18
23
  let patternIdx = 0;
19
24
  let score = 0;
20
25
  let consecutiveBonus = 0;
26
+ let maxGap = 0;
21
27
 
22
28
  for (let i = 0; i < textLower.length && patternIdx < patternLower.length; i++) {
23
29
  if (textLower[i] === patternLower[patternIdx]) {
24
30
  indices.push(i);
25
- if (indices.length > 1 && indices[indices.length - 2] === i - 1) {
26
- consecutiveBonus += 5;
31
+ if (indices.length > 1) {
32
+ const gap = i - indices[indices.length - 2];
33
+ if (gap === 1) {
34
+ consecutiveBonus += 5;
35
+ }
36
+ if (gap > maxGap) maxGap = gap;
27
37
  }
28
38
  if (i === 0 || text[i - 1] === ' ' || text[i - 1] === '-' || text[i - 1] === '_') {
29
39
  score += 10;
@@ -36,9 +46,19 @@ function fuzzyMatch(text: string, pattern: string): FuzzyMatch | null {
36
46
  return null;
37
47
  }
38
48
 
49
+ // Penalize large gaps between matched characters
50
+ if (maxGap > 5) {
51
+ score -= maxGap * 2;
52
+ }
53
+
39
54
  score += consecutiveBonus;
40
55
  score += (patternLower.length / textLower.length) * 20;
41
56
 
57
+ // Reject very low scores (scattered single-char matches)
58
+ if (score <= 0 && patternLower.length > 1) {
59
+ return null;
60
+ }
61
+
42
62
  return { score, indices };
43
63
  }
44
64
 
@@ -364,7 +384,7 @@ export function LeftSidebar({ fragments, activeFragment, searchQuery, onSelect,
364
384
  <Sidebar.Footer>
365
385
  <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
366
386
  <Text size="xs" color="tertiary">
367
- {isFilterActive ? `${flatItems.length} / ${fragments.length}` : fragments.length} components
387
+ {isFilterActive || searchResults ? `${flatItems.length} of ${fragments.length}` : fragments.length} components
368
388
  </Text>
369
389
  <Sidebar.CollapseToggle />
370
390
  </div>
@@ -262,7 +262,7 @@ export const SHORTCUTS = [
262
262
  { keys: ["p"], description: "Toggle addons panel" },
263
263
  { keys: ["m"], description: "Matrix view" },
264
264
  { keys: ["v"], description: "Responsive view" },
265
- { keys: ["/", "⌘K"], description: "Search" },
265
+ { keys: ["/", "⌘K"], description: "Command palette" },
266
266
  { keys: ["?"], description: "Show shortcuts" },
267
267
  { keys: ["Esc"], description: "Close / Clear" },
268
268
  ];