dslinter 0.0.6

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 (63) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/LICENSE +201 -0
  3. package/README.md +104 -0
  4. package/bin/dslinter.mjs +29 -0
  5. package/components.json +20 -0
  6. package/package.json +90 -0
  7. package/src/components/InlineCode.tsx +5 -0
  8. package/src/components/icons.tsx +121 -0
  9. package/src/components/ui/badge.tsx +52 -0
  10. package/src/components/ui/button.tsx +57 -0
  11. package/src/components/ui/checkbox.tsx +25 -0
  12. package/src/components/ui/command.tsx +183 -0
  13. package/src/components/ui/dialog.tsx +156 -0
  14. package/src/components/ui/hover-card.tsx +42 -0
  15. package/src/components/ui/input.tsx +22 -0
  16. package/src/components/ui/label.tsx +19 -0
  17. package/src/components/ui/select.tsx +149 -0
  18. package/src/components/ui/table.tsx +118 -0
  19. package/src/components/ui/toggle-group.tsx +83 -0
  20. package/src/components/ui/toggle.tsx +45 -0
  21. package/src/dashboard/ComponentCatalog.tsx +210 -0
  22. package/src/dashboard/ComponentUsageDetails.tsx +109 -0
  23. package/src/dashboard/DashboardBody.tsx +71 -0
  24. package/src/dashboard/FindingsList.tsx +151 -0
  25. package/src/dashboard/ScoreStrip.tsx +28 -0
  26. package/src/dashboard/TokenWall.tsx +241 -0
  27. package/src/dashboard/aggregate.ts +73 -0
  28. package/src/dashboard/paths.ts +10 -0
  29. package/src/dashboard/useWorkspaceReport.ts +136 -0
  30. package/src/index.ts +67 -0
  31. package/src/lib/utils.ts +6 -0
  32. package/src/playground/definePlayground.tsx +99 -0
  33. package/src/playground/enumerateControlCombinations.test.ts +112 -0
  34. package/src/playground/enumerateControlCombinations.ts +74 -0
  35. package/src/report/a11yForModule.ts +35 -0
  36. package/src/report/codeScoreForModule.ts +41 -0
  37. package/src/report/modulePathMatch.ts +27 -0
  38. package/src/report/tokenStyleFindingsForModule.ts +24 -0
  39. package/src/shell/ComponentPlaygroundPane.tsx +438 -0
  40. package/src/shell/DashboardCommandPalette.tsx +134 -0
  41. package/src/shell/DashboardLayout.tsx +230 -0
  42. package/src/shell/EmptyCard.tsx +21 -0
  43. package/src/shell/GovernancePane.tsx +77 -0
  44. package/src/shell/PlaygroundA11yAndCode.tsx +387 -0
  45. package/src/shell/PlaygroundControlField.tsx +213 -0
  46. package/src/shell/PlaygroundControls.tsx +66 -0
  47. package/src/shell/PlaygroundUsageCode.tsx +51 -0
  48. package/src/shell/PlaygroundVariantMatrix.tsx +68 -0
  49. package/src/shell/Section.tsx +34 -0
  50. package/src/shell/Sidebar.tsx +203 -0
  51. package/src/shell/TokensPane.tsx +26 -0
  52. package/src/shell/controlApiTable.ts +53 -0
  53. package/src/shell/hashRoute.ts +49 -0
  54. package/src/shell/playgroundUsageHighlight.ts +53 -0
  55. package/src/shell/playgroundUsageTwoslash.ts +69 -0
  56. package/src/shell/useHashRoute.ts +29 -0
  57. package/src/styles/dashboard-theme.css +188 -0
  58. package/src/types/controls.ts +62 -0
  59. package/src/types/defaultTailwindTypography.ts +55 -0
  60. package/src/types/playground.ts +21 -0
  61. package/src/types/preview.ts +8 -0
  62. package/src/types/report.ts +116 -0
  63. package/src/types/tokenCatalog.ts +54 -0
