@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,512 @@
1
+ import { useState } from "react";
2
+ import type { PropDefinition } from "../../core/index.js";
3
+ import clsx from "clsx";
4
+ import { ControlsIcon, ChevronDownIcon, RefreshIcon } from "./Icons.js";
5
+
6
+ interface PropsEditorProps {
7
+ props: Record<string, PropDefinition>;
8
+ values: Record<string, unknown>;
9
+ onChange: (name: string, value: unknown) => void;
10
+ onReset: () => void;
11
+ hasChanges: boolean;
12
+ compact?: boolean;
13
+ }
14
+
15
+ export function PropsEditor({
16
+ props,
17
+ values,
18
+ onChange,
19
+ onReset,
20
+ hasChanges,
21
+ compact = false,
22
+ }: PropsEditorProps) {
23
+ const [isCollapsed, setIsCollapsed] = useState(false);
24
+ const propEntries = Object.entries(props);
25
+
26
+ if (propEntries.length === 0) return null;
27
+
28
+ // Compact mode - show controls in a single horizontal line
29
+ if (compact) {
30
+ return (
31
+ <div className="flex flex-col gap-6">
32
+ {propEntries.map(([name, prop]) => (
33
+ <PropControl
34
+ key={name}
35
+ name={name}
36
+ prop={prop}
37
+ value={values[name]}
38
+ onChange={(value) => onChange(name, value)}
39
+ compact
40
+ />
41
+ ))}
42
+ {hasChanges && (
43
+ <button
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"
46
+ >
47
+ <RefreshIcon className="w-3 h-3" />
48
+ Reset
49
+ </button>
50
+ )}
51
+ </div>
52
+ );
53
+ }
54
+
55
+ return (
56
+ <div className="border border-[--border] rounded-xl overflow-hidden bg-[--bg-elevated]">
57
+ {/* Header */}
58
+ <button
59
+ 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"
61
+ >
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">
65
+ Props Editor
66
+ </span>
67
+ {hasChanges && (
68
+ <span className="px-1.5 py-0.5 text-[10px] font-medium bg-[--color-accent] text-white rounded">
69
+ Modified
70
+ </span>
71
+ )}
72
+ </div>
73
+ <ChevronDownIcon
74
+ className={clsx(
75
+ "w-4 h-4 text-tertiary transition-transform",
76
+ isCollapsed && "-rotate-90"
77
+ )}
78
+ />
79
+ </button>
80
+
81
+ {/* Content */}
82
+ {!isCollapsed && (
83
+ <div className="p-4 space-y-4">
84
+ {/* Reset button */}
85
+ {hasChanges && (
86
+ <button
87
+ 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"
89
+ >
90
+ <RefreshIcon className="w-3.5 h-3.5" />
91
+ Reset to defaults
92
+ </button>
93
+ )}
94
+
95
+ {/* Prop controls */}
96
+ <div className="space-y-3">
97
+ {propEntries.map(([name, prop]) => (
98
+ <PropControl
99
+ key={name}
100
+ name={name}
101
+ prop={prop}
102
+ value={values[name]}
103
+ onChange={(value) => onChange(name, value)}
104
+ />
105
+ ))}
106
+ </div>
107
+ </div>
108
+ )}
109
+ </div>
110
+ );
111
+ }
112
+
113
+ interface PropControlProps {
114
+ name: string;
115
+ prop: PropDefinition;
116
+ value: unknown;
117
+ onChange: (value: unknown) => void;
118
+ compact?: boolean;
119
+ }
120
+
121
+ /**
122
+ * Determine which control to render based on prop type and controlType
123
+ */
124
+ function getControlForProp(
125
+ prop: PropDefinition,
126
+ displayValue: unknown,
127
+ onChange: (value: unknown) => void,
128
+ compact: boolean = false
129
+ ) {
130
+ // Check controlType first for special controls (color, date, range)
131
+ if (prop.controlType === "color") {
132
+ return (
133
+ <ColorControl
134
+ value={displayValue as string}
135
+ onChange={onChange}
136
+ presetColors={prop.controlOptions?.presetColors}
137
+ />
138
+ );
139
+ }
140
+
141
+ if (prop.controlType === "date") {
142
+ return <DateControl value={displayValue as string} onChange={onChange} />;
143
+ }
144
+
145
+ if (prop.controlType === "range") {
146
+ return (
147
+ <RangeControl
148
+ value={displayValue as number}
149
+ onChange={onChange}
150
+ min={prop.controlOptions?.min}
151
+ max={prop.controlOptions?.max}
152
+ step={prop.controlOptions?.step}
153
+ />
154
+ );
155
+ }
156
+
157
+ // Fall back to type-based controls
158
+ if (prop.type === "boolean") {
159
+ return <BooleanControl value={displayValue as boolean} onChange={onChange} />;
160
+ }
161
+
162
+ if (prop.type === "enum" && prop.values) {
163
+ return (
164
+ <EnumControl
165
+ value={displayValue as string}
166
+ values={prop.values as string[]}
167
+ onChange={onChange}
168
+ />
169
+ );
170
+ }
171
+
172
+ if (prop.type === "number") {
173
+ return <NumberControl value={displayValue as number} onChange={onChange} />;
174
+ }
175
+
176
+ if (prop.type === "string") {
177
+ return <StringControl value={displayValue as string} onChange={onChange} compact={compact} />;
178
+ }
179
+
180
+ if (prop.type === "function") {
181
+ return <FunctionControl value={displayValue} onChange={onChange} />;
182
+ }
183
+
184
+ return <StringControl value={String(displayValue)} onChange={onChange} compact={compact} />;
185
+ }
186
+
187
+ function PropControl({
188
+ name,
189
+ prop,
190
+ value,
191
+ onChange,
192
+ compact = false,
193
+ }: PropControlProps) {
194
+ const displayValue = value ?? prop.default ?? "";
195
+
196
+ if (compact) {
197
+ return (
198
+ <div className="flex gap-2">
199
+ <label className="text-[11px] font-medium text-secondary font-mono whitespace-nowrap min-w-[100px]">
200
+ {name}
201
+ </label>
202
+ {getControlForProp(prop, displayValue, onChange, true)}
203
+ </div>
204
+ );
205
+ }
206
+
207
+ 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">
211
+ {name}
212
+ {prop.required && (
213
+ <span className="text-[--color-danger] ml-0.5">*</span>
214
+ )}
215
+ </label>
216
+ <span className="text-[11px] text-tertiary">{prop.type}</span>
217
+ {prop.controlType && prop.controlType !== prop.type && (
218
+ <span className="text-[10px] text-tertiary bg-[--bg-tertiary] px-1.5 py-0.5 rounded">
219
+ {prop.controlType}
220
+ </span>
221
+ )}
222
+ </div>
223
+
224
+ {getControlForProp(prop, displayValue, onChange, false)}
225
+
226
+ {prop.description && (
227
+ <p className="text-[11px] text-tertiary leading-relaxed">
228
+ {prop.description}
229
+ </p>
230
+ )}
231
+ </div>
232
+ );
233
+ }
234
+
235
+ function BooleanControl({
236
+ value,
237
+ onChange,
238
+ }: {
239
+ value: boolean;
240
+ onChange: (v: boolean) => void;
241
+ }) {
242
+ 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>
257
+ );
258
+ }
259
+
260
+ function EnumControl({
261
+ value,
262
+ values,
263
+ onChange,
264
+ }: {
265
+ value: string;
266
+ values: string[];
267
+ onChange: (v: string) => void;
268
+ }) {
269
+ return (
270
+ <select
271
+ 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"
274
+ >
275
+ {values.map((v) => (
276
+ <option key={v} value={v}>
277
+ {v}
278
+ </option>
279
+ ))}
280
+ </select>
281
+ );
282
+ }
283
+
284
+ function NumberControl({
285
+ value,
286
+ onChange,
287
+ }: {
288
+ value: number;
289
+ onChange: (v: number) => void;
290
+ }) {
291
+ return (
292
+ <input
293
+ type="number"
294
+ value={value ?? ""}
295
+ onChange={(e) =>
296
+ onChange(
297
+ e.target.value
298
+ ? Number(e.target.value)
299
+ : (undefined as unknown as number)
300
+ )
301
+ }
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"
303
+ />
304
+ );
305
+ }
306
+
307
+ function StringControl({
308
+ value,
309
+ onChange,
310
+ compact = false,
311
+ }: {
312
+ value: string;
313
+ onChange: (v: string) => void;
314
+ compact?: boolean;
315
+ }) {
316
+ return (
317
+ <input
318
+ type="text"
319
+ 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
+ )}
325
+ />
326
+ );
327
+ }
328
+
329
+ function FunctionControl({
330
+ value,
331
+ onChange,
332
+ }: {
333
+ value: unknown;
334
+ onChange: (v: unknown) => void;
335
+ }) {
336
+ const [isEditing, setIsEditing] = useState(false);
337
+ const [inputValue, setInputValue] = useState('');
338
+
339
+ // Get a display string for the function
340
+ const getDisplayValue = () => {
341
+ if (typeof value === 'function') {
342
+ const fnStr = value.toString();
343
+ // Truncate if too long
344
+ if (fnStr.length > 50) {
345
+ return fnStr.slice(0, 47) + '...';
346
+ }
347
+ return fnStr;
348
+ }
349
+ if (typeof value === 'string' && value.trim()) {
350
+ return value;
351
+ }
352
+ return '() => {}';
353
+ };
354
+
355
+ const handleStartEdit = () => {
356
+ // Convert function to string for editing
357
+ if (typeof value === 'function') {
358
+ setInputValue(value.toString());
359
+ } else if (typeof value === 'string') {
360
+ setInputValue(value);
361
+ } else {
362
+ setInputValue('() => {}');
363
+ }
364
+ setIsEditing(true);
365
+ };
366
+
367
+ const handleSave = () => {
368
+ // Save as string - the parent will handle conversion
369
+ onChange(inputValue);
370
+ setIsEditing(false);
371
+ };
372
+
373
+ const handleCancel = () => {
374
+ setIsEditing(false);
375
+ setInputValue('');
376
+ };
377
+
378
+ if (isEditing) {
379
+ return (
380
+ <div className="flex flex-col gap-2 flex-1">
381
+ <textarea
382
+ value={inputValue}
383
+ 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]"
385
+ placeholder="() => console.log('clicked')"
386
+ autoFocus
387
+ />
388
+ <div className="flex gap-1.5">
389
+ <button
390
+ onClick={handleSave}
391
+ className="px-2 py-1 text-[10px] font-medium bg-[--color-accent] text-white rounded hover:opacity-90 transition-opacity"
392
+ >
393
+ Apply
394
+ </button>
395
+ <button
396
+ onClick={handleCancel}
397
+ className="px-2 py-1 text-[10px] font-medium text-tertiary hover:text-secondary hover:bg-[--bg-hover] rounded transition-colors"
398
+ >
399
+ Cancel
400
+ </button>
401
+ </div>
402
+ </div>
403
+ );
404
+ }
405
+
406
+ return (
407
+ <button
408
+ 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"
410
+ title="Click to edit function"
411
+ >
412
+ {getDisplayValue()}
413
+ </button>
414
+ );
415
+ }
416
+
417
+ function ColorControl({
418
+ value,
419
+ onChange,
420
+ presetColors,
421
+ }: {
422
+ value: string;
423
+ onChange: (v: string) => void;
424
+ presetColors?: string[];
425
+ }) {
426
+ return (
427
+ <div className="flex items-center gap-2">
428
+ <input
429
+ type="color"
430
+ value={value || "#000000"}
431
+ onChange={(e) => onChange(e.target.value)}
432
+ className="w-8 h-8 rounded border border-[--border] cursor-pointer bg-transparent"
433
+ />
434
+ <input
435
+ type="text"
436
+ value={value ?? ""}
437
+ onChange={(e) => onChange(e.target.value)}
438
+ 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"
440
+ />
441
+ {presetColors && presetColors.length > 0 && (
442
+ <div className="flex gap-1 flex-wrap">
443
+ {presetColors.slice(0, 6).map((color) => (
444
+ <button
445
+ key={color}
446
+ onClick={() => onChange(color)}
447
+ className="w-5 h-5 rounded border border-[--border] hover:ring-2 hover:ring-[--color-accent]"
448
+ style={{ backgroundColor: color }}
449
+ title={color}
450
+ />
451
+ ))}
452
+ </div>
453
+ )}
454
+ </div>
455
+ );
456
+ }
457
+
458
+ function DateControl({
459
+ value,
460
+ onChange,
461
+ }: {
462
+ value: string;
463
+ onChange: (v: string) => void;
464
+ }) {
465
+ // Convert ISO string to datetime-local format for input
466
+ const inputValue = value
467
+ ? new Date(value).toISOString().slice(0, 16)
468
+ : "";
469
+
470
+ return (
471
+ <input
472
+ type="datetime-local"
473
+ value={inputValue}
474
+ onChange={(e) => {
475
+ const date = e.target.value ? new Date(e.target.value).toISOString() : "";
476
+ onChange(date);
477
+ }}
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
+ />
480
+ );
481
+ }
482
+
483
+ function RangeControl({
484
+ value,
485
+ onChange,
486
+ min = 0,
487
+ max = 100,
488
+ step = 1,
489
+ }: {
490
+ value: number;
491
+ onChange: (v: number) => void;
492
+ min?: number;
493
+ max?: number;
494
+ step?: number;
495
+ }) {
496
+ return (
497
+ <div className="flex items-center gap-3">
498
+ <input
499
+ type="range"
500
+ value={value ?? min}
501
+ min={min}
502
+ max={max}
503
+ step={step}
504
+ 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]"
506
+ />
507
+ <span className="text-[11px] font-mono text-secondary min-w-[3rem] text-right">
508
+ {value ?? min}
509
+ </span>
510
+ </div>
511
+ );
512
+ }
@@ -0,0 +1,98 @@
1
+ import type { PropDefinition } from '../../core/index.js';
2
+ import { WarningIcon } from './Icons.js';
3
+
4
+ interface PropsTableProps {
5
+ props: Record<string, PropDefinition>;
6
+ }
7
+
8
+ export function PropsTable({ props }: PropsTableProps) {
9
+ const propEntries = Object.entries(props);
10
+
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>
74
+ );
75
+ }
76
+
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
+ }
92
+
93
+ return (
94
+ <code className="text-[12px] font-mono text-secondary">
95
+ {type}
96
+ </code>
97
+ );
98
+ }
@@ -0,0 +1,57 @@
1
+ import type { ComponentRelation } from '../../core/index.js';
2
+ import clsx from 'clsx';
3
+ import { getRelationshipConfig } from '../constants/ui.js';
4
+ import { ChevronRightIcon } from './Icons.js';
5
+
6
+ interface RelationsSectionProps {
7
+ relations: ComponentRelation[];
8
+ onNavigate?: (componentName: string) => void;
9
+ }
10
+
11
+ export function RelationsSection({ relations, onNavigate }: RelationsSectionProps) {
12
+ if (relations.length === 0) return null;
13
+
14
+ return (
15
+ <section id="relations" className="scroll-mt-24">
16
+ <h2 className="text-base font-semibold text-primary mb-4">Related Components</h2>
17
+ <div className="grid sm:grid-cols-2 gap-3">
18
+ {relations.map((relation, index) => {
19
+ const config = getRelationshipConfig(relation.relationship);
20
+
21
+ return (
22
+ <button
23
+ key={index}
24
+ onClick={() => onNavigate?.(relation.component)}
25
+ className={clsx(
26
+ 'group text-left p-4 rounded-xl border border-[--border]',
27
+ 'bg-[--bg-elevated] hover:border-[--border-strong]',
28
+ 'transition-all duration-150',
29
+ 'focus:outline-none focus-visible:ring-2 focus-visible:ring-[--color-accent]'
30
+ )}
31
+ >
32
+ <div className="flex items-center gap-2 mb-1.5">
33
+ <span className="text-[13px] font-medium text-primary group-hover:text-[--color-accent] transition-colors">
34
+ {relation.component}
35
+ </span>
36
+ <span className={clsx(
37
+ 'text-[10px] font-medium px-1.5 py-0.5 rounded',
38
+ config.bg,
39
+ config.text
40
+ )}>
41
+ {config.label}
42
+ </span>
43
+ </div>
44
+ {relation.note && (
45
+ <p className="text-[13px] text-secondary leading-relaxed">{relation.note}</p>
46
+ )}
47
+ <div className="mt-2 flex items-center text-[12px] text-tertiary group-hover:text-secondary transition-colors">
48
+ <span>View component</span>
49
+ <ChevronRightIcon className="w-3 h-3 ml-1 group-hover:translate-x-0.5 transition-transform" />
50
+ </div>
51
+ </button>
52
+ );
53
+ })}
54
+ </div>
55
+ </section>
56
+ );
57
+ }