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.
- package/CHANGELOG.md +76 -0
- package/LICENSE +201 -0
- package/README.md +104 -0
- package/bin/dslinter.mjs +29 -0
- package/components.json +20 -0
- package/package.json +90 -0
- package/src/components/InlineCode.tsx +5 -0
- package/src/components/icons.tsx +121 -0
- package/src/components/ui/badge.tsx +52 -0
- package/src/components/ui/button.tsx +57 -0
- package/src/components/ui/checkbox.tsx +25 -0
- package/src/components/ui/command.tsx +183 -0
- package/src/components/ui/dialog.tsx +156 -0
- package/src/components/ui/hover-card.tsx +42 -0
- package/src/components/ui/input.tsx +22 -0
- package/src/components/ui/label.tsx +19 -0
- package/src/components/ui/select.tsx +149 -0
- package/src/components/ui/table.tsx +118 -0
- package/src/components/ui/toggle-group.tsx +83 -0
- package/src/components/ui/toggle.tsx +45 -0
- package/src/dashboard/ComponentCatalog.tsx +210 -0
- package/src/dashboard/ComponentUsageDetails.tsx +109 -0
- package/src/dashboard/DashboardBody.tsx +71 -0
- package/src/dashboard/FindingsList.tsx +151 -0
- package/src/dashboard/ScoreStrip.tsx +28 -0
- package/src/dashboard/TokenWall.tsx +241 -0
- package/src/dashboard/aggregate.ts +73 -0
- package/src/dashboard/paths.ts +10 -0
- package/src/dashboard/useWorkspaceReport.ts +136 -0
- package/src/index.ts +67 -0
- package/src/lib/utils.ts +6 -0
- package/src/playground/definePlayground.tsx +99 -0
- package/src/playground/enumerateControlCombinations.test.ts +112 -0
- package/src/playground/enumerateControlCombinations.ts +74 -0
- package/src/report/a11yForModule.ts +35 -0
- package/src/report/codeScoreForModule.ts +41 -0
- package/src/report/modulePathMatch.ts +27 -0
- package/src/report/tokenStyleFindingsForModule.ts +24 -0
- package/src/shell/ComponentPlaygroundPane.tsx +438 -0
- package/src/shell/DashboardCommandPalette.tsx +134 -0
- package/src/shell/DashboardLayout.tsx +230 -0
- package/src/shell/EmptyCard.tsx +21 -0
- package/src/shell/GovernancePane.tsx +77 -0
- package/src/shell/PlaygroundA11yAndCode.tsx +387 -0
- package/src/shell/PlaygroundControlField.tsx +213 -0
- package/src/shell/PlaygroundControls.tsx +66 -0
- package/src/shell/PlaygroundUsageCode.tsx +51 -0
- package/src/shell/PlaygroundVariantMatrix.tsx +68 -0
- package/src/shell/Section.tsx +34 -0
- package/src/shell/Sidebar.tsx +203 -0
- package/src/shell/TokensPane.tsx +26 -0
- package/src/shell/controlApiTable.ts +53 -0
- package/src/shell/hashRoute.ts +49 -0
- package/src/shell/playgroundUsageHighlight.ts +53 -0
- package/src/shell/playgroundUsageTwoslash.ts +69 -0
- package/src/shell/useHashRoute.ts +29 -0
- package/src/styles/dashboard-theme.css +188 -0
- package/src/types/controls.ts +62 -0
- package/src/types/defaultTailwindTypography.ts +55 -0
- package/src/types/playground.ts +21 -0
- package/src/types/preview.ts +8 -0
- package/src/types/report.ts +116 -0
- 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
|
+
}
|