dslinter 0.2.3 → 0.4.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 (64) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/bin/lib/infer-prop-types-from-ts.mjs +14 -1
  3. package/bin/lib/infer-prop-types-from-ts.test.mjs +32 -0
  4. package/dashboard-dist/assets/{DashboardLayoutAuto-h0gP_iKd.js → DashboardLayoutAuto-BWuyjHPD.js} +1 -1
  5. package/dashboard-dist/assets/DashboardLayoutAuto-CAO6F6-q.css +1 -0
  6. package/dashboard-dist/assets/{axe-DDaE9JTN.js → axe-DHHCqGjV.js} +1 -1
  7. package/dashboard-dist/assets/index-Bxk7tA3F.js +219 -0
  8. package/dashboard-dist/assets/index-D0O_5w5V.css +1 -0
  9. package/dashboard-dist/dslinter-report.json +23929 -0
  10. package/dashboard-dist/index.html +2 -2
  11. package/index.cjs +52 -52
  12. package/package.json +6 -6
  13. package/src/components/CatalogPane.tsx +94 -0
  14. package/src/components/ComponentInspectPane.tsx +125 -125
  15. package/src/components/ComponentPlaygroundPane.tsx +42 -27
  16. package/src/components/DashboardCommandPalette.tsx +9 -0
  17. package/src/components/GovernanceInventoryTabs.tsx +51 -0
  18. package/src/components/GovernancePane.tsx +18 -5
  19. package/src/components/PlaygroundA11yAndCode.tsx +0 -52
  20. package/src/components/PlaygroundControlField.tsx +2 -0
  21. package/src/components/ScoreGauge.test.ts +22 -0
  22. package/src/components/ScoreGauge.tsx +179 -0
  23. package/src/components/Sidebar.tsx +97 -23
  24. package/src/components/TokensPane.tsx +11 -13
  25. package/src/components/controlApiTable.test.ts +15 -0
  26. package/src/components/controlApiTable.ts +4 -0
  27. package/src/components/ui/badge.tsx +5 -5
  28. package/src/dashboard/ComponentCatalog.tsx +10 -1
  29. package/src/dashboard/ComponentPropUsageDetail.tsx +127 -42
  30. package/src/dashboard/ComponentUsageDetails.tsx +39 -9
  31. package/src/dashboard/DashboardBody.tsx +83 -12
  32. package/src/dashboard/ScannedTokenWall.tsx +9 -6
  33. package/src/dashboard/UnusedComponentsList.tsx +74 -0
  34. package/src/dashboard/aggregate.test.ts +381 -12
  35. package/src/dashboard/aggregate.ts +167 -30
  36. package/src/dashboard/mergeTokenCatalog.ts +5 -0
  37. package/src/dashboard/paths.test.ts +18 -1
  38. package/src/dashboard/paths.ts +8 -0
  39. package/src/mcp/agent-query.ts +1 -1
  40. package/src/mcp/css-color.test.ts +52 -0
  41. package/src/mcp/css-color.ts +73 -0
  42. package/src/mcp/rule-catalog.json +3 -3
  43. package/src/mcp/verify-loop.test.ts +24 -0
  44. package/src/mcp/verify-loop.ts +28 -6
  45. package/src/playground/buildPlaygroundEntriesFromReport.test.ts +3 -3
  46. package/src/playground/controls.ts +16 -3
  47. package/src/playground/enrichKitControls.ts +5 -5
  48. package/src/playground/inferKitJsx.test.ts +0 -11
  49. package/src/playground/inferPropTypesFromTs.d.mts +1 -1
  50. package/src/playground/inferPropTypesFromTs.mjs +19 -3
  51. package/src/playground/inferPropTypesFromTs.test.ts +32 -0
  52. package/src/playground/inferPropTypesFromTs.ts +1 -1
  53. package/src/playground/playgroundJoin.ts +34 -0
  54. package/src/playground/propCoerce.ts +2 -2
  55. package/src/playground/snippet.ts +1 -0
  56. package/src/shell/DashboardLayout.tsx +21 -4
  57. package/src/shell/hashRoute.test.ts +9 -0
  58. package/src/shell/hashRoute.ts +6 -0
  59. package/src/types/controls.ts +12 -0
  60. package/src/types/report.ts +1 -1
  61. package/vite/embedTailwindSources.ts +8 -6
  62. package/dashboard-dist/assets/DashboardLayoutAuto-Bja3BuZZ.css +0 -1
  63. package/dashboard-dist/assets/index-B9sZ6wHm.css +0 -1
  64. package/dashboard-dist/assets/index-DIDBt5ed.js +0 -218
