@fragments-sdk/cli 0.5.2 → 0.7.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 (124) hide show
  1. package/dist/bin.js +996 -79
  2. package/dist/bin.js.map +1 -1
  3. package/dist/{chunk-ICAIQ57V.js → chunk-6JBGU74P.js} +5 -3
  4. package/dist/chunk-6JBGU74P.js.map +1 -0
  5. package/dist/chunk-7OPWMLOE.js +1625 -0
  6. package/dist/chunk-7OPWMLOE.js.map +1 -0
  7. package/dist/{chunk-2H2JAA3U.js → chunk-CVXKXVOY.js} +3 -3
  8. package/dist/{chunk-2H2JAA3U.js.map → chunk-CVXKXVOY.js.map} +1 -1
  9. package/dist/{chunk-IOJE35DZ.js → chunk-NWQ4CJOQ.js} +3 -3
  10. package/dist/{chunk-2DJH4F4P.js → chunk-RVRTRESS.js} +3 -3
  11. package/dist/{chunk-V7YLRR4C.js → chunk-TJ34N7C7.js} +41 -4
  12. package/dist/{chunk-V7YLRR4C.js.map → chunk-TJ34N7C7.js.map} +1 -1
  13. package/dist/{chunk-XNWDI6UT.js → chunk-XHUDJNN3.js} +5 -5
  14. package/dist/{core-DKHB7FYV.js → core-W2HYIQW6.js} +4 -4
  15. package/dist/{generate-KL24VZVD.js → generate-LMTISDIJ.js} +5 -5
  16. package/dist/index.d.ts +1 -0
  17. package/dist/index.js +15 -7
  18. package/dist/index.js.map +1 -1
  19. package/dist/{init-NION5S3M.js → init-7CHRKQ7P.js} +5 -5
  20. package/dist/mcp-bin.js +8 -220
  21. package/dist/mcp-bin.js.map +1 -1
  22. package/dist/scan-WY23TJCP.js +12 -0
  23. package/dist/{service-RWUMZ3EW.js → service-T2L7VLTE.js} +5 -5
  24. package/dist/static-viewer-GBR7YNF3.js +12 -0
  25. package/dist/{test-ECPEXFDN.js → test-OJRXNDO2.js} +4 -4
  26. package/dist/{tokens-ITADYVPF.js → tokens-3BWDESVM.js} +6 -6
  27. package/dist/viewer-SUFOISZM.js +1822 -0
  28. package/dist/viewer-SUFOISZM.js.map +1 -0
  29. package/package.json +6 -5
  30. package/src/bin.ts +31 -0
  31. package/src/build.ts +147 -13
  32. package/src/cli-commands.ts +18 -0
  33. package/src/commands/__tests__/a11y-scoring.test.ts +278 -0
  34. package/src/commands/a11y-report.ts +625 -0
  35. package/src/commands/a11y.ts +168 -14
  36. package/src/commands/build.ts +16 -0
  37. package/src/commands/graph.ts +274 -0
  38. package/src/core/auto-props.ts +464 -0
  39. package/src/core/composition.ts +64 -1
  40. package/src/core/graph-extractor.test.ts +542 -0
  41. package/src/core/graph-extractor.ts +601 -0
  42. package/src/core/importAnalyzer.ts +5 -0
  43. package/src/core/schema.ts +2 -0
  44. package/src/core/types.ts +3 -1
  45. package/src/index.ts +4 -0
  46. package/src/mcp/server.ts +13 -220
  47. package/src/theme/__tests__/component-contrast.test.ts +338 -0
  48. package/src/theme/__tests__/contrast-validation.test.ts +326 -0
  49. package/src/theme/contrast.test.ts +331 -0
  50. package/src/theme/contrast.ts +246 -0
  51. package/src/theme/generator.ts +213 -1
  52. package/src/theme/index.ts +16 -0
  53. package/src/theme/types.ts +51 -0
  54. package/src/viewer/__tests__/a11y-fixes.test.ts +358 -0
  55. package/src/viewer/__tests__/viewer-integration.test.ts +2 -7
  56. package/src/viewer/components/AccessibilityPanel.tsx +493 -433
  57. package/src/viewer/components/ActionCapture.tsx +1 -1
  58. package/src/viewer/components/ActionsPanel.tsx +142 -183
  59. package/src/viewer/components/App.tsx +276 -183
  60. package/src/viewer/components/BottomPanel.tsx +40 -80
  61. package/src/viewer/components/CodePanel.tsx +9 -87
  62. package/src/viewer/components/CommandPalette.tsx +117 -74
  63. package/src/viewer/components/ComponentGraph.tsx +143 -126
  64. package/src/viewer/components/ComponentHeader.tsx +46 -43
  65. package/src/viewer/components/ContractPanel.tsx +124 -117
  66. package/src/viewer/components/ErrorBoundary.tsx +47 -35
  67. package/src/viewer/components/FigmaEmbed.tsx +18 -13
  68. package/src/viewer/components/FragmentEditor.tsx +126 -63
  69. package/src/viewer/components/HealthDashboard.tsx +146 -171
  70. package/src/viewer/components/HmrStatusIndicator.tsx +31 -41
  71. package/src/viewer/components/Icons.tsx +151 -98
  72. package/src/viewer/components/InteractionsPanel.tsx +317 -264
  73. package/src/viewer/components/IsolatedPreviewFrame.tsx +52 -27
  74. package/src/viewer/components/IsolatedRender.tsx +12 -6
  75. package/src/viewer/components/KeyboardShortcutsHelp.tsx +34 -70
  76. package/src/viewer/components/LandingPage.tsx +285 -305
  77. package/src/viewer/components/Layout.tsx +12 -10
  78. package/src/viewer/components/LeftSidebar.tsx +103 -155
  79. package/src/viewer/components/MultiViewportPreview.tsx +254 -63
  80. package/src/viewer/components/PreviewArea.tsx +113 -44
  81. package/src/viewer/components/PreviewFrameHost.tsx +36 -6
  82. package/src/viewer/components/PreviewPane.tsx +2 -3
  83. package/src/viewer/components/PreviewToolbar.tsx +109 -105
  84. package/src/viewer/components/PropsEditor.tsx +154 -74
  85. package/src/viewer/components/PropsTable.tsx +95 -82
  86. package/src/viewer/components/RelationsSection.tsx +71 -40
  87. package/src/viewer/components/ResizablePanel.tsx +158 -55
  88. package/src/viewer/components/RightSidebar.tsx +46 -56
  89. package/src/viewer/components/ScreenshotButton.tsx +12 -12
  90. package/src/viewer/components/SkeletonLoader.tsx +99 -83
  91. package/src/viewer/components/StoryRenderer.tsx +4 -11
  92. package/src/viewer/components/Toast.tsx +3 -67
  93. package/src/viewer/components/TokenStylePanel.tsx +136 -118
  94. package/src/viewer/components/UsageSection.tsx +26 -26
  95. package/src/viewer/components/VariantMatrix.tsx +140 -47
  96. package/src/viewer/components/VariantTabs.tsx +24 -68
  97. package/src/viewer/components/ViewportSelector.tsx +121 -114
  98. package/src/viewer/constants/ui.ts +23 -22
  99. package/src/viewer/entry.tsx +8 -3
  100. package/src/viewer/index.ts +3 -6
  101. package/src/viewer/preview-frame.html +43 -18
  102. package/src/viewer/server.ts +7 -16
  103. package/src/viewer/styles/globals.css +46 -85
  104. package/src/viewer/utils/a11y-fixes.ts +53 -30
  105. package/dist/chunk-ICAIQ57V.js.map +0 -1
  106. package/dist/chunk-U4GQ2JTD.js +0 -832
  107. package/dist/chunk-U4GQ2JTD.js.map +0 -1
  108. package/dist/scan-ESEXV7LF.js +0 -12
  109. package/dist/static-viewer-O37MJ5B6.js +0 -12
  110. package/dist/viewer-YDGFDTK5.js +0 -11104
  111. package/dist/viewer-YDGFDTK5.js.map +0 -1
  112. package/src/viewer/postcss.config.js +0 -6
  113. package/src/viewer/tailwind.config.js +0 -37
  114. /package/dist/{chunk-IOJE35DZ.js.map → chunk-NWQ4CJOQ.js.map} +0 -0
  115. /package/dist/{chunk-2DJH4F4P.js.map → chunk-RVRTRESS.js.map} +0 -0
  116. /package/dist/{chunk-XNWDI6UT.js.map → chunk-XHUDJNN3.js.map} +0 -0
  117. /package/dist/{core-DKHB7FYV.js.map → core-W2HYIQW6.js.map} +0 -0
  118. /package/dist/{generate-KL24VZVD.js.map → generate-LMTISDIJ.js.map} +0 -0
  119. /package/dist/{init-NION5S3M.js.map → init-7CHRKQ7P.js.map} +0 -0
  120. /package/dist/{scan-ESEXV7LF.js.map → scan-WY23TJCP.js.map} +0 -0
  121. /package/dist/{service-RWUMZ3EW.js.map → service-T2L7VLTE.js.map} +0 -0
  122. /package/dist/{static-viewer-O37MJ5B6.js.map → static-viewer-GBR7YNF3.js.map} +0 -0
  123. /package/dist/{test-ECPEXFDN.js.map → test-OJRXNDO2.js.map} +0 -0
  124. /package/dist/{tokens-ITADYVPF.js.map → tokens-3BWDESVM.js.map} +0 -0
