@principal-ai/subsystems-react 0.20.4 → 0.20.6

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 (36) hide show
  1. package/dist/graphify/construct.d.ts +23 -0
  2. package/dist/graphify/construct.d.ts.map +1 -0
  3. package/dist/graphify/construct.js +77 -0
  4. package/dist/graphify/construct.js.map +1 -0
  5. package/dist/graphify/index.d.ts +2 -2
  6. package/dist/graphify/index.d.ts.map +1 -1
  7. package/dist/graphify/index.js +1 -1
  8. package/dist/graphify/index.js.map +1 -1
  9. package/dist/index.d.ts +3 -3
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +2 -2
  12. package/dist/index.js.map +1 -1
  13. package/dist/subsystem/ComponentDeclaration.js +1 -1
  14. package/dist/subsystem/ComponentDeclaration.js.map +1 -1
  15. package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
  16. package/dist/subsystem/SubsystemComponentGraph.js +87 -21
  17. package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
  18. package/dist/subsystem/model.d.ts +24 -7
  19. package/dist/subsystem/model.d.ts.map +1 -1
  20. package/dist/subsystem/model.js +25 -2
  21. package/dist/subsystem/model.js.map +1 -1
  22. package/dist/subsystem/nodes.d.ts.map +1 -1
  23. package/dist/subsystem/nodes.js +18 -10
  24. package/dist/subsystem/nodes.js.map +1 -1
  25. package/package.json +1 -1
  26. package/src/graphify/{kind.test.ts → construct.test.ts} +20 -20
  27. package/src/graphify/{kind.ts → construct.ts} +17 -17
  28. package/src/graphify/index.ts +5 -2
  29. package/src/index.ts +7 -4
  30. package/src/stories/Subsystem/ComponentGraph/Captures.stories.tsx +2 -6
  31. package/src/stories/Subsystem/ComponentGraph/Proposed.stories.tsx +254 -0
  32. package/src/subsystem/ComponentDeclaration.tsx +1 -1
  33. package/src/subsystem/SubsystemComponentGraph.tsx +90 -24
  34. package/src/subsystem/model.test.ts +22 -0
  35. package/src/subsystem/model.ts +38 -9
  36. package/src/subsystem/nodes.tsx +21 -14
@@ -236,8 +236,10 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
236
236
  // `null` = whole flow focused; a number = that single step's edge focused.
237
237
  const [focusedStepIndex, setFocusedStepIndex] = useState<number | null>(null);
238
238
  // Hovered throughline in the flows panel: dims every canvas node/edge not
239
- // involved. `stepIndex: null` = whole flow (collapsed title hover);
240
- // a number = that step. Transient — no camera move, no drawer, no focus.
239
+ // involved in the hover preview (or selected ∪ hovered when a step is
240
+ // focused). `stepIndex: null` = whole flow (collapsed title hover);
241
+ // a number = that step. Transient — no drawer. Camera only reframes when
242
+ // a step is already selected.
241
243
  const [hoveredThroughlineStep, setHoveredThroughlineStep] = useState<{
242
244
  throughlineId: string;
243
245
  stepIndex: number | null;
@@ -605,7 +607,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
605
607
  }
606
608
  return ids.size > 0 ? ids : null;
607
609
  };
608
- // Edge/nodes involved in the hovered step or whole flow — dim everything else.
610
+ // Edge/nodes involved in the hovered step or whole flow.
609
611
  const hoverEdgeIds = useMemo(() => {
610
612
  if (!hoveredThroughlineStep || !throughlines) return null;
611
613
  const tl = throughlines.find((t) => t.id === hoveredThroughlineStep.throughlineId);
@@ -616,10 +618,20 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
616
618
  const step = tl.steps[hoveredThroughlineStep.stepIndex];
617
619
  return step ? new Set([step.edgeId]) : null;
618
620
  }, [throughlines, hoveredThroughlineStep]);
