@principal-ai/subsystems-react 0.36.0 → 0.36.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.
@@ -27,7 +27,6 @@ import {
27
27
  storageBadgeLabel,
28
28
  storageBadgeColor,
29
29
  deriveNameFromSymbol,
30
- BADGE_EDGE_INSET,
31
30
  nodeMinWidthForBadges,
32
31
  MODULE_BADGE_INSET,
33
32
  moduleBadgeLabel,
@@ -70,9 +69,6 @@ function breakWords(s: string): string {
70
69
  export interface SubsystemGraphCallbacks {
71
70
  /** Click a component — open its file/entry point. */
72
71
  onSelect?: (componentAlias: string) => void;
73
- /** Click the filename badge — open that component's file in the drawer
74
- * (same path as clicking the file link in the declaration panel). */
75
- onOpenFile?: (componentAlias: string) => void;
76
72
  /** Click an edge (or its label) — select the relationship. */
77
73
  onEdgeSelect?: (edgeId: string) => void;
78
74
  /** Hover a component (null on leave) — associates it with the file tree. */
@@ -121,6 +117,7 @@ export function SubsystemComponentNode(props: NodeProps<Node<SubsystemGraphNodeD
121
117
  const callbacks = useSubsystemCallbacks();
122
118
  const { data, selected, width: nodeWidth, height: nodeHeight } = props;
123
119
  const c = data.component;
120
+ const [hover, setHover] = useState(false);
124
121
  // Construct owns node color, derived from the active Pierre syntax theme —
125
122
  // the same palette the declaration panel and file drawer render with. Role
126
123
  // shows as the hover badge, not as color — for now; a role glyph/accent may
@@ -129,7 +126,6 @@ export function SubsystemComponentNode(props: NodeProps<Node<SubsystemGraphNodeD
129
126
  // Left badge: framework brand (e.g. React cyan) when the label is a
130
127
  // framework stereotype; otherwise the same construct color as the border.
131
128
  const badgeColor = constructBadgeColor(c) ?? color;
132
- const [hover, setHover] = useState(false);
133
129
  const configuredMax = callbacks.maxNodeWidth;
134
130
  const maxWidth = configuredMax ?? 300;
135
131
  // `symbol` is the source of truth; `name` is derived from it consistently.
@@ -145,6 +141,11 @@ export function SubsystemComponentNode(props: NodeProps<Node<SubsystemGraphNodeD
145
141
  // Set while a file is open in the drawer: true → spotlight, false → dim,
146
142
  // absent (no file open) → neutral.
147
143
  const fileMatch = data.fileMatch as boolean | undefined;
144
+ // Node fill lifts on hover as the clickability affordance; badges share it
145
+ // so they read as tabs on the same surface rather than floating patches.
146
+ const nodeBg = theme.colors.backgroundSecondary ?? theme.colors.background;
147
+ const hoverBg =
148
+ theme.colors.backgroundHover ?? theme.colors.backgroundTertiary ?? nodeBg;
148
149
  // Border: proposed uses the goldenrod accent (dashed); construct color stays
149
150
  // on the left badge. File-open spotlight still wins with primary.
150
151
  const borderColor = fileMatch
@@ -159,6 +160,14 @@ export function SubsystemComponentNode(props: NodeProps<Node<SubsystemGraphNodeD
159
160
  const isExternal = c.construct === 'external';
160
161
  const nodeRadius = isExternal ? 0 : 8;
161
162
  const badgeRadius = isExternal ? 0 : 4;
163
+ // The border thickens on selection / file-match. Absolutely-positioned
164
+ // badges are laid out from the padding box (inside the border), so a raw
165
+ // `top` / `left` would slide with the border. Anchor them to the border box
166
+ // instead, offsetting by the border width. Side badges sit flush with the
167
+ // node's left / right edge (`-borderW`); the top reference stays at -9.
168
+ const borderW = isSelected || fileMatch ? 4 : 2;
169
+ const badgeTop = -9 - borderW;
170
+ const badgeEdge = -borderW;
162
171
 
163
172
  return (
164
173
  <div
@@ -187,15 +196,15 @@ export function SubsystemComponentNode(props: NodeProps<Node<SubsystemGraphNodeD
187
196
  maxWidth,
188
197
  padding: '6px 10px',
189
198
  borderRadius: nodeRadius,
190
- background: theme.colors.backgroundSecondary ?? theme.colors.background,
199
+ background: hover ? hoverBg : nodeBg,
191
200
  // Selected / file-matched nodes get a thicker border. Proposed nodes
192
201
  // use a dashed goldenrod border; left construct badge keeps construct color.
193
- border: `${isSelected || fileMatch ? 4 : 2}px ${c.proposed ? 'dashed' : 'solid'} ${borderColor}`,
202
+ border: `${borderW}px ${c.proposed ? 'dashed' : 'solid'} ${borderColor}`,
194
203
  boxShadow: fileMatch
195
204
  ? `0 1px 4px rgba(0,0,0,0.25), 0 0 12px ${theme.colors.primary}55`
196
205
  : '0 1px 4px rgba(0,0,0,0.25)',
197
206
  opacity: fileMatch === false || data.dimmed === true ? 0.18 : 1,
198
- transition: 'opacity 150ms ease',
207
+ transition: 'opacity 150ms ease, background-color 120ms ease',
199
208
  cursor: 'pointer',
200
209
  fontFamily: theme.fonts.body,
201
210
  }}
@@ -207,8 +216,8 @@ export function SubsystemComponentNode(props: NodeProps<Node<SubsystemGraphNodeD
207
216
  <div
208
217
  style={{
209
218
  position: 'absolute',
210
- top: -11,
211
- left: BADGE_EDGE_INSET,
219
+ top: badgeTop,
220
+ left: badgeEdge,
212
221
  zIndex: 1,
213
222
  fontFamily: theme.fonts.monospace,
214
223
  fontSize: theme.fontSizes[1],
@@ -217,7 +226,7 @@ export function SubsystemComponentNode(props: NodeProps<Node<SubsystemGraphNodeD
217
226
  lineHeight: '17px',
218
227
  whiteSpace: 'nowrap',
219
228
  color: badgeColor,
220
- background: theme.colors.backgroundSecondary ?? theme.colors.background,
229
+ background: nodeBg,
221
230
  border: `2px solid ${badgeColor}`,
222
231
  borderRadius: badgeRadius,
223
232
  padding: '2px 8px',
@@ -226,15 +235,18 @@ export function SubsystemComponentNode(props: NodeProps<Node<SubsystemGraphNodeD
226
235
  {constructBadgeLabel(c)}
227
236
  </div>
228
237
 
229
- {/* Right badge cluster — proposed wins over role when both are set;
238
+ {/* Secondary badge cluster — proposed wins over role when both are set;
230
239
  store storage (retention backing) sits beside, since it is
231
- orthogonal to topology. Persistent; pointer-events pass through. */}
240
+ orthogonal to topology. Persistent; pointer-events pass through.
241
+ Sits astride the bottom edge, centered (mirroring the top construct
242
+ badge's straddle), so the top edge stays free for the construct tag. */}
232
243
  {(topRightLabel != null || storageLabel != null) && (
233
244
  <div
234
245
  style={{
235
246
  position: 'absolute',
236
- top: -11,
237
- right: BADGE_EDGE_INSET,
247
+ bottom: badgeTop,
248
+ left: '50%',
249
+ transform: 'translateX(-50%)',
238
250
  zIndex: 1,
239
251
  display: 'flex',
240
252
  gap: 6,
@@ -250,7 +262,7 @@ export function SubsystemComponentNode(props: NodeProps<Node<SubsystemGraphNodeD
250
262
  textTransform: 'uppercase',
251
263
  lineHeight: '17px',
252
264
  color: storageColor,
253
- background: theme.colors.backgroundSecondary ?? theme.colors.background,
265
+ background: nodeBg,
254
266
  border: `2px solid ${storageColor}`,
255
267
  borderRadius: badgeRadius,
256
268
  padding: '2px 8px',
@@ -268,7 +280,7 @@ export function SubsystemComponentNode(props: NodeProps<Node<SubsystemGraphNodeD
268
280
  textTransform: 'uppercase',
269
281
  lineHeight: '17px',
270
282
  color: topRightColor,
271
- background: theme.colors.backgroundSecondary ?? theme.colors.background,
283
+ background: nodeBg,
272
284
  border: `2px solid ${topRightColor}`,
273
285
  borderRadius: badgeRadius,
274
286
  padding: '2px 8px',
@@ -280,41 +292,6 @@ export function SubsystemComponentNode(props: NodeProps<Node<SubsystemGraphNodeD
280
292
  </div>
281
293
  )}
282
294
 
283
- {/* Filename badge — a strip across the bottom border, shown on hover
284
- and pinned while this node's file is open in the drawer (fileMatch).
285
- Mirrors the construct/role tab badges; hidden for fileless nodes. */}
286
- {(hover || fileMatch === true) && c.file ? (
287
- <div
288
- style={{
289
- position: 'absolute',
290
- bottom: -14,
291
- left: '50%',
292
- transform: 'translateX(-50%)',
293
- zIndex: 1,
294
- fontFamily: theme.fonts.monospace,
295
- fontSize: theme.fontSizes[1],
296
- letterSpacing: 0.5,
297
- lineHeight: '17px',
298
- whiteSpace: 'nowrap',
299
- // filename stays neutral — the construct color belongs to the
300
- // borders and badges, not the file name
301
- color: theme.colors.text ?? theme.colors.textSecondary,
302
- background: theme.colors.backgroundSecondary ?? theme.colors.background,
303
- border: `2px solid ${color}`,
304
- borderRadius: badgeRadius,
305
- padding: '2px 8px',
306
- cursor: 'pointer',
307
- }}
308
- onClick={(e) => {
309
- // Open the file directly; don't also toggle node selection.
310
- e.stopPropagation();
311
- callbacks.onOpenFile?.(c.alias);
312
- }}
313
- >
314
- {c.file.split('/').pop()}
315
- </div>
316
- ) : null}
317
-
318
295
  {/* Purpose is no longer shown as a hover tooltip below the node — it
319
296
  lives in the declaration panel and the graphify detail payload. */}
320
297
 
@@ -454,12 +431,13 @@ export function SubsystemGroupNode(props: NodeProps<Node<SubsystemGroupNodeData,
454
431
  } : undefined}
455
432
  style={{
456
433
  position: 'absolute',
457
- // Process: badge fill starts with the process fill (inside the
458
- // frame border); no top badge border so widths don't fight.
459
- // Module/package are centred astride the top edge like component
460
- // badges. Always set left/transform explicitly — React Flow reuses
461
- // group DOM nodes and `undefined` does not clear a prior value.
462
- top: 0,
434
+ // Process: the tab sits flush over the frame's top border (`top:
435
+ // -2` — the frame border is 2px) and carries its own full border, so
436
+ // the two read as one piece. Module/package are centred astride the
437
+ // top edge like component badges. Always set left/transform
438
+ // explicitly — React Flow reuses group DOM nodes and `undefined`
439
+ // does not clear a prior value.
440
+ top: isProcess ? -2 : 0,
463
441
  left: isProcess ? '50%' : 12,
464
442
  transform: isProcess ? 'translateX(-50%)' : 'translateY(-50%)',
465
443
  zIndex: canToggle ? 10 : undefined,
@@ -475,7 +453,11 @@ export function SubsystemGroupNode(props: NodeProps<Node<SubsystemGroupNodeData,
475
453
  cursor: canToggle ? 'pointer' : undefined,
476
454
  // The trailing halo squares the rounded corners back off in the fill
477
455
  // colour, so an edge crossing a corner can't show through the notch.
478
- boxShadow: `${canToggle && hover ? `0 2px 10px ${color}55, ` : ''}0 0 0 1.5px ${badgeBg}`,
456
+ // Square (process) badges have no notch, and the halo would erase the
457
+ // frame border they now sit flush on, so they skip it.
458
+ boxShadow: isProcess
459
+ ? undefined
460
+ : `${canToggle && hover ? `0 2px 10px ${color}55, ` : ''}0 0 0 1.5px ${badgeBg}`,
479
461
  fontFamily: theme.fonts.monospace,
480
462
  fontSize: theme.fontSizes[3],
481
463
  fontWeight: 700,
@@ -483,7 +465,7 @@ export function SubsystemGroupNode(props: NodeProps<Node<SubsystemGroupNodeData,
483
465
  color,
484
466
  background: badgeBg,
485
467
  border: `2px solid ${color}`,
486
- borderTopWidth: isProcess ? 0 : 2,
468
+ borderTopWidth: 2,
487
469
  borderRadius: badgeRadius,
488
470
  padding: '3px 9px',
489
471
  whiteSpace: 'nowrap',
@@ -642,7 +642,10 @@ export async function computeElkLayout(
642
642
  const compoundLayoutOptions: LayoutOptions = {
643
643
  'elk.algorithm': 'layered',
644
644
  'elk.direction': direction,
645
- 'elk.padding': '[top=48,left=24,bottom=24,right=24]',
645
+ // `top` is the header band a boundary badge sits in. It has to clear the
646
+ // badge (~28px tall, and a nested frame's badge can overhang its own top
647
+ // edge by ~13px) plus a gap before the first child.
648
+ 'elk.padding': '[top=64,left=24,bottom=24,right=24]',
646
649
  'elk.spacing.nodeNode': '40',
647
650
  };
648
651