dslinter 0.1.13 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -0
- package/README.md +50 -29
- package/bin/dslinter.mjs +26 -5
- package/bin/lib/config-hide-component.mjs +44 -0
- package/bin/lib/config-hide-component.test.mjs +33 -0
- package/bin/lib/constants.mjs +20 -0
- package/bin/lib/dev-banner.mjs +16 -51
- package/bin/lib/dev-banner.test.mjs +20 -18
- package/bin/lib/enrich-playgrounds-from-ts.mjs +201 -0
- package/bin/lib/enrich-playgrounds-from-ts.test.mjs +74 -0
- package/bin/lib/enrich-report-cli.mjs +14 -0
- package/bin/lib/env.mjs +20 -0
- package/bin/lib/infer-prop-types-from-ts.mjs +381 -0
- package/bin/lib/infer-prop-types-from-ts.test.mjs +174 -0
- package/bin/lib/parse-args.mjs +13 -1
- package/bin/lib/parse-args.test.mjs +7 -1
- package/bin/lib/paths.mjs +8 -0
- package/bin/lib/project-root.mjs +72 -10
- package/bin/lib/project-root.test.mjs +32 -1
- package/bin/lib/prompt.mjs +31 -0
- package/bin/lib/resolve-project.mjs +78 -0
- package/bin/lib/resolve-project.test.mjs +74 -0
- package/bin/lib/run-scanner.mjs +40 -6
- package/bin/lib/scaffold-config.mjs +96 -8
- package/bin/lib/scaffold-config.test.mjs +12 -2
- package/bin/lib/scan-host.mjs +44 -0
- package/bin/lib/scan-host.test.mjs +41 -0
- package/bin/lib/setup-readiness.mjs +153 -0
- package/bin/lib/setup-readiness.test.mjs +32 -0
- package/bin/modes/build.mjs +31 -6
- package/bin/modes/dev.mjs +55 -21
- package/bin/modes/init.mjs +3 -22
- package/bin/modes/init.test.mjs +1 -1
- package/bin/modes/mcp.mjs +49 -0
- package/bin/modes/report.mjs +29 -4
- package/bin/modes/watch.mjs +85 -0
- package/dashboard-dist/assets/DashboardLayoutAuto-Bja3BuZZ.css +1 -0
- package/dashboard-dist/assets/DashboardLayoutAuto-h0gP_iKd.js +1 -0
- package/dashboard-dist/assets/axe-DDaE9JTN.js +20 -0
- package/dashboard-dist/assets/index-B9sZ6wHm.css +1 -0
- package/dashboard-dist/assets/index-DIDBt5ed.js +218 -0
- package/dashboard-dist/index.html +2 -2
- package/index.cjs +53 -52
- package/index.d.ts +3 -0
- package/package.json +18 -12
- package/shared/env.ts +15 -0
- package/shared/paths.ts +8 -0
- package/shared/reportPath.test.ts +19 -0
- package/shared/reportPath.ts +12 -0
- package/shared/servePort.ts +16 -0
- package/src/components/ComponentInspectPane.tsx +67 -19
- package/src/components/ComponentPlaygroundPane.tsx +262 -113
- package/src/components/DashboardCommandPalette.tsx +6 -11
- package/src/components/GovernancePane.tsx +2 -2
- package/src/components/HideFromCatalogButton.tsx +44 -0
- package/src/components/OpenInEditorButton.tsx +36 -0
- package/src/components/PlaygroundA11yAndCode.tsx +53 -53
- package/src/components/PlaygroundAppThemeWrapper.tsx +82 -0
- package/src/components/PlaygroundControls.tsx +5 -11
- package/src/components/PlaygroundPreviewErrorBoundary.tsx +54 -0
- package/src/components/PlaygroundUsageCode.tsx +6 -4
- package/src/components/PlaygroundVariantMatrix.tsx +101 -34
- package/src/components/Section.tsx +5 -2
- package/src/components/Sidebar.tsx +131 -46
- package/src/components/TruncatedPath.tsx +44 -0
- package/src/components/controlApiTable.test.ts +29 -0
- package/src/components/controlApiTable.ts +3 -0
- package/src/components/playgroundUsageHighlight.ts +14 -3
- package/src/components/ui/badge.tsx +1 -1
- package/src/components/ui/table.tsx +2 -2
- package/src/dashboard/ComponentCatalog.tsx +16 -23
- package/src/dashboard/ComponentUsageDetails.tsx +6 -15
- package/src/dashboard/DashboardBody.tsx +0 -35
- package/src/dashboard/FindingsList.tsx +65 -55
- package/src/dashboard/ScannedTokenWall.tsx +3 -3
- package/src/dashboard/aggregate.test.ts +74 -0
- package/src/dashboard/aggregate.ts +145 -21
- package/src/dashboard/catalogVisibility.test.ts +93 -0
- package/src/dashboard/catalogVisibility.ts +108 -0
- package/src/dashboard/editorLink.test.ts +57 -0
- package/src/dashboard/editorLink.ts +71 -0
- package/src/dashboard/paths.test.ts +49 -0
- package/src/dashboard/paths.ts +51 -3
- package/src/dashboard/updateDslintConfig.ts +22 -0
- package/src/dashboard/useWorkspaceReport.ts +21 -17
- package/src/index.ts +26 -0
- package/src/mcp/agent-context.ts +148 -0
- package/src/mcp/agent-query.test.ts +89 -0
- package/src/mcp/agent-query.ts +373 -0
- package/src/mcp/config.ts +53 -0
- package/src/mcp/index.ts +18 -0
- package/src/mcp/normalize-paths.ts +65 -0
- package/src/mcp/report-cache.ts +209 -0
- package/src/mcp/rule-catalog.json +156 -0
- package/src/mcp/rule-catalog.ts +33 -0
- package/src/mcp/schemas.ts +54 -0
- package/src/mcp/server.test.ts +44 -0
- package/src/mcp/server.ts +343 -0
- package/src/mcp/start.ts +29 -0
- package/src/mcp/verify-loop.test.ts +49 -0
- package/src/mcp/verify-loop.ts +149 -0
- package/src/playground/appPreviewTheme.test.ts +148 -0
- package/src/playground/appPreviewTheme.ts +137 -0
- package/src/playground/buildCompoundPlaygroundEntries.test.ts +348 -0
- package/src/playground/buildCompoundPlaygroundEntries.ts +625 -0
- package/src/playground/buildPlaygroundEntriesFromReport.test.ts +420 -6
- package/src/playground/buildPlaygroundEntriesFromReport.ts +206 -285
- package/src/playground/catalogIdFromPlaygroundExport.test.ts +15 -0
- package/src/playground/catalogIdFromPlaygroundExport.ts +8 -0
- package/src/playground/collectDefinedPlaygrounds.test.ts +59 -0
- package/src/playground/collectDefinedPlaygrounds.ts +68 -0
- package/src/playground/controls.ts +177 -0
- package/src/playground/createPlaygroundRegistry.ts +1 -1
- package/src/playground/definePlayground.tsx +88 -16
- package/src/playground/definePlaygroundFromKit.ts +17 -0
- package/src/playground/embedGlobKey.ts +8 -0
- package/src/playground/enrichKitControls.test.ts +25 -0
- package/src/playground/enrichKitControls.ts +197 -0
- package/src/playground/expandPlaygroundControls.test.ts +50 -0
- package/src/playground/expandPlaygroundControls.ts +97 -0
- package/src/playground/inferKitJsx.test.ts +77 -0
- package/src/playground/inferKitJsx.ts +165 -0
- package/src/playground/inferKitParams.test.ts +41 -0
- package/src/playground/inferKitParams.ts +113 -0
- package/src/playground/inferPropTypesFromTs.d.mts +47 -0
- package/src/playground/inferPropTypesFromTs.mjs +343 -0
- package/src/playground/inferPropTypesFromTs.test.ts +227 -0
- package/src/playground/inferPropTypesFromTs.ts +17 -0
- package/src/playground/mergePlaygroundEntries.test.ts +32 -0
- package/src/playground/mergePlaygroundEntries.ts +28 -0
- package/src/playground/playgroundJoin.test.ts +79 -19
- package/src/playground/playgroundJoin.ts +47 -22
- package/src/playground/playgroundModuleExport.test.ts +42 -0
- package/src/playground/playgroundModuleExport.ts +22 -0
- package/src/playground/playgroundSpecsKey.ts +8 -0
- package/src/playground/propCoerce.ts +91 -0
- package/src/playground/scanVariantA11y.test.ts +46 -0
- package/src/playground/scanVariantA11y.ts +107 -0
- package/src/playground/snippet.ts +83 -0
- package/src/playground/usePlaygroundFromReport.test.ts +18 -8
- package/src/playground/usePlaygroundFromReport.ts +3 -1
- package/src/report/a11yForModule.ts +2 -7
- package/src/report/a11yScoring.test.ts +24 -0
- package/src/report/a11yScoring.ts +17 -0
- package/src/report/index.ts +6 -0
- package/src/shell/DashboardLayout.tsx +71 -45
- package/src/shell/DashboardLayoutAuto.tsx +0 -4
- package/src/shell/hashRoute.test.ts +7 -15
- package/src/shell/hashRoute.ts +31 -31
- package/src/shell/useHashRoute.ts +38 -13
- package/src/styles/dashboard-theme.css +18 -7
- package/src/types/controls.ts +11 -0
- package/src/types/playground.ts +4 -0
- package/src/types/report.ts +32 -9
- package/templates/playground/buildRegistry.ts +1 -1
- package/templates/vite.dslinter.snippet.ts +15 -4
- package/vite/collectScanModules.test.ts +51 -3
- package/vite/collectScanModules.ts +85 -29
- package/vite/consumer.config.mjs +6 -3
- package/vite/consumerAlias.test.ts +47 -0
- package/vite/consumerAlias.ts +114 -0
- package/vite/embedTailwindSources.test.ts +74 -0
- package/vite/embedTailwindSources.ts +97 -0
- package/vite/loadConsumerAliases.test.ts +131 -0
- package/vite/loadConsumerAliases.ts +155 -0
- package/vite/openFileInEditor.mjs +196 -0
- package/vite/openFileInEditor.test.mjs +87 -0
- package/vite/plugin.resolve.test.ts +72 -0
- package/vite/plugin.ts +216 -19
- package/vite/reportPath.test.ts +19 -0
- package/vite/resolveWayfinderImport.ts +56 -0
- package/vite/shims/inertia-react.tsx +85 -0
- package/vite/shims/wayfinder-actions.ts +33 -0
- package/vite/shims/wayfinder-routes.ts +30 -0
- package/vite/shims/ziggy-js.ts +12 -0
- package/dashboard-dist/assets/DashboardLayoutAuto-Bm7yfyC-.css +0 -1
- package/dashboard-dist/assets/DashboardLayoutAuto-DgwO_itB.js +0 -1
- package/dashboard-dist/assets/index-Cbv7vXvH.css +0 -1
- package/dashboard-dist/assets/index-e20cwqnb.js +0 -206
- package/src/components/playgroundUsageTwoslash.ts +0 -69
- package/templates/vite.dslint-scan-alias.snippet.ts +0 -4
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { useEffect, useMemo, useState } from "react";
|
|
2
|
-
import { IconMoon, IconSearch, IconSun } from "./icons";
|
|
2
|
+
import { IconChevronDown, IconMoon, IconSearch, IconSun } from "./icons";
|
|
3
3
|
import { ToggleGroup, ToggleGroupItem } from "./ui/toggle-group";
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
componentCatalogTreeFromReport,
|
|
7
|
+
} from "../dashboard/aggregate";
|
|
6
8
|
import type { WorkspaceReport } from "../types/report";
|
|
7
9
|
import type { HashRoute } from "../shell/hashRoute";
|
|
8
10
|
import type { DashboardThemePreference } from "../shell/DashboardLayout";
|
|
@@ -16,6 +18,7 @@ type Props = {
|
|
|
16
18
|
onOpenCommandPalette: () => void;
|
|
17
19
|
theme: DashboardThemePreference;
|
|
18
20
|
onThemeChange: (next: DashboardThemePreference) => void;
|
|
21
|
+
catalogNames: string[];
|
|
19
22
|
};
|
|
20
23
|
|
|
21
24
|
function SearchShortcutBadge() {
|
|
@@ -32,13 +35,29 @@ function SearchShortcutBadge() {
|
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
function navButtonClass(active: boolean) {
|
|
35
|
-
return `w-full rounded-md px-2.5 py-1.5 text-left text-sm transition ${
|
|
38
|
+
return `w-full rounded-md truncate px-2.5 py-1.5 text-left text-sm transition ${
|
|
36
39
|
active
|
|
37
40
|
? "bg-sidebar-primary text-sidebar-primary-foreground shadow-xs"
|
|
38
41
|
: "text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
|
|
39
42
|
}`;
|
|
40
43
|
}
|
|
41
44
|
|
|
45
|
+
function familyParentButtonClass(active: boolean) {
|
|
46
|
+
return `min-w-0 flex-1 rounded-l-md truncate px-2.5 py-1.5 text-left text-sm transition ${
|
|
47
|
+
active
|
|
48
|
+
? "bg-sidebar-primary text-sidebar-primary-foreground shadow-xs"
|
|
49
|
+
: "text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
|
|
50
|
+
}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function familyToggleClass(active: boolean) {
|
|
54
|
+
return `flex h-[32px] w-8 shrink-0 items-center justify-center rounded-r-md transition ${
|
|
55
|
+
active
|
|
56
|
+
? "bg-sidebar-primary text-sidebar-primary-foreground shadow-xs"
|
|
57
|
+
: "text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
|
|
58
|
+
}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
42
61
|
function sectionLabel(text: string) {
|
|
43
62
|
return (
|
|
44
63
|
<p className="mb-1.5 mt-4 px-2.5 text-xs font-semibold uppercase tracking-wider text-sidebar-foreground/50 first:mt-0">
|
|
@@ -56,16 +75,31 @@ export function Sidebar({
|
|
|
56
75
|
onOpenCommandPalette,
|
|
57
76
|
theme,
|
|
58
77
|
onThemeChange,
|
|
78
|
+
catalogNames,
|
|
59
79
|
}: Props) {
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
[report],
|
|
63
|
-
);
|
|
80
|
+
const catalogTree = useMemo(() => componentCatalogTreeFromReport(report), [report]);
|
|
81
|
+
const [expandedFamilies, setExpandedFamilies] = useState<Set<string>>(() => new Set());
|
|
64
82
|
const tokensActive = route.view === "tokens";
|
|
65
|
-
const governanceActive =
|
|
66
|
-
|
|
83
|
+
const governanceActive = route.view === "governance";
|
|
84
|
+
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (route.view !== "component") return;
|
|
87
|
+
const activeFamily = catalogTree.find(
|
|
88
|
+
(item) =>
|
|
89
|
+
item.type === "family" &&
|
|
90
|
+
(item.parent === route.componentId || item.children.includes(route.componentId)),
|
|
91
|
+
);
|
|
92
|
+
if (!activeFamily || activeFamily.type !== "family") return;
|
|
93
|
+
setExpandedFamilies((prev) => {
|
|
94
|
+
if (prev.has(activeFamily.parent)) return prev;
|
|
95
|
+
const next = new Set(prev);
|
|
96
|
+
next.add(activeFamily.parent);
|
|
97
|
+
return next;
|
|
98
|
+
});
|
|
99
|
+
}, [catalogTree, route]);
|
|
67
100
|
|
|
68
101
|
const onThemeValueChange = (value: string) => {
|
|
102
|
+
// Radix ToggleGroup (single) emits "" when re-clicking the active item — keep selection.
|
|
69
103
|
if (value !== "light" && value !== "dark") return;
|
|
70
104
|
onThemeChange(value);
|
|
71
105
|
};
|
|
@@ -75,12 +109,7 @@ export function Sidebar({
|
|
|
75
109
|
<div className="sticky top-0 z-10 shrink-0 border-b border-sidebar-border bg-sidebar px-6 py-4">
|
|
76
110
|
<div className="flex items-center justify-between gap-2">
|
|
77
111
|
<div className="flex items-center text-sidebar-foreground">
|
|
78
|
-
<svg
|
|
79
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
80
|
-
width="24"
|
|
81
|
-
height="24"
|
|
82
|
-
viewBox="0 0 24 24"
|
|
83
|
-
>
|
|
112
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
|
84
113
|
<g fill="currentColor">
|
|
85
114
|
<path
|
|
86
115
|
d="m22.346,4.836l-3.182-3.182c-.779-.78-2.049-.78-2.828,0l-3.182,3.182c-.78.78-.78,2.048,0,2.828l3.182,3.182c.39.39.902.585,1.414.585s1.024-.195,1.414-.585l3.182-3.182c.78-.78.78-2.048,0-2.828Z"
|
|
@@ -150,15 +179,11 @@ export function Sidebar({
|
|
|
150
179
|
</button>
|
|
151
180
|
|
|
152
181
|
{sectionLabel(
|
|
153
|
-
catalogNames.length > 0
|
|
154
|
-
? `Components (${catalogNames.length})`
|
|
155
|
-
: "Components",
|
|
182
|
+
catalogNames.length > 0 ? `Components (${catalogNames.length})` : "Components",
|
|
156
183
|
)}
|
|
157
184
|
<div className="space-y-0.5">
|
|
158
185
|
{reportLoading && catalogNames.length === 0 ? (
|
|
159
|
-
<p className="px-2.5 py-1.5 text-sm text-sidebar-foreground/50">
|
|
160
|
-
Loading components…
|
|
161
|
-
</p>
|
|
186
|
+
<p className="px-2.5 py-1.5 text-sm text-sidebar-foreground/50">Loading components…</p>
|
|
162
187
|
) : null}
|
|
163
188
|
{reportError && catalogNames.length === 0 ? (
|
|
164
189
|
<p className="px-2.5 py-1.5 text-sm text-sidebar-foreground/50">
|
|
@@ -170,20 +195,90 @@ export function Sidebar({
|
|
|
170
195
|
No components in scan
|
|
171
196
|
</p>
|
|
172
197
|
) : null}
|
|
173
|
-
{
|
|
174
|
-
|
|
175
|
-
route.view === "component" && route.componentId === name;
|
|
198
|
+
{catalogTree.map((item) => {
|
|
199
|
+
if (item.type === "component") {
|
|
200
|
+
const active = route.view === "component" && route.componentId === item.name;
|
|
201
|
+
return (
|
|
202
|
+
<button
|
|
203
|
+
key={item.name}
|
|
204
|
+
type="button"
|
|
205
|
+
onClick={() => onNavigate({ view: "component", componentId: item.name })}
|
|
206
|
+
className={navButtonClass(active)}
|
|
207
|
+
>
|
|
208
|
+
{item.name}
|
|
209
|
+
</button>
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const parentActive = route.view === "component" && route.componentId === item.parent;
|
|
214
|
+
const childActive =
|
|
215
|
+
route.view === "component" && item.children.includes(route.componentId);
|
|
216
|
+
const expanded = expandedFamilies.has(item.parent);
|
|
176
217
|
return (
|
|
177
|
-
<
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
218
|
+
<div key={item.parent}>
|
|
219
|
+
<div className="flex min-w-0">
|
|
220
|
+
<button
|
|
221
|
+
type="button"
|
|
222
|
+
onClick={() =>
|
|
223
|
+
onNavigate({
|
|
224
|
+
view: "component",
|
|
225
|
+
componentId: item.parent,
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
className={familyParentButtonClass(parentActive)}
|
|
229
|
+
>
|
|
230
|
+
{item.parent}
|
|
231
|
+
</button>
|
|
232
|
+
<button
|
|
233
|
+
type="button"
|
|
234
|
+
onClick={() =>
|
|
235
|
+
setExpandedFamilies((prev) => {
|
|
236
|
+
const next = new Set(prev);
|
|
237
|
+
if (next.has(item.parent)) {
|
|
238
|
+
next.delete(item.parent);
|
|
239
|
+
} else {
|
|
240
|
+
next.add(item.parent);
|
|
241
|
+
}
|
|
242
|
+
return next;
|
|
243
|
+
})
|
|
244
|
+
}
|
|
245
|
+
className={familyToggleClass(parentActive || childActive)}
|
|
246
|
+
aria-label={
|
|
247
|
+
expanded
|
|
248
|
+
? `Collapse ${item.parent} subcomponents`
|
|
249
|
+
: `Expand ${item.parent} subcomponents`
|
|
250
|
+
}
|
|
251
|
+
aria-expanded={expanded}
|
|
252
|
+
>
|
|
253
|
+
<IconChevronDown
|
|
254
|
+
className={`size-4 transition-transform ${expanded ? "rotate-180" : ""}`}
|
|
255
|
+
aria-hidden
|
|
256
|
+
/>
|
|
257
|
+
</button>
|
|
258
|
+
</div>
|
|
259
|
+
{expanded ? (
|
|
260
|
+
<div className="mt-0.5 space-y-0.5 border-l border-sidebar-border/70 pl-3">
|
|
261
|
+
{item.children.map((child) => {
|
|
262
|
+
const active = route.view === "component" && route.componentId === child;
|
|
263
|
+
return (
|
|
264
|
+
<button
|
|
265
|
+
key={child}
|
|
266
|
+
type="button"
|
|
267
|
+
onClick={() =>
|
|
268
|
+
onNavigate({
|
|
269
|
+
view: "component",
|
|
270
|
+
componentId: child,
|
|
271
|
+
})
|
|
272
|
+
}
|
|
273
|
+
className={`${navButtonClass(active)} text-xs`}
|
|
274
|
+
>
|
|
275
|
+
{child}
|
|
276
|
+
</button>
|
|
277
|
+
);
|
|
278
|
+
})}
|
|
279
|
+
</div>
|
|
280
|
+
) : null}
|
|
281
|
+
</div>
|
|
187
282
|
);
|
|
188
283
|
})}
|
|
189
284
|
</div>
|
|
@@ -203,20 +298,10 @@ export function Sidebar({
|
|
|
203
298
|
onValueChange={onThemeValueChange}
|
|
204
299
|
aria-label="Color theme"
|
|
205
300
|
>
|
|
206
|
-
<ToggleGroupItem
|
|
207
|
-
value="light"
|
|
208
|
-
className="flex-1"
|
|
209
|
-
aria-label="Light theme"
|
|
210
|
-
title="Light"
|
|
211
|
-
>
|
|
301
|
+
<ToggleGroupItem value="light" className="flex-1" aria-label="Light theme" title="Light">
|
|
212
302
|
<IconSun className="size-4" aria-hidden />
|
|
213
303
|
</ToggleGroupItem>
|
|
214
|
-
<ToggleGroupItem
|
|
215
|
-
value="dark"
|
|
216
|
-
className="flex-1"
|
|
217
|
-
aria-label="Dark theme"
|
|
218
|
-
title="Dark"
|
|
219
|
-
>
|
|
304
|
+
<ToggleGroupItem value="dark" className="flex-1" aria-label="Dark theme" title="Dark">
|
|
220
305
|
<IconMoon className="size-4" aria-hidden />
|
|
221
306
|
</ToggleGroupItem>
|
|
222
307
|
</ToggleGroup>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { useLayoutEffect, useRef, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
monospaceCharCountThatFits,
|
|
4
|
+
truncatePathMiddle,
|
|
5
|
+
} from "../dashboard/paths";
|
|
6
|
+
import { cn } from "../lib/utils";
|
|
7
|
+
|
|
8
|
+
export function TruncatedPath({
|
|
9
|
+
path,
|
|
10
|
+
className,
|
|
11
|
+
title,
|
|
12
|
+
}: {
|
|
13
|
+
path: string;
|
|
14
|
+
className?: string;
|
|
15
|
+
title?: string;
|
|
16
|
+
}) {
|
|
17
|
+
const ref = useRef<HTMLSpanElement>(null);
|
|
18
|
+
const [displayPath, setDisplayPath] = useState(path);
|
|
19
|
+
|
|
20
|
+
useLayoutEffect(() => {
|
|
21
|
+
const el = ref.current;
|
|
22
|
+
if (!el) return;
|
|
23
|
+
|
|
24
|
+
const sync = () => {
|
|
25
|
+
const maxLength = monospaceCharCountThatFits(el);
|
|
26
|
+
setDisplayPath(truncatePathMiddle(path, maxLength));
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
sync();
|
|
30
|
+
const ro = new ResizeObserver(sync);
|
|
31
|
+
ro.observe(el);
|
|
32
|
+
return () => ro.disconnect();
|
|
33
|
+
}, [path]);
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<span
|
|
37
|
+
ref={ref}
|
|
38
|
+
className={cn("block min-w-0 font-mono", className)}
|
|
39
|
+
title={title ?? path}
|
|
40
|
+
>
|
|
41
|
+
{displayPath}
|
|
42
|
+
</span>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { controlsToApiRows } from "./controlApiTable";
|
|
3
|
+
import type { PlaygroundControl } from "../types/controls";
|
|
4
|
+
|
|
5
|
+
describe("controlsToApiRows", () => {
|
|
6
|
+
it("does not expose generated example defaults as type badges", () => {
|
|
7
|
+
const controls: PlaygroundControl[] = [
|
|
8
|
+
{
|
|
9
|
+
key: "children",
|
|
10
|
+
label: "children",
|
|
11
|
+
type: "string",
|
|
12
|
+
default: "Example",
|
|
13
|
+
defaultSource: "example",
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
key: "variant",
|
|
17
|
+
label: "variant",
|
|
18
|
+
type: "string",
|
|
19
|
+
default: "default",
|
|
20
|
+
defaultSource: "type",
|
|
21
|
+
},
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
expect(controlsToApiRows(controls)).toMatchObject([
|
|
25
|
+
{ prop: "children", default: "\"Example\"", defaultBadge: null },
|
|
26
|
+
{ prop: "variant", default: "\"default\"", defaultBadge: "\"default\"" },
|
|
27
|
+
]);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -6,6 +6,8 @@ export type ApiTableRow = {
|
|
|
6
6
|
/** Select option values; Type column renders these as badges instead of a `|` string. */
|
|
7
7
|
unionLiterals: string[] | null;
|
|
8
8
|
default: string;
|
|
9
|
+
/** Default text that is API-sourced enough to show in the Type badge. */
|
|
10
|
+
defaultBadge: string | null;
|
|
9
11
|
};
|
|
10
12
|
|
|
11
13
|
function formatDefault(c: PlaygroundControl): string {
|
|
@@ -49,5 +51,6 @@ export function controlsToApiRows(controls: PlaygroundControl[]): ApiTableRow[]
|
|
|
49
51
|
type: formatType(c),
|
|
50
52
|
unionLiterals: unionLiteralsForControl(c),
|
|
51
53
|
default: formatDefault(c),
|
|
54
|
+
defaultBadge: c.defaultSource === "type" ? formatDefault(c) : null,
|
|
52
55
|
}));
|
|
53
56
|
}
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import tsx from "@shikijs/langs/tsx";
|
|
2
2
|
import githubDark from "@shikijs/themes/github-dark";
|
|
3
|
+
import githubLight from "@shikijs/themes/github-light";
|
|
3
4
|
import { createHighlighterCore, type HighlighterCore } from "shiki/core";
|
|
4
5
|
import { createJavaScriptRegexEngine } from "shiki/engine/javascript";
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
export type PlaygroundUsageTheme = "light" | "dark";
|
|
8
|
+
|
|
9
|
+
const SHIKI_THEMES: Record<PlaygroundUsageTheme, string> = {
|
|
10
|
+
light: "github-light",
|
|
11
|
+
dark: "github-dark",
|
|
12
|
+
};
|
|
13
|
+
|
|
7
14
|
const LANG = "tsx";
|
|
8
15
|
|
|
9
16
|
let highlighterPromise: Promise<HighlighterCore> | null = null;
|
|
@@ -11,7 +18,7 @@ let highlighterPromise: Promise<HighlighterCore> | null = null;
|
|
|
11
18
|
function getHighlighter(): Promise<HighlighterCore> {
|
|
12
19
|
if (highlighterPromise == null) {
|
|
13
20
|
highlighterPromise = createHighlighterCore({
|
|
14
|
-
themes: [githubDark],
|
|
21
|
+
themes: [githubLight, githubDark],
|
|
15
22
|
langs: [tsx],
|
|
16
23
|
engine: createJavaScriptRegexEngine(),
|
|
17
24
|
});
|
|
@@ -26,9 +33,13 @@ function assertNotAborted(signal: AbortSignal | undefined): void {
|
|
|
26
33
|
/** Renders usage source to Shiki HTML. */
|
|
27
34
|
export async function renderPlaygroundUsageHtml(
|
|
28
35
|
source: string,
|
|
36
|
+
theme: PlaygroundUsageTheme,
|
|
29
37
|
signal?: AbortSignal,
|
|
30
38
|
): Promise<string> {
|
|
31
39
|
const highlighter = await getHighlighter();
|
|
32
40
|
assertNotAborted(signal);
|
|
33
|
-
return highlighter.codeToHtml(source, {
|
|
41
|
+
return highlighter.codeToHtml(source, {
|
|
42
|
+
lang: LANG,
|
|
43
|
+
theme: SHIKI_THEMES[theme],
|
|
44
|
+
});
|
|
34
45
|
}
|
|
@@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority";
|
|
|
5
5
|
import { cn } from "../../lib/utils";
|
|
6
6
|
|
|
7
7
|
const badgeVariants = cva(
|
|
8
|
-
"inline-flex items-center justify-center border font-semibold
|
|
8
|
+
"inline-flex items-center justify-center border font-semibold focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
9
9
|
{
|
|
10
10
|
variants: {
|
|
11
11
|
variant: {
|
|
@@ -7,7 +7,7 @@ function Table({ className, ...props }: React.ComponentProps<"table">) {
|
|
|
7
7
|
<div
|
|
8
8
|
data-slot="table-container"
|
|
9
9
|
className={cn(
|
|
10
|
-
"relative w-full overflow-x-auto rounded-lg border border-border bg-card
|
|
10
|
+
"relative w-full overflow-x-auto rounded-lg border border-border bg-card",
|
|
11
11
|
className,
|
|
12
12
|
)}
|
|
13
13
|
>
|
|
@@ -58,7 +58,7 @@ function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
|
|
58
58
|
<tr
|
|
59
59
|
data-slot="table-row"
|
|
60
60
|
className={cn(
|
|
61
|
-
"border-b
|
|
61
|
+
"border-b hover:bg-muted/50 has-aria-expanded:bg-muted/50 data-[state=selected]:bg-muted",
|
|
62
62
|
className,
|
|
63
63
|
)}
|
|
64
64
|
{...props}
|
|
@@ -26,21 +26,7 @@ import {
|
|
|
26
26
|
import { shortPath } from "./paths";
|
|
27
27
|
import type { WorkspaceReport } from "../types/report";
|
|
28
28
|
import { pluralize } from "usemods";
|
|
29
|
-
|
|
30
|
-
/** Set of `"ComponentName/propName"` keys for every declared prop with no recorded usage. */
|
|
31
|
-
function buildUnusedPropSet(report: WorkspaceReport): Set<string> {
|
|
32
|
-
const s = new Set<string>();
|
|
33
|
-
const declaredByName = aggregateDeclaredProps(report);
|
|
34
|
-
for (const [componentName, declared] of declaredByName) {
|
|
35
|
-
const unused = buildUnusedPropSetForComponent(
|
|
36
|
-
report,
|
|
37
|
-
componentName,
|
|
38
|
-
declared,
|
|
39
|
-
);
|
|
40
|
-
for (const key of unused) s.add(key);
|
|
41
|
-
}
|
|
42
|
-
return s;
|
|
43
|
-
}
|
|
29
|
+
import { TruncatedPath } from "../components/TruncatedPath";
|
|
44
30
|
|
|
45
31
|
const catalogHoverTriggerClass =
|
|
46
32
|
"cursor-default text-xs underline decoration-dotted underline-offset-2 hover:text-foreground";
|
|
@@ -103,8 +89,11 @@ function CatalogAppUsageHover({
|
|
|
103
89
|
</p>
|
|
104
90
|
<ul className="mt-2 max-h-48 space-y-0.5 overflow-y-auto font-mono text-xs text-muted-foreground">
|
|
105
91
|
{sortedFiles.map((file) => (
|
|
106
|
-
<li key={file} className="
|
|
107
|
-
|
|
92
|
+
<li key={file} className="min-w-0">
|
|
93
|
+
<TruncatedPath
|
|
94
|
+
path={shortPath(root, file)}
|
|
95
|
+
className="text-xs text-muted-foreground"
|
|
96
|
+
/>
|
|
108
97
|
</li>
|
|
109
98
|
))}
|
|
110
99
|
</ul>
|
|
@@ -122,12 +111,16 @@ export function ComponentCatalog({
|
|
|
122
111
|
}) {
|
|
123
112
|
const defs = aggregateDefinitions(report);
|
|
124
113
|
const usages = usageMap(report);
|
|
125
|
-
const names = catalogComponentNames(defs, usages);
|
|
126
|
-
const unusedProps = useMemo(() =>
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
[
|
|
130
|
-
|
|
114
|
+
const names = catalogComponentNames(defs, usages, report);
|
|
115
|
+
const { unusedProps, declaredByName } = useMemo(() => {
|
|
116
|
+
const declaredByName = aggregateDeclaredProps(report);
|
|
117
|
+
const unusedProps = new Set<string>();
|
|
118
|
+
for (const [componentName, declared] of declaredByName) {
|
|
119
|
+
const unused = buildUnusedPropSetForComponent(report, componentName, declared);
|
|
120
|
+
for (const key of unused) unusedProps.add(key);
|
|
121
|
+
}
|
|
122
|
+
return { unusedProps, declaredByName };
|
|
123
|
+
}, [report]);
|
|
131
124
|
|
|
132
125
|
return (
|
|
133
126
|
<Table>
|
|
@@ -11,7 +11,7 @@ import type { UsageLocation, WorkspaceReport } from "../types/report";
|
|
|
11
11
|
import { usageMap } from "./aggregate";
|
|
12
12
|
import { shortPath } from "./paths";
|
|
13
13
|
import { EmptyCard } from "../components/EmptyCard";
|
|
14
|
-
import {
|
|
14
|
+
import { TruncatedPath } from "../components/TruncatedPath";
|
|
15
15
|
|
|
16
16
|
function formatCallSiteProps(loc: UsageLocation): string {
|
|
17
17
|
if (!loc.props.length) return "—";
|
|
@@ -47,18 +47,14 @@ export function ComponentUsageDetails({
|
|
|
47
47
|
if (!report) {
|
|
48
48
|
return (
|
|
49
49
|
<p className="text-sm text-muted-foreground">
|
|
50
|
-
Load <span className="font-mono">
|
|
51
|
-
this component is used in the workspace.
|
|
50
|
+
Load <span className="font-mono">dslinter-report.json</span> to see
|
|
51
|
+
where this component is used in the workspace.
|
|
52
52
|
</p>
|
|
53
53
|
);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
if (!usage) {
|
|
57
|
-
return
|
|
58
|
-
<EmptyCard>
|
|
59
|
-
No found usage for <InlineCode>{componentId}</InlineCode>.
|
|
60
|
-
</EmptyCard>
|
|
61
|
-
);
|
|
57
|
+
return <EmptyCard>0 imports of {componentId} found in codebase</EmptyCard>;
|
|
62
58
|
}
|
|
63
59
|
|
|
64
60
|
const rows = sortedLocations(usage.usage_locations);
|
|
@@ -87,13 +83,8 @@ export function ComponentUsageDetails({
|
|
|
87
83
|
const propsText = formatCallSiteProps(loc);
|
|
88
84
|
return (
|
|
89
85
|
<TableRow key={`${loc.path}-${loc.line}-${i}`}>
|
|
90
|
-
<TableCell className="min-w-0
|
|
91
|
-
<
|
|
92
|
-
className="block truncate font-mono text-xs text-foreground"
|
|
93
|
-
title={fileText}
|
|
94
|
-
>
|
|
95
|
-
{fileText}
|
|
96
|
-
</span>
|
|
86
|
+
<TableCell className="min-w-0">
|
|
87
|
+
<TruncatedPath path={fileText} className="text-xs" />
|
|
97
88
|
</TableCell>
|
|
98
89
|
<TableCell className="tabular-nums text-muted-foreground">
|
|
99
90
|
{loc.line}
|
|
@@ -1,12 +1,4 @@
|
|
|
1
1
|
import { Section } from "../components/Section";
|
|
2
|
-
import {
|
|
3
|
-
Table,
|
|
4
|
-
TableBody,
|
|
5
|
-
TableCell,
|
|
6
|
-
TableHead,
|
|
7
|
-
TableHeader,
|
|
8
|
-
TableRow,
|
|
9
|
-
} from "../components/ui/table";
|
|
10
2
|
import type { WorkspaceReport } from "../types/report";
|
|
11
3
|
import { ComponentCatalog } from "./ComponentCatalog";
|
|
12
4
|
import { FindingsList } from "./FindingsList";
|
|
@@ -30,33 +22,6 @@ export function DashboardBody({
|
|
|
30
22
|
</div>
|
|
31
23
|
) : null}
|
|
32
24
|
|
|
33
|
-
{report.ownership.length > 0 ? (
|
|
34
|
-
<Section
|
|
35
|
-
id="ownership"
|
|
36
|
-
title="Ownership"
|
|
37
|
-
description="Prefix match from <span className='font-mono'>.dslint.json</span> — useful for adoption rollups."
|
|
38
|
-
>
|
|
39
|
-
<Table>
|
|
40
|
-
<TableHeader>
|
|
41
|
-
<TableRow>
|
|
42
|
-
<TableHead>Owner</TableHead>
|
|
43
|
-
<TableHead>Files</TableHead>
|
|
44
|
-
<TableHead>Definitions</TableHead>
|
|
45
|
-
</TableRow>
|
|
46
|
-
</TableHeader>
|
|
47
|
-
<TableBody>
|
|
48
|
-
{report.ownership.map((row) => (
|
|
49
|
-
<TableRow key={row.owner}>
|
|
50
|
-
<TableCell>{row.owner}</TableCell>
|
|
51
|
-
<TableCell>{row.files}</TableCell>
|
|
52
|
-
<TableCell>{row.definitions}</TableCell>
|
|
53
|
-
</TableRow>
|
|
54
|
-
))}
|
|
55
|
-
</TableBody>
|
|
56
|
-
</Table>
|
|
57
|
-
</Section>
|
|
58
|
-
) : null}
|
|
59
|
-
|
|
60
25
|
<Section
|
|
61
26
|
id="components"
|
|
62
27
|
title="Components"
|