dslinter 0.0.6
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 +76 -0
- package/LICENSE +201 -0
- package/README.md +104 -0
- package/bin/dslinter.mjs +29 -0
- package/components.json +20 -0
- package/package.json +90 -0
- package/src/components/InlineCode.tsx +5 -0
- package/src/components/icons.tsx +121 -0
- package/src/components/ui/badge.tsx +52 -0
- package/src/components/ui/button.tsx +57 -0
- package/src/components/ui/checkbox.tsx +25 -0
- package/src/components/ui/command.tsx +183 -0
- package/src/components/ui/dialog.tsx +156 -0
- package/src/components/ui/hover-card.tsx +42 -0
- package/src/components/ui/input.tsx +22 -0
- package/src/components/ui/label.tsx +19 -0
- package/src/components/ui/select.tsx +149 -0
- package/src/components/ui/table.tsx +118 -0
- package/src/components/ui/toggle-group.tsx +83 -0
- package/src/components/ui/toggle.tsx +45 -0
- package/src/dashboard/ComponentCatalog.tsx +210 -0
- package/src/dashboard/ComponentUsageDetails.tsx +109 -0
- package/src/dashboard/DashboardBody.tsx +71 -0
- package/src/dashboard/FindingsList.tsx +151 -0
- package/src/dashboard/ScoreStrip.tsx +28 -0
- package/src/dashboard/TokenWall.tsx +241 -0
- package/src/dashboard/aggregate.ts +73 -0
- package/src/dashboard/paths.ts +10 -0
- package/src/dashboard/useWorkspaceReport.ts +136 -0
- package/src/index.ts +67 -0
- package/src/lib/utils.ts +6 -0
- package/src/playground/definePlayground.tsx +99 -0
- package/src/playground/enumerateControlCombinations.test.ts +112 -0
- package/src/playground/enumerateControlCombinations.ts +74 -0
- package/src/report/a11yForModule.ts +35 -0
- package/src/report/codeScoreForModule.ts +41 -0
- package/src/report/modulePathMatch.ts +27 -0
- package/src/report/tokenStyleFindingsForModule.ts +24 -0
- package/src/shell/ComponentPlaygroundPane.tsx +438 -0
- package/src/shell/DashboardCommandPalette.tsx +134 -0
- package/src/shell/DashboardLayout.tsx +230 -0
- package/src/shell/EmptyCard.tsx +21 -0
- package/src/shell/GovernancePane.tsx +77 -0
- package/src/shell/PlaygroundA11yAndCode.tsx +387 -0
- package/src/shell/PlaygroundControlField.tsx +213 -0
- package/src/shell/PlaygroundControls.tsx +66 -0
- package/src/shell/PlaygroundUsageCode.tsx +51 -0
- package/src/shell/PlaygroundVariantMatrix.tsx +68 -0
- package/src/shell/Section.tsx +34 -0
- package/src/shell/Sidebar.tsx +203 -0
- package/src/shell/TokensPane.tsx +26 -0
- package/src/shell/controlApiTable.ts +53 -0
- package/src/shell/hashRoute.ts +49 -0
- package/src/shell/playgroundUsageHighlight.ts +53 -0
- package/src/shell/playgroundUsageTwoslash.ts +69 -0
- package/src/shell/useHashRoute.ts +29 -0
- package/src/styles/dashboard-theme.css +188 -0
- package/src/types/controls.ts +62 -0
- package/src/types/defaultTailwindTypography.ts +55 -0
- package/src/types/playground.ts +21 -0
- package/src/types/preview.ts +8 -0
- package/src/types/report.ts +116 -0
- package/src/types/tokenCatalog.ts +54 -0
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useCallback,
|
|
3
|
+
useEffect,
|
|
4
|
+
useLayoutEffect,
|
|
5
|
+
useMemo,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
type MouseEvent,
|
|
9
|
+
type ReactNode,
|
|
10
|
+
} from "react";
|
|
11
|
+
import { detectBreakpoint, detectContainerBreakpoint } from "usemods";
|
|
12
|
+
import { a11ySummaryForModule } from "../report/a11yForModule";
|
|
13
|
+
import { codeScoreSummaryForModule } from "../report/codeScoreForModule";
|
|
14
|
+
import { tokenStyleFindingsForModule } from "../report/tokenStyleFindingsForModule";
|
|
15
|
+
import type { WorkspaceReport } from "../types/report";
|
|
16
|
+
import { aggregateDeclaredProps, usageMap } from "../dashboard/aggregate";
|
|
17
|
+
import { defaultArgsFromControls } from "../types/controls";
|
|
18
|
+
import type { PlaygroundArgs } from "../types/controls";
|
|
19
|
+
import type { PlaygroundEntry } from "../types/playground";
|
|
20
|
+
import { ComponentUsageDetails } from "../dashboard/ComponentUsageDetails";
|
|
21
|
+
import {
|
|
22
|
+
PlaygroundA11ySection,
|
|
23
|
+
PlaygroundApiReference,
|
|
24
|
+
PlaygroundCodeScoreSection,
|
|
25
|
+
PlaygroundTokenStyleSection,
|
|
26
|
+
PlaygroundUsageSection,
|
|
27
|
+
} from "./PlaygroundA11yAndCode";
|
|
28
|
+
import { PlaygroundVariantMatrix } from "./PlaygroundVariantMatrix";
|
|
29
|
+
import { enumerateControlCombinations } from "../playground/enumerateControlCombinations";
|
|
30
|
+
import { Section } from "./Section";
|
|
31
|
+
|
|
32
|
+
type Props = {
|
|
33
|
+
entry: PlaygroundEntry;
|
|
34
|
+
formatModulePath?: (modulePath: string) => string;
|
|
35
|
+
workspaceReport: WorkspaceReport | null;
|
|
36
|
+
reportReady: boolean;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const MIN_PREVIEW_PX = 280;
|
|
40
|
+
const DEFAULT_PREVIEW_PX = 1024;
|
|
41
|
+
|
|
42
|
+
function clampPreviewWidth(w: number, maxOuterPx: number): number {
|
|
43
|
+
if (!Number.isFinite(maxOuterPx) || maxOuterPx <= 0) return w;
|
|
44
|
+
const minBound = Math.min(MIN_PREVIEW_PX, maxOuterPx);
|
|
45
|
+
return Math.min(Math.max(w, minBound), maxOuterPx);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** If the preview was flush with the previous container width, grow/shrink with the viewport. */
|
|
49
|
+
function nextPreviewWidthForResize(
|
|
50
|
+
prevPreview: number,
|
|
51
|
+
prevOuter: number,
|
|
52
|
+
nextOuter: number,
|
|
53
|
+
): number {
|
|
54
|
+
if (prevOuter <= 0) return clampPreviewWidth(nextOuter, nextOuter);
|
|
55
|
+
if (nextOuter < prevOuter) return clampPreviewWidth(prevPreview, nextOuter);
|
|
56
|
+
if (Math.abs(prevPreview - prevOuter) <= 2)
|
|
57
|
+
return clampPreviewWidth(nextOuter, nextOuter);
|
|
58
|
+
return clampPreviewWidth(prevPreview, nextOuter);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function TocLink({ href, children }: { href: string; children: ReactNode }) {
|
|
62
|
+
const handleClick = (e: MouseEvent<HTMLAnchorElement>) => {
|
|
63
|
+
// Modifier keys / non-primary clicks → fall back to default browser behaviour.
|
|
64
|
+
if (
|
|
65
|
+
e.defaultPrevented ||
|
|
66
|
+
e.button !== 0 ||
|
|
67
|
+
e.metaKey ||
|
|
68
|
+
e.ctrlKey ||
|
|
69
|
+
e.shiftKey ||
|
|
70
|
+
e.altKey
|
|
71
|
+
) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const id = href.startsWith("#") ? href.slice(1) : href;
|
|
75
|
+
const target = id ? document.getElementById(id) : null;
|
|
76
|
+
if (!target) return;
|
|
77
|
+
e.preventDefault();
|
|
78
|
+
target.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
79
|
+
if (typeof window !== "undefined" && window.history?.replaceState) {
|
|
80
|
+
window.history.replaceState(null, "", `#${id}`);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<a
|
|
86
|
+
href={href}
|
|
87
|
+
onClick={handleClick}
|
|
88
|
+
className="block rounded-md py-1 text-muted-foreground transition hover:bg-accent hover:text-accent-foreground"
|
|
89
|
+
>
|
|
90
|
+
{children}
|
|
91
|
+
</a>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function ComponentPlaygroundPane({
|
|
96
|
+
entry,
|
|
97
|
+
formatModulePath,
|
|
98
|
+
workspaceReport,
|
|
99
|
+
reportReady,
|
|
100
|
+
}: Props) {
|
|
101
|
+
const { Preview } = entry;
|
|
102
|
+
const rel = formatModulePath
|
|
103
|
+
? formatModulePath(entry.modulePath)
|
|
104
|
+
: entry.modulePath.replace(/^\.\.\//, "");
|
|
105
|
+
|
|
106
|
+
const [values, setValues] = useState<PlaygroundArgs>(() =>
|
|
107
|
+
defaultArgsFromControls(entry.controls),
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
setValues(defaultArgsFromControls(entry.controls));
|
|
112
|
+
}, [entry.id]);
|
|
113
|
+
|
|
114
|
+
const a11y = useMemo(
|
|
115
|
+
() =>
|
|
116
|
+
a11ySummaryForModule(
|
|
117
|
+
reportReady ? workspaceReport : null,
|
|
118
|
+
entry.modulePath,
|
|
119
|
+
),
|
|
120
|
+
[workspaceReport, entry.modulePath, reportReady],
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const tokenStyleFindings = useMemo(
|
|
124
|
+
() =>
|
|
125
|
+
tokenStyleFindingsForModule(
|
|
126
|
+
reportReady ? workspaceReport : null,
|
|
127
|
+
entry.modulePath,
|
|
128
|
+
),
|
|
129
|
+
[workspaceReport, entry.modulePath, reportReady],
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const codeScore = useMemo(
|
|
133
|
+
() =>
|
|
134
|
+
codeScoreSummaryForModule(
|
|
135
|
+
reportReady ? workspaceReport : null,
|
|
136
|
+
entry.modulePath,
|
|
137
|
+
),
|
|
138
|
+
[workspaceReport, entry.modulePath, reportReady],
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
const repoUsage = useMemo(() => {
|
|
142
|
+
if (!reportReady || !workspaceReport) return undefined;
|
|
143
|
+
return usageMap(workspaceReport).get(entry.id);
|
|
144
|
+
}, [workspaceReport, entry.id, reportReady]);
|
|
145
|
+
|
|
146
|
+
const declaredPropsFromScan = useMemo(() => {
|
|
147
|
+
if (!reportReady || !workspaceReport) return [];
|
|
148
|
+
return aggregateDeclaredProps(workspaceReport).get(entry.id) ?? [];
|
|
149
|
+
}, [workspaceReport, entry.id, reportReady]);
|
|
150
|
+
|
|
151
|
+
const previewMeasureRef = useRef<HTMLDivElement>(null);
|
|
152
|
+
const previewFrameRef = useRef<HTMLDivElement>(null);
|
|
153
|
+
const maxOuterRef = useRef(0);
|
|
154
|
+
const [maxOuterPx, setMaxOuterPx] = useState(0);
|
|
155
|
+
const [previewWidthPx, setPreviewWidthPx] = useState(DEFAULT_PREVIEW_PX);
|
|
156
|
+
const [windowBreakpoint, setWindowBreakpoint] = useState<string | null>(null);
|
|
157
|
+
const [containerBreakpoint, setContainerBreakpoint] = useState<string | null>(
|
|
158
|
+
null,
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
const syncPreviewToOuterWidth = useCallback((nextOuter: number) => {
|
|
162
|
+
if (!Number.isFinite(nextOuter) || nextOuter <= 0) return;
|
|
163
|
+
const prevOuter = maxOuterRef.current;
|
|
164
|
+
maxOuterRef.current = nextOuter;
|
|
165
|
+
setMaxOuterPx(nextOuter);
|
|
166
|
+
setPreviewWidthPx((pw) =>
|
|
167
|
+
nextPreviewWidthForResize(pw, prevOuter, nextOuter),
|
|
168
|
+
);
|
|
169
|
+
}, []);
|
|
170
|
+
|
|
171
|
+
useLayoutEffect(() => {
|
|
172
|
+
const el = previewMeasureRef.current;
|
|
173
|
+
if (!el) return;
|
|
174
|
+
const ro = new ResizeObserver(() => {
|
|
175
|
+
syncPreviewToOuterWidth(el.clientWidth);
|
|
176
|
+
});
|
|
177
|
+
ro.observe(el);
|
|
178
|
+
syncPreviewToOuterWidth(el.clientWidth);
|
|
179
|
+
return () => ro.disconnect();
|
|
180
|
+
}, [syncPreviewToOuterWidth]);
|
|
181
|
+
|
|
182
|
+
const syncUsemodsBreakpoints = useCallback(() => {
|
|
183
|
+
setWindowBreakpoint(detectBreakpoint());
|
|
184
|
+
const frame = previewFrameRef.current;
|
|
185
|
+
setContainerBreakpoint(frame ? detectContainerBreakpoint(frame) : null);
|
|
186
|
+
}, []);
|
|
187
|
+
|
|
188
|
+
useLayoutEffect(() => {
|
|
189
|
+
const frame = previewFrameRef.current;
|
|
190
|
+
if (!frame) return;
|
|
191
|
+
const ro = new ResizeObserver(() => {
|
|
192
|
+
syncUsemodsBreakpoints();
|
|
193
|
+
});
|
|
194
|
+
ro.observe(frame);
|
|
195
|
+
syncUsemodsBreakpoints();
|
|
196
|
+
return () => ro.disconnect();
|
|
197
|
+
}, [syncUsemodsBreakpoints]);
|
|
198
|
+
|
|
199
|
+
useEffect(() => {
|
|
200
|
+
syncUsemodsBreakpoints();
|
|
201
|
+
window.addEventListener("resize", syncUsemodsBreakpoints);
|
|
202
|
+
return () => window.removeEventListener("resize", syncUsemodsBreakpoints);
|
|
203
|
+
}, [syncUsemodsBreakpoints]);
|
|
204
|
+
|
|
205
|
+
const attachSymmetricWidthDrag = useCallback((side: "left" | "right") => {
|
|
206
|
+
return (e: React.PointerEvent<HTMLButtonElement>) => {
|
|
207
|
+
if (e.button !== 0) return;
|
|
208
|
+
e.preventDefault();
|
|
209
|
+
const target = e.currentTarget;
|
|
210
|
+
target.setPointerCapture(e.pointerId);
|
|
211
|
+
let lastX = e.clientX;
|
|
212
|
+
const sign = side === "right" ? 1 : -1;
|
|
213
|
+
const onMove = (ev: PointerEvent) => {
|
|
214
|
+
const dx = ev.clientX - lastX;
|
|
215
|
+
lastX = ev.clientX;
|
|
216
|
+
setPreviewWidthPx((w) =>
|
|
217
|
+
clampPreviewWidth(w + sign * 2 * dx, maxOuterRef.current),
|
|
218
|
+
);
|
|
219
|
+
};
|
|
220
|
+
const onUp = (ev: PointerEvent) => {
|
|
221
|
+
target.releasePointerCapture(ev.pointerId);
|
|
222
|
+
window.removeEventListener("pointermove", onMove);
|
|
223
|
+
window.removeEventListener("pointerup", onUp);
|
|
224
|
+
window.removeEventListener("pointercancel", onUp);
|
|
225
|
+
};
|
|
226
|
+
window.addEventListener("pointermove", onMove);
|
|
227
|
+
window.addEventListener("pointerup", onUp);
|
|
228
|
+
window.addEventListener("pointercancel", onUp);
|
|
229
|
+
};
|
|
230
|
+
}, []);
|
|
231
|
+
|
|
232
|
+
const hasControls = entry.controls.length > 0;
|
|
233
|
+
|
|
234
|
+
const variantEnumeration = useMemo(
|
|
235
|
+
() => enumerateControlCombinations(entry.controls, values),
|
|
236
|
+
[entry.controls, values],
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
const showVariantsSection =
|
|
240
|
+
hasControls &&
|
|
241
|
+
(variantEnumeration.combinations.length > 0 ||
|
|
242
|
+
(variantEnumeration.combinations.length === 0 &&
|
|
243
|
+
variantEnumeration.totalCount === 0));
|
|
244
|
+
|
|
245
|
+
return (
|
|
246
|
+
<div className="flex min-h-0 flex-1 flex-col overflow-hidden bg-background">
|
|
247
|
+
<div className="min-h-0 flex-1 overflow-auto">
|
|
248
|
+
<header id="source" className="scroll-mt-20 border-b border-border bg-card p-6">
|
|
249
|
+
<div className="mx-auto">
|
|
250
|
+
<div className="flex flex-wrap items-start justify-between gap-4">
|
|
251
|
+
<div className="min-w-0">
|
|
252
|
+
<p className="text-sm font-medium text-muted-foreground">
|
|
253
|
+
{entry.meta.group ? (
|
|
254
|
+
<>
|
|
255
|
+
Components <span className="text-muted-foreground/40">/</span>{" "}
|
|
256
|
+
<span className="capitalize text-foreground/80">
|
|
257
|
+
{entry.meta.group}
|
|
258
|
+
</span>
|
|
259
|
+
</>
|
|
260
|
+
) : (
|
|
261
|
+
"Components"
|
|
262
|
+
)}
|
|
263
|
+
</p>
|
|
264
|
+
<h1 className="text-3xl font-semibold tracking-tight text-foreground">
|
|
265
|
+
{entry.meta.title}
|
|
266
|
+
</h1>
|
|
267
|
+
<p
|
|
268
|
+
className="mt-1 truncate font-mono text-xs text-muted-foreground"
|
|
269
|
+
title={rel}
|
|
270
|
+
>
|
|
271
|
+
{rel}
|
|
272
|
+
</p>
|
|
273
|
+
</div>
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
</header>
|
|
277
|
+
|
|
278
|
+
<section
|
|
279
|
+
id="examples"
|
|
280
|
+
className="ds-playground-dot-surface border-b px-16 py-10"
|
|
281
|
+
>
|
|
282
|
+
<div ref={previewMeasureRef}>
|
|
283
|
+
<div className="flex justify-center">
|
|
284
|
+
<div
|
|
285
|
+
ref={previewFrameRef}
|
|
286
|
+
className="relative min-w-0 shrink-0 select-none rounded-lg border border-border bg-muted/50 shadow-xs"
|
|
287
|
+
style={{ width: previewWidthPx }}
|
|
288
|
+
>
|
|
289
|
+
<button
|
|
290
|
+
type="button"
|
|
291
|
+
className="absolute left-0 top-0 bottom-0 z-10 flex w-4 -translate-x-1/2 cursor-ew-resize touch-none items-center justify-center rounded border-0 bg-muted p-0 shadow-xs ring-1 ring-border hover:bg-accent focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
292
|
+
aria-label="Resize preview from center (drag left or right)"
|
|
293
|
+
onPointerDown={attachSymmetricWidthDrag("left")}
|
|
294
|
+
>
|
|
295
|
+
<span
|
|
296
|
+
className="h-10 w-px rounded-full bg-muted-foreground/40"
|
|
297
|
+
aria-hidden
|
|
298
|
+
/>
|
|
299
|
+
</button>
|
|
300
|
+
<button
|
|
301
|
+
type="button"
|
|
302
|
+
className="absolute right-0 top-0 bottom-0 z-10 flex w-4 translate-x-1/2 cursor-ew-resize touch-none items-center justify-center rounded border-0 bg-muted p-0 shadow-xs ring-1 ring-border hover:bg-accent focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
303
|
+
aria-label="Resize preview from center (drag left or right)"
|
|
304
|
+
onPointerDown={attachSymmetricWidthDrag("right")}
|
|
305
|
+
>
|
|
306
|
+
<span
|
|
307
|
+
className="h-10 w-px rounded-full bg-muted-foreground/40"
|
|
308
|
+
aria-hidden
|
|
309
|
+
/>
|
|
310
|
+
</button>
|
|
311
|
+
<div className="min-w-0 bg-card p-8">
|
|
312
|
+
<Preview values={values} />
|
|
313
|
+
</div>
|
|
314
|
+
</div>
|
|
315
|
+
</div>
|
|
316
|
+
{maxOuterPx > 0 ? (
|
|
317
|
+
<div className="mt-4 divide-x divide-border h-6 overflow-hidden items-center mx-auto flex w-fit border border-border bg-card text-center text-xs/none tabular-nums font-mono rounded-sm text-muted-foreground">
|
|
318
|
+
<span className="p-2.5">{Math.round(previewWidthPx)}px</span>
|
|
319
|
+
<span className=" p-2.5" title="usemods detectBreakpoint">
|
|
320
|
+
Screen: {windowBreakpoint ?? "—"}
|
|
321
|
+
</span>
|
|
322
|
+
<span
|
|
323
|
+
className=" p-2.5"
|
|
324
|
+
title="usemods detectContainerBreakpoint"
|
|
325
|
+
>
|
|
326
|
+
Container: {containerBreakpoint ?? "—"}
|
|
327
|
+
</span>
|
|
328
|
+
</div>
|
|
329
|
+
) : null}
|
|
330
|
+
</div>
|
|
331
|
+
</section>
|
|
332
|
+
|
|
333
|
+
<div className="min-w-0 w-full px-6 py-10 lg:px-12">
|
|
334
|
+
<div className="xl:grid xl:grid-cols-[minmax(0,1fr)_12rem] xl:gap-14">
|
|
335
|
+
<div className="min-w-0 space-y-14">
|
|
336
|
+
{hasControls ? (
|
|
337
|
+
<PlaygroundApiReference
|
|
338
|
+
controls={entry.controls}
|
|
339
|
+
values={values}
|
|
340
|
+
onChange={setValues}
|
|
341
|
+
onReset={() =>
|
|
342
|
+
setValues(defaultArgsFromControls(entry.controls))
|
|
343
|
+
}
|
|
344
|
+
reportUsage={repoUsage}
|
|
345
|
+
declaredPropsFromScan={declaredPropsFromScan}
|
|
346
|
+
governanceReportLoaded={
|
|
347
|
+
reportReady && workspaceReport != null
|
|
348
|
+
}
|
|
349
|
+
/>
|
|
350
|
+
) : null}
|
|
351
|
+
|
|
352
|
+
<PlaygroundUsageSection entry={entry} values={values} />
|
|
353
|
+
|
|
354
|
+
<Section id="repo-usage" title="Repo usage" description="">
|
|
355
|
+
<ComponentUsageDetails
|
|
356
|
+
report={reportReady ? workspaceReport : null}
|
|
357
|
+
componentId={entry.id}
|
|
358
|
+
/>
|
|
359
|
+
</Section>
|
|
360
|
+
|
|
361
|
+
<Section
|
|
362
|
+
id="design-tokens"
|
|
363
|
+
title="Design tokens and colors"
|
|
364
|
+
description="Hardcoded colors and non-token styling flagged by DSLinter for this module's source file."
|
|
365
|
+
>
|
|
366
|
+
<PlaygroundTokenStyleSection
|
|
367
|
+
findings={tokenStyleFindings}
|
|
368
|
+
reportReady={reportReady}
|
|
369
|
+
/>
|
|
370
|
+
</Section>
|
|
371
|
+
|
|
372
|
+
<Section
|
|
373
|
+
id="code-score"
|
|
374
|
+
title={`Code score: ${reportReady ? codeScore.score : "—"}/100`}
|
|
375
|
+
description="Static quality rules and findings from the workspace DSLinter report scoped to this file."
|
|
376
|
+
>
|
|
377
|
+
<PlaygroundCodeScoreSection
|
|
378
|
+
codeScore={codeScore}
|
|
379
|
+
reportReady={reportReady}
|
|
380
|
+
/>
|
|
381
|
+
</Section>
|
|
382
|
+
|
|
383
|
+
<Section
|
|
384
|
+
id="accessibility"
|
|
385
|
+
title={`Accessibility: ${reportReady ? a11y.score : "—"}/100`}
|
|
386
|
+
description="Accessibility checks and findings from the workspace DSLinter report scoped to this file."
|
|
387
|
+
>
|
|
388
|
+
<PlaygroundA11ySection a11y={a11y} reportReady={reportReady} />
|
|
389
|
+
</Section>
|
|
390
|
+
</div>
|
|
391
|
+
|
|
392
|
+
<aside className="mt-12 hidden self-start sticky top-8 xl:mt-0 xl:block">
|
|
393
|
+
<nav
|
|
394
|
+
aria-label="On this page"
|
|
395
|
+
className="space-y-0.5 border-l border-border pl-4 text-sm"
|
|
396
|
+
>
|
|
397
|
+
<p className="mb-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
398
|
+
On this page
|
|
399
|
+
</p>
|
|
400
|
+
{hasControls ? (
|
|
401
|
+
<TocLink href="#api-reference">API reference</TocLink>
|
|
402
|
+
) : null}
|
|
403
|
+
<TocLink href="#usage">Usage</TocLink>
|
|
404
|
+
<TocLink href="#repo-usage">Repo usage</TocLink>
|
|
405
|
+
<TocLink href="#design-tokens">Design tokens</TocLink>
|
|
406
|
+
<TocLink href="#code-score">Code score</TocLink>
|
|
407
|
+
<TocLink href="#accessibility">Accessibility</TocLink>
|
|
408
|
+
{showVariantsSection ? (
|
|
409
|
+
<TocLink href="#variants">Variants</TocLink>
|
|
410
|
+
) : null}
|
|
411
|
+
</nav>
|
|
412
|
+
</aside>
|
|
413
|
+
</div>
|
|
414
|
+
</div>
|
|
415
|
+
|
|
416
|
+
{variantEnumeration.combinations.length > 0 ? (
|
|
417
|
+
<section
|
|
418
|
+
id="variants"
|
|
419
|
+
className="ds-playground-dot-surface mt-8 w-full scroll-mt-20 border-t pt-10 pb-12"
|
|
420
|
+
>
|
|
421
|
+
<div className="min-w-0 w-full px-6 lg:px-12">
|
|
422
|
+
<h2 className="w-fit bg-card text-xl font-semibold tracking-tight text-foreground">
|
|
423
|
+
All variants
|
|
424
|
+
</h2>
|
|
425
|
+
<PlaygroundVariantMatrix
|
|
426
|
+
Preview={Preview}
|
|
427
|
+
combinations={variantEnumeration.combinations}
|
|
428
|
+
finiteAxisKeys={variantEnumeration.finiteAxisKeys}
|
|
429
|
+
totalCount={variantEnumeration.totalCount}
|
|
430
|
+
capped={variantEnumeration.capped}
|
|
431
|
+
/>
|
|
432
|
+
</div>
|
|
433
|
+
</section>
|
|
434
|
+
) : null}
|
|
435
|
+
</div>
|
|
436
|
+
</div>
|
|
437
|
+
);
|
|
438
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { useCallback, useEffect } from "react";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
CommandDialog,
|
|
5
|
+
CommandEmpty,
|
|
6
|
+
CommandGroup,
|
|
7
|
+
CommandInput,
|
|
8
|
+
CommandItem,
|
|
9
|
+
CommandList,
|
|
10
|
+
} from "@/components/ui/command";
|
|
11
|
+
|
|
12
|
+
import type { PlaygroundEntry } from "../types/playground";
|
|
13
|
+
import type { HashRoute } from "./hashRoute";
|
|
14
|
+
|
|
15
|
+
type Props = {
|
|
16
|
+
entries: PlaygroundEntry[];
|
|
17
|
+
onNavigate: (next: HashRoute) => void;
|
|
18
|
+
open: boolean;
|
|
19
|
+
onOpenChange: (open: boolean) => void;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function eventTargetIsEditable(target: EventTarget | null): boolean {
|
|
23
|
+
if (!target || !(target instanceof HTMLElement)) return false;
|
|
24
|
+
if (target.isContentEditable) return true;
|
|
25
|
+
const tag = target.tagName;
|
|
26
|
+
return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type ComponentChunk = {
|
|
30
|
+
heading: string;
|
|
31
|
+
entries: PlaygroundEntry[];
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function chunkPlaygroundEntries(entries: PlaygroundEntry[]): ComponentChunk[] {
|
|
35
|
+
if (entries.length === 0) return [];
|
|
36
|
+
const chunks: ComponentChunk[] = [];
|
|
37
|
+
let chunk: PlaygroundEntry[] = [];
|
|
38
|
+
let heading = "Components";
|
|
39
|
+
|
|
40
|
+
for (let i = 0; i < entries.length; i++) {
|
|
41
|
+
const e = entries[i];
|
|
42
|
+
const prev = i > 0 ? entries[i - 1] : undefined;
|
|
43
|
+
const showGroup =
|
|
44
|
+
Boolean(e.meta.group) &&
|
|
45
|
+
(prev == null || prev.meta.group !== e.meta.group);
|
|
46
|
+
|
|
47
|
+
if (showGroup && chunk.length > 0) {
|
|
48
|
+
chunks.push({ heading, entries: chunk });
|
|
49
|
+
chunk = [];
|
|
50
|
+
heading = e.meta.group ?? "Components";
|
|
51
|
+
} else if (showGroup && chunk.length === 0) {
|
|
52
|
+
heading = e.meta.group ?? "Components";
|
|
53
|
+
}
|
|
54
|
+
chunk.push(e);
|
|
55
|
+
}
|
|
56
|
+
if (chunk.length > 0) {
|
|
57
|
+
chunks.push({ heading, entries: chunk });
|
|
58
|
+
}
|
|
59
|
+
return chunks;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function DashboardCommandPalette({
|
|
63
|
+
entries,
|
|
64
|
+
onNavigate,
|
|
65
|
+
open,
|
|
66
|
+
onOpenChange,
|
|
67
|
+
}: Props) {
|
|
68
|
+
const close = useCallback(() => onOpenChange(false), [onOpenChange]);
|
|
69
|
+
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
const onKeyDown = (e: KeyboardEvent) => {
|
|
72
|
+
if (!(e.metaKey || e.ctrlKey) || e.key !== "k") return;
|
|
73
|
+
if (!open && eventTargetIsEditable(e.target)) return;
|
|
74
|
+
e.preventDefault();
|
|
75
|
+
onOpenChange(!open);
|
|
76
|
+
};
|
|
77
|
+
document.addEventListener("keydown", onKeyDown);
|
|
78
|
+
return () => document.removeEventListener("keydown", onKeyDown);
|
|
79
|
+
}, [open, onOpenChange]);
|
|
80
|
+
|
|
81
|
+
const componentChunks = chunkPlaygroundEntries(entries);
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<CommandDialog
|
|
85
|
+
open={open}
|
|
86
|
+
onOpenChange={onOpenChange}
|
|
87
|
+
title="Jump to"
|
|
88
|
+
description="Search components and views"
|
|
89
|
+
>
|
|
90
|
+
<CommandInput placeholder="Search components…" />
|
|
91
|
+
<CommandList>
|
|
92
|
+
<CommandEmpty>No matches.</CommandEmpty>
|
|
93
|
+
<CommandGroup heading="Explore">
|
|
94
|
+
<CommandItem
|
|
95
|
+
value="governance explore overview"
|
|
96
|
+
onSelect={() => {
|
|
97
|
+
onNavigate({ view: "governance" });
|
|
98
|
+
close();
|
|
99
|
+
}}
|
|
100
|
+
>
|
|
101
|
+
Governance
|
|
102
|
+
</CommandItem>
|
|
103
|
+
</CommandGroup>
|
|
104
|
+
{componentChunks.map((c, chunkIndex) => (
|
|
105
|
+
<CommandGroup key={`${c.heading}-${chunkIndex}`} heading={c.heading}>
|
|
106
|
+
{c.entries.map((entry) => (
|
|
107
|
+
<CommandItem
|
|
108
|
+
key={entry.id}
|
|
109
|
+
value={`${entry.id} ${entry.meta.title} ${entry.meta.group ?? ""}`}
|
|
110
|
+
onSelect={() => {
|
|
111
|
+
onNavigate({ view: "component", componentId: entry.id });
|
|
112
|
+
close();
|
|
113
|
+
}}
|
|
114
|
+
>
|
|
115
|
+
{entry.meta.title}
|
|
116
|
+
</CommandItem>
|
|
117
|
+
))}
|
|
118
|
+
</CommandGroup>
|
|
119
|
+
))}
|
|
120
|
+
<CommandGroup heading="System">
|
|
121
|
+
<CommandItem
|
|
122
|
+
value="tokens design system colors"
|
|
123
|
+
onSelect={() => {
|
|
124
|
+
onNavigate({ view: "tokens" });
|
|
125
|
+
close();
|
|
126
|
+
}}
|
|
127
|
+
>
|
|
128
|
+
Design tokens
|
|
129
|
+
</CommandItem>
|
|
130
|
+
</CommandGroup>
|
|
131
|
+
</CommandList>
|
|
132
|
+
</CommandDialog>
|
|
133
|
+
);
|
|
134
|
+
}
|