dslinter 0.0.32 → 0.0.33

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.
@@ -12,7 +12,13 @@ import { ComponentCatalog } from "./ComponentCatalog";
12
12
  import { FindingsList } from "./FindingsList";
13
13
  import { ScoreStrip } from "./ScoreStrip";
14
14
 
15
- export function DashboardBody({ report }: { report: WorkspaceReport }) {
15
+ export function DashboardBody({
16
+ report,
17
+ focusName,
18
+ }: {
19
+ report: WorkspaceReport;
20
+ focusName?: string;
21
+ }) {
16
22
  return (
17
23
  <div className="space-y-10">
18
24
  <ScoreStrip scores={report.scores} />
@@ -56,7 +62,7 @@ export function DashboardBody({ report }: { report: WorkspaceReport }) {
56
62
  title="Components"
57
63
  description="Definitions and JSX usage from the latest snapshot."
58
64
  >
59
- <ComponentCatalog report={report} />
65
+ <ComponentCatalog report={report} focusName={focusName} />
60
66
  </Section>
61
67
 
62
68
  <Section
@@ -10,8 +10,8 @@ const HIDDEN_COMPONENTS = new Set(["App", "React.StrictMode"]);
10
10
 
11
11
  export function aggregateDefinitions(report: WorkspaceReport): Map<string, DefinitionSite[]> {
12
12
  const map = new Map<string, DefinitionSite[]>();
13
- for (const file of report.files) {
14
- for (const d of file.definitions) {
13
+ for (const file of report.files ?? []) {
14
+ for (const d of file.definitions ?? []) {
15
15
  if (HIDDEN_COMPONENTS.has(d.name)) continue;
16
16
  const list = map.get(d.name) ?? [];
17
17
  list.push({ kind: d.kind, path: file.path, line: d.line });
@@ -55,7 +55,7 @@ export function aggregateDeclaredProps(report: WorkspaceReport): Map<string, str
55
55
 
56
56
  export function usageMap(report: WorkspaceReport): Map<string, UsageSummary> {
57
57
  const m = new Map<string, UsageSummary>();
58
- for (const row of report.usage_by_component) {
58
+ for (const row of report.usage_by_component ?? []) {
59
59
  if (HIDDEN_COMPONENTS.has(row.component)) continue;
60
60
  m.set(row.component, row);
61
61
  }
@@ -71,3 +71,16 @@ export function catalogComponentNames(
71
71
  for (const k of usages.keys()) names.add(k);
72
72
  return [...names].sort((a, b) => a.localeCompare(b));
73
73
  }
74
+
75
+ /** Stable DOM id for a catalog table row (used for hash deep-links). */
76
+ export function catalogRowDomId(name: string): string {
77
+ return `catalog-row-${encodeURIComponent(name)}`;
78
+ }
79
+
80
+ /** Unique component names for sidebar / command palette (definitions ∪ usage). */
81
+ export function componentCatalogNamesFromReport(
82
+ report: WorkspaceReport | null | undefined,
83
+ ): string[] {
84
+ if (!report) return [];
85
+ return catalogComponentNames(aggregateDefinitions(report), usageMap(report));
86
+ }
@@ -52,9 +52,16 @@ export function useWorkspaceReport(
52
52
  const etagRef = useRef<string | null>(null);
53
53
 
54
54
  // Core fetch function.
55
- const fetchReport = (url: string, cancelled: { value: boolean }) => {
56
- setError(null);
57
- setLoading(true);
55
+ const fetchReport = (
56
+ url: string,
57
+ cancelled: { value: boolean },
58
+ options?: { showLoading?: boolean },
59
+ ) => {
60
+ const showLoading = options?.showLoading !== false;
61
+ if (showLoading) {
62
+ setError(null);
63
+ setLoading(true);
64
+ }
58
65
  loadReport(url)
59
66
  .then((r) => {
60
67
  if (!cancelled.value) setReport(r);
@@ -63,14 +70,14 @@ export function useWorkspaceReport(
63
70
  if (!cancelled.value) setError(e instanceof Error ? e.message : "Failed to load report");
64
71
  })
65
72
  .finally(() => {
66
- if (!cancelled.value) setLoading(false);
73
+ if (!cancelled.value && showLoading) setLoading(false);
67
74
  });
68
75
  };
69
76
 
70
77
  // Initial load.
71
78
  useEffect(() => {
72
79
  const cancelled = { value: false };
73
- fetchReport(reportUrl, cancelled);
80
+ fetchReport(reportUrl, cancelled, { showLoading: true });
74
81
  return () => {
75
82
  cancelled.value = true;
76
83
  };
@@ -86,7 +93,7 @@ export function useWorkspaceReport(
86
93
  source.onmessage = (e) => {
87
94
  if (cancelled.value) return;
88
95
  if (e.data === "updated") {
89
- fetchReport(reportUrl, cancelled);
96
+ fetchReport(reportUrl, cancelled, { showLoading: false });
90
97
  }
91
98
  };
92
99
 
@@ -118,7 +125,7 @@ export function useWorkspaceReport(
118
125
  return;
119
126
  }
120
127
  etagRef.current = etag;
121
- fetchReport(reportUrl, cancelled);
128
+ fetchReport(reportUrl, cancelled, { showLoading: false });
122
129
  } catch {
123
130
  // Network error during poll — ignore silently; the user will see the
124
131
  // previous state.
@@ -17,6 +17,7 @@ import { GovernancePane } from "../components/GovernancePane";
17
17
  import { Sidebar } from "../components/Sidebar";
18
18
  import { TokensPane } from "../components/TokensPane";
19
19
  import { DashboardCommandPalette } from "../components/DashboardCommandPalette";
20
+ import { componentCatalogNamesFromReport } from "../dashboard/aggregate";
20
21
  import { useHashRoute } from "./useHashRoute";
21
22
 
22
23
  const STORAGE_KEY = "dslinter-dashboard-theme";
@@ -144,8 +145,28 @@ function DashboardLayoutInner({
144
145
  const [commandPaletteOpen, setCommandPaletteOpen] = useState(false);
145
146
  const { theme, setTheme, resolvedTheme } = useDashboardTheme();
146
147
 
148
+ const catalogNames = useMemo(
149
+ () => componentCatalogNamesFromReport(dslinterReport.report),
150
+ [dslinterReport.report],
151
+ );
152
+
153
+ const playgroundIds = useMemo(
154
+ () => new Set(playgroundEntries.map((e) => e.id)),
155
+ [playgroundEntries],
156
+ );
157
+
158
+ const focusName =
159
+ route.view === "governance" ? route.catalog : undefined;
160
+
147
161
  const getEntry = (id: string) => playgroundEntries.find((e) => e.id === id);
148
162
 
163
+ useEffect(() => {
164
+ if (route.view !== "component") return;
165
+ if (getEntry(route.componentId)) return;
166
+ if (!catalogNames.includes(route.componentId)) return;
167
+ navigate({ view: "governance", catalog: route.componentId });
168
+ }, [route, catalogNames, playgroundEntries, navigate]);
169
+
149
170
  let main: ReactNode;
150
171
  if (route.view === "tokens") {
151
172
  main = <TokensPane tokenCatalog={tokenCatalog} />;
@@ -156,6 +177,7 @@ function DashboardLayoutInner({
156
177
  reportUrl={reportUrl}
157
178
  dslinterReportHint={dslinterReportHint}
158
179
  dslinterReport={dslinterReport}
180
+ focusName={focusName}
159
181
  />
160
182
  );
161
183
  } else {
@@ -201,13 +223,17 @@ function DashboardLayoutInner({
201
223
  )}
202
224
  >
203
225
  <DashboardCommandPalette
204
- entries={playgroundEntries}
226
+ catalogNames={catalogNames}
227
+ playgroundIds={playgroundIds}
205
228
  onNavigate={navigate}
206
229
  open={commandPaletteOpen}
207
230
  onOpenChange={setCommandPaletteOpen}
208
231
  />
209
232
  <Sidebar
210
- entries={playgroundEntries}
233
+ report={dslinterReport.report}
234
+ playgroundIds={playgroundIds}
235
+ reportLoading={dslinterReport.loading}
236
+ reportError={dslinterReport.error}
211
237
  route={route}
212
238
  onNavigate={navigate}
213
239
  onOpenCommandPalette={() => setCommandPaletteOpen(true)}
@@ -1,6 +1,6 @@
1
1
  export type HashRoute =
2
2
  | { view: "tokens" }
3
- | { view: "governance" }
3
+ | { view: "governance"; catalog?: string }
4
4
  | { view: "component"; componentId: string };
5
5
 
6
6
  const PREFIX = "#!/";
@@ -26,6 +26,12 @@ export function parseHashRoute(hash: string): HashRoute {
26
26
  if (raw === "governance") {
27
27
  return { view: "governance" };
28
28
  }
29
+ if (raw.startsWith("governance/")) {
30
+ const catalog = decodeURIComponent(raw.slice("governance/".length));
31
+ if (catalog.length > 0) {
32
+ return { view: "governance", catalog };
33
+ }
34
+ }
29
35
  if (raw.startsWith("component/")) {
30
36
  const componentId = decodeURIComponent(raw.slice("component/".length));
31
37
  if (componentId.length > 0) {
@@ -40,7 +46,9 @@ export function formatHashRoute(route: HashRoute): string {
40
46
  case "tokens":
41
47
  return `${PREFIX}tokens`;
42
48
  case "governance":
43
- return `${PREFIX}governance`;
49
+ return route.catalog
50
+ ? `${PREFIX}governance/${encodeURIComponent(route.catalog)}`
51
+ : `${PREFIX}governance`;
44
52
  case "component":
45
53
  return `${PREFIX}component/${encodeURIComponent(route.componentId)}`;
46
54
  default: