@principal-ai/subsystems-react 0.37.0 → 0.37.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.
Files changed (40) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +1 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/subsystem/ComponentDeclaration.d.ts +16 -1
  6. package/dist/subsystem/ComponentDeclaration.d.ts.map +1 -1
  7. package/dist/subsystem/ComponentDeclaration.js +75 -7
  8. package/dist/subsystem/ComponentDeclaration.js.map +1 -1
  9. package/dist/subsystem/ConstructsCatalog.d.ts +13 -3
  10. package/dist/subsystem/ConstructsCatalog.d.ts.map +1 -1
  11. package/dist/subsystem/ConstructsCatalog.js +273 -220
  12. package/dist/subsystem/ConstructsCatalog.js.map +1 -1
  13. package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
  14. package/dist/subsystem/SubsystemComponentGraph.js +36 -28
  15. package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
  16. package/dist/subsystem/SymbolInspectionCard.d.ts.map +1 -1
  17. package/dist/subsystem/SymbolInspectionCard.js +1 -1
  18. package/dist/subsystem/SymbolInspectionCard.js.map +1 -1
  19. package/dist/subsystem/model.d.ts.map +1 -1
  20. package/dist/subsystem/model.js +7 -1
  21. package/dist/subsystem/model.js.map +1 -1
  22. package/dist/utils/edgeLabel.d.ts +51 -0
  23. package/dist/utils/edgeLabel.d.ts.map +1 -0
  24. package/dist/utils/edgeLabel.js +81 -0
  25. package/dist/utils/edgeLabel.js.map +1 -0
  26. package/dist/utils/elkLayout.d.ts +6 -0
  27. package/dist/utils/elkLayout.d.ts.map +1 -1
  28. package/dist/utils/elkLayout.js +27 -6
  29. package/dist/utils/elkLayout.js.map +1 -1
  30. package/package.json +1 -1
  31. package/src/index.ts +7 -0
  32. package/src/stories/Subsystem/ConstructsCatalog.stories.tsx +457 -0
  33. package/src/subsystem/ComponentDeclaration.tsx +130 -7
  34. package/src/subsystem/ConstructsCatalog.tsx +431 -341
  35. package/src/subsystem/SubsystemComponentGraph.tsx +74 -29
  36. package/src/subsystem/SymbolInspectionCard.tsx +10 -9
  37. package/src/subsystem/model.ts +7 -1
  38. package/src/utils/edgeLabel.test.ts +23 -0
  39. package/src/utils/edgeLabel.ts +95 -0
  40. package/src/utils/elkLayout.ts +41 -6
@@ -6,7 +6,7 @@
6
6
  * comments. File content lives in the bottom FileDrawer, not here.
7
7
  */
8
8
 
9
- import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
9
+ import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties, type ReactNode } from 'react';
10
10
  import { createPortal } from 'react-dom';
11
11
  import { AlignLeft, FileText } from 'lucide-react';
12
12
  import { useTheme } from '@principal-ade/industry-theme';
@@ -138,6 +138,10 @@ function DetailLink({ children, onClick }: { children: ReactNode; onClick?: () =
138
138
  );
139
139
  }
140
140
 
141
+ /** Width of the `lineNumbers` gutter column and the gap after it. */
142
+ const GUTTER_NUMBER_WIDTH = 40;
143
+ const GUTTER_NUMBER_GAP = 8;
144
+
141
145
  /** Panel props. Callbacks are wired by the graph; standalone usage without
142
146
  * them renders the same panel with nothing clickable. */
