dslinter 0.1.13 → 0.2.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 (181) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +50 -29
  3. package/bin/dslinter.mjs +26 -5
  4. package/bin/lib/config-hide-component.mjs +44 -0
  5. package/bin/lib/config-hide-component.test.mjs +33 -0
  6. package/bin/lib/constants.mjs +20 -0
  7. package/bin/lib/dev-banner.mjs +16 -51
  8. package/bin/lib/dev-banner.test.mjs +20 -18
  9. package/bin/lib/enrich-playgrounds-from-ts.mjs +201 -0
  10. package/bin/lib/enrich-playgrounds-from-ts.test.mjs +74 -0
  11. package/bin/lib/enrich-report-cli.mjs +14 -0
  12. package/bin/lib/env.mjs +20 -0
  13. package/bin/lib/infer-prop-types-from-ts.mjs +381 -0
  14. package/bin/lib/infer-prop-types-from-ts.test.mjs +174 -0
  15. package/bin/lib/parse-args.mjs +13 -1
  16. package/bin/lib/parse-args.test.mjs +7 -1
  17. package/bin/lib/paths.mjs +8 -0
  18. package/bin/lib/project-root.mjs +72 -10
  19. package/bin/lib/project-root.test.mjs +32 -1
  20. package/bin/lib/prompt.mjs +31 -0
  21. package/bin/lib/resolve-project.mjs +78 -0
  22. package/bin/lib/resolve-project.test.mjs +74 -0
  23. package/bin/lib/run-scanner.mjs +40 -6
  24. package/bin/lib/scaffold-config.mjs +96 -8
  25. package/bin/lib/scaffold-config.test.mjs +12 -2
  26. package/bin/lib/scan-host.mjs +44 -0
  27. package/bin/lib/scan-host.test.mjs +41 -0
  28. package/bin/lib/setup-readiness.mjs +153 -0
  29. package/bin/lib/setup-readiness.test.mjs +32 -0
  30. package/bin/modes/build.mjs +31 -6
  31. package/bin/modes/dev.mjs +55 -21
  32. package/bin/modes/init.mjs +3 -22
  33. package/bin/modes/init.test.mjs +1 -1
  34. package/bin/modes/mcp.mjs +49 -0
  35. package/bin/modes/report.mjs +29 -4
  36. package/bin/modes/watch.mjs +85 -0
  37. package/dashboard-dist/assets/DashboardLayoutAuto-Bja3BuZZ.css +1 -0
  38. package/dashboard-dist/assets/DashboardLayoutAuto-h0gP_iKd.js +1 -0
  39. package/dashboard-dist/assets/axe-DDaE9JTN.js +20 -0
  40. package/dashboard-dist/assets/index-B9sZ6wHm.css +1 -0
  41. package/dashboard-dist/assets/index-DIDBt5ed.js +218 -0
  42. package/dashboard-dist/index.html +2 -2
  43. package/index.cjs +53 -52
  44. package/index.d.ts +3 -0
  45. package/package.json +18 -12
  46. package/shared/env.ts +15 -0
  47. package/shared/paths.ts +8 -0
  48. package/shared/reportPath.test.ts +19 -0
  49. package/shared/reportPath.ts +12 -0
  50. package/shared/servePort.ts +16 -0
  51. package/src/components/ComponentInspectPane.tsx +67 -19
  52. package/src/components/ComponentPlaygroundPane.tsx +262 -113
  53. package/src/components/DashboardCommandPalette.tsx +6 -11
  54. package/src/components/GovernancePane.tsx +2 -2
  55. package/src/components/HideFromCatalogButton.tsx +44 -0
  56. package/src/components/OpenInEditorButton.tsx +36 -0
  57. package/src/components/PlaygroundA11yAndCode.tsx +53 -53
  58. package/src/components/PlaygroundAppThemeWrapper.tsx +82 -0
  59. package/src/components/PlaygroundControls.tsx +5 -11
  60. package/src/components/PlaygroundPreviewErrorBoundary.tsx +54 -0
  61. package/src/components/PlaygroundUsageCode.tsx +6 -4
  62. package/src/components/PlaygroundVariantMatrix.tsx +101 -34
  63. package/src/components/Section.tsx +5 -2
  64. package/src/components/Sidebar.tsx +131 -46
  65. package/src/components/TruncatedPath.tsx +44 -0
  66. package/src/components/controlApiTable.test.ts +29 -0
  67. package/src/components/controlApiTable.ts +3 -0
  68. package/src/components/playgroundUsageHighlight.ts +14 -3
  69. package/src/components/ui/badge.tsx +1 -1
  70. package/src/components/ui/table.tsx +2 -2
  71. package/src/dashboard/ComponentCatalog.tsx +16 -23
  72. package/src/dashboard/ComponentUsageDetails.tsx +6 -15
  73. package/src/dashboard/DashboardBody.tsx +0 -35
  74. package/src/dashboard/FindingsList.tsx +65 -55
  75. package/src/dashboard/ScannedTokenWall.tsx +3 -3
  76. package/src/dashboard/aggregate.test.ts +74 -0
  77. package/src/dashboard/aggregate.ts +145 -21
  78. package/src/dashboard/catalogVisibility.test.ts +93 -0
  79. package/src/dashboard/catalogVisibility.ts +108 -0
  80. package/src/dashboard/editorLink.test.ts +57 -0
  81. package/src/dashboard/editorLink.ts +71 -0
  82. package/src/dashboard/paths.test.ts +49 -0
  83. package/src/dashboard/paths.ts +51 -3
  84. package/src/dashboard/updateDslintConfig.ts +22 -0
  85. package/src/dashboard/useWorkspaceReport.ts +21 -17
  86. package/src/index.ts +26 -0
  87. package/src/mcp/agent-context.ts +148 -0
  88. package/src/mcp/agent-query.test.ts +89 -0
  89. package/src/mcp/agent-query.ts +373 -0
  90. package/src/mcp/config.ts +53 -0
  91. package/src/mcp/index.ts +18 -0
  92. package/src/mcp/normalize-paths.ts +65 -0
  93. package/src/mcp/report-cache.ts +209 -0
  94. package/src/mcp/rule-catalog.json +156 -0
  95. package/src/mcp/rule-catalog.ts +33 -0
  96. package/src/mcp/schemas.ts +54 -0
  97. package/src/mcp/server.test.ts +44 -0
  98. package/src/mcp/server.ts +343 -0
  99. package/src/mcp/start.ts +29 -0
  100. package/src/mcp/verify-loop.test.ts +49 -0
  101. package/src/mcp/verify-loop.ts +149 -0
  102. package/src/playground/appPreviewTheme.test.ts +148 -0
  103. package/src/playground/appPreviewTheme.ts +137 -0
  104. package/src/playground/buildCompoundPlaygroundEntries.test.ts +348 -0
  105. package/src/playground/buildCompoundPlaygroundEntries.ts +625 -0
  106. package/src/playground/buildPlaygroundEntriesFromReport.test.ts +420 -6
  107. package/src/playground/buildPlaygroundEntriesFromReport.ts +206 -285
  108. package/src/playground/catalogIdFromPlaygroundExport.test.ts +15 -0
  109. package/src/playground/catalogIdFromPlaygroundExport.ts +8 -0
  110. package/src/playground/collectDefinedPlaygrounds.test.ts +59 -0
  111. package/src/playground/collectDefinedPlaygrounds.ts +68 -0
  112. package/src/playground/controls.ts +177 -0
  113. package/src/playground/createPlaygroundRegistry.ts +1 -1
  114. package/src/playground/definePlayground.tsx +88 -16
  115. package/src/playground/definePlaygroundFromKit.ts +17 -0
  116. package/src/playground/embedGlobKey.ts +8 -0
  117. package/src/playground/enrichKitControls.test.ts +25 -0
  118. package/src/playground/enrichKitControls.ts +197 -0
  119. package/src/playground/expandPlaygroundControls.test.ts +50 -0
  120. package/src/playground/expandPlaygroundControls.ts +97 -0
  121. package/src/playground/inferKitJsx.test.ts +77 -0
  122. package/src/playground/inferKitJsx.ts +165 -0
  123. package/src/playground/inferKitParams.test.ts +41 -0
  124. package/src/playground/inferKitParams.ts +113 -0
  125. package/src/playground/inferPropTypesFromTs.d.mts +47 -0
  126. package/src/playground/inferPropTypesFromTs.mjs +343 -0
  127. package/src/playground/inferPropTypesFromTs.test.ts +227 -0
  128. package/src/playground/inferPropTypesFromTs.ts +17 -0
  129. package/src/playground/mergePlaygroundEntries.test.ts +32 -0
  130. package/src/playground/mergePlaygroundEntries.ts +28 -0
  131. package/src/playground/playgroundJoin.test.ts +79 -19
  132. package/src/playground/playgroundJoin.ts +47 -22
  133. package/src/playground/playgroundModuleExport.test.ts +42 -0
  134. package/src/playground/playgroundModuleExport.ts +22 -0
  135. package/src/playground/playgroundSpecsKey.ts +8 -0
  136. package/src/playground/propCoerce.ts +91 -0
  137. package/src/playground/scanVariantA11y.test.ts +46 -0
  138. package/src/playground/scanVariantA11y.ts +107 -0
  139. package/src/playground/snippet.ts +83 -0
  140. package/src/playground/usePlaygroundFromReport.test.ts +18 -8
  141. package/src/playground/usePlaygroundFromReport.ts +3 -1
  142. package/src/report/a11yForModule.ts +2 -7
  143. package/src/report/a11yScoring.test.ts +24 -0
  144. package/src/report/a11yScoring.ts +17 -0
  145. package/src/report/index.ts +6 -0
  146. package/src/shell/DashboardLayout.tsx +71 -45
  147. package/src/shell/DashboardLayoutAuto.tsx +0 -4
  148. package/src/shell/hashRoute.test.ts +7 -15
  149. package/src/shell/hashRoute.ts +31 -31
  150. package/src/shell/useHashRoute.ts +38 -13
  151. package/src/styles/dashboard-theme.css +18 -7
  152. package/src/types/controls.ts +11 -0
  153. package/src/types/playground.ts +4 -0
  154. package/src/types/report.ts +32 -9
  155. package/templates/playground/buildRegistry.ts +1 -1
  156. package/templates/vite.dslinter.snippet.ts +15 -4
  157. package/vite/collectScanModules.test.ts +51 -3
  158. package/vite/collectScanModules.ts +85 -29
  159. package/vite/consumer.config.mjs +6 -3
  160. package/vite/consumerAlias.test.ts +47 -0
  161. package/vite/consumerAlias.ts +114 -0
  162. package/vite/embedTailwindSources.test.ts +74 -0
  163. package/vite/embedTailwindSources.ts +97 -0
  164. package/vite/loadConsumerAliases.test.ts +131 -0
  165. package/vite/loadConsumerAliases.ts +155 -0
  166. package/vite/openFileInEditor.mjs +196 -0
  167. package/vite/openFileInEditor.test.mjs +87 -0
  168. package/vite/plugin.resolve.test.ts +72 -0
  169. package/vite/plugin.ts +216 -19
  170. package/vite/reportPath.test.ts +19 -0
  171. package/vite/resolveWayfinderImport.ts +56 -0
  172. package/vite/shims/inertia-react.tsx +85 -0
  173. package/vite/shims/wayfinder-actions.ts +33 -0
  174. package/vite/shims/wayfinder-routes.ts +30 -0
  175. package/vite/shims/ziggy-js.ts +12 -0
  176. package/dashboard-dist/assets/DashboardLayoutAuto-Bm7yfyC-.css +0 -1
  177. package/dashboard-dist/assets/DashboardLayoutAuto-DgwO_itB.js +0 -1
  178. package/dashboard-dist/assets/index-Cbv7vXvH.css +0 -1
  179. package/dashboard-dist/assets/index-e20cwqnb.js +0 -206
  180. package/src/components/playgroundUsageTwoslash.ts +0 -69
  181. package/templates/vite.dslint-scan-alias.snippet.ts +0 -4