@@ -0,0 +1,109 @@
1
+ import { useMemo } from "react";
2
+ import {
3
+ Table,
4
+ TableBody,
5
+ TableCell,
6
+ TableHead,
7
+ TableHeader,
8
+ TableRow,
9
+ } from "@/components/ui/table";
10
+ import type { UsageLocation, WorkspaceReport } from "../types/report";
11
+ import { usageMap } from "./aggregate";
12
+ import { shortPath } from "./paths";
13
+ import { EmptyCard } from "../shell/EmptyCard";
14
+ import { InlineCode } from "@/components/InlineCode";
15
+
16
+ function formatCallSiteProps(loc: UsageLocation): string {
17
+ if (!loc.props.length) return "—";
18
+ return loc.props
19
+ .map((p) =>
20
+ loc.prop_values?.[p] != null
21
+ ? `${p}=${JSON.stringify(loc.prop_values[p])}`
22
+ : p,
23
+ )
24
+ .join(", ");
25
+ }
26
+
27
+ function sortedLocations(
28
+ locations: UsageLocation[] | undefined,
29
+ ): UsageLocation[] {
30
+ const list = [...(locations ?? [])];
31
+ list.sort((a, b) => a.path.localeCompare(b.path) || a.line - b.line);
32
+ return list;
33
+ }
34
+
35
+ export function ComponentUsageDetails({
36
+ report,
37
+ componentId,
38
+ }: {
39
+ report: WorkspaceReport | null;
40
+ componentId: string;
41
+ }) {
42
+ const usage = useMemo(() => {
43
+ if (!report) return undefined;
44
+ return usageMap(report).get(componentId);
45
+ }, [report, componentId]);
46
+
47
+ if (!report) {
48
+ return (
49
+ <p className="text-sm text-muted-foreground">
50
+ Load <span className="font-mono">dslint-report.json</span> to see where
51
+ this component is used in the workspace.
52
+ </p>
53
+ );
54
+ }
55
+
56
+ if (!usage) {
57
+ return (
58
+ <EmptyCard>
59
+ No found usage for <InlineCode>{componentId}</InlineCode>.
60
+ </EmptyCard>
61
+ );
62
+ }
63
+
64
+ const rows = sortedLocations(usage.usage_locations);
65
+
66
+ if (rows.length === 0) {
67
+ return (
68
+ <p className="text-sm text-muted-foreground">
69
+ Usage totals exist but individual call sites were not recorded in this
70
+ report.
71
+ </p>
72
+ );
73
+ }
74
+
75
+ return (
76
+ <Table className="[&>table]:table-fixed [&>table]:w-full">
77
+ <TableHeader>
78
+ <TableRow>
79
+ <TableHead className="w-[40%]">File</TableHead>
80
+ <TableHead className="w-14 whitespace-nowrap">Line</TableHead>
81
+ <TableHead className="min-w-0">Props at this call site</TableHead>
82
+ </TableRow>
83
+ </TableHeader>
84
+ <TableBody>
85
+ {rows.map((loc, i) => {
86
+ const propsText = formatCallSiteProps(loc);
87
+ return (
88
+ <TableRow key={`${loc.path}-${loc.line}-${i}`}>
89
+ <TableCell className="font-mono text-xs text-foreground">
90
+ {shortPath(report.root, loc.path)}
91
+ </TableCell>
92
+ <TableCell className="tabular-nums text-muted-foreground">
93
+ {loc.line}
94
+ </TableCell>
95
+ <TableCell className="min-w-0">
96
+ <span
97
+ className="block truncate font-mono text-xs text-foreground"
98
+ title={propsText}
99
+ >
100
+ {propsText}
101
+ </span>
102
+ </TableCell>
103
+ </TableRow>
104
+ );
105
+ })}
106
+ </TableBody>
107
+ </Table>
108
+ );
109
+ }
@@ -0,0 +1,71 @@
1
+ import { Section } from "../shell/Section";
2
+ import {
3
+ Table,
4
+ TableBody,
5
+ TableCell,
6
+ TableHead,
7
+ TableHeader,
8
+ TableRow,
9
+ } from "@/components/ui/table";
10
+ import type { WorkspaceReport } from "../types/report";
11
+ import { ComponentCatalog } from "./ComponentCatalog";
12
+ import { FindingsList } from "./FindingsList";
13
+ import { ScoreStrip } from "./ScoreStrip";
14
+
15
+ export function DashboardBody({ report }: { report: WorkspaceReport }) {
16
+ return (
17
+ <div className="space-y-10">
18
+ <ScoreStrip scores={report.scores} />
19
+
20
+ {report.duplicate_components.length > 0 ? (
21
+ <div className="rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-xs text-amber-950">
22
+ <span className="font-semibold">Duplicate component names: </span>
23
+ {report.duplicate_components.map((d) => d.name).join(", ")}
24
+ </div>
25
+ ) : null}
26
+
27
+ {report.ownership.length > 0 ? (
28
+ <Section
29
+ id="ownership"
30
+ title="Ownership"
31
+ description="Prefix match from <span className='font-mono'>.dslint.json</span> — useful for adoption rollups."
32
+ >
33
+ <Table>
34
+ <TableHeader>
35
+ <TableRow>
36
+ <TableHead>Owner</TableHead>
37
+ <TableHead>Files</TableHead>
38
+ <TableHead>Definitions</TableHead>
39
+ </TableRow>
40
+ </TableHeader>
41
+ <TableBody>
42
+ {report.ownership.map((row) => (
43
+ <TableRow key={row.owner}>
44
+ <TableCell>{row.owner}</TableCell>
45
+ <TableCell>{row.files}</TableCell>
46
+ <TableCell>{row.definitions}</TableCell>
47
+ </TableRow>
48
+ ))}
49
+ </TableBody>
50
+ </Table>
51
+ </Section>
52
+ ) : null}
53
+
54
+ <Section
55
+ id="components"
56
+ title="Components"
57
+ description="Definitions and JSX usage from the latest snapshot."
58
+ >
59
+ <ComponentCatalog report={report} />
60
+ </Section>
61
+
62
+ <Section
63
+ id="issues"
64
+ title="Issues"
65
+ description="Findings from the workspace DSLinter report scoped to this file."
66
+ >
67
+ <FindingsList findings={report.findings} root={report.root} />
68
+ </Section>
69
+ </div>
70
+ );
71
+ }
@@ -0,0 +1,151 @@
1
+ import { useMemo, useState } from "react";
2
+ import {
3
+ Table,
4
+ TableBody,
5
+ TableCell,
6
+ TableHead,
7
+ TableHeader,
8
+ TableRow,
9
+ } from "@/components/ui/table";
10
+ import { shortPath } from "./paths";
11
+ import type { LintFinding, Severity } from "../types/report";
12
+ import { Badge } from "@/components/ui/badge";
13
+ import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
14
+
15
+ type Filter = "all" | Severity;
16
+
17
+ function isFilter(value: string): value is Filter {
18
+ return (
19
+ value === "all" ||
20
+ value === "error" ||
21
+ value === "warning" ||
22
+ value === "info"
23
+ );
24
+ }
25
+
26
+ export function FindingsList({
27
+ findings,
28
+ root,
29
+ }: {
30
+ findings: LintFinding[];
31
+ root: string;
32
+ }) {
33
+ const [filter, setFilter] = useState<Filter>("all");
34
+
35
+ const counts = useMemo(() => {
36
+ const c = { error: 0, warning: 0, info: 0 };
37
+ for (const f of findings) {
38
+ if (f.severity === "error") c.error += 1;
39
+ else if (f.severity === "warning") c.warning += 1;
40
+ else c.info += 1;
41
+ }
42
+ return c;
43
+ }, [findings]);
44
+
45
+ const filtered =
46
+ filter === "all" ? findings : findings.filter((f) => f.severity === filter);
47
+
48
+ if (findings.length === 0) {
49
+ return (
50
+ <p className="rounded-lg border border-dashed border-border bg-muted/30 px-4 py-8 text-center text-sm text-muted-foreground">
51
+ No findings — rules are quiet on this snapshot.
52
+ </p>
53
+ );
54
+ }
55
+
56
+ return (
57
+ <div className="space-y-3">
58
+ <div className="flex flex-wrap gap-2">
59
+ <ToggleGroup
60
+ type="single"
61
+ value={filter}
62
+ onValueChange={(value) => {
63
+ if (isFilter(value)) setFilter(value);
64
+ }}
65
+ variant="outline"
66
+ size="sm"
67
+ aria-label="Filter findings by severity"
68
+ className="contents"
69
+ >
70
+ <ToggleGroupItem
71
+ value="all"
72
+ className="rounded-full px-2.5 text-xs font-medium"
73
+ >
74
+ All
75
+ <span className="ml-1 tabular-nums text-muted-foreground">
76
+ {findings.length}
77
+ </span>
78
+ </ToggleGroupItem>
79
+ <ToggleGroupItem
80
+ value="warning"
81
+ className="rounded-full px-2.5 text-xs font-medium"
82
+ >
83
+ Warnings
84
+ <span className="ml-1 tabular-nums text-muted-foreground">
85
+ {counts.warning}
86
+ </span>
87
+ </ToggleGroupItem>
88
+ <ToggleGroupItem
89
+ value="error"
90
+ className="rounded-full px-2.5 text-xs font-medium"
91
+ >
92
+ Errors
93
+ <span className="ml-1 tabular-nums text-muted-foreground">
94
+ {counts.error}
95
+ </span>
96
+ </ToggleGroupItem>
97
+ <ToggleGroupItem
98
+ value="info"
99
+ className="rounded-full px-2.5 text-xs font-medium"
100
+ >
101
+ Info
102
+ <span className="ml-1 tabular-nums text-muted-foreground">
103
+ {counts.info}
104
+ </span>
105
+ </ToggleGroupItem>
106
+ </ToggleGroup>
107
+ </div>
108
+ <Table>
109
+ <TableHeader>
110
+ <TableRow>
111
+ <TableHead scope="col">Severity</TableHead>
112
+ <TableHead scope="col">Rule</TableHead>
113
+ <TableHead scope="col">Message</TableHead>
114
+ <TableHead scope="col">File</TableHead>
115
+ </TableRow>
116
+ </TableHeader>
117
+ <TableBody className="align-top text-foreground">
118
+ {filtered.map((f, i) => (
119
+ <TableRow
120
+ key={`${f.rule_id}-${f.path}-${f.line ?? "x"}-${i}`}
121
+ className="border-border hover:bg-transparent"
122
+ >
123
+ <TableCell className="px-3 py-2">
124
+ <Badge
125
+ variant={
126
+ f.severity === "error"
127
+ ? "destructive"
128
+ : f.severity === "warning"
129
+ ? "secondary"
130
+ : "outline"
131
+ }
132
+ >
133
+ {f.severity}
134
+ </Badge>
135
+ </TableCell>
136
+ <TableCell className="px-3 py-2 font-mono text-xs text-muted-foreground">
137
+ {f.rule_id}
138
+ </TableCell>
139
+ <TableCell className="whitespace-normal px-3 py-2 text-sm">
140
+ {f.message}
141
+ </TableCell>
142
+ <TableCell className="whitespace-normal px-3 py-2 font-mono text-xs text-muted-foreground">
143
+ {shortPath(root, f.path)}:{f.line != null ? f.line : "—"}
144
+ </TableCell>
145
+ </TableRow>
146
+ ))}
147
+ </TableBody>
148
+ </Table>
149
+ </div>
150
+ );
151
+ }
@@ -0,0 +1,28 @@
1
+ import type { GovernanceScores } from "../types/report";
2
+
3
+ export function ScoreStrip({ scores }: { scores: GovernanceScores }) {
4
+ const items: { label: string; value: number }[] = [
5
+ { label: "System health", value: scores.design_system_health },
6
+ { label: "UX consistency", value: scores.ux_consistency },
7
+ { label: "Accessibility", value: scores.accessibility },
8
+ { label: "Maintainability", value: scores.maintainability },
9
+ ];
10
+
11
+ return (
12
+ <section className="grid grid-cols-2 gap-1 rounded-xl border border-border bg-muted/30 p-1 md:grid-cols-4">
13
+ {items.map(({ label, value }) => (
14
+ <div
15
+ key={label}
16
+ className="rounded-lg border border-border bg-card px-4 py-3 text-card-foreground shadow-xs"
17
+ >
18
+ <p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
19
+ {label}
20
+ </p>
21
+ <p className="mt-1 text-2xl font-semibold tabular-nums text-foreground">
22
+ {value}
23
+ </p>
24
+ </div>
25
+ ))}
26
+ </section>
27
+ );
28
+ }
@@ -0,0 +1,241 @@
1
+ import type { TokenCatalog } from "../types/tokenCatalog";
2
+
3
+ function hasTypographyContent(catalog: TokenCatalog): boolean {
4
+ const t = catalog.typography;
5
+ if (!t) return false;
6
+ return (
7
+ t.families.length > 0 || t.sizes.length > 0 || (t.weights?.length ?? 0) > 0
8
+ );
9
+ }
10
+
11
+ export function TokenWall({ catalog }: { catalog: TokenCatalog }) {
12
+ const typo = catalog.typography;
13
+ const weights = typo?.weights ?? [];
14
+
15
+ return (
16
+ <section className="space-y-6">
17
+ <div>
18
+ <h2 className="text-sm font-semibold text-foreground">Colors</h2>
19
+ <p className="mt-1 text-xs text-muted-foreground">
20
+ From Tailwind theme extensions.
21
+ </p>
22
+ <ul className="mt-4 grid gap-3 sm:grid-cols-2">
23
+ {catalog.colors.map((c) => (
24
+ <li
25
+ key={`${c.token}-${c.shade}`}
26
+ className="flex items-center gap-3 rounded-lg border border-border bg-card px-3 py-2"
27
+ title={c.value}
28
+ >
29
+ <svg
30
+ className="h-9 w-9 shrink-0 overflow-hidden rounded border border-border shadow-inner"
31
+ viewBox="0 0 36 36"
32
+ aria-hidden
33
+ >
34
+ <rect width="36" height="36" fill={c.value} />
35
+ </svg>
36
+ <div className="min-w-0">
37
+ <p className="truncate font-mono text-xs text-foreground">
38
+ {c.token}/{c.shade}
39
+ </p>
40
+ <p className="truncate text-xs text-muted-foreground">{c.value}</p>
41
+ <p className="truncate font-mono text-xs text-muted-foreground/70">
42
+ {c.tw}
43
+ </p>
44
+ </div>
45
+ </li>
46
+ ))}
47
+ </ul>
48
+ </div>
49
+
50
+ {hasTypographyContent(catalog) && typo ? (
51
+ <div className="space-y-8">
52
+ <div>
53
+ <h2 className="text-sm font-semibold text-foreground">
54
+ Typography
55
+ </h2>
56
+ <p className="mt-1 text-xs text-muted-foreground">
57
+ Font stacks and Tailwind utilities — preview matches your theme
58
+ tokens.
59
+ </p>
60
+ </div>
61
+
62
+ {typo.families.length > 0 ? (
63
+ <div>
64
+ <h3 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
65
+ Font families
66
+ </h3>
67
+ <ul className="mt-3 grid gap-3 sm:grid-cols-2">
68
+ {typo.families.map((f) => (
69
+ <li
70
+ key={f.key}
71
+ className="rounded-lg border border-border bg-card px-3 py-3"
72
+ title={f.value}
73
+ >
74
+ <p className={`${f.tw} text-sm text-foreground`}>
75
+ The quick brown fox jumps over the lazy dog.
76
+ </p>
77
+ <p className="mt-2 truncate font-mono text-xs text-foreground">
78
+ {f.tw}
79
+ </p>
80
+ <p className="mt-1 break-all text-xs leading-snug text-muted-foreground">
81
+ {f.value}
82
+ </p>
83
+ </li>
84
+ ))}
85
+ </ul>
86
+ </div>
87
+ ) : null}
88
+
89
+ {typo.families.length > 0 && typo.sizes.length > 0 ? (
90
+ typo.families.map((family) => (
91
+ <div key={family.key}>
92
+ <h3 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
93
+ Type scale · {family.tw}
94
+ </h3>
95
+ <ul className="mt-3 divide-y divide-border overflow-x-auto rounded-lg border border-border bg-card">
96
+ {typo.sizes.map((s) => (
97
+ <li
98
+ key={`${family.key}-${s.token}`}
99
+ className="flex flex-wrap items-baseline gap-x-4 gap-y-1 px-3 py-2.5 text-xs sm:flex-nowrap"
100
+ >
101
+ <span className="w-24 shrink-0 font-mono text-foreground/90">
102
+ {s.tw}
103
+ </span>
104
+ <span className="hidden w-36 shrink-0 text-muted-foreground sm:inline">
105
+ {s.value}
106
+ </span>
107
+ <span
108
+ className={`min-w-0 flex-1 truncate ${family.tw} ${s.tw} text-foreground`}
109
+ >
110
+ Aa Bb Cc 0123456789
111
+ </span>
112
+ </li>
113
+ ))}
114
+ </ul>
115
+ </div>
116
+ ))
117
+ ) : typo.sizes.length > 0 ? (
118
+ <div>
119
+ <h3 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
120
+ Type scale · font-sans
121
+ </h3>
122
+ <ul className="mt-3 divide-y divide-border overflow-x-auto rounded-lg border border-border bg-card">
123
+ {typo.sizes.map((s) => (
124
+ <li
125
+ key={s.token}
126
+ className="flex flex-wrap items-baseline gap-x-4 gap-y-1 px-3 py-2.5 text-xs sm:flex-nowrap"
127
+ >
128
+ <span className="w-24 shrink-0 font-mono text-foreground/90">
129
+ {s.tw}
130
+ </span>
131
+ <span className="hidden w-36 shrink-0 text-muted-foreground sm:inline">
132
+ {s.value}
133
+ </span>
134
+ <span
135
+ className={`min-w-0 flex-1 truncate font-sans ${s.tw} text-foreground`}
136
+ >
137
+ Aa Bb Cc 0123456789
138
+ </span>
139
+ </li>
140
+ ))}
141
+ </ul>
142
+ </div>
143
+ ) : null}
144
+
145
+ {typo.families.length > 0 && weights.length > 0 ? (
146
+ typo.families.map((family) => (
147
+ <div key={`w-${family.key}`}>
148
+ <h3 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
149
+ Font weights · {family.tw}
150
+ </h3>
151
+ <ul className="mt-3 divide-y divide-border rounded-lg border border-border bg-card">
152
+ {weights.map((w) => (
153
+ <li
154
+ key={`${family.key}-${w.token}`}
155
+ className="flex flex-wrap items-baseline gap-x-4 gap-y-1 px-3 py-2 text-xs"
156
+ >
157
+ <span className="w-28 shrink-0 font-mono text-foreground/90">
158
+ {w.tw}
159
+ </span>
160
+ <span className="w-10 shrink-0 tabular-nums text-muted-foreground">
161
+ {w.value ?? "—"}
162
+ </span>
163
+ <span
164
+ className={`min-w-0 flex-1 ${family.tw} ${w.tw} text-base text-foreground`}
165
+ >
166
+ Agile beige sharks vex polite judges.
167
+ </span>
168
+ </li>
169
+ ))}
170
+ </ul>
171
+ </div>
172
+ ))
173
+ ) : weights.length > 0 ? (
174
+ <div>
175
+ <h3 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
176
+ Font weights · font-sans
177
+ </h3>
178
+ <ul className="mt-3 divide-y divide-border rounded-lg border border-border bg-card">
179
+ {weights.map((w) => (
180
+ <li
181
+ key={w.token}
182
+ className="flex flex-wrap items-baseline gap-x-4 gap-y-1 px-3 py-2 text-xs"
183
+ >
184
+ <span className="w-28 shrink-0 font-mono text-foreground/90">
185
+ {w.tw}
186
+ </span>
187
+ <span className="w-10 shrink-0 tabular-nums text-muted-foreground">
188
+ {w.value ?? "—"}
189
+ </span>
190
+ <span
191
+ className={`min-w-0 flex-1 font-sans ${w.tw} text-base text-foreground`}
192
+ >
193
+ Agile beige sharks vex polite judges.
194
+ </span>
195
+ </li>
196
+ ))}
197
+ </ul>
198
+ </div>
199
+ ) : null}
200
+ </div>
201
+ ) : null}
202
+
203
+ <div className="grid gap-6 md:grid-cols-2">
204
+ <div>
205
+ <h2 className="text-sm font-semibold text-foreground">Spacing</h2>
206
+ <ul className="mt-3 divide-y divide-border rounded-lg border border-border bg-card">
207
+ {catalog.spacing.map((s) => (
208
+ <li
209
+ key={s.token}
210
+ className="flex justify-between gap-4 px-3 py-2 text-xs"
211
+ >
212
+ <span className="font-mono text-foreground/90">{s.token}</span>
213
+ <span className="text-muted-foreground">{s.value}</span>
214
+ <span className="hidden font-mono text-muted-foreground/70 sm:inline">
215
+ {s.tw}
216
+ </span>
217
+ </li>
218
+ ))}
219
+ </ul>
220
+ </div>
221
+ <div>
222
+ <h2 className="text-sm font-semibold text-foreground">Radius</h2>
223
+ <ul className="mt-3 divide-y divide-border rounded-lg border border-border bg-card">
224
+ {catalog.radius.map((r) => (
225
+ <li
226
+ key={r.token}
227
+ className="flex justify-between gap-4 px-3 py-2 text-xs"
228
+ >
229
+ <span className="font-mono text-foreground/90">{r.token}</span>
230
+ <span className="text-muted-foreground">{r.value}</span>
231
+ <span className="hidden font-mono text-muted-foreground/70 sm:inline">
232
+ {r.tw}
233
+ </span>
234
+ </li>
235
+ ))}
236
+ </ul>
237
+ </div>
238
+ </div>
239
+ </section>
240
+ );
241
+ }
@@ -0,0 +1,73 @@
1
+ import type { ComponentDefinition, UsageSummary, WorkspaceReport } from "../types/report";
2
+
3
+ export interface DefinitionSite {
4
+ kind: ComponentDefinition["kind"];
5
+ path: string;
6
+ line: number;
7
+ }
8
+
9
+ const HIDDEN_COMPONENTS = new Set(["App", "React.StrictMode"]);
10
+
11
+ export function aggregateDefinitions(report: WorkspaceReport): Map<string, DefinitionSite[]> {
12
+ const map = new Map<string, DefinitionSite[]>();
13
+ for (const file of report.files) {
14
+ for (const d of file.definitions) {
15
+ if (HIDDEN_COMPONENTS.has(d.name)) continue;
16
+ const list = map.get(d.name) ?? [];
17
+ list.push({ kind: d.kind, path: file.path, line: d.line });
18
+ map.set(d.name, list);
19
+ }
20
+ }
21
+ for (const [, sites] of map) {
22
+ sites.sort((a, b) => a.path.localeCompare(b.path) || a.line - b.line);
23
+ }
24
+ return map;
25
+ }
26
+
27
+ /** Merges `declared_props` from scan definitions and playground rows (source order, then deduped). */
28
+ export function aggregateDeclaredProps(report: WorkspaceReport): Map<string, string[]> {
29
+ const map = new Map<string, string[]>();
30
+
31
+ const add = (name: string, props: readonly string[] | undefined) => {
32
+ if (HIDDEN_COMPONENTS.has(name)) return;
33
+ if (!props?.length) return;
34
+ let list = map.get(name);
35
+ if (!list) {
36
+ list = [];
37
+ map.set(name, list);
38
+ }
39
+ for (const p of props) {
40
+ if (!list.includes(p)) list.push(p);
41
+ }
42
+ };
43
+
44
+ for (const file of report.files ?? []) {
45
+ for (const d of file.definitions ?? []) {
46
+ add(d.name, d.declared_props);
47
+ }
48
+ }
49
+ for (const pg of report.playgrounds ?? []) {
50
+ add(pg.export_name, pg.declared_props);
51
+ }
52
+
53
+ return map;
54
+ }
55
+
56
+ export function usageMap(report: WorkspaceReport): Map<string, UsageSummary> {
57
+ const m = new Map<string, UsageSummary>();
58
+ for (const row of report.usage_by_component) {
59
+ if (HIDDEN_COMPONENTS.has(row.component)) continue;
60
+ m.set(row.component, row);
61
+ }
62
+ return m;
63
+ }
64
+
65
+ export function catalogComponentNames(
66
+ defs: Map<string, DefinitionSite[]>,
67
+ usages: Map<string, UsageSummary>,
68
+ ): string[] {
69
+ const names = new Set<string>();
70
+ for (const k of defs.keys()) names.add(k);
71
+ for (const k of usages.keys()) names.add(k);
72
+ return [...names].sort((a, b) => a.localeCompare(b));
73
+ }
@@ -0,0 +1,10 @@
1
+ export function shortPath(root: string, fullPath: string): string {
2
+ const norm = (p: string) => p.replace(/\\/g, "/").replace(/\/$/, "");
3
+ const r = norm(root);
4
+ const f = norm(fullPath);
5
+ if (f.startsWith(r + "/")) {
6
+ return f.slice(r.length + 1);
7
+ }
8
+ const parts = f.split("/");
9
+ return parts.slice(-3).join("/");
10
+ }