dslinter 0.1.13 → 0.2.2
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 +72 -0
- package/README.md +50 -29
- package/bin/dslinter.mjs +26 -5
- package/bin/lib/config-hide-component.mjs +44 -0
- package/bin/lib/config-hide-component.test.mjs +33 -0
- package/bin/lib/constants.mjs +20 -0
- package/bin/lib/dev-banner.mjs +16 -51
- package/bin/lib/dev-banner.test.mjs +20 -18
- package/bin/lib/enrich-playgrounds-from-ts.mjs +201 -0
- package/bin/lib/enrich-playgrounds-from-ts.test.mjs +74 -0
- package/bin/lib/enrich-report-cli.mjs +14 -0
- package/bin/lib/env.mjs +20 -0
- package/bin/lib/infer-prop-types-from-ts.mjs +381 -0
- package/bin/lib/infer-prop-types-from-ts.test.mjs +174 -0
- package/bin/lib/parse-args.mjs +13 -1
- package/bin/lib/parse-args.test.mjs +7 -1
- package/bin/lib/paths.mjs +8 -0
- package/bin/lib/project-root.mjs +72 -10
- package/bin/lib/project-root.test.mjs +32 -1
- package/bin/lib/prompt.mjs +31 -0
- package/bin/lib/resolve-project.mjs +78 -0
- package/bin/lib/resolve-project.test.mjs +74 -0
- package/bin/lib/run-scanner.mjs +40 -6
- package/bin/lib/scaffold-config.mjs +128 -9
- package/bin/lib/scaffold-config.test.mjs +24 -2
- package/bin/lib/scan-host.mjs +44 -0
- package/bin/lib/scan-host.test.mjs +41 -0
- package/bin/lib/setup-readiness.mjs +153 -0
- package/bin/lib/setup-readiness.test.mjs +32 -0
- package/bin/modes/build.mjs +31 -6
- package/bin/modes/dev.mjs +55 -21
- package/bin/modes/init.mjs +3 -22
- package/bin/modes/init.test.mjs +1 -1
- package/bin/modes/mcp.mjs +49 -0
- package/bin/modes/report.mjs +29 -4
- package/bin/modes/watch.mjs +85 -0
- package/dashboard-dist/assets/DashboardLayoutAuto-Bja3BuZZ.css +1 -0
- package/dashboard-dist/assets/DashboardLayoutAuto-h0gP_iKd.js +1 -0
- package/dashboard-dist/assets/axe-DDaE9JTN.js +20 -0
- package/dashboard-dist/assets/index-B9sZ6wHm.css +1 -0
- package/dashboard-dist/assets/index-DIDBt5ed.js +218 -0
- package/dashboard-dist/index.html +2 -2
- package/index.cjs +53 -52
- package/index.d.ts +3 -0
- package/package.json +18 -12
- package/shared/env.ts +15 -0
- package/shared/paths.ts +8 -0
- package/shared/reportPath.test.ts +19 -0
- package/shared/reportPath.ts +12 -0
- package/shared/servePort.ts +16 -0
- package/src/components/ComponentInspectPane.tsx +67 -19
- package/src/components/ComponentPlaygroundPane.tsx +262 -113
- package/src/components/DashboardCommandPalette.tsx +6 -11
- package/src/components/GovernancePane.tsx +2 -2
- package/src/components/HideFromCatalogButton.tsx +44 -0
- package/src/components/OpenInEditorButton.tsx +36 -0
- package/src/components/PlaygroundA11yAndCode.tsx +53 -53
- package/src/components/PlaygroundAppThemeWrapper.tsx +82 -0
- package/src/components/PlaygroundControls.tsx +5 -11
- package/src/components/PlaygroundPreviewErrorBoundary.tsx +54 -0
- package/src/components/PlaygroundUsageCode.tsx +6 -4
- package/src/components/PlaygroundVariantMatrix.tsx +101 -34
- package/src/components/Section.tsx +5 -2
- package/src/components/Sidebar.tsx +131 -46
- package/src/components/TruncatedPath.tsx +44 -0
- package/src/components/controlApiTable.test.ts +29 -0
- package/src/components/controlApiTable.ts +3 -0
- package/src/components/playgroundUsageHighlight.ts +14 -3
- package/src/components/ui/badge.tsx +1 -1
- package/src/components/ui/table.tsx +2 -2
- package/src/dashboard/ComponentCatalog.tsx +16 -23
- package/src/dashboard/ComponentUsageDetails.tsx +6 -15
- package/src/dashboard/DashboardBody.tsx +0 -35
- package/src/dashboard/FindingsList.tsx +65 -55
- package/src/dashboard/ScannedTokenWall.tsx +3 -3
- package/src/dashboard/aggregate.test.ts +74 -0
- package/src/dashboard/aggregate.ts +145 -21
- package/src/dashboard/catalogVisibility.test.ts +93 -0
- package/src/dashboard/catalogVisibility.ts +108 -0
- package/src/dashboard/editorLink.test.ts +57 -0
- package/src/dashboard/editorLink.ts +71 -0
- package/src/dashboard/paths.test.ts +49 -0
- package/src/dashboard/paths.ts +51 -3
- package/src/dashboard/updateDslintConfig.ts +22 -0
- package/src/dashboard/useWorkspaceReport.ts +21 -17
- package/src/index.ts +26 -0
- package/src/mcp/agent-context.ts +148 -0
- package/src/mcp/agent-query.test.ts +89 -0
- package/src/mcp/agent-query.ts +373 -0
- package/src/mcp/config.ts +53 -0
- package/src/mcp/index.ts +18 -0
- package/src/mcp/normalize-paths.ts +65 -0
- package/src/mcp/report-cache.ts +212 -0
- package/src/mcp/rule-catalog.json +156 -0
- package/src/mcp/rule-catalog.ts +33 -0
- package/src/mcp/schemas.ts +54 -0
- package/src/mcp/server.test.ts +44 -0
- package/src/mcp/server.ts +343 -0
- package/src/mcp/start.ts +29 -0
- package/src/mcp/verify-loop.test.ts +49 -0
- package/src/mcp/verify-loop.ts +149 -0
- package/src/playground/appPreviewTheme.test.ts +148 -0
- package/src/playground/appPreviewTheme.ts +137 -0
- package/src/playground/buildCompoundPlaygroundEntries.test.ts +348 -0
- package/src/playground/buildCompoundPlaygroundEntries.ts +625 -0
- package/src/playground/buildPlaygroundEntriesFromReport.test.ts +420 -6
- package/src/playground/buildPlaygroundEntriesFromReport.ts +206 -285
- package/src/playground/catalogIdFromPlaygroundExport.test.ts +15 -0
- package/src/playground/catalogIdFromPlaygroundExport.ts +8 -0
- package/src/playground/collectDefinedPlaygrounds.test.ts +59 -0
- package/src/playground/collectDefinedPlaygrounds.ts +68 -0
- package/src/playground/controls.ts +177 -0
- package/src/playground/createPlaygroundRegistry.ts +1 -1
- package/src/playground/definePlayground.tsx +88 -16
- package/src/playground/definePlaygroundFromKit.ts +17 -0
- package/src/playground/embedGlobKey.ts +8 -0
- package/src/playground/enrichKitControls.test.ts +25 -0
- package/src/playground/enrichKitControls.ts +197 -0
- package/src/playground/expandPlaygroundControls.test.ts +50 -0
- package/src/playground/expandPlaygroundControls.ts +97 -0
- package/src/playground/inferKitJsx.test.ts +77 -0
- package/src/playground/inferKitJsx.ts +165 -0
- package/src/playground/inferKitParams.test.ts +41 -0
- package/src/playground/inferKitParams.ts +113 -0
- package/src/playground/inferPropTypesFromTs.d.mts +47 -0
- package/src/playground/inferPropTypesFromTs.mjs +343 -0
- package/src/playground/inferPropTypesFromTs.test.ts +227 -0
- package/src/playground/inferPropTypesFromTs.ts +17 -0
- package/src/playground/mergePlaygroundEntries.test.ts +32 -0
- package/src/playground/mergePlaygroundEntries.ts +28 -0
- package/src/playground/playgroundJoin.test.ts +79 -19
- package/src/playground/playgroundJoin.ts +47 -22
- package/src/playground/playgroundModuleExport.test.ts +42 -0
- package/src/playground/playgroundModuleExport.ts +22 -0
- package/src/playground/playgroundSpecsKey.ts +8 -0
- package/src/playground/propCoerce.ts +91 -0
- package/src/playground/scanVariantA11y.test.ts +46 -0
- package/src/playground/scanVariantA11y.ts +107 -0
- package/src/playground/snippet.ts +83 -0
- package/src/playground/usePlaygroundFromReport.test.ts +18 -8
- package/src/playground/usePlaygroundFromReport.ts +3 -1
- package/src/report/a11yForModule.ts +2 -7
- package/src/report/a11yScoring.test.ts +24 -0
- package/src/report/a11yScoring.ts +17 -0
- package/src/report/index.ts +6 -0
- package/src/shell/DashboardLayout.tsx +71 -45
- package/src/shell/DashboardLayoutAuto.tsx +0 -4
- package/src/shell/hashRoute.test.ts +7 -15
- package/src/shell/hashRoute.ts +31 -31
- package/src/shell/useHashRoute.ts +38 -13
- package/src/styles/dashboard-theme.css +18 -7
- package/src/types/controls.ts +11 -0
- package/src/types/playground.ts +4 -0
- package/src/types/report.ts +32 -9
- package/templates/playground/buildRegistry.ts +1 -1
- package/templates/vite.dslinter.snippet.ts +15 -4
- package/vite/collectScanModules.test.ts +91 -3
- package/vite/collectScanModules.ts +94 -29
- package/vite/consumer.config.mjs +6 -3
- package/vite/consumerAlias.test.ts +47 -0
- package/vite/consumerAlias.ts +114 -0
- package/vite/embedTailwindSources.test.ts +74 -0
- package/vite/embedTailwindSources.ts +97 -0
- package/vite/loadConsumerAliases.test.ts +131 -0
- package/vite/loadConsumerAliases.ts +155 -0
- package/vite/openFileInEditor.mjs +196 -0
- package/vite/openFileInEditor.test.mjs +87 -0
- package/vite/plugin.resolve.test.ts +72 -0
- package/vite/plugin.ts +216 -19
- package/vite/reportPath.test.ts +19 -0
- package/vite/resolveWayfinderImport.ts +56 -0
- package/vite/shims/inertia-react.tsx +85 -0
- package/vite/shims/wayfinder-actions.ts +33 -0
- package/vite/shims/wayfinder-routes.ts +30 -0
- package/vite/shims/ziggy-js.ts +12 -0
- package/dashboard-dist/assets/DashboardLayoutAuto-Bm7yfyC-.css +0 -1
- package/dashboard-dist/assets/DashboardLayoutAuto-DgwO_itB.js +0 -1
- package/dashboard-dist/assets/index-Cbv7vXvH.css +0 -1
- package/dashboard-dist/assets/index-e20cwqnb.js +0 -206
- package/src/components/playgroundUsageTwoslash.ts +0 -69
- package/templates/vite.dslint-scan-alias.snippet.ts +0 -4
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import { embedGlobKeyFromRelPath } from "../../vite/collectScanModules";
|
|
3
|
+
import type { WorkspaceReport } from "../types/report";
|
|
3
4
|
import { buildPlaygroundEntriesFromReportWithSkips } from "./buildPlaygroundEntriesFromReport";
|
|
4
5
|
|
|
5
6
|
describe("autoPlayground join (embed glob keys)", () => {
|
|
6
7
|
it("joins report playgrounds to virtual module keys", () => {
|
|
7
|
-
const relPath =
|
|
8
|
-
"resources/js/Components/Billing/AdditionalEventLimitModal.tsx";
|
|
8
|
+
const relPath = "resources/js/Components/Billing/AdditionalEventLimitModal.tsx";
|
|
9
9
|
const globKey = embedGlobKeyFromRelPath(relPath);
|
|
10
10
|
const modules = {
|
|
11
11
|
[globKey]: {
|
|
@@ -14,21 +14,31 @@ describe("autoPlayground join (embed glob keys)", () => {
|
|
|
14
14
|
},
|
|
15
15
|
},
|
|
16
16
|
};
|
|
17
|
-
const report = {
|
|
17
|
+
const report: WorkspaceReport = {
|
|
18
|
+
root: "/repo",
|
|
19
|
+
files: [],
|
|
20
|
+
findings: [],
|
|
21
|
+
duplicate_components: [],
|
|
22
|
+
usage_by_component: [],
|
|
23
|
+
scores: {
|
|
24
|
+
design_system_health: 0,
|
|
25
|
+
ux_consistency: 0,
|
|
26
|
+
accessibility: 0,
|
|
27
|
+
maintainability: 0,
|
|
28
|
+
},
|
|
18
29
|
playgrounds: [
|
|
19
30
|
{
|
|
20
31
|
id: "AdditionalEventLimitModal",
|
|
21
32
|
export_name: "AdditionalEventLimitModal",
|
|
22
33
|
rel_path: relPath,
|
|
34
|
+
declared_props: [],
|
|
23
35
|
},
|
|
24
36
|
],
|
|
25
37
|
};
|
|
26
38
|
|
|
27
|
-
const { entries, skipped } = buildPlaygroundEntriesFromReportWithSkips(
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
{ logJoinSkips: false },
|
|
31
|
-
);
|
|
39
|
+
const { entries, skipped } = buildPlaygroundEntriesFromReportWithSkips(report, modules, {
|
|
40
|
+
logJoinSkips: false,
|
|
41
|
+
});
|
|
32
42
|
|
|
33
43
|
expect(skipped).toHaveLength(0);
|
|
34
44
|
expect(entries).toHaveLength(1);
|
|
@@ -2,6 +2,7 @@ import { useMemo } from "react";
|
|
|
2
2
|
import type { WorkspaceReport } from "../types/report";
|
|
3
3
|
import { buildPlaygroundEntriesFromReportWithSkips } from "./buildPlaygroundEntriesFromReport";
|
|
4
4
|
import type { BuildPlaygroundResult } from "./buildPlaygroundEntriesFromReport";
|
|
5
|
+
import { playgroundSpecsKey } from "./playgroundSpecsKey";
|
|
5
6
|
import { scanPlaygroundModules } from "./scanPlaygroundModules";
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -12,12 +13,13 @@ import { scanPlaygroundModules } from "./scanPlaygroundModules";
|
|
|
12
13
|
export function usePlaygroundFromReport(
|
|
13
14
|
report: WorkspaceReport | null | undefined,
|
|
14
15
|
): BuildPlaygroundResult {
|
|
16
|
+
const specsKey = playgroundSpecsKey(report);
|
|
15
17
|
return useMemo(
|
|
16
18
|
() =>
|
|
17
19
|
buildPlaygroundEntriesFromReportWithSkips(
|
|
18
20
|
report,
|
|
19
21
|
scanPlaygroundModules,
|
|
20
22
|
),
|
|
21
|
-
[
|
|
23
|
+
[specsKey],
|
|
22
24
|
);
|
|
23
25
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { LintFinding, WorkspaceReport } from "../types/report";
|
|
2
|
+
import { a11yScoreFromFindings } from "./a11yScoring";
|
|
2
3
|
import { pathsMatch, resolveModuleSourcePath } from "./modulePathMatch";
|
|
3
4
|
|
|
4
5
|
export type A11yModuleSummary = {
|
|
@@ -23,13 +24,7 @@ export function a11ySummaryForModule(
|
|
|
23
24
|
const all = report.findings.filter((f) => f.rule_id.startsWith("a11y-"));
|
|
24
25
|
const findings = all.filter((f) => pathsMatch(f.path, target));
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
for (const f of findings) {
|
|
28
|
-
if (f.severity === "error") penalty += 25;
|
|
29
|
-
else if (f.severity === "warning") penalty += 10;
|
|
30
|
-
else penalty += 3;
|
|
31
|
-
}
|
|
32
|
-
const score = Math.max(0, Math.min(100, Math.round(100 - penalty)));
|
|
27
|
+
const score = a11yScoreFromFindings(findings);
|
|
33
28
|
|
|
34
29
|
return { score, issueCount: findings.length, findings };
|
|
35
30
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { a11yScoreFromFindings } from "./a11yScoring";
|
|
3
|
+
|
|
4
|
+
describe("a11yScoreFromFindings", () => {
|
|
5
|
+
it("starts at 100 with no findings", () => {
|
|
6
|
+
expect(a11yScoreFromFindings([])).toBe(100);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("applies severity penalties", () => {
|
|
10
|
+
expect(
|
|
11
|
+
a11yScoreFromFindings([
|
|
12
|
+
{ severity: "error" },
|
|
13
|
+
{ severity: "warning" },
|
|
14
|
+
{ severity: "info" },
|
|
15
|
+
]),
|
|
16
|
+
).toBe(62);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("never drops below zero", () => {
|
|
20
|
+
expect(
|
|
21
|
+
a11yScoreFromFindings(Array.from({ length: 10 }, () => ({ severity: "error" as const }))),
|
|
22
|
+
).toBe(0);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Severity } from "../types/report";
|
|
2
|
+
|
|
3
|
+
export function a11yScoreFromSeverities(severities: Severity[]): number {
|
|
4
|
+
let penalty = 0;
|
|
5
|
+
for (const severity of severities) {
|
|
6
|
+
if (severity === "error") penalty += 25;
|
|
7
|
+
else if (severity === "warning") penalty += 10;
|
|
8
|
+
else penalty += 3;
|
|
9
|
+
}
|
|
10
|
+
return Math.max(0, Math.min(100, Math.round(100 - penalty)));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function a11yScoreFromFindings(
|
|
14
|
+
findings: ReadonlyArray<{ severity: Severity }>,
|
|
15
|
+
): number {
|
|
16
|
+
return a11yScoreFromSeverities(findings.map((f) => f.severity));
|
|
17
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type { A11yModuleSummary } from "./a11yForModule";
|
|
2
|
+
export { a11ySummaryForModule, resolveModuleSourcePath } from "./a11yForModule";
|
|
3
|
+
export type { CodeScoreModuleSummary } from "./codeScoreForModule";
|
|
4
|
+
export { codeScoreSummaryForModule } from "./codeScoreForModule";
|
|
5
|
+
export { tokenStyleFindingsForModule } from "./tokenStyleFindingsForModule";
|
|
6
|
+
export { findingsForComponent } from "./findingsForComponent";
|
|
@@ -12,15 +12,17 @@ import {
|
|
|
12
12
|
import type { PlaygroundEntry } from "../types/playground";
|
|
13
13
|
import type { TokenCatalog } from "../types/tokenCatalog";
|
|
14
14
|
import type { DslinterReportState } from "../dashboard/useWorkspaceReport";
|
|
15
|
-
import { Button } from "../components/ui/button";
|
|
16
|
-
import { cn } from "../lib/utils";
|
|
17
15
|
import { ComponentInspectPane } from "../components/ComponentInspectPane";
|
|
18
16
|
import { ComponentPlaygroundPane } from "../components/ComponentPlaygroundPane";
|
|
19
17
|
import { GovernancePane } from "../components/GovernancePane";
|
|
20
18
|
import { Sidebar } from "../components/Sidebar";
|
|
21
19
|
import { TokensPane } from "../components/TokensPane";
|
|
22
20
|
import { DashboardCommandPalette } from "../components/DashboardCommandPalette";
|
|
23
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
componentCatalogNamesFromReport,
|
|
23
|
+
componentCatalogTreeFromReport,
|
|
24
|
+
} from "../dashboard/aggregate";
|
|
25
|
+
import { reportWithExtraHidden } from "../dashboard/catalogVisibility";
|
|
24
26
|
import { resolvePlaygroundEntry } from "../playground/buildPlaygroundEntriesFromReport";
|
|
25
27
|
import {
|
|
26
28
|
findPlaygroundJoinSkip,
|
|
@@ -48,13 +50,10 @@ function readStored(): DashboardThemePreference | null {
|
|
|
48
50
|
}
|
|
49
51
|
}
|
|
50
52
|
if (v === "light" || v === "dark") return v;
|
|
51
|
-
/** Migrate legacy `system` (and any unknown)
|
|
52
|
-
if (v
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
: "light";
|
|
56
|
-
localStorage.setItem(STORAGE_KEY, next);
|
|
57
|
-
return next;
|
|
53
|
+
/** Migrate legacy `system` (and any unknown) — OS preference is ignored. */
|
|
54
|
+
if (v != null) {
|
|
55
|
+
localStorage.setItem(STORAGE_KEY, "light");
|
|
56
|
+
return "light";
|
|
58
57
|
}
|
|
59
58
|
} catch {
|
|
60
59
|
/* ignore */
|
|
@@ -139,14 +138,12 @@ export type DashboardLayoutProps = {
|
|
|
139
138
|
/** Join failures from `buildPlaygroundEntriesFromReportWithSkips` — powers inspect-pane hints. */
|
|
140
139
|
playgroundJoinSkips?: PlaygroundJoinSkip[];
|
|
141
140
|
tokenCatalog?: TokenCatalog;
|
|
142
|
-
/** Custom intro shown above the governance inventory on
|
|
141
|
+
/** Custom intro shown above the governance inventory on `/governance`; defaults to package copy. */
|
|
143
142
|
overview?: ReactNode;
|
|
144
143
|
/** Fetch URL for `dslint --json` output. */
|
|
145
144
|
reportUrl?: string;
|
|
146
145
|
/** Shown next to the governance refresh hint. */
|
|
147
146
|
dslinterReportHint?: string;
|
|
148
|
-
/** Maps Vite `import.meta.glob` path to a label in the component header. */
|
|
149
|
-
formatModulePath?: (modulePath: string) => string;
|
|
150
147
|
/** Workspace report fetch state (shared by governance + component a11y). */
|
|
151
148
|
dslinterReport: DslinterReportState;
|
|
152
149
|
};
|
|
@@ -166,16 +163,48 @@ export function DashboardLayoutInner({
|
|
|
166
163
|
overview,
|
|
167
164
|
reportUrl,
|
|
168
165
|
dslinterReportHint,
|
|
169
|
-
formatModulePath,
|
|
170
166
|
dslinterReport,
|
|
171
167
|
}: DashboardLayoutInnerProps) {
|
|
172
168
|
const [route, navigate] = useHashRoute();
|
|
173
169
|
const [commandPaletteOpen, setCommandPaletteOpen] = useState(false);
|
|
170
|
+
const [optimisticHidden, setOptimisticHidden] = useState<string[]>([]);
|
|
174
171
|
const { theme, setTheme, resolvedTheme } = useDashboardTheme();
|
|
175
172
|
|
|
173
|
+
const catalogReport = useMemo(
|
|
174
|
+
() => reportWithExtraHidden(dslinterReport.report, optimisticHidden),
|
|
175
|
+
[dslinterReport.report, optimisticHidden],
|
|
176
|
+
);
|
|
177
|
+
|
|
176
178
|
const catalogNames = useMemo(
|
|
177
|
-
() => componentCatalogNamesFromReport(
|
|
178
|
-
[
|
|
179
|
+
() => componentCatalogNamesFromReport(catalogReport),
|
|
180
|
+
[catalogReport],
|
|
181
|
+
);
|
|
182
|
+
const catalogEntries = useMemo(() => {
|
|
183
|
+
const tree = componentCatalogTreeFromReport(catalogReport);
|
|
184
|
+
return tree.flatMap((item) => {
|
|
185
|
+
if (item.type === "component") {
|
|
186
|
+
return [{ name: item.name, label: item.name }];
|
|
187
|
+
}
|
|
188
|
+
return [
|
|
189
|
+
{ name: item.parent, label: item.parent },
|
|
190
|
+
...item.children.map((child) => ({
|
|
191
|
+
name: child,
|
|
192
|
+
label: `${item.parent} / ${child}`,
|
|
193
|
+
})),
|
|
194
|
+
];
|
|
195
|
+
});
|
|
196
|
+
}, [catalogReport]);
|
|
197
|
+
|
|
198
|
+
const handleHideFromCatalog = useCallback(
|
|
199
|
+
(componentName: string) => {
|
|
200
|
+
setOptimisticHidden((prev) =>
|
|
201
|
+
prev.includes(componentName) ? prev : [...prev, componentName],
|
|
202
|
+
);
|
|
203
|
+
if (route.view === "component" && route.componentId === componentName) {
|
|
204
|
+
navigate({ view: "governance" });
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
[route, navigate],
|
|
179
208
|
);
|
|
180
209
|
|
|
181
210
|
const reportReady =
|
|
@@ -216,9 +245,12 @@ export function DashboardLayoutInner({
|
|
|
216
245
|
main = (
|
|
217
246
|
<ComponentPlaygroundPane
|
|
218
247
|
entry={entry}
|
|
219
|
-
formatModulePath={formatModulePath}
|
|
220
248
|
workspaceReport={dslinterReport.report}
|
|
221
249
|
reportReady={reportReady}
|
|
250
|
+
onOpenComponent={(name) =>
|
|
251
|
+
navigate({ view: "component", componentId: name })
|
|
252
|
+
}
|
|
253
|
+
onHideFromCatalog={handleHideFromCatalog}
|
|
222
254
|
/>
|
|
223
255
|
);
|
|
224
256
|
} else if (inCatalog) {
|
|
@@ -233,23 +265,22 @@ export function DashboardLayoutInner({
|
|
|
233
265
|
componentId,
|
|
234
266
|
)}
|
|
235
267
|
onBackToGovernance={() => navigate({ view: "governance" })}
|
|
268
|
+
onOpenComponent={(name) =>
|
|
269
|
+
navigate({ view: "component", componentId: name })
|
|
270
|
+
}
|
|
271
|
+
onHideFromCatalog={handleHideFromCatalog}
|
|
236
272
|
/>
|
|
237
273
|
);
|
|
238
274
|
} else {
|
|
239
275
|
main = (
|
|
240
276
|
<div className="flex min-h-0 flex-1 flex-col items-center justify-center gap-3 bg-muted/40 px-8 text-center">
|
|
241
|
-
<p className="text-sm font-medium text-foreground">
|
|
277
|
+
<p className="text-sm font-medium text-foreground">
|
|
278
|
+
Unknown component
|
|
279
|
+
</p>
|
|
242
280
|
<p className="max-w-md text-xs text-muted-foreground">
|
|
243
281
|
<span className="font-mono">{componentId}</span> is not in the
|
|
244
282
|
latest scan catalog.
|
|
245
283
|
</p>
|
|
246
|
-
<Button
|
|
247
|
-
type="button"
|
|
248
|
-
size="sm"
|
|
249
|
-
onClick={() => navigate({ view: "governance" })}
|
|
250
|
-
>
|
|
251
|
-
Back to governance
|
|
252
|
-
</Button>
|
|
253
284
|
</div>
|
|
254
285
|
);
|
|
255
286
|
}
|
|
@@ -257,19 +288,17 @@ export function DashboardLayoutInner({
|
|
|
257
288
|
|
|
258
289
|
return (
|
|
259
290
|
<div
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
resolvedTheme === "dark" && "dark",
|
|
263
|
-
)}
|
|
291
|
+
data-dashboard-theme={resolvedTheme}
|
|
292
|
+
className="flex h-screen min-h-0 bg-background text-foreground"
|
|
264
293
|
>
|
|
265
294
|
<DashboardCommandPalette
|
|
266
|
-
|
|
295
|
+
catalogEntries={catalogEntries}
|
|
267
296
|
onNavigate={navigate}
|
|
268
297
|
open={commandPaletteOpen}
|
|
269
298
|
onOpenChange={setCommandPaletteOpen}
|
|
270
299
|
/>
|
|
271
300
|
<Sidebar
|
|
272
|
-
report={
|
|
301
|
+
report={catalogReport ?? null}
|
|
273
302
|
reportLoading={dslinterReport.loading}
|
|
274
303
|
reportError={dslinterReport.error}
|
|
275
304
|
route={route}
|
|
@@ -277,6 +306,7 @@ export function DashboardLayoutInner({
|
|
|
277
306
|
onOpenCommandPalette={() => setCommandPaletteOpen(true)}
|
|
278
307
|
theme={theme}
|
|
279
308
|
onThemeChange={setTheme}
|
|
309
|
+
catalogNames={catalogNames}
|
|
280
310
|
/>
|
|
281
311
|
<div className="ml-[240px] flex min-h-0 min-w-0 flex-1 flex-col">
|
|
282
312
|
{main}
|
|
@@ -286,23 +316,19 @@ export function DashboardLayoutInner({
|
|
|
286
316
|
}
|
|
287
317
|
|
|
288
318
|
export function DashboardLayout(props: DashboardLayoutProps) {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
319
|
+
return (
|
|
320
|
+
<DashboardThemeProvider>
|
|
321
|
+
{props.autoPlayground ? (
|
|
292
322
|
<Suspense fallback={null}>
|
|
293
323
|
<DashboardLayoutAuto {...props} />
|
|
294
324
|
</Suspense>
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
{...props}
|
|
303
|
-
playgroundEntries={props.playgroundEntries ?? []}
|
|
304
|
-
playgroundJoinSkips={props.playgroundJoinSkips}
|
|
305
|
-
/>
|
|
325
|
+
) : (
|
|
326
|
+
<DashboardLayoutInner
|
|
327
|
+
{...props}
|
|
328
|
+
playgroundEntries={props.playgroundEntries ?? []}
|
|
329
|
+
playgroundJoinSkips={props.playgroundJoinSkips}
|
|
330
|
+
/>
|
|
331
|
+
)}
|
|
306
332
|
</DashboardThemeProvider>
|
|
307
333
|
);
|
|
308
334
|
}
|
|
@@ -11,10 +11,6 @@ export default function DashboardLayoutAuto(props: DashboardLayoutProps) {
|
|
|
11
11
|
{...props}
|
|
12
12
|
playgroundEntries={autoPlaygroundBuild.entries}
|
|
13
13
|
playgroundJoinSkips={autoPlaygroundBuild.skipped}
|
|
14
|
-
formatModulePath={
|
|
15
|
-
props.formatModulePath ??
|
|
16
|
-
((modulePath: string) => modulePath.replace(/^@dslint-scan\//, ""))
|
|
17
|
-
}
|
|
18
14
|
/>
|
|
19
15
|
);
|
|
20
16
|
}
|
|
@@ -3,26 +3,20 @@ import { formatHashRoute, parseHashRoute } from "./hashRoute";
|
|
|
3
3
|
|
|
4
4
|
describe("parseHashRoute", () => {
|
|
5
5
|
it("parses governance overview", () => {
|
|
6
|
-
expect(parseHashRoute("
|
|
6
|
+
expect(parseHashRoute("/governance")).toEqual({ view: "governance" });
|
|
7
|
+
expect(parseHashRoute("/")).toEqual({ view: "governance" });
|
|
7
8
|
expect(parseHashRoute("")).toEqual({ view: "governance" });
|
|
8
9
|
});
|
|
9
10
|
|
|
10
|
-
it("parses legacy governance catalog deep link as component route", () => {
|
|
11
|
-
expect(parseHashRoute("#!/governance/ActionItem")).toEqual({
|
|
12
|
-
view: "component",
|
|
13
|
-
componentId: "ActionItem",
|
|
14
|
-
});
|
|
15
|
-
});
|
|
16
|
-
|
|
17
11
|
it("parses component route", () => {
|
|
18
|
-
expect(parseHashRoute("
|
|
12
|
+
expect(parseHashRoute("/component/ActionItem")).toEqual({
|
|
19
13
|
view: "component",
|
|
20
14
|
componentId: "ActionItem",
|
|
21
15
|
});
|
|
22
16
|
});
|
|
23
17
|
|
|
24
18
|
it("parses tokens", () => {
|
|
25
|
-
expect(parseHashRoute("
|
|
19
|
+
expect(parseHashRoute("/tokens")).toEqual({ view: "tokens" });
|
|
26
20
|
});
|
|
27
21
|
});
|
|
28
22
|
|
|
@@ -30,12 +24,10 @@ describe("formatHashRoute", () => {
|
|
|
30
24
|
it("formats component route", () => {
|
|
31
25
|
expect(
|
|
32
26
|
formatHashRoute({ view: "component", componentId: "ActionItem" }),
|
|
33
|
-
).toBe("
|
|
27
|
+
).toBe("/component/ActionItem");
|
|
34
28
|
});
|
|
35
29
|
|
|
36
|
-
it("formats governance
|
|
37
|
-
expect(
|
|
38
|
-
formatHashRoute({ view: "governance", catalog: "ActionItem" }),
|
|
39
|
-
).toBe("#!/governance/ActionItem");
|
|
30
|
+
it("formats governance", () => {
|
|
31
|
+
expect(formatHashRoute({ view: "governance" })).toBe("/governance");
|
|
40
32
|
});
|
|
41
33
|
});
|
package/src/shell/hashRoute.ts
CHANGED
|
@@ -1,40 +1,34 @@
|
|
|
1
1
|
export type HashRoute =
|
|
2
2
|
| { view: "tokens" }
|
|
3
|
-
| { view: "governance"
|
|
3
|
+
| { view: "governance" }
|
|
4
4
|
| { view: "component"; componentId: string };
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
return hash.slice(PREFIX.length);
|
|
6
|
+
function trimTrailingSlashes(path: string): string {
|
|
7
|
+
let end = path.length;
|
|
8
|
+
while (end > 1 && path[end - 1] === "/") {
|
|
9
|
+
end--;
|
|
11
10
|
}
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
return path.slice(0, end);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function normalizePath(input: string): string {
|
|
15
|
+
const raw = input.trim();
|
|
16
|
+
if (raw.startsWith("/")) {
|
|
17
|
+
return trimTrailingSlashes(raw);
|
|
14
18
|
}
|
|
15
|
-
return
|
|
19
|
+
return raw.length > 0 ? `/${raw}` : "/";
|
|
16
20
|
}
|
|
17
21
|
|
|
18
|
-
export function parseHashRoute(
|
|
19
|
-
const
|
|
20
|
-
if (
|
|
22
|
+
export function parseHashRoute(pathname: string): HashRoute {
|
|
23
|
+
const path = normalizePath(pathname);
|
|
24
|
+
if (path === "/" || path === "/overview" || path === "/governance") {
|
|
21
25
|
return { view: "governance" };
|
|
22
26
|
}
|
|
23
|
-
if (
|
|
27
|
+
if (path === "/tokens") {
|
|
24
28
|
return { view: "tokens" };
|
|
25
29
|
}
|
|
26
|
-
if (
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
if (raw.startsWith("governance/")) {
|
|
30
|
-
const catalog = decodeURIComponent(raw.slice("governance/".length));
|
|
31
|
-
if (catalog.length > 0) {
|
|
32
|
-
/** Legacy deep links — component pages replaced governance catalog scroll. */
|
|
33
|
-
return { view: "component", componentId: catalog };
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
if (raw.startsWith("component/")) {
|
|
37
|
-
const componentId = decodeURIComponent(raw.slice("component/".length));
|
|
30
|
+
if (path.startsWith("/component/")) {
|
|
31
|
+
const componentId = decodeURIComponent(path.slice("/component/".length));
|
|
38
32
|
if (componentId.length > 0) {
|
|
39
33
|
return { view: "component", componentId };
|
|
40
34
|
}
|
|
@@ -45,14 +39,20 @@ export function parseHashRoute(hash: string): HashRoute {
|
|
|
45
39
|
export function formatHashRoute(route: HashRoute): string {
|
|
46
40
|
switch (route.view) {
|
|
47
41
|
case "tokens":
|
|
48
|
-
return
|
|
42
|
+
return "/tokens";
|
|
49
43
|
case "governance":
|
|
50
|
-
return
|
|
51
|
-
? `${PREFIX}governance/${encodeURIComponent(route.catalog)}`
|
|
52
|
-
: `${PREFIX}governance`;
|
|
44
|
+
return "/governance";
|
|
53
45
|
case "component":
|
|
54
|
-
return
|
|
46
|
+
return `/component/${encodeURIComponent(route.componentId)}`;
|
|
55
47
|
default:
|
|
56
|
-
return
|
|
48
|
+
return "/governance";
|
|
57
49
|
}
|
|
58
50
|
}
|
|
51
|
+
|
|
52
|
+
/** Drop stale `#!/…` fragments left over from hash routing. */
|
|
53
|
+
export function stripLegacyHashFragment(): boolean {
|
|
54
|
+
if (!window.location.hash.startsWith("#!/")) return false;
|
|
55
|
+
const clean = window.location.pathname + window.location.search;
|
|
56
|
+
window.history.replaceState(null, "", clean || "/governance");
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
@@ -1,27 +1,52 @@
|
|
|
1
|
-
import { useCallback, useSyncExternalStore } from "react";
|
|
2
|
-
import {
|
|
1
|
+
import { useCallback, useEffect, useSyncExternalStore } from "react";
|
|
2
|
+
import {
|
|
3
|
+
formatHashRoute,
|
|
4
|
+
parseHashRoute,
|
|
5
|
+
stripLegacyHashFragment,
|
|
6
|
+
type HashRoute,
|
|
7
|
+
} from "./hashRoute";
|
|
8
|
+
|
|
9
|
+
const NAVIGATE_EVENT = "dslinter:navigate";
|
|
3
10
|
|
|
4
11
|
function subscribe(onStoreChange: () => void) {
|
|
5
|
-
window.addEventListener("
|
|
6
|
-
|
|
12
|
+
window.addEventListener("popstate", onStoreChange);
|
|
13
|
+
window.addEventListener(NAVIGATE_EVENT, onStoreChange);
|
|
14
|
+
return () => {
|
|
15
|
+
window.removeEventListener("popstate", onStoreChange);
|
|
16
|
+
window.removeEventListener(NAVIGATE_EVENT, onStoreChange);
|
|
17
|
+
};
|
|
7
18
|
}
|
|
8
19
|
|
|
9
|
-
function
|
|
10
|
-
return window.location.
|
|
20
|
+
function getPathSnapshot() {
|
|
21
|
+
return window.location.pathname || "/governance";
|
|
11
22
|
}
|
|
12
23
|
|
|
13
|
-
function
|
|
14
|
-
return "
|
|
24
|
+
function getServerPathSnapshot() {
|
|
25
|
+
return "/governance";
|
|
15
26
|
}
|
|
16
27
|
|
|
17
28
|
export function useHashRoute(): [HashRoute, (next: HashRoute) => void] {
|
|
18
|
-
const
|
|
19
|
-
|
|
29
|
+
const pathname = useSyncExternalStore(
|
|
30
|
+
subscribe,
|
|
31
|
+
getPathSnapshot,
|
|
32
|
+
getServerPathSnapshot,
|
|
33
|
+
);
|
|
34
|
+
const route = parseHashRoute(pathname);
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (stripLegacyHashFragment()) {
|
|
38
|
+
window.dispatchEvent(new Event(NAVIGATE_EVENT));
|
|
39
|
+
}
|
|
40
|
+
}, []);
|
|
20
41
|
|
|
21
42
|
const navigate = useCallback((next: HashRoute) => {
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
43
|
+
const nextPath = formatHashRoute(next);
|
|
44
|
+
const pathChanged = nextPath !== window.location.pathname;
|
|
45
|
+
if (pathChanged) {
|
|
46
|
+
window.history.pushState(null, "", nextPath);
|
|
47
|
+
}
|
|
48
|
+
if (stripLegacyHashFragment() || pathChanged) {
|
|
49
|
+
window.dispatchEvent(new Event(NAVIGATE_EVENT));
|
|
25
50
|
}
|
|
26
51
|
}, []);
|
|
27
52
|
|
|
@@ -2,10 +2,20 @@
|
|
|
2
2
|
* DSLinter dashboard design tokens + shadcn/ui theme.
|
|
3
3
|
* Host apps import once: `@import "dslinter/theme.css";` after `@import "tailwindcss"` and `@source` for this package.
|
|
4
4
|
*/
|
|
5
|
+
|
|
5
6
|
@import "tw-animate-css";
|
|
6
7
|
@import "shadcn/tailwind.css";
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
/** Class-based dark only — never `prefers-color-scheme` or host `.dark`. */
|
|
10
|
+
@custom-variant dark (&:is([data-dashboard-theme="dark"] *));
|
|
11
|
+
|
|
12
|
+
html:has([data-dashboard-theme="light"]) {
|
|
13
|
+
color-scheme: only light;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
html:has([data-dashboard-theme="dark"]) {
|
|
17
|
+
color-scheme: only dark;
|
|
18
|
+
}
|
|
9
19
|
|
|
10
20
|
@theme inline {
|
|
11
21
|
--color-background: var(--background);
|
|
@@ -87,8 +97,9 @@
|
|
|
87
97
|
}
|
|
88
98
|
}
|
|
89
99
|
|
|
90
|
-
:root
|
|
91
|
-
|
|
100
|
+
:root,
|
|
101
|
+
[data-dashboard-theme="light"] {
|
|
102
|
+
color-scheme: only light;
|
|
92
103
|
--radius: 0.625rem;
|
|
93
104
|
--background: oklch(1 0 0);
|
|
94
105
|
--foreground: oklch(0.145 0 0);
|
|
@@ -125,8 +136,8 @@
|
|
|
125
136
|
--sidebar-ring: oklch(0.708 0 0);
|
|
126
137
|
}
|
|
127
138
|
|
|
128
|
-
|
|
129
|
-
color-scheme: dark;
|
|
139
|
+
[data-dashboard-theme="dark"] {
|
|
140
|
+
color-scheme: only dark;
|
|
130
141
|
--background: oklch(0.145 0 0);
|
|
131
142
|
--foreground: oklch(0.985 0 0);
|
|
132
143
|
--card: oklch(0.205 0 0);
|
|
@@ -178,10 +189,10 @@
|
|
|
178
189
|
background-size: 10px 10px;
|
|
179
190
|
}
|
|
180
191
|
|
|
181
|
-
|
|
192
|
+
[data-dashboard-theme="dark"] .ds-playground-dot-surface {
|
|
182
193
|
background-image: radial-gradient(
|
|
183
194
|
circle,
|
|
184
|
-
rgb(148 163 184 / 0.
|
|
195
|
+
rgb(148 163 184 / 0.15) 1px,
|
|
185
196
|
transparent 1px
|
|
186
197
|
);
|
|
187
198
|
}
|
package/src/types/controls.ts
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
/** Values passed from the dashboard control panel into `PlaygroundPreview`. */
|
|
2
2
|
export type PlaygroundArgs = Record<string, string | number | boolean>;
|
|
3
3
|
|
|
4
|
+
export type PlaygroundValuesUpdater = (
|
|
5
|
+
next: PlaygroundArgs | ((prev: PlaygroundArgs) => PlaygroundArgs),
|
|
6
|
+
) => void;
|
|
7
|
+
|
|
8
|
+
/** Generated examples should not be presented as real API defaults. */
|
|
9
|
+
export type PlaygroundDefaultSource = "type" | "example" | "manual";
|
|
10
|
+
|
|
4
11
|
export type PlaygroundBooleanControl = {
|
|
5
12
|
key: string;
|
|
6
13
|
label: string;
|
|
7
14
|
type: "boolean";
|
|
8
15
|
default: boolean;
|
|
16
|
+
defaultSource?: PlaygroundDefaultSource;
|
|
9
17
|
hint?: string;
|
|
10
18
|
};
|
|
11
19
|
|
|
@@ -14,6 +22,7 @@ export type PlaygroundStringControl = {
|
|
|
14
22
|
label: string;
|
|
15
23
|
type: "string";
|
|
16
24
|
default: string;
|
|
25
|
+
defaultSource?: PlaygroundDefaultSource;
|
|
17
26
|
placeholder?: string;
|
|
18
27
|
};
|
|
19
28
|
|
|
@@ -22,6 +31,7 @@ export type PlaygroundNumberControl = {
|
|
|
22
31
|
label: string;
|
|
23
32
|
type: "number";
|
|
24
33
|
default: number;
|
|
34
|
+
defaultSource?: PlaygroundDefaultSource;
|
|
25
35
|
min?: number;
|
|
26
36
|
max?: number;
|
|
27
37
|
step?: number;
|
|
@@ -32,6 +42,7 @@ export type PlaygroundSelectControl = {
|
|
|
32
42
|
label: string;
|
|
33
43
|
type: "select";
|
|
34
44
|
default: string;
|
|
45
|
+
defaultSource?: PlaygroundDefaultSource;
|
|
35
46
|
options: { value: string; label: string }[];
|
|
36
47
|
};
|
|
37
48
|
|