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,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
|
+
}
|