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
@@ -1,7 +1,12 @@
1
1
  import { useCallback } from "react";
2
- import type { PlaygroundArgs, PlaygroundControl } from "../types/controls";
2
+ import type {
3
+ PlaygroundArgs,
4
+ PlaygroundControl,
5
+ PlaygroundValuesUpdater,
6
+ } from "../types/controls";
3
7
  import type { PlaygroundEntry } from "../types/playground";
4
8
  import type { A11yModuleSummary } from "../report/a11yForModule";
9
+ import type { PlaygroundA11yFinding } from "../playground/scanVariantA11y";
5
10
  import type { CodeScoreModuleSummary } from "../report/codeScoreForModule";
6
11
  import type { LintFinding, UsageSummary } from "../types/report";
7
12
  import { Badge } from "./ui/badge";
@@ -27,16 +32,10 @@ type UsageProps = {
27
32
  };
28
33
 
29
34
  export function PlaygroundUsageSection({ entry, values }: UsageProps) {
30
- const usage =
31
- entry.usageSnippet?.(values) ??
32
- `// Pass usageSnippet on this PlaygroundEntry, or derive snippets from dslint controls.\n<${entry.id} />`;
35
+ const usage = entry.usageSnippet?.(values) ?? `<${entry.id} />`;
33
36
 
34
37
  return (
35
- <Section
36
- id="usage"
37
- title="Usage"
38
- description="Example usage for the current playground values."
39
- >
38
+ <Section id="usage" title="Usage">
40
39
  <PlaygroundUsageCode source={usage} />
41
40
  </Section>
42
41
  );
@@ -90,7 +89,7 @@ export function PlaygroundTokenStyleSection({
90
89
  ) : (
91
90
  <EmptyCard>
92
91
  Token findings update when{" "}
93
- <span className="font-mono">dslint-report.json</span> is available
92
+ <span className="font-mono">dslinter-report.json</span> is available
94
93
  (same fetch as Governance).
95
94
  </EmptyCard>
96
95
  )}
@@ -143,7 +142,7 @@ export function PlaygroundCodeScoreSection({
143
142
  ) : (
144
143
  <EmptyCard>
145
144
  Code score updates when{" "}
146
- <span className="font-mono">dslint-report.json</span> is available
145
+ <span className="font-mono">dslinter-report.json</span> is available
147
146
  (same fetch as Governance).
148
147
  </EmptyCard>
149
148
  )}
@@ -152,12 +151,22 @@ export function PlaygroundCodeScoreSection({
152
151
  }
153
152
 
154
153
  type A11yProps = {
155
- a11y: A11yModuleSummary;
154
+ a11y: Omit<A11yModuleSummary, "findings"> & {
155
+ findings: PlaygroundA11yFinding[];
156
+ };
156
157
  reportReady: boolean;
158
+ variantScanPending?: boolean;
157
159
  };
158
160
 
159
- export function PlaygroundA11ySection({ a11y, reportReady }: A11yProps) {
161
+ export function PlaygroundA11ySection({
162
+ a11y,
163
+ reportReady,
164
+ variantScanPending = false,
165
+ }: A11yProps) {
160
166
  const hasFindingRows = reportReady && a11y.findings.length > 0;
167
+ const showVariantColumn = a11y.findings.some(
168
+ (f) => f.variant_label != null && f.variant_label !== "",
169
+ );
161
170
 
162
171
  return (
163
172
  <>
@@ -165,6 +174,7 @@ export function PlaygroundA11ySection({ a11y, reportReady }: A11yProps) {
165
174
  <Table>
166
175
  <TableHeader>
167
176
  <TableRow>
177
+ {showVariantColumn ? <TableHead>Variant</TableHead> : null}
168
178
  <TableHead>Rule</TableHead>
169
179
  <TableHead>Line</TableHead>
170
180
  <TableHead>Severity</TableHead>
@@ -173,7 +183,14 @@ export function PlaygroundA11ySection({ a11y, reportReady }: A11yProps) {
173
183
  </TableHeader>
174
184
  <TableBody>
175
185
  {a11y.findings.map((f, i) => (
176
- <TableRow key={`${f.rule_id}-${f.line ?? "x"}-${i}`}>
186
+ <TableRow
187
+ key={`${f.rule_id}-${f.line ?? "x"}-${f.variant_label ?? ""}-${i}`}
188
+ >
189
+ {showVariantColumn ? (
190
+ <TableCell className="font-mono text-xs text-muted-foreground">
191
+ {f.variant_label ?? "—"}
192
+ </TableCell>
193
+ ) : null}
177
194
  <TableCell>{f.rule_id}</TableCell>
178
195
  <TableCell>{f.line ?? "—"}</TableCell>
179
196
  <TableCell>{f.severity}</TableCell>
@@ -184,12 +201,14 @@ export function PlaygroundA11ySection({ a11y, reportReady }: A11yProps) {
184
201
  </Table>
185
202
  ) : reportReady && a11y.issueCount === 0 ? (
186
203
  <EmptyCard>
187
- No accessibility findings on this file in the current report.
204
+ {variantScanPending
205
+ ? "Scanning variant previews for color contrast…"
206
+ : "No accessibility findings on this file or its variant previews in the current report."}
188
207
  </EmptyCard>
189
208
  ) : (
190
209
  <EmptyCard>
191
210
  A11y score updates when{" "}
192
- <span className="font-mono">dslint-report.json</span> is available
211
+ <span className="font-mono">dslinter-report.json</span> is available
193
212
  (same fetch as Governance).
194
213
  </EmptyCard>
195
214
  )}
@@ -198,15 +217,16 @@ export function PlaygroundA11ySection({ a11y, reportReady }: A11yProps) {
198
217
  }
199
218
 
200
219
  type ApiProps = {
220
+ entry: PlaygroundEntry;
201
221
  controls: PlaygroundControl[];
202
222
  values: PlaygroundArgs;
203
- onChange: (next: PlaygroundArgs) => void;
223
+ onChange: PlaygroundValuesUpdater;
204
224
  onReset: () => void;
205
225
  /** When set, adds columns for how often each prop appears at scanned JSX call sites. */
206
226
  reportUsage?: UsageSummary;
207
227
  /** Declared prop names from the scan (definitions + playground specs), used for “never passed” hints. */
208
228
  declaredPropsFromScan?: string[];
209
- /** True when `dslint-report.json` is loaded (even if this component has no usage row). */
229
+ /** True when `dslinter-report.json` is loaded (even if this component has no usage row). */
210
230
  governanceReportLoaded?: boolean;
211
231
  };
212
232
 
@@ -237,9 +257,9 @@ export function PlaygroundApiReference({
237
257
 
238
258
  const patch = useCallback(
239
259
  (key: string, value: string | number | boolean) => {
240
- onChange({ ...values, [key]: value });
260
+ onChange((prev) => ({ ...prev, [key]: value }));
241
261
  },
242
- [onChange, values],
262
+ [onChange],
243
263
  );
244
264
 
245
265
  const rows = controlsToApiRows(controls);
@@ -252,12 +272,13 @@ export function PlaygroundApiReference({
252
272
  .filter((k) => !controlKeys.has(k))
253
273
  .sort((a, b) => a.localeCompare(b))
254
274
  : [];
255
-
275
+ const repoUsageProps = showRepo
276
+ ? [...rows.map((r) => r.prop), ...extraRepoProps]
277
+ : [];
256
278
  return (
257
279
  <Section
258
280
  id="api-reference"
259
281
  title="API reference"
260
- description=""
261
282
  actions={
262
283
  <Button type="button" variant="outline" size="sm" onClick={onReset}>
263
284
  Reset defaults
@@ -270,20 +291,12 @@ export function PlaygroundApiReference({
270
291
  <TableHead>Prop</TableHead>
271
292
  <TableHead>Type</TableHead>
272
293
  <TableHead>Value</TableHead>
273
- {showRepo ? (
274
- <>
275
- <TableHead>Usage</TableHead>
276
- <TableHead>Values</TableHead>
277
- </>
278
- ) : null}
279
294
  </TableRow>
280
295
  </TableHeader>
281
296
  <TableBody>
282
297
  {controls.map((c) => {
283
298
  const r = rows.find((row) => row.prop === c.key);
284
299
  if (!r) return null;
285
- const n = showRepo ? (freqs[r.prop] ?? 0) : 0;
286
- const valueChips = formatRepoLiteralChips(valueFreqs[r.prop]);
287
300
  return (
288
301
  <TableRow key={r.prop}>
289
302
  <TableCell className="font-medium">{r.prop}</TableCell>
@@ -322,9 +335,9 @@ export function PlaygroundApiReference({
322
335
  ) : (
323
336
  <span className="font-mono text-xs flex items-center gap-1">
324
337
  {r.type}
325
- {r.default !== "—" ? (
338
+ {r.defaultBadge ? (
326
339
  <Badge variant="secondary" size="sm">
327
- {r.default}
340
+ {r.defaultBadge}
328
341
  </Badge>
329
342
  ) : null}
330
343
  </span>
@@ -339,40 +352,27 @@ export function PlaygroundApiReference({
339
352
  layout="table"
340
353
  />
341
354
  </TableCell>
342
- {showRepo ? (
343
- <>
344
- <TableCell>{n}</TableCell>
345
- <TableCell>{valueChips}</TableCell>
346
- </>
347
- ) : null}
348
355
  </TableRow>
349
356
  );
350
357
  })}
351
358
  </TableBody>
352
359
  </Table>
353
360
 
354
- {showRepo && extraRepoProps.length > 0 ? (
355
- <div className="mt-4">
356
- <h3 className="text-sm font-semibold text-foreground">
357
- Also seen in repo (not in playground)
358
- </h3>
359
- <p className="mt-1 text-xs text-muted-foreground">
360
- These prop names appear in scanned JSX but are not wired as
361
- playground controls on this page.
362
- </p>
361
+ {showRepo ? (
362
+ <Section id="repo-usage" title="Repo usage" className="mt-4">
363
363
  <Table>
364
364
  <TableHeader>
365
365
  <TableRow>
366
366
  <TableHead>Prop</TableHead>
367
- <TableHead>Repo call sites</TableHead>
368
- <TableHead>Repo literals</TableHead>
367
+ <TableHead>Count</TableHead>
368
+ <TableHead>Values</TableHead>
369
369
  </TableRow>
370
370
  </TableHeader>
371
371
  <TableBody>
372
- {extraRepoProps.map((prop) => (
372
+ {repoUsageProps.map((prop) => (
373
373
  <TableRow key={prop}>
374
- <TableCell>{prop}</TableCell>
375
- <TableCell{freqs[prop] ?? 0}</TableCell>
374
+ <TableCell className="font-medium">{prop}</TableCell>
375
+ <TableCell>{freqs[prop] ?? 0}</TableCell>
376
376
  <TableCell>
377
377
  {formatRepoLiteralChips(valueFreqs[prop])}
378
378
  </TableCell>
@@ -380,7 +380,7 @@ export function PlaygroundApiReference({
380
380
  ))}
381
381
  </TableBody>
382
382
  </Table>
383
- </div>
383
+ </Section>
384
384
  ) : null}
385
385
  </Section>
386
386
  );
@@ -0,0 +1,82 @@
1
+ import { forwardRef, useMemo, type CSSProperties, type ReactNode } from "react";
2
+ import type { WorkspaceReport } from "../types/report";
3
+ import {
4
+ buildAppPreviewThemeFromReport,
5
+ cssVariablesForPreviewTheme,
6
+ } from "../playground/appPreviewTheme";
7
+ import { useDashboardTheme } from "../shell/DashboardLayout";
8
+ import { cn } from "../lib/utils";
9
+
10
+ type Props = {
11
+ children: ReactNode;
12
+ workspaceReport: WorkspaceReport | null;
13
+ className?: string;
14
+ };
15
+
16
+ export const PlaygroundAppThemeWrapper = forwardRef<HTMLDivElement, Props>(
17
+ function PlaygroundAppThemeWrapper(
18
+ { children, workspaceReport, className },
19
+ ref,
20
+ ) {
21
+ const { resolvedTheme } = useDashboardTheme();
22
+ const previewTheme = useMemo(
23
+ () => buildAppPreviewThemeFromReport(workspaceReport),
24
+ [workspaceReport],
25
+ );
26
+ const isDark = resolvedTheme === "dark";
27
+ const hasDarkTokens =
28
+ previewTheme != null && Object.keys(previewTheme.dark).length > 0;
29
+
30
+ if (!previewTheme) {
31
+ return (
32
+ <div
33
+ ref={ref}
34
+ className={cn(isDark && "dark", className)}
35
+ data-app-preview-theme={resolvedTheme}
36
+ >
37
+ {children}
38
+ </div>
39
+ );
40
+ }
41
+
42
+ // App CSS has light tokens only — don't inject them in dark mode; inherit
43
+ // dashboard dark tokens from the surrounding [data-dashboard-theme] tree.
44
+ if (isDark && !hasDarkTokens) {
45
+ return (
46
+ <div
47
+ ref={ref}
48
+ className={cn("ds-playground-app-preview", className)}
49
+ data-app-preview-theme={resolvedTheme}
50
+ >
51
+ {children}
52
+ </div>
53
+ );
54
+ }
55
+
56
+ const vars = cssVariablesForPreviewTheme(
57
+ previewTheme,
58
+ isDark ? "dark" : "light",
59
+ );
60
+ const usesDarkTokens = isDark && hasDarkTokens;
61
+
62
+ const style = {
63
+ ...vars,
64
+ colorScheme: usesDarkTokens ? "dark" : "light",
65
+ } as CSSProperties;
66
+
67
+ return (
68
+ <div
69
+ ref={ref}
70
+ className={cn(
71
+ "ds-playground-app-preview",
72
+ isDark && "dark",
73
+ className,
74
+ )}
75
+ style={style}
76
+ data-app-preview-theme={resolvedTheme}
77
+ >
78
+ {children}
79
+ </div>
80
+ );
81
+ },
82
+ );
@@ -1,29 +1,23 @@
1
1
  import { useCallback } from "react";
2
- import type { PlaygroundArgs, PlaygroundControl } from "../types/controls";
2
+ import type { PlaygroundArgs, PlaygroundControl, PlaygroundValuesUpdater } from "../types/controls";
3
3
  import { Button } from "./ui/button";
4
4
  import { PlaygroundControlField } from "./PlaygroundControlField";
5
5
 
6
6
  type Props = {
7
7
  controls: PlaygroundControl[];
8
8
  values: PlaygroundArgs;
9
- onChange: (next: PlaygroundArgs) => void;
9
+ onChange: PlaygroundValuesUpdater;
10
10
  onReset: () => void;
11
11
  /** Omit the outer card wrapper (doc-style pages provide their own section chrome). */
12
12
  bare?: boolean;
13
13
  };
14
14
 
15
- export function PlaygroundControls({
16
- controls,
17
- values,
18
- onChange,
19
- onReset,
20
- bare,
21
- }: Props) {
15
+ export function PlaygroundControls({ controls, values, onChange, onReset, bare }: Props) {
22
16
  const patch = useCallback(
23
17
  (key: string, value: string | number | boolean) => {
24
- onChange({ ...values, [key]: value });
18
+ onChange((prev) => ({ ...prev, [key]: value }));
25
19
  },
26
- [onChange, values],
20
+ [onChange],
27
21
  );
28
22
 
29
23
  if (controls.length === 0) return null;
@@ -0,0 +1,54 @@
1
+ import { Component, type ErrorInfo, type ReactNode } from "react";
2
+
3
+ type Props = {
4
+ children: ReactNode;
5
+ componentName?: string;
6
+ };
7
+
8
+ type State = {
9
+ error: Error | null;
10
+ };
11
+
12
+ export class PlaygroundPreviewErrorBoundary extends Component<Props, State> {
13
+ state: State = { error: null };
14
+
15
+ static getDerivedStateFromError(error: Error): State {
16
+ return { error };
17
+ }
18
+
19
+ componentDidCatch(error: Error, info: ErrorInfo) {
20
+ if (process.env.NODE_ENV === "development") {
21
+ console.warn(
22
+ "[dslinter] preview render failed",
23
+ error,
24
+ info.componentStack,
25
+ );
26
+ }
27
+ }
28
+
29
+ componentDidUpdate(prevProps: Props) {
30
+ if (prevProps.children !== this.props.children && this.state.error) {
31
+ this.setState({ error: null });
32
+ }
33
+ }
34
+
35
+ render() {
36
+ if (this.state.error) {
37
+ const name = this.props.componentName ?? "Component";
38
+ return (
39
+ <div className="rounded-md border border-border bg-muted/40 px-4 py-6 text-sm text-muted-foreground">
40
+ <p className="font-medium text-foreground">
41
+ Preview could not render {name}
42
+ </p>
43
+ <p className="mt-2">
44
+ This component may need Inertia page props, a provider, or other app
45
+ context. The scanner snapshot and governance panels still reflect
46
+ static analysis.
47
+ </p>
48
+ <p className="mt-2 font-mono text-xs">{this.state.error.message}</p>
49
+ </div>
50
+ );
51
+ }
52
+ return this.props.children;
53
+ }
54
+ }
@@ -1,18 +1,20 @@
1
1
  import { useEffect, useRef, useState } from "react";
2
+ import { useDashboardTheme } from "../shell/DashboardLayout";
2
3
  import { renderPlaygroundUsageHtml } from "./playgroundUsageHighlight";
3
4
 
4
5
  const shellClass =
5
- "playground-usage-shiki mt-4 overflow-x-auto rounded-lg border bg-gray-900 p-4 text-sm leading-relaxed shadow-xs " +
6
+ "playground-usage-shiki mt-4 overflow-x-auto rounded-lg border border-border p-4 text-sm leading-relaxed text-foreground " +
6
7
  "[&_.shiki]:!bg-transparent [&_pre.shiki]:!m-0 [&_pre.shiki]:!bg-transparent [&_pre.shiki]:!p-0";
7
8
 
8
9
  const plainPreClass =
9
- "m-0 whitespace-pre font-mono text-sm leading-relaxed text-gray-100";
10
+ "m-0 whitespace-pre font-mono text-sm leading-relaxed text-foreground";
10
11
 
11
12
  type Props = {
12
13
  source: string;
13
14
  };
14
15
 
15
16
  export function PlaygroundUsageCode({ source }: Props) {
17
+ const { theme } = useDashboardTheme();
16
18
  const [html, setHtml] = useState<string | null>(null);
17
19
  const seq = useRef(0);
18
20
 
@@ -23,7 +25,7 @@ export function PlaygroundUsageCode({ source }: Props) {
23
25
 
24
26
  void (async () => {
25
27
  try {
26
- const next = await renderPlaygroundUsageHtml(source, ac.signal);
28
+ const next = await renderPlaygroundUsageHtml(source, theme, ac.signal);
27
29
  if (id !== seq.current) return;
28
30
  setHtml(next);
29
31
  } catch (e) {
@@ -34,7 +36,7 @@ export function PlaygroundUsageCode({ source }: Props) {
34
36
  })();
35
37
 
36
38
  return () => ac.abort();
37
- }, [source]);
39
+ }, [source, theme]);
38
40
 
39
41
  if (html) {
40
42
  return (
@@ -1,14 +1,20 @@
1
+ import { useEffect, useRef } from "react";
2
+ import type { ReactNode } from "react";
1
3
  import type { PlaygroundArgs } from "../types/controls";
2
- import type { PlaygroundPreviewComponent } from "../types/preview";
4
+ import {
5
+ scanVariantPreviews,
6
+ type PlaygroundA11yFinding,
7
+ } from "../playground/scanVariantA11y";
3
8
  import { Badge } from "./ui/badge";
4
9
  import { PLAYGROUND_VARIANT_MATRIX_CAP } from "../playground/enumerateControlCombinations";
5
10
 
6
11
  type Props = {
7
- Preview: PlaygroundPreviewComponent;
12
+ renderPreview: (values: PlaygroundArgs) => ReactNode;
8
13
  combinations: PlaygroundArgs[];
9
14
  finiteAxisKeys: string[];
10
15
  totalCount: number;
11
16
  capped: boolean;
17
+ onVariantA11yScan?: (findings: PlaygroundA11yFinding[]) => void;
12
18
  };
13
19
 
14
20
  function formatValue(value: string | number | boolean): string {
@@ -16,52 +22,113 @@ function formatValue(value: string | number | boolean): string {
16
22
  return JSON.stringify(value);
17
23
  }
18
24
 
25
+ function variantKey(combo: PlaygroundArgs, axisKeys: string[]): string {
26
+ return axisKeys.map((k) => `${k}:${formatValue(combo[k] ?? "")}`).join("|");
27
+ }
28
+
19
29
  export function PlaygroundVariantMatrix({
20
- Preview,
30
+ renderPreview,
21
31
  combinations,
22
32
  finiteAxisKeys,
23
33
  totalCount,
24
34
  capped,
35
+ onVariantA11yScan,
25
36
  }: Props) {
26
- if (combinations.length === 0) return null;
37
+ const previewRefs = useRef(new Map<string, HTMLDivElement>());
38
+
39
+ const visibleCombinations = combinations.filter(
40
+ (combo) => combo.asChild !== true,
41
+ );
42
+ const visibleAxisKeys = finiteAxisKeys.filter((key) => key !== "asChild");
43
+ const skipsAsChildAxis = finiteAxisKeys.includes("asChild");
44
+ const adjustedTotalCount = skipsAsChildAxis
45
+ ? Math.ceil(totalCount / 2)
46
+ : totalCount;
47
+
48
+ useEffect(() => {
49
+ if (!onVariantA11yScan || visibleCombinations.length === 0) return;
50
+
51
+ let cancelled = false;
52
+ const frameId = requestAnimationFrame(() => {
53
+ void (async () => {
54
+ await new Promise<void>((resolve) => {
55
+ requestAnimationFrame(() => resolve());
56
+ });
57
+ if (cancelled) return;
58
+
59
+ const targets = visibleCombinations.flatMap((combo) => {
60
+ const key = variantKey(combo, visibleAxisKeys);
61
+ const element = previewRefs.current.get(key);
62
+ if (!element) return [];
63
+ return [{ element, combo, axisKeys: visibleAxisKeys }];
64
+ });
65
+
66
+ if (targets.length === 0) {
67
+ if (!cancelled) onVariantA11yScan([]);
68
+ return;
69
+ }
70
+
71
+ try {
72
+ const findings = await scanVariantPreviews(targets);
73
+ if (!cancelled) onVariantA11yScan(findings);
74
+ } catch {
75
+ if (!cancelled) onVariantA11yScan([]);
76
+ }
77
+ })();
78
+ });
79
+
80
+ return () => {
81
+ cancelled = true;
82
+ cancelAnimationFrame(frameId);
83
+ };
84
+ }, [onVariantA11yScan, visibleCombinations, visibleAxisKeys]);
85
+
86
+ if (visibleCombinations.length === 0) return null;
27
87
 
28
88
  return (
29
89
  <>
30
90
  {capped ? (
31
- <p className="rounded-md border border-border bg-muted/60 px-3 py-2 text-sm text-muted-foreground">
32
- Showing {combinations.length} of {totalCount} combinations (limit{" "}
33
- {PLAYGROUND_VARIANT_MATRIX_CAP}). Reduce select options or split
34
- controls to preview more here.
91
+ <p className="rounded-md border border-border px-3 py-2 text-sm text-muted-foreground">
92
+ Showing {visibleCombinations.length} of {adjustedTotalCount}{" "}
93
+ combinations (limit {PLAYGROUND_VARIANT_MATRIX_CAP}). Reduce select
94
+ options or split controls to preview more here.
35
95
  </p>
36
96
  ) : null}
37
97
  <div className="mt-4 flex flex-col gap-4">
38
- {combinations.map((combo) => (
39
- <div
40
- key={finiteAxisKeys
41
- .map((k) => `${k}:${formatValue(combo[k] ?? "")}`)
42
- .join("|")}
43
- className="flex min-w-0 flex-col overflow-hidden rounded-lg border border-border bg-card text-card-foreground shadow-xs"
44
- >
45
- <div className="flex flex-wrap gap-1 border-b p-2">
46
- {finiteAxisKeys.map((k) => {
47
- const v = combo[k];
48
- return (
49
- <Badge
50
- key={k}
51
- variant="outline"
52
- size="sm"
53
- className="font-mono text-xs"
54
- >
55
- {k}={v === undefined ? "?" : formatValue(v)}
56
- </Badge>
57
- );
58
- })}
59
- </div>
60
- <div className="min-w-0 bg-card p-3">
61
- <Preview values={combo} />
98
+ {visibleCombinations.map((combo) => {
99
+ const key = variantKey(combo, visibleAxisKeys);
100
+ return (
101
+ <div
102
+ key={key}
103
+ className="flex min-w-0 flex-col overflow-hidden rounded-lg border border-border bg-card text-card-foreground shadow-xs"
104
+ >
105
+ <div className="flex flex-wrap gap-1 border-b p-2">
106
+ {visibleAxisKeys.map((k) => {
107
+ const v = combo[k];
108
+ return (
109
+ <Badge
110
+ key={k}
111
+ variant="outline"
112
+ size="sm"
113
+ className="font-mono text-xs"
114
+ >
115
+ {k}={v === undefined ? "?" : formatValue(v)}
116
+ </Badge>
117
+ );
118
+ })}
119
+ </div>
120
+ <div
121
+ ref={(el) => {
122
+ if (el) previewRefs.current.set(key, el);
123
+ else previewRefs.current.delete(key);
124
+ }}
125
+ className="min-w-0 p-3"
126
+ >
127
+ {renderPreview(combo)}
128
+ </div>
62
129
  </div>
63
- </div>
64
- ))}
130
+ );
131
+ })}
65
132
  </div>
66
133
  </>
67
134
  );
@@ -1,20 +1,23 @@
1
1
  import type { ReactNode } from "react";
2
+ import { cn } from "../lib/utils";
2
3
 
3
4
  export function Section({
4
5
  id,
6
+ className,
5
7
  children,
6
8
  title,
7
9
  description,
8
10
  actions,
9
11
  }: {
10
12
  id: string;
13
+ className?: string;
11
14
  children: ReactNode;
12
15
  title: string;
13
- description: string;
16
+ description?: string;
14
17
  actions?: ReactNode;
15
18
  }) {
16
19
  return (
17
- <section id={id} className="scroll-mt-20">
20
+ <section id={id} className={cn("scroll-mt-20", className)}>
18
21
  <div className="flex items-center justify-between gap-2">
19
22
  <div>
20
23
  <h2 className="text-lg/none font-semibold tracking-tight text-foreground">