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,230 @@
1
+ import type { ReactNode } from "react";
2
+ import {
3
+ createContext,
4
+ useCallback,
5
+ useContext,
6
+ useEffect,
7
+ useMemo,
8
+ useState,
9
+ } from "react";
10
+ import type { PlaygroundEntry } from "../types/playground";
11
+ import type { TokenCatalog } from "../types/tokenCatalog";
12
+ import type { DslinterReportState } from "../dashboard/useWorkspaceReport";
13
+ import { Button } from "@/components/ui/button";
14
+ import { cn } from "../lib/utils";
15
+ import { ComponentPlaygroundPane } from "./ComponentPlaygroundPane";
16
+ import { GovernancePane } from "./GovernancePane";
17
+ import { Sidebar } from "./Sidebar";
18
+ import { TokensPane } from "./TokensPane";
19
+ import { DashboardCommandPalette } from "./DashboardCommandPalette";
20
+ import { useHashRoute } from "./useHashRoute";
21
+
22
+ const STORAGE_KEY = "dslinter-dashboard-theme";
23
+
24
+ export type DashboardThemePreference = "light" | "dark";
25
+ export type DashboardResolvedTheme = DashboardThemePreference;
26
+
27
+ function readStored(): DashboardThemePreference | null {
28
+ if (typeof window === "undefined") return null;
29
+ try {
30
+ const LEGACY_KEY = "dslinter-workbench-theme";
31
+ let v = localStorage.getItem(STORAGE_KEY);
32
+ if (v == null) {
33
+ v = localStorage.getItem(LEGACY_KEY);
34
+ if (v === "light" || v === "dark") {
35
+ localStorage.setItem(STORAGE_KEY, v);
36
+ localStorage.removeItem(LEGACY_KEY);
37
+ }
38
+ }
39
+ if (v === "light" || v === "dark") return v;
40
+ /** Migrate legacy `system` (and any unknown) to an explicit mode. */
41
+ if (v === "system") {
42
+ const next = window.matchMedia("(prefers-color-scheme: dark)").matches
43
+ ? "dark"
44
+ : "light";
45
+ localStorage.setItem(STORAGE_KEY, next);
46
+ return next;
47
+ }
48
+ } catch {
49
+ /* ignore */
50
+ }
51
+ return null;
52
+ }
53
+
54
+ function readInitialTheme(): DashboardThemePreference {
55
+ return readStored() ?? "light";
56
+ }
57
+
58
+ type DashboardThemeContextValue = {
59
+ theme: DashboardThemePreference;
60
+ setTheme: (next: DashboardThemePreference) => void;
61
+ /** Same as `theme`; kept for callers that already used `resolvedTheme`. */
62
+ resolvedTheme: DashboardResolvedTheme;
63
+ };
64
+
65
+ const DashboardThemeContext = createContext<DashboardThemeContextValue | null>(
66
+ null,
67
+ );
68
+
69
+ export function DashboardThemeProvider({ children }: { children: ReactNode }) {
70
+ const [theme, setThemeState] = useState<DashboardThemePreference>(() =>
71
+ readInitialTheme(),
72
+ );
73
+
74
+ const setTheme = useCallback((next: DashboardThemePreference) => {
75
+ setThemeState(next);
76
+ }, []);
77
+
78
+ useEffect(() => {
79
+ try {
80
+ localStorage.setItem(STORAGE_KEY, theme);
81
+ } catch {
82
+ /* ignore */
83
+ }
84
+ }, [theme]);
85
+
86
+ useEffect(() => {
87
+ const onStorage = (e: StorageEvent) => {
88
+ if (e.key !== STORAGE_KEY || e.newValue == null) return;
89
+ if (e.newValue === "light" || e.newValue === "dark") {
90
+ setThemeState(e.newValue);
91
+ }
92
+ };
93
+ window.addEventListener("storage", onStorage);
94
+ return () => window.removeEventListener("storage", onStorage);
95
+ }, []);
96
+
97
+ const value = useMemo(
98
+ () => ({ theme, setTheme, resolvedTheme: theme }),
99
+ [theme, setTheme],
100
+ );
101
+
102
+ return (
103
+ <DashboardThemeContext.Provider value={value}>
104
+ {children}
105
+ </DashboardThemeContext.Provider>
106
+ );
107
+ }
108
+
109
+ export function useDashboardTheme(): DashboardThemeContextValue {
110
+ const ctx = useContext(DashboardThemeContext);
111
+ if (!ctx) {
112
+ throw new Error(
113
+ "useDashboardTheme must be used within DashboardThemeProvider",
114
+ );
115
+ }
116
+ return ctx;
117
+ }
118
+
119
+ export type DashboardLayoutProps = {
120
+ playgroundEntries: PlaygroundEntry[];
121
+ tokenCatalog: TokenCatalog;
122
+ /** Custom intro shown above the governance inventory on `#!/governance`; defaults to package copy. */
123
+ overview?: ReactNode;
124
+ /** Fetch URL for `dslint --json` output. */
125
+ reportUrl?: string;
126
+ /** Shown next to the governance refresh hint. */
127
+ dslinterReportHint?: string;
128
+ /** Maps Vite `import.meta.glob` path to a label in the component header. */
129
+ formatModulePath?: (modulePath: string) => string;
130
+ /** Workspace report fetch state (shared by governance + component a11y). */
131
+ dslinterReport: DslinterReportState;
132
+ };
133
+
134
+ function DashboardLayoutInner({
135
+ playgroundEntries,
136
+ tokenCatalog,
137
+ overview,
138
+ reportUrl,
139
+ dslinterReportHint,
140
+ formatModulePath,
141
+ dslinterReport,
142
+ }: DashboardLayoutProps) {
143
+ const [route, navigate] = useHashRoute();
144
+ const [commandPaletteOpen, setCommandPaletteOpen] = useState(false);
145
+ const { theme, setTheme, resolvedTheme } = useDashboardTheme();
146
+
147
+ const getEntry = (id: string) => playgroundEntries.find((e) => e.id === id);
148
+
149
+ let main: ReactNode;
150
+ if (route.view === "tokens") {
151
+ main = <TokensPane tokenCatalog={tokenCatalog} />;
152
+ } else if (route.view === "governance") {
153
+ main = (
154
+ <GovernancePane
155
+ landing={overview}
156
+ reportUrl={reportUrl}
157
+ dslinterReportHint={dslinterReportHint}
158
+ dslinterReport={dslinterReport}
159
+ />
160
+ );
161
+ } else {
162
+ const entry = getEntry(route.componentId);
163
+ if (!entry) {
164
+ main = (
165
+ <div className="flex min-h-0 flex-1 flex-col items-center justify-center gap-3 bg-muted/40 px-8 text-center">
166
+ <p className="text-sm font-medium text-foreground">Unknown preview</p>
167
+ <p className="max-w-md text-xs text-muted-foreground">
168
+ No playground registered for{" "}
169
+ <span className="font-mono">{route.componentId}</span>.
170
+ </p>
171
+ <Button
172
+ type="button"
173
+ size="sm"
174
+ onClick={() => navigate({ view: "governance" })}
175
+ >
176
+ Back to governance
177
+ </Button>
178
+ </div>
179
+ );
180
+ } else {
181
+ main = (
182
+ <ComponentPlaygroundPane
183
+ entry={entry}
184
+ formatModulePath={formatModulePath}
185
+ workspaceReport={dslinterReport.report}
186
+ reportReady={
187
+ !dslinterReport.loading &&
188
+ dslinterReport.error == null &&
189
+ dslinterReport.report != null
190
+ }
191
+ />
192
+ );
193
+ }
194
+ }
195
+
196
+ return (
197
+ <div
198
+ className={cn(
199
+ "flex h-screen min-h-0 bg-background text-foreground",
200
+ resolvedTheme === "dark" && "dark",
201
+ )}
202
+ >
203
+ <DashboardCommandPalette
204
+ entries={playgroundEntries}
205
+ onNavigate={navigate}
206
+ open={commandPaletteOpen}
207
+ onOpenChange={setCommandPaletteOpen}
208
+ />
209
+ <Sidebar
210
+ entries={playgroundEntries}
211
+ route={route}
212
+ onNavigate={navigate}
213
+ onOpenCommandPalette={() => setCommandPaletteOpen(true)}
214
+ theme={theme}
215
+ onThemeChange={setTheme}
216
+ />
217
+ <div className="ml-[240px] flex min-h-0 min-w-0 flex-1 flex-col">
218
+ {main}
219
+ </div>
220
+ </div>
221
+ );
222
+ }
223
+
224
+ export function DashboardLayout(props: DashboardLayoutProps) {
225
+ return (
226
+ <DashboardThemeProvider>
227
+ <DashboardLayoutInner {...props} />
228
+ </DashboardThemeProvider>
229
+ );
230
+ }
@@ -0,0 +1,21 @@
1
+ import type { ReactNode } from "react";
2
+ import { cn } from "../lib/utils";
3
+
4
+ export function EmptyCard({
5
+ children,
6
+ className,
7
+ }: {
8
+ children: ReactNode;
9
+ className?: string;
10
+ }) {
11
+ return (
12
+ <div
13
+ className={cn(
14
+ "rounded-lg border border-border bg-muted/50 px-4 py-3 text-sm text-muted-foreground",
15
+ className,
16
+ )}
17
+ >
18
+ {children}
19
+ </div>
20
+ );
21
+ }
@@ -0,0 +1,77 @@
1
+ import type { ReactNode } from "react";
2
+ import { DashboardBody } from "../dashboard/DashboardBody";
3
+ import type { DslinterReportState } from "../dashboard/useWorkspaceReport";
4
+
5
+ type Props = {
6
+ /** Intro / landing copy shown above the governance inventory. */
7
+ landing?: ReactNode;
8
+ reportUrl?: string;
9
+ dslinterReportHint?: string;
10
+ dslinterReport: DslinterReportState;
11
+ };
12
+
13
+ export function GovernancePane({
14
+ landing,
15
+ reportUrl: _reportUrl = "/dslint-report.json",
16
+ dslinterReportHint = "npm run dslint:report",
17
+ dslinterReport,
18
+ }: Props) {
19
+ const { report, error, loading } = dslinterReport;
20
+
21
+ if (error) {
22
+ return (
23
+ <div className="min-h-0 flex-1 overflow-auto bg-muted/40">
24
+ {landing}
25
+ <header className="border-b border-border bg-card px-8 py-6">
26
+ <h1 className="text-lg font-semibold tracking-tight text-foreground">
27
+ Governance
28
+ </h1>
29
+ </header>
30
+ <div className="mx-auto max-w-lg px-8 py-16 text-center">
31
+ <p className="text-sm font-medium text-foreground">
32
+ Could not load DSLinter report
33
+ </p>
34
+ <p className="mt-2 text-xs text-muted-foreground">{error}</p>
35
+ <p className="mt-6 text-xs text-muted-foreground">
36
+ Regenerate the JSON, then refresh. Example:{" "}
37
+ <code className="rounded bg-muted px-1.5 py-0.5 font-mono text-foreground">
38
+ {dslinterReportHint}
39
+ </code>
40
+ </p>
41
+ </div>
42
+ </div>
43
+ );
44
+ }
45
+
46
+ if (loading || !report) {
47
+ return (
48
+ <div className="flex min-h-0 flex-1 flex-col overflow-auto bg-muted/40">
49
+ {landing}
50
+ <div className="flex flex-1 items-center justify-center text-sm text-muted-foreground">
51
+ Loading inventory…
52
+ </div>
53
+ </div>
54
+ );
55
+ }
56
+
57
+ return (
58
+ <div className="min-h-0 flex-1 overflow-auto bg-muted/40">
59
+ {landing}
60
+ <header className="border-b border-border bg-card px-8 py-6">
61
+ <p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
62
+ Inventory
63
+ </p>
64
+ <h1 className="mt-1 text-lg font-semibold tracking-tight text-foreground">
65
+ Governance
66
+ </h1>
67
+ <p className="text-sm text-muted-foreground">
68
+ Scores, component catalog, token wall, and findings from the latest
69
+ DSLinter snapshot
70
+ </p>
71
+ </header>
72
+ <div className="min-w-0 w-full px-6 py-8">
73
+ <DashboardBody report={report} />
74
+ </div>
75
+ </div>
76
+ );
77
+ }