@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,485 @@
1
+ import { useState, useCallback } from "react";
2
+ import { useForm, useFieldArray, Controller } from "react-hook-form";
3
+ import type { Fragment, FragmentUsage, FragmentMeta } from "../../core/index.js";
4
+ import clsx from "clsx";
5
+ import { ChevronDownIcon } from "./Icons.js";
6
+
7
+ interface FragmentEditorProps {
8
+ /** Component name */
9
+ componentName: string;
10
+ /** Initial fragment data (if exists) */
11
+ fragment?: Fragment;
12
+ /** Called when the user saves changes */
13
+ onSave: (fragment: Fragment) => Promise<void>;
14
+ /** Whether save is in progress */
15
+ saving?: boolean;
16
+ }
17
+
18
+ interface FormData {
19
+ name: string;
20
+ description: string;
21
+ usage: {
22
+ when: string[];
23
+ doNot: Array<{ text: string; instead?: string }>;
24
+ };
25
+ accessibility: {
26
+ role: string;
27
+ requirements: string[];
28
+ };
29
+ related: {
30
+ similar: string[];
31
+ composedWith: string[];
32
+ usedIn: string[];
33
+ };
34
+ meta: {
35
+ owner: string;
36
+ status: "draft" | "experimental" | "beta" | "stable" | "deprecated";
37
+ since: string;
38
+ tags: string[];
39
+ };
40
+ }
41
+
42
+ export function FragmentEditor({
43
+ componentName,
44
+ fragment,
45
+ onSave,
46
+ saving = false,
47
+ }: FragmentEditorProps) {
48
+ const [expandedSections, setExpandedSections] = useState<Set<string>>(
49
+ new Set(["usage", "meta"])
50
+ );
51
+
52
+ const toggleSection = (section: string) => {
53
+ setExpandedSections((prev) => {
54
+ const next = new Set(prev);
55
+ if (next.has(section)) {
56
+ next.delete(section);
57
+ } else {
58
+ next.add(section);
59
+ }
60
+ return next;
61
+ });
62
+ };
63
+
64
+ const defaultValues: FormData = {
65
+ name: fragment?.name || componentName,
66
+ description: fragment?.description || "",
67
+ usage: {
68
+ when: fragment?.usage?.when || [],
69
+ doNot:
70
+ fragment?.usage?.doNot?.map((item) =>
71
+ typeof item === "string" ? { text: item } : item
72
+ ) || [],
73
+ },
74
+ accessibility: {
75
+ role: fragment?.accessibility?.role || "",
76
+ requirements: fragment?.accessibility?.requirements || [],
77
+ },
78
+ related: {
79
+ similar: fragment?.related?.similar || [],
80
+ composedWith: fragment?.related?.composedWith || [],
81
+ usedIn: fragment?.related?.usedIn || [],
82
+ },
83
+ meta: {
84
+ owner: fragment?.meta?.owner || "",
85
+ status: fragment?.meta?.status || "stable",
86
+ since: fragment?.meta?.since || "",
87
+ tags: fragment?.meta?.tags || [],
88
+ },
89
+ };
90
+
91
+ const {
92
+ register,
93
+ control,
94
+ handleSubmit,
95
+ formState: { isDirty },
96
+ } = useForm<FormData>({ defaultValues });
97
+
98
+ const {
99
+ fields: whenFields,
100
+ append: appendWhen,
101
+ remove: removeWhen,
102
+ } = useFieldArray({ control, name: "usage.when" as const });
103
+
104
+ const {
105
+ fields: doNotFields,
106
+ append: appendDoNot,
107
+ remove: removeDoNot,
108
+ } = useFieldArray({ control, name: "usage.doNot" });
109
+
110
+ const {
111
+ fields: requirementsFields,
112
+ append: appendRequirement,
113
+ remove: removeRequirement,
114
+ } = useFieldArray({ control, name: "accessibility.requirements" as const });
115
+
116
+ const onSubmit = useCallback(
117
+ async (data: FormData) => {
118
+ const fragment: Fragment = {
119
+ $schema: "https://fragments.dev/schema/v1.json",
120
+ name: data.name,
121
+ description: data.description || undefined,
122
+ usage: {
123
+ when: data.usage.when.filter(Boolean),
124
+ doNot: data.usage.doNot.filter((d) => d.text),
125
+ },
126
+ accessibility:
127
+ data.accessibility.role || data.accessibility.requirements.length > 0
128
+ ? {
129
+ role: data.accessibility.role || undefined,
130
+ requirements: data.accessibility.requirements.filter(Boolean),
131
+ }
132
+ : undefined,
133
+ related:
134
+ data.related.similar.length > 0 ||
135
+ data.related.composedWith.length > 0 ||
136
+ data.related.usedIn.length > 0
137
+ ? {
138
+ similar: data.related.similar.filter(Boolean),
139
+ composedWith: data.related.composedWith.filter(Boolean),
140
+ usedIn: data.related.usedIn.filter(Boolean),
141
+ }
142
+ : undefined,
143
+ meta: {
144
+ status: data.meta.status,
145
+ owner: data.meta.owner || undefined,
146
+ since: data.meta.since || undefined,
147
+ tags: data.meta.tags.filter(Boolean),
148
+ },
149
+ };
150
+
151
+ await onSave(fragment);
152
+ },
153
+ [onSave]
154
+ );
155
+
156
+ return (
157
+ <form
158
+ onSubmit={handleSubmit(onSubmit)}
159
+ className="h-full flex flex-col bg-[--bg-primary]"
160
+ >
161
+ {/* Header */}
162
+ <div className="px-4 py-3 border-b border-[--border] bg-[--bg-secondary]">
163
+ <h3 className="text-sm font-semibold text-primary">Fragment Editor</h3>
164
+ <p className="text-xs text-secondary mt-0.5">
165
+ Enrich {componentName} with metadata
166
+ </p>
167
+ </div>
168
+
169
+ {/* Scrollable content */}
170
+ <div className="flex-1 overflow-y-auto">
171
+ {/* Basic Info */}
172
+ <div className="p-4 border-b border-[--border]">
173
+ <label className="block text-xs font-medium text-secondary mb-1.5">
174
+ Description
175
+ </label>
176
+ <textarea
177
+ {...register("description")}
178
+ rows={3}
179
+ className="w-full px-3 py-2 text-sm bg-[--bg-elevated] border border-[--border] rounded-lg resize-none focus:outline-none focus:ring-2 focus:ring-[--color-accent] text-primary"
180
+ placeholder="Brief description of the component's purpose..."
181
+ />
182
+ </div>
183
+
184
+ {/* Usage Section */}
185
+ <Section
186
+ title="Usage Guidelines"
187
+ expanded={expandedSections.has("usage")}
188
+ onToggle={() => toggleSection("usage")}
189
+ >
190
+ {/* When to Use */}
191
+ <div className="mb-4">
192
+ <label className="block text-xs font-medium text-secondary mb-2">
193
+ When to Use
194
+ </label>
195
+ <div className="space-y-2">
196
+ {whenFields.map((field, index) => (
197
+ <div key={field.id} className="flex gap-2">
198
+ <input
199
+ {...register(`usage.when.${index}` as const)}
200
+ className="flex-1 px-3 py-1.5 text-sm bg-[--bg-elevated] border border-[--border] rounded-lg focus:outline-none focus:ring-2 focus:ring-[--color-accent] text-primary"
201
+ placeholder="Scenario..."
202
+ />
203
+ <button
204
+ type="button"
205
+ onClick={() => removeWhen(index)}
206
+ className="px-2 text-secondary hover:text-error"
207
+ >
208
+ &times;
209
+ </button>
210
+ </div>
211
+ ))}
212
+ <button
213
+ type="button"
214
+ onClick={() => appendWhen("")}
215
+ className="text-xs text-[--color-accent] hover:underline"
216
+ >
217
+ + Add scenario
218
+ </button>
219
+ </div>
220
+ </div>
221
+
222
+ {/* Do Not */}
223
+ <div>
224
+ <label className="block text-xs font-medium text-secondary mb-2">
225
+ Do Not
226
+ </label>
227
+ <div className="space-y-2">
228
+ {doNotFields.map((field, index) => (
229
+ <div key={field.id} className="flex gap-2">
230
+ <input
231
+ {...register(`usage.doNot.${index}.text` as const)}
232
+ className="flex-1 px-3 py-1.5 text-sm bg-[--bg-elevated] border border-[--border] rounded-lg focus:outline-none focus:ring-2 focus:ring-[--color-accent] text-primary"
233
+ placeholder="What not to do..."
234
+ />
235
+ <input
236
+ {...register(`usage.doNot.${index}.instead` as const)}
237
+ className="w-32 px-3 py-1.5 text-sm bg-[--bg-elevated] border border-[--border] rounded-lg focus:outline-none focus:ring-2 focus:ring-[--color-accent] text-primary"
238
+ placeholder="Instead..."
239
+ />
240
+ <button
241
+ type="button"
242
+ onClick={() => removeDoNot(index)}
243
+ className="px-2 text-secondary hover:text-error"
244
+ >
245
+ &times;
246
+ </button>
247
+ </div>
248
+ ))}
249
+ <button
250
+ type="button"
251
+ onClick={() => appendDoNot({ text: "", instead: "" })}
252
+ className="text-xs text-[--color-accent] hover:underline"
253
+ >
254
+ + Add anti-pattern
255
+ </button>
256
+ </div>
257
+ </div>
258
+ </Section>
259
+
260
+ {/* Accessibility Section */}
261
+ <Section
262
+ title="Accessibility"
263
+ expanded={expandedSections.has("accessibility")}
264
+ onToggle={() => toggleSection("accessibility")}
265
+ >
266
+ <div className="mb-4">
267
+ <label className="block text-xs font-medium text-secondary mb-1.5">
268
+ ARIA Role
269
+ </label>
270
+ <input
271
+ {...register("accessibility.role")}
272
+ className="w-full px-3 py-1.5 text-sm bg-[--bg-elevated] border border-[--border] rounded-lg focus:outline-none focus:ring-2 focus:ring-[--color-accent] text-primary"
273
+ placeholder="button, dialog, etc."
274
+ />
275
+ </div>
276
+
277
+ <div>
278
+ <label className="block text-xs font-medium text-secondary mb-2">
279
+ Requirements
280
+ </label>
281
+ <div className="space-y-2">
282
+ {requirementsFields.map((field, index) => (
283
+ <div key={field.id} className="flex gap-2">
284
+ <input
285
+ {...register(
286
+ `accessibility.requirements.${index}` as const
287
+ )}
288
+ className="flex-1 px-3 py-1.5 text-sm bg-[--bg-elevated] border border-[--border] rounded-lg focus:outline-none focus:ring-2 focus:ring-[--color-accent] text-primary"
289
+ placeholder="Requirement..."
290
+ />
291
+ <button
292
+ type="button"
293
+ onClick={() => removeRequirement(index)}
294
+ className="px-2 text-secondary hover:text-error"
295
+ >
296
+ &times;
297
+ </button>
298
+ </div>
299
+ ))}
300
+ <button
301
+ type="button"
302
+ onClick={() => appendRequirement("")}
303
+ className="text-xs text-[--color-accent] hover:underline"
304
+ >
305
+ + Add requirement
306
+ </button>
307
+ </div>
308
+ </div>
309
+ </Section>
310
+
311
+ {/* Related Components Section */}
312
+ <Section
313
+ title="Related Components"
314
+ expanded={expandedSections.has("related")}
315
+ onToggle={() => toggleSection("related")}
316
+ >
317
+ <RelatedField
318
+ label="Similar"
319
+ description="Alternative components"
320
+ name="related.similar"
321
+ control={control}
322
+ register={register}
323
+ />
324
+ <RelatedField
325
+ label="Composed With"
326
+ description="Often used together"
327
+ name="related.composedWith"
328
+ control={control}
329
+ register={register}
330
+ />
331
+ <RelatedField
332
+ label="Used In"
333
+ description="Parent components"
334
+ name="related.usedIn"
335
+ control={control}
336
+ register={register}
337
+ />
338
+ </Section>
339
+
340
+ {/* Meta Section */}
341
+ <Section
342
+ title="Metadata"
343
+ expanded={expandedSections.has("meta")}
344
+ onToggle={() => toggleSection("meta")}
345
+ >
346
+ <div className="grid grid-cols-2 gap-4 mb-4">
347
+ <div>
348
+ <label className="block text-xs font-medium text-secondary mb-1.5">
349
+ Status
350
+ </label>
351
+ <select
352
+ {...register("meta.status")}
353
+ className="w-full px-3 py-1.5 text-sm bg-[--bg-elevated] border border-[--border] rounded-lg focus:outline-none focus:ring-2 focus:ring-[--color-accent] text-primary"
354
+ >
355
+ <option value="draft">Draft</option>
356
+ <option value="experimental">Experimental</option>
357
+ <option value="beta">Beta</option>
358
+ <option value="stable">Stable</option>
359
+ <option value="deprecated">Deprecated</option>
360
+ </select>
361
+ </div>
362
+ <div>
363
+ <label className="block text-xs font-medium text-secondary mb-1.5">
364
+ Since Version
365
+ </label>
366
+ <input
367
+ {...register("meta.since")}
368
+ className="w-full px-3 py-1.5 text-sm bg-[--bg-elevated] border border-[--border] rounded-lg focus:outline-none focus:ring-2 focus:ring-[--color-accent] text-primary"
369
+ placeholder="1.0.0"
370
+ />
371
+ </div>
372
+ </div>
373
+ <div className="mb-4">
374
+ <label className="block text-xs font-medium text-secondary mb-1.5">
375
+ Owner
376
+ </label>
377
+ <input
378
+ {...register("meta.owner")}
379
+ className="w-full px-3 py-1.5 text-sm bg-[--bg-elevated] border border-[--border] rounded-lg focus:outline-none focus:ring-2 focus:ring-[--color-accent] text-primary"
380
+ placeholder="@team-name or email"
381
+ />
382
+ </div>
383
+ </Section>
384
+ </div>
385
+
386
+ {/* Footer with save button */}
387
+ <div className="px-4 py-3 border-t border-[--border] bg-[--bg-secondary]">
388
+ <button
389
+ type="submit"
390
+ disabled={!isDirty || saving}
391
+ className={clsx(
392
+ "w-full py-2 px-4 text-sm font-medium rounded-lg transition-colors",
393
+ isDirty && !saving
394
+ ? "bg-[--color-accent] text-white hover:bg-[--color-accent-hover]"
395
+ : "bg-[--bg-disabled] text-tertiary cursor-not-allowed"
396
+ )}
397
+ >
398
+ {saving ? "Saving..." : "Save Fragment"}
399
+ </button>
400
+ </div>
401
+ </form>
402
+ );
403
+ }
404
+
405
+ interface SectionProps {
406
+ title: string;
407
+ expanded: boolean;
408
+ onToggle: () => void;
409
+ children: React.ReactNode;
410
+ }
411
+
412
+ function Section({ title, expanded, onToggle, children }: SectionProps) {
413
+ return (
414
+ <div className="border-b border-[--border]">
415
+ <button
416
+ type="button"
417
+ onClick={onToggle}
418
+ className="w-full px-4 py-3 flex items-center justify-between bg-[--bg-secondary] hover:bg-[--bg-hover] transition-colors"
419
+ >
420
+ <span className="text-xs font-semibold text-primary uppercase tracking-wide">
421
+ {title}
422
+ </span>
423
+ <ChevronDownIcon
424
+ className={clsx(
425
+ "w-4 h-4 text-tertiary transition-transform",
426
+ !expanded && "-rotate-90"
427
+ )}
428
+ />
429
+ </button>
430
+ {expanded && <div className="p-4">{children}</div>}
431
+ </div>
432
+ );
433
+ }
434
+
435
+ interface RelatedFieldProps {
436
+ label: string;
437
+ description: string;
438
+ name: string;
439
+ control: any;
440
+ register: any;
441
+ }
442
+
443
+ function RelatedField({
444
+ label,
445
+ description,
446
+ name,
447
+ control,
448
+ register,
449
+ }: RelatedFieldProps) {
450
+ const { fields, append, remove } = useFieldArray({ control, name });
451
+
452
+ return (
453
+ <div className="mb-4 last:mb-0">
454
+ <label className="block text-xs font-medium text-secondary mb-0.5">
455
+ {label}
456
+ </label>
457
+ <p className="text-[10px] text-tertiary mb-2">{description}</p>
458
+ <div className="space-y-2">
459
+ {fields.map((field, index) => (
460
+ <div key={field.id} className="flex gap-2">
461
+ <input
462
+ {...register(`${name}.${index}` as const)}
463
+ className="flex-1 px-3 py-1.5 text-sm bg-[--bg-elevated] border border-[--border] rounded-lg focus:outline-none focus:ring-2 focus:ring-[--color-accent] text-primary"
464
+ placeholder="ComponentName"
465
+ />
466
+ <button
467
+ type="button"
468
+ onClick={() => remove(index)}
469
+ className="px-2 text-secondary hover:text-error"
470
+ >
471
+ &times;
472
+ </button>
473
+ </div>
474
+ ))}
475
+ <button
476
+ type="button"
477
+ onClick={() => append("")}
478
+ className="text-xs text-[--color-accent] hover:underline"
479
+ >
480
+ + Add component
481
+ </button>
482
+ </div>
483
+ </div>
484
+ );
485
+ }