619
- const hoverNodeIds = useMemo(
620
- () => endpointsOf(hoverEdgeIds),
621
+ // While hovering with a *step* already selected, brighten the union of
622
+ // selected + hovered participants. Whole-flow focus (or no focus) keeps
623
+ // the old hover-replace preview so a step hover still dims the rest.
624
+ const previewEdgeIds = useMemo(() => {
625
+ if (!hoverEdgeIds) return null;
626
+ if (focusedStepIndex == null || !focusEdgeIds) return hoverEdgeIds;
627
+ const ids = new Set(hoverEdgeIds);
628
+ for (const id of focusEdgeIds) ids.add(id);
629
+ return ids;
630
+ }, [hoverEdgeIds, focusEdgeIds, focusedStepIndex]);
631
+ const previewNodeIds = useMemo(
632
+ () => endpointsOf(previewEdgeIds),
621
633
  // eslint-disable-next-line react-hooks/exhaustive-deps
622
- [baseEdges, hoverEdgeIds],
634
+ [baseEdges, previewEdgeIds],
623
635
  );
624
636
  const openedNodeIds = useMemo(
625
637
  () => endpointsOf(openedEdgeIds),
@@ -654,9 +666,9 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
654
666
  // `isSelected` rides in data because the node's stopPropagation() keeps
655
667
  // React Flow's own selection state from updating.
656
668
  const dispNodes = useMemo(() => {
657
- // While a step/flow is hovered, only its participants stay bright;
658
- // everything else among the currently-visible (opened-throughline) nodes
659
- // is dimmed. Nodes outside the opened-throughline set stay hidden.
669
+ // While a step/flow is hovered, selected + hovered participants stay
670
+ // bright; everything else among the currently-visible (opened-throughline)
671
+ // nodes is dimmed. Nodes outside the opened-throughline set stay hidden.
660
672
  return xyflowNodesBase.map((n) => {
661
673
  // Boundary frames follow their members: hidden when no member is
662
674
  // visible, dimmed when members are dimmed. Never selectable.
@@ -668,8 +680,8 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
668
680
  anyOpened: openedNodeIds != null,
669
681
  anySelected: brightNodeIds != null,
670
682
  });
671
- const dimmed = hoverNodeIds
672
- ? vis.hidden || !memberIds.some((id) => hoverNodeIds.has(id))
683
+ const dimmed = previewNodeIds
684
+ ? vis.hidden || !memberIds.some((id) => previewNodeIds.has(id))
673
685
  : vis.dimmed;
674
686
  const hidden = vis.hidden;
675
687
  return {
@@ -691,8 +703,8 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
691
703
  anyOpened: openedNodeIds != null,
692
704
  anySelected: brightNodeIds != null,
693
705
  });
694
- const dimmed = hoverNodeIds ? (vis.hidden || !hoverNodeIds.has(n.id)) : vis.dimmed;
695
- const hidden = vis.hidden;
706
+ const dimmed = previewNodeIds ? (vis.hidden || !previewNodeIds.has(n.id)) : vis.dimmed;
707
+ const hidden = vis.hidden;
696
708
  if (fileMatch === undefined && !isSelected && !dimmed) {
697
709
  const { fileMatch: _f, isSelected: _s, dimmed: _d, ...rest } = n.data as Record<string, unknown>;
698
710
  return { ...n, hidden, data: rest };
@@ -708,7 +720,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
708
720
  },
709
721
  };
710
722
  });
711
- }, [xyflowNodesBase, openFile, selected, focusNodeIds, openedNodeIds, brightNodeIds, hoverNodeIds]);
723
+ }, [xyflowNodesBase, openFile, selected, focusNodeIds, openedNodeIds, brightNodeIds, previewNodeIds]);
712
724
 
713
725
  const baseNodesKey = useMemo(() => nodes.map((n) => n.id).sort().join(','), [nodes]);
714
726
  const baseEdgesKey = useMemo(
@@ -729,7 +741,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
729
741
  markerEnd: nextMarker,
730
742
  };
731
743
  };
