@principal-ai/subsystems-react 0.24.24 → 0.24.26

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.
@@ -502,6 +502,27 @@ export function moduleGroupNodeId(moduleKey: string): string {
502
502
  return `module:${moduleKey}`;
503
503
  }
504
504
 
505
+ export const MODULE_BADGE_INSET = 12;
506
+ const MODULE_BADGE_CHAR_WIDTH = 12.5;
507
+ const MODULE_BADGE_CHROME = 16;
508
+ const MODULE_FRAME_BORDER = 4;
509
+
510
+ export function moduleBadgeLabel(path: string, availableWidth = 0): string {
511
+ if (path.length * MODULE_BADGE_CHAR_WIDTH + MODULE_BADGE_CHROME <= availableWidth) {
512
+ return path;
513
+ }
514
+ const parts = path.split(/[\\/]/).filter((part) => part !== '' && part !== '.');
515
+ if (parts.length <= 2) return path;
516
+ return `${parts[0]}/…/${parts[parts.length - 1]}`;
517
+ }
518
+
519
+ export function moduleMinWidthForBadge(path: string): number {
520
+ return Math.ceil(
521
+ moduleBadgeLabel(path).length * MODULE_BADGE_CHAR_WIDTH +
522
+ MODULE_BADGE_CHROME + MODULE_BADGE_INSET * 2 + MODULE_FRAME_BORDER,
523
+ );
524
+ }
525
+
505
526
  /** React Flow id for a package (repo) boundary group node. */
506
527
  export function packageGroupNodeId(packageKey: string): string {
507
528
  return `package:${packageKey}`;
@@ -1363,6 +1384,7 @@ export async function buildSubsystemGraph(
1363
1384
  id: g.id,
1364
1385
  memberIds: g.memberIds,
1365
1386
  parentId: g.parentId,
1387
+ minWidth: g.region.kind === 'module' ? moduleMinWidthForBadge(g.region.label) : undefined,
1366
1388
  })),
1367
1389
  });
1368
1390
  const builtGroupIds = new Set(result.groupBounds.keys());
@@ -1,5 +1,9 @@
1
- import { describe, expect, test } from 'bun:test';
2
- import { EDGE_DIM_ALPHA, fileMatchForNode, flowElementVisibility, hexWithAlpha } from './nodes';
1
+ import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test';
2
+ import { createElement, type ComponentProps } from 'react';
3
+ import { cleanup, fireEvent, render } from '@testing-library/react/pure';
4
+ import { ThemeProvider } from '@principal-ade/industry-theme';
5
+ import { Window } from 'happy-dom';
6
+ import { EDGE_DIM_ALPHA, fileMatchForNode, flowElementVisibility, hexWithAlpha, SubsystemGroupNode } from './nodes';
3
7
 
4
8
  describe('hexWithAlpha', () => {
5
9
  test('appends a two-digit alpha to #rrggbb', () => {
@@ -29,6 +29,8 @@ import {
29
29
  deriveNameFromSymbol,
30
30
  BADGE_EDGE_INSET,
31
31
  nodeMinWidthForBadges,
32
+ MODULE_BADGE_INSET,
33
+ moduleBadgeLabel,
32
34
  packageColor,
33
35
  type SubsystemGraphNodeData,
34
36
  type SubsystemGroupNodeData,
@@ -352,6 +354,12 @@ export function SubsystemGroupNode(props: NodeProps<Node<SubsystemGroupNodeData,
352
354
  // Kind is carried by frame chrome (square = process; rounded = module/package)
353
355
  // — don't prefix `module ·` / `package ·` or the path reads twice.
354
356
  const label = region?.label ?? '';
357
+ const [expandedLabel, setExpandedLabel] = useState<string | null>(null);
358
+ const isModule = region?.kind === 'module';
359
+ const expanded = isModule && expandedLabel === label;
360
+ const availableBadgeWidth = Math.max(0, (width ?? 400) - MODULE_BADGE_INSET * 2 - 4);
361
+ const collapsedLabel = isModule ? moduleBadgeLabel(label, availableBadgeWidth) : label;
362
+ const canExpand = isModule && collapsedLabel !== label;
355
363
  // Process frames match external nodes — square corners (outside the soft
356
364
  // component language). Module / package frames keep the rounded look.
357
365
  const isProcess = region?.kind === 'process';
@@ -382,6 +390,26 @@ export function SubsystemGroupNode(props: NodeProps<Node<SubsystemGroupNodeData,
382
390
  }}
383
391
  >
384
392
  <div
393
+ className={isModule ? 'nodrag nopan' : undefined}
394
+ role={canExpand ? 'button' : undefined}
395
+ tabIndex={canExpand && !hidden ? 0 : undefined}
396
+ aria-expanded={canExpand ? expanded : undefined}
397
+ aria-label={canExpand ? `${expanded ? 'Collapse' : 'Expand'} module path: ${label}` : undefined}
398
+ title={isModule ? label : undefined}
399
+ onClick={canExpand ? (event) => {
400
+ event.stopPropagation();
401
+ setExpandedLabel(expanded ? null : label);
402
+ } : undefined}
403
+ onKeyDown={canExpand ? (event) => {
404
+ if (event.key === 'Enter' || event.key === ' ') {
405
+ event.preventDefault();
406
+ event.stopPropagation();
407
+ setExpandedLabel(expanded ? null : label);
408
+ } else if (event.key === 'Escape' && expanded) {
409
+ event.stopPropagation();
410
+ setExpandedLabel(null);
411
+ }
412
+ } : undefined}
385
413
  style={{
386
414
  position: 'absolute',
387
415
  // Process: badge fill starts with the process fill (inside the
@@ -79,7 +79,7 @@ export interface ElkLayoutOptions {
79
79
  * nests this group under another group; omit for a root-level frame.
80
80
  * Members reference their immediate parent via React Flow `parentId`.
81
81
  */
82
- groups?: Array<{ id: string; memberIds: string[]; parentId?: string }>;
82
+ groups?: Array<{ id: string; memberIds: string[]; parentId?: string; minWidth?: number }>;
83
83
  }
84
84
 
85
85
  /** Result of ELK layout computation */
@@ -587,7 +587,11 @@ export async function computeElkLayout(
587
587
  builtGroups.set(g.id, {
588
588
  id: g.id,
589
589
  children: childNodes,
590
- layoutOptions: compoundLayoutOptions,
590
+ layoutOptions: g.minWidth == null ? compoundLayoutOptions : {
591
+ ...compoundLayoutOptions,
592
+ 'elk.nodeSize.constraints': 'MINIMUM_SIZE',
593
+ 'elk.nodeSize.minimum': `(${g.minWidth},0)`,
594
+ },
591
595
  });
592
596
  }
593
597
  if (!progress) {