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.
Files changed (63) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/LICENSE +201 -0
  3. package/README.md +104 -0
  4. package/bin/dslinter.mjs +29 -0
  5. package/components.json +20 -0
  6. package/package.json +90 -0
  7. package/src/components/InlineCode.tsx +5 -0
  8. package/src/components/icons.tsx +121 -0
  9. package/src/components/ui/badge.tsx +52 -0
  10. package/src/components/ui/button.tsx +57 -0
  11. package/src/components/ui/checkbox.tsx +25 -0
  12. package/src/components/ui/command.tsx +183 -0
  13. package/src/components/ui/dialog.tsx +156 -0
  14. package/src/components/ui/hover-card.tsx +42 -0
  15. package/src/components/ui/input.tsx +22 -0
  16. package/src/components/ui/label.tsx +19 -0
  17. package/src/components/ui/select.tsx +149 -0
  18. package/src/components/ui/table.tsx +118 -0
  19. package/src/components/ui/toggle-group.tsx +83 -0
  20. package/src/components/ui/toggle.tsx +45 -0
  21. package/src/dashboard/ComponentCatalog.tsx +210 -0
  22. package/src/dashboard/ComponentUsageDetails.tsx +109 -0
  23. package/src/dashboard/DashboardBody.tsx +71 -0
  24. package/src/dashboard/FindingsList.tsx +151 -0
  25. package/src/dashboard/ScoreStrip.tsx +28 -0
  26. package/src/dashboard/TokenWall.tsx +241 -0
  27. package/src/dashboard/aggregate.ts +73 -0
  28. package/src/dashboard/paths.ts +10 -0
  29. package/src/dashboard/useWorkspaceReport.ts +136 -0
  30. package/src/index.ts +67 -0
  31. package/src/lib/utils.ts +6 -0
  32. package/src/playground/definePlayground.tsx +99 -0
  33. package/src/playground/enumerateControlCombinations.test.ts +112 -0
  34. package/src/playground/enumerateControlCombinations.ts +74 -0
  35. package/src/report/a11yForModule.ts +35 -0
  36. package/src/report/codeScoreForModule.ts +41 -0
  37. package/src/report/modulePathMatch.ts +27 -0
  38. package/src/report/tokenStyleFindingsForModule.ts +24 -0
  39. package/src/shell/ComponentPlaygroundPane.tsx +438 -0
  40. package/src/shell/DashboardCommandPalette.tsx +134 -0
  41. package/src/shell/DashboardLayout.tsx +230 -0
  42. package/src/shell/EmptyCard.tsx +21 -0
  43. package/src/shell/GovernancePane.tsx +77 -0
  44. package/src/shell/PlaygroundA11yAndCode.tsx +387 -0
  45. package/src/shell/PlaygroundControlField.tsx +213 -0
  46. package/src/shell/PlaygroundControls.tsx +66 -0
  47. package/src/shell/PlaygroundUsageCode.tsx +51 -0
  48. package/src/shell/PlaygroundVariantMatrix.tsx +68 -0
  49. package/src/shell/Section.tsx +34 -0
  50. package/src/shell/Sidebar.tsx +203 -0
  51. package/src/shell/TokensPane.tsx +26 -0
  52. package/src/shell/controlApiTable.ts +53 -0
  53. package/src/shell/hashRoute.ts +49 -0
  54. package/src/shell/playgroundUsageHighlight.ts +53 -0
  55. package/src/shell/playgroundUsageTwoslash.ts +69 -0
  56. package/src/shell/useHashRoute.ts +29 -0
  57. package/src/styles/dashboard-theme.css +188 -0
  58. package/src/types/controls.ts +62 -0
  59. package/src/types/defaultTailwindTypography.ts +55 -0
  60. package/src/types/playground.ts +21 -0
  61. package/src/types/preview.ts +8 -0
  62. package/src/types/report.ts +116 -0
  63. 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
+ }