@@ -14,7 +14,11 @@ import { a11ySummaryForModule } from "../report/a11yForModule";
14
14
  import { codeScoreSummaryForModule } from "../report/codeScoreForModule";
15
15
  import { tokenStyleFindingsForModule } from "../report/tokenStyleFindingsForModule";
16
16
  import type { WorkspaceReport } from "../types/report";
17
- import { aggregateDeclaredProps, usageMap } from "../dashboard/aggregate";
17
+ import {
18
+ aggregateDeclaredProps,
19
+ componentCatalogFamilyForName,
20
+ usageMap,
21
+ } from "../dashboard/aggregate";
18
22
  import { defaultArgsFromControls } from "../types/controls";
19
23
  import type { PlaygroundArgs } from "../types/controls";
20
24
  import type { PlaygroundEntry } from "../types/playground";
@@ -26,15 +30,28 @@ import {
26
30
  PlaygroundTokenStyleSection,
27
31
  PlaygroundUsageSection,
28
32
  } from "./PlaygroundA11yAndCode";
33
+ import { PlaygroundAppThemeWrapper } from "./PlaygroundAppThemeWrapper";
34
+ import { PlaygroundPreviewErrorBoundary } from "./PlaygroundPreviewErrorBoundary";
29
35
  import { PlaygroundVariantMatrix } from "./PlaygroundVariantMatrix";