@@ -1,44 +1,100 @@
1
1
  import { useMemo } from "react";
2
- import {
3
- Table,
4
- TableBody,
5
- TableCell,
6
- TableHead,
7
- TableHeader,
8
- TableRow,
9
- } from "./ui/table";
10
2
  import {
11
3
  aggregateDeclaredProps,
12
4
  aggregateDefinitions,
5
+ catalogChildComponentsFor,
13
6
  componentCatalogFamilyForName,
7
+ type DefinitionSite,
14
8
  } from "../dashboard/aggregate";
15
9
  import {
16
10
  buildUnusedPropSetForComponent,
17
11
  ComponentPropUsageDetail,
12
+ propFrequenciesForComponent,
18
13
  } from "../dashboard/ComponentPropUsageDetail";
19
14
  import { ComponentUsageDetails } from "../dashboard/ComponentUsageDetails";
20
15
  import { FindingsList } from "../dashboard/FindingsList";
21
16
  import { shortPath } from "../dashboard/paths";
22
17
  import { findingsForComponent } from "../report/findingsForComponent";
18
+ import {
19
+ findPlaygroundSpec,
20
+ playgroundJoinDetailMessage,
21
+ type PlaygroundJoinSkip,
22
+ } from "../playground/playgroundJoin";
23
23
  import type { WorkspaceReport } from "../types/report";
24
- import type { PlaygroundJoinSkip } from "../playground/playgroundJoin";
25
- import { findPlaygroundSpec } from "../playground/playgroundJoin";
26
24
  import { HideFromCatalogButton } from "./HideFromCatalogButton";
27
25
  import { Section } from "./Section";
28
26
  import { TruncatedPath } from "./TruncatedPath";
27
+ import {
28
+ Table,
29
+ TableBody,
30
+ TableCell,
31
+ TableHead,
32
+ TableHeader,
33
+ TableRow,
34
+ } from "./ui/table";
29
35
 
30
36
  type Props = {
31
37
  componentId: string;
32
38
  workspaceReport: WorkspaceReport | null;
33
39
  reportReady: boolean;
34
40
  hasPlaygroundSpec: boolean;
35
- /** When the report row exists but Vite could not load the module/export. */
36
41
  playgroundJoinSkip?: PlaygroundJoinSkip;
37
- onBackToGovernance: () => void;
38
42
  onOpenComponent: (componentId: string) => void;
39
43
  onHideFromCatalog?: (componentId: string) => void;
40
44
  };
41
45
 
