dslinter 0.2.2 → 0.3.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.
Files changed (59) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/bin/lib/infer-prop-types-from-ts.mjs +14 -1
  3. package/bin/lib/infer-prop-types-from-ts.test.mjs +32 -0
  4. package/dashboard-dist/assets/{DashboardLayoutAuto-h0gP_iKd.js → DashboardLayoutAuto-B4P-sy4z.js} +1 -1
  5. package/dashboard-dist/assets/DashboardLayoutAuto-CAO6F6-q.css +1 -0
  6. package/dashboard-dist/assets/{axe-DDaE9JTN.js → axe-CaxTXfM9.js} +1 -1
  7. package/dashboard-dist/assets/index-B432JkIx.js +219 -0
  8. package/dashboard-dist/assets/index-D0O_5w5V.css +1 -0
  9. package/dashboard-dist/dslinter-report.json +23929 -0
  10. package/dashboard-dist/index.html +2 -2
  11. package/index.cjs +52 -52
  12. package/package.json +6 -6
  13. package/src/components/CatalogPane.tsx +94 -0
  14. package/src/components/ComponentInspectPane.tsx +125 -125
  15. package/src/components/ComponentPlaygroundPane.tsx +42 -27
  16. package/src/components/DashboardCommandPalette.tsx +9 -0
  17. package/src/components/GovernanceInventoryTabs.tsx +51 -0
  18. package/src/components/GovernancePane.tsx +18 -5
  19. package/src/components/PlaygroundA11yAndCode.tsx +0 -52
  20. package/src/components/PlaygroundControlField.tsx +2 -0
  21. package/src/components/ScoreGauge.test.ts +22 -0
  22. package/src/components/ScoreGauge.tsx +179 -0
  23. package/src/components/Sidebar.tsx +97 -23
  24. package/src/components/TokensPane.tsx +11 -13
  25. package/src/components/controlApiTable.test.ts +15 -0
  26. package/src/components/controlApiTable.ts +4 -0
  27. package/src/components/ui/badge.tsx +5 -5
  28. package/src/dashboard/ComponentCatalog.tsx +10 -1
  29. package/src/dashboard/ComponentPropUsageDetail.tsx +127 -42
  30. package/src/dashboard/ComponentUsageDetails.tsx +39 -9
  31. package/src/dashboard/DashboardBody.tsx +83 -12
  32. package/src/dashboard/ScannedTokenWall.tsx +9 -6
  33. package/src/dashboard/UnusedComponentsList.tsx +74 -0
  34. package/src/dashboard/aggregate.test.ts +381 -12
  35. package/src/dashboard/aggregate.ts +167 -30
  36. package/src/dashboard/mergeTokenCatalog.ts +5 -0
  37. package/src/dashboard/paths.test.ts +18 -1
  38. package/src/dashboard/paths.ts +8 -0
  39. package/src/mcp/agent-query.ts +1 -1
  40. package/src/playground/buildPlaygroundEntriesFromReport.test.ts +3 -3
  41. package/src/playground/controls.ts +16 -3
  42. package/src/playground/enrichKitControls.ts +5 -5
  43. package/src/playground/inferKitJsx.test.ts +0 -11
  44. package/src/playground/inferPropTypesFromTs.d.mts +1 -1
  45. package/src/playground/inferPropTypesFromTs.mjs +19 -3
  46. package/src/playground/inferPropTypesFromTs.test.ts +32 -0
  47. package/src/playground/inferPropTypesFromTs.ts +1 -1
  48. package/src/playground/playgroundJoin.ts +34 -0
  49. package/src/playground/propCoerce.ts +2 -2
  50. package/src/playground/snippet.ts +1 -0
  51. package/src/shell/DashboardLayout.tsx +21 -4
  52. package/src/shell/hashRoute.test.ts +9 -0
  53. package/src/shell/hashRoute.ts +6 -0
  54. package/src/types/controls.ts +12 -0
  55. package/src/types/report.ts +3 -2
  56. package/vite/embedTailwindSources.ts +8 -6
  57. package/dashboard-dist/assets/DashboardLayoutAuto-Bja3BuZZ.css +0 -1
  58. package/dashboard-dist/assets/index-B9sZ6wHm.css +0 -1
  59. package/dashboard-dist/assets/index-DIDBt5ed.js +0 -218
