@principal-ai/subsystems-react 0.49.0 → 0.50.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.
- package/dist/pierre/PierreFileView.d.ts +1 -1
- package/dist/pierre/PierreFileView.d.ts.map +1 -1
- package/dist/pierre/PierreFileView.js +2 -1
- package/dist/pierre/PierreFileView.js.map +1 -1
- package/dist/pierre/PierreSnippetView.d.ts +1 -1
- package/dist/pierre/PierreSnippetView.d.ts.map +1 -1
- package/dist/pierre/PierreSnippetView.js +2 -1
- package/dist/pierre/PierreSnippetView.js.map +1 -1
- package/dist/pierre/PierreTrailCodeView.d.ts +7 -1
- package/dist/pierre/PierreTrailCodeView.d.ts.map +1 -1
- package/dist/pierre/PierreTrailCodeView.js +71 -18
- package/dist/pierre/PierreTrailCodeView.js.map +1 -1
- package/dist/pierre/pierreBackground.d.ts +36 -2
- package/dist/pierre/pierreBackground.d.ts.map +1 -1
- package/dist/pierre/pierreBackground.js +40 -13
- package/dist/pierre/pierreBackground.js.map +1 -1
- package/dist/pierre/visibleStep.d.ts +48 -0
- package/dist/pierre/visibleStep.d.ts.map +1 -0
- package/dist/pierre/visibleStep.js +53 -0
- package/dist/pierre/visibleStep.js.map +1 -0
- package/dist/subsystem/FileDrawer.d.ts +10 -1
- package/dist/subsystem/FileDrawer.d.ts.map +1 -1
- package/dist/subsystem/FileDrawer.js +61 -40
- package/dist/subsystem/FileDrawer.js.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts +6 -0
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +21 -10
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/package.json +1 -1
- package/src/pierre/PierreFileView.tsx +2 -1
- package/src/pierre/PierreSnippetView.tsx +2 -1
- package/src/pierre/PierreTrailCodeView.tsx +81 -22
- package/src/pierre/pierreBackground.ts +62 -12
- package/src/pierre/visibleStep.test.ts +108 -0
- package/src/pierre/visibleStep.ts +76 -0
- package/src/stories/Subsystem/ComponentGraph/Flows.stories.tsx +6 -0
- package/src/subsystem/FileDrawer.tsx +112 -63
- package/src/subsystem/SubsystemComponentGraph.tsx +29 -5
|
@@ -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
|
-
|
|
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
|
-
<
|
|
116
|
-
title={title ?? undefined}
|
|
120
|
+
<div
|
|
117
121
|
style={{
|
|
118
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
<
|
|
177
|
-
|
|
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
|
-
|
|
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
|