dslinter 0.1.5 → 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 +112 -0
  2. package/README.md +54 -27
  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 +92 -24
  19. package/bin/lib/project-root.test.mjs +52 -0
  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 +163 -0
  25. package/bin/lib/scaffold-config.test.mjs +43 -0
  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 +56 -13
  32. package/bin/modes/init.mjs +35 -47
  33. package/bin/modes/init.test.mjs +16 -0
  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-BPPtPsYh.css +0 -1
  177. package/dashboard-dist/assets/DashboardLayoutAuto-Dp3bAQxH.js +0 -1
  178. package/dashboard-dist/assets/index-DsjwnDdX.js +0 -206
  179. package/dashboard-dist/assets/index-jaCmZJlW.css +0 -1
  180. package/src/components/playgroundUsageTwoslash.ts +0 -69
  181. package/templates/vite.dslint-scan-alias.snippet.ts +0 -4
@@ -1,11 +1,11 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
  import { embedGlobKeyFromRelPath } from "../../vite/collectScanModules";
3
+ import type { WorkspaceReport } from "../types/report";
3
4
  import { buildPlaygroundEntriesFromReportWithSkips } from "./buildPlaygroundEntriesFromReport";
4
5
 
5
6
  describe("autoPlayground join (embed glob keys)", () => {
6
7
  it("joins report playgrounds to virtual module keys", () => {
7
- const relPath =
8
- "resources/js/Components/Billing/AdditionalEventLimitModal.tsx";
8
+ const relPath = "resources/js/Components/Billing/AdditionalEventLimitModal.tsx";
9
9
  const globKey = embedGlobKeyFromRelPath(relPath);
10
10
  const modules = {
11
11
  [globKey]: {
@@ -14,21 +14,31 @@ describe("autoPlayground join (embed glob keys)", () => {
14
14
  },
15
15
  },
16
16
  };
17
- const report = {
17
+ const report: WorkspaceReport = {
18
+ root: "/repo",
19
+ files: [],
20
+ findings: [],
21
+ duplicate_components: [],
22
+ usage_by_component: [],
23
+ scores: {
24
+ design_system_health: 0,
25
+ ux_consistency: 0,
26
+ accessibility: 0,
27
+ maintainability: 0,
28
+ },
18
29
  playgrounds: [
19
30
  {
20
31
  id: "AdditionalEventLimitModal",
21
32
  export_name: "AdditionalEventLimitModal",
22
33
  rel_path: relPath,
34
+ declared_props: [],
23
35
  },
24
36
  ],
25
37
  };
26
38
 
27
- const { entries, skipped } = buildPlaygroundEntriesFromReportWithSkips(
28
- report,
29
- modules,
30
- { logJoinSkips: false },
31
- );
39
+ const { entries, skipped } = buildPlaygroundEntriesFromReportWithSkips(report, modules, {
40
+ logJoinSkips: false,
41
+ });
32
42
 
33
43
  expect(skipped).toHaveLength(0);
34
44
  expect(entries).toHaveLength(1);
@@ -2,6 +2,7 @@ import { useMemo } from "react";
2
2
  import type { WorkspaceReport } from "../types/report";
3
3
  import { buildPlaygroundEntriesFromReportWithSkips } from "./buildPlaygroundEntriesFromReport";
4
4
  import type { BuildPlaygroundResult } from "./buildPlaygroundEntriesFromReport";
5
+ import { playgroundSpecsKey } from "./playgroundSpecsKey";
5
6
  import { scanPlaygroundModules } from "./scanPlaygroundModules";
6
7
 
7
8
  /**
@@ -12,12 +13,13 @@ import { scanPlaygroundModules } from "./scanPlaygroundModules";
12
13
  export function usePlaygroundFromReport(
13
14
  report: WorkspaceReport | null | undefined,
14
15
  ): BuildPlaygroundResult {
16
+ const specsKey = playgroundSpecsKey(report);
15
17
  return useMemo(
16
18
  () =>
17
19
  buildPlaygroundEntriesFromReportWithSkips(
18
20
  report,
19
21
  scanPlaygroundModules,
20
22
  ),
21
- [report],
23
+ [specsKey],
22
24
  );
23
25
  }
@@ -1,4 +1,5 @@
1
1
  import type { LintFinding, WorkspaceReport } from "../types/report";
2
+ import { a11yScoreFromFindings } from "./a11yScoring";
2
3
  import { pathsMatch, resolveModuleSourcePath } from "./modulePathMatch";
3
4
 
4
5
  export type A11yModuleSummary = {
@@ -23,13 +24,7 @@ export function a11ySummaryForModule(
23
24
  const all = report.findings.filter((f) => f.rule_id.startsWith("a11y-"));
24
25
  const findings = all.filter((f) => pathsMatch(f.path, target));
25
26
 
26
- let penalty = 0;
27
- for (const f of findings) {
28
- if (f.severity === "error") penalty += 25;
29
- else if (f.severity === "warning") penalty += 10;
30
- else penalty += 3;
31
- }
32
- const score = Math.max(0, Math.min(100, Math.round(100 - penalty)));
27
+ const score = a11yScoreFromFindings(findings);
33
28
 
34
29
  return { score, issueCount: findings.length, findings };
35
30
  }
@@ -0,0 +1,24 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { a11yScoreFromFindings } from "./a11yScoring";
3
+
4
+ describe("a11yScoreFromFindings", () => {
5
+ it("starts at 100 with no findings", () => {
6
+ expect(a11yScoreFromFindings([])).toBe(100);
7
+ });
8
+
9
+ it("applies severity penalties", () => {
10
+ expect(
11
+ a11yScoreFromFindings([
12
+ { severity: "error" },
13
+ { severity: "warning" },
14
+ { severity: "info" },
15
+ ]),
16
+ ).toBe(62);
17
+ });
18
+
19
+ it("never drops below zero", () => {
20
+ expect(
21
+ a11yScoreFromFindings(Array.from({ length: 10 }, () => ({ severity: "error" as const }))),
22
+ ).toBe(0);
23
+ });
24
+ });
@@ -0,0 +1,17 @@
1
+ import type { Severity } from "../types/report";
2
+
3
+ export function a11yScoreFromSeverities(severities: Severity[]): number {
4
+ let penalty = 0;
5
+ for (const severity of severities) {
6
+ if (severity === "error") penalty += 25;
7
+ else if (severity === "warning") penalty += 10;
8
+ else penalty += 3;
9
+ }
10
+ return Math.max(0, Math.min(100, Math.round(100 - penalty)));
11
+ }
12
+
13
+ export function a11yScoreFromFindings(
14
+ findings: ReadonlyArray<{ severity: Severity }>,
15
+ ): number {
16
+ return a11yScoreFromSeverities(findings.map((f) => f.severity));
17
+ }
@@ -0,0 +1,6 @@
1
+ export type { A11yModuleSummary } from "./a11yForModule";
2
+ export { a11ySummaryForModule, resolveModuleSourcePath } from "./a11yForModule";
3
+ export type { CodeScoreModuleSummary } from "./codeScoreForModule";
4
+ export { codeScoreSummaryForModule } from "./codeScoreForModule";
5
+ export { tokenStyleFindingsForModule } from "./tokenStyleFindingsForModule";
6
+ export { findingsForComponent } from "./findingsForComponent";
@@ -12,15 +12,17 @@ import {
12
12
  import type { PlaygroundEntry } from "../types/playground";
13
13
  import type { TokenCatalog } from "../types/tokenCatalog";
14
14
  import type { DslinterReportState } from "../dashboard/useWorkspaceReport";
15
- import { Button } from "../components/ui/button";
16
- import { cn } from "../lib/utils";
17
15
  import { ComponentInspectPane } from "../components/ComponentInspectPane";
18
16
  import { ComponentPlaygroundPane } from "../components/ComponentPlaygroundPane";
19
17
  import { GovernancePane } from "../components/GovernancePane";
20
18
  import { Sidebar } from "../components/Sidebar";
21
19
  import { TokensPane } from "../components/TokensPane";
22
20
  import { DashboardCommandPalette } from "../components/DashboardCommandPalette";
23
- import { componentCatalogNamesFromReport } from "../dashboard/aggregate";
21
+ import {
22
+ componentCatalogNamesFromReport,
23
+ componentCatalogTreeFromReport,
24
+ } from "../dashboard/aggregate";
25
+ import { reportWithExtraHidden } from "../dashboard/catalogVisibility";
24
26
  import { resolvePlaygroundEntry } from "../playground/buildPlaygroundEntriesFromReport";
25
27
  import {
26
28
  findPlaygroundJoinSkip,
@@ -48,13 +50,10 @@ function readStored(): DashboardThemePreference | null {
48
50
  }
49
51
  }
50
52
  if (v === "light" || v === "dark") return v;
51
- /** Migrate legacy `system` (and any unknown) to an explicit mode. */
52
- if (v === "system") {
53
- const next = window.matchMedia("(prefers-color-scheme: dark)").matches
54
- ? "dark"
55
- : "light";
56
- localStorage.setItem(STORAGE_KEY, next);
57
- return next;
53
+ /** Migrate legacy `system` (and any unknown) OS preference is ignored. */
54
+ if (v != null) {
55
+ localStorage.setItem(STORAGE_KEY, "light");
56
+ return "light";
58
57
  }
59
58
  } catch {
60
59
  /* ignore */
@@ -139,14 +138,12 @@ export type DashboardLayoutProps = {
139
138
  /** Join failures from `buildPlaygroundEntriesFromReportWithSkips` — powers inspect-pane hints. */
140
139
  playgroundJoinSkips?: PlaygroundJoinSkip[];
141
140
  tokenCatalog?: TokenCatalog;
142
- /** Custom intro shown above the governance inventory on `#!/governance`; defaults to package copy. */
141
+ /** Custom intro shown above the governance inventory on `/governance`; defaults to package copy. */
143
142
  overview?: ReactNode;
144
143
  /** Fetch URL for `dslint --json` output. */
145
144
  reportUrl?: string;
146
145
  /** Shown next to the governance refresh hint. */
147
146
  dslinterReportHint?: string;
148
- /** Maps Vite `import.meta.glob` path to a label in the component header. */
149
- formatModulePath?: (modulePath: string) => string;
150
147
  /** Workspace report fetch state (shared by governance + component a11y). */
151
148
  dslinterReport: DslinterReportState;
152
149
  };
@@ -166,16 +163,48 @@ export function DashboardLayoutInner({
166
163
  overview,
167
164
  reportUrl,
168
165
  dslinterReportHint,
169
- formatModulePath,
170
166
  dslinterReport,
171
167
  }: DashboardLayoutInnerProps) {
172
168
  const [route, navigate] = useHashRoute();
173
169
  const [commandPaletteOpen, setCommandPaletteOpen] = useState(false);
170
+ const [optimisticHidden, setOptimisticHidden] = useState<string[]>([]);
174
171
  const { theme, setTheme, resolvedTheme } = useDashboardTheme();
175
172
 
173
+ const catalogReport = useMemo(
174
+ () => reportWithExtraHidden(dslinterReport.report, optimisticHidden),
175
+ [dslinterReport.report, optimisticHidden],
176
+ );
177
+
176
178
  const catalogNames = useMemo(
177
- () => componentCatalogNamesFromReport(dslinterReport.report),
178
- [dslinterReport.report],
179
+ () => componentCatalogNamesFromReport(catalogReport),
180
+ [catalogReport],
181
+ );
182
+ const catalogEntries = useMemo(() => {
183
+ const tree = componentCatalogTreeFromReport(catalogReport);
184
+ return tree.flatMap((item) => {
185
+ if (item.type === "component") {
186
+ return [{ name: item.name, label: item.name }];
187
+ }
188
+ return [
189
+ { name: item.parent, label: item.parent },
190
+ ...item.children.map((child) => ({
191
+ name: child,
192
+ label: `${item.parent} / ${child}`,
193
+ })),
194
+ ];
195
+ });
196
+ }, [catalogReport]);
197
+
198
+ const handleHideFromCatalog = useCallback(
199
+ (componentName: string) => {
200
+ setOptimisticHidden((prev) =>
201
+ prev.includes(componentName) ? prev : [...prev, componentName],
202
+ );
203
+ if (route.view === "component" && route.componentId === componentName) {
204
+ navigate({ view: "governance" });
205
+ }
206
+ },
207
+ [route, navigate],
179
208
  );
180
209
 
181
210
  const reportReady =
@@ -216,9 +245,12 @@ export function DashboardLayoutInner({
216
245
  main = (
217
246
  <ComponentPlaygroundPane
218
247
  entry={entry}
219
- formatModulePath={formatModulePath}
220
248
  workspaceReport={dslinterReport.report}
221
249
  reportReady={reportReady}
250
+ onOpenComponent={(name) =>
251
+ navigate({ view: "component", componentId: name })
252
+ }
253
+ onHideFromCatalog={handleHideFromCatalog}
222
254
  />
223
255
  );
224
256
  } else if (inCatalog) {
@@ -233,23 +265,22 @@ export function DashboardLayoutInner({
233
265
  componentId,
234
266
  )}
235
267
  onBackToGovernance={() => navigate({ view: "governance" })}
268
+ onOpenComponent={(name) =>
269
+ navigate({ view: "component", componentId: name })
270
+ }
271
+ onHideFromCatalog={handleHideFromCatalog}
236
272
  />
237
273
  );
238
274
  } else {
239
275
  main = (
240
276
  <div className="flex min-h-0 flex-1 flex-col items-center justify-center gap-3 bg-muted/40 px-8 text-center">
241
- <p className="text-sm font-medium text-foreground">Unknown component</p>
277
+ <p className="text-sm font-medium text-foreground">
278
+ Unknown component
279
+ </p>
242
280
  <p className="max-w-md text-xs text-muted-foreground">
243
281
  <span className="font-mono">{componentId}</span> is not in the
244
282
  latest scan catalog.
245
283
  </p>
246
- <Button
247
- type="button"
248
- size="sm"
249
- onClick={() => navigate({ view: "governance" })}
250
- >
251
- Back to governance
252
- </Button>
253
284
  </div>
254
285
  );
255
286
  }
@@ -257,19 +288,17 @@ export function DashboardLayoutInner({
257
288
 
258
289
  return (
259
290
  <div
260
- className={cn(
261
- "flex h-screen min-h-0 bg-background text-foreground",
262
- resolvedTheme === "dark" && "dark",
263
- )}
291
+ data-dashboard-theme={resolvedTheme}
292
+ className="flex h-screen min-h-0 bg-background text-foreground"
264
293
  >
265
294
  <DashboardCommandPalette
266
- catalogNames={catalogNames}
295
+ catalogEntries={catalogEntries}
267
296
  onNavigate={navigate}
268
297
  open={commandPaletteOpen}
269
298
  onOpenChange={setCommandPaletteOpen}
270
299
  />
271
300
  <Sidebar
272
- report={dslinterReport.report}
301
+ report={catalogReport ?? null}
273
302
  reportLoading={dslinterReport.loading}
274
303
  reportError={dslinterReport.error}
275
304
  route={route}
@@ -277,6 +306,7 @@ export function DashboardLayoutInner({
277
306
  onOpenCommandPalette={() => setCommandPaletteOpen(true)}
278
307
  theme={theme}
279
308
  onThemeChange={setTheme}
309
+ catalogNames={catalogNames}
280
310
  />
281
311
  <div className="ml-[240px] flex min-h-0 min-w-0 flex-1 flex-col">
282
312
  {main}
@@ -286,23 +316,19 @@ export function DashboardLayoutInner({
286
316
  }
287
317
 
288
318
  export function DashboardLayout(props: DashboardLayoutProps) {
289
- if (props.autoPlayground) {
290
- return (
291
- <DashboardThemeProvider>
319
+ return (
320
+ <DashboardThemeProvider>
321
+ {props.autoPlayground ? (
292
322
  <Suspense fallback={null}>
293
323
  <DashboardLayoutAuto {...props} />
294
324
  </Suspense>
295
- </DashboardThemeProvider>
296
- );
297
- }
298
-
299
- return (
300
- <DashboardThemeProvider>
301
- <DashboardLayoutInner
302
- {...props}
303
- playgroundEntries={props.playgroundEntries ?? []}
304
- playgroundJoinSkips={props.playgroundJoinSkips}
305
- />
325
+ ) : (
326
+ <DashboardLayoutInner
327
+ {...props}
328
+ playgroundEntries={props.playgroundEntries ?? []}
329
+ playgroundJoinSkips={props.playgroundJoinSkips}
330
+ />
331
+ )}
306
332
  </DashboardThemeProvider>
307
333
  );
308
334
  }
@@ -11,10 +11,6 @@ export default function DashboardLayoutAuto(props: DashboardLayoutProps) {
11
11
  {...props}
12
12
  playgroundEntries={autoPlaygroundBuild.entries}
13
13
  playgroundJoinSkips={autoPlaygroundBuild.skipped}
14
- formatModulePath={
15
- props.formatModulePath ??
16
- ((modulePath: string) => modulePath.replace(/^@dslint-scan\//, ""))
17
- }
18
14
  />
19
15
  );
20
16
  }
@@ -3,26 +3,20 @@ import { formatHashRoute, parseHashRoute } from "./hashRoute";
3
3
 
4
4
  describe("parseHashRoute", () => {
5
5
  it("parses governance overview", () => {
6
- expect(parseHashRoute("#!/governance")).toEqual({ view: "governance" });
6
+ expect(parseHashRoute("/governance")).toEqual({ view: "governance" });
7
+ expect(parseHashRoute("/")).toEqual({ view: "governance" });
7
8
  expect(parseHashRoute("")).toEqual({ view: "governance" });
8
9
  });
9
10
 
10
- it("parses legacy governance catalog deep link as component route", () => {
11
- expect(parseHashRoute("#!/governance/ActionItem")).toEqual({
12
- view: "component",
13
- componentId: "ActionItem",
14
- });
15
- });
16
-
17
11
  it("parses component route", () => {
18
- expect(parseHashRoute("#!/component/ActionItem")).toEqual({
12
+ expect(parseHashRoute("/component/ActionItem")).toEqual({
19
13
  view: "component",
20
14
  componentId: "ActionItem",
21
15
  });
22
16
  });
23
17
 
24
18
  it("parses tokens", () => {
25
- expect(parseHashRoute("#!/tokens")).toEqual({ view: "tokens" });
19
+ expect(parseHashRoute("/tokens")).toEqual({ view: "tokens" });
26
20
  });
27
21
  });
28
22
 
@@ -30,12 +24,10 @@ describe("formatHashRoute", () => {
30
24
  it("formats component route", () => {
31
25
  expect(
32
26
  formatHashRoute({ view: "component", componentId: "ActionItem" }),
33
- ).toBe("#!/component/ActionItem");
27
+ ).toBe("/component/ActionItem");
34
28
  });
35
29
 
36
- it("formats governance catalog", () => {
37
- expect(
38
- formatHashRoute({ view: "governance", catalog: "ActionItem" }),
39
- ).toBe("#!/governance/ActionItem");
30
+ it("formats governance", () => {
31
+ expect(formatHashRoute({ view: "governance" })).toBe("/governance");
40
32
  });
41
33
  });
@@ -1,40 +1,34 @@
1
1
  export type HashRoute =
2
2
  | { view: "tokens" }
3
- | { view: "governance"; catalog?: string }
3
+ | { view: "governance" }
4
4
  | { view: "component"; componentId: string };
5
5
 
6
- const PREFIX = "#!/";
7
-
8
- function stripShebang(hash: string): string {
9
- if (hash.startsWith(PREFIX)) {
10
- return hash.slice(PREFIX.length);
6
+ function trimTrailingSlashes(path: string): string {
7
+ let end = path.length;
8
+ while (end > 1 && path[end - 1] === "/") {
9
+ end--;
11
10
  }
12
- if (hash.startsWith("#")) {
13
- return hash.slice(1);
11
+ return path.slice(0, end);
12
+ }
13
+
14
+ function normalizePath(input: string): string {
15
+ const raw = input.trim();
16
+ if (raw.startsWith("/")) {
17
+ return trimTrailingSlashes(raw);
14
18
  }
15
- return hash;
19
+ return raw.length > 0 ? `/${raw}` : "/";
16
20
  }
17
21
 
18
- export function parseHashRoute(hash: string): HashRoute {
19
- const raw = stripShebang(hash).trim();
20
- if (raw === "" || raw === "overview") {
22
+ export function parseHashRoute(pathname: string): HashRoute {
23
+ const path = normalizePath(pathname);
24
+ if (path === "/" || path === "/overview" || path === "/governance") {
21
25
  return { view: "governance" };
22
26
  }
23
- if (raw === "tokens") {
27
+ if (path === "/tokens") {
24
28
  return { view: "tokens" };
25
29
  }
26
- if (raw === "governance") {
27
- return { view: "governance" };
28
- }
29
- if (raw.startsWith("governance/")) {
30
- const catalog = decodeURIComponent(raw.slice("governance/".length));
31
- if (catalog.length > 0) {
32
- /** Legacy deep links — component pages replaced governance catalog scroll. */
33
- return { view: "component", componentId: catalog };
34
- }
35
- }
36
- if (raw.startsWith("component/")) {
37
- const componentId = decodeURIComponent(raw.slice("component/".length));
30
+ if (path.startsWith("/component/")) {
31
+ const componentId = decodeURIComponent(path.slice("/component/".length));
38
32
  if (componentId.length > 0) {
39
33
  return { view: "component", componentId };
40
34
  }
@@ -45,14 +39,20 @@ export function parseHashRoute(hash: string): HashRoute {
45
39
  export function formatHashRoute(route: HashRoute): string {
46
40
  switch (route.view) {
47
41
  case "tokens":
48
- return `${PREFIX}tokens`;
42
+ return "/tokens";
49
43
  case "governance":
50
- return route.catalog
51
- ? `${PREFIX}governance/${encodeURIComponent(route.catalog)}`
52
- : `${PREFIX}governance`;
44
+ return "/governance";
53
45
  case "component":
54
- return `${PREFIX}component/${encodeURIComponent(route.componentId)}`;
46
+ return `/component/${encodeURIComponent(route.componentId)}`;
55
47
  default:
56
- return `${PREFIX}governance`;
48
+ return "/governance";
57
49
  }
58
50
  }
51
+
52
+ /** Drop stale `#!/…` fragments left over from hash routing. */
53
+ export function stripLegacyHashFragment(): boolean {
54
+ if (!window.location.hash.startsWith("#!/")) return false;
55
+ const clean = window.location.pathname + window.location.search;
56
+ window.history.replaceState(null, "", clean || "/governance");
57
+ return true;
58
+ }
@@ -1,27 +1,52 @@
1
- import { useCallback, useSyncExternalStore } from "react";
2
- import { formatHashRoute, parseHashRoute, type HashRoute } from "./hashRoute";
1
+ import { useCallback, useEffect, useSyncExternalStore } from "react";
2
+ import {
3
+ formatHashRoute,
4
+ parseHashRoute,
5
+ stripLegacyHashFragment,
6
+ type HashRoute,
7
+ } from "./hashRoute";
8
+
9
+ const NAVIGATE_EVENT = "dslinter:navigate";
3
10
 
4
11
  function subscribe(onStoreChange: () => void) {
5
- window.addEventListener("hashchange", onStoreChange);
6
- return () => window.removeEventListener("hashchange", onStoreChange);
12
+ window.addEventListener("popstate", onStoreChange);
13
+ window.addEventListener(NAVIGATE_EVENT, onStoreChange);
14
+ return () => {
15
+ window.removeEventListener("popstate", onStoreChange);
16
+ window.removeEventListener(NAVIGATE_EVENT, onStoreChange);
17
+ };
7
18
  }
8
19
 
9
- function getHashSnapshot() {
10
- return window.location.hash || "#!/governance";
20
+ function getPathSnapshot() {
21
+ return window.location.pathname || "/governance";
11
22
  }
12
23
 
13
- function getServerHashSnapshot() {
14
- return "#!/governance";
24
+ function getServerPathSnapshot() {
25
+ return "/governance";
15
26
  }
16
27
 
17
28
  export function useHashRoute(): [HashRoute, (next: HashRoute) => void] {
18
- const hash = useSyncExternalStore(subscribe, getHashSnapshot, getServerHashSnapshot);
19
- const route = parseHashRoute(hash);
29
+ const pathname = useSyncExternalStore(
30
+ subscribe,
31
+ getPathSnapshot,
32
+ getServerPathSnapshot,
33
+ );
34
+ const route = parseHashRoute(pathname);
35
+
36
+ useEffect(() => {
37
+ if (stripLegacyHashFragment()) {
38
+ window.dispatchEvent(new Event(NAVIGATE_EVENT));
39
+ }
40
+ }, []);
20
41
 
21
42
  const navigate = useCallback((next: HashRoute) => {
22
- const nextHash = formatHashRoute(next);
23
- if (nextHash !== window.location.hash) {
24
- window.location.hash = nextHash;
43
+ const nextPath = formatHashRoute(next);
44
+ const pathChanged = nextPath !== window.location.pathname;
45
+ if (pathChanged) {
46
+ window.history.pushState(null, "", nextPath);
47
+ }
48
+ if (stripLegacyHashFragment() || pathChanged) {
49
+ window.dispatchEvent(new Event(NAVIGATE_EVENT));
25
50
  }
26
51
  }, []);
27
52
 
@@ -2,10 +2,20 @@
2
2
  * DSLinter dashboard design tokens + shadcn/ui theme.
3
3
  * Host apps import once: `@import "dslinter/theme.css";` after `@import "tailwindcss"` and `@source` for this package.
4
4
  */
5
+
5
6
  @import "tw-animate-css";
6
7
  @import "shadcn/tailwind.css";
7
8
 
8
- @custom-variant dark (&:is(.dark *));
9
+ /** Class-based dark only — never `prefers-color-scheme` or host `.dark`. */
10
+ @custom-variant dark (&:is([data-dashboard-theme="dark"] *));
11
+
12
+ html:has([data-dashboard-theme="light"]) {
13
+ color-scheme: only light;
14
+ }
15
+
16
+ html:has([data-dashboard-theme="dark"]) {
17
+ color-scheme: only dark;
18
+ }
9
19
 
10
20
  @theme inline {
11
21
  --color-background: var(--background);
@@ -87,8 +97,9 @@
87
97
  }
88
98
  }
89
99
 
90
- :root {
91
- color-scheme: light;
100
+ :root,
101
+ [data-dashboard-theme="light"] {
102
+ color-scheme: only light;
92
103
  --radius: 0.625rem;
93
104
  --background: oklch(1 0 0);
94
105
  --foreground: oklch(0.145 0 0);
@@ -125,8 +136,8 @@
125
136
  --sidebar-ring: oklch(0.708 0 0);
126
137
  }
127
138
 
128
- .dark {
129
- color-scheme: dark;
139
+ [data-dashboard-theme="dark"] {
140
+ color-scheme: only dark;
130
141
  --background: oklch(0.145 0 0);
131
142
  --foreground: oklch(0.985 0 0);
132
143
  --card: oklch(0.205 0 0);
@@ -178,10 +189,10 @@
178
189
  background-size: 10px 10px;
179
190
  }
180
191
 
181
- .dark .ds-playground-dot-surface {
192
+ [data-dashboard-theme="dark"] .ds-playground-dot-surface {
182
193
  background-image: radial-gradient(
183
194
  circle,
184
- rgb(148 163 184 / 0.22) 1px,
195
+ rgb(148 163 184 / 0.15) 1px,
185
196
  transparent 1px
186
197
  );
187
198
  }
@@ -1,11 +1,19 @@
1
1
  /** Values passed from the dashboard control panel into `PlaygroundPreview`. */
2
2
  export type PlaygroundArgs = Record<string, string | number | boolean>;
3
3
 
4
+ export type PlaygroundValuesUpdater = (
5
+ next: PlaygroundArgs | ((prev: PlaygroundArgs) => PlaygroundArgs),
6
+ ) => void;
7
+
8
+ /** Generated examples should not be presented as real API defaults. */
9
+ export type PlaygroundDefaultSource = "type" | "example" | "manual";
10
+
4
11
  export type PlaygroundBooleanControl = {
5
12
  key: string;
6
13
  label: string;
7
14
  type: "boolean";
8
15
  default: boolean;
16
+ defaultSource?: PlaygroundDefaultSource;
9
17
  hint?: string;
10
18
  };
11
19
 
@@ -14,6 +22,7 @@ export type PlaygroundStringControl = {
14
22
  label: string;
15
23
  type: "string";
16
24
  default: string;
25
+ defaultSource?: PlaygroundDefaultSource;
17
26
  placeholder?: string;
18
27
  };
19
28
 
@@ -22,6 +31,7 @@ export type PlaygroundNumberControl = {
22
31
  label: string;
23
32
  type: "number";
24
33
  default: number;
34
+ defaultSource?: PlaygroundDefaultSource;
25
35
  min?: number;
26
36
  max?: number;
27
37
  step?: number;
@@ -32,6 +42,7 @@ export type PlaygroundSelectControl = {
32
42
  label: string;
33
43
  type: "select";
34
44
  default: string;
45
+ defaultSource?: PlaygroundDefaultSource;
35
46
  options: { value: string; label: string }[];
36
47
  };
37
48