@@ -0,0 +1,22 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { scoreGaugeBand } from "./ScoreGauge";
3
+
4
+ describe("scoreGaugeBand", () => {
5
+ it("returns poor for scores below 50", () => {
6
+ expect(scoreGaugeBand(46)).toBe("poor");
7
+ expect(scoreGaugeBand(0)).toBe("poor");
8
+ expect(scoreGaugeBand(49)).toBe("poor");
9
+ });
10
+
11
+ it("returns average for scores from 50 to 89", () => {
12
+ expect(scoreGaugeBand(75)).toBe("average");
13
+ expect(scoreGaugeBand(50)).toBe("average");
14
+ expect(scoreGaugeBand(89)).toBe("average");
15
+ });
16
+
17
+ it("returns good for scores 90 and above", () => {
18
+ expect(scoreGaugeBand(95)).toBe("good");
19
+ expect(scoreGaugeBand(90)).toBe("good");
20
+ expect(scoreGaugeBand(100)).toBe("good");
21
+ });
22
+ });
@@ -0,0 +1,179 @@
1
+ import type { MouseEvent } from "react";
2
+ import { cn } from "../lib/utils";
3
+
4
+ /** Score bands (0–49 poor, 50–89 average, 90–100 good). */
5
+ export type ScoreGaugeBand = "poor" | "average" | "good";
6
+
7
+ export function scoreGaugeBand(score: number): ScoreGaugeBand {
8
+ if (score >= 90) return "good";
9
+ if (score >= 50) return "average";
10
+ return "poor";
11
+ }
12
+
13
+ const BAND_STYLES: Record<
14
+ ScoreGaugeBand,
15
+ { stroke: string; track: string; text: string }
16
+ > = {
17
+ good: {
18
+ stroke: "stroke-success",
19
+ track: "stroke-success/15",
20
+ text: "text-success",
21
+ },
22
+ average: {
23
+ stroke: "stroke-warning",
24
+ track: "stroke-warning/15",
25
+ text: "text-warning",
26
+ },
27
+ poor: {
28
+ stroke: "stroke-destructive",
29
+ track: "stroke-destructive/15",
30
+ text: "text-destructive",
31
+ },
32
+ };
33
+
34
+ const PENDING_STYLES = {
35
+ stroke: "stroke-muted-foreground",
36
+ track: "stroke-border",
37
+ text: "text-muted-foreground",
38
+ };
39
+
40
+ type ScoreGaugeProps = {
41
+ label: string;
42
+ /** 0–100, or null when the score is not yet available. */
43
+ value: number | null;
44
+ href?: string;
45
+ /** Show a trailing ellipsis on the numeric label (e.g. variant scan still running). */
46
+ pending?: boolean;
47
+ className?: string;
48
+ };
49
+
50
+ const GAUGE_SIZE = 56;
51
+ const STROKE_WIDTH = 4;
52
+
53
+ function scrollToHash(href: string, e: MouseEvent<HTMLAnchorElement>) {
54
+ if (
55
+ e.defaultPrevented ||
56
+ e.button !== 0 ||
57
+ e.metaKey ||
58
+ e.ctrlKey ||
59
+ e.shiftKey ||
60
+ e.altKey
61
+ ) {
62
+ return;
63
+ }
64
+ const id = href.startsWith("#") ? href.slice(1) : href;
65
+ const target = id ? document.getElementById(id) : null;
66
+ if (!target) return;
67
+ e.preventDefault();
68
+ target.scrollIntoView({ behavior: "smooth", block: "start" });
69
+ if (typeof window !== "undefined" && window.history?.replaceState) {
70
+ window.history.replaceState(null, "", `#${id}`);
71
+ }
72
+ }
73
+
74
+ export function ScoreGauge({
75
+ label,
76
+ value,
77
+ href,
78
+ pending = false,
79
+ className,
80
+ }: ScoreGaugeProps) {
81
+ const radius = (GAUGE_SIZE - STROKE_WIDTH) / 2;
82
+ const circumference = 2 * Math.PI * radius;
83
+ const clamped =
84
+ value == null ? 0 : Math.max(0, Math.min(100, Math.round(value)));
85
+ const styles =
86
+ value == null ? PENDING_STYLES : BAND_STYLES[scoreGaugeBand(clamped)];
87
+ const dashOffset =
88
+ value == null
89
+ ? circumference
90
+ : circumference - (clamped / 100) * circumference;
91
+
92
+ const content = (
93
+ <>
94
+ <div
95
+ className="relative shrink-0"
96
+ style={{ width: GAUGE_SIZE, height: GAUGE_SIZE }}
97
+ aria-hidden
98
+ >
99
+ <svg
100
+ width={GAUGE_SIZE}
101
+ height={GAUGE_SIZE}
102
+ viewBox={`0 0 ${GAUGE_SIZE} ${GAUGE_SIZE}`}
103
+ className="-rotate-90"
104
+ fill="none"
105
+ >
106
+ <circle
107
+ cx={GAUGE_SIZE / 2}
108
+ cy={GAUGE_SIZE / 2}
109
+ r={radius}
110
+ className={cn("fill-none", styles.track)}
111
+ strokeWidth={STROKE_WIDTH}
112
+ />
113
+ {value != null ? (
114
+ <circle
115
+ cx={GAUGE_SIZE / 2}
116
+ cy={GAUGE_SIZE / 2}
117
+ r={radius}
118
+ className={cn("fill-none", styles.stroke)}
119
+ strokeWidth={STROKE_WIDTH}
120
+ strokeLinecap="round"
121
+ strokeDasharray={circumference}
122
+ strokeDashoffset={dashOffset}
123
+ />
124
+ ) : null}
125
+ </svg>
126
+ <span
127
+ className={cn(
128
+ "absolute inset-0 flex items-center justify-center text-base font-semibold tabular-nums",
129
+ styles.text,
130
+ )}
131
+ >
132
+ {value == null ? "—" : clamped}
133
+ </span>
134
+ </div>
135
+ <span className="mt-1 max-w-22 text-center text-xs leading-tight text-muted-foreground">
136
+ {label}
137
+ {pending ? "…" : null}
138
+ </span>
139
+ </>
140
+ );
141
+
142
+ const rootClass = cn(
143
+ "flex flex-col items-center gap-0",
144
+ href &&
145
+ "rounded-lg transition hover:bg-accent/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
146
+ className,
147
+ );
148
+
149
+ if (href) {
150
+ return (
151
+ <a
152
+ href={href}
153
+ onClick={(e) => scrollToHash(href, e)}
154
+ className={rootClass}
155
+ aria-label={
156
+ value == null
157
+ ? `${label}: not available`
158
+ : `${label}: ${clamped} out of 100${pending ? ", updating" : ""}`
159
+ }
160
+ >
161
+ {content}
162
+ </a>
163
+ );
164
+ }
165
+
166
+ return (
167
+ <div
168
+ className={rootClass}
169
+ role="img"
170
+ aria-label={
171
+ value == null
172
+ ? `${label}: not available`
173
+ : `${label}: ${clamped} out of 100${pending ? ", updating" : ""}`
174
+ }
175
+ >
176
+ {content}
177
+ </div>
178
+ );
179
+ }
@@ -1,9 +1,10 @@
1
- import { useEffect, useMemo, useState } from "react";
1
+ import { useEffect, useMemo, useRef, useState } from "react";
2
2
  import { IconChevronDown, IconMoon, IconSearch, IconSun } from "./icons";