732
- if (openedEdgeIds || focusEdgeIds || hoverEdgeIds) {
744
+ if (openedEdgeIds || focusEdgeIds || previewEdgeIds) {
733
745
  return baseEdges.map((e) => {
734
746
  const vis = flowElementVisibility({
735
747
  inOpened: openedEdgeIds?.has(e.id) === true,
@@ -737,14 +749,14 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
737
749
  anyOpened: openedEdgeIds != null,
738
750
  anySelected: focusEdgeIds != null,
739
751
  });
740
- const dimmed = hoverEdgeIds ? vis.hidden || !hoverEdgeIds.has(e.id) : vis.dimmed;
752
+ const dimmed = previewEdgeIds ? vis.hidden || !previewEdgeIds.has(e.id) : vis.dimmed;
741
753
  const hidden = vis.hidden;
742
754
  return { ...paint(e, dimmed), hidden };
743
755
  });
744
756
  }
745
757
  if (!selectedEdgeId) return baseEdges;
746
758
  return baseEdges.map((e) => paint(e, e.id !== selectedEdgeId));
747
- }, [baseEdges, selectedEdgeId, openedEdgeIds, focusEdgeIds, hoverEdgeIds]);
759
+ }, [baseEdges, selectedEdgeId, openedEdgeIds, focusEdgeIds, previewEdgeIds]);
748
760
 
749
761
  const onNodesChange = useCallback(
750
762
  (changes: NodeChange[]) => {
@@ -934,7 +946,8 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
934
946
  }, []);
935
947
 
936
948
  // Focus a single step's edge on the canvas and open/scroll the throughline
937
- // drawer to that step's snippet.
949
+ // drawer to that step's snippet. Clicking the already-focused step clears
950
+ // step focus and returns to whole-flow (high-level) framing.
938
951
  // In `dim` mode: only dim non-participants (same as hovering a step) —
939
952
  // no camera move, no drawer, no hide.
940
953
  const focusThroughlineStep = useCallback(
@@ -950,6 +963,20 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
950
963
  setDrawerTarget((prev) => (prev?.kind === 'throughline' ? null : prev));
951
964
  return;
952
965
  }
966
+ // Toggle: clicking the selected step unselects it (back to whole flow).
967
+ if (focusedThroughlineId === tl.id && focusedStepIndex === stepIndex) {
968
+ setFocusedStepIndex(null);
969
+ // Pointer is still over the row — keep hover preview so dimming doesn't
970
+ // flash off until mouseleave.
971
+ setHoveredThroughlineStep({ throughlineId: tl.id, stepIndex });
972
+ setDrawerTarget((prev) => (prev?.kind === 'throughline' ? null : prev));
973
+ if (pendingFocusFitRef.current != null) {
974
+ window.clearTimeout(pendingFocusFitRef.current);
975
+ pendingFocusFitRef.current = null;
976
+ }
977
+ fitFocusBounds(new Set(tl.steps.map((s) => s.edgeId)));
978
+ return;
979
+ }
953
980
  setFocusedStepIndex(stepIndex);
954
981
  setFocusedThroughlineId(tl.id);
955
982
  setHoveredThroughlineStep(null);
@@ -983,7 +1010,13 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
983
1010
  }, FILE_DRAWER_HEIGHT_MS + 20);
984
1011
  }
985
1012
  },
986
- [fitFocusBounds, renderThroughlineViewer, throughlineStepMode],
1013
+ [
1014
+ fitFocusBounds,
1015
+ focusedStepIndex,
1016
+ focusedThroughlineId,
1017
+ renderThroughlineViewer,
1018
+ throughlineStepMode,
1019
+ ],
987
1020
  );
988
1021
 
989
1022
  useEffect(() => {
@@ -994,6 +1027,37 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
994
1027
  };
995
1028
  }, []);
996
1029
 
