@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.
@@ -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. File opens still use the bottom
10
- * FileDrawer when the host injects a viewer.
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 { AlignLeft, ChevronDown, ChevronRight, FileText, Folder } from 'lucide-react';
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
- // Files whose shared-header description toggle is expanded (keyed by file run).
208
- const [openPurposes, setOpenPurposes] = useState<Set<string>>(new Set());
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 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
- });
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
- !!(title || description || diagnostic || sidebarExtra || sidebarAfterDescription);
546
- const showDesc = !!description && descriptionVisible;
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={descriptionVisible}
645
+ aria-expanded={descriptionOpen}
615
646
  aria-label={
616
- descriptionVisible ? 'Hide description' : 'Show description'
647
+ descriptionOpen ? 'Hide description' : 'Show description'
617
648
  }
618
649
  title={
619
- descriptionVisible ? 'Hide description' : 'Show description'
650
+ descriptionOpen ? 'Hide description' : 'Show description'
620
651
  }
621
652
  onMouseEnter={() => setDescToggleHover(true)}
622
653
  onMouseLeave={() => setDescToggleHover(false)}
623
- onClick={() => setDescriptionVisible((v) => !v)}
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: descriptionVisible || descToggleHover ? theme.colors.border : 'transparent',
635
- color: descriptionVisible || descToggleHover ? theme.colors.text : muted,
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
- // Every file-backed run gets the combined header, one
776
- // declaration or many, so the style stays consistent.
777
- const showHeader = hasFile;
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 = openPurposes.has(runKey);
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
- <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 }} />
927
+ {isExternal ? (
843
928
  <span
844
929
  style={{
845
- overflow: 'hidden',
846
- textOverflow: 'ellipsis',
847
- whiteSpace: 'nowrap',
930
+ display: 'flex',
931
+ alignItems: 'center',
932
+ gap: 6,
933
+ flex: 1,
934
+ minWidth: 0,
848
935
  }}
849
936
  >
850
- {lead.file}
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
- </button>
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={() => togglePurpose(runKey)}
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}