143
147
  export interface ComponentDeclarationProps {
@@ -174,6 +178,21 @@ export interface ComponentDeclarationProps {
174
178
  * the file badge: drop the top-right `L#` (redundant) and put the
175
179
  * description toggle in the badge after the line segment. */
176
180
  fileBadgeChrome?: boolean;
181
+ /** Suppress the per-declaration action chrome entirely (file toggle, line
182
+ * label, description toggle). For hosts that surface those in a shared
183
+ * header instead. */
184
+ hideActions?: boolean;
185
+ /** For `external` constructs, render the construct name as the body instead
186
+ * of `external '…'` — the kind is named by the caller's header. */
187
+ externalName?: boolean;
188
+ /** Controlled purpose/description visibility. When provided it overrides the
189
+ * internal toggle, so a host can drive it from a shared header. */
190
+ showPurpose?: boolean;
191
+ /** Render a gutter beside the declaration body and label the construct's own
192
+ * line with its known start line — only that line is accurate, so the rest
193
+ * of the gutter stays blank and aligned. The label opens the file at that
194
+ * line when `onOpenFile` is set. */
195
+ lineNumbers?: boolean;
177
196
  }
178
197
 
179
198
  function verificationSummary(
@@ -318,6 +337,10 @@ export function ComponentDeclaration({
318
337
  declarationOpen = false,
319
338
  showRepoIdentity = true,
320
339
  fileBadgeChrome = false,
340
+ hideActions = false,
341
+ externalName = false,
342
+ showPurpose: showPurposeProp,
343
+ lineNumbers = false,
321
344
  }: ComponentDeclarationProps) {
322
345
  const { theme, mode } = useTheme();
323
346
  const pierreSyntaxTheme = resolvePierreSyntaxThemeName(mode);
@@ -325,7 +348,8 @@ export function ComponentDeclaration({
325
348
  'path' | 'line' | 'desc' | 'action' | null
326
349
  >(null);
327
350
  const [showFile, setShowFile] = useState(defaultShowFile);
328
- const [showPurpose, setShowPurpose] = useState(false);
351
+ const [showPurposeState, setShowPurposeState] = useState(false);
352
+ const showPurpose = showPurposeProp ?? showPurposeState;
329
353
  const muted = theme.colors.textMuted ?? theme.colors.textSecondary;
330
354
  const color = componentColor(component, pierreSyntaxTheme);
331
355
  const inlineChrome = fileBadgeChrome && showFile && !!component.file;
@@ -399,6 +423,13 @@ export function ComponentDeclaration({
399
423
  parseSourceLocation(verification?.anchor?.source_location) ??
400
424
  undefined;
401
425
 
426
+ const externalKind =
427
+ (component.declaration?.kind ?? component.construct) === 'external';
428
+ const externalNameText =
429
+ component.name?.trim() ||
430
+ (component.declaration?.kind === 'external' ? component.declaration.label : '') ||
431
+ component.alias;
432
+
402
433
  const openDeclarationFile = () => {
403
434
  if (!onOpenFile || !component.file) return;
404
435
  onOpenFile(
@@ -497,7 +528,7 @@ export function ComponentDeclaration({
497
528
  {!inlineChrome && lineLocationLabel}
498
529
  {!inlineChrome &&
499
530
  component.purpose?.trim() &&
500
- toggleBtn(showPurpose, () => setShowPurpose((v) => !v), 'Toggle description', AlignLeft)}
531
+ toggleBtn(showPurpose, () => setShowPurposeState((v) => !v), 'Toggle description', AlignLeft)}
501
532
  </span>
502
533
  );
503
534
 
@@ -560,7 +591,7 @@ export function ComponentDeclaration({
560
591
  'purl',
561
592
  ),
562
593
  );
563
- } else if (!showRepoIdentity && !inlineChrome) {
594
+ } else if (!showRepoIdentity && !inlineChrome && !hideActions) {
564
595
  lines.push(line(headerActions, 'actions'));
565
596
  }
566
597
 
@@ -695,7 +726,7 @@ export function ComponentDeclaration({
695
726
  aria-label={showPurpose ? 'Hide description' : 'Show description'}
696
727
  onClick={(e) => {
697
728
  e.stopPropagation();
698
- setShowPurpose((v) => !v);
729
+ setShowPurposeState((v) => !v);
699
730
  }}
700
731
  {...segHover('desc')}
701
732
  style={{
@@ -888,14 +919,17 @@ export function ComponentDeclaration({
888
919
  );
889
920
 
890
921
  const declLines: ReactNode[][] = [[]];
922
+ const declLineText: string[] = [''];
891
923
  let di = 0;
892
924
  let offset = 0;
893
925
  for (const tok of tokens) {
894
926
  if (tok.kind === 'newline') {
895
927
  declLines.push([]);
928
+ declLineText.push('');
896
929
  di++;
897
930
  continue;
898
931
  }
932
+ declLineText[di] += tok.text;
899
933
  const color = tok.color ?? tokenColor[tok.kind] ?? declarationTextColor;
900
934
  let i = 0;
901
935
  while (i < tok.text.length) {
@@ -956,8 +990,97 @@ export function ComponentDeclaration({
956
990
  }
957
991
  offset += tok.text.length;
958
992
  }
993
+ // The rendered line carrying the construct's own name gets the known line
994
+ // number — a method stub wraps it in `class Host {`, so the number belongs on
995
+ // the member line, not the synthetic first row. Falls back to the first line.
996
+ const constructName =
997
+ component.name || component.symbol?.split('.').pop() || '';
998
+ let labelDi = -1;
999
+ if (constructName) {
1000
+ for (let li = 0; li < declLines.length; li++) {
1001
+ if (declLines[li].length > 0 && declLineText[li].includes(constructName)) {
1002
+ labelDi = li;
1003
+ break;
1004
+ }
1005
+ }
1006
+ }
1007
+ if (labelDi === -1) {
1008
+ labelDi = declLines.findIndex((l) => l.length > 0);
1009
+ }
1010
+ const externalNameLine = externalName && externalKind;
1011
+ if (externalNameLine) {
1012
+ // The caller's header names the kind, so the body just names the construct.
1013
+ lines.push(
1014
+ <div
1015
+ key="external-name"
1016
+ style={{
1017
+ display: 'block',
1018
+ whiteSpace: 'pre-wrap',
1019
+ overflowWrap: 'anywhere',
1020
+ minWidth: 0,
1021
+ minHeight: 18,
1022
+ }}
1023
+ >
1024
+ <span style={{ color }}>{externalNameText}</span>
1025
+ </div>,
1026
+ );
1027
+ }
959
1028
  for (let li = 0; li < declLines.length; li++) {
1029
+ if (externalNameLine) break;
960
1030
  if (declLines[li].length === 0) continue;
1031
+ if (lineNumbers && declarationStartLine != null) {
1032
+ // Only the construct's own line is known; the rest of the gutter stays
1033
+ // blank (but aligned) since later rendered lines needn't map to source.
1034
+ const n = li === labelDi ? declarationStartLine : null;
1035
+ const interactive = n != null && !!onOpenFile && !!component.file;
1036
+ const gutterStyle: CSSProperties = {
1037
+ flexShrink: 0,
1038
+ width: GUTTER_NUMBER_WIDTH,
1039
+ paddingRight: GUTTER_NUMBER_GAP,
1040
+ border: 'none',
1041
+ background: 'transparent',
1042
+ textAlign: 'right',
1043
+ color: muted,
1044
+ fontFamily: 'inherit',
1045
+ fontSize: 'inherit',
1046
+ lineHeight: 'inherit',
1047
+ cursor: interactive ? 'pointer' : 'default',
1048
+ userSelect: 'none',
1049
+ };
1050
+ lines.push(
1051
+ <div
1052
+ key={`decl-${li}`}
1053
+ style={{ display: 'flex', alignItems: 'flex-start', minWidth: 0 }}
1054
+ >
1055
+ {n == null ? (
1056
+ <span style={gutterStyle} aria-hidden="true" />
1057
+ ) : interactive ? (
1058
+ <button
1059
+ type="button"
1060
+ title={`Open line ${n}`}
1061
+ onClick={() => onOpenFile!(component.file, { startLine: n })}
1062
+ style={gutterStyle}
1063
+ >
1064
+ {n}
1065
+ </button>
1066
+ ) : (
1067
+ <span style={gutterStyle}>{n}</span>
1068
+ )}
1069
+ <div
1070
+ style={{
1071
+ flex: 1,
1072
+ minWidth: 0,
1073
+ whiteSpace: 'pre-wrap',
1074
+ overflowWrap: 'anywhere',
1075
+ minHeight: 18,
1076
+ }}
1077
+ >
1078
+ {declLines[li]}
1079
+ </div>
1080
+ </div>,
1081
+ );
1082
+ continue;
1083
+ }
961
1084
  lines.push(
962
1085
  <div
963
1086
  key={`decl-${li}`}
@@ -986,7 +1109,7 @@ export function ComponentDeclaration({
986
1109
  {purposeLine.trim()}
987
1110
  </span>,
988
1111
  `purpose${i}`,
989
- false,
1112
+ lineNumbers ? GUTTER_NUMBER_WIDTH + GUTTER_NUMBER_GAP : false,
990
1113
  'normal',
991
1114
  ),
992
1115
  ),
@@ -1015,7 +1138,7 @@ export function ComponentDeclaration({
1015
1138
  minWidth: 0,
1016
1139
  boxSizing: 'border-box',
1017
1140
  background: theme.colors.backgroundSecondary,
1018
- padding: '10px 12px',
1141
+ padding: lineNumbers ? '10px 12px 10px 0' : '10px 12px',
1019
1142
  fontFamily: theme.fonts.monospace,
1020
1143
  fontSize: theme.fontSizes[1],
1021
1144
  lineHeight: 1.7,