46
+ const PREVIEW_NOTE = {
47
+ missing: "No playable component definition was found",
48
+ unloadable:
49
+ "A preview was expected for this component but the module could not be loaded in the dashboard bundle (check the file path, export name, or that npx dslinter was run from the project root).",
50
+ } as const;
51
+
52
+ function reportPlaceholder(message: string) {
53
+ return <p className="text-sm text-muted-foreground">{message}</p>;
54
+ }
55
+
56
+ function DefinitionsTable({
57
+ definitions,
58
+ root,
59
+ }: {
60
+ definitions: DefinitionSite[];
61
+ root: string | undefined;
62
+ }) {
63
+ if (definitions.length === 0) {
64
+ return (
65
+ <p className="text-sm text-muted-foreground">
66
+ No definition sites recorded — this name may appear only from JSX usage.
67
+ </p>
68
+ );
69
+ }
70
+
71
+ return (
72
+ <Table>
73
+ <TableHeader>
74
+ <TableRow>
75
+ <TableHead>File</TableHead>
76
+ <TableHead className="w-20">Line</TableHead>
77
+ <TableHead>Kind</TableHead>
78
+ </TableRow>
79
+ </TableHeader>
80
+ <TableBody>
81
+ {definitions.map((site) => (
82
+ <TableRow key={`${site.path}:${site.line}:${site.kind}`}>
83
+ <TableCell className="min-w-0 font-mono text-xs">
84
+ <TruncatedPath
85
+ path={root ? shortPath(root, site.path) : site.path}
86
+ className="text-xs"
87
+ />
88
+ </TableCell>
89
+ <TableCell>{site.line}</TableCell>
90
+ <TableCell className="text-muted-foreground">{site.kind}</TableCell>
91
+ </TableRow>
92
+ ))}
93
+ </TableBody>
94
+ </Table>
95
+ );
96
+ }
97
+
42
98
  export function ComponentInspectPane({
43
99
  componentId,
44
100
  workspaceReport,
@@ -48,69 +104,50 @@ export function ComponentInspectPane({
48
104
  onOpenComponent,
49
105
  onHideFromCatalog,
50
106
  }: Props) {
107
+ const report = reportReady ? workspaceReport : null;
51
108
  const playgroundSpec = findPlaygroundSpec(workspaceReport, componentId);
52
- const definitions = useMemo(() => {
53
- if (!workspaceReport) return [];
54
- return aggregateDefinitions(workspaceReport).get(componentId) ?? [];
55
- }, [workspaceReport, componentId]);
56
-
57
- const declared = useMemo(() => {
58
- if (!workspaceReport) return [];
59
- return aggregateDeclaredProps(workspaceReport).get(componentId) ?? [];
60
- }, [workspaceReport, componentId]);
61
-
62
- const unusedProps = useMemo(() => {
63
- if (!workspaceReport) return new Set<string>();
64
- return buildUnusedPropSetForComponent(
65
- workspaceReport,
66
- componentId,
67
- declared,
68
- );
69
- }, [workspaceReport, componentId, declared]);
70
-
71
- const findings = useMemo(
72
- () => findingsForComponent(workspaceReport, componentId),
73
- [workspaceReport, componentId],
74
- );
75
- const family = useMemo(
76
- () => componentCatalogFamilyForName(workspaceReport, componentId),
77
- [workspaceReport, componentId],
109
+ const joinDetail = playgroundJoinDetailMessage(
110
+ playgroundJoinSkip,
111
+ playgroundSpec,
78
112
  );
79
- const childComponents = family?.parent === componentId ? family.children : [];
80
-
81
- const previewNote = hasPlaygroundSpec
82
- ? "A preview was expected for this component but the module could not be loaded in the dashboard bundle (check the file path, export name, or that npx dslinter was run from the project root)."
83
- : "No playable component definition was found";
84
113
 
85
- const joinDetail = (() => {
86
- if (playgroundJoinSkip?.reason === "module_not_found") {
87
- const { globKey, rel_path } = playgroundJoinSkip;
88
- const subdirHint = !rel_path.includes("/")
89
- ? " This usually means the scanner was run from a subdirectory. Re-run from the project root: npx dslinter ."
90
- : "";
91
- if (globKey.startsWith("@dslinter-scan/")) {
92
- return [
93
- `Expected module key "${globKey}" but the dslinter Vite plugin did not load it.`,
94
- `Use <DashboardLayout autoPlayground /> and run via npx dslinter (zero vite.config changes), or add plugins: [dslinter()] from dslinter/vite to vite.config.ts.`,
95
- `Run the scanner from the project root so rel_path "${rel_path}" matches files under DSLINTER_SCAN_ROOT.`,
96
- ].join(" ");
97
- }
98
- return [
99
- `Vite glob is missing key "${globKey}" for report path "${rel_path}".`,
100
- `Prefer <DashboardLayout autoPlayground /> with plugins: [dslinter()] from dslinter/vite, or run npx dslinter init for a custom buildRegistry.ts glob.`,
101
- subdirHint,
102
- ]
103
- .filter(Boolean)
104
- .join("");
114
+ const {
115
+ definitions,
116
+ declared,
117
+ unusedProps,
118
+ propFrequencies,
119
+ findings,
120
+ childComponents,
121
+ } = useMemo(() => {
122
+ if (!workspaceReport) {
123
+ return {
124
+ definitions: [] as DefinitionSite[],
125
+ declared: [] as string[],
126
+ unusedProps: new Set<string>(),
127
+ propFrequencies: {},
128
+ findings: [],
129
+ childComponents: [] as string[],
130
+ };
105
131
  }
106
- if (playgroundJoinSkip?.reason === "export_not_found") {
107
- return `Module loaded but named export "${playgroundJoinSkip.export_name}" was not found. Use export function ${playgroundJoinSkip.export_name}(…) in ${playgroundJoinSkip.rel_path}.`;
108
- }
109
- if (playgroundSpec) {
110
- return `Report path: ${playgroundSpec.rel_path} (export ${playgroundSpec.export_name}). Use autoPlayground with dslinter/vite, or ensure buildRegistry.ts glob covers this file.`;
111
- }
112
- return null;
113
- })();
132
+
133
+ const declared =
134
+ aggregateDeclaredProps(workspaceReport).get(componentId) ?? [];
135
+ const family = componentCatalogFamilyForName(workspaceReport, componentId);
136
+
137
+ return {
138
+ definitions:
139
+ aggregateDefinitions(workspaceReport).get(componentId) ?? [],
140
+ declared,
141
+ unusedProps: buildUnusedPropSetForComponent(
142
+ workspaceReport,
143
+ componentId,
144
+ declared,
145
+ ),
146
+ propFrequencies: propFrequenciesForComponent(workspaceReport, componentId),
147
+ findings: findingsForComponent(workspaceReport, componentId),
148
+ childComponents: catalogChildComponentsFor(family, componentId),
149
+ };
150
+ }, [workspaceReport, componentId]);
114
151
 
115
152
  return (
116
153
  <div className="flex min-h-0 flex-1 flex-col overflow-hidden bg-background">
@@ -125,7 +162,7 @@ export function ComponentInspectPane({
125
162
  {componentId}
126
163
  </h1>
127
164
  <p className="mt-2 max-w-2xl text-sm text-muted-foreground">
128
- {previewNote}
165
+ {hasPlaygroundSpec ? PREVIEW_NOTE.unloadable : PREVIEW_NOTE.missing}
129
166
  </p>
130
167
  {joinDetail ? (
131
168
  <p className="mt-2 max-w-2xl font-mono text-xs text-muted-foreground">
@@ -133,14 +170,12 @@ export function ComponentInspectPane({
133
170
  </p>
134
171
  ) : null}
135
172
  </div>
136
- <div className="flex shrink-0 flex-wrap items-center gap-2">
137
- {onHideFromCatalog ? (
138
- <HideFromCatalogButton
139
- componentName={componentId}
140
- onHidden={onHideFromCatalog}
141
- />
142
- ) : null}
143
- </div>
173
+ {onHideFromCatalog ? (
174
+ <HideFromCatalogButton
175
+ componentName={componentId}
176
+ onHidden={onHideFromCatalog}
177
+ />
178
+ ) : null}
144
179
  </div>
145
180
  </header>
146
181
 
@@ -150,42 +185,10 @@ export function ComponentInspectPane({
150
185
  title="Definitions"
151
186
  description="Source files where this component is defined."
152
187
  >
153
- {definitions.length > 0 ? (
154
- <Table>
155
- <TableHeader>
156
- <TableRow>
157
- <TableHead>File</TableHead>
158
- <TableHead className="w-20">Line</TableHead>
159
- <TableHead>Kind</TableHead>
160
- </TableRow>
161
- </TableHeader>
162
- <TableBody>
163
- {definitions.map((site) => (
164
- <TableRow key={`${site.path}:${site.line}:${site.kind}`}>
165
- <TableCell className="min-w-0 font-mono text-xs">
166
- <TruncatedPath
167
- path={
168
- workspaceReport
169
- ? shortPath(workspaceReport.root, site.path)
170
- : site.path
171
- }
172
- className="text-xs"
173
- />
174
- </TableCell>
175
- <TableCell>{site.line}</TableCell>
176
- <TableCell className="text-muted-foreground">
177
- {site.kind}
178
- </TableCell>
179
- </TableRow>
180
- ))}
181
- </TableBody>
182
- </Table>
183
- ) : (
184
- <p className="text-sm text-muted-foreground">
185
- No definition sites recorded — this name may appear only from
186
- JSX usage.
187
- </p>
188
- )}
188
+ <DefinitionsTable
189
+ definitions={definitions}
190
+ root={workspaceReport?.root}
191
+ />
189
192
  </Section>
190
193
 
191
194
  {childComponents.length > 0 ? (
@@ -214,16 +217,15 @@ export function ComponentInspectPane({
214
217
  title="Props"
215
218
  description="Declared props and workspace usage from the latest scan."
216
219
  >
217
- {reportReady && workspaceReport ? (
220
+ {report ? (
218
221
  <ComponentPropUsageDetail
219
222
  component={componentId}
220
223
  declared={declared}
221
224
  unusedProps={unusedProps}
225
+ propFrequencies={propFrequencies}
222
226
  />
223
227
  ) : (
224
- <p className="text-sm text-muted-foreground">
225
- Load the DSLinter report to see prop usage.
226
- </p>
228
+ reportPlaceholder("Load the DSLinter report to see prop usage.")
227
229
  )}
228
230
  </Section>
229
231
 
@@ -243,12 +245,10 @@ export function ComponentInspectPane({
243
245
  title="Findings"
244
246
  description="DSLinter findings on files where this component is defined."
245
247
  >
246
- {reportReady && workspaceReport ? (
247
- <FindingsList findings={findings} root={workspaceReport.root} />
248
+ {report ? (
249
+ <FindingsList findings={findings} root={report.root} />
248
250
  ) : (
249
- <p className="text-sm text-muted-foreground">
250
- Load the DSLinter report to see findings.
251
- </p>
251
+ reportPlaceholder("Load the DSLinter report to see findings.")
252
252
  )}
253
253
  </Section>
254
254
  </div>
@@ -16,6 +16,7 @@ import { tokenStyleFindingsForModule } from "../report/tokenStyleFindingsForModu
16
16
  import type { WorkspaceReport } from "../types/report";
17
17
  import {
18
18
  aggregateDeclaredProps,
19
+ catalogChildComponentsFor,
19
20
  componentCatalogFamilyForName,
20
21
  usageMap,
21
22
  } from "../dashboard/aggregate";
@@ -41,10 +42,9 @@ import {
41
42
  } from "../playground/scanVariantA11y";
42
43
  import { HideFromCatalogButton } from "./HideFromCatalogButton";
43
44
  import { OpenInEditorButton } from "./OpenInEditorButton";
45
+ import { ScoreGauge } from "./ScoreGauge";
44
46
  import { Section } from "./Section";
45
- import {
46
- resolveModuleAbsolutePath,
47
- } from "../dashboard/editorLink";
47
+ import { resolveModuleAbsolutePath } from "../dashboard/editorLink";
48
48
 
49
49
  type Props = {
50
50
  entry: PlaygroundEntry;
@@ -139,7 +139,7 @@ export function ComponentPlaygroundPane({
139
139
  entry,
140
140
  workspaceReport,
141
141
  reportReady,
142
- onOpenComponent,
142
+ onOpenComponent: _onOpenComponent,
143
143
  onHideFromCatalog,
144
144
  }: Props) {
145
145
  const { renderPreview } = entry;
@@ -377,7 +377,7 @@ export function ComponentPlaygroundPane({
377
377
  () => componentCatalogFamilyForName(report, entry.id),
378
378
  [report, entry.id],
379
379
  );
380
- const childComponents = family?.parent === entry.id ? family.children : [];
380
+ const childComponents = catalogChildComponentsFor(family, entry.id);
381
381
  const resetControls = () =>
382
382
  setValues(defaultArgsFromControls(entry.controls));
383
383
 
@@ -401,27 +401,27 @@ export function ComponentPlaygroundPane({
401
401
  <div className="min-h-0 flex-1 overflow-auto">
402
402
  <header
403
403
  id="source"
404
- className="scroll-mt-20 border-b border-border bg-card p-6"
404
+ className="scroll-mt-20 border-b border-border bg-card p-6 flex flex-wrap items-center justify-between gap-4"
405
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">
406
+ <div className="min-w-0">
407
+ <p className="text-sm font-medium text-muted-foreground">
408
+ Components
409
+ {entry.meta.group ? (
410
+ <>
411
+ {" "}
412
+ <span className="text-muted-foreground/40">/</span>{" "}
413
+ <span className="capitalize text-foreground/80">
414
+ {entry.meta.group}
415
+ </span>
416
+ </>
417
+ ) : null}
418
+ </p>
419
+ <h1 className="text-3xl font-semibold tracking-tight text-foreground">
420
+ {entry.meta.title}
421
+ </h1>
422
+ </div>
423
+ <div className="flex shrink-0 flex-wrap items-center gap-4">
424
+ <div className="flex flex-wrap items-center gap-2">
425
425
  {sourceAbsolutePath ? (
426
426
  <OpenInEditorButton filePath={sourceAbsolutePath} />
427
427
  ) : null}
@@ -432,6 +432,21 @@ export function ComponentPlaygroundPane({
432
432
  />
433
433
  ) : null}
434
434
  </div>
435
+ <div className="flex items-start gap-5">
436
+ <ScoreGauge
437
+ label="Code score"
438
+ value={reportReady ? codeScore.score : null}
439
+ href="#code-score"
440
+ />
441
+ <ScoreGauge
442
+ label="Accessibility"
443
+ value={
444
+ reportReady || variantScanComplete ? combinedA11y.score : null
445
+ }
446
+ href="#accessibility"
447
+ pending={variantScanPending}
448
+ />
449
+ </div>
435
450
  </div>
436
451
  </header>
437
452
 
@@ -443,7 +458,7 @@ export function ComponentPlaygroundPane({
443
458
  <div className="flex justify-center">
444
459
  <div
445
460
  ref={previewFrameRef}
446
- className="relative min-w-0 shrink-0 select-none rounded-lg border border-border bg-muted/50 shadow-xs will-change-[width]"
461
+ className="relative min-w-0 shrink-0 select-none rounded-lg border border-border bg-background shadow-xs will-change-[width]"
447
462
  style={{ width: previewWidthPx }}
448
463
  >
449
464
  <PreviewResizeHandle
@@ -456,7 +471,7 @@ export function ComponentPlaygroundPane({
456
471
  />
457
472
  <PlaygroundAppThemeWrapper
458
473
  workspaceReport={report}
459
- className="min-w-0 p-8 backdrop-blur-2xl"
474
+ className="min-w-0 p-8"
460
475
  >
461
476
  <PlaygroundPreviewErrorBoundary
462
477
  componentName={entry.meta.title}
@@ -59,6 +59,15 @@ export function DashboardCommandPalette({ catalogEntries, onNavigate, open, onOp
59
59
  >
60
60
  Governance
61
61
  </CommandItem>
62
+ <CommandItem
63
+ value="catalog all components inventory"
64
+ onSelect={() => {
65
+ onNavigate({ view: "catalog" });
66
+ close();
67
+ }}
68
+ >
69
+ All components
70
+ </CommandItem>
62
71
  </CommandGroup>
63
72
  {catalogEntries.length > 0 ? (
64
73
  <CommandGroup heading="Components">
@@ -0,0 +1,51 @@
1
+ import { ToggleGroup, ToggleGroupItem } from "./ui/toggle-group";
2
+ import type { GovernanceInventoryTab } from "../dashboard/aggregate";
3
+
4
+ const tabs: { id: GovernanceInventoryTab; label: string }[] = [
5
+ { id: "all", label: "All issues" },
6
+ { id: "a11y", label: "Accessibility" },
7
+ { id: "code", label: "Code quality" },
8
+ { id: "token", label: "Tokens" },
9
+ { id: "unused", label: "Unused" },
10
+ ];
11
+
12
+ function isGovernanceInventoryTab(value: string): value is GovernanceInventoryTab {
13
+ return tabs.some((tab) => tab.id === value);
14
+ }
15
+
16
+ export function GovernanceInventoryTabs({
17
+ value,
18
+ onChange,
19
+ counts,
20
+ }: {
21
+ value: GovernanceInventoryTab;
22
+ onChange: (value: GovernanceInventoryTab) => void;
23
+ counts: Record<GovernanceInventoryTab, number>;
24
+ }) {
25
+ return (
26
+ <ToggleGroup
27
+ type="single"
28
+ value={value}
29
+ onValueChange={(next) => {
30
+ if (isGovernanceInventoryTab(next)) onChange(next);
31
+ }}
32
+ variant="outline"
33
+ size="sm"
34
+ aria-label="Filter governance inventory"
35
+ className="contents"
36
+ >
37
+ {tabs.map((tab) => (
38
+ <ToggleGroupItem
39
+ key={tab.id}
40
+ value={tab.id}
41
+ className="rounded-full px-2.5 text-xs font-medium"
42
+ >
43
+ {tab.label}
44
+ <span className="ml-1 tabular-nums text-muted-foreground">
45
+ {counts[tab.id]}
46
+ </span>
47
+ </ToggleGroupItem>
48
+ ))}
49
+ </ToggleGroup>
50
+ );
51
+ }
@@ -1,5 +1,9 @@
1
1
  import type { ReactNode } from "react";
2
- import { componentCatalogNamesFromReport } from "../dashboard/aggregate";
2
+ import {
3
+ componentCatalogNamesFromReport,
4
+ governanceTabCounts,
5
+ unusedComponentsFromReport,
6
+ } from "../dashboard/aggregate";
3
7
  import { DashboardBody } from "../dashboard/DashboardBody";
4
8
  import type { DslinterReportState } from "../dashboard/useWorkspaceReport";
5
9
 
@@ -10,6 +14,7 @@ type Props = {
10
14
  dslinterReportHint?: string;
11
15
  dslinterReport: DslinterReportState;
12
16
  onOpenComponent?: (name: string) => void;
17
+ onOpenCatalog?: () => void;
13
18
  };
14
19
 
15
20
  export function GovernancePane({
@@ -18,11 +23,14 @@ export function GovernancePane({
18
23
  dslinterReportHint = "npm run dslinter:report",
19
24
  dslinterReport,
20
25
  onOpenComponent,
26
+ onOpenCatalog,
21
27
  }: Props) {
22
28
  const { report, error, loading } = dslinterReport;
23
29
  const componentCatalogCount = report
24
30
  ? componentCatalogNamesFromReport(report).length
25
31
  : 0;
32
+ const unusedCount = report ? unusedComponentsFromReport(report).length : 0;
33
+ const findingCount = report ? governanceTabCounts(report).all : 0;
26
34
 
27
35
  if (error) {
28
36
  return (
@@ -71,16 +79,21 @@ export function GovernancePane({
71
79
  Governance
72
80
  <span className="font-normal text-muted-foreground">
73
81
  {" "}
74
- · {componentCatalogCount} components
82
+ · {findingCount} {findingCount === 1 ? "issue" : "issues"} ·{" "}
83
+ {unusedCount} unused · {componentCatalogCount} total
75
84
  </span>
76
85
  </h1>
77
86
  <p className="text-sm text-muted-foreground">
78
- Scores, component catalog, token wall, and findings from the latest
79
- DSLinter snapshot
87
+ Governance scores, workspace findings, and components with no usage
88
+ from the latest DSLinter snapshot
80
89
  </p>
81
90
  </header>
82
91
  <div className="min-w-0 w-full px-6 py-8">
83
- <DashboardBody report={report} onOpenComponent={onOpenComponent} />
92
+ <DashboardBody
93
+ report={report}
94
+ onOpenComponent={onOpenComponent}
95
+ onOpenCatalog={onOpenCatalog}
96
+ />
84
97
  </div>
85
98
  </div>
86
99
  );
@@ -230,26 +230,11 @@ type ApiProps = {
230
230
  governanceReportLoaded?: boolean;
231
231
  };
232
232
 
233
- function formatRepoLiteralChips(
234
- byVal: Record<string, number> | undefined,
235
- max = 6,
236
- ): string {
237
- if (!byVal || Object.keys(byVal).length === 0) return "—";
238
- const entries = Object.entries(byVal).sort((x, y) => y[1] - x[1]);
239
- const shown = entries.slice(0, max);
240
- const tail = Math.max(0, entries.length - max);
241
- return (
242
- shown.map(([val, n]) => `${JSON.stringify(val)} ×${n}`).join(" · ") +
243
- (tail > 0 ? ` · +${tail}` : "")
244
- );
245
- }
246
-
247
233
  export function PlaygroundApiReference({
248
234
  controls,
249
235
  values,
250
236
  onChange,
251
237
  onReset,
252
- reportUsage,
253
238
  declaredPropsFromScan: _declaredPropsFromScan = [],
254
239
  governanceReportLoaded: _governanceReportLoaded = false,
255
240
  }: ApiProps) {
@@ -263,18 +248,6 @@ export function PlaygroundApiReference({
263
248
  );
264
249
 
265
250
  const rows = controlsToApiRows(controls);
266
- const showRepo = reportUsage != null;
267
- const freqs = reportUsage?.prop_frequencies ?? {};
268
- const valueFreqs = reportUsage?.prop_value_frequencies ?? {};
269
- const controlKeys = new Set(rows.map((r) => r.prop));
270
- const extraRepoProps = showRepo
271
- ? Object.keys(freqs)
272
- .filter((k) => !controlKeys.has(k))
273
- .sort((a, b) => a.localeCompare(b))
274
- : [];
275
- const repoUsageProps = showRepo
276
- ? [...rows.map((r) => r.prop), ...extraRepoProps]
277
- : [];
278
251
  return (
279
252
  <Section
280
253
  id="api-reference"
@@ -357,31 +330,6 @@ export function PlaygroundApiReference({
357
330
  })}
358
331
  </TableBody>
359
332
  </Table>
360
-
361
- {showRepo ? (
362
- <Section id="repo-usage" title="Repo usage" className="mt-4">
363
- <Table>
364
- <TableHeader>
365
- <TableRow>
366
- <TableHead>Prop</TableHead>
367
- <TableHead>Count</TableHead>
368
- <TableHead>Values</TableHead>
369
- </TableRow>
370
- </TableHeader>
371
- <TableBody>
372
- {repoUsageProps.map((prop) => (
373
- <TableRow key={prop}>
374
- <TableCell className="font-medium">{prop}</TableCell>
375
- <TableCell>{freqs[prop] ?? 0}</TableCell>
376
- <TableCell>
377
- {formatRepoLiteralChips(valueFreqs[prop])}
378
- </TableCell>
379
- </TableRow>
380
- ))}
381
- </TableBody>
382
- </Table>
383
- </Section>
384
- ) : null}
385
333
  </Section>
386
334
  );
387
335
  }
@@ -58,6 +58,7 @@ export function PlaygroundControlField({
58
58
  );
59
59
  }
60
60
  case "string":
61
+ case "node":
61
62
  return (
62
63
  <div className="flex min-w-0 flex-col gap-1.5">
63
64
  <Label htmlFor={id} className={labelClass}>
@@ -153,6 +154,7 @@ export function PlaygroundControlField({
153
154
  );
154
155
  }
155
156
  case "string":
157
+ case "node":
156
158
  return (
157
159
  <Input
158
160
  id={id}