@principal-ai/subsystems-react 0.37.1 → 0.37.3
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/subsystem/ComponentDeclaration.d.ts +4 -1
- package/dist/subsystem/ComponentDeclaration.d.ts.map +1 -1
- package/dist/subsystem/ComponentDeclaration.js +18 -1
- package/dist/subsystem/ComponentDeclaration.js.map +1 -1
- package/dist/subsystem/ConstructsCatalog.d.ts +17 -3
- package/dist/subsystem/ConstructsCatalog.d.ts.map +1 -1
- package/dist/subsystem/ConstructsCatalog.js +99 -30
- package/dist/subsystem/ConstructsCatalog.js.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts +6 -0
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/package.json +1 -1
- package/src/stories/Subsystem/ConstructsCatalog.stories.tsx +15 -1
- package/src/subsystem/ComponentDeclaration.tsx +30 -0
- package/src/subsystem/ConstructsCatalog.tsx +207 -71
- package/src/subsystem/SubsystemComponentGraph.tsx +8 -0
|
@@ -6,15 +6,26 @@
|
|
|
6
6
|
* constructs. Right: signatures grouped by repo, and within a repo stacked
|
|
7
7
|
* per file under one combined file header carrying the path and a
|
|
8
8
|
* description toggle; a declaration with a known line labels just that
|
|
9
|
-
* construct's line in a file-like gutter
|
|
10
|
-
*
|
|
9
|
+
* construct's line in a file-like gutter, and `external` constructs get a
|
|
10
|
+
* header naming their kind with the body naming the construct. The model
|
|
11
|
+
* description's toggle sits in the left chrome and opens the markdown as an
|
|
12
|
+
* overlay over the construct area. File opens still use the bottom FileDrawer
|
|
13
|
+
* when the host injects a viewer.
|
|
11
14
|
*/
|
|
12
15
|
|
|
13
16
|
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
14
17
|
import type { ReactNode } from 'react';
|
|
15
18
|
import { useTheme } from '@principal-ade/industry-theme';
|
|
16
19
|
import { IndustryMarkdownSlide } from 'themed-markdown';
|
|
17
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
AlignLeft,
|
|
22
|
+
Box,
|
|
23
|
+
ChevronDown,
|
|
24
|
+
ChevronRight,
|
|
25
|
+
FileText,
|
|
26
|
+
Folder,
|
|
27
|
+
X,
|
|
28
|
+
} from 'lucide-react';
|
|
18
29
|
import {
|
|
19
30
|
formatPurl,
|
|
20
31
|
type SubsystemComponent,
|
|
@@ -33,7 +44,18 @@ export interface ConstructsCatalogProps {
|
|
|
33
44
|
onSelect?: (componentAlias: string) => void;
|
|
34
45
|
title?: string;
|
|
35
46
|
hideSidebar?: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Model description. Its toggle lives in the left chrome; opening it shows
|
|
49
|
+
* the markdown as an overlay over the construct area.
|
|
50
|
+
*/
|
|
36
51
|
description?: string;
|
|
52
|
+
/**
|
|
53
|
+
* Controlled open state for the description overlay. When provided the host
|
|
54
|
+
* owns it (e.g. a button in its own header) and the built-in toggle just
|
|
55
|
+
* reports changes; otherwise the built-in toggle drives the state.
|
|
56
|
+
*/
|
|
57
|
+
descriptionOpen?: boolean;
|
|
58
|
+
onDescriptionOpenChange?: (open: boolean) => void;
|
|
37
59
|
diagnostic?: SubsystemDiagnostic;
|
|
38
60
|
sidebarExtra?: ReactNode;
|
|
39
61
|
sidebarAfterDescription?: ReactNode;
|
|
@@ -164,6 +186,8 @@ export function ConstructsCatalog({
|
|
|
164
186
|
title,
|
|
165
187
|
hideSidebar,
|
|
166
188
|
description,
|
|
189
|
+
descriptionOpen: descriptionOpenProp,
|
|
190
|
+
onDescriptionOpenChange,
|
|
167
191
|
diagnostic,
|
|
168
192
|
sidebarExtra,
|
|
169
193
|
sidebarAfterDescription,
|
|
@@ -199,13 +223,21 @@ export function ConstructsCatalog({
|
|
|
199
223
|
const [focusedAlias, setFocusedAlias] = useState<string | null>(null);
|
|
200
224
|
const [searchQuery, setSearchQuery] = useState('');
|
|
201
225
|
const [descriptionVisible, setDescriptionVisible] = useState(false);
|
|
226
|
+
const descriptionOpen = descriptionOpenProp ?? descriptionVisible;
|
|
227
|
+
const setDescriptionOpen = (open: boolean) => {
|
|
228
|
+
setDescriptionVisible(open);
|
|
229
|
+
onDescriptionOpenChange?.(open);
|
|
230
|
+
};
|
|
202
231
|
const [descToggleHover, setDescToggleHover] = useState(false);
|
|
203
232
|
const [hoveredRow, setHoveredRow] = useState<string | null>(null);
|
|
204
233
|
const [collapsedFolders, setCollapsedFolders] = useState<Set<string>>(
|
|
205
234
|
new Set(),
|
|
206
235
|
);
|
|
207
|
-
//
|
|
208
|
-
|
|
236
|
+
// Explicit description toggles per file run. Unset falls back to the kind's
|
|
237
|
+
// default (externals open, files closed).
|
|
238
|
+
const [purposeOverrides, setPurposeOverrides] = useState<
|
|
239
|
+
Record<string, boolean>
|
|
240
|
+
>({});
|
|
209
241
|
const [drawer, setDrawer] = useState<{ file: string; startLine?: number } | null>(
|
|
210
242
|
null,
|
|
211
243
|
);
|
|
@@ -317,13 +349,8 @@ export function ConstructsCatalog({
|
|
|
317
349
|
});
|
|
318
350
|
}, []);
|
|
319
351
|
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
const next = new Set(prev);
|
|
323
|
-
if (next.has(key)) next.delete(key);
|
|
324
|
-
else next.add(key);
|
|
325
|
-
return next;
|
|
326
|
-
});
|
|
352
|
+
const setPurposeOpen = useCallback((key: string, open: boolean) => {
|
|
353
|
+
setPurposeOverrides((prev) => ({ ...prev, [key]: open }));
|
|
327
354
|
}, []);
|
|
328
355
|
|
|
329
356
|
/** File-tree click: show every construct declared in the file, or hide them
|
|
@@ -542,8 +569,13 @@ export function ConstructsCatalog({
|
|
|
542
569
|
|
|
543
570
|
const showChrome =
|
|
544
571
|
!hideSidebar &&
|
|
545
|
-
!!(
|
|
546
|
-
|
|
572
|
+
!!(
|
|
573
|
+
title ||
|
|
574
|
+
description ||
|
|
575
|
+
diagnostic ||
|
|
576
|
+
sidebarExtra ||
|
|
577
|
+
sidebarAfterDescription
|
|
578
|
+
);
|
|
547
579
|
|
|
548
580
|
return (
|
|
549
581
|
<div
|
|
@@ -573,7 +605,6 @@ export function ConstructsCatalog({
|
|
|
573
605
|
{showChrome && (
|
|
574
606
|
<div
|
|
575
607
|
style={{
|
|
576
|
-
flex: showDesc ? '0 0 auto' : undefined,
|
|
577
608
|
padding: '16px 16px 8px',
|
|
578
609
|
display: 'flex',
|
|
579
610
|
flexDirection: 'column',
|
|
@@ -611,16 +642,16 @@ export function ConstructsCatalog({
|
|
|
611
642
|
{description && (
|
|
612
643
|
<button
|
|
613
644
|
type="button"
|
|
614
|
-
aria-expanded={
|
|
645
|
+
aria-expanded={descriptionOpen}
|
|
615
646
|
aria-label={
|
|
616
|
-
|
|
647
|
+
descriptionOpen ? 'Hide description' : 'Show description'
|
|
617
648
|
}
|
|
618
649
|
title={
|
|
619
|
-
|
|
650
|
+
descriptionOpen ? 'Hide description' : 'Show description'
|
|
620
651
|
}
|
|
621
652
|
onMouseEnter={() => setDescToggleHover(true)}
|
|
622
653
|
onMouseLeave={() => setDescToggleHover(false)}
|
|
623
|
-
onClick={() =>
|
|
654
|
+
onClick={() => setDescriptionOpen(!descriptionOpen)}
|
|
624
655
|
style={{
|
|
625
656
|
display: 'inline-flex',
|
|
626
657
|
alignItems: 'center',
|
|
@@ -631,8 +662,8 @@ export function ConstructsCatalog({
|
|
|
631
662
|
padding: 0,
|
|
632
663
|
border: 'none',
|
|
633
664
|
borderRadius: 4,
|
|
634
|
-
background:
|
|
635
|
-
color:
|
|
665
|
+
background: descriptionOpen || descToggleHover ? theme.colors.border : 'transparent',
|
|
666
|
+
color: descriptionOpen || descToggleHover ? theme.colors.text : muted,
|
|
636
667
|
cursor: 'pointer',
|
|
637
668
|
}}
|
|
638
669
|
>
|
|
@@ -642,21 +673,6 @@ export function ConstructsCatalog({
|
|
|
642
673
|
</div>
|
|
643
674
|
</div>
|
|
644
675
|
)}
|
|
645
|
-
{showDesc && (
|
|
646
|
-
<div style={{ fontSize: theme.fontSizes[0], lineHeight: 1.5 }}>
|
|
647
|
-
<IndustryMarkdownSlide
|
|
648
|
-
content={description!}
|
|
649
|
-
slideIdPrefix="constructs-desc"
|
|
650
|
-
slideIndex={0}
|
|
651
|
-
isVisible={true}
|
|
652
|
-
theme={theme}
|
|
653
|
-
disableScroll={true}
|
|
654
|
-
disableBasePadding
|
|
655
|
-
enableKeyboardScrolling={false}
|
|
656
|
-
autoFocusOnVisible={false}
|
|
657
|
-
/>
|
|
658
|
-
</div>
|
|
659
|
-
)}
|
|
660
676
|
{sidebarAfterDescription}
|
|
661
677
|
</div>
|
|
662
678
|
)}
|
|
@@ -699,6 +715,97 @@ export function ConstructsCatalog({
|
|
|
699
715
|
flexDirection: 'column',
|
|
700
716
|
}}
|
|
701
717
|
>
|
|
718
|
+
{description && descriptionOpen && (
|
|
719
|
+
<>
|
|
720
|
+
<div
|
|
721
|
+
onClick={() => setDescriptionOpen(false)}
|
|
722
|
+
style={{
|
|
723
|
+
position: 'absolute',
|
|
724
|
+
inset: 0,
|
|
725
|
+
zIndex: 2,
|
|
726
|
+
background: 'rgba(0,0,0,0.45)',
|
|
727
|
+
}}
|
|
728
|
+
/>
|
|
729
|
+
<div
|
|
730
|
+
style={{
|
|
731
|
+
position: 'absolute',
|
|
732
|
+
top: '50%',
|
|
733
|
+
left: '50%',
|
|
734
|
+
transform: 'translate(-50%, -50%)',
|
|
735
|
+
zIndex: 3,
|
|
736
|
+
width: 360,
|
|
737
|
+
maxWidth: 'calc(100% - 16px)',
|
|
738
|
+
maxHeight: '70%',
|
|
739
|
+
overflow: 'auto',
|
|
740
|
+
padding: 16,
|
|
741
|
+
border: `1px solid ${theme.colors.border}`,
|
|
742
|
+
borderRadius: 8,
|
|
743
|
+
background: theme.colors.background,
|
|
744
|
+
boxShadow: '0 8px 24px rgba(0,0,0,0.28)',
|
|
745
|
+
fontSize: theme.fontSizes[0],
|
|
746
|
+
lineHeight: 1.5,
|
|
747
|
+
}}
|
|
748
|
+
>
|
|
749
|
+
<div
|
|
750
|
+
style={{
|
|
751
|
+
display: 'flex',
|
|
752
|
+
alignItems: 'center',
|
|
753
|
+
gap: 8,
|
|
754
|
+
marginBottom: 12,
|
|
755
|
+
}}
|
|
756
|
+
>
|
|
757
|
+
<h3
|
|
758
|
+
style={{
|
|
759
|
+
margin: 0,
|
|
760
|
+
flex: 1,
|
|
761
|
+
minWidth: 0,
|
|
762
|
+
fontSize: theme.fontSizes[1],
|
|
763
|
+
fontWeight: 600,
|
|
764
|
+
fontFamily: theme.fonts.monospace,
|
|
765
|
+
letterSpacing: 0.4,
|
|
766
|
+
textTransform: 'uppercase',
|
|
767
|
+
color: theme.colors.textSecondary ?? muted,
|
|
768
|
+
}}
|
|
769
|
+
>
|
|
770
|
+
Overview
|
|
771
|
+
</h3>
|
|
772
|
+
<button
|
|
773
|
+
type="button"
|
|
774
|
+
aria-label="Close description"
|
|
775
|
+
title="Close"
|
|
776
|
+
onClick={() => setDescriptionOpen(false)}
|
|
777
|
+
style={{
|
|
778
|
+
display: 'inline-flex',
|
|
779
|
+
alignItems: 'center',
|
|
780
|
+
justifyContent: 'center',
|
|
781
|
+
flexShrink: 0,
|
|
782
|
+
width: 22,
|
|
783
|
+
height: 22,
|
|
784
|
+
padding: 0,
|
|
785
|
+
border: 'none',
|
|
786
|
+
borderRadius: 4,
|
|
787
|
+
background: 'transparent',
|
|
788
|
+
color: muted,
|
|
789
|
+
cursor: 'pointer',
|
|
790
|
+
}}
|
|
791
|
+
>
|
|
792
|
+
<X size={14} />
|
|
793
|
+
</button>
|
|
794
|
+
</div>
|
|
795
|
+
<IndustryMarkdownSlide
|
|
796
|
+
content={description}
|
|
797
|
+
slideIdPrefix="constructs-desc-overlay"
|
|
798
|
+
slideIndex={0}
|
|
799
|
+
isVisible
|
|
800
|
+
theme={theme}
|
|
801
|
+
disableScroll
|
|
802
|
+
disableBasePadding
|
|
803
|
+
enableKeyboardScrolling={false}
|
|
804
|
+
autoFocusOnVisible={false}
|
|
805
|
+
/>
|
|
806
|
+
</div>
|
|
807
|
+
</>
|
|
808
|
+
)}
|
|
702
809
|
{searchActive && (
|
|
703
810
|
<div style={{ flexShrink: 0, padding: '12px 16px 0' }}>
|
|
704
811
|
<input
|
|
@@ -772,12 +879,14 @@ export function ConstructsCatalog({
|
|
|
772
879
|
{splitByFileRun(group.items).map((run) => {
|
|
773
880
|
const lead = run[0]!;
|
|
774
881
|
const hasFile = !!lead.file;
|
|
775
|
-
|
|
776
|
-
//
|
|
777
|
-
|
|
882
|
+
const isExternal = lead.construct === 'external';
|
|
883
|
+
// File-backed runs and externals both get a header; externals
|
|
884
|
+
// name their kind instead of a path, so the body doesn't have
|
|
885
|
+
// to spell it out.
|
|
886
|
+
const showHeader = hasFile || isExternal;
|
|
778
887
|
const runKey = `${group.key}:${lead.file || lead.alias}`;
|
|
779
888
|
const runHasPurpose = run.some((c) => !!c.purpose?.trim());
|
|
780
|
-
const purposeOpen =
|
|
889
|
+
const purposeOpen = purposeOverrides[runKey] ?? isExternal;
|
|
781
890
|
const runFileOpen =
|
|
782
891
|
drawer != null && hasFile && lead.file === drawer.file;
|
|
783
892
|
return (
|
|
@@ -815,41 +924,64 @@ export function ConstructsCatalog({
|
|
|
815
924
|
fontSize: theme.fontSizes[0],
|
|
816
925
|
}}
|
|
817
926
|
>
|
|
818
|
-
|
|
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 }} />
|
|
927
|
+
{isExternal ? (
|
|
843
928
|
<span
|
|
844
929
|
style={{
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
930
|
+
display: 'flex',
|
|
931
|
+
alignItems: 'center',
|
|
932
|
+
gap: 6,
|
|
933
|
+
flex: 1,
|
|
934
|
+
minWidth: 0,
|
|
848
935
|
}}
|
|
849
936
|
>
|
|
850
|
-
{
|
|
937
|
+
<Box size={12} style={{ flexShrink: 0 }} />
|
|
938
|
+
<span
|
|
939
|
+
style={{
|
|
940
|
+
overflow: 'hidden',
|
|
941
|
+
textOverflow: 'ellipsis',
|
|
942
|
+
whiteSpace: 'nowrap',
|
|
943
|
+
}}
|
|
944
|
+
>
|
|
945
|
+
external
|
|
946
|
+
</span>
|
|
851
947
|
</span>
|
|
852
|
-
|
|
948
|
+
) : (
|
|
949
|
+
<button
|
|
950
|
+
type="button"
|
|
951
|
+
title={fileViewer ? `Open ${lead.file}` : lead.file}
|
|
952
|
+
onClick={
|
|
953
|
+
fileViewer
|
|
954
|
+
? () => onOpenFile(lead.file)
|
|
955
|
+
: undefined
|
|
956
|
+
}
|
|
957
|
+
style={{
|
|
958
|
+
display: 'flex',
|
|
959
|
+
alignItems: 'center',
|
|
960
|
+
gap: 6,
|
|
961
|
+
flex: 1,
|
|
962
|
+
minWidth: 0,
|
|
963
|
+
padding: 0,
|
|
964
|
+
border: 'none',
|
|
965
|
+
background: 'transparent',
|
|
966
|
+
color: 'inherit',
|
|
967
|
+
fontFamily: 'inherit',
|
|
968
|
+
fontSize: 'inherit',
|
|
969
|
+
textAlign: 'left',
|
|
970
|
+
cursor: fileViewer ? 'pointer' : 'default',
|
|
971
|
+
}}
|
|
972
|
+
>
|
|
973
|
+
<FileText size={12} style={{ flexShrink: 0 }} />
|
|
974
|
+
<span
|
|
975
|
+
style={{
|
|
976
|
+
overflow: 'hidden',
|
|
977
|
+
textOverflow: 'ellipsis',
|
|
978
|
+
whiteSpace: 'nowrap',
|
|
979
|
+
}}
|
|
980
|
+
>
|
|
981
|
+
{lead.file}
|
|
982
|
+
</span>
|
|
983
|
+
</button>
|
|
984
|
+
)}
|
|
853
985
|
{runHasPurpose && (
|
|
854
986
|
<button
|
|
855
987
|
type="button"
|
|
@@ -860,7 +992,7 @@ export function ConstructsCatalog({
|
|
|
860
992
|
purposeOpen ? 'Hide description' : 'Show description'
|
|
861
993
|
}
|
|
862
994
|
aria-expanded={purposeOpen}
|
|
863
|
-
onClick={() =>
|
|
995
|
+
onClick={() => setPurposeOpen(runKey, !purposeOpen)}
|
|
864
996
|
style={{
|
|
865
997
|
display: 'inline-flex',
|
|
866
998
|
alignItems: 'center',
|
|
@@ -898,6 +1030,9 @@ export function ConstructsCatalog({
|
|
|
898
1030
|
// render unnumbered. Without a header, keep the badge.
|
|
899
1031
|
const numbered =
|
|
900
1032
|
hasFile && c.declarationRef?.startLine != null;
|
|
1033
|
+
// The header names the external kind, so the body just
|
|
1034
|
+
// names the construct.
|
|
1035
|
+
const isExt = c.construct === 'external';
|
|
901
1036
|
return (
|
|
902
1037
|
<div
|
|
903
1038
|
key={c.alias}
|
|
@@ -933,6 +1068,7 @@ export function ConstructsCatalog({
|
|
|
933
1068
|
showRepoIdentity={false}
|
|
934
1069
|
fileBadgeChrome={showHeader ? false : !!fileViewer}
|
|
935
1070
|
hideActions={showHeader}
|
|
1071
|
+
externalName={isExt}
|
|
936
1072
|
showPurpose={showHeader ? purposeOpen : undefined}
|
|
937
1073
|
lineNumbers={numbered}
|
|
938
1074
|
verification={
|
|
@@ -259,6 +259,12 @@ export interface SubsystemComponentGraphProps {
|
|
|
259
259
|
showWalkthroughTitle?: boolean;
|
|
260
260
|
/** Markdown description rendered in the sidebar. */
|
|
261
261
|
description?: string;
|
|
262
|
+
/**
|
|
263
|
+
* Controlled open state for the constructs catalog's description overlay.
|
|
264
|
+
* When provided, the host owns it (e.g. a button in its own header).
|
|
265
|
+
*/
|
|
266
|
+
descriptionOpen?: boolean;
|
|
267
|
+
onDescriptionOpenChange?: (open: boolean) => void;
|
|
262
268
|
/** Rendered over the graph canvas only (not the title/legend sidebar). */
|
|
263
269
|
canvasOverlay?: ReactNode;
|
|
264
270
|
/** Extra controls at the top of the title/legend sidebar. */
|
|
@@ -2543,6 +2549,8 @@ export function SubsystemComponentGraph(props: SubsystemComponentGraphProps) {
|
|
|
2543
2549
|
title={props.title}
|
|
2544
2550
|
hideSidebar={props.hideSidebar}
|
|
2545
2551
|
description={props.description}
|
|
2552
|
+
descriptionOpen={props.descriptionOpen}
|
|
2553
|
+
onDescriptionOpenChange={props.onDescriptionOpenChange}
|
|
2546
2554
|
diagnostic={props.diagnostic}
|
|
2547
2555
|
sidebarExtra={props.sidebarExtra}
|
|
2548
2556
|
sidebarAfterDescription={props.sidebarAfterDescription}
|