dslinter 0.1.13 → 0.2.2

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 (181) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/README.md +50 -29
  3. package/bin/dslinter.mjs +26 -5
  4. package/bin/lib/config-hide-component.mjs +44 -0
  5. package/bin/lib/config-hide-component.test.mjs +33 -0
  6. package/bin/lib/constants.mjs +20 -0
  7. package/bin/lib/dev-banner.mjs +16 -51
  8. package/bin/lib/dev-banner.test.mjs +20 -18
  9. package/bin/lib/enrich-playgrounds-from-ts.mjs +201 -0
  10. package/bin/lib/enrich-playgrounds-from-ts.test.mjs +74 -0
  11. package/bin/lib/enrich-report-cli.mjs +14 -0
  12. package/bin/lib/env.mjs +20 -0
  13. package/bin/lib/infer-prop-types-from-ts.mjs +381 -0
  14. package/bin/lib/infer-prop-types-from-ts.test.mjs +174 -0
  15. package/bin/lib/parse-args.mjs +13 -1
  16. package/bin/lib/parse-args.test.mjs +7 -1
  17. package/bin/lib/paths.mjs +8 -0
  18. package/bin/lib/project-root.mjs +72 -10
  19. package/bin/lib/project-root.test.mjs +32 -1
  20. package/bin/lib/prompt.mjs +31 -0
  21. package/bin/lib/resolve-project.mjs +78 -0
  22. package/bin/lib/resolve-project.test.mjs +74 -0
  23. package/bin/lib/run-scanner.mjs +40 -6
  24. package/bin/lib/scaffold-config.mjs +128 -9
  25. package/bin/lib/scaffold-config.test.mjs +24 -2
  26. package/bin/lib/scan-host.mjs +44 -0
  27. package/bin/lib/scan-host.test.mjs +41 -0
  28. package/bin/lib/setup-readiness.mjs +153 -0
  29. package/bin/lib/setup-readiness.test.mjs +32 -0
  30. package/bin/modes/build.mjs +31 -6
  31. package/bin/modes/dev.mjs +55 -21
  32. package/bin/modes/init.mjs +3 -22
  33. package/bin/modes/init.test.mjs +1 -1
  34. package/bin/modes/mcp.mjs +49 -0
  35. package/bin/modes/report.mjs +29 -4
  36. package/bin/modes/watch.mjs +85 -0
  37. package/dashboard-dist/assets/DashboardLayoutAuto-Bja3BuZZ.css +1 -0
  38. package/dashboard-dist/assets/DashboardLayoutAuto-h0gP_iKd.js +1 -0
  39. package/dashboard-dist/assets/axe-DDaE9JTN.js +20 -0
  40. package/dashboard-dist/assets/index-B9sZ6wHm.css +1 -0
  41. package/dashboard-dist/assets/index-DIDBt5ed.js +218 -0
  42. package/dashboard-dist/index.html +2 -2
  43. package/index.cjs +53 -52
  44. package/index.d.ts +3 -0
  45. package/package.json +18 -12
  46. package/shared/env.ts +15 -0
  47. package/shared/paths.ts +8 -0
  48. package/shared/reportPath.test.ts +19 -0
  49. package/shared/reportPath.ts +12 -0
  50. package/shared/servePort.ts +16 -0
  51. package/src/components/ComponentInspectPane.tsx +67 -19
  52. package/src/components/ComponentPlaygroundPane.tsx +262 -113
  53. package/src/components/DashboardCommandPalette.tsx +6 -11
  54. package/src/components/GovernancePane.tsx +2 -2
  55. package/src/components/HideFromCatalogButton.tsx +44 -0
  56. package/src/components/OpenInEditorButton.tsx +36 -0
  57. package/src/components/PlaygroundA11yAndCode.tsx +53 -53
  58. package/src/components/PlaygroundAppThemeWrapper.tsx +82 -0
  59. package/src/components/PlaygroundControls.tsx +5 -11
  60. package/src/components/PlaygroundPreviewErrorBoundary.tsx +54 -0
  61. package/src/components/PlaygroundUsageCode.tsx +6 -4
  62. package/src/components/PlaygroundVariantMatrix.tsx +101 -34
  63. package/src/components/Section.tsx +5 -2
  64. package/src/components/Sidebar.tsx +131 -46
  65. package/src/components/TruncatedPath.tsx +44 -0
  66. package/src/components/controlApiTable.test.ts +29 -0
  67. package/src/components/controlApiTable.ts +3 -0
  68. package/src/components/playgroundUsageHighlight.ts +14 -3
  69. package/src/components/ui/badge.tsx +1 -1
  70. package/src/components/ui/table.tsx +2 -2
  71. package/src/dashboard/ComponentCatalog.tsx +16 -23
  72. package/src/dashboard/ComponentUsageDetails.tsx +6 -15
  73. package/src/dashboard/DashboardBody.tsx +0 -35
  74. package/src/dashboard/FindingsList.tsx +65 -55
  75. package/src/dashboard/ScannedTokenWall.tsx +3 -3
  76. package/src/dashboard/aggregate.test.ts +74 -0
  77. package/src/dashboard/aggregate.ts +145 -21
  78. package/src/dashboard/catalogVisibility.test.ts +93 -0
  79. package/src/dashboard/catalogVisibility.ts +108 -0
  80. package/src/dashboard/editorLink.test.ts +57 -0
  81. package/src/dashboard/editorLink.ts +71 -0
  82. package/src/dashboard/paths.test.ts +49 -0
  83. package/src/dashboard/paths.ts +51 -3
  84. package/src/dashboard/updateDslintConfig.ts +22 -0
  85. package/src/dashboard/useWorkspaceReport.ts +21 -17
  86. package/src/index.ts +26 -0
  87. package/src/mcp/agent-context.ts +148 -0
  88. package/src/mcp/agent-query.test.ts +89 -0
  89. package/src/mcp/agent-query.ts +373 -0
  90. package/src/mcp/config.ts +53 -0
  91. package/src/mcp/index.ts +18 -0
  92. package/src/mcp/normalize-paths.ts +65 -0
  93. package/src/mcp/report-cache.ts +212 -0
  94. package/src/mcp/rule-catalog.json +156 -0
  95. package/src/mcp/rule-catalog.ts +33 -0
  96. package/src/mcp/schemas.ts +54 -0
  97. package/src/mcp/server.test.ts +44 -0
  98. package/src/mcp/server.ts +343 -0
  99. package/src/mcp/start.ts +29 -0
  100. package/src/mcp/verify-loop.test.ts +49 -0
  101. package/src/mcp/verify-loop.ts +149 -0
  102. package/src/playground/appPreviewTheme.test.ts +148 -0
  103. package/src/playground/appPreviewTheme.ts +137 -0
  104. package/src/playground/buildCompoundPlaygroundEntries.test.ts +348 -0
  105. package/src/playground/buildCompoundPlaygroundEntries.ts +625 -0
  106. package/src/playground/buildPlaygroundEntriesFromReport.test.ts +420 -6
  107. package/src/playground/buildPlaygroundEntriesFromReport.ts +206 -285
  108. package/src/playground/catalogIdFromPlaygroundExport.test.ts +15 -0
  109. package/src/playground/catalogIdFromPlaygroundExport.ts +8 -0
  110. package/src/playground/collectDefinedPlaygrounds.test.ts +59 -0
  111. package/src/playground/collectDefinedPlaygrounds.ts +68 -0
  112. package/src/playground/controls.ts +177 -0
  113. package/src/playground/createPlaygroundRegistry.ts +1 -1
  114. package/src/playground/definePlayground.tsx +88 -16
  115. package/src/playground/definePlaygroundFromKit.ts +17 -0
  116. package/src/playground/embedGlobKey.ts +8 -0
  117. package/src/playground/enrichKitControls.test.ts +25 -0
  118. package/src/playground/enrichKitControls.ts +197 -0
  119. package/src/playground/expandPlaygroundControls.test.ts +50 -0
  120. package/src/playground/expandPlaygroundControls.ts +97 -0
  121. package/src/playground/inferKitJsx.test.ts +77 -0
  122. package/src/playground/inferKitJsx.ts +165 -0
  123. package/src/playground/inferKitParams.test.ts +41 -0
  124. package/src/playground/inferKitParams.ts +113 -0
  125. package/src/playground/inferPropTypesFromTs.d.mts +47 -0
  126. package/src/playground/inferPropTypesFromTs.mjs +343 -0
  127. package/src/playground/inferPropTypesFromTs.test.ts +227 -0
  128. package/src/playground/inferPropTypesFromTs.ts +17 -0
  129. package/src/playground/mergePlaygroundEntries.test.ts +32 -0
  130. package/src/playground/mergePlaygroundEntries.ts +28 -0
  131. package/src/playground/playgroundJoin.test.ts +79 -19
  132. package/src/playground/playgroundJoin.ts +47 -22
  133. package/src/playground/playgroundModuleExport.test.ts +42 -0
  134. package/src/playground/playgroundModuleExport.ts +22 -0
  135. package/src/playground/playgroundSpecsKey.ts +8 -0
  136. package/src/playground/propCoerce.ts +91 -0
  137. package/src/playground/scanVariantA11y.test.ts +46 -0
  138. package/src/playground/scanVariantA11y.ts +107 -0
  139. package/src/playground/snippet.ts +83 -0
  140. package/src/playground/usePlaygroundFromReport.test.ts +18 -8
  141. package/src/playground/usePlaygroundFromReport.ts +3 -1
  142. package/src/report/a11yForModule.ts +2 -7
  143. package/src/report/a11yScoring.test.ts +24 -0
  144. package/src/report/a11yScoring.ts +17 -0
  145. package/src/report/index.ts +6 -0
  146. package/src/shell/DashboardLayout.tsx +71 -45
  147. package/src/shell/DashboardLayoutAuto.tsx +0 -4
  148. package/src/shell/hashRoute.test.ts +7 -15
  149. package/src/shell/hashRoute.ts +31 -31
  150. package/src/shell/useHashRoute.ts +38 -13
  151. package/src/styles/dashboard-theme.css +18 -7
  152. package/src/types/controls.ts +11 -0
  153. package/src/types/playground.ts +4 -0
  154. package/src/types/report.ts +32 -9
  155. package/templates/playground/buildRegistry.ts +1 -1
  156. package/templates/vite.dslinter.snippet.ts +15 -4
  157. package/vite/collectScanModules.test.ts +91 -3
  158. package/vite/collectScanModules.ts +94 -29
  159. package/vite/consumer.config.mjs +6 -3
  160. package/vite/consumerAlias.test.ts +47 -0
  161. package/vite/consumerAlias.ts +114 -0
  162. package/vite/embedTailwindSources.test.ts +74 -0
  163. package/vite/embedTailwindSources.ts +97 -0
  164. package/vite/loadConsumerAliases.test.ts +131 -0
  165. package/vite/loadConsumerAliases.ts +155 -0
  166. package/vite/openFileInEditor.mjs +196 -0
  167. package/vite/openFileInEditor.test.mjs +87 -0
  168. package/vite/plugin.resolve.test.ts +72 -0
  169. package/vite/plugin.ts +216 -19
  170. package/vite/reportPath.test.ts +19 -0
  171. package/vite/resolveWayfinderImport.ts +56 -0
  172. package/vite/shims/inertia-react.tsx +85 -0
  173. package/vite/shims/wayfinder-actions.ts +33 -0
  174. package/vite/shims/wayfinder-routes.ts +30 -0
  175. package/vite/shims/ziggy-js.ts +12 -0
  176. package/dashboard-dist/assets/DashboardLayoutAuto-Bm7yfyC-.css +0 -1
  177. package/dashboard-dist/assets/DashboardLayoutAuto-DgwO_itB.js +0 -1
  178. package/dashboard-dist/assets/index-Cbv7vXvH.css +0 -1
  179. package/dashboard-dist/assets/index-e20cwqnb.js +0 -206
  180. package/src/components/playgroundUsageTwoslash.ts +0 -69
  181. 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 { componentCatalogNamesFromReport } from "../dashboard/aggregate";
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 catalogNames = useMemo(
61
- () => componentCatalogNamesFromReport(report),
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
- route.view === "governance" && route.catalog == null;
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
- {catalogNames.map((name) => {
174
- const active =
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
- <button
178
- key={name}
179
- type="button"
180
- onClick={() =>
181
- onNavigate({ view: "component", componentId: name })
182
- }
183
- className={navButtonClass(active)}
184
- >
185
- {name}
186
- </button>
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
- const THEME = "github-dark";
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, { lang: LANG, theme: THEME });
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 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
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 shadow-xs",
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 transition-colors hover:bg-muted/50 has-aria-expanded:bg-muted/50 data-[state=selected]:bg-muted",
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="truncate" title={shortPath(root, file)}>
107
- {shortPath(root, file)}
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(() => buildUnusedPropSet(report), [report]);
127
- const declaredByName = useMemo(
128
- () => aggregateDeclaredProps(report),
129
- [report],
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 { InlineCode } from "../components/InlineCode";
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">dslint-report.json</span> to see where
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 max-w-0">
91
- <span
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"