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.
- package/CHANGELOG.md +29 -0
- package/bin/lib/infer-prop-types-from-ts.mjs +14 -1
- package/bin/lib/infer-prop-types-from-ts.test.mjs +32 -0
- package/dashboard-dist/assets/{DashboardLayoutAuto-h0gP_iKd.js → DashboardLayoutAuto-BWuyjHPD.js} +1 -1
- package/dashboard-dist/assets/DashboardLayoutAuto-CAO6F6-q.css +1 -0
- package/dashboard-dist/assets/{axe-DDaE9JTN.js → axe-DHHCqGjV.js} +1 -1
- package/dashboard-dist/assets/index-Bxk7tA3F.js +219 -0
- package/dashboard-dist/assets/index-D0O_5w5V.css +1 -0
- package/dashboard-dist/dslinter-report.json +23929 -0
- package/dashboard-dist/index.html +2 -2
- package/index.cjs +52 -52
- package/package.json +6 -6
- package/src/components/CatalogPane.tsx +94 -0
- package/src/components/ComponentInspectPane.tsx +125 -125
- package/src/components/ComponentPlaygroundPane.tsx +42 -27
- package/src/components/DashboardCommandPalette.tsx +9 -0
- package/src/components/GovernanceInventoryTabs.tsx +51 -0
- package/src/components/GovernancePane.tsx +18 -5
- package/src/components/PlaygroundA11yAndCode.tsx +0 -52
- package/src/components/PlaygroundControlField.tsx +2 -0
- package/src/components/ScoreGauge.test.ts +22 -0
- package/src/components/ScoreGauge.tsx +179 -0
- package/src/components/Sidebar.tsx +97 -23
- package/src/components/TokensPane.tsx +11 -13
- package/src/components/controlApiTable.test.ts +15 -0
- package/src/components/controlApiTable.ts +4 -0
- package/src/components/ui/badge.tsx +5 -5
- package/src/dashboard/ComponentCatalog.tsx +10 -1
- package/src/dashboard/ComponentPropUsageDetail.tsx +127 -42
- package/src/dashboard/ComponentUsageDetails.tsx +39 -9
- package/src/dashboard/DashboardBody.tsx +83 -12
- package/src/dashboard/ScannedTokenWall.tsx +9 -6
- package/src/dashboard/UnusedComponentsList.tsx +74 -0
- package/src/dashboard/aggregate.test.ts +381 -12
- package/src/dashboard/aggregate.ts +167 -30
- package/src/dashboard/mergeTokenCatalog.ts +5 -0
- package/src/dashboard/paths.test.ts +18 -1
- package/src/dashboard/paths.ts +8 -0
- package/src/mcp/agent-query.ts +1 -1
- package/src/mcp/css-color.test.ts +52 -0
- package/src/mcp/css-color.ts +73 -0
- package/src/mcp/rule-catalog.json +3 -3
- package/src/mcp/verify-loop.test.ts +24 -0
- package/src/mcp/verify-loop.ts +28 -6
- package/src/playground/buildPlaygroundEntriesFromReport.test.ts +3 -3
- package/src/playground/controls.ts +16 -3
- package/src/playground/enrichKitControls.ts +5 -5
- package/src/playground/inferKitJsx.test.ts +0 -11
- package/src/playground/inferPropTypesFromTs.d.mts +1 -1
- package/src/playground/inferPropTypesFromTs.mjs +19 -3
- package/src/playground/inferPropTypesFromTs.test.ts +32 -0
- package/src/playground/inferPropTypesFromTs.ts +1 -1
- package/src/playground/playgroundJoin.ts +34 -0
- package/src/playground/propCoerce.ts +2 -2
- package/src/playground/snippet.ts +1 -0
- package/src/shell/DashboardLayout.tsx +21 -4
- package/src/shell/hashRoute.test.ts +9 -0
- package/src/shell/hashRoute.ts +6 -0
- package/src/types/controls.ts +12 -0
- package/src/types/report.ts +1 -1
- package/vite/embedTailwindSources.ts +8 -6
- package/dashboard-dist/assets/DashboardLayoutAuto-Bja3BuZZ.css +0 -1
- package/dashboard-dist/assets/index-B9sZ6wHm.css +0 -1
- package/dashboard-dist/assets/index-DIDBt5ed.js +0 -218
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
import { Badge } from "../components/ui/badge";
|
|
2
|
+
import {
|
|
3
|
+
Table,
|
|
4
|
+
TableBody,
|
|
5
|
+
TableCell,
|
|
6
|
+
TableHead,
|
|
7
|
+
TableHeader,
|
|
8
|
+
TableRow,
|
|
9
|
+
} from "../components/ui/table";
|
|
10
|
+
import { cn } from "../lib/utils";
|
|
1
11
|
import type { WorkspaceReport } from "../types/report";
|
|
2
12
|
import { pluralize } from "usemods";
|
|
3
13
|
|
|
@@ -8,6 +18,16 @@ export function catalogAttributeProps(props: string[]): string[] {
|
|
|
8
18
|
return props.filter((prop) => !SLOT_PROPS.has(prop));
|
|
9
19
|
}
|
|
10
20
|
|
|
21
|
+
export function propFrequenciesForComponent(
|
|
22
|
+
report: WorkspaceReport,
|
|
23
|
+
componentName: string,
|
|
24
|
+
): Record<string, number> {
|
|
25
|
+
const usageRow = (report.usage_by_component ?? []).find(
|
|
26
|
+
(u) => u.component === componentName,
|
|
27
|
+
);
|
|
28
|
+
return usageRow?.prop_frequencies ?? {};
|
|
29
|
+
}
|
|
30
|
+
|
|
11
31
|
/** Set of `"ComponentName/propName"` keys for props with no recorded usage. */
|
|
12
32
|
export function buildUnusedPropSetForComponent(
|
|
13
33
|
report: WorkspaceReport,
|
|
@@ -15,10 +35,7 @@ export function buildUnusedPropSetForComponent(
|
|
|
15
35
|
declared: string[],
|
|
16
36
|
): Set<string> {
|
|
17
37
|
const s = new Set<string>();
|
|
18
|
-
const
|
|
19
|
-
(u) => u.component === componentName,
|
|
20
|
-
);
|
|
21
|
-
const propFrequencies = usageRow?.prop_frequencies ?? {};
|
|
38
|
+
const propFrequencies = propFrequenciesForComponent(report, componentName);
|
|
22
39
|
for (const propName of catalogAttributeProps(declared)) {
|
|
23
40
|
if ((propFrequencies[propName] ?? 0) === 0) {
|
|
24
41
|
s.add(`${componentName}/${propName}`);
|
|
@@ -27,23 +44,106 @@ export function buildUnusedPropSetForComponent(
|
|
|
27
44
|
return s;
|
|
28
45
|
}
|
|
29
46
|
|
|
47
|
+
function sortedAttributeProps(declared: string[]): string[] {
|
|
48
|
+
return [...catalogAttributeProps(declared)].sort((a, b) =>
|
|
49
|
+
a.localeCompare(b),
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function PropUsageSummary({
|
|
54
|
+
usedPropCount,
|
|
55
|
+
totalPropCount,
|
|
56
|
+
}: {
|
|
57
|
+
usedPropCount: number;
|
|
58
|
+
totalPropCount: number;
|
|
59
|
+
}) {
|
|
60
|
+
return (
|
|
61
|
+
<p className="text-sm text-muted-foreground">
|
|
62
|
+
{usedPropCount}/{totalPropCount} {pluralize("prop", usedPropCount)} used
|
|
63
|
+
in the workspace snapshot.
|
|
64
|
+
</p>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function PropUsageTable({
|
|
69
|
+
props,
|
|
70
|
+
propFrequencies,
|
|
71
|
+
}: {
|
|
72
|
+
props: string[];
|
|
73
|
+
propFrequencies: Record<string, number>;
|
|
74
|
+
}) {
|
|
75
|
+
return (
|
|
76
|
+
<Table>
|
|
77
|
+
<TableHeader>
|
|
78
|
+
<TableRow>
|
|
79
|
+
<TableHead>Prop</TableHead>
|
|
80
|
+
<TableHead className="w-20 text-right">Uses</TableHead>
|
|
81
|
+
</TableRow>
|
|
82
|
+
</TableHeader>
|
|
83
|
+
<TableBody>
|
|
84
|
+
{props.map((prop) => {
|
|
85
|
+
const count = propFrequencies[prop] ?? 0;
|
|
86
|
+
const isUnused = count === 0;
|
|
87
|
+
return (
|
|
88
|
+
<TableRow
|
|
89
|
+
key={prop}
|
|
90
|
+
className={isUnused ? "text-muted-foreground" : undefined}
|
|
91
|
+
>
|
|
92
|
+
<TableCell className="font-mono text-xs">{prop}</TableCell>
|
|
93
|
+
<TableCell className="text-right tabular-nums">{count}</TableCell>
|
|
94
|
+
</TableRow>
|
|
95
|
+
);
|
|
96
|
+
})}
|
|
97
|
+
</TableBody>
|
|
98
|
+
</Table>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function PropUsageBadges({
|
|
103
|
+
props,
|
|
104
|
+
propFrequencies,
|
|
105
|
+
}: {
|
|
106
|
+
props: string[];
|
|
107
|
+
propFrequencies: Record<string, number>;
|
|
108
|
+
}) {
|
|
109
|
+
return (
|
|
110
|
+
<div className="flex flex-wrap gap-1">
|
|
111
|
+
{props.map((prop) => {
|
|
112
|
+
const count = propFrequencies[prop] ?? 0;
|
|
113
|
+
const isUsed = count > 0;
|
|
114
|
+
return (
|
|
115
|
+
<Badge
|
|
116
|
+
key={prop}
|
|
117
|
+
variant={isUsed ? "secondary" : "outline"}
|
|
118
|
+
size="sm"
|
|
119
|
+
className={cn(!isUsed && "text-muted-foreground")}
|
|
120
|
+
title={isUsed ? `${count} uses` : "Never passed"}
|
|
121
|
+
>
|
|
122
|
+
{prop}
|
|
123
|
+
</Badge>
|
|
124
|
+
);
|
|
125
|
+
})}
|
|
126
|
+
</div>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
30
130
|
export function ComponentPropUsageDetail({
|
|
31
131
|
component,
|
|
32
132
|
declared,
|
|
33
133
|
unusedProps,
|
|
134
|
+
propFrequencies = {},
|
|
135
|
+
variant = "table",
|
|
34
136
|
}: {
|
|
35
137
|
component: string;
|
|
36
138
|
declared: string[];
|
|
37
139
|
unusedProps: Set<string>;
|
|
140
|
+
propFrequencies?: Record<string, number>;
|
|
141
|
+
variant?: "table" | "compact";
|
|
38
142
|
}) {
|
|
39
|
-
const attributeProps =
|
|
40
|
-
const
|
|
143
|
+
const attributeProps = sortedAttributeProps(declared);
|
|
144
|
+
const usedPropCount = attributeProps.filter(
|
|
41
145
|
(prop) => !unusedProps.has(`${component}/${prop}`),
|
|
42
|
-
);
|
|
43
|
-
const unused = attributeProps.filter((prop) =>
|
|
44
|
-
unusedProps.has(`${component}/${prop}`),
|
|
45
|
-
);
|
|
46
|
-
const usedPropCount = used.length;
|
|
146
|
+
).length;
|
|
47
147
|
|
|
48
148
|
if (attributeProps.length === 0) {
|
|
49
149
|
return (
|
|
@@ -54,37 +154,22 @@ export function ComponentPropUsageDetail({
|
|
|
54
154
|
}
|
|
55
155
|
|
|
56
156
|
return (
|
|
57
|
-
<div className="space-y-4">
|
|
58
|
-
<
|
|
59
|
-
{usedPropCount}
|
|
60
|
-
{
|
|
61
|
-
|
|
62
|
-
{
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
) : null}
|
|
74
|
-
{unused.length > 0 ? (
|
|
75
|
-
<div>
|
|
76
|
-
<p className="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
|
|
77
|
-
Never passed
|
|
78
|
-
</p>
|
|
79
|
-
<ul className="mt-1 space-y-0.5 font-mono text-xs text-muted-foreground/70">
|
|
80
|
-
{unused.map((prop) => (
|
|
81
|
-
<li key={prop} className="line-through">
|
|
82
|
-
{prop}
|
|
83
|
-
</li>
|
|
84
|
-
))}
|
|
85
|
-
</ul>
|
|
86
|
-
</div>
|
|
87
|
-
) : null}
|
|
157
|
+
<div className={variant === "compact" ? "space-y-2" : "space-y-4"}>
|
|
158
|
+
<PropUsageSummary
|
|
159
|
+
usedPropCount={usedPropCount}
|
|
160
|
+
totalPropCount={attributeProps.length}
|
|
161
|
+
/>
|
|
162
|
+
{variant === "compact" ? (
|
|
163
|
+
<PropUsageBadges
|
|
164
|
+
props={attributeProps}
|
|
165
|
+
propFrequencies={propFrequencies}
|
|
166
|
+
/>
|
|
167
|
+
) : (
|
|
168
|
+
<PropUsageTable
|
|
169
|
+
props={attributeProps}
|
|
170
|
+
propFrequencies={propFrequencies}
|
|
171
|
+
/>
|
|
172
|
+
)}
|
|
88
173
|
</div>
|
|
89
174
|
);
|
|
90
175
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useMemo } from "react";
|
|
1
|
+
import { useCallback, useMemo } from "react";
|
|
2
2
|
import {
|
|
3
3
|
Table,
|
|
4
4
|
TableBody,
|
|
@@ -9,7 +9,8 @@ import {
|
|
|
9
9
|
} from "../components/ui/table";
|
|
10
10
|
import type { UsageLocation, WorkspaceReport } from "../types/report";
|
|
11
11
|
import { usageMap } from "./aggregate";
|
|
12
|
-
import {
|
|
12
|
+
import { openSourceFile } from "./editorLink";
|
|
13
|
+
import { resolveReportAbsolutePath, shortPath } from "./paths";
|
|
13
14
|
import { EmptyCard } from "../components/EmptyCard";
|
|
14
15
|
import { TruncatedPath } from "../components/TruncatedPath";
|
|
15
16
|
|
|
@@ -32,6 +33,40 @@ function sortedLocations(
|
|
|
32
33
|
return list;
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
function UsageLocationLink({
|
|
37
|
+
root,
|
|
38
|
+
loc,
|
|
39
|
+
}: {
|
|
40
|
+
root: string;
|
|
41
|
+
loc: UsageLocation;
|
|
42
|
+
}) {
|
|
43
|
+
const fileText = shortPath(root, loc.path);
|
|
44
|
+
const locationText = `${fileText}:${loc.line}`;
|
|
45
|
+
const absolutePath = resolveReportAbsolutePath(root, loc.path);
|
|
46
|
+
|
|
47
|
+
const handleClick = useCallback(() => {
|
|
48
|
+
void openSourceFile(absolutePath, loc.line).catch((err) => {
|
|
49
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
50
|
+
window.alert(`Could not open file: ${message}`);
|
|
51
|
+
});
|
|
52
|
+
}, [absolutePath, loc.line]);
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<button
|
|
56
|
+
type="button"
|
|
57
|
+
onClick={handleClick}
|
|
58
|
+
className="block min-w-0 w-full text-left text-xs text-muted-foreground transition-colors hover:text-foreground hover:underline"
|
|
59
|
+
title={locationText}
|
|
60
|
+
>
|
|
61
|
+
<TruncatedPath
|
|
62
|
+
path={`${fileText}:${loc.line}`}
|
|
63
|
+
className="text-xs"
|
|
64
|
+
title={undefined}
|
|
65
|
+
/>
|
|
66
|
+
</button>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
35
70
|
export function ComponentUsageDetails({
|
|
36
71
|
report,
|
|
37
72
|
componentId,
|
|
@@ -72,22 +107,17 @@ export function ComponentUsageDetails({
|
|
|
72
107
|
<Table className="[&>table]:table-fixed [&>table]:w-full">
|
|
73
108
|
<TableHeader>
|
|
74
109
|
<TableRow>
|
|
75
|
-
<TableHead className="w-[40%] min-w-0">
|
|
76
|
-
<TableHead className="w-14 whitespace-nowrap">Line</TableHead>
|
|
110
|
+
<TableHead className="w-[40%] min-w-0">Location</TableHead>
|
|
77
111
|
<TableHead className="min-w-0">Props at this call site</TableHead>
|
|
78
112
|
</TableRow>
|
|
79
113
|
</TableHeader>
|
|
80
114
|
<TableBody>
|
|
81
115
|
{rows.map((loc, i) => {
|
|
82
|
-
const fileText = shortPath(report.root, loc.path);
|
|
83
116
|
const propsText = formatCallSiteProps(loc);
|
|
84
117
|
return (
|
|
85
118
|
<TableRow key={`${loc.path}-${loc.line}-${i}`}>
|
|
86
119
|
<TableCell className="min-w-0">
|
|
87
|
-
<
|
|
88
|
-
</TableCell>
|
|
89
|
-
<TableCell className="tabular-nums text-muted-foreground">
|
|
90
|
-
{loc.line}
|
|
120
|
+
<UsageLocationLink root={report.root} loc={loc} />
|
|
91
121
|
</TableCell>
|
|
92
122
|
<TableCell className="min-w-0">
|
|
93
123
|
<span
|
|
@@ -1,16 +1,71 @@
|
|
|
1
|
+
import { useMemo, useState } from "react";
|
|
2
|
+
import { GovernanceInventoryTabs } from "../components/GovernanceInventoryTabs";
|
|
1
3
|
import { Section } from "../components/Section";
|
|
2
4
|
import type { WorkspaceReport } from "../types/report";
|
|
3
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
findingsForGovernanceTab,
|
|
7
|
+
governanceTabCounts,
|
|
8
|
+
type GovernanceInventoryTab,
|
|
9
|
+
unusedComponentsFromReport,
|
|
10
|
+
} from "./aggregate";
|
|
4
11
|
import { FindingsList } from "./FindingsList";
|
|
5
12
|
import { ScoreStrip } from "./ScoreStrip";
|
|
13
|
+
import { UnusedComponentsList } from "./UnusedComponentsList";
|
|
14
|
+
|
|
15
|
+
const sectionMeta: Record<
|
|
16
|
+
GovernanceInventoryTab,
|
|
17
|
+
{ id: string; title: string; description: string }
|
|
18
|
+
> = {
|
|
19
|
+
all: {
|
|
20
|
+
id: "issues",
|
|
21
|
+
title: "Issues",
|
|
22
|
+
description: "All findings from the workspace DSLinter report.",
|
|
23
|
+
},
|
|
24
|
+
a11y: {
|
|
25
|
+
id: "accessibility-issues",
|
|
26
|
+
title: "Accessibility issues",
|
|
27
|
+
description: "Findings from accessibility rules in the latest snapshot.",
|
|
28
|
+
},
|
|
29
|
+
code: {
|
|
30
|
+
id: "code-issues",
|
|
31
|
+
title: "Code quality issues",
|
|
32
|
+
description: "Findings from code quality rules in the latest snapshot.",
|
|
33
|
+
},
|
|
34
|
+
token: {
|
|
35
|
+
id: "token-issues",
|
|
36
|
+
title: "Token issues",
|
|
37
|
+
description: "Findings from design token rules in the latest snapshot.",
|
|
38
|
+
},
|
|
39
|
+
unused: {
|
|
40
|
+
id: "unused-components",
|
|
41
|
+
title: "Unused components",
|
|
42
|
+
description:
|
|
43
|
+
"Scanned definitions with no JSX references elsewhere in the workspace.",
|
|
44
|
+
},
|
|
45
|
+
};
|
|
6
46
|
|
|
7
47
|
export function DashboardBody({
|
|
8
48
|
report,
|
|
9
49
|
onOpenComponent,
|
|
50
|
+
onOpenCatalog,
|
|
10
51
|
}: {
|
|
11
52
|
report: WorkspaceReport;
|
|
12
53
|
onOpenComponent?: (name: string) => void;
|
|
54
|
+
onOpenCatalog?: () => void;
|
|
13
55
|
}) {
|
|
56
|
+
const [tab, setTab] = useState<GovernanceInventoryTab>("all");
|
|
57
|
+
|
|
58
|
+
const counts = useMemo(() => governanceTabCounts(report), [report]);
|
|
59
|
+
const filteredFindings = useMemo(
|
|
60
|
+
() => findingsForGovernanceTab(report, tab),
|
|
61
|
+
[report, tab],
|
|
62
|
+
);
|
|
63
|
+
const unusedComponents = useMemo(
|
|
64
|
+
() => unusedComponentsFromReport(report),
|
|
65
|
+
[report],
|
|
66
|
+
);
|
|
67
|
+
const section = sectionMeta[tab];
|
|
68
|
+
|
|
14
69
|
return (
|
|
15
70
|
<div className="space-y-10">
|
|
16
71
|
<ScoreStrip scores={report.scores} />
|
|
@@ -22,21 +77,37 @@ export function DashboardBody({
|
|
|
22
77
|
</div>
|
|
23
78
|
) : null}
|
|
24
79
|
|
|
25
|
-
<
|
|
26
|
-
id="components"
|
|
27
|
-
title="Components"
|
|
28
|
-
description="Definitions and JSX usage from the latest snapshot."
|
|
29
|
-
>
|
|
30
|
-
<ComponentCatalog report={report} onOpenComponent={onOpenComponent} />
|
|
31
|
-
</Section>
|
|
80
|
+
<GovernanceInventoryTabs value={tab} onChange={setTab} counts={counts} />
|
|
32
81
|
|
|
33
82
|
<Section
|
|
34
|
-
id=
|
|
35
|
-
title=
|
|
36
|
-
description=
|
|
83
|
+
id={section.id}
|
|
84
|
+
title={section.title}
|
|
85
|
+
description={section.description}
|
|
37
86
|
>
|
|
38
|
-
|
|
87
|
+
{tab === "unused" ? (
|
|
88
|
+
<UnusedComponentsList
|
|
89
|
+
components={unusedComponents}
|
|
90
|
+
root={report.root}
|
|
91
|
+
onOpenComponent={onOpenComponent}
|
|
92
|
+
/>
|
|
93
|
+
) : (
|
|
94
|
+
<FindingsList findings={filteredFindings} root={report.root} />
|
|
95
|
+
)}
|
|
39
96
|
</Section>
|
|
97
|
+
|
|
98
|
+
{onOpenCatalog ? (
|
|
99
|
+
<p className="text-sm text-muted-foreground">
|
|
100
|
+
<button
|
|
101
|
+
type="button"
|
|
102
|
+
onClick={onOpenCatalog}
|
|
103
|
+
className="font-medium text-foreground underline decoration-dotted underline-offset-2 transition hover:decoration-solid"
|
|
104
|
+
>
|
|
105
|
+
View all components
|
|
106
|
+
</button>
|
|
107
|
+
{" "}
|
|
108
|
+
for prop usage and app reference details.
|
|
109
|
+
</p>
|
|
110
|
+
) : null}
|
|
40
111
|
</div>
|
|
41
112
|
);
|
|
42
113
|
}
|
|
@@ -8,6 +8,7 @@ import { cn } from "../lib/utils";
|
|
|
8
8
|
import { TruncatedPath } from "../components/TruncatedPath";
|
|
9
9
|
import {
|
|
10
10
|
filterTokenRows,
|
|
11
|
+
scannedTokenRowKey,
|
|
11
12
|
type MergedTokenView,
|
|
12
13
|
type ScannedTokenRow,
|
|
13
14
|
type TokenUsageFilter,
|
|
@@ -40,13 +41,13 @@ function TokenSection({
|
|
|
40
41
|
function TokenUsageBadge({ row }: { row: ScannedTokenRow }) {
|
|
41
42
|
if (row.isUnused) {
|
|
42
43
|
return (
|
|
43
|
-
<span className="shrink-0 rounded-full bg-muted px-2 py-0.5 text-
|
|
44
|
+
<span className="shrink-0 rounded-full bg-muted px-2 py-0.5 text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
|
44
45
|
unused
|
|
45
46
|
</span>
|
|
46
47
|
);
|
|
47
48
|
}
|
|
48
49
|
return (
|
|
49
|
-
<span className="shrink-0 rounded-full bg-primary/10 px-2 py-0.5 text-
|
|
50
|
+
<span className="shrink-0 rounded-full bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary">
|
|
50
51
|
{row.fileCount} {row.fileCount === 1 ? "file" : "files"}
|
|
51
52
|
</span>
|
|
52
53
|
);
|
|
@@ -95,7 +96,9 @@ function TokenRowBody({
|
|
|
95
96
|
}) {
|
|
96
97
|
return (
|
|
97
98
|
<div className={cn("min-w-0", className)}>
|
|
98
|
-
<p className="truncate font-mono text-xs text-foreground">
|
|
99
|
+
<p className="truncate font-mono text-xs text-foreground">
|
|
100
|
+
{row.cssName}
|
|
101
|
+
</p>
|
|
99
102
|
<p className="truncate text-xs text-muted-foreground">{row.value}</p>
|
|
100
103
|
{row.tw ? (
|
|
101
104
|
<p className="truncate font-mono text-xs text-muted-foreground/70">
|
|
@@ -118,8 +121,8 @@ function ColorSection({ rows }: { rows: ScannedTokenRow[] }) {
|
|
|
118
121
|
<ul className="mt-4 grid gap-3 sm:grid-cols-2">
|
|
119
122
|
{colors.map((row) => (
|
|
120
123
|
<li
|
|
121
|
-
key={row
|
|
122
|
-
className="flex items-center gap-3 rounded-lg border border-border bg-card
|
|
124
|
+
key={scannedTokenRowKey(row)}
|
|
125
|
+
className="flex items-center gap-3 rounded-lg border border-border bg-card p-2 pr-3.5"
|
|
123
126
|
title={row.value}
|
|
124
127
|
>
|
|
125
128
|
{row.displayValue &&
|
|
@@ -162,7 +165,7 @@ function ListSection({
|
|
|
162
165
|
<ul className="mt-3 divide-y divide-border rounded-lg border border-border bg-card">
|
|
163
166
|
{items.map((row) => (
|
|
164
167
|
<li
|
|
165
|
-
key={row
|
|
168
|
+
key={scannedTokenRowKey(row)}
|
|
166
169
|
className="flex flex-wrap items-center justify-between gap-2 px-3 py-2 text-xs"
|
|
167
170
|
>
|
|
168
171
|
<TokenRowBody row={row} className="flex-1" />
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Table,
|
|
3
|
+
TableBody,
|
|
4
|
+
TableCell,
|
|
5
|
+
TableHead,
|
|
6
|
+
TableHeader,
|
|
7
|
+
TableRow,
|
|
8
|
+
} from "../components/ui/table";
|
|
9
|
+
import { TruncatedPath } from "../components/TruncatedPath";
|
|
10
|
+
import type { UnusedComponent } from "./aggregate";
|
|
11
|
+
import { shortPath } from "./paths";
|
|
12
|
+
import { pluralize } from "usemods";
|
|
13
|
+
|
|
14
|
+
export function UnusedComponentsList({
|
|
15
|
+
components,
|
|
16
|
+
root,
|
|
17
|
+
onOpenComponent,
|
|
18
|
+
}: {
|
|
19
|
+
components: UnusedComponent[];
|
|
20
|
+
root: string;
|
|
21
|
+
onOpenComponent?: (name: string) => void;
|
|
22
|
+
}) {
|
|
23
|
+
if (components.length === 0) {
|
|
24
|
+
return (
|
|
25
|
+
<p className="rounded-lg border border-dashed border-border bg-muted/30 px-4 py-8 text-center text-sm text-muted-foreground">
|
|
26
|
+
No unused components — every scanned definition has at least one JSX
|
|
27
|
+
reference in the workspace.
|
|
28
|
+
</p>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<Table>
|
|
34
|
+
<TableHeader>
|
|
35
|
+
<TableRow>
|
|
36
|
+
<TableHead scope="col">Component</TableHead>
|
|
37
|
+
<TableHead scope="col">Defined in</TableHead>
|
|
38
|
+
</TableRow>
|
|
39
|
+
</TableHeader>
|
|
40
|
+
<TableBody>
|
|
41
|
+
{components.map(({ name, definitionPaths }) => (
|
|
42
|
+
<TableRow key={name}>
|
|
43
|
+
<TableCell>
|
|
44
|
+
{onOpenComponent ? (
|
|
45
|
+
<button
|
|
46
|
+
type="button"
|
|
47
|
+
onClick={() => onOpenComponent(name)}
|
|
48
|
+
className="text-left font-medium text-foreground underline decoration-transparent underline-offset-2 transition hover:decoration-current"
|
|
49
|
+
>
|
|
50
|
+
{name}
|
|
51
|
+
</button>
|
|
52
|
+
) : (
|
|
53
|
+
name
|
|
54
|
+
)}
|
|
55
|
+
</TableCell>
|
|
56
|
+
<TableCell className="min-w-0 font-mono text-xs text-muted-foreground">
|
|
57
|
+
{definitionPaths.length === 1 ? (
|
|
58
|
+
<TruncatedPath
|
|
59
|
+
path={shortPath(root, definitionPaths[0]!)}
|
|
60
|
+
className="text-xs"
|
|
61
|
+
/>
|
|
62
|
+
) : (
|
|
63
|
+
<span title={definitionPaths.map((p) => shortPath(root, p)).join("\n")}>
|
|
64
|
+
{definitionPaths.length}{" "}
|
|
65
|
+
{pluralize("file", definitionPaths.length)}
|
|
66
|
+
</span>
|
|
67
|
+
)}
|
|
68
|
+
</TableCell>
|
|
69
|
+
</TableRow>
|
|
70
|
+
))}
|
|
71
|
+
</TableBody>
|
|
72
|
+
</Table>
|
|
73
|
+
);
|
|
74
|
+
}
|