1030
+ // While a *step* is selected, hovering another step/flow reframes the
1031
+ // camera to the selected ∪ hovered participants. Leaving hover snaps back
1032
+ // to the selection alone. No step selected → hover only dims; camera stays
1033
+ // at the high-level view. Click-to-focus still owns its own fit (including
1034
+ // the drawer-open delay) — this only reacts to previewEdgeIds transitions.
1035
+ const prevPreviewEdgeIdsRef = useRef<ReadonlySet<string> | null>(null);
1036
+ useEffect(() => {
1037
+ const prev = prevPreviewEdgeIdsRef.current;
1038
+ prevPreviewEdgeIdsRef.current = previewEdgeIds;
1039
+
1040
+ if (!zoomOnThroughlineFocus || throughlineStepMode === 'dim') return;
1041
+ // Camera follows hover only when a specific step is already focused.
1042
+ if (focusedThroughlineId == null || focusedStepIndex == null) return;
1043
+
1044
+ if (previewEdgeIds) {
1045
+ fitFocusBounds(previewEdgeIds);
1046
+ return;
1047
+ }
1048
+ if (prev != null && focusEdgeIds) {
1049
+ fitFocusBounds(focusEdgeIds);
1050
+ }
1051
+ }, [
1052
+ previewEdgeIds,
1053
+ focusEdgeIds,
1054
+ focusedThroughlineId,
1055
+ focusedStepIndex,
1056
+ fitFocusBounds,
1057
+ zoomOnThroughlineFocus,
1058
+ throughlineStepMode,
1059
+ ]);
1060
+
997
1061
  // Graph-only embeds: cycle throughline steps with hover-style dimming.
