@principal-ai/subsystems-react 0.49.0 → 0.51.0

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 (97) hide show
  1. package/dist/index.d.ts +8 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +5 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/pierre/PierreFileView.d.ts +1 -1
  6. package/dist/pierre/PierreFileView.d.ts.map +1 -1
  7. package/dist/pierre/PierreFileView.js +2 -1
  8. package/dist/pierre/PierreFileView.js.map +1 -1
  9. package/dist/pierre/PierreSnippetView.d.ts +1 -1
  10. package/dist/pierre/PierreSnippetView.d.ts.map +1 -1
  11. package/dist/pierre/PierreSnippetView.js +2 -1
  12. package/dist/pierre/PierreSnippetView.js.map +1 -1
  13. package/dist/pierre/PierreTrailCodeView.d.ts +7 -1
  14. package/dist/pierre/PierreTrailCodeView.d.ts.map +1 -1
  15. package/dist/pierre/PierreTrailCodeView.js +71 -18
  16. package/dist/pierre/PierreTrailCodeView.js.map +1 -1
  17. package/dist/pierre/pierreBackground.d.ts +36 -2
  18. package/dist/pierre/pierreBackground.d.ts.map +1 -1
  19. package/dist/pierre/pierreBackground.js +40 -13
  20. package/dist/pierre/pierreBackground.js.map +1 -1
  21. package/dist/pierre/visibleStep.d.ts +48 -0
  22. package/dist/pierre/visibleStep.d.ts.map +1 -0
  23. package/dist/pierre/visibleStep.js +53 -0
  24. package/dist/pierre/visibleStep.js.map +1 -0
  25. package/dist/stories/Subsystem/C4Graph/c4Associations.fixture.d.ts +95 -0
  26. package/dist/stories/Subsystem/C4Graph/c4Associations.fixture.d.ts.map +1 -0
  27. package/dist/stories/Subsystem/C4Graph/c4Associations.fixture.js +340 -0
  28. package/dist/stories/Subsystem/C4Graph/c4Associations.fixture.js.map +1 -0
  29. package/dist/stories/Subsystem/C4Graph/c4Fixture.d.ts +4 -0
  30. package/dist/stories/Subsystem/C4Graph/c4Fixture.d.ts.map +1 -1
  31. package/dist/stories/Subsystem/C4Graph/c4Fixture.js +7334 -1
  32. package/dist/stories/Subsystem/C4Graph/c4Fixture.js.map +1 -1
  33. package/dist/stories/Subsystem/C4Graph/generateFixture.d.ts +2 -0
  34. package/dist/stories/Subsystem/C4Graph/generateFixture.d.ts.map +1 -0
  35. package/dist/stories/Subsystem/C4Graph/generateFixture.js +145 -0
  36. package/dist/stories/Subsystem/C4Graph/generateFixture.js.map +1 -0
  37. package/dist/subsystem/C4Graph.d.ts.map +1 -1
  38. package/dist/subsystem/C4Graph.js +16 -60
  39. package/dist/subsystem/C4Graph.js.map +1 -1
  40. package/dist/subsystem/C4NodeCard.d.ts +84 -0
  41. package/dist/subsystem/C4NodeCard.d.ts.map +1 -0
  42. package/dist/subsystem/C4NodeCard.js +229 -0
  43. package/dist/subsystem/C4NodeCard.js.map +1 -0
  44. package/dist/subsystem/DiagnosticToggle.d.ts +8 -1
  45. package/dist/subsystem/DiagnosticToggle.d.ts.map +1 -1
  46. package/dist/subsystem/DiagnosticToggle.js +40 -6
  47. package/dist/subsystem/DiagnosticToggle.js.map +1 -1
  48. package/dist/subsystem/FileDrawer.d.ts +10 -1
  49. package/dist/subsystem/FileDrawer.d.ts.map +1 -1
  50. package/dist/subsystem/FileDrawer.js +61 -40
  51. package/dist/subsystem/FileDrawer.js.map +1 -1
  52. package/dist/subsystem/SubsystemComponentGraph.d.ts +6 -0
  53. package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
  54. package/dist/subsystem/SubsystemComponentGraph.js +21 -10
  55. package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
  56. package/dist/subsystem/c4Associations.d.ts +186 -0
  57. package/dist/subsystem/c4Associations.d.ts.map +1 -0
  58. package/dist/subsystem/c4Associations.js +284 -0
  59. package/dist/subsystem/c4Associations.js.map +1 -0
  60. package/dist/subsystem/model.d.ts +25 -1
  61. package/dist/subsystem/model.d.ts.map +1 -1
  62. package/dist/subsystem/model.js +15 -0
  63. package/dist/subsystem/model.js.map +1 -1
  64. package/dist/subsystem/nodes.d.ts.map +1 -1
  65. package/dist/subsystem/nodes.js +78 -7
  66. package/dist/subsystem/nodes.js.map +1 -1
  67. package/dist/subsystem/toC4.d.ts +119 -0
  68. package/dist/subsystem/toC4.d.ts.map +1 -1
  69. package/dist/subsystem/toC4.js +95 -5
  70. package/dist/subsystem/toC4.js.map +1 -1
  71. package/package.json +2 -2
  72. package/src/index.ts +39 -0
  73. package/src/pierre/PierreFileView.tsx +2 -1
  74. package/src/pierre/PierreSnippetView.tsx +2 -1
  75. package/src/pierre/PierreTrailCodeView.tsx +81 -22
  76. package/src/pierre/pierreBackground.ts +62 -12
  77. package/src/pierre/visibleStep.test.ts +108 -0
  78. package/src/pierre/visibleStep.ts +76 -0
  79. package/src/stories/Subsystem/C4Graph/C4Associations.stories.tsx +273 -0
  80. package/src/stories/Subsystem/C4Graph/C4NodeGallery.stories.tsx +325 -0
  81. package/src/stories/Subsystem/C4Graph/c4Associations.fixture.ts +344 -0
  82. package/src/stories/Subsystem/C4Graph/c4Fixture.ts +7337 -4
  83. package/src/stories/Subsystem/C4Graph/generateFixture.ts +161 -0
  84. package/src/stories/Subsystem/ComponentGraph/Deprecated.stories.tsx +358 -0
  85. package/src/stories/Subsystem/ComponentGraph/Diagnostics.stories.tsx +27 -2
  86. package/src/stories/Subsystem/ComponentGraph/{Flows.stories.tsx → Trails.stories.tsx} +10 -4
  87. package/src/subsystem/C4Graph.tsx +51 -92
  88. package/src/subsystem/C4NodeCard.test.ts +196 -0
  89. package/src/subsystem/C4NodeCard.tsx +307 -0
  90. package/src/subsystem/DiagnosticToggle.tsx +58 -4
  91. package/src/subsystem/FileDrawer.tsx +112 -63
  92. package/src/subsystem/SubsystemComponentGraph.tsx +29 -5
  93. package/src/subsystem/c4Associations.test.ts +484 -0
  94. package/src/subsystem/c4Associations.ts +398 -0
  95. package/src/subsystem/model.ts +34 -1
  96. package/src/subsystem/nodes.tsx +124 -5
  97. package/src/subsystem/toC4.ts +210 -5
