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
|
@@ -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(
|
|
81
|
-
|
|
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 ||
|
|
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-
|
|
109
|
-
<div className="sticky top-0 z-10 shrink-0 border-b border-
|
|
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-
|
|
112
|
-
<svg
|
|
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-
|
|
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
|
|
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">
|
|
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 =
|
|
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={() =>
|
|
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 =
|
|
266
|
+
const parentActive =
|
|
267
|
+
route.view === "component" && route.componentId === item.parent;
|
|
214
268
|
const childActive =
|
|
215
|
-
route.view === "component" &&
|
|
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.
|
|
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:
|
|
282
|
+
componentId: resolveFamilyNavigationTarget(
|
|
283
|
+
item,
|
|
284
|
+
catalogNames,
|
|
285
|
+
),
|
|
226
286
|
})
|
|
227
287
|
}
|
|
228
|
-
className={familyParentButtonClass(
|
|
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(
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
40
|
-
{
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
<
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
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
|
|
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-
|
|
22
|
-
sm: "px-
|
|
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-
|
|
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}
|