@fragments-sdk/cli 0.9.0 → 0.9.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 (123) hide show
  1. package/dist/bin.js +83 -33
  2. package/dist/bin.js.map +1 -1
  3. package/dist/{chunk-WI6SLMSO.js → chunk-5GT62FCB.js} +2 -2
  4. package/dist/{chunk-CJEGT3WD.js → chunk-BW3ZATBW.js} +20 -3
  5. package/dist/chunk-BW3ZATBW.js.map +1 -0
  6. package/dist/{chunk-2JIKCJX3.js → chunk-D7372LQX.js} +13 -6
  7. package/dist/chunk-D7372LQX.js.map +1 -0
  8. package/dist/chunk-EZYXYWNF.js +131 -0
  9. package/dist/chunk-EZYXYWNF.js.map +1 -0
  10. package/dist/{chunk-NGIMCIK2.js → chunk-GF6OVPIN.js} +2 -2
  11. package/dist/{chunk-GOVI6COW.js → chunk-NVSPGSKB.js} +12 -4
  12. package/dist/chunk-NVSPGSKB.js.map +1 -0
  13. package/dist/core/index.d.ts +105 -3
  14. package/dist/core/index.js +12 -2
  15. package/dist/{defineFragment-D0UTve-I.d.ts → defineFragment-CBMS7Bab.d.ts} +21 -1
  16. package/dist/generate-LQA2R7FN.js +461 -0
  17. package/dist/generate-LQA2R7FN.js.map +1 -0
  18. package/dist/index.d.ts +2 -2
  19. package/dist/index.js +5 -4
  20. package/dist/index.js.map +1 -1
  21. package/dist/{init-KSAAS7X3.js → init-2GEGVIUQ.js} +13 -75
  22. package/dist/init-2GEGVIUQ.js.map +1 -0
  23. package/dist/mcp-bin.js +4 -3
  24. package/dist/mcp-bin.js.map +1 -1
  25. package/dist/{scan-65RH3QMM.js → scan-JGS65S7P.js} +6 -5
  26. package/dist/{service-A5GIGGGK.js → service-XP2EAJXD.js} +4 -3
  27. package/dist/{static-viewer-NSODM5VX.js → static-viewer-XCS7UJTO.js} +4 -3
  28. package/dist/storyFilters-3LUYAFZF.js +15 -0
  29. package/dist/storyFilters-3LUYAFZF.js.map +1 -0
  30. package/dist/{test-RPWZAYSJ.js → test-TD6TJNVY.js} +3 -3
  31. package/dist/{tokens-NIXSZRX7.js → tokens-2EXPCVP3.js} +5 -4
  32. package/dist/{tokens-NIXSZRX7.js.map → tokens-2EXPCVP3.js.map} +1 -1
  33. package/dist/{viewer-SBTJDMP7.js → viewer-RFA2KVBG.js} +243 -18
  34. package/dist/viewer-RFA2KVBG.js.map +1 -0
  35. package/package.json +1 -1
  36. package/src/build.ts +12 -2
  37. package/src/commands/build.ts +16 -2
  38. package/src/commands/generate.ts +383 -68
  39. package/src/commands/init.ts +9 -51
  40. package/src/core/config.ts +15 -2
  41. package/src/core/generators/typescript-extractor.ts +10 -0
  42. package/src/core/index.ts +15 -0
  43. package/src/core/schema.ts +10 -2
  44. package/src/core/storyFilters.test.ts +350 -0
  45. package/src/core/storyFilters.ts +253 -0
  46. package/src/core/types.ts +22 -0
  47. package/src/migrate/converter.ts +9 -1
  48. package/src/migrate/parser.ts +2 -0
  49. package/src/migrate/types.ts +2 -0
  50. package/src/setup.ts +69 -24
  51. package/src/viewer/__tests__/viewer-integration.test.ts +1 -1
  52. package/src/viewer/components/AccessibilityPanel.tsx +305 -312
  53. package/src/viewer/components/ActionsPanel.tsx +31 -29
  54. package/src/viewer/components/AllVariantsPreview.tsx +78 -0
  55. package/src/viewer/components/App.tsx +187 -740
  56. package/src/viewer/components/BottomPanel.tsx +228 -132
  57. package/src/viewer/components/CodePanel.tsx +1 -1
  58. package/src/viewer/components/CommandPalette.tsx +7 -10
  59. package/src/viewer/components/ComponentDocView.tsx +164 -0
  60. package/src/viewer/components/ComponentGraph.tsx +111 -142
  61. package/src/viewer/components/ContractPanel.tsx +6 -6
  62. package/src/viewer/components/EmptyVariantMessage.tsx +54 -0
  63. package/src/viewer/components/FigmaEmbed.tsx +20 -18
  64. package/src/viewer/components/FragmentEditor.tsx +92 -115
  65. package/src/viewer/components/HeaderSearch.tsx +24 -0
  66. package/src/viewer/components/HealthDashboard.tsx +16 -2
  67. package/src/viewer/components/Icons.tsx +9 -0
  68. package/src/viewer/components/InteractionsPanel.tsx +101 -117
  69. package/src/viewer/components/IsolatedPreviewFrame.tsx +1 -0
  70. package/src/viewer/components/LandingPage.tsx +3 -3
  71. package/src/viewer/components/LeftSidebar.tsx +141 -63
  72. package/src/viewer/components/LoadErrorMessage.tsx +102 -0
  73. package/src/viewer/components/MultiViewportPreview.tsx +61 -142
  74. package/src/viewer/components/NoVariantsMessage.tsx +59 -0
  75. package/src/viewer/components/PanelShell.tsx +161 -0
  76. package/src/viewer/components/PerformancePanel.tsx +31 -28
  77. package/src/viewer/components/PreviewArea.tsx +1 -1
  78. package/src/viewer/components/PreviewAside.tsx +168 -0
  79. package/src/viewer/components/PreviewFrameHost.tsx +3 -3
  80. package/src/viewer/components/PropsEditor.tsx +70 -156
  81. package/src/viewer/components/ResizablePanel.tsx +103 -263
  82. package/src/viewer/components/RightSidebar.tsx +3 -9
  83. package/src/viewer/components/SkeletonLoader.tsx +13 -13
  84. package/src/viewer/components/TokenStylePanel.tsx +182 -209
  85. package/src/viewer/components/TopToolbar.tsx +159 -0
  86. package/src/viewer/components/VariantMatrix.tsx +42 -86
  87. package/src/viewer/components/VariantTabs.tsx +3 -3
  88. package/src/viewer/components/ViewerHeader.tsx +69 -0
  89. package/src/viewer/components/WebMCPDevTools.tsx +17 -23
  90. package/src/viewer/components/viewer-utils.ts +16 -0
  91. package/src/viewer/entry.tsx +5 -0
  92. package/src/viewer/hooks/useAppState.ts +27 -4
  93. package/src/viewer/hooks/usePreviewBridge.ts +2 -2
  94. package/src/viewer/preview-frame.html +6 -12
  95. package/src/viewer/server.ts +169 -2
  96. package/src/viewer/vendor/shared/src/ComponentDocContent.module.scss +10 -0
  97. package/src/viewer/vendor/shared/src/ComponentDocContent.module.scss.d.ts +2 -0
  98. package/src/viewer/vendor/shared/src/ComponentDocContent.tsx +274 -0
  99. package/src/viewer/vendor/shared/src/DocsHeaderBar.tsx +6 -18
  100. package/src/viewer/vendor/shared/src/DocsPageShell.tsx +5 -0
  101. package/src/viewer/vendor/shared/src/DocsSidebarNav.tsx +5 -16
  102. package/src/viewer/vendor/shared/src/PropsTable.module.scss +68 -0
  103. package/src/viewer/vendor/shared/src/PropsTable.module.scss.d.ts +2 -0
  104. package/src/viewer/vendor/shared/src/PropsTable.tsx +76 -0
  105. package/src/viewer/vendor/shared/src/VariantPreviewCard.module.scss +122 -0
  106. package/src/viewer/vendor/shared/src/VariantPreviewCard.module.scss.d.ts +2 -0
  107. package/src/viewer/vendor/shared/src/VariantPreviewCard.tsx +134 -0
  108. package/src/viewer/vendor/shared/src/index.ts +8 -0
  109. package/src/viewer/vendor/shared/src/types.ts +12 -0
  110. package/src/viewer/vite-plugin.ts +109 -4
  111. package/dist/chunk-2JIKCJX3.js.map +0 -1
  112. package/dist/chunk-CJEGT3WD.js.map +0 -1
  113. package/dist/chunk-GOVI6COW.js.map +0 -1
  114. package/dist/generate-35OIMW4Y.js +0 -252
  115. package/dist/generate-35OIMW4Y.js.map +0 -1
  116. package/dist/init-KSAAS7X3.js.map +0 -1
  117. package/dist/viewer-SBTJDMP7.js.map +0 -1
  118. /package/dist/{chunk-WI6SLMSO.js.map → chunk-5GT62FCB.js.map} +0 -0
  119. /package/dist/{chunk-NGIMCIK2.js.map → chunk-GF6OVPIN.js.map} +0 -0
  120. /package/dist/{scan-65RH3QMM.js.map → scan-JGS65S7P.js.map} +0 -0
  121. /package/dist/{service-A5GIGGGK.js.map → service-XP2EAJXD.js.map} +0 -0
  122. /package/dist/{static-viewer-NSODM5VX.js.map → static-viewer-XCS7UJTO.js.map} +0 -0
  123. /package/dist/{test-RPWZAYSJ.js.map → test-TD6TJNVY.js.map} +0 -0