998
1062
  useEffect(() => {
999
1063
  if (!autoPlayThroughlines || !throughlines?.length || !layoutReady) return;
@@ -2037,7 +2101,13 @@ function ThroughlineFlow({
2037
2101
  )}
2038
2102
  </div>
2039
2103
  {!collapsed && (
2040
- <div style={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
2104
+ <div
2105
+ style={{ display: 'flex', flexDirection: 'column' }}
2106
+ onMouseLeave={() => {
2107
+ setHoveredStep(null);
2108
+ onLeaveStep();
2109
+ }}
2110
+ >
2041
2111
  {throughline.steps.map((step, i) => {
2042
2112
  const edge = edgeById.get(step.edgeId);
2043
2113
  const mech = edge?.mechanism;
@@ -2054,10 +2124,6 @@ function ThroughlineFlow({
2054
2124
  setHoveredStep(i);
2055
2125
  onHoverStep(throughline, i);
2056
2126
  }}
2057
- onMouseLeave={() => {
2058
- setHoveredStep(null);
2059
- onLeaveStep();
2060
- }}
2061
2127
  onClick={() => onFocusStep(throughline, i)}
2062
2128
  style={{
2063
2129
  display: 'flex',
@@ -8,6 +8,7 @@ import {
8
8
  buildSubsystemGraph,
9
9
  deriveNameFromSymbol,
10
10
  constructBadgeLabel,
11
+ rightBadgeLabel,
11
12
  nodeMinWidthForBadges,
12
13
  formatPurl,
13
14
  packageColor,
@@ -156,6 +157,27 @@ describe('subsystem graph model', () => {
156
157
  role: 'entry',
157
158
  });
158
159
  expect(withRole).toBeGreaterThan(stereotype);
160
+
161
+ const withProposed = nodeMinWidthForBadges({
162
+ construct: 'function',
163
+ proposed: true,
164
+ });
165
+ expect(withProposed).toBeGreaterThan(plain);
166
+
167
+ const withBoth = nodeMinWidthForBadges({
168
+ construct: 'function',
169
+ role: 'entry',
170
+ proposed: true,
171
+ });
172
+ // proposed-only label is shorter than entry · proposed; still wider than plain
173
+ expect(withBoth).toBe(withProposed);
174
+ });
175
+
176
+ test('rightBadgeLabel prefers proposed over role', () => {
177
+ expect(rightBadgeLabel({})).toBeNull();
178
+ expect(rightBadgeLabel({ proposed: true })).toBe('proposed');
179
+ expect(rightBadgeLabel({ role: 'entry' })).toBe('entry');
180
+ expect(rightBadgeLabel({ role: 'service', proposed: true })).toBe('proposed');
159
181
  });
160
182
 
161
183
  test('deriveNameFromSymbol falls back to file basename for modules', () => {
@@ -126,6 +126,13 @@ export interface SubsystemComponent {
126
126
  purl: string;
127
127
  /** One-line purpose shown on the node. */
128
128
  purpose?: string;
129
+ /**
130
+ * Design / migration placeholder — participates in edges and flows but is
131
+ * not a live source declaration yet. Verification skips source checks until
132
+ * promoted (`proposed` cleared, `file` + `symbol` filled). Orthogonal to
133
+ * `construct` (intended shape) and `role` (topology).
134
+ */
135
+ proposed?: boolean;
129
136
  /**
130
137
  * Semantic role — where the node sits in the topology (boundary element,
131
138
  * external system), orthogonal to `construct` (what it is). Drives the
@@ -179,13 +186,6 @@ export interface SubsystemComponent {
179
186
  * letting ELK guess the order.
180
187
  */
181
188
  layer?: number;
182
- /**
183
- * How this session *occupied* the component — `edited` (modified it) vs
184
- * `analyzed` (read deeply / built an understanding) vs `referenced`
185
- * (touched in passing). Drives clustering: edits strengthen a subsystem,
186
- * but a read-only investigation can still form one around an analyzed seam.
187
- */
188
- capture?: 'edited' | 'analyzed' | 'referenced';
189
189
  /**
190
190
  * Graphify drill-down detail for this component, when the facet is anchored
191
191
  * to a graphify node. Discriminated by kind (class/function/type/module/store/
@@ -525,6 +525,33 @@ export const ROLE_LABEL: Record<SubsystemComponentRole, string> = {
525
525
  service: 'service',
526
526
  };
527
527
 
528
+ /** Right-badge accent for `proposed: true` (no role, or combined with role). */
529
+ export const PROPOSED_COLOR = '#b8860b'; // darkgoldenrod — not-yet-in-source
530
+
531
+ /**
532
+ * Top-right badge label: proposed wins over role when both are set
533
+ * (proposed is the temporary exception to scan for; role stays on the model).
534
+ * Returns null when neither applies.
535
+ */
536
+ export function rightBadgeLabel(component: {
537
+ role?: SubsystemComponentRole;
538
+ proposed?: boolean;
539
+ }): string | null {
540
+ if (component.proposed) return 'proposed';
541
+ if (component.role != null) return ROLE_LABEL[component.role];
542
+ return null;
543
+ }
544
+
545
+ /** Color for the top-right badge (role and/or proposed). */
546
+ export function rightBadgeColor(component: {
547
+ role?: SubsystemComponentRole;
548
+ proposed?: boolean;
549
+ }): string | null {
550
+ if (component.proposed) return PROPOSED_COLOR;
551
+ if (component.role != null) return ROLE_COLOR[component.role];
552
+ return null;
553
+ }
554
+
528
555
  /**
529
556
  * Primary badge text for a node: prefer framework stereotype over the
530
557
  * language construct so a React UI unit reads as "component" / "hook"
@@ -576,13 +603,15 @@ export function nodeMinWidthForBadges(component: {
576
603
  framework?: string;
577
604
  stereotype?: string;
578
605
  role?: SubsystemComponentRole;
606
+ proposed?: boolean;
579
607
  entityKind?: string;
580
608
  }): number {
581
609
  const left = estimateBadgeLabelWidth(constructBadgeLabel(component));
582
- if (component.role == null) {
610
+ const rightLabel = rightBadgeLabel(component);
611
+ if (rightLabel == null) {
583
612
  return Math.max(NODE_CSS_MIN_WIDTH, BADGE_EDGE_INSET + left + BADGE_EDGE_INSET);
584
613
  }
585
- const right = estimateBadgeLabelWidth(ROLE_LABEL[component.role]);
614
+ const right = estimateBadgeLabelWidth(rightLabel);
586
615
  return Math.max(
587
616
  NODE_CSS_MIN_WIDTH,
588
617
  BADGE_EDGE_INSET + left + BADGE_PAIR_GAP + right + BADGE_EDGE_INSET,
@@ -19,9 +19,10 @@ import { useTheme } from '@principal-ade/industry-theme';
19
19
  import {
20
20
  MECHANISM_COLOR,
21
21
  MECHANISM_STYLE,
22
- ROLE_COLOR,
23
- ROLE_LABEL,
22
+ PROPOSED_COLOR,
24
23
  constructBadgeLabel,
24
+ rightBadgeLabel,
25
+ rightBadgeColor,
25
26
  deriveNameFromSymbol,
26
27
  BADGE_EDGE_INSET,
27
28
  nodeMinWidthForBadges,
@@ -92,11 +93,20 @@ export function SubsystemComponentNode(props: NodeProps<Node<SubsystemGraphNodeD
92
93
  // `symbol` is the source of truth; `name` is derived from it consistently.
93
94
  const displayName = deriveNameFromSymbol(c.symbol, c.construct, c.name, c.file, c.stereotype);
94
95
  // Top badges are absolutely positioned — widen the node so they nowrap
95
- // instead of wrapping, including when construct + role badges share the top.
96
+ // instead of wrapping, including when construct + role/proposed share the top.
96
97
  const badgeMinWidth = nodeMinWidthForBadges(c);
98
+ const topRightLabel = rightBadgeLabel(c);
99
+ const topRightColor = rightBadgeColor(c);
97
100
  // Set while a file is open in the drawer: true → spotlight, false → dim,
98
101
  // absent (no file open) → neutral.
99
102
  const fileMatch = data.fileMatch as boolean | undefined;
103
+ // Border: proposed uses the goldenrod accent (dashed); construct color stays
104
+ // on the left badge. File-open spotlight still wins with primary.
105
+ const borderColor = fileMatch
106
+ ? theme.colors.primary
107
+ : c.proposed
108
+ ? PROPOSED_COLOR
109
+ : color;
100
110
  // Selection is stamped into data by the graph component — React Flow's own
101
111
  // `selected` never updates because the node stops click propagation.
102
112
  const isSelected = selected || (data.isSelected as boolean | undefined) === true;
@@ -129,10 +139,9 @@ export function SubsystemComponentNode(props: NodeProps<Node<SubsystemGraphNodeD
129
139
  padding: '6px 10px',
130
140
  borderRadius: 8,
131
141
  background: theme.colors.backgroundSecondary ?? theme.colors.background,
132
- // Selected / file-matched nodes get a thicker border; kind colors are
133
- // kept for selection (thickness only). The file-open spotlight keeps
134
- // its primary tint.
135
- border: `${isSelected || fileMatch ? 4 : 2}px solid ${fileMatch ? theme.colors.primary : color}`,
142
+ // Selected / file-matched nodes get a thicker border. Proposed nodes
143
+ // use a dashed goldenrod border; left construct badge keeps construct color.
144
+ border: `${isSelected || fileMatch ? 4 : 2}px ${c.proposed ? 'dashed' : 'solid'} ${borderColor}`,
136
145
  boxShadow: fileMatch
137
146
  ? `0 1px 4px rgba(0,0,0,0.25), 0 0 12px ${theme.colors.primary}55`
138
147
  : '0 1px 4px rgba(0,0,0,0.25)',
@@ -167,10 +176,8 @@ export function SubsystemComponentNode(props: NodeProps<Node<SubsystemGraphNodeD
167
176
  {constructBadgeLabel(c)}
168
177
  </div>
169
178
 
170
- {/* Role badge — top-right, only when the node carries a topology role.
171
- Role color (entry orange / service blue) on the badge; the node
172
- border stays construct-colored. */}
173
- {c.role != null && (
179
+ {/* Right badge — proposed wins over role when both are set. */}
180
+ {topRightLabel != null && topRightColor != null && (
174
181
  <div
175
182
  style={{
176
183
  position: 'absolute',
@@ -183,14 +190,14 @@ export function SubsystemComponentNode(props: NodeProps<Node<SubsystemGraphNodeD
183
190
  textTransform: 'uppercase',
184
191
  lineHeight: '17px',
185
192
  whiteSpace: 'nowrap',
186
- color: ROLE_COLOR[c.role],
193
+ color: topRightColor,
187
194
  background: theme.colors.backgroundSecondary ?? theme.colors.background,
188
- border: `1px solid ${ROLE_COLOR[c.role]}`,
195
+ border: `1px solid ${topRightColor}`,
189
196
  borderRadius: 4,
190
197
  padding: '0 5px',
191
198
  }}
192
199
  >
193
- {ROLE_LABEL[c.role]}
200
+ {topRightLabel}
194
201
  </div>
195
202
  )}
196
203