3
3
  import { ToggleGroup, ToggleGroupItem } from "./ui/toggle-group";
4
4
 
5
5
  import {
6
6
  componentCatalogTreeFromReport,
7
+ resolveFamilyNavigationTarget,
7
8
  } from "../dashboard/aggregate";
8
9
  import type { WorkspaceReport } from "../types/report";
9
10
  import type { HashRoute } from "../shell/hashRoute";
@@ -66,6 +67,20 @@ function sectionLabel(text: string) {
66
67
  );
67
68
  }
68
69
 
70
+ function scrollActiveNavItemToTop(nav: HTMLElement) {
71
+ const active = nav.querySelector<HTMLElement>('[data-nav-active="true"]');
72
+ if (!active) return;
73
+
74
+ const activeRect = active.getBoundingClientRect();
75
+ const navRect = nav.getBoundingClientRect();
76
+ const visible =
77
+ activeRect.bottom > navRect.top && activeRect.top < navRect.bottom;
78
+ if (visible) return;
79
+
80
+ const offset = activeRect.top - navRect.top;
81
+ nav.scrollTop += offset;
82
+ }
83
+
69
84
  export function Sidebar({
70
85
  report,
71
86
  reportLoading,
@@ -77,17 +92,25 @@ export function Sidebar({
77
92
  onThemeChange,
78
93
  catalogNames,
79
94
  }: Props) {
80
- const catalogTree = useMemo(() => componentCatalogTreeFromReport(report), [report]);
81
- const [expandedFamilies, setExpandedFamilies] = useState<Set<string>>(() => new Set());
95
+ const catalogTree = useMemo(
96
+ () => componentCatalogTreeFromReport(report),
97
+ [report],
98
+ );
99
+ const navRef = useRef<HTMLElement>(null);
100
+ const [expandedFamilies, setExpandedFamilies] = useState<Set<string>>(
101
+ () => new Set(),
102
+ );
82
103
  const tokensActive = route.view === "tokens";
83
104
  const governanceActive = route.view === "governance";
105
+ const catalogActive = route.view === "catalog";
84
106
 
85
107
  useEffect(() => {
86
108
  if (route.view !== "component") return;
87
109
  const activeFamily = catalogTree.find(
88
110
  (item) =>
89
111
  item.type === "family" &&
90
- (item.parent === route.componentId || item.children.includes(route.componentId)),
112
+ (item.parent === route.componentId ||
113
+ item.children.includes(route.componentId)),
91
114
  );
92
115
  if (!activeFamily || activeFamily.type !== "family") return;
93
116
  setExpandedFamilies((prev) => {
@@ -98,6 +121,13 @@ export function Sidebar({
98
121
  });
99
122
  }, [catalogTree, route]);
100
123
 
124
+ useEffect(() => {
125
+ const nav = navRef.current;
126
+ if (!nav) return;
127
+ const frame = requestAnimationFrame(() => scrollActiveNavItemToTop(nav));
128
+ return () => cancelAnimationFrame(frame);
129
+ }, [route, catalogTree, expandedFamilies, catalogNames.length]);
130
+
101
131
  const onThemeValueChange = (value: string) => {
102
132
  // Radix ToggleGroup (single) emits "" when re-clicking the active item — keep selection.
103
133
  if (value !== "light" && value !== "dark") return;
@@ -105,11 +135,16 @@ export function Sidebar({
105
135
  };
106
136
 
107
137
  return (
108
- <aside className="fixed flex h-full w-[240px] shrink-0 flex-col overflow-hidden border-r border-sidebar-border bg-sidebar text-sidebar-foreground">
109
- <div className="sticky top-0 z-10 shrink-0 border-b border-sidebar-border bg-sidebar px-6 py-4">
138
+ <aside className="fixed flex h-full w-[240px] shrink-0 flex-col overflow-hidden border-r border-border bg-background text-sidebar-foreground">
139
+ <div className="sticky top-0 z-10 shrink-0 border-b border-border bg-background px-6 py-4">
110
140
  <div className="flex items-center justify-between gap-2">
111
- <div className="flex items-center text-sidebar-foreground">
112
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
141
+ <div className="flex items-center text-foreground">
142
+ <svg
143
+ xmlns="http://www.w3.org/2000/svg"
144
+ width="24"
145
+ height="24"
146
+ viewBox="0 0 24 24"
147
+ >
113
148
  <g fill="currentColor">
114
149
  <path
115
150
  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"
@@ -152,7 +187,7 @@ export function Sidebar({
152
187
  <button
153
188
  type="button"
154
189
  onClick={onOpenCommandPalette}
155
- className="flex shrink-0 items-center gap-1.5 rounded-md px-1.5 py-1 text-sidebar-foreground/70 transition hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
190
+ className="flex shrink-0 items-center gap-1.5 rounded-md px-1.5 py-1 text-foreground/70 transition hover:bg-accent hover:text-accent-foreground"
156
191
  aria-label="Search components and views"
157
192
  >
158
193
  <IconSearch className="size-4 shrink-0" aria-hidden />
@@ -161,29 +196,43 @@ export function Sidebar({
161
196
  </div>
162
197
  </div>
163
198
 
164
- <nav className="min-h-0 flex-1 overflow-y-auto px-3 py-3">
199
+ <nav ref={navRef} className="min-h-0 flex-1 overflow-y-auto px-3 py-3">
165
200
  {sectionLabel("Explore")}
166
201
  <button
167
202
  type="button"
168
203
  onClick={() => onNavigate({ view: "governance" })}
169
204
  className={navButtonClass(governanceActive)}
205
+ data-nav-active={governanceActive ? "true" : undefined}
170
206
  >
171
207
  Governance
172
208
  </button>
209
+ <button
210
+ type="button"
211
+ onClick={() => onNavigate({ view: "catalog" })}
212
+ className={navButtonClass(catalogActive)}
213
+ data-nav-active={catalogActive ? "true" : undefined}
214
+ >
215
+ All components
216
+ </button>
173
217
  <button
174
218
  type="button"
175
219
  onClick={() => onNavigate({ view: "tokens" })}
176
220
  className={navButtonClass(tokensActive)}
221
+ data-nav-active={tokensActive ? "true" : undefined}
177
222
  >
178
223
  Design tokens
179
224
  </button>
180
225
 
181
226
  {sectionLabel(
182
- catalogNames.length > 0 ? `Components (${catalogNames.length})` : "Components",
227
+ catalogNames.length > 0
228
+ ? `Components (${catalogNames.length})`
229
+ : "Components",
183
230
  )}
184
231
  <div className="space-y-0.5">
185
232
  {reportLoading && catalogNames.length === 0 ? (
186
- <p className="px-2.5 py-1.5 text-sm text-sidebar-foreground/50">Loading components…</p>
233
+ <p className="px-2.5 py-1.5 text-sm text-sidebar-foreground/50">
234
+ Loading components…
235
+ </p>
187
236
  ) : null}
188
237
  {reportError && catalogNames.length === 0 ? (
189
238
  <p className="px-2.5 py-1.5 text-sm text-sidebar-foreground/50">
@@ -197,35 +246,47 @@ export function Sidebar({
197
246
  ) : null}
198
247
  {catalogTree.map((item) => {
199
248
  if (item.type === "component") {
200
- const active = route.view === "component" && route.componentId === item.name;
249
+ const active =
250
+ route.view === "component" && route.componentId === item.name;
201
251
  return (
202
252
  <button
203
253
  key={item.name}
204
254
  type="button"
205
- onClick={() => onNavigate({ view: "component", componentId: item.name })}
255
+ onClick={() =>
256
+ onNavigate({ view: "component", componentId: item.name })
257
+ }
206
258
  className={navButtonClass(active)}
259
+ data-nav-active={active ? "true" : undefined}
207
260
  >
208
261
  {item.name}
209
262
  </button>
210
263
  );
211
264
  }
212
265
 
213
- const parentActive = route.view === "component" && route.componentId === item.parent;
266
+ const parentActive =
267
+ route.view === "component" && route.componentId === item.parent;
214
268
  const childActive =
215
- route.view === "component" && item.children.includes(route.componentId);
269
+ route.view === "component" &&
270
+ item.children.includes(route.componentId);
271
+ const familyActive = parentActive || childActive;
272
+ const parentNavActive = parentActive && !childActive;
216
273
  const expanded = expandedFamilies.has(item.parent);
217
274
  return (
218
- <div key={item.parent}>
275
+ <div key={item.path}>
219
276
  <div className="flex min-w-0">
220
277
  <button
221
278
  type="button"
222
279
  onClick={() =>
223
280
  onNavigate({
224
281
  view: "component",
225
- componentId: item.parent,
282
+ componentId: resolveFamilyNavigationTarget(
283
+ item,
284
+ catalogNames,
285
+ ),
226
286
  })
227
287
  }
228
- className={familyParentButtonClass(parentActive)}
288
+ className={familyParentButtonClass(familyActive)}
289
+ data-nav-active={parentNavActive ? "true" : undefined}
229
290
  >
230
291
  {item.parent}
231
292
  </button>
@@ -242,7 +303,7 @@ export function Sidebar({
242
303
  return next;
243
304
  })
244
305
  }
245
- className={familyToggleClass(parentActive || childActive)}
306
+ className={familyToggleClass(familyActive)}
246
307
  aria-label={
247
308
  expanded
248
309
  ? `Collapse ${item.parent} subcomponents`
@@ -259,7 +320,9 @@ export function Sidebar({
259
320
  {expanded ? (
260
321
  <div className="mt-0.5 space-y-0.5 border-l border-sidebar-border/70 pl-3">
261
322
  {item.children.map((child) => {
262
- const active = route.view === "component" && route.componentId === child;
323
+ const active =
324
+ route.view === "component" &&
325
+ route.componentId === child;
263
326
  return (
264
327
  <button
265
328
  key={child}
@@ -271,6 +334,7 @@ export function Sidebar({
271
334
  })
272
335
  }
273
336
  className={`${navButtonClass(active)} text-xs`}
337
+ data-nav-active={active ? "true" : undefined}
274
338
  >
275
339
  {child}
276
340
  </button>
@@ -298,10 +362,20 @@ export function Sidebar({
298
362
  onValueChange={onThemeValueChange}
299
363
  aria-label="Color theme"
300
364
  >
301
- <ToggleGroupItem value="light" className="flex-1" aria-label="Light theme" title="Light">
365
+ <ToggleGroupItem
366
+ value="light"
367
+ className="flex-1"
368
+ aria-label="Light theme"
369
+ title="Light"
370
+ >
302
371
  <IconSun className="size-4" aria-hidden />
303
372
  </ToggleGroupItem>
304
- <ToggleGroupItem value="dark" className="flex-1" aria-label="Dark theme" title="Dark">
373
+ <ToggleGroupItem
374
+ value="dark"
375
+ className="flex-1"
376
+ aria-label="Dark theme"
377
+ title="Dark"
378
+ >
305
379
  <IconMoon className="size-4" aria-hidden />
306
380
  </ToggleGroupItem>
307
381
  </ToggleGroup>
@@ -36,19 +36,17 @@ export function TokensPane({
36
36
  </p>
37
37
  </header>
38
38
  <div className="min-w-0 w-full px-8 py-8">
39
- <div className="rounded-ds-lg border border-border bg-card p-6 text-card-foreground shadow-xs">
40
- {hasScanned && merged ? (
41
- <ScannedTokenWall view={merged} />
42
- ) : hasManual ? (
43
- <TokenWall catalog={tokenCatalog} />
44
- ) : (
45
- <p className="text-sm text-muted-foreground">
46
- Run <span className="font-mono">dslinter --json</span> on a
47
- project with CSS token sources, or pass a{" "}
48
- <span className="font-mono">tokenCatalog</span> prop.
49
- </p>
50
- )}
51
- </div>
39
+ {hasScanned && merged ? (
40
+ <ScannedTokenWall view={merged} />
41
+ ) : hasManual ? (
42
+ <TokenWall catalog={tokenCatalog} />
43
+ ) : (
44
+ <p className="text-sm text-muted-foreground">
45
+ Run <span className="font-mono">dslinter --json</span> on a project
46
+ with CSS token sources, or pass a{" "}
47
+ <span className="font-mono">tokenCatalog</span> prop.
48
+ </p>
49
+ )}
52
50
  </div>
53
51
  </div>
54
52
  );
@@ -26,4 +26,19 @@ describe("controlsToApiRows", () => {
26
26
  { prop: "variant", default: "\"default\"", defaultBadge: "\"default\"" },
27
27
  ]);
28
28
  });
29
+
30
+ it("maps node controls to ReactNode in the type column", () => {
31
+ const controls: PlaygroundControl[] = [
32
+ {
33
+ key: "actions",
34
+ label: "actions",
35
+ type: "node",
36
+ default: "actions",
37
+ },
38
+ ];
39
+
40
+ expect(controlsToApiRows(controls)).toMatchObject([
41
+ { prop: "actions", type: "ReactNode" },
42
+ ]);
43
+ });
29
44
  });
@@ -18,6 +18,8 @@ function formatDefault(c: PlaygroundControl): string {
18
18
  return String(c.default);
19
19
  case "string":
20
20
  return c.default === "" ? "—" : JSON.stringify(c.default);
21
+ case "node":
22
+ return c.default === "" ? "—" : JSON.stringify(c.default);
21
23
  case "select":
22
24
  return c.default === "" ? "—" : c.default;
23
25
  default:
@@ -33,6 +35,8 @@ function formatType(c: PlaygroundControl): string {
33
35
  return "number";
34
36
  case "string":
35
37
  return "string";
38
+ case "node":
39
+ return "ReactNode";
36
40
  case "select":
37
41
  return "string";
38
42
  default:
@@ -5,21 +5,21 @@ 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 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
8
+ "inline-flex rounded-full 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: {
12
12
  default:
13
- "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
13
+ "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
14
14
  secondary:
15
15
  "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
16
16
  destructive:
17
- "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
17
+ "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
18
18
  outline: "text-foreground",
19
19
  },
20
20
  size: {
21
- default: "px-2.5 py-0.5 text-xs rounded-sm",
22
- sm: "px-1.5 py-0.5 font-mono text-[10px] font-normal leading-tight rounded-sm",
21
+ default: "px-3 py-0.5 text-xs",
22
+ sm: "px-2 py-0.5 font-mono text-xs font-normal leading-tight",
23
23
  },
24
24
  },
25
25
  defaultVariants: {
@@ -22,6 +22,7 @@ import {
22
22
  catalogAttributeProps,
23
23
  ComponentPropUsageDetail,
24
24
  buildUnusedPropSetForComponent,
25
+ propFrequenciesForComponent,
25
26
  } from "./ComponentPropUsageDetail";
26
27
  import { shortPath } from "./paths";
27
28
  import type { WorkspaceReport } from "../types/report";
@@ -32,11 +33,13 @@ const catalogHoverTriggerClass =
32
33
  "cursor-default text-xs underline decoration-dotted underline-offset-2 hover:text-foreground";
33
34
 
34
35
  function CatalogPropUsageHover({
36
+ report,
35
37
  component,
36
38
  declared,
37
39
  unusedProps,
38
40
  usedPropCount,
39
41
  }: {
42
+ report: WorkspaceReport;
40
43
  component: string;
41
44
  declared: string[];
42
45
  unusedProps: Set<string>;
@@ -54,11 +57,16 @@ function CatalogPropUsageHover({
54
57
  </span>
55
58
  </button>
56
59
  </HoverCardTrigger>
57
- <HoverCardContent align="start" className="w-56 p-3">
60
+ <HoverCardContent align="start" className="w-64 p-3">
58
61
  <ComponentPropUsageDetail
59
62
  component={component}
60
63
  declared={declared}
61
64
  unusedProps={unusedProps}
65
+ propFrequencies={propFrequenciesForComponent(
66
+ report,
67
+ component,
68
+ )}
69
+ variant="compact"
62
70
  />
63
71
  </HoverCardContent>
64
72
  </HoverCard>
@@ -159,6 +167,7 @@ export function ComponentCatalog({
159
167
  <TableCell>
160
168
  {attributeProps.length > 0 ? (
161
169
  <CatalogPropUsageHover
170
+ report={report}
162
171
  component={name}
163
172
  declared={declared}
164
173
  unusedProps={unusedProps}