30
36
  import { enumerateControlCombinations } from "../playground/enumerateControlCombinations";
37
+ import {
38
+ mergePlaygroundA11yFindings,
39
+ playgroundA11yScore,
40
+ type PlaygroundA11yFinding,
41
+ } from "../playground/scanVariantA11y";
42
+ import { HideFromCatalogButton } from "./HideFromCatalogButton";
43
+ import { OpenInEditorButton } from "./OpenInEditorButton";
31
44
  import { Section } from "./Section";
45
+ import {
46
+ resolveModuleAbsolutePath,
47
+ } from "../dashboard/editorLink";
32
48
 
33
49
  type Props = {
34
50
  entry: PlaygroundEntry;
35
- formatModulePath?: (modulePath: string) => string;
36
51
  workspaceReport: WorkspaceReport | null;
37
52
  reportReady: boolean;
53
+ onOpenComponent: (componentId: string) => void;
54
+ onHideFromCatalog?: (componentId: string) => void;
38
55
  };
39
56
 
40
57
  const MIN_PREVIEW_PX = 280;
@@ -59,6 +76,31 @@ function nextPreviewWidthForResize(
59
76
  return clampPreviewWidth(prevPreview, nextOuter);
60
77
  }
61
78
 
79
+ function PreviewResizeHandle({
80
+ side,
81
+ onPointerDown,
82
+ }: {
83
+ side: "left" | "right";
84
+ onPointerDown: (e: PointerEvent<HTMLButtonElement>) => void;
85
+ }) {
86
+ const positionClass =
87
+ side === "left" ? "left-0 -translate-x-1/2" : "right-0 translate-x-1/2";
88
+
89
+ return (
90
+ <button
91
+ type="button"
92
+ className={`absolute top-0 bottom-0 z-10 flex w-4 ${positionClass} cursor-ew-resize touch-none items-center justify-center rounded border-0 bg-muted p-0 shadow-xs ring-1 ring-border hover:bg-accent focus:outline-none focus-visible:ring-2 focus-visible:ring-ring`}
93
+ aria-label="Resize preview from center (drag left or right)"
94
+ onPointerDown={onPointerDown}
95
+ >
96
+ <span
97
+ className="h-10 w-px rounded-full bg-muted-foreground/40"
98
+ aria-hidden
99
+ />
100
+ </button>
101
+ );
102
+ }
103
+
62
104
  function TocLink({ href, children }: { href: string; children: ReactNode }) {
63
105
  const handleClick = (e: MouseEvent<HTMLAnchorElement>) => {
64
106
  // Modifier keys / non-primary clicks → fall back to default browser behaviour.
@@ -95,14 +137,15 @@ function TocLink({ href, children }: { href: string; children: ReactNode }) {
95
137
 
96
138
  export function ComponentPlaygroundPane({
97
139
  entry,
98
- formatModulePath,
99
140
  workspaceReport,
100
141
  reportReady,
142
+ onOpenComponent,
143
+ onHideFromCatalog,
101
144
  }: Props) {
102
- const { Preview } = entry;
103
- const rel = formatModulePath
104
- ? formatModulePath(entry.modulePath)
105
- : entry.modulePath.replace(/^\.\.\//, "");
145
+ const { renderPreview } = entry;
146
+ const sourceAbsolutePath = workspaceReport?.root
147
+ ? resolveModuleAbsolutePath(workspaceReport.root, entry.modulePath)
148
+ : undefined;
106
149
 
107
150
  const [values, setValues] = useState<PlaygroundArgs>(() =>
108
151
  defaultArgsFromControls(entry.controls),
@@ -151,6 +194,8 @@ export function ComponentPlaygroundPane({
151
194
 
152
195
  const previewMeasureRef = useRef<HTMLDivElement>(null);
153
196
  const previewFrameRef = useRef<HTMLDivElement>(null);
197
+ const previewWidthLabelRef = useRef<HTMLSpanElement>(null);
198
+ const livePreviewWidthRef = useRef(DEFAULT_PREVIEW_PX);
154
199
  const maxOuterRef = useRef(0);
155
200
  const [maxOuterPx, setMaxOuterPx] = useState(0);
156
201
  const [previewWidthPx, setPreviewWidthPx] = useState(DEFAULT_PREVIEW_PX);
@@ -159,6 +204,24 @@ export function ComponentPlaygroundPane({
159
204
  null,
160
205
  );
161
206
 
207
+ useEffect(() => {
208
+ livePreviewWidthRef.current = previewWidthPx;
209
+ }, [previewWidthPx]);
210
+
211
+ const applyLivePreviewWidth = useCallback((w: number) => {
212
+ const clamped = clampPreviewWidth(w, maxOuterRef.current);
213
+ livePreviewWidthRef.current = clamped;
214
+ const frame = previewFrameRef.current;
215
+ if (frame) {
216
+ frame.style.width = `${clamped}px`;
217
+ }
218
+ const label = previewWidthLabelRef.current;
219
+ if (label) {
220
+ label.textContent = `${Math.round(clamped)}px`;
221
+ }
222
+ return clamped;
223
+ }, []);
224
+
162
225
  const syncPreviewToOuterWidth = useCallback((nextOuter: number) => {
163
226
  if (!Number.isFinite(nextOuter) || nextOuter <= 0) return;
164
227
  const prevOuter = maxOuterRef.current;
@@ -203,32 +266,61 @@ export function ComponentPlaygroundPane({
203
266
  return () => window.removeEventListener("resize", syncUsemodsBreakpoints);
204
267
  }, [syncUsemodsBreakpoints]);
205
268
 
206
- const attachSymmetricWidthDrag = useCallback((side: "left" | "right") => {
207
- return (e: PointerEvent<HTMLButtonElement>) => {
208
- if (e.button !== 0) return;
209
- e.preventDefault();
210
- const target = e.currentTarget;
211
- target.setPointerCapture(e.pointerId);
212
- let lastX = e.clientX;
213
- const sign = side === "right" ? 1 : -1;
214
- const onMove = (ev: globalThis.PointerEvent) => {
215
- const dx = ev.clientX - lastX;
216
- lastX = ev.clientX;
217
- setPreviewWidthPx((w) =>
218
- clampPreviewWidth(w + sign * 2 * dx, maxOuterRef.current),
219
- );
220
- };
221
- const onUp = (ev: globalThis.PointerEvent) => {
222
- target.releasePointerCapture(ev.pointerId);
223
- window.removeEventListener("pointermove", onMove);
224
- window.removeEventListener("pointerup", onUp);
225
- window.removeEventListener("pointercancel", onUp);
269
+ const attachSymmetricWidthDrag = useCallback(
270
+ (side: "left" | "right") => {
271
+ return (e: PointerEvent<HTMLButtonElement>) => {
272
+ if (e.button !== 0) return;
273
+ e.preventDefault();
274
+ const target = e.currentTarget;
275
+ target.setPointerCapture(e.pointerId);
276
+
277
+ let lastX = e.clientX;
278
+ let currentWidth = livePreviewWidthRef.current;
279
+ let rafId = 0;
280
+ const sign = side === "right" ? 1 : -1;
281
+ const prevBodyCursor = document.body.style.cursor;
282
+ const prevBodyUserSelect = document.body.style.userSelect;
283
+ document.body.style.cursor = "ew-resize";
284
+ document.body.style.userSelect = "none";
285
+
286
+ const flushWidth = () => {
287
+ rafId = 0;
288
+ applyLivePreviewWidth(currentWidth);
289
+ };
290
+
291
+ const onMove = (ev: globalThis.PointerEvent) => {
292
+ const dx = ev.clientX - lastX;
293
+ lastX = ev.clientX;
294
+ currentWidth = clampPreviewWidth(
295
+ currentWidth + sign * 2 * dx,
296
+ maxOuterRef.current,
297
+ );
298
+ if (!rafId) {
299
+ rafId = requestAnimationFrame(flushWidth);
300
+ }
301
+ };
302
+
303
+ const endDrag = (ev: globalThis.PointerEvent) => {
304
+ if (rafId) {
305
+ cancelAnimationFrame(rafId);
306
+ applyLivePreviewWidth(currentWidth);
307
+ }
308
+ setPreviewWidthPx(livePreviewWidthRef.current);
309
+ document.body.style.cursor = prevBodyCursor;
310
+ document.body.style.userSelect = prevBodyUserSelect;
311
+ target.releasePointerCapture(ev.pointerId);
312
+ window.removeEventListener("pointermove", onMove);
313
+ window.removeEventListener("pointerup", endDrag);
314
+ window.removeEventListener("pointercancel", endDrag);
315
+ };
316
+
317
+ window.addEventListener("pointermove", onMove);
318
+ window.addEventListener("pointerup", endDrag);
319
+ window.addEventListener("pointercancel", endDrag);
226
320
  };
227
- window.addEventListener("pointermove", onMove);
228
- window.addEventListener("pointerup", onUp);
229
- window.addEventListener("pointercancel", onUp);
230
- };
231
- }, []);
321
+ },
322
+ [applyLivePreviewWidth],
323
+ );
232
324
 
233
325
  const hasControls = entry.controls.length > 0;
234
326
 
@@ -240,38 +332,105 @@ export function ComponentPlaygroundPane({
240
332
  const showVariantsSection =
241
333
  hasControls &&
242
334
  (variantEnumeration.combinations.length > 0 ||
243
- (variantEnumeration.combinations.length === 0 &&
244
- variantEnumeration.totalCount === 0));
335
+ variantEnumeration.totalCount === 0);
336
+
337
+ const [variantA11yFindings, setVariantA11yFindings] = useState<
338
+ PlaygroundA11yFinding[]
339
+ >([]);
340
+ const [variantScanComplete, setVariantScanComplete] = useState(false);
341
+
342
+ useEffect(() => {
343
+ setVariantA11yFindings([]);
344
+ setVariantScanComplete(false);
345
+ }, [entry.id, variantEnumeration.combinations]);
346
+
347
+ const handleVariantA11yScan = useCallback(
348
+ (findings: PlaygroundA11yFinding[]) => {
349
+ setVariantA11yFindings(findings);
350
+ setVariantScanComplete(true);
351
+ },
352
+ [],
353
+ );
354
+
355
+ const combinedA11y = useMemo(() => {
356
+ const findings = mergePlaygroundA11yFindings(
357
+ a11y.findings,
358
+ variantA11yFindings,
359
+ );
360
+ return {
361
+ ...a11y,
362
+ findings,
363
+ issueCount: findings.length,
364
+ score: playgroundA11yScore(a11y.findings, variantA11yFindings),
365
+ };
366
+ }, [a11y, variantA11yFindings]);
367
+
368
+ const hasVariantMatrix = variantEnumeration.combinations.length > 0;
369
+ const variantScanPending = hasVariantMatrix && !variantScanComplete;
370
+ const a11yScoreLabel =
371
+ reportReady || variantScanComplete
372
+ ? `${combinedA11y.score}/100${variantScanPending ? "…" : ""}`
373
+ : "—";
374
+
375
+ const report = reportReady ? workspaceReport : null;
376
+ const family = useMemo(
377
+ () => componentCatalogFamilyForName(report, entry.id),
378
+ [report, entry.id],
379
+ );
380
+ const childComponents = family?.parent === entry.id ? family.children : [];
381
+ const resetControls = () =>
382
+ setValues(defaultArgsFromControls(entry.controls));
383
+
384
+ const tocItems: { href: string; label: string; show?: boolean }[] = [
385
+ { href: "#api-reference", label: "API reference", show: hasControls },
386
+ { href: "#usage", label: "Usage" },
387
+ {
388
+ href: "#subcomponents",
389
+ label: "Subcomponents",
390
+ show: childComponents.length > 0,
391
+ },
392
+ { href: "#repo-usage", label: "Repo usage" },
393
+ { href: "#design-tokens", label: "Design tokens" },
394
+ { href: "#code-score", label: "Code score" },
395
+ { href: "#accessibility", label: "Accessibility" },
396
+ { href: "#variants", label: "Variants", show: showVariantsSection },
397
+ ];
245
398
 
246
399
  return (
247
400
  <div className="flex min-h-0 flex-1 flex-col overflow-hidden bg-background">
248
401
  <div className="min-h-0 flex-1 overflow-auto">
249
- <header id="source" className="scroll-mt-20 border-b border-border bg-card p-6">
250
- <div className="mx-auto">
251
- <div className="flex flex-wrap items-start justify-between gap-4">
252
- <div className="min-w-0">
253
- <p className="text-sm font-medium text-muted-foreground">
254
- {entry.meta.group ? (
255
- <>
256
- Components <span className="text-muted-foreground/40">/</span>{" "}
257
- <span className="capitalize text-foreground/80">
258
- {entry.meta.group}
259
- </span>
260
- </>
261
- ) : (
262
- "Components"
263
- )}
264
- </p>
265
- <h1 className="text-3xl font-semibold tracking-tight text-foreground">
266
- {entry.meta.title}
267
- </h1>
268
- <p
269
- className="mt-1 truncate font-mono text-xs text-muted-foreground"
270
- title={rel}
271
- >
272
- {rel}
273
- </p>
274
- </div>
402
+ <header
403
+ id="source"
404
+ className="scroll-mt-20 border-b border-border bg-card p-6"
405
+ >
406
+ <div className="flex flex-wrap items-start justify-between gap-4">
407
+ <div className="min-w-0">
408
+ <p className="text-sm font-medium text-muted-foreground">
409
+ Components
410
+ {entry.meta.group ? (
411
+ <>
412
+ {" "}
413
+ <span className="text-muted-foreground/40">/</span>{" "}
414
+ <span className="capitalize text-foreground/80">
415
+ {entry.meta.group}
416
+ </span>
417
+ </>
418
+ ) : null}
419
+ </p>
420
+ <h1 className="text-3xl font-semibold tracking-tight text-foreground">
421
+ {entry.meta.title}
422
+ </h1>
423
+ </div>
424
+ <div className="flex shrink-0 flex-wrap items-center gap-2">
425
+ {sourceAbsolutePath ? (
426
+ <OpenInEditorButton filePath={sourceAbsolutePath} />
427
+ ) : null}
428
+ {onHideFromCatalog ? (
429
+ <HideFromCatalogButton
430
+ componentName={entry.meta.id}
431
+ onHidden={onHideFromCatalog}
432
+ />
433
+ ) : null}
275
434
  </div>
276
435
  </div>
277
436
  </header>
@@ -284,44 +443,39 @@ export function ComponentPlaygroundPane({
284
443
  <div className="flex justify-center">
285
444
  <div
286
445
  ref={previewFrameRef}
287
- className="relative min-w-0 shrink-0 select-none rounded-lg border border-border bg-muted/50 shadow-xs"
446
+ className="relative min-w-0 shrink-0 select-none rounded-lg border border-border bg-muted/50 shadow-xs will-change-[width]"
288
447
  style={{ width: previewWidthPx }}
289
448
  >
290
- <button
291
- type="button"
292
- className="absolute left-0 top-0 bottom-0 z-10 flex w-4 -translate-x-1/2 cursor-ew-resize touch-none items-center justify-center rounded border-0 bg-muted p-0 shadow-xs ring-1 ring-border hover:bg-accent focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
293
- aria-label="Resize preview from center (drag left or right)"
449
+ <PreviewResizeHandle
450
+ side="left"
294
451
  onPointerDown={attachSymmetricWidthDrag("left")}
295
- >
296
- <span
297
- className="h-10 w-px rounded-full bg-muted-foreground/40"
298
- aria-hidden
299
- />
300
- </button>
301
- <button
302
- type="button"
303
- className="absolute right-0 top-0 bottom-0 z-10 flex w-4 translate-x-1/2 cursor-ew-resize touch-none items-center justify-center rounded border-0 bg-muted p-0 shadow-xs ring-1 ring-border hover:bg-accent focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
304
- aria-label="Resize preview from center (drag left or right)"
452
+ />
453
+ <PreviewResizeHandle
454
+ side="right"
305
455
  onPointerDown={attachSymmetricWidthDrag("right")}
456
+ />
457
+ <PlaygroundAppThemeWrapper
458
+ workspaceReport={report}
459
+ className="min-w-0 p-8 backdrop-blur-2xl"
306
460
  >
307
- <span
308
- className="h-10 w-px rounded-full bg-muted-foreground/40"
309
- aria-hidden
310
- />
311
- </button>
312
- <div className="min-w-0 bg-card p-8">
313
- <Preview values={values} />
314
- </div>
461
+ <PlaygroundPreviewErrorBoundary
462
+ componentName={entry.meta.title}
463
+ >
464
+ {renderPreview(values)}
465
+ </PlaygroundPreviewErrorBoundary>
466
+ </PlaygroundAppThemeWrapper>
315
467
  </div>
316
468
  </div>
317
469
  {maxOuterPx > 0 ? (
318
- <div className="mt-4 divide-x divide-border h-6 overflow-hidden items-center mx-auto flex w-fit border border-border bg-card text-center text-xs/none tabular-nums font-mono rounded-sm text-muted-foreground">
319
- <span className="p-2.5">{Math.round(previewWidthPx)}px</span>
320
- <span className=" p-2.5" title="usemods detectBreakpoint">
470
+ <div className="mx-auto mt-4 flex h-6 w-fit items-center overflow-hidden rounded-sm border border-border bg-card text-center font-mono text-xs/none tabular-nums text-muted-foreground divide-x divide-border">
471
+ <span ref={previewWidthLabelRef} className="p-2.5">
472
+ {Math.round(previewWidthPx)}px
473
+ </span>
474
+ <span className="p-2.5" title="usemods detectBreakpoint">
321
475
  Screen: {windowBreakpoint ?? "—"}
322
476
  </span>
323
477
  <span
324
- className=" p-2.5"
478
+ className="p-2.5"
325
479
  title="usemods detectContainerBreakpoint"
326
480
  >
327
481
  Container: {containerBreakpoint ?? "—"}
@@ -336,27 +490,21 @@ export function ComponentPlaygroundPane({
336
490
  <div className="min-w-0 space-y-14">
337
491
  {hasControls ? (
338
492
  <PlaygroundApiReference
493
+ entry={entry}
339
494
  controls={entry.controls}
340
495
  values={values}
341
496
  onChange={setValues}
342
- onReset={() =>
343
- setValues(defaultArgsFromControls(entry.controls))
344
- }
497
+ onReset={resetControls}
345
498
  reportUsage={repoUsage}
346
499
  declaredPropsFromScan={declaredPropsFromScan}
347
- governanceReportLoaded={
348
- reportReady && workspaceReport != null
349
- }
500
+ governanceReportLoaded={report != null}
350
501
  />
351
502
  ) : null}
352
503
 
353
504
  <PlaygroundUsageSection entry={entry} values={values} />
354
505
 
355
506
  <Section id="repo-usage" title="Repo usage" description="">
356
- <ComponentUsageDetails
357
- report={reportReady ? workspaceReport : null}
358
- componentId={entry.id}
359
- />
507
+ <ComponentUsageDetails report={report} componentId={entry.id} />
360
508
  </Section>
361
509
 
362
510
  <Section
@@ -383,10 +531,14 @@ export function ComponentPlaygroundPane({
383
531
 
384
532
  <Section
385
533
  id="accessibility"
386
- title={`Accessibility: ${reportReady ? a11y.score : "—"}/100`}
387
- description="Accessibility checks and findings from the workspace DSLinter report scoped to this file."
534
+ title={`Accessibility: ${a11yScoreLabel}`}
535
+ description="Static accessibility rules from the DSLinter report, plus runtime color-contrast checks on each variant preview below."
388
536
  >
389
- <PlaygroundA11ySection a11y={a11y} reportReady={reportReady} />
537
+ <PlaygroundA11ySection
538
+ a11y={combinedA11y}
539
+ reportReady={reportReady || variantScanComplete}
540
+ variantScanPending={variantScanPending}
541
+ />
390
542
  </Section>
391
543
  </div>
392
544
 
@@ -398,17 +550,13 @@ export function ComponentPlaygroundPane({
398
550
  <p className="mb-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
399
551
  On this page
400
552
  </p>
401
- {hasControls ? (
402
- <TocLink href="#api-reference">API reference</TocLink>
403
- ) : null}
404
- <TocLink href="#usage">Usage</TocLink>
405
- <TocLink href="#repo-usage">Repo usage</TocLink>
406
- <TocLink href="#design-tokens">Design tokens</TocLink>
407
- <TocLink href="#code-score">Code score</TocLink>
408
- <TocLink href="#accessibility">Accessibility</TocLink>
409
- {showVariantsSection ? (
410
- <TocLink href="#variants">Variants</TocLink>
411
- ) : null}
553
+ {tocItems.map(({ href, label, show = true }) =>
554
+ show ? (
555
+ <TocLink key={href} href={href}>
556
+ {label}
557
+ </TocLink>
558
+ ) : null,
559
+ )}
412
560
  </nav>
413
561
  </aside>
414
562
  </div>
@@ -417,18 +565,19 @@ export function ComponentPlaygroundPane({
417
565
  {variantEnumeration.combinations.length > 0 ? (
418
566
  <section
419
567
  id="variants"
420
- className="ds-playground-dot-surface mt-8 w-full scroll-mt-20 border-t pt-10 pb-12"
568
+ className="ds-playground-dot-surface mt-8 w-full scroll-mt-20 border-t pt-10 pb-12"
421
569
  >
422
570
  <div className="min-w-0 w-full px-6 lg:px-12">
423
571
  <h2 className="w-fit bg-card text-xl font-semibold tracking-tight text-foreground">
424
572
  All variants
425
573
  </h2>
426
574
  <PlaygroundVariantMatrix
427
- Preview={Preview}
575
+ renderPreview={renderPreview}
428
576
  combinations={variantEnumeration.combinations}
429
577
  finiteAxisKeys={variantEnumeration.finiteAxisKeys}
430
578
  totalCount={variantEnumeration.totalCount}
431
579
  capped={variantEnumeration.capped}
580
+ onVariantA11yScan={handleVariantA11yScan}
432
581
  />
433
582
  </div>
434
583
  </section>
@@ -12,7 +12,7 @@ import {
12
12
  import type { HashRoute } from "../shell/hashRoute";
13
13
 
14
14
  type Props = {
15
- catalogNames: string[];
15
+ catalogEntries: { name: string; label: string }[];
16
16
  onNavigate: (next: HashRoute) => void;
17
17
  open: boolean;
18
18
  onOpenChange: (open: boolean) => void;
@@ -25,12 +25,7 @@ function eventTargetIsEditable(target: EventTarget | null): boolean {
25
25
  return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT";
26
26
  }
27
27
 
28
- export function DashboardCommandPalette({
29
- catalogNames,
30
- onNavigate,
31
- open,
32
- onOpenChange,
33
- }: Props) {
28
+ export function DashboardCommandPalette({ catalogEntries, onNavigate, open, onOpenChange }: Props) {
34
29
  const close = useCallback(() => onOpenChange(false), [onOpenChange]);
35
30
 
36
31
  useEffect(() => {
@@ -65,18 +60,18 @@ export function DashboardCommandPalette({
65
60
  Governance
66
61
  </CommandItem>
67
62
  </CommandGroup>
68
- {catalogNames.length > 0 ? (
63
+ {catalogEntries.length > 0 ? (
69
64
  <CommandGroup heading="Components">
70
- {catalogNames.map((name) => (
65
+ {catalogEntries.map(({ name, label }) => (
71
66
  <CommandItem
72
67
  key={name}
73
- value={name}
68
+ value={`${name} ${label}`}
74
69
  onSelect={() => {
75
70
  onNavigate({ view: "component", componentId: name });
76
71
  close();
77
72
  }}
78
73
  >
79
- {name}
74
+ {label}
80
75
  </CommandItem>
81
76
  ))}
82
77
  </CommandGroup>
@@ -14,8 +14,8 @@ type Props = {
14
14
 
15
15
  export function GovernancePane({
16
16
  landing,
17
- reportUrl: _reportUrl = "/dslint-report.json",
18
- dslinterReportHint = "npm run dslint:report",
17
+ reportUrl: _reportUrl = "/dslinter-report.json",
18
+ dslinterReportHint = "npm run dslinter:report",
19
19
  dslinterReport,
20
20
  onOpenComponent,
21
21
  }: Props) {
@@ -0,0 +1,44 @@
1
+ import { useCallback, useState } from "react";
2
+ import { hideCatalogComponent } from "../dashboard/updateDslintConfig";
3
+ import { Button } from "./ui/button";
4
+
5
+ type Props = {
6
+ componentName: string;
7
+ onHidden: (componentName: string) => void;
8
+ };
9
+
10
+ export function HideFromCatalogButton({ componentName, onHidden }: Props) {
11
+ const [pending, setPending] = useState(false);
12
+
13
+ const handleClick = useCallback(async () => {
14
+ if (
15
+ !window.confirm(
16
+ `Hide ${componentName} from the component catalog? This updates hidden_components in .dslinter.json.`,
17
+ )
18
+ ) {
19
+ return;
20
+ }
21
+ setPending(true);
22
+ try {
23
+ await hideCatalogComponent(componentName);
24
+ onHidden(componentName);
25
+ } catch (err) {
26
+ const message = err instanceof Error ? err.message : String(err);
27
+ window.alert(`Could not hide ${componentName}: ${message}`);
28
+ } finally {
29
+ setPending(false);
30
+ }
31
+ }, [componentName, onHidden]);
32
+
33
+ return (
34
+ <Button
35
+ type="button"
36
+ size="sm"
37
+ variant="outline"
38
+ disabled={pending}
39
+ onClick={() => void handleClick()}
40
+ >
41
+ {pending ? "Hiding…" : "Hide Component"}
42
+ </Button>
43
+ );
44
+ }
@@ -0,0 +1,36 @@
1
+ import { useCallback, useState } from "react";
2
+ import { openSourceFile } from "../dashboard/editorLink";
3
+ import { Button } from "./ui/button";
4
+
5
+ type Props = {
6
+ filePath: string;
7
+ line?: number;
8
+ };
9
+
10
+ export function OpenInEditorButton({ filePath, line }: Props) {
11
+ const [pending, setPending] = useState(false);
12
+
13
+ const handleClick = useCallback(async () => {
14
+ setPending(true);
15
+ try {
16
+ await openSourceFile(filePath, line);
17
+ } catch (err) {
18
+ const message = err instanceof Error ? err.message : String(err);
19
+ window.alert(`Could not open file: ${message}`);
20
+ } finally {
21
+ setPending(false);
22
+ }
23
+ }, [filePath, line]);
24
+
25
+ return (
26
+ <Button
27
+ type="button"
28
+ size="sm"
29
+ variant="outline"
30
+ disabled={pending}
31
+ onClick={() => void handleClick()}
32
+ >
33
+ {pending ? "Opening…" : "Open in Editor"}
34
+ </Button>
35
+ );
36
+ }