@principal-ai/subsystems-react 0.36.2 → 0.37.1
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/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/pierre/PierreWalkthroughCodeView.d.ts +19 -1
- package/dist/pierre/PierreWalkthroughCodeView.d.ts.map +1 -1
- package/dist/pierre/PierreWalkthroughCodeView.js +53 -2
- package/dist/pierre/PierreWalkthroughCodeView.js.map +1 -1
- package/dist/subsystem/ComponentDeclaration.d.ts +13 -1
- package/dist/subsystem/ComponentDeclaration.d.ts.map +1 -1
- package/dist/subsystem/ComponentDeclaration.js +58 -7
- package/dist/subsystem/ComponentDeclaration.js.map +1 -1
- package/dist/subsystem/ConstructsCatalog.d.ts +6 -3
- package/dist/subsystem/ConstructsCatalog.d.ts.map +1 -1
- package/dist/subsystem/ConstructsCatalog.js +206 -217
- package/dist/subsystem/ConstructsCatalog.js.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts +10 -0
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +108 -31
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/SymbolInspectionCard.d.ts.map +1 -1
- package/dist/subsystem/SymbolInspectionCard.js +1 -1
- package/dist/subsystem/SymbolInspectionCard.js.map +1 -1
- package/dist/subsystem/model.d.ts.map +1 -1
- package/dist/subsystem/model.js +7 -1
- package/dist/subsystem/model.js.map +1 -1
- package/dist/utils/edgeLabel.d.ts +51 -0
- package/dist/utils/edgeLabel.d.ts.map +1 -0
- package/dist/utils/edgeLabel.js +81 -0
- package/dist/utils/edgeLabel.js.map +1 -0
- package/dist/utils/elkLayout.d.ts +6 -0
- package/dist/utils/elkLayout.d.ts.map +1 -1
- package/dist/utils/elkLayout.js +27 -6
- package/dist/utils/elkLayout.js.map +1 -1
- package/package.json +3 -2
- package/src/index.ts +7 -0
- package/src/pierre/PierreWalkthroughCodeView.tsx +75 -1
- package/src/stories/Subsystem/ComponentGraph/Flows.stories.tsx +222 -0
- package/src/stories/Subsystem/ConstructsCatalog.stories.tsx +443 -0
- package/src/subsystem/ComponentDeclaration.tsx +100 -7
- package/src/subsystem/ConstructsCatalog.tsx +291 -323
- package/src/subsystem/SubsystemComponentGraph.tsx +173 -30
- package/src/subsystem/SymbolInspectionCard.tsx +10 -9
- package/src/subsystem/model.ts +7 -1
- package/src/utils/edgeLabel.test.ts +23 -0
- package/src/utils/edgeLabel.ts +95 -0
- package/src/utils/elkLayout.ts +41 -6
|
@@ -2,20 +2,20 @@
|
|
|
2
2
|
* ConstructsCatalog — master/detail for a constructs-only subsystem
|
|
3
3
|
* (components, no topology or walkthrough edges).
|
|
4
4
|
*
|
|
5
|
-
* Left: the model's
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Left: the model's files as a tree; clicking a file toggles its
|
|
6
|
+
* constructs. Right: signatures grouped by repo, and within a repo stacked
|
|
7
|
+
* per file under one combined file header carrying the path and a
|
|
8
|
+
* description toggle; a declaration with a known line labels just that
|
|
9
|
+
* construct's line in a file-like gutter. File opens still use the bottom
|
|
10
|
+
* FileDrawer when the host injects a viewer.
|
|
8
11
|
*/
|
|
9
12
|
|
|
10
13
|
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
11
14
|
import type { ReactNode } from 'react';
|
|
12
15
|
import { useTheme } from '@principal-ade/industry-theme';
|
|
13
16
|
import { IndustryMarkdownSlide } from 'themed-markdown';
|
|
14
|
-
import { ChevronDown, ChevronRight, FileText, Folder } from 'lucide-react';
|
|
17
|
+
import { AlignLeft, ChevronDown, ChevronRight, FileText, Folder } from 'lucide-react';
|
|
15
18
|
import {
|
|
16
|
-
constructBadgeColor,
|
|
17
|
-
constructBadgeLabel,
|
|
18
|
-
deriveNameFromSymbol,
|
|
19
19
|
formatPurl,
|
|
20
20
|
type SubsystemComponent,
|
|
21
21
|
} from './model';
|
|
@@ -140,6 +140,24 @@ function repoLogo(c: SubsystemComponent): string | undefined {
|
|
|
140
140
|
return undefined;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Split a repo's declarations into consecutive runs that share a `file`, so
|
|
145
|
+
* each run can render as one connected stack. Components with no `file` are
|
|
146
|
+
* never merged (they have no shared location to group under).
|
|
147
|
+
*/
|
|
148
|
+
function splitByFileRun(items: SubsystemComponent[]): SubsystemComponent[][] {
|
|
149
|
+
const runs: SubsystemComponent[][] = [];
|
|
150
|
+
for (const c of items) {
|
|
151
|
+
const prev = runs[runs.length - 1];
|
|
152
|
+
const key = c.file || `alias:${c.alias}`;
|
|
153
|
+
const prevKey =
|
|
154
|
+
prev && prev[0] ? prev[0].file || `alias:${prev[0].alias}` : null;
|
|
155
|
+
if (prev && prevKey === key) prev.push(c);
|
|
156
|
+
else runs.push([c]);
|
|
157
|
+
}
|
|
158
|
+
return runs;
|
|
159
|
+
}
|
|
160
|
+
|
|
143
161
|
export function ConstructsCatalog({
|
|
144
162
|
components,
|
|
145
163
|
onSelect,
|
|
@@ -177,24 +195,20 @@ export function ConstructsCatalog({
|
|
|
177
195
|
.map((x) => x.c);
|
|
178
196
|
}, [components]);
|
|
179
197
|
|
|
180
|
-
const [visibleAliases, setVisibleAliases] = useState<string[]>(
|
|
181
|
-
|
|
182
|
-
);
|
|
183
|
-
const [focusedAlias, setFocusedAlias] = useState<string | null>(
|
|
184
|
-
() => orderedComponents[0]?.alias ?? null,
|
|
185
|
-
);
|
|
198
|
+
const [visibleAliases, setVisibleAliases] = useState<string[]>([]);
|
|
199
|
+
const [focusedAlias, setFocusedAlias] = useState<string | null>(null);
|
|
200
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
186
201
|
const [descriptionVisible, setDescriptionVisible] = useState(false);
|
|
187
202
|
const [descToggleHover, setDescToggleHover] = useState(false);
|
|
188
203
|
const [hoveredRow, setHoveredRow] = useState<string | null>(null);
|
|
189
|
-
const [sidebarTab, setSidebarTab] = useState<'constructs' | 'files'>('files');
|
|
190
204
|
const [collapsedFolders, setCollapsedFolders] = useState<Set<string>>(
|
|
191
205
|
new Set(),
|
|
192
206
|
);
|
|
207
|
+
// Files whose shared-header description toggle is expanded (keyed by file run).
|
|
208
|
+
const [openPurposes, setOpenPurposes] = useState<Set<string>>(new Set());
|
|
193
209
|
const [drawer, setDrawer] = useState<{ file: string; startLine?: number } | null>(
|
|
194
210
|
null,
|
|
195
211
|
);
|
|
196
|
-
const focusedAliasRef = useRef<string | null>(focusedAlias);
|
|
197
|
-
focusedAliasRef.current = focusedAlias;
|
|
198
212
|
|
|
199
213
|
const visibleSet = useMemo(() => new Set(visibleAliases), [visibleAliases]);
|
|
200
214
|
const visibleComponents = useMemo(
|
|
@@ -202,15 +216,38 @@ export function ConstructsCatalog({
|
|
|
202
216
|
[orderedComponents, visibleSet],
|
|
203
217
|
);
|
|
204
218
|
|
|
219
|
+
// When nothing is toggled on, the right pane falls back to a searchable list
|
|
220
|
+
// of every construct, rendered as if all were selected.
|
|
221
|
+
const searchActive = visibleComponents.length === 0;
|
|
222
|
+
const searchResults = useMemo(() => {
|
|
223
|
+
if (!searchActive) return visibleComponents;
|
|
224
|
+
const q = searchQuery.trim().toLowerCase();
|
|
225
|
+
if (!q) return orderedComponents;
|
|
226
|
+
return orderedComponents.filter((c) => {
|
|
227
|
+
const haystack = [
|
|
228
|
+
c.name,
|
|
229
|
+
c.symbol,
|
|
230
|
+
c.alias,
|
|
231
|
+
c.file,
|
|
232
|
+
c.construct,
|
|
233
|
+
c.stereotype,
|
|
234
|
+
c.purpose,
|
|
235
|
+
]
|
|
236
|
+
.filter(Boolean)
|
|
237
|
+
.join(' ')
|
|
238
|
+
.toLowerCase();
|
|
239
|
+
return haystack.includes(q);
|
|
240
|
+
});
|
|
241
|
+
}, [searchActive, searchQuery, orderedComponents, visibleComponents]);
|
|
242
|
+
|
|
205
243
|
const fileTree = useMemo(() => buildFileTree(orderedComponents), [orderedComponents]);
|
|
206
244
|
const hasFiles = useMemo(
|
|
207
245
|
() => orderedComponents.some((c) => !!c.file),
|
|
208
246
|
[orderedComponents],
|
|
209
247
|
);
|
|
210
|
-
const effectiveTab = hasFiles ? sidebarTab : 'constructs';
|
|
211
248
|
|
|
212
|
-
// Group
|
|
213
|
-
// renders once per group instead of on every card.
|
|
249
|
+
// Group the declarations on screen by purl so the repo/app identity
|
|
250
|
+
// (logo + name) renders once per group instead of on every card.
|
|
214
251
|
const repoGroups = useMemo(() => {
|
|
215
252
|
const groups: Array<{
|
|
216
253
|
key: string;
|
|
@@ -219,7 +256,7 @@ export function ConstructsCatalog({
|
|
|
219
256
|
items: SubsystemComponent[];
|
|
220
257
|
}> = [];
|
|
221
258
|
const index = new Map<string, number>();
|
|
222
|
-
for (const c of
|
|
259
|
+
for (const c of searchResults) {
|
|
223
260
|
const key = c.purl || c.alias;
|
|
224
261
|
let at = index.get(key);
|
|
225
262
|
if (at == null) {
|
|
@@ -231,12 +268,7 @@ export function ConstructsCatalog({
|
|
|
231
268
|
groups[at]!.items.push(c);
|
|
232
269
|
}
|
|
233
270
|
return groups;
|
|
234
|
-
}, [
|
|
235
|
-
|
|
236
|
-
useEffect(() => {
|
|
237
|
-
if (!focusedAlias) return;
|
|
238
|
-
document.getElementById(`construct-${focusedAlias}`)?.scrollIntoView({ block: 'nearest' });
|
|
239
|
-
}, [focusedAlias]);
|
|
271
|
+
}, [searchResults]);
|
|
240
272
|
|
|
241
273
|
useEffect(() => {
|
|
242
274
|
const last = visibleAliases[visibleAliases.length - 1];
|
|
@@ -260,31 +292,13 @@ export function ConstructsCatalog({
|
|
|
260
292
|
?.scrollIntoView({ block: 'nearest' });
|
|
261
293
|
}, [orderedComponents, drawer]);
|
|
262
294
|
|
|
263
|
-
// Drop aliases that left the model;
|
|
295
|
+
// Drop aliases that left the model; an empty set is valid (search fallback).
|
|
264
296
|
useEffect(() => {
|
|
265
297
|
const aliases = new Set(orderedComponents.map((c) => c.alias));
|
|
266
|
-
setVisibleAliases((prev) =>
|
|
267
|
-
|
|
268
|
-
if (next.length > 0 || orderedComponents.length === 0) return next;
|
|
269
|
-
return [orderedComponents[0]!.alias];
|
|
270
|
-
});
|
|
271
|
-
setFocusedAlias((prev) => {
|
|
272
|
-
if (prev && aliases.has(prev)) return prev;
|
|
273
|
-
return orderedComponents[0]?.alias ?? null;
|
|
274
|
-
});
|
|
298
|
+
setVisibleAliases((prev) => prev.filter((alias) => aliases.has(alias)));
|
|
299
|
+
setFocusedAlias((prev) => (prev && aliases.has(prev) ? prev : null));
|
|
275
300
|
}, [orderedComponents]);
|
|
276
301
|
|
|
277
|
-
const toggle = useCallback(
|
|
278
|
-
(alias: string) => {
|
|
279
|
-
setFocusedAlias(alias);
|
|
280
|
-
setVisibleAliases((prev) =>
|
|
281
|
-
prev.includes(alias) ? prev.filter((x) => x !== alias) : [...prev, alias],
|
|
282
|
-
);
|
|
283
|
-
onSelect?.(alias);
|
|
284
|
-
},
|
|
285
|
-
[onSelect],
|
|
286
|
-
);
|
|
287
|
-
|
|
288
302
|
const show = useCallback(
|
|
289
303
|
(alias: string) => {
|
|
290
304
|
setFocusedAlias(alias);
|
|
@@ -294,13 +308,6 @@ export function ConstructsCatalog({
|
|
|
294
308
|
[onSelect],
|
|
295
309
|
);
|
|
296
310
|
|
|
297
|
-
const focusAt = useCallback(
|
|
298
|
-
(alias: string) => {
|
|
299
|
-
setFocusedAlias(alias);
|
|
300
|
-
},
|
|
301
|
-
[],
|
|
302
|
-
);
|
|
303
|
-
|
|
304
311
|
const toggleFolder = useCallback((path: string) => {
|
|
305
312
|
setCollapsedFolders((prev) => {
|
|
306
313
|
const next = new Set(prev);
|
|
@@ -310,8 +317,17 @@ export function ConstructsCatalog({
|
|
|
310
317
|
});
|
|
311
318
|
}, []);
|
|
312
319
|
|
|
320
|
+
const togglePurpose = useCallback((key: string) => {
|
|
321
|
+
setOpenPurposes((prev) => {
|
|
322
|
+
const next = new Set(prev);
|
|
323
|
+
if (next.has(key)) next.delete(key);
|
|
324
|
+
else next.add(key);
|
|
325
|
+
return next;
|
|
326
|
+
});
|
|
327
|
+
}, []);
|
|
328
|
+
|
|
313
329
|
/** File-tree click: show every construct declared in the file, or hide them
|
|
314
|
-
* all when they're already shown —
|
|
330
|
+
* all when they're already shown — a bulk toggle of the file's constructs. */
|
|
315
331
|
const toggleFile = useCallback(
|
|
316
332
|
(comps: SubsystemComponent[]) => {
|
|
317
333
|
const aliases = comps.map((c) => c.alias);
|
|
@@ -480,9 +496,18 @@ export function ConstructsCatalog({
|
|
|
480
496
|
(ref: string) => {
|
|
481
497
|
const comp = resolveRelated(components, ref);
|
|
482
498
|
if (!comp) return;
|
|
499
|
+
// In the all-constructs search fallback nothing needs revealing — just
|
|
500
|
+
// surface the target. Otherwise add it to the visible set.
|
|
501
|
+
if (searchActive) {
|
|
502
|
+
setFocusedAlias(comp.alias);
|
|
503
|
+
document
|
|
504
|
+
.getElementById(`construct-signature-${comp.alias}`)
|
|
505
|
+
?.scrollIntoView({ block: 'nearest' });
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
483
508
|
show(comp.alias);
|
|
484
509
|
},
|
|
485
|
-
[components, show],
|
|
510
|
+
[components, show, searchActive],
|
|
486
511
|
);
|
|
487
512
|
|
|
488
513
|
const fileViewer = useMemo(() => {
|
|
@@ -507,60 +532,13 @@ export function ConstructsCatalog({
|
|
|
507
532
|
[],
|
|
508
533
|
);
|
|
509
534
|
|
|
510
|
-
const moveFocus = useCallback(
|
|
511
|
-
(delta: number) => {
|
|
512
|
-
if (orderedComponents.length === 0) return;
|
|
513
|
-
const idx = Math.max(
|
|
514
|
-
0,
|
|
515
|
-
orderedComponents.findIndex((c) => c.alias === focusedAliasRef.current),
|
|
516
|
-
);
|
|
517
|
-
const next =
|
|
518
|
-
orderedComponents[
|
|
519
|
-
(idx + delta + orderedComponents.length) % orderedComponents.length
|
|
520
|
-
]!;
|
|
521
|
-
focusAt(next.alias);
|
|
522
|
-
},
|
|
523
|
-
[orderedComponents, focusAt],
|
|
524
|
-
);
|
|
525
|
-
|
|
526
535
|
useEffect(() => {
|
|
527
536
|
const onKey = (e: KeyboardEvent) => {
|
|
528
|
-
|
|
529
|
-
if (
|
|
530
|
-
el &&
|
|
531
|
-
(el.tagName === 'INPUT' ||
|
|
532
|
-
el.tagName === 'TEXTAREA' ||
|
|
533
|
-
el.tagName === 'SELECT' ||
|
|
534
|
-
el.isContentEditable)
|
|
535
|
-
) {
|
|
536
|
-
return;
|
|
537
|
-
}
|
|
538
|
-
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
|
539
|
-
if (e.key === 'ArrowDown') {
|
|
540
|
-
e.preventDefault();
|
|
541
|
-
moveFocus(1);
|
|
542
|
-
} else if (e.key === 'ArrowUp') {
|
|
543
|
-
e.preventDefault();
|
|
544
|
-
moveFocus(-1);
|
|
545
|
-
} else if (e.key === 'Home') {
|
|
546
|
-
e.preventDefault();
|
|
547
|
-
if (orderedComponents[0]) focusAt(orderedComponents[0].alias);
|
|
548
|
-
} else if (e.key === 'End') {
|
|
549
|
-
e.preventDefault();
|
|
550
|
-
const last = orderedComponents[orderedComponents.length - 1];
|
|
551
|
-
if (last) focusAt(last.alias);
|
|
552
|
-
} else if (e.key === ' ' || e.key === 'Enter') {
|
|
553
|
-
if (focusedAliasRef.current) {
|
|
554
|
-
e.preventDefault();
|
|
555
|
-
toggle(focusedAliasRef.current);
|
|
556
|
-
}
|
|
557
|
-
} else if (e.key === 'Escape' && drawer) {
|
|
558
|
-
setDrawer(null);
|
|
559
|
-
}
|
|
537
|
+
if (e.key === 'Escape' && drawer) setDrawer(null);
|
|
560
538
|
};
|
|
561
539
|
window.addEventListener('keydown', onKey);
|
|
562
540
|
return () => window.removeEventListener('keydown', onKey);
|
|
563
|
-
}, [
|
|
541
|
+
}, [drawer]);
|
|
564
542
|
|
|
565
543
|
const showChrome =
|
|
566
544
|
!hideSidebar &&
|
|
@@ -682,183 +660,7 @@ export function ConstructsCatalog({
|
|
|
682
660
|
{sidebarAfterDescription}
|
|
683
661
|
</div>
|
|
684
662
|
)}
|
|
685
|
-
|
|
686
|
-
role="tablist"
|
|
687
|
-
aria-label="Sidebar view"
|
|
688
|
-
style={{
|
|
689
|
-
display: 'flex',
|
|
690
|
-
width: '100%',
|
|
691
|
-
flexShrink: 0,
|
|
692
|
-
borderBottom: `1px solid ${theme.colors.border}`,
|
|
693
|
-
background:
|
|
694
|
-
theme.colors.backgroundSecondary ?? theme.colors.background,
|
|
695
|
-
}}
|
|
696
|
-
>
|
|
697
|
-
{(['files', 'constructs'] as const).map((tab) => {
|
|
698
|
-
if (tab === 'files' && !hasFiles) return null;
|
|
699
|
-
const active = effectiveTab === tab;
|
|
700
|
-
return (
|
|
701
|
-
<button
|
|
702
|
-
key={tab}
|
|
703
|
-
type="button"
|
|
704
|
-
role="tab"
|
|
705
|
-
aria-selected={active}
|
|
706
|
-
onClick={() => setSidebarTab(tab)}
|
|
707
|
-
style={{
|
|
708
|
-
flex: 1,
|
|
709
|
-
minWidth: 0,
|
|
710
|
-
padding: '8px 8px',
|
|
711
|
-
border: 'none',
|
|
712
|
-
borderRadius: 0,
|
|
713
|
-
background: active
|
|
714
|
-
? theme.colors.background
|
|
715
|
-
: 'transparent',
|
|
716
|
-
color: active ? theme.colors.text : muted,
|
|
717
|
-
fontSize: theme.fontSizes[0],
|
|
718
|
-
fontFamily: theme.fonts.monospace,
|
|
719
|
-
letterSpacing: 0.4,
|
|
720
|
-
textTransform: 'uppercase',
|
|
721
|
-
cursor: 'pointer',
|
|
722
|
-
}}
|
|
723
|
-
>
|
|
724
|
-
{tab === 'constructs' ? 'Constructs' : 'Files'}
|
|
725
|
-
</button>
|
|
726
|
-
);
|
|
727
|
-
})}
|
|
728
|
-
</div>
|
|
729
|
-
{effectiveTab === 'constructs' ? (
|
|
730
|
-
<ul
|
|
731
|
-
role="listbox"
|
|
732
|
-
aria-label="Constructs"
|
|
733
|
-
aria-multiselectable="true"
|
|
734
|
-
aria-activedescendant={focusedAlias ? `construct-${focusedAlias}` : undefined}
|
|
735
|
-
style={{
|
|
736
|
-
listStyle: 'none',
|
|
737
|
-
margin: 0,
|
|
738
|
-
padding: '0 8px 12px',
|
|
739
|
-
overflowY: 'auto',
|
|
740
|
-
flex: 1,
|
|
741
|
-
minHeight: 0,
|
|
742
|
-
}}
|
|
743
|
-
>
|
|
744
|
-
{orderedComponents.map((c) => {
|
|
745
|
-
const visible = visibleSet.has(c.alias);
|
|
746
|
-
const focused = c.alias === focusedAlias;
|
|
747
|
-
const color = componentColor(c, pierreTheme);
|
|
748
|
-
const badgeColor = constructBadgeColor(c) ?? color;
|
|
749
|
-
const badgeLabel = constructBadgeLabel(c);
|
|
750
|
-
const displayName = deriveNameFromSymbol(
|
|
751
|
-
c.symbol,
|
|
752
|
-
c.construct,
|
|
753
|
-
c.name,
|
|
754
|
-
c.file,
|
|
755
|
-
c.stereotype,
|
|
756
|
-
);
|
|
757
|
-
const rowOpen =
|
|
758
|
-
drawer != null &&
|
|
759
|
-
drawer.startLine != null &&
|
|
760
|
-
!!c.file &&
|
|
761
|
-
c.file === drawer.file &&
|
|
762
|
-
c.declarationRef?.startLine === drawer.startLine;
|
|
763
|
-
const hovered = hoveredRow === c.alias;
|
|
764
|
-
return (
|
|
765
|
-
<li key={c.alias} role="presentation">
|
|
766
|
-
<button
|
|
767
|
-
type="button"
|
|
768
|
-
id={`construct-${c.alias}`}
|
|
769
|
-
role="option"
|
|
770
|
-
aria-selected={visible}
|
|
771
|
-
data-testid={`construct-row-${c.alias}`}
|
|
772
|
-
onClick={() => toggle(c.alias)}
|
|
773
|
-
onMouseEnter={() => setHoveredRow(c.alias)}
|
|
774
|
-
onMouseLeave={() =>
|
|
775
|
-
setHoveredRow((h) => (h === c.alias ? null : h))
|
|
776
|
-
}
|
|
777
|
-
style={{
|
|
778
|
-
display: 'flex',
|
|
779
|
-
flexDirection: 'column',
|
|
780
|
-
alignItems: 'stretch',
|
|
781
|
-
gap: 2,
|
|
782
|
-
width: '100%',
|
|
783
|
-
margin: '2px 0',
|
|
784
|
-
padding: '8px 10px 8px 12px',
|
|
785
|
-
textAlign: 'left',
|
|
786
|
-
border: 'none',
|
|
787
|
-
borderRadius: 6,
|
|
788
|
-
background: hovered
|
|
789
|
-
? theme.colors.border
|
|
790
|
-
: visible
|
|
791
|
-
? theme.colors.background
|
|
792
|
-
: 'transparent',
|
|
793
|
-
cursor: 'pointer',
|
|
794
|
-
transition: 'background 100ms ease, opacity 100ms ease',
|
|
795
|
-
boxShadow: [
|
|
796
|
-
visible ? `inset 0 0 0 1px ${theme.colors.border}` : null,
|
|
797
|
-
rowOpen ? `inset 2px 0 0 ${accentColor}` : null,
|
|
798
|
-
]
|
|
799
|
-
.filter(Boolean)
|
|
800
|
-
.join(', ') || undefined,
|
|
801
|
-
outline: focused && !visible ? `1px dotted ${muted}` : undefined,
|
|
802
|
-
outlineOffset: -1,
|
|
803
|
-
opacity: visible ? 1 : hovered ? 1 : 0.72,
|
|
804
|
-
}}
|
|
805
|
-
>
|
|
806
|
-
<span
|
|
807
|
-
style={{
|
|
808
|
-
display: 'flex',
|
|
809
|
-
alignItems: 'baseline',
|
|
810
|
-
gap: 8,
|
|
811
|
-
minWidth: 0,
|
|
812
|
-
}}
|
|
813
|
-
>
|
|
814
|
-
<span
|
|
815
|
-
style={{
|
|
816
|
-
flex: 1,
|
|
817
|
-
minWidth: 0,
|
|
818
|
-
overflow: 'hidden',
|
|
819
|
-
textOverflow: 'ellipsis',
|
|
820
|
-
whiteSpace: 'nowrap',
|
|
821
|
-
fontFamily: theme.fonts.monospace,
|
|
822
|
-
fontSize: theme.fontSizes[1],
|
|
823
|
-
color,
|
|
824
|
-
fontWeight: visible ? 600 : 500,
|
|
825
|
-
}}
|
|
826
|
-
>
|
|
827
|
-
{displayName}
|
|
828
|
-
</span>
|
|
829
|
-
</span>
|
|
830
|
-
{badgeLabel && (
|
|
831
|
-
<span
|
|
832
|
-
style={{
|
|
833
|
-
display: 'flex',
|
|
834
|
-
alignItems: 'center',
|
|
835
|
-
minWidth: 0,
|
|
836
|
-
}}
|
|
837
|
-
>
|
|
838
|
-
<span
|
|
839
|
-
style={{
|
|
840
|
-
flex: 1,
|
|
841
|
-
minWidth: 0,
|
|
842
|
-
overflow: 'hidden',
|
|
843
|
-
textOverflow: 'ellipsis',
|
|
844
|
-
whiteSpace: 'nowrap',
|
|
845
|
-
fontFamily: theme.fonts.monospace,
|
|
846
|
-
fontSize: theme.fontSizes[0],
|
|
847
|
-
letterSpacing: 0.4,
|
|
848
|
-
textTransform: 'uppercase',
|
|
849
|
-
color: badgeColor,
|
|
850
|
-
}}
|
|
851
|
-
>
|
|
852
|
-
{badgeLabel}
|
|
853
|
-
</span>
|
|
854
|
-
</span>
|
|
855
|
-
)}
|
|
856
|
-
</button>
|
|
857
|
-
</li>
|
|
858
|
-
);
|
|
859
|
-
})}
|
|
860
|
-
</ul>
|
|
861
|
-
) : (
|
|
663
|
+
{hasFiles ? (
|
|
862
664
|
<ul
|
|
863
665
|
role="tree"
|
|
864
666
|
aria-label="Files"
|
|
@@ -873,6 +675,18 @@ export function ConstructsCatalog({
|
|
|
873
675
|
>
|
|
874
676
|
{renderTree(fileTree, 0)}
|
|
875
677
|
</ul>
|
|
678
|
+
) : (
|
|
679
|
+
<p
|
|
680
|
+
style={{
|
|
681
|
+
margin: 0,
|
|
682
|
+
padding: '12px 16px',
|
|
683
|
+
color: muted,
|
|
684
|
+
fontFamily: theme.fonts.body,
|
|
685
|
+
fontSize: theme.fontSizes[0],
|
|
686
|
+
}}
|
|
687
|
+
>
|
|
688
|
+
No files in this model.
|
|
689
|
+
</p>
|
|
876
690
|
)}
|
|
877
691
|
</aside>
|
|
878
692
|
<div
|
|
@@ -885,6 +699,30 @@ export function ConstructsCatalog({
|
|
|
885
699
|
flexDirection: 'column',
|
|
886
700
|
}}
|
|
887
701
|
>
|
|
702
|
+
{searchActive && (
|
|
703
|
+
<div style={{ flexShrink: 0, padding: '12px 16px 0' }}>
|
|
704
|
+
<input
|
|
705
|
+
type="search"
|
|
706
|
+
value={searchQuery}
|
|
707
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
708
|
+
placeholder="Search constructs…"
|
|
709
|
+
aria-label="Search constructs"
|
|
710
|
+
data-testid="construct-search"
|
|
711
|
+
style={{
|
|
712
|
+
width: '100%',
|
|
713
|
+
boxSizing: 'border-box',
|
|
714
|
+
padding: '8px 10px',
|
|
715
|
+
border: `1px solid ${theme.colors.border}`,
|
|
716
|
+
borderRadius: 6,
|
|
717
|
+
background: theme.colors.background,
|
|
718
|
+
color: theme.colors.text,
|
|
719
|
+
fontFamily: theme.fonts.monospace,
|
|
720
|
+
fontSize: theme.fontSizes[1],
|
|
721
|
+
outline: 'none',
|
|
722
|
+
}}
|
|
723
|
+
/>
|
|
724
|
+
</div>
|
|
725
|
+
)}
|
|
888
726
|
<div
|
|
889
727
|
data-testid="construct-signature"
|
|
890
728
|
style={{
|
|
@@ -897,7 +735,7 @@ export function ConstructsCatalog({
|
|
|
897
735
|
gap: 12,
|
|
898
736
|
}}
|
|
899
737
|
>
|
|
900
|
-
{
|
|
738
|
+
{searchResults.length > 0 ? (
|
|
901
739
|
repoGroups.map((group) => (
|
|
902
740
|
<div
|
|
903
741
|
key={group.key}
|
|
@@ -931,53 +769,181 @@ export function ConstructsCatalog({
|
|
|
931
769
|
{group.label}
|
|
932
770
|
</span>
|
|
933
771
|
</div>
|
|
934
|
-
{group.items.map((
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
//
|
|
938
|
-
|
|
939
|
-
const
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
772
|
+
{splitByFileRun(group.items).map((run) => {
|
|
773
|
+
const lead = run[0]!;
|
|
774
|
+
const hasFile = !!lead.file;
|
|
775
|
+
// Every file-backed run gets the combined header, one
|
|
776
|
+
// declaration or many, so the style stays consistent.
|
|
777
|
+
const showHeader = hasFile;
|
|
778
|
+
const runKey = `${group.key}:${lead.file || lead.alias}`;
|
|
779
|
+
const runHasPurpose = run.some((c) => !!c.purpose?.trim());
|
|
780
|
+
const purposeOpen = openPurposes.has(runKey);
|
|
781
|
+
const runFileOpen =
|
|
782
|
+
drawer != null && hasFile && lead.file === drawer.file;
|
|
943
783
|
return (
|
|
944
784
|
<div
|
|
945
|
-
key={
|
|
946
|
-
id={`construct-signature-${c.alias}`}
|
|
785
|
+
key={runKey}
|
|
947
786
|
style={{
|
|
948
|
-
|
|
949
|
-
|
|
787
|
+
display: 'flex',
|
|
788
|
+
flexDirection: 'column',
|
|
789
|
+
border: `1px solid ${runFileOpen ? accentColor : theme.colors.border}`,
|
|
950
790
|
borderRadius: 8,
|
|
951
791
|
overflow: 'hidden',
|
|
952
792
|
flexShrink: 0,
|
|
953
793
|
}}
|
|
954
794
|
>
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
795
|
+
{showHeader && (
|
|
796
|
+
<div
|
|
797
|
+
style={{
|
|
798
|
+
display: 'flex',
|
|
799
|
+
alignItems: 'center',
|
|
800
|
+
gap: 6,
|
|
801
|
+
width: '100%',
|
|
802
|
+
boxSizing: 'border-box',
|
|
803
|
+
padding: '6px 10px',
|
|
804
|
+
borderBottom: `1px solid ${
|
|
805
|
+
runFileOpen ? accentColor : theme.colors.border
|
|
806
|
+
}`,
|
|
807
|
+
background: runFileOpen
|
|
808
|
+
? `${accentColor}14`
|
|
809
|
+
: theme.colors.backgroundSecondary ??
|
|
810
|
+
theme.colors.background,
|
|
811
|
+
color: runFileOpen
|
|
812
|
+
? accentColor
|
|
813
|
+
: theme.colors.textSecondary ?? muted,
|
|
814
|
+
fontFamily: theme.fonts.monospace,
|
|
815
|
+
fontSize: theme.fontSizes[0],
|
|
816
|
+
}}
|
|
817
|
+
>
|
|
818
|
+
<button
|
|
819
|
+
type="button"
|
|
820
|
+
title={fileViewer ? `Open ${lead.file}` : lead.file}
|
|
821
|
+
onClick={
|
|
822
|
+
fileViewer
|
|
823
|
+
? () => onOpenFile(lead.file)
|
|
824
|
+
: undefined
|
|
825
|
+
}
|
|
826
|
+
style={{
|
|
827
|
+
display: 'flex',
|
|
828
|
+
alignItems: 'center',
|
|
829
|
+
gap: 6,
|
|
830
|
+
flex: 1,
|
|
831
|
+
minWidth: 0,
|
|
832
|
+
padding: 0,
|
|
833
|
+
border: 'none',
|
|
834
|
+
background: 'transparent',
|
|
835
|
+
color: 'inherit',
|
|
836
|
+
fontFamily: 'inherit',
|
|
837
|
+
fontSize: 'inherit',
|
|
838
|
+
textAlign: 'left',
|
|
839
|
+
cursor: fileViewer ? 'pointer' : 'default',
|
|
840
|
+
}}
|
|
841
|
+
>
|
|
842
|
+
<FileText size={12} style={{ flexShrink: 0 }} />
|
|
843
|
+
<span
|
|
844
|
+
style={{
|
|
845
|
+
overflow: 'hidden',
|
|
846
|
+
textOverflow: 'ellipsis',
|
|
847
|
+
whiteSpace: 'nowrap',
|
|
848
|
+
}}
|
|
849
|
+
>
|
|
850
|
+
{lead.file}
|
|
851
|
+
</span>
|
|
852
|
+
</button>
|
|
853
|
+
{runHasPurpose && (
|
|
854
|
+
<button
|
|
855
|
+
type="button"
|
|
856
|
+
title={
|
|
857
|
+
purposeOpen ? 'Hide description' : 'Show description'
|
|
858
|
+
}
|
|
859
|
+
aria-label={
|
|
860
|
+
purposeOpen ? 'Hide description' : 'Show description'
|
|
861
|
+
}
|
|
862
|
+
aria-expanded={purposeOpen}
|
|
863
|
+
onClick={() => togglePurpose(runKey)}
|
|
864
|
+
style={{
|
|
865
|
+
display: 'inline-flex',
|
|
866
|
+
alignItems: 'center',
|
|
867
|
+
justifyContent: 'center',
|
|
868
|
+
flexShrink: 0,
|
|
869
|
+
width: 20,
|
|
870
|
+
height: 20,
|
|
871
|
+
padding: 0,
|
|
872
|
+
border: 'none',
|
|
873
|
+
borderRadius: 4,
|
|
874
|
+
background: 'transparent',
|
|
875
|
+
color: purposeOpen
|
|
876
|
+
? accentColor
|
|
877
|
+
: theme.colors.textSecondary ?? muted,
|
|
878
|
+
cursor: 'pointer',
|
|
879
|
+
}}
|
|
880
|
+
>
|
|
881
|
+
<AlignLeft size={13} />
|
|
882
|
+
</button>
|
|
883
|
+
)}
|
|
884
|
+
</div>
|
|
885
|
+
)}
|
|
886
|
+
{run.map((c, i) => {
|
|
887
|
+
// File-level: every declaration sharing the open file
|
|
888
|
+
// shows the indicator. Line-level: only the declaration
|
|
889
|
+
// actually highlighted gets the inner accent ring.
|
|
890
|
+
const fileOpen =
|
|
891
|
+
drawer != null && !!c.file && c.file === drawer.file;
|
|
892
|
+
const lineOpen =
|
|
893
|
+
fileOpen &&
|
|
894
|
+
drawer.startLine != null &&
|
|
895
|
+
c.declarationRef?.startLine === drawer.startLine;
|
|
896
|
+
// A combined header owns the path, so declarations under
|
|
897
|
+
// it drop their own badge; those with no known line just
|
|
898
|
+
// render unnumbered. Without a header, keep the badge.
|
|
899
|
+
const numbered =
|
|
900
|
+
hasFile && c.declarationRef?.startLine != null;
|
|
901
|
+
return (
|
|
902
|
+
<div
|
|
903
|
+
key={c.alias}
|
|
904
|
+
id={`construct-signature-${c.alias}`}
|
|
905
|
+
style={{
|
|
906
|
+
borderTop:
|
|
907
|
+
i > 0
|
|
908
|
+
? `1px solid ${theme.colors.border}`
|
|
909
|
+
: undefined,
|
|
910
|
+
boxShadow: lineOpen
|
|
911
|
+
? `inset 0 0 0 1px ${accentColor}`
|
|
912
|
+
: undefined,
|
|
913
|
+
}}
|
|
914
|
+
>
|
|
915
|
+
<ComponentDeclaration
|
|
916
|
+
component={c}
|
|
917
|
+
onOpenFile={fileViewer ? onOpenFile : undefined}
|
|
918
|
+
defaultShowFile={showHeader ? false : !!fileViewer}
|
|
919
|
+
onRelatedSelect={onRelatedSelect}
|
|
920
|
+
onInspectSymbol={
|
|
921
|
+
onInspectSymbol
|
|
922
|
+
? (symbol, ref) =>
|
|
923
|
+
onInspectSymbol({
|
|
924
|
+
purl: c.purl,
|
|
925
|
+
file: c.file,
|
|
926
|
+
symbol,
|
|
927
|
+
ref,
|
|
928
|
+
})
|
|
929
|
+
: undefined
|
|
930
|
+
}
|
|
931
|
+
fileOpen={fileOpen}
|
|
932
|
+
declarationOpen={lineOpen}
|
|
933
|
+
showRepoIdentity={false}
|
|
934
|
+
fileBadgeChrome={showHeader ? false : !!fileViewer}
|
|
935
|
+
hideActions={showHeader}
|
|
936
|
+
showPurpose={showHeader ? purposeOpen : undefined}
|
|
937
|
+
lineNumbers={numbered}
|
|
938
|
+
verification={
|
|
939
|
+
componentVerification && focusedAlias === c.alias
|
|
940
|
+
? componentVerification
|
|
941
|
+
: undefined
|
|
942
|
+
}
|
|
943
|
+
/>
|
|
944
|
+
</div>
|
|
945
|
+
);
|
|
946
|
+
})}
|
|
981
947
|
</div>
|
|
982
948
|
);
|
|
983
949
|
})}
|
|
@@ -992,7 +958,9 @@ export function ConstructsCatalog({
|
|
|
992
958
|
fontSize: theme.fontSizes[1],
|
|
993
959
|
}}
|
|
994
960
|
>
|
|
995
|
-
|
|
961
|
+
{searchQuery.trim()
|
|
962
|
+
? `No constructs match “${searchQuery.trim()}”.`
|
|
963
|
+
: 'No constructs in this model.'}
|
|
996
964
|
</p>
|
|
997
965
|
)}
|
|
998
966
|
</div>
|