@@ -1,6 +1,7 @@
1
1
  import { useState, useCallback } from "react";
2
2
  import { useForm, useFieldArray, Controller } from "react-hook-form";
3
3
  import type { Fragment, FragmentUsage, FragmentMeta } from "../../core/index.js";
4
+ import { Box, Text, Button, Stack } from "@fragments-sdk/ui";
4
5
  import { ChevronDownIcon } from "./Icons.js";
5
6
 
6
7
  interface FragmentEditorProps {
@@ -50,32 +51,6 @@ const inputStyle: React.CSSProperties = {
50
51
  boxSizing: 'border-box',
51
52
  };
52
53
 
53
- const labelStyle: React.CSSProperties = {
54
- display: 'block',
55
- fontSize: '12px',
56
- fontWeight: 500,
57
- color: 'var(--text-secondary)',
58
- marginBottom: '6px',
59
- };
60
-
61
- const removeButtonStyle: React.CSSProperties = {
62
- padding: '0 8px',
63
- color: 'var(--text-secondary)',
64
- background: 'none',
65
- border: 'none',
66
- cursor: 'pointer',
67
- fontSize: '16px',
68
- };
69
-
70
- const addButtonStyle: React.CSSProperties = {
71
- fontSize: '12px',
72
- color: 'var(--color-accent)',
73
- background: 'none',
74
- border: 'none',
75
- cursor: 'pointer',
76
- padding: 0,
77
- };
78
-
79
54
  export function FragmentEditor({
80
55
  componentName,
81
56
  fragment,
@@ -196,20 +171,20 @@ export function FragmentEditor({
196
171
  style={{ height: '100%', display: 'flex', flexDirection: 'column', background: 'var(--bg-primary)' }}
197
172
  >
198
173
  {/* Header */}
199
- <div style={{ padding: '12px 16px', borderBottom: '1px solid var(--border)', background: 'var(--bg-secondary)' }}>
200
- <h3 style={{ fontSize: '14px', fontWeight: 600, color: 'var(--text-primary)', margin: 0 }}>Fragment Editor</h3>
201
- <p style={{ fontSize: '12px', color: 'var(--text-secondary)', marginTop: '2px', marginBottom: 0 }}>
174
+ <Box paddingX="md" paddingY="sm" borderBottom background="secondary">
175
+ <Text as="h3" size="sm" weight="semibold" style={{ margin: 0 }}>Fragment Editor</Text>
176
+ <Text size="xs" color="secondary" style={{ marginTop: '2px' }}>
202
177
  Enrich {componentName} with metadata
203
- </p>
204
- </div>
178
+ </Text>
179
+ </Box>
205
180
 
206
181
  {/* Scrollable content */}
207
- <div style={{ flex: 1, overflowY: 'auto' }}>
182
+ <Box overflow="auto" style={{ flex: 1 }}>
208
183
  {/* Basic Info */}
209
- <div style={{ padding: '16px', borderBottom: '1px solid var(--border)' }}>
210
- <label style={labelStyle}>
184
+ <Box padding="md" borderBottom>
185
+ <Text as="label" size="xs" weight="medium" color="secondary" style={{ display: 'block', marginBottom: '6px' }}>
211
186
  Description
212
- </label>
187
+ </Text>
213
188
  <textarea
214
189
  {...register("description")}
215
190
  rows={3}
@@ -219,7 +194,7 @@ export function FragmentEditor({
219
194
  }}
220
195
  placeholder="Brief description of the component's purpose..."
221
196
  />
222
- </div>
197
+ </Box>
223
198
 
224
199
  {/* Usage Section */}
225
200
  <Section
@@ -229,44 +204,47 @@ export function FragmentEditor({
229
204
  >
230
205
  {/* When to Use */}
231
206
  <div style={{ marginBottom: '16px' }}>
232
- <label style={{ ...labelStyle, marginBottom: '8px' }}>
207
+ <Text as="label" size="xs" weight="medium" color="secondary" style={{ display: 'block', marginBottom: '8px' }}>
233
208
  When to Use
234
- </label>
235
- <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
209
+ </Text>
210
+ <Stack gap="sm">
236
211
  {whenFields.map((field, index) => (
237
- <div key={field.id} style={{ display: 'flex', gap: '8px' }}>
212
+ <Stack key={field.id} direction="row" gap="sm">
238
213
  <input
239
214
  {...register(`usage.when.${index}` as const)}
240
215
  style={{ ...inputStyle, flex: 1 }}
241
216
  placeholder="Scenario..."
242
217
  />
243
- <button
218
+ <Button
244
219
  type="button"
220
+ variant="ghost"
221
+ size="sm"
245
222
  onClick={() => removeWhen(index)}
246
- style={removeButtonStyle}
247
223
  >
248
224
  &times;
249
- </button>
250
- </div>
225
+ </Button>
226
+ </Stack>
251
227
  ))}
252
- <button
228
+ <Button
253
229
  type="button"
230
+ variant="ghost"
231
+ size="sm"
254
232
  onClick={() => appendWhen("")}
255
- style={addButtonStyle}
233
+ style={{ padding: 0, alignSelf: 'flex-start' }}
256
234
  >
257
235
  + Add scenario
258
- </button>
259
- </div>
236
+ </Button>
237
+ </Stack>
260
238
  </div>
261
239
 
262
240
  {/* Do Not */}
263
241
  <div>
264
- <label style={{ ...labelStyle, marginBottom: '8px' }}>
242
+ <Text as="label" size="xs" weight="medium" color="secondary" style={{ display: 'block', marginBottom: '8px' }}>
265
243
  Do Not
266
- </label>
267
- <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
244
+ </Text>
245
+ <Stack gap="sm">
268
246
  {doNotFields.map((field, index) => (
269
- <div key={field.id} style={{ display: 'flex', gap: '8px' }}>
247
+ <Stack key={field.id} direction="row" gap="sm">
270
248
  <input
271
249
  {...register(`usage.doNot.${index}.text` as const)}
272
250
  style={{ ...inputStyle, flex: 1 }}
@@ -277,23 +255,26 @@ export function FragmentEditor({
277
255
  style={{ ...inputStyle, width: '128px' }}
278
256
  placeholder="Instead..."
279
257
  />
280
- <button
258
+ <Button
281
259
  type="button"
260
+ variant="ghost"
261
+ size="sm"
282
262
  onClick={() => removeDoNot(index)}
283
- style={removeButtonStyle}
284
263
  >
285
264
  &times;
286
- </button>
287
- </div>
265
+ </Button>
266
+ </Stack>
288
267
  ))}
289
- <button
268
+ <Button
290
269
  type="button"
270
+ variant="ghost"
271
+ size="sm"
291
272
  onClick={() => appendDoNot({ text: "", instead: "" })}
292
- style={addButtonStyle}
273
+ style={{ padding: 0, alignSelf: 'flex-start' }}
293
274
  >
294
275
  + Add anti-pattern
295
- </button>
296
- </div>
276
+ </Button>
277
+ </Stack>
297
278
  </div>
298
279
  </Section>
299
280
 
@@ -304,9 +285,9 @@ export function FragmentEditor({
304
285
  onToggle={() => toggleSection("accessibility")}
305
286
  >
306
287
  <div style={{ marginBottom: '16px' }}>
307
- <label style={labelStyle}>
288
+ <Text as="label" size="xs" weight="medium" color="secondary" style={{ display: 'block', marginBottom: '6px' }}>
308
289
  ARIA Role
309
- </label>
290
+ </Text>
310
291
  <input
311
292
  {...register("accessibility.role")}
312
293
  style={inputStyle}
@@ -315,12 +296,12 @@ export function FragmentEditor({
315
296
  </div>
316
297
 
317
298
  <div>
318
- <label style={{ ...labelStyle, marginBottom: '8px' }}>
299
+ <Text as="label" size="xs" weight="medium" color="secondary" style={{ display: 'block', marginBottom: '8px' }}>
319
300
  Requirements
320
- </label>
321
- <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
301
+ </Text>
302
+ <Stack gap="sm">
322
303
  {requirementsFields.map((field, index) => (
323
- <div key={field.id} style={{ display: 'flex', gap: '8px' }}>
304
+ <Stack key={field.id} direction="row" gap="sm">
324
305
  <input
325
306
  {...register(
326
307
  `accessibility.requirements.${index}` as const
@@ -328,23 +309,26 @@ export function FragmentEditor({
328
309
  style={{ ...inputStyle, flex: 1 }}
329
310
  placeholder="Requirement..."
330
311
  />
331
- <button
312
+ <Button
332
313
  type="button"
314
+ variant="ghost"
315
+ size="sm"
333
316
  onClick={() => removeRequirement(index)}
334
- style={removeButtonStyle}
335
317
  >
336
318
  &times;
337
- </button>
338
- </div>
319
+ </Button>
320
+ </Stack>
339
321
  ))}
340
- <button
322
+ <Button
341
323
  type="button"
324
+ variant="ghost"
325
+ size="sm"
342
326
  onClick={() => appendRequirement("")}
343
- style={addButtonStyle}
327
+ style={{ padding: 0, alignSelf: 'flex-start' }}
344
328
  >
345
329
  + Add requirement
346
- </button>
347
- </div>
330
+ </Button>
331
+ </Stack>
348
332
  </div>
349
333
  </Section>
350
334
 
@@ -385,9 +369,9 @@ export function FragmentEditor({
385
369
  >
386
370
  <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px', marginBottom: '16px' }}>
387
371
  <div>
388
- <label style={labelStyle}>
372
+ <Text as="label" size="xs" weight="medium" color="secondary" style={{ display: 'block', marginBottom: '6px' }}>
389
373
  Status
390
- </label>
374
+ </Text>
391
375
  <select
392
376
  {...register("meta.status")}
393
377
  style={inputStyle}
@@ -400,9 +384,9 @@ export function FragmentEditor({
400
384
  </select>
401
385
  </div>
402
386
  <div>
403
- <label style={labelStyle}>
387
+ <Text as="label" size="xs" weight="medium" color="secondary" style={{ display: 'block', marginBottom: '6px' }}>
404
388
  Since Version
405
- </label>
389
+ </Text>
406
390
  <input
407
391
  {...register("meta.since")}
408
392
  style={inputStyle}
@@ -411,9 +395,9 @@ export function FragmentEditor({
411
395
  </div>
412
396
  </div>
413
397
  <div style={{ marginBottom: '16px' }}>
414
- <label style={labelStyle}>
398
+ <Text as="label" size="xs" weight="medium" color="secondary" style={{ display: 'block', marginBottom: '6px' }}>
415
399
  Owner
416
- </label>
400
+ </Text>
417
401
  <input
418
402
  {...register("meta.owner")}
419
403
  style={inputStyle}
@@ -421,29 +405,19 @@ export function FragmentEditor({
421
405
  />
422
406
  </div>
423
407
  </Section>
424
- </div>
408
+ </Box>
425
409
 
426
410
  {/* Footer with save button */}
427
- <div style={{ padding: '12px 16px', borderTop: '1px solid var(--border)', background: 'var(--bg-secondary)' }}>
428
- <button
411
+ <Box paddingX="md" paddingY="sm" borderTop background="secondary">
412
+ <Button
429
413
  type="submit"
414
+ variant="primary"
415
+ fullWidth
430
416
  disabled={!isDirty || saving}
431
- style={{
432
- width: '100%',
433
- padding: '8px 16px',
434
- fontSize: '14px',
435
- fontWeight: 500,
436
- borderRadius: '8px',
437
- transition: 'background-color 150ms',
438
- border: 'none',
439
- cursor: isDirty && !saving ? 'pointer' : 'not-allowed',
440
- background: isDirty && !saving ? 'var(--color-accent)' : 'var(--bg-secondary)',
441
- color: isDirty && !saving ? 'white' : 'var(--text-tertiary)',
442
- }}
443
417
  >
444
418
  {saving ? "Saving..." : "Save Fragment"}
445
- </button>
446
- </div>
419
+ </Button>
420
+ </Box>
447
421
  </form>
448
422
  );
449
423
  }
@@ -459,7 +433,7 @@ function Section({ title, expanded, onToggle, children }: SectionProps) {
459
433
  const [hovered, setHovered] = useState(false);
460
434
 
461
435
  return (
462
- <div style={{ borderBottom: '1px solid var(--border)' }}>
436
+ <Box borderBottom>
463
437
  <button
464
438
  type="button"
465
439
  onClick={onToggle}
@@ -477,9 +451,9 @@ function Section({ title, expanded, onToggle, children }: SectionProps) {
477
451
  cursor: 'pointer',
478
452
  }}
479
453
  >
480
- <span style={{ fontSize: '12px', fontWeight: 600, color: 'var(--text-primary)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
454
+ <Text size="xs" weight="semibold" style={{ textTransform: 'uppercase', letterSpacing: '0.05em' }}>
481
455
  {title}
482
- </span>
456
+ </Text>
483
457
  <ChevronDownIcon
484
458
  style={{
485
459
  width: '16px',
@@ -490,8 +464,8 @@ function Section({ title, expanded, onToggle, children }: SectionProps) {
490
464
  }}
491
465
  />
492
466
  </button>
493
- {expanded && <div style={{ padding: '16px' }}>{children}</div>}
494
- </div>
467
+ {expanded && <Box padding="md">{children}</Box>}
468
+ </Box>
495
469
  );
496
470
  }
497
471
 
@@ -514,35 +488,38 @@ function RelatedField({
514
488
 
515
489
  return (
516
490
  <div style={{ marginBottom: '16px' }}>
517
- <label style={{ display: 'block', fontSize: '12px', fontWeight: 500, color: 'var(--text-secondary)', marginBottom: '2px' }}>
491
+ <Text as="label" size="xs" weight="medium" color="secondary" style={{ display: 'block', marginBottom: '2px' }}>
518
492
  {label}
519
- </label>
520
- <p style={{ fontSize: '10px', color: 'var(--text-tertiary)', marginBottom: '8px', marginTop: 0 }}>{description}</p>
521
- <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
493
+ </Text>
494
+ <Text size="xs" color="tertiary" style={{ marginBottom: '8px', marginTop: 0, fontSize: '10px' }}>{description}</Text>
495
+ <Stack gap="sm">
522
496
  {fields.map((field, index) => (
523
- <div key={field.id} style={{ display: 'flex', gap: '8px' }}>
497
+ <Stack key={field.id} direction="row" gap="sm">
524
498
  <input
525
499
  {...register(`${name}.${index}` as const)}
526
500
  style={{ ...inputStyle, flex: 1 }}
527
501
  placeholder="ComponentName"
528
502
  />
529
- <button
503
+ <Button
530
504
  type="button"
505
+ variant="ghost"
506
+ size="sm"
531
507
  onClick={() => remove(index)}
532
- style={removeButtonStyle}
533
508
  >
534
509
  &times;
535
- </button>
536
- </div>
510
+ </Button>
511
+ </Stack>
537
512
  ))}
538
- <button
513
+ <Button
539
514
  type="button"
515
+ variant="ghost"
516
+ size="sm"
540
517
  onClick={() => append("")}
541
- style={addButtonStyle}
518
+ style={{ padding: 0, alignSelf: 'flex-start' }}
542
519
  >
543
520
  + Add component
544
- </button>
545
- </div>
521
+ </Button>
522
+ </Stack>
546
523
  </div>
547
524
  );
548
525
  }
@@ -0,0 +1,24 @@
1
+ import type { RefObject } from "react";
2
+ import { Header, Input } from "@fragments-sdk/ui";
3
+
4
+ interface HeaderSearchProps {
5
+ value: string;
6
+ onChange: (value: string) => void;
7
+ inputRef: RefObject<HTMLInputElement>;
8
+ }
9
+
10
+ export function HeaderSearch({ value, onChange, inputRef }: HeaderSearchProps) {
11
+ return (
12
+ <Header.Search expandable>
13
+ <Input
14
+ ref={inputRef}
15
+ value={value}
16
+ onChange={onChange}
17
+ placeholder="Search components"
18
+ aria-label="Search components"
19
+ size="sm"
20
+ style={{ width: "240px" }}
21
+ />
22
+ </Header.Search>
23
+ );
24
+ }
@@ -16,6 +16,20 @@ import {
16
16
  } from '../hooks/useA11yCache.js';
17
17
  import type { A11ySummary, CachedA11yResult } from '../types/a11y.js';
18
18
 
19
+ /** Normalize category to Title Case for display */
20
+ function titleCase(str: string): string {
21
+ return str.replace(/\b\w/g, (c) => c.toUpperCase());
22
+ }
23
+
24
+ /** Normalize category to a canonical key so "Form" and "Forms" merge into one group */
25
+ function categoryKey(raw: string): string {
26
+ const tc = titleCase(raw.trim());
27
+ if (tc.length > 4 && tc.endsWith('s') && !tc.endsWith('ss')) {
28
+ return tc.slice(0, -1);
29
+ }
30
+ return tc;
31
+ }
32
+
19
33
  interface HealthDashboardProps {
20
34
  fragments: Array<{ path: string; fragment: FragmentDefinition }>;
21
35
  onNavigate?: (componentName: string) => void;
@@ -68,7 +82,7 @@ function calculateCoverage(
68
82
 
69
83
  for (const { fragment } of fragments) {
70
84
  const cat = fragment.meta.category || 'uncategorized';
71
- categories.add(cat);
85
+ categories.add(categoryKey(cat));
72
86
 
73
87
  if (fragment.meta.description && fragment.meta.description.trim().length > 10) {
74
88
  documented++;
@@ -89,7 +103,7 @@ function calculateCoverage(
89
103
 
90
104
  components.push({
91
105
  name: fragment.meta.name,
92
- category: cat,
106
+ category: titleCase(cat),
93
107
  variantCount,
94
108
  status: fragment.meta.status || 'stable',
95
109
  });
@@ -301,6 +301,15 @@ export function EmptyIcon({ className, style }: IconProps) {
301
301
  );
302
302
  }
303
303
 
304
+ // GitHub
305
+ export function GitHubIcon({ className, style }: IconProps) {
306
+ return (
307
+ <svg className={className} style={style} width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
308
+ <path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
309
+ </svg>
310
+ );
311
+ }
312
+
304
313
  // Figma
305
314
  export function FigmaIcon({ className, style }: IconProps) {
306
315
  return (