@@ -1,6 +1,6 @@
1
1
  import { useState } from "react";
2
2
  import type { PropDefinition } from "../../core/index.js";
3
- import clsx from "clsx";
3
+ import { Input, Select, Toggle } from "@fragments/ui";
4
4
  import { ControlsIcon, ChevronDownIcon, RefreshIcon } from "./Icons.js";
5
5
 
6
6
  interface PropsEditorProps {
@@ -28,7 +28,7 @@ export function PropsEditor({
28
28
  // Compact mode - show controls in a single horizontal line
29
29
  if (compact) {
30
30
  return (
31
- <div className="flex flex-col gap-6">
31
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
32
32
  {propEntries.map(([name, prop]) => (
33
33
  <PropControl
34
34
  key={name}
@@ -42,9 +42,22 @@ export function PropsEditor({
42
42
  {hasChanges && (
43
43
  <button
44
44
  onClick={onReset}
45
- className="flex items-center gap-1.5 px-2 py-1 text-xs font-medium text-[--color-accent] hover:bg-[--bg-hover] rounded transition-colors"
45
+ style={{
46
+ display: 'flex',
47
+ alignItems: 'center',
48
+ gap: '6px',
49
+ padding: '4px 8px',
50
+ fontSize: '12px',
51
+ fontWeight: 500,
52
+ color: 'var(--color-accent)',
53
+ background: 'transparent',
54
+ border: 'none',
55
+ borderRadius: '4px',
56
+ cursor: 'pointer',
57
+ transition: 'background 0.15s',
58
+ }}
46
59
  >
47
- <RefreshIcon className="w-3 h-3" />
60
+ <RefreshIcon style={{ width: 12, height: 12 }} />
48
61
  Reset
49
62
  </button>
50
63
  )}
@@ -53,47 +66,75 @@ export function PropsEditor({
53
66
  }
54
67
 
55
68
  return (
56
- <div className="border border-[--border] rounded-xl overflow-hidden bg-[--bg-elevated]">
69
+ <div style={{ border: '1px solid var(--border)', borderRadius: '12px', overflow: 'hidden', background: 'var(--bg-elevated)' }}>
57
70
  {/* Header */}
58
71
  <button
59
72
  onClick={() => setIsCollapsed(!isCollapsed)}
60
- className="w-full px-4 py-3 flex items-center justify-between bg-[--bg-secondary] border-b border-[--border-subtle] hover:bg-[--bg-hover] transition-colors"
73
+ style={{
74
+ width: '100%',
75
+ padding: '12px 16px',
76
+ display: 'flex',
77
+ alignItems: 'center',
78
+ justifyContent: 'space-between',
79
+ background: 'var(--bg-secondary)',
80
+ borderBottom: '1px solid var(--border)',
81
+ cursor: 'pointer',
82
+ transition: 'background 0.15s',
83
+ border: 'none',
84
+ color: 'inherit',
85
+ }}
61
86
  >
62
- <div className="flex items-center gap-2">
63
- <ControlsIcon className="w-4 h-4 text-secondary" />
64
- <span className="text-[13px] font-medium text-primary">
87
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
88
+ <ControlsIcon style={{ width: 16, height: 16, color: 'var(--text-secondary)' }} />
89
+ <span style={{ fontSize: '13px', fontWeight: 500, color: 'var(--text-primary)' }}>
65
90
  Props Editor
66
91
  </span>
67
92
  {hasChanges && (
68
- <span className="px-1.5 py-0.5 text-[10px] font-medium bg-[--color-accent] text-white rounded">
93
+ <span style={{ padding: '2px 6px', fontSize: '10px', fontWeight: 500, background: 'var(--color-accent)', color: '#fff', borderRadius: '4px' }}>
69
94
  Modified
70
95
  </span>
71
96
  )}
72
97
  </div>
73
98
  <ChevronDownIcon
74
- className={clsx(
75
- "w-4 h-4 text-tertiary transition-transform",
76
- isCollapsed && "-rotate-90"
77
- )}
99
+ style={{
100
+ width: 16,
101
+ height: 16,
102
+ color: 'var(--text-tertiary)',
103
+ transition: 'transform 0.15s',
104
+ transform: isCollapsed ? 'rotate(-90deg)' : 'rotate(0deg)',
105
+ }}
78
106
  />
79
107
  </button>
80
108
 
81
109
  {/* Content */}
82
110
  {!isCollapsed && (
83
- <div className="p-4 space-y-4">
111
+ <div style={{ padding: '16px', display: 'flex', flexDirection: 'column', gap: '16px' }}>
84
112
  {/* Reset button */}
85
113
  {hasChanges && (
86
114
  <button
87
115
  onClick={onReset}
88
- className="flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium text-[--color-accent] hover:bg-[--bg-hover] rounded transition-colors"
116
+ style={{
117
+ display: 'flex',
118
+ alignItems: 'center',
119
+ gap: '6px',
120
+ padding: '6px 10px',
121
+ fontSize: '12px',
122
+ fontWeight: 500,
123
+ color: 'var(--color-accent)',
124
+ background: 'transparent',
125
+ border: 'none',
126
+ borderRadius: '4px',
127
+ cursor: 'pointer',
128
+ transition: 'background 0.15s',
129
+ }}
89
130
  >
90
- <RefreshIcon className="w-3.5 h-3.5" />
131
+ <RefreshIcon style={{ width: 14, height: 14 }} />
91
132
  Reset to defaults
92
133
  </button>
93
134
  )}
94
135
 
95
136
  {/* Prop controls */}
96
- <div className="space-y-3">
137
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
97
138
  {propEntries.map(([name, prop]) => (
98
139
  <PropControl
99
140
  key={name}
@@ -195,8 +236,8 @@ function PropControl({
195
236
 
196
237
  if (compact) {
197
238
  return (
198
- <div className="flex gap-2">
199
- <label className="text-[11px] font-medium text-secondary font-mono whitespace-nowrap min-w-[100px]">
239
+ <div style={{ display: 'flex', gap: '8px' }}>
240
+ <label style={{ fontSize: '11px', fontWeight: 500, color: 'var(--text-secondary)', fontFamily: 'monospace', whiteSpace: 'nowrap', minWidth: '100px' }}>
200
241
  {name}
201
242
  </label>
202
243
  {getControlForProp(prop, displayValue, onChange, true)}
@@ -205,17 +246,17 @@ function PropControl({
205
246
  }
206
247
 
207
248
  return (
208
- <div className="flex flex-col gap-1.5">
209
- <div className="flex items-center gap-2">
210
- <label className="text-[12px] font-medium text-primary font-mono">
249
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
250
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
251
+ <label style={{ fontSize: '12px', fontWeight: 500, color: 'var(--text-primary)', fontFamily: 'monospace' }}>
211
252
  {name}
212
253
  {prop.required && (
213
- <span className="text-[--color-danger] ml-0.5">*</span>
254
+ <span style={{ color: 'var(--color-danger, #ef4444)', marginLeft: '2px' }}>*</span>
214
255
  )}
215
256
  </label>
216
- <span className="text-[11px] text-tertiary">{prop.type}</span>
257
+ <span style={{ fontSize: '11px', color: 'var(--text-tertiary)' }}>{prop.type}</span>
217
258
  {prop.controlType && prop.controlType !== prop.type && (
218
- <span className="text-[10px] text-tertiary bg-[--bg-tertiary] px-1.5 py-0.5 rounded">
259
+ <span style={{ fontSize: '10px', color: 'var(--text-tertiary)', background: 'var(--bg-secondary)', padding: '2px 6px', borderRadius: '4px' }}>
219
260
  {prop.controlType}
220
261
  </span>
221
262
  )}
@@ -224,7 +265,7 @@ function PropControl({
224
265
  {getControlForProp(prop, displayValue, onChange, false)}
225
266
 
226
267
  {prop.description && (
227
- <p className="text-[11px] text-tertiary leading-relaxed">
268
+ <p style={{ fontSize: '11px', color: 'var(--text-tertiary)', lineHeight: 1.6 }}>
228
269
  {prop.description}
229
270
  </p>
230
271
  )}
@@ -240,20 +281,10 @@ function BooleanControl({
240
281
  onChange: (v: boolean) => void;
241
282
  }) {
242
283
  return (
243
- <button
244
- onClick={() => onChange(!value)}
245
- className={clsx(
246
- "relative inline-flex h-6 w-11 items-center rounded-full transition-colors",
247
- value ? "bg-[--color-accent]" : "bg-[--bg-tertiary]"
248
- )}
249
- >
250
- <span
251
- className={clsx(
252
- "inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform",
253
- value ? "translate-x-6" : "translate-x-1"
254
- )}
255
- />
256
- </button>
284
+ <Toggle
285
+ checked={value}
286
+ onChange={() => onChange(!value)}
287
+ />
257
288
  );
258
289
  }
259
290
 
@@ -267,17 +298,16 @@ function EnumControl({
267
298
  onChange: (v: string) => void;
268
299
  }) {
269
300
  return (
270
- <select
301
+ <Select
271
302
  value={value}
272
- onChange={(e) => onChange(e.target.value)}
273
- className="px-2.5 py-1.5 text-[12px] bg-[--bg-primary] border border-[--border] rounded-md text-primary focus:outline-none focus:ring-2 focus:ring-[--color-accent] focus:border-transparent"
303
+ onChange={(e: React.ChangeEvent<HTMLSelectElement>) => onChange(e.target.value)}
274
304
  >
275
305
  {values.map((v) => (
276
306
  <option key={v} value={v}>
277
307
  {v}
278
308
  </option>
279
309
  ))}
280
- </select>
310
+ </Select>
281
311
  );
282
312
  }
283
313
 
@@ -289,17 +319,17 @@ function NumberControl({
289
319
  onChange: (v: number) => void;
290
320
  }) {
291
321
  return (
292
- <input
322
+ <Input
293
323
  type="number"
294
324
  value={value ?? ""}
295
- onChange={(e) =>
325
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
296
326
  onChange(
297
327
  e.target.value
298
328
  ? Number(e.target.value)
299
329
  : (undefined as unknown as number)
300
330
  )
301
331
  }
302
- className="px-2.5 py-1.5 text-[12px] font-mono bg-[--bg-primary] border border-[--border] rounded-md text-primary focus:outline-none focus:ring-2 focus:ring-[--color-accent] focus:border-transparent w-32"
332
+ style={{ width: '128px', fontFamily: 'monospace' }}
303
333
  />
304
334
  );
305
335
  }
@@ -314,14 +344,14 @@ function StringControl({
314
344
  compact?: boolean;
315
345
  }) {
316
346
  return (
317
- <input
347
+ <Input
318
348
  type="text"
319
349
  value={value ?? ""}
320
- onChange={(e) => onChange(e.target.value)}
321
- className={clsx(
322
- "px-2 py-1 text-[11px] font-mono bg-[--bg-primary] border border-[--border] rounded text-primary focus:outline-none focus:ring-1 focus:ring-[--color-accent] focus:border-transparent",
323
- compact ? "w-24" : "w-full max-w-xs px-2.5 py-1.5 text-[12px]"
324
- )}
350
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => onChange(e.target.value)}
351
+ style={compact
352
+ ? { width: '96px', fontSize: '11px', fontFamily: 'monospace' }
353
+ : { width: '100%', maxWidth: '320px', fontFamily: 'monospace' }
354
+ }
325
355
  />
326
356
  );
327
357
  }
@@ -377,24 +407,56 @@ function FunctionControl({
377
407
 
378
408
  if (isEditing) {
379
409
  return (
380
- <div className="flex flex-col gap-2 flex-1">
410
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '8px', flex: 1 }}>
381
411
  <textarea
382
412
  value={inputValue}
383
413
  onChange={(e) => setInputValue(e.target.value)}
384
- className="w-full px-2 py-1.5 text-[11px] font-mono bg-[--bg-primary] border border-[--border] rounded text-primary focus:outline-none focus:ring-1 focus:ring-[--color-accent] resize-y min-h-[60px]"
414
+ style={{
415
+ width: '100%',
416
+ padding: '6px 8px',
417
+ fontSize: '11px',
418
+ fontFamily: 'monospace',
419
+ background: 'var(--bg-primary)',
420
+ border: '1px solid var(--border)',
421
+ borderRadius: '4px',
422
+ color: 'var(--text-primary)',
423
+ outline: 'none',
424
+ resize: 'vertical',
425
+ minHeight: '60px',
426
+ }}
385
427
  placeholder="() => console.log('clicked')"
386
428
  autoFocus
387
429
  />
388
- <div className="flex gap-1.5">
430
+ <div style={{ display: 'flex', gap: '6px' }}>
389
431
  <button
390
432
  onClick={handleSave}
391
- className="px-2 py-1 text-[10px] font-medium bg-[--color-accent] text-white rounded hover:opacity-90 transition-opacity"
433
+ style={{
434
+ padding: '4px 8px',
435
+ fontSize: '10px',
436
+ fontWeight: 500,
437
+ background: 'var(--color-accent)',
438
+ color: '#fff',
439
+ borderRadius: '4px',
440
+ border: 'none',
441
+ cursor: 'pointer',
442
+ transition: 'opacity 0.15s',
443
+ }}
392
444
  >
393
445
  Apply
394
446
  </button>
395
447
  <button
396
448
  onClick={handleCancel}
397
- className="px-2 py-1 text-[10px] font-medium text-tertiary hover:text-secondary hover:bg-[--bg-hover] rounded transition-colors"
449
+ style={{
450
+ padding: '4px 8px',
451
+ fontSize: '10px',
452
+ fontWeight: 500,
453
+ color: 'var(--text-tertiary)',
454
+ background: 'transparent',
455
+ borderRadius: '4px',
456
+ border: 'none',
457
+ cursor: 'pointer',
458
+ transition: 'color 0.15s, background 0.15s',
459
+ }}
398
460
  >
399
461
  Cancel
400
462
  </button>
@@ -406,7 +468,19 @@ function FunctionControl({
406
468
  return (
407
469
  <button
408
470
  onClick={handleStartEdit}
409
- className="px-2.5 py-1.5 text-[11px] font-mono text-tertiary bg-[--bg-tertiary] hover:bg-[--bg-hover] rounded-md w-fit text-left transition-colors cursor-pointer"
471
+ style={{
472
+ padding: '6px 10px',
473
+ fontSize: '11px',
474
+ fontFamily: 'monospace',
475
+ color: 'var(--text-tertiary)',
476
+ background: 'var(--bg-secondary)',
477
+ borderRadius: '6px',
478
+ width: 'fit-content',
479
+ textAlign: 'left',
480
+ transition: 'background 0.15s',
481
+ cursor: 'pointer',
482
+ border: 'none',
483
+ }}
410
484
  title="Click to edit function"
411
485
  >
412
486
  {getDisplayValue()}
@@ -424,28 +498,35 @@ function ColorControl({
424
498
  presetColors?: string[];
425
499
  }) {
426
500
  return (
427
- <div className="flex items-center gap-2">
501
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
428
502
  <input
429
503
  type="color"
430
504
  value={value || "#000000"}
431
505
  onChange={(e) => onChange(e.target.value)}
432
- className="w-8 h-8 rounded border border-[--border] cursor-pointer bg-transparent"
506
+ style={{ width: '32px', height: '32px', borderRadius: '4px', border: '1px solid var(--border)', cursor: 'pointer', background: 'transparent' }}
433
507
  />
434
- <input
508
+ <Input
435
509
  type="text"
436
510
  value={value ?? ""}
437
- onChange={(e) => onChange(e.target.value)}
511
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => onChange(e.target.value)}
438
512
  placeholder="#000000"
439
- className="px-2 py-1 text-[11px] font-mono bg-[--bg-primary] border border-[--border] rounded text-primary focus:outline-none focus:ring-1 focus:ring-[--color-accent] w-24"
513
+ style={{ width: '96px', fontSize: '11px', fontFamily: 'monospace' }}
440
514
  />
441
515
  {presetColors && presetColors.length > 0 && (
442
- <div className="flex gap-1 flex-wrap">
516
+ <div style={{ display: 'flex', gap: '4px', flexWrap: 'wrap' }}>
443
517
  {presetColors.slice(0, 6).map((color) => (
444
518
  <button
445
519
  key={color}
446
520
  onClick={() => onChange(color)}
447
- className="w-5 h-5 rounded border border-[--border] hover:ring-2 hover:ring-[--color-accent]"
448
- style={{ backgroundColor: color }}
521
+ style={{
522
+ width: '20px',
523
+ height: '20px',
524
+ borderRadius: '4px',
525
+ border: '1px solid var(--border)',
526
+ backgroundColor: color,
527
+ cursor: 'pointer',
528
+ padding: 0,
529
+ }}
449
530
  title={color}
450
531
  />
451
532
  ))}
@@ -468,14 +549,13 @@ function DateControl({
468
549
  : "";
469
550
 
470
551
  return (
471
- <input
552
+ <Input
472
553
  type="datetime-local"
473
554
  value={inputValue}
474
- onChange={(e) => {
555
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
475
556
  const date = e.target.value ? new Date(e.target.value).toISOString() : "";
476
557
  onChange(date);
477
558
  }}
478
- className="px-2.5 py-1.5 text-[12px] bg-[--bg-primary] border border-[--border] rounded-md text-primary focus:outline-none focus:ring-2 focus:ring-[--color-accent] focus:border-transparent"
479
559
  />
480
560
  );
481
561
  }
@@ -494,7 +574,7 @@ function RangeControl({
494
574
  step?: number;
495
575
  }) {
496
576
  return (
497
- <div className="flex items-center gap-3">
577
+ <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
498
578
  <input
499
579
  type="range"
500
580
  value={value ?? min}
@@ -502,9 +582,9 @@ function RangeControl({
502
582
  max={max}
503
583
  step={step}
504
584
  onChange={(e) => onChange(Number(e.target.value))}
505
- className="flex-1 h-2 bg-[--bg-tertiary] rounded-lg appearance-none cursor-pointer accent-[--color-accent]"
585
+ style={{ flex: 1, height: '8px', background: 'var(--bg-secondary)', borderRadius: '8px', cursor: 'pointer', accentColor: 'var(--color-accent)' }}
506
586
  />
507
- <span className="text-[11px] font-mono text-secondary min-w-[3rem] text-right">
587
+ <span style={{ fontSize: '11px', fontFamily: 'monospace', color: 'var(--text-secondary)', minWidth: '3rem', textAlign: 'right' }}>
508
588
  {value ?? min}
509
589
  </span>
510
590
  </div>
@@ -1,98 +1,111 @@
1
+ import { useMemo } from 'react';
1
2
  import type { PropDefinition } from '../../core/index.js';
3
+ import { Table, createColumns, Badge, Text, Stack } from '@fragments/ui';
2
4
  import { WarningIcon } from './Icons.js';
3
5
 
4
6
  interface PropsTableProps {
5
7
  props: Record<string, PropDefinition>;
6
8
  }
7
9
 
10
+ interface PropRow {
11
+ name: string;
12
+ required: boolean;
13
+ type: string;
14
+ values?: string[];
15
+ default: unknown;
16
+ description: string;
17
+ constraints?: string[];
18
+ }
19
+
8
20
  export function PropsTable({ props }: PropsTableProps) {
9
21
  const propEntries = Object.entries(props);
10
22
 
11
- if (propEntries.length === 0) return null;
12
-
13
- return (
14
- <section id="props" className="scroll-mt-24">
15
- <h2 className="text-base font-semibold text-primary mb-4">Props</h2>
16
- <div className="border border-[--border] rounded-xl overflow-hidden">
17
- <table className="w-full text-[13px]">
18
- <thead>
19
- <tr className="bg-[--bg-secondary] border-b border-[--border-subtle]">
20
- <th className="text-left px-4 py-3 text-[11px] font-medium text-tertiary uppercase tracking-wider">Prop</th>
21
- <th className="text-left px-4 py-3 text-[11px] font-medium text-tertiary uppercase tracking-wider">Type</th>
22
- <th className="text-left px-4 py-3 text-[11px] font-medium text-tertiary uppercase tracking-wider">Default</th>
23
- <th className="text-left px-4 py-3 text-[11px] font-medium text-tertiary uppercase tracking-wider">Description</th>
24
- </tr>
25
- </thead>
26
- <tbody>
27
- {propEntries.map(([name, prop], index) => (
28
- <tr
29
- key={name}
30
- className={`hover:bg-[--bg-hover] transition-colors ${
31
- index !== propEntries.length - 1 ? 'border-b border-[--border-subtle]' : ''
32
- }`}
33
- >
34
- <td className="px-4 py-3 align-top">
35
- <code className="text-[13px] font-mono text-primary font-medium">
36
- {name}
37
- {prop.required && <span className="text-[--color-danger] ml-0.5">*</span>}
38
- </code>
39
- </td>
40
- <td className="px-4 py-3 align-top">
41
- <PropType type={prop.type} values={prop.values} />
42
- </td>
43
- <td className="px-4 py-3 align-top">
44
- {prop.default !== undefined ? (
45
- <code className="text-[12px] font-mono bg-[--bg-tertiary] px-1.5 py-0.5 rounded text-secondary">
46
- {String(prop.default)}
47
- </code>
48
- ) : (
49
- <span className="text-[--text-muted]">—</span>
50
- )}
51
- </td>
52
- <td className="px-4 py-3 align-top">
53
- <div className="text-secondary leading-relaxed">{prop.description}</div>
54
- {prop.constraints && prop.constraints.length > 0 && (
55
- <div className="mt-2 space-y-1">
56
- {prop.constraints.map((constraint, index) => (
57
- <div
58
- key={index}
59
- className="text-[12px] text-[--color-warning] flex items-start gap-1.5"
60
- >
61
- <WarningIcon className="w-3 h-3 mt-0.5 flex-shrink-0" />
62
- <span>{constraint}</span>
63
- </div>
64
- ))}
65
- </div>
66
- )}
67
- </td>
68
- </tr>
69
- ))}
70
- </tbody>
71
- </table>
72
- </div>
73
- </section>
23
+ const data = useMemo<PropRow[]>(() =>
24
+ propEntries.map(([name, prop]) => ({
25
+ name,
26
+ required: !!prop.required,
27
+ type: prop.type,
28
+ values: prop.values,
29
+ default: prop.default,
30
+ description: prop.description,
31
+ constraints: prop.constraints,
32
+ })),
33
+ [propEntries]
74
34
  );
75
- }
76
35
 
77
- function PropType({ type, values }: { type: string; values?: string[] }) {
78
- if (type === 'enum' && values && values.length > 0) {
79
- return (
80
- <div className="flex flex-wrap gap-1">
81
- {values.map((value, index) => (
82
- <span key={value} className="inline-flex items-center">
83
- <code className="text-[12px] font-mono bg-[--bg-tertiary] px-1.5 py-0.5 rounded text-[--color-accent]">
84
- {value}
85
- </code>
86
- {index < values.length - 1 && <span className="text-[--text-muted] mx-1">|</span>}
87
- </span>
88
- ))}
89
- </div>
90
- );
91
- }
36
+ const columns = useMemo(() => createColumns<PropRow>([
37
+ {
38
+ key: 'name',
39
+ header: 'Prop',
40
+ cell: (row) => (
41
+ <Text font="mono" size="sm" weight="medium">
42
+ {row.name}
43
+ {row.required && <span style={{ color: 'var(--color-danger, #ef4444)', marginLeft: '2px' }}>*</span>}
44
+ </Text>
45
+ ),
46
+ },
47
+ {
48
+ key: 'type',
49
+ header: 'Type',
50
+ cell: (row) => {
51
+ if (row.type === 'enum' && row.values && row.values.length > 0) {
52
+ return (
53
+ <Stack direction="row" gap="xs" style={{ flexWrap: 'wrap' }}>
54
+ {row.values.map((value, index) => (
55
+ <span key={value} style={{ display: 'inline-flex', alignItems: 'center' }}>
56
+ <Badge size="sm">{value}</Badge>
57
+ {index < row.values!.length - 1 && <Text size="xs" color="tertiary" style={{ margin: '0 4px' }}>|</Text>}
58
+ </span>
59
+ ))}
60
+ </Stack>
61
+ );
62
+ }
63
+ return <Text font="mono" size="xs" color="secondary">{row.type}</Text>;
64
+ },
65
+ },
66
+ {
67
+ key: 'default',
68
+ header: 'Default',
69
+ cell: (row) => {
70
+ if (row.default !== undefined) {
71
+ return <Text font="mono" size="xs" color="secondary">{String(row.default)}</Text>;
72
+ }
73
+ return <Text color="tertiary">&mdash;</Text>;
74
+ },
75
+ },
76
+ {
77
+ key: 'description',
78
+ header: 'Description',
79
+ cell: (row) => (
80
+ <Stack gap="xs">
81
+ <Text size="sm" color="secondary">{row.description}</Text>
82
+ {row.constraints && row.constraints.length > 0 && (
83
+ <Stack gap="xs">
84
+ {row.constraints.map((constraint, index) => (
85
+ <Stack key={index} direction="row" gap="xs" align="start" style={{ fontSize: '12px', color: 'var(--color-warning, #f59e0b)' }}>
86
+ <WarningIcon style={{ width: 12, height: 12, marginTop: '2px', flexShrink: 0 }} />
87
+ <span>{constraint}</span>
88
+ </Stack>
89
+ ))}
90
+ </Stack>
91
+ )}
92
+ </Stack>
93
+ ),
94
+ },
95
+ ]), []);
96
+
97
+ if (propEntries.length === 0) return null;
92
98
 
93
99
  return (
94
- <code className="text-[12px] font-mono text-secondary">
95
- {type}
96
- </code>
100
+ <section id="props" style={{ scrollMarginTop: '96px' }}>
101
+ <Text as="h2" size="md" weight="semibold" style={{ marginBottom: '16px' }}>Props</Text>
102
+ <Table
103
+ columns={columns}
104
+ data={data}
105
+ size="sm"
106
+ bordered
107
+ getRowId={(row) => row.name}
108
+ />
109
+ </section>
97
110
  );
98
111
  }