@@ -28,6 +28,13 @@ export interface SubsystemDiagnostic {
28
28
  status: SubsystemDiagnosticStatus;
29
29
  /** Count shown beside the icon (e.g. error + warn findings). Hidden when 0/undefined. */
30
30
  issueCount?: number;
31
+ /**
32
+ * How many of the open findings carry a one-click deterministic fix (a
33
+ * re-pin, an adoptable signature fill, a unique Graphify file relocate).
34
+ * Badged on the icon so it's visible without expanding the list — the fixes
35
+ * live inside collapsed categories, so the count was previously invisible.
36
+ */
37
+ fixableCount?: number;
31
38
  /** Inputs changed since the report was produced. */
32
39
  stale?: boolean;
33
40
  /** A verification pass is running right now. */
@@ -55,7 +62,7 @@ export function diagnosticStatusColor(
55
62
  /** Human tooltip for the current diagnostic state. */
56
63
  export function diagnosticTitle(diagnostic: SubsystemDiagnostic): string {
57
64
  if (diagnostic.title) return diagnostic.title;
58
- const { status, issueCount, stale, busy } = diagnostic;
65
+ const { status, issueCount, fixableCount, stale, busy } = diagnostic;
59
66
  let head: string;
60
67
  if (status === 'ok') head = 'Diagnostics: no issues';
61
68
  else if (status === 'issues')
@@ -69,9 +76,15 @@ export function diagnosticTitle(diagnostic: SubsystemDiagnostic): string {
69
76
  ? `Diagnostics: ${issueCount} unconfirmed`
70
77
  : 'Diagnostics: unconfirmed claims';
71
78
  else head = 'Diagnostics: not checked yet';
79
+ // Name the one-click count first when there is work a click can close —
80
+ // that's the actionable part of the list.
81
+ const fixable =
82
+ fixableCount != null && fixableCount > 0
83
+ ? `${fixableCount} fixable with one click`
84
+ : '';
72
85
  if (busy) return `${head} — checking…`;
73
- if (stale) return `${head} (stale) — toggle the list`;
74
- return `${head} — toggle the list`;
86
+ if (stale) return `${head} (stale) — toggle the list${fixable ? `, ${fixable}` : ''}`;
87
+ return `${head} — toggle the list${fixable ? `, ${fixable}` : ''}`;
75
88
  }
76
89
 
77
90
  export interface SubsystemDiagnosticToggleProps extends SubsystemDiagnostic {
@@ -82,6 +95,7 @@ export interface SubsystemDiagnosticToggleProps extends SubsystemDiagnostic {
82
95
  export function SubsystemDiagnosticToggle({
83
96
  status,
84
97
  issueCount,
98
+ fixableCount = 0,
85
99
  stale = false,
86
100
  busy = false,
87
101
  active = false,
@@ -93,8 +107,20 @@ export function SubsystemDiagnosticToggle({
93
107
  const [hover, setHover] = useState(false);
94
108
  const muted = theme.colors.textMuted ?? theme.colors.textSecondary;
95
109
  const color = diagnosticStatusColor(status, theme.colors, muted);
96
- const label = diagnosticTitle({ status, issueCount, stale, busy, active, ...rest });
110
+ const label = diagnosticTitle({
111
+ status,
112
+ issueCount,
113
+ fixableCount,
114
+ stale,
115
+ busy,
116
+ active,
117
+ ...rest,
118
+ });
97
119
  const showCount = issueCount != null && issueCount > 0;
120
+ // Green regardless of status: this marks "a click fixes this", not severity,
121
+ // and green is the same accent the per-finding Apply button uses.
122
+ const showFixable = fixableCount > 0 && !busy;
123
+ const fixableColor = theme.colors.success ?? '#2da44e';
98
124
  const background = active
99
125
  ? hexWithAlpha(color, hover ? 0.24 : 0.16)
100
126
  : hover
@@ -151,6 +177,34 @@ export function SubsystemDiagnosticToggle({
151
177
  }}
152
178
  />
153
179
  )}
180
+ {/* Fixable badge — superscript count on the icon's trailing edge.
181
+ Offset up-and-out from the `stale` dot so both read at once;
182
+ the badge is the call to action, the dot only a freshness note. */}
183
+ {showFixable && (
184
+ <span
185
+ aria-hidden
186
+ style={{
187
+ position: 'absolute',
188
+ top: -5,
189
+ right: -8,
190
+ minWidth: 12,
191
+ height: 12,
192
+ padding: '0 3px',
193
+ borderRadius: 6,
194
+ display: 'inline-flex',
195
+ alignItems: 'center',
196
+ justifyContent: 'center',
197
+ background: fixableColor,
198
+ color: theme.colors.background,
199
+ fontFamily: theme.fonts.monospace,
200
+ fontSize: 9,
201
+ fontWeight: 700,
202
+ lineHeight: 1,
203
+ }}
204
+ >
205
+ {fixableCount > 99 ? '99+' : fixableCount}
206
+ </span>
207
+ )}
154
208
  </span>
155
209
  {showCount && (
156
210
  <span
@@ -25,6 +25,7 @@ export function FileDrawer({
25
25
  fillHeight = false,
26
26
  suppressEscape = false,
27
27
  hidden = false,
28
+ progress = null,
28
29
  children,
29
30
  }: {
30
31
  /** Drawer chrome title; `null` closes the drawer. */
@@ -36,11 +37,18 @@ export function FileDrawer({
36
37
  suppressEscape?: boolean;
37
38
  /** Suppress the drawer entirely — no spacer, no panel (embeds). */
38
39
  hidden?: boolean;
40
+ /**
41
+ * Segmented progress readout for the drawer content, e.g. trail step 2 of 5.
42
+ * Rendered as one tick per step, filled through the current one. Omit for
43
+ * content with no position (a single file).
44
+ */
45
+ progress?: { index: number; total: number } | null;
39
46
  children?: ReactNode;
40
47
  }) {
41
48
  const { theme } = useTheme();
42
49
  const muted = theme.colors.textMuted ?? theme.colors.textSecondary;
43
50
  const open = title !== null && !hidden;
51
+ const showProgress = progress != null && progress.total > 1;
44
52
  const [closeHover, setCloseHover] = useState(false);
45
53
  const [maxHover, setMaxHover] = useState(false);
46
54
  // Full-height mode: the maximize button or double-clicking the header.
@@ -103,78 +111,119 @@ export function FileDrawer({
103
111
  onDoubleClick={() => setMaximized((v) => !v)}
104
112
  style={{
105
113
  display: "flex",
106
- alignItems: "center",
107
- gap: 8,
108
- padding: "6px 10px",
109
- borderBottom: `1px solid ${theme.colors.border}`,
114
+ flexDirection: "column",
110
115
  flexShrink: 0,
111
116
  cursor: "default",
112
117
  userSelect: "none",
113
118
  }}
114
119
  >
115
- <span
116
- title={title ?? undefined}
120
+ <div
117
121
  style={{
118
- flex: 1,
119
- minWidth: 0,
120
- overflow: "hidden",
121
- textOverflow: "ellipsis",
122
- whiteSpace: "nowrap",
123
- fontFamily: theme.fonts.monospace,
124
- fontSize: theme.fontSizes[0],
125
- color: muted,
126
- }}
127
- >
128
- {title}
129
- </span>
130
- <button
131
- type="button"
132
- onClick={() => setMaximized((v) => !v)}
133
- onMouseEnter={() => setMaxHover(true)}
134
- onMouseLeave={() => setMaxHover(false)}
135
- aria-label={maximized ? "Minimize code view" : "Maximize code view"}
136
- aria-pressed={maximized}
137
- title={maximized ? "Minimize code view" : "Maximize code view"}
138
- style={{
139
- display: "inline-flex",
140
- alignItems: "center",
141
- justifyContent: "center",
142
- width: 22,
143
- height: 22,
144
- padding: 0,
145
- border: "none",
146
- borderRadius: 4,
147
- background: maxHover ? theme.colors.border : "transparent",
148
- color: maxHover ? theme.colors.text : muted,
149
- cursor: "pointer",
150
- transition: "background 120ms ease, color 120ms ease",
151
- }}
152
- >
153
- {maximized ? <Minimize2 size={14} /> : <Maximize2 size={14} />}
154
- </button>
155
- <button
156
- type="button"
157
- onClick={onClose}
158
- onMouseEnter={() => setCloseHover(true)}
159
- onMouseLeave={() => setCloseHover(false)}
160
- aria-label="Close file"
161
- style={{
162
- display: "inline-flex",
122
+ display: "flex",
163
123
  alignItems: "center",
164
- justifyContent: "center",
165
- width: 22,
166
- height: 22,
167
- padding: 0,
168
- border: "none",
169
- borderRadius: 4,
170
- background: closeHover ? theme.colors.border : "transparent",
171
- color: closeHover ? theme.colors.text : muted,
172
- cursor: "pointer",
173
- transition: "background 120ms ease, color 120ms ease",
124
+ gap: 8,
125
+ padding: "6px 10px",
126
+ // With segments present they form the header's bottom edge, so
127
+ // only draw the hairline when there is nothing to stand in for it.
128
+ borderBottom: showProgress
129
+ ? "none"
130
+ : `1px solid ${theme.colors.border}`,
174
131
  }}
175
132
  >
176
- <X size={14} />
177
- </button>
133
+ <span
134
+ title={title ?? undefined}
135
+ style={{
136
+ flex: 1,
137
+ minWidth: 0,
138
+ overflow: "hidden",
139
+ textOverflow: "ellipsis",
140
+ whiteSpace: "nowrap",
141
+ fontFamily: theme.fonts.monospace,
142
+ fontSize: theme.fontSizes[1],
143
+ fontWeight: 600,
144
+ color: theme.colors.text,
145
+ }}
146
+ >
147
+ {title}
148
+ </span>
149
+ <button
150
+ type="button"
151
+ onClick={() => setMaximized((v) => !v)}
152
+ onMouseEnter={() => setMaxHover(true)}
153
+ onMouseLeave={() => setMaxHover(false)}
154
+ aria-label={
155
+ maximized ? "Minimize code view" : "Maximize code view"
156
+ }
157
+ aria-pressed={maximized}
158
+ title={maximized ? "Minimize code view" : "Maximize code view"}
159
+ style={{
160
+ display: "inline-flex",
161
+ alignItems: "center",
162
+ justifyContent: "center",
163
+ width: 22,
164
+ height: 22,
165
+ padding: 0,
166
+ border: "none",
167
+ borderRadius: 4,
168
+ background: maxHover ? theme.colors.border : "transparent",
169
+ color: maxHover ? theme.colors.text : muted,
170
+ cursor: "pointer",
171
+ transition: "background 120ms ease, color 120ms ease",
172
+ }}
173
+ >
174
+ {maximized ? <Minimize2 size={14} /> : <Maximize2 size={14} />}
175
+ </button>
176
+ <button
177
+ type="button"
178
+ onClick={onClose}
179
+ onMouseEnter={() => setCloseHover(true)}
180
+ onMouseLeave={() => setCloseHover(false)}
181
+ aria-label="Close file"
182
+ style={{
183
+ display: "inline-flex",
184
+ alignItems: "center",
185
+ justifyContent: "center",
186
+ width: 22,
187
+ height: 22,
188
+ padding: 0,
189
+ border: "none",
190
+ borderRadius: 4,
191
+ background: closeHover ? theme.colors.border : "transparent",
192
+ color: closeHover ? theme.colors.text : muted,
193
+ cursor: "pointer",
194
+ transition: "background 120ms ease, color 120ms ease",
195
+ }}
196
+ >
197
+ <X size={14} />
198
+ </button>
199
+ </div>
200
+ {showProgress && progress != null && (
201
+ <div
202
+ role="img"
203
+ aria-label={`Step ${progress.index + 1} of ${progress.total}`}
204
+ style={{
205
+ display: "flex",
206
+ gap: 2,
207
+ flexShrink: 0,
208
+ padding: "0 10px 6px",
209
+ }}
210
+ >
211
+ {Array.from({ length: progress.total }, (_, i) => (
212
+ <span
213
+ key={i}
214
+ style={{
215
+ flex: 1,
216
+ height: 2,
217
+ borderRadius: 1,
218
+ background:
219
+ i <= progress.index
220
+ ? theme.colors.primary
221
+ : theme.colors.border,
222
+ }}
223
+ />
224
+ ))}
225
+ </div>
226
+ )}
178
227
  </div>
179
228
  <div style={{ flex: 1, minHeight: 0, overflow: "auto" }}>
180
229
  {children}
@@ -114,6 +114,12 @@ export interface TrailViewerContext {
114
114
  resolveSymbol?: (query: TrailSymbolQuery) => string | null;
115
115
  /** A clicked construct token — open that construct's declaration line. */
116
116
  onSymbolClick?: (symbol: string, query: TrailSymbolQuery) => void;
117
+ /**
118
+ * Report which step scrolling has brought to the top of the code view, so the
119
+ * drawer can track the viewport instead of only the selected step. Forward
120
+ * the code view's scroll callback here.
121
+ */
122
+ onVisibleStepChange?: (index: number) => void;
117
123
  }
118
124
 
119
125
  /** Identifiers a token could match to name this component as a construct. */
@@ -491,6 +497,7 @@ const TrailDrawerContent = memo(function TrailDrawerContent({
491
497
  proposedAliases,
492
498
  resolveSymbol,
493
499
  onSymbolClick,
500
+ onVisibleStepChange,
494
501
  }: {
495
502
  render: (ctx: TrailViewerContext) => ReactNode;
496
503
  trail: SubsystemTrail;
@@ -499,6 +506,7 @@ const TrailDrawerContent = memo(function TrailDrawerContent({
499
506
  proposedAliases: ReadonlySet<string>;
500
507
  resolveSymbol?: (query: TrailSymbolQuery) => string | null;
501
508
  onSymbolClick?: (symbol: string, query: TrailSymbolQuery) => void;
509
+ onVisibleStepChange?: (index: number) => void;
502
510
  }) {
503
511
  return (
504
512
  <>
@@ -509,6 +517,7 @@ const TrailDrawerContent = memo(function TrailDrawerContent({
509
517
  proposedAliases,
510
518
  resolveSymbol,
511
519
  onSymbolClick,
520
+ onVisibleStepChange,
512
521
  })}
513
522
  </>
514
523
  );
@@ -848,15 +857,28 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
848
857
  const drawerTitle = useMemo(() => {
849
858
  if (!drawerTarget) return null;
850
859
  if (drawerTarget.kind === 'file') return drawerTarget.file;
860
+ // Trail steps already show their own file header per file, so the drawer
861
+ // title is just the trail name.
851
862
  const tl = focusedTrail;
852
863
  if (!tl) return null;
853
- if (drawerTarget.stepIndex == null) return tl.title;
854
- const step = tl.steps[drawerTarget.stepIndex];
855
- if (!step) return tl.title;
856
- const site = `${step.file.split('/').pop() ?? step.file}:${step.line}`;
857
- return `${tl.title} · ${site}`;
864
+ return tl.title;
858
865
  }, [drawerTarget, focusedTrail]);
859
866
 
867
+ // Segmented step readout for trail drawers — a single file has no position.
868
+ // Follows the viewport once the code view reports a scroll, and falls back to
869
+ // the selected step until it does.
870
+ const [visibleStepIndex, setVisibleStepIndex] = useState<number | null>(null);
871
+ const drawerStepIndex = drawerTarget?.kind === 'trail' ? drawerTarget.stepIndex : null;
872
+ const drawerProgress = useMemo(() => {
873
+ if (drawerStepIndex == null || !focusedTrail) return null;
874
+ return { index: visibleStepIndex ?? drawerStepIndex, total: focusedTrail.steps.length };
875
+ }, [drawerStepIndex, focusedTrail, visibleStepIndex]);
876
+
877
+ // A new trail or step invalidates whatever the last scroll reported.
878
+ useEffect(() => {
879
+ setVisibleStepIndex(null);
880
+ }, [focusedTrail?.id, drawerStepIndex]);
881
+
860
882
  // Refresh selected component when the components list updates (e.g. verify
861
883
  // writes back declarationRef).
862
884
  useEffect(() => {
@@ -3145,6 +3167,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
3145
3167
  fillHeight={drawerFillHeight}
3146
3168
  suppressEscape={fileOverlayOpen}
3147
3169
  hidden={hideDrawer}
3170
+ progress={drawerProgress}
3148
3171
  >
3149
3172
  {drawerTarget?.kind === 'trail' &&
3150
3173
  focusedTrail &&
@@ -3157,6 +3180,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
3157
3180
  proposedAliases={proposedAliases}
3158
3181
  resolveSymbol={resolveTrailSymbol}
3159
3182
  onSymbolClick={openConstructDeclaration}
3183
+ onVisibleStepChange={setVisibleStepIndex}
3160
3184
  />
3161
3185
  ) : drawerTarget?.kind === 'file' ? (
3162
3186
  <FileDrawerContent