@principal-ai/principal-view-react 0.16.60 → 0.16.62

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 (44) hide show
  1. package/dist/index.d.ts +5 -4
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +2 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/pierre/PierreThroughlineCodeView.d.ts +19 -0
  6. package/dist/pierre/PierreThroughlineCodeView.d.ts.map +1 -0
  7. package/dist/pierre/PierreThroughlineCodeView.js +139 -0
  8. package/dist/pierre/PierreThroughlineCodeView.js.map +1 -0
  9. package/dist/pierre/constructColors.d.ts.map +1 -1
  10. package/dist/pierre/constructColors.js +1 -4
  11. package/dist/pierre/constructColors.js.map +1 -1
  12. package/dist/pierre/index.d.ts +2 -0
  13. package/dist/pierre/index.d.ts.map +1 -1
  14. package/dist/pierre/index.js +1 -0
  15. package/dist/pierre/index.js.map +1 -1
  16. package/dist/subsystem/FileDrawer.d.ts +8 -7
  17. package/dist/subsystem/FileDrawer.d.ts.map +1 -1
  18. package/dist/subsystem/FileDrawer.js +9 -9
  19. package/dist/subsystem/FileDrawer.js.map +1 -1
  20. package/dist/subsystem/SubsystemComponentGraph.d.ts +17 -5
  21. package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
  22. package/dist/subsystem/SubsystemComponentGraph.js +76 -21
  23. package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
  24. package/dist/subsystem/model.d.ts +64 -10
  25. package/dist/subsystem/model.d.ts.map +1 -1
  26. package/dist/subsystem/model.js +53 -8
  27. package/dist/subsystem/model.js.map +1 -1
  28. package/dist/subsystem/nodes.d.ts.map +1 -1
  29. package/dist/subsystem/nodes.js +16 -8
  30. package/dist/subsystem/nodes.js.map +1 -1
  31. package/package.json +3 -3
  32. package/src/index.ts +15 -2
  33. package/src/pierre/PierreThroughlineCodeView.tsx +210 -0
  34. package/src/pierre/constructColors.ts +1 -4
  35. package/src/pierre/index.ts +2 -0
  36. package/src/stories/Pierre/CodeView.stories.tsx +196 -0
  37. package/src/stories/Subsystem/ComponentGraph/Flows.stories.tsx +62 -4
  38. package/src/stories/Subsystem/ComponentGraph/FrameworkStereotype.stories.tsx +216 -0
  39. package/src/stories/Subsystem/ComponentGraph/Spotlights.stories.tsx +38 -3
  40. package/src/subsystem/FileDrawer.tsx +11 -10
  41. package/src/subsystem/SubsystemComponentGraph.tsx +121 -34
  42. package/src/subsystem/model.test.ts +44 -0
  43. package/src/subsystem/model.ts +105 -16
  44. package/src/subsystem/nodes.tsx +24 -12
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Demo: Pierre CodeView with snippets from different files.
3
+ *
4
+ * Throughlines want many short windows across files in one scroll —
5
+ * CodeView is the Pierre primitive for that (virtualized list of file items).
6
+ */
7
+ import React, { useMemo, useRef } from 'react';
8
+ import type { Meta, StoryObj } from '@storybook/react';
9
+ import {
10
+ CodeView,
11
+ type CodeViewHandle,
12
+ type CodeViewItem,
13
+ } from '@pierre/diffs/react';
14
+ import { ThemeProvider, defaultEditorTheme, useTheme } from '@principal-ade/industry-theme';
15
+ import { sliceSnippetWindow } from '../../pierre/sliceSnippet';
16
+ import { resolvePierreSyntaxThemeName } from '../../pierre/pierreSyntaxTheme';
17
+ import componentDeclarationSource from '../../subsystem/ComponentDeclaration.tsx?raw';
18
+ import resolveSource from '../../graphify/resolve.ts?raw';
19
+ import modelSource from '../../subsystem/model.ts?raw';
20
+
21
+ type SnippetSpec = {
22
+ id: string;
23
+ path: string;
24
+ contents: string;
25
+ startLine: number;
26
+ endLine: number;
27
+ focusLine?: number;
28
+ label: string;
29
+ };
30
+
31
+ const SNIPPETS: SnippetSpec[] = [
32
+ {
33
+ id: 'step-1',
34
+ path: 'packages/react/src/subsystem/ComponentDeclaration.tsx',
35
+ contents: componentDeclarationSource,
36
+ startLine: 252,
37
+ endLine: 270,
38
+ focusLine: 252,
39
+ label: '1 · ComponentDeclaration export',
40
+ },
41
+ {
42
+ id: 'step-2',
43
+ path: 'packages/react/src/graphify/resolve.ts',
44
+ contents: resolveSource,
45
+ startLine: 1,
46
+ endLine: 40,
47
+ focusLine: 1,
48
+ label: '2 · graphify resolve',
49
+ },
50
+ {
51
+ id: 'step-3',
52
+ path: 'packages/react/src/subsystem/model.ts',
53
+ contents: modelSource,
54
+ startLine: 216,
55
+ endLine: 250,
56
+ focusLine: 216,
57
+ label: '3 · Throughline types',
58
+ },
59
+ ];
60
+
61
+ const meta = {
62
+ title: 'Pierre/CodeView',
63
+ parameters: {
64
+ layout: 'fullscreen',
65
+ },
66
+ tags: ['autodocs'],
67
+ decorators: [
68
+ (Story) => (
69
+ <ThemeProvider theme={defaultEditorTheme}>
70
+ <Story />
71
+ </ThemeProvider>
72
+ ),
73
+ ],
74
+ } satisfies Meta;
75
+
76
+ export default meta;
77
+ type Story = StoryObj;
78
+
79
+ function MultiFileSnippetsDemo() {
80
+ const { theme, mode } = useTheme();
81
+ const viewRef = useRef<CodeViewHandle>(null);
82
+
83
+ const items = useMemo((): CodeViewItem[] => {
84
+ return SNIPPETS.map((spec) => {
85
+ const slice = sliceSnippetWindow(
86
+ spec.contents,
87
+ spec.startLine,
88
+ spec.endLine,
89
+ 2,
90
+ spec.focusLine ?? spec.startLine,
91
+ );
92
+ return {
93
+ id: spec.id,
94
+ type: 'file' as const,
95
+ file: {
96
+ name: spec.path,
97
+ contents: slice.contents,
98
+ cacheKey: `${spec.id}:${slice.sliceStart}-${slice.sliceEnd}`,
99
+ },
100
+ annotations:
101
+ slice.focusOffset != null
102
+ ? [{ lineNumber: slice.focusOffset }]
103
+ : undefined,
104
+ };
105
+ });
106
+ }, []);
107
+
108
+ const options = useMemo(
109
+ () => ({
110
+ theme: {
111
+ dark: resolvePierreSyntaxThemeName('dark'),
112
+ light: resolvePierreSyntaxThemeName('light'),
113
+ } as const,
114
+ stickyHeaders: true,
115
+ layout: { paddingTop: 12, paddingBottom: 24, gap: 16 },
116
+ ...(mode === 'light' || mode === 'dark' ? { themeType: mode as 'light' | 'dark' } : {}),
117
+ }),
118
+ [mode],
119
+ );
120
+
121
+ return (
122
+ <div
123
+ style={{
124
+ display: 'flex',
125
+ height: '100vh',
126
+ background: theme.colors.background,
127
+ color: theme.colors.text,
128
+ fontFamily: theme.fonts.sans,
129
+ }}
130
+ >
131
+ <aside
132
+ style={{
133
+ width: 260,
134
+ flexShrink: 0,
135
+ borderRight: `1px solid ${theme.colors.border ?? '#333'}`,
136
+ padding: 16,
137
+ display: 'flex',
138
+ flexDirection: 'column',
139
+ gap: 8,
140
+ }}
141
+ >
142
+ <div style={{ fontSize: 13, fontWeight: 600, marginBottom: 4 }}>
143
+ Throughline steps
144
+ </div>
145
+ <div style={{ fontSize: 12, color: theme.colors.textSecondary, marginBottom: 8 }}>
146
+ Pierre <code>CodeView</code> — one scroll, snippets from different files.
147
+ </div>
148
+ {SNIPPETS.map((spec) => (
149
+ <button
150
+ key={spec.id}
151
+ type="button"
152
+ onClick={() => viewRef.current?.scrollTo({ type: 'item', id: spec.id })}
153
+ style={{
154
+ textAlign: 'left',
155
+ padding: '8px 10px',
156
+ borderRadius: 6,
157
+ border: `1px solid ${theme.colors.border ?? '#333'}`,
158
+ background: theme.colors.surface ?? 'transparent',
159
+ color: theme.colors.text,
160
+ cursor: 'pointer',
161
+ fontSize: 12,
162
+ }}
163
+ >
164
+ {spec.label}
165
+ <div
166
+ style={{
167
+ marginTop: 4,
168
+ fontFamily: theme.fonts.monospace,
169
+ fontSize: 10,
170
+ color: theme.colors.textSecondary,
171
+ overflow: 'hidden',
172
+ textOverflow: 'ellipsis',
173
+ whiteSpace: 'nowrap',
174
+ }}
175
+ >
176
+ {spec.path.split('/').pop()}:{spec.startLine}
177
+ </div>
178
+ </button>
179
+ ))}
180
+ </aside>
181
+ <div style={{ flex: 1, minWidth: 0, minHeight: 0 }}>
182
+ <CodeView
183
+ ref={viewRef}
184
+ items={items}
185
+ options={options}
186
+ style={{ height: '100%', overflow: 'auto' }}
187
+ />
188
+ </div>
189
+ </div>
190
+ );
191
+ }
192
+
193
+ export const MultiFileSnippets: Story = {
194
+ name: 'Multi-file snippets',
195
+ render: () => <MultiFileSnippetsDemo />,
196
+ };
@@ -1,4 +1,4 @@
1
- import React from 'react';
1
+ import React, { useCallback } from 'react';
2
2
  import '@xyflow/react/dist/style.css';
3
3
  import type { Meta, StoryObj } from '@storybook/react';
4
4
  import { ThemeProvider, defaultEditorTheme } from '@principal-ade/industry-theme';
@@ -8,6 +8,8 @@ import type {
8
8
  SubsystemComponentEdge,
9
9
  SubsystemThroughline,
10
10
  } from '../../../subsystem/model';
11
+ import { PierreThroughlineCodeView } from '../../../pierre';
12
+ import type { ThroughlineViewerContext } from '../../../subsystem/SubsystemComponentGraph';
11
13
 
12
14
  const meta = {
13
15
  title: 'Subsystem/ComponentGraph/Flows',
@@ -32,10 +34,53 @@ type Story = StoryObj<typeof meta>;
32
34
  // Mirror of the retrofitted electron-app drawing graph: the sidebar's Files
33
35
  // panel swaps to a Flows panel listing three throughlines (open / save /
34
36
  // delete). Clicking a flow row toggles its steps; clicking a step focuses
35
- // that step's edge and frames it on the canvas. Other opened flows stay
36
- // dimmed; the rest of the graph is hidden.
37
+ // that step's edge, frames it on the canvas, and scrolls the bottom CodeView
38
+ // to that step's snippet. Other opened flows stay dimmed; the rest of the
39
+ // graph is hidden.
37
40
  // ---------------------------------------------------------------------------
38
41
 
42
+ /** Build enough placeholder lines so step `line` values land inside the file. */
43
+ function fakeSource(path: string, markedLines: number[]): string {
44
+ const maxLine = Math.max(80, ...markedLines);
45
+ const marks = new Set(markedLines);
46
+ const lines: string[] = [`// ${path}`, ''];
47
+ for (let i = 3; i <= maxLine; i++) {
48
+ if (marks.has(i)) {
49
+ lines.push(`export function stepAtLine${i}() {`);
50
+ lines.push(` return ${i};`);
51
+ lines.push(`}`);
52
+ lines.push('');
53
+ } else {
54
+ lines.push(`// context line ${i}`);
55
+ }
56
+ }
57
+ return lines.join('\n');
58
+ }
59
+
60
+ const storyFiles: Record<string, string> = {
61
+ 'src/panels/DrawingsLeftPanel.tsx': fakeSource('src/panels/DrawingsLeftPanel.tsx', [
62
+ 56, 85,
63
+ ]),
64
+ 'src/hooks/useDrawingsHost.ts': fakeSource('src/hooks/useDrawingsHost.ts', [
65
+ 45, 64, 66,
66
+ ]),
67
+ 'src/workspace/WorkspaceShell.tsx': fakeSource('src/workspace/WorkspaceShell.tsx', [
68
+ 369, 372,
69
+ ]),
70
+ 'src/storage/drawingsStorage.ts': fakeSource('src/storage/drawingsStorage.ts', [90]),
71
+ 'src/components/DrawingTabContent.tsx': fakeSource('src/components/DrawingTabContent.tsx', [
72
+ 83, 112, 121,
73
+ ]),
74
+ };
75
+
76
+ function readStoryFile(path: string): Promise<string> {
77
+ const content = storyFiles[path];
78
+ if (content == null) {
79
+ return Promise.reject(new Error(`No story fixture for ${path}`));
80
+ }
81
+ return Promise.resolve(content);
82
+ }
83
+
39
84
  const drawingComponents: SubsystemComponent[] = [
40
85
  {
41
86
  id: 'panel',
@@ -148,6 +193,18 @@ const drawingThroughlines: SubsystemThroughline[] = [
148
193
  ];
149
194
 
150
195
  function FlowsDemo() {
196
+ const renderThroughlineViewer = useCallback(
197
+ ({ throughline, stepIndex }: ThroughlineViewerContext) => (
198
+ <PierreThroughlineCodeView
199
+ throughline={throughline}
200
+ stepIndex={stepIndex}
201
+ readFile={readStoryFile}
202
+ contextLines={4}
203
+ />
204
+ ),
205
+ [],
206
+ );
207
+
151
208
  return (
152
209
  <div style={{ width: '100%', height: '100vh', display: 'flex', flexDirection: 'column' }}>
153
210
  <SubsystemComponentGraph
@@ -155,7 +212,8 @@ function FlowsDemo() {
155
212
  edges={drawingEdges}
156
213
  throughlines={drawingThroughlines}
157
214
  title="drawing-files flow"
158
- description="Three throughlines over one graph — opening, saving, and deleting a drawing. The sidebar's **Flows** panel lists each step by **symbol** (the frame at that hop); clicking a step focuses that edge on the canvas."
215
+ description="Three throughlines over one graph — opening, saving, and deleting a drawing. The sidebar's **Flows** panel lists each step by **symbol**; clicking a step focuses that edge and scrolls the bottom CodeView to that snippet."
216
+ renderThroughlineViewer={renderThroughlineViewer}
159
217
  renderFileViewer={(file, opts) => (
160
218
  <div
161
219
  style={{
@@ -0,0 +1,216 @@
1
+ import '@xyflow/react/dist/style.css';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { ThemeProvider, defaultEditorTheme } from '@principal-ade/industry-theme';
4
+ import { SubsystemComponentGraph } from '../../../subsystem/SubsystemComponentGraph';
5
+ import type { SubsystemComponent, SubsystemComponentEdge } from '../../../subsystem/model';
6
+
7
+ const meta = {
8
+ title: 'Subsystem/ComponentGraph/FrameworkStereotype',
9
+ component: SubsystemComponentGraph,
10
+ parameters: { layout: 'fullscreen' },
11
+ tags: ['autodocs'],
12
+ decorators: [
13
+ (Story) => (
14
+ <ThemeProvider theme={defaultEditorTheme}>
15
+ <Story />
16
+ </ThemeProvider>
17
+ ),
18
+ ],
19
+ } satisfies Meta<typeof SubsystemComponentGraph>;
20
+
21
+ export default meta;
22
+ type Story = StoryObj<typeof meta>;
23
+
24
+ const purl = 'pkg:github/principal-ai/principal-view-core-library';
25
+
26
+ /**
27
+ * Same language construct (`function`) with and without framework stereotypes.
28
+ * Badge text prefers `framework · stereotype` so UI units read as components /
29
+ * hooks instead of plain functions; construct color stays function-indigo.
30
+ */
31
+ const reactUiComponents: SubsystemComponent[] = [
32
+ {
33
+ id: 'create-graph',
34
+ name: 'createSubsystemGraph',
35
+ construct: 'function',
36
+ file: 'packages/trail-viewer/src/bun/subsystem-graph-store.ts',
37
+ purl,
38
+ symbol: 'createSubsystemGraph',
39
+ purpose: 'language-only helper — no framework / stereotype',
40
+ process: 'trail-viewer/host',
41
+ layer: 1,
42
+ },
43
+ {
44
+ id: 'http-entry',
45
+ name: 'handleSubsystemGraphRequest',
46
+ construct: 'function',
47
+ file: 'packages/trail-viewer/src/bun/http-server.ts',
48
+ purl,
49
+ symbol: 'handleSubsystemGraphRequest',
50
+ purpose: 'HTTP bridge entry — topology role, still no React stereotype',
51
+ role: 'entry',
52
+ process: 'trail-viewer/host',
53
+ layer: 1,
54
+ },
55
+ {
56
+ id: 'sessions-view',
57
+ name: 'AgentSessionsOverviewView',
58
+ construct: 'function',
59
+ file: 'packages/trail-viewer/src/mainview/views/AgentSessions.tsx',
60
+ purl,
61
+ symbol: 'AgentSessionsOverviewView',
62
+ purpose: 'React view — construct stays function; stereotype labels it',
63
+ framework: 'react',
64
+ stereotype: 'component',
65
+ process: 'trail-viewer/renderer',
66
+ layer: 2,
67
+ },
68
+ {
69
+ id: 'analysis-view',
70
+ name: 'AnalysisView',
71
+ construct: 'function',
72
+ file: 'packages/trail-viewer/src/mainview/views/AnalysisView.tsx',
73
+ purl,
74
+ symbol: 'AnalysisView',
75
+ purpose: 'another React component alongside a hook',
76
+ framework: 'react',
77
+ stereotype: 'component',
78
+ process: 'trail-viewer/renderer',
79
+ layer: 2,
80
+ },
81
+ {
82
+ id: 'drawings-host',
83
+ name: 'useDrawingsHost',
84
+ construct: 'function',
85
+ file: 'packages/trail-viewer/src/mainview/hooks/useDrawingsHost.ts',
86
+ purl,
87
+ symbol: 'useDrawingsHost',
88
+ purpose: 'React hook — same construct, different stereotype',
89
+ framework: 'react',
90
+ stereotype: 'hook',
91
+ process: 'trail-viewer/renderer',
92
+ layer: 2,
93
+ },
94
+ {
95
+ id: 'graph-doc-type',
96
+ name: 'SubsystemGraphDocument',
97
+ construct: 'type_alias',
98
+ file: 'packages/core/src/types/subsystem-graph.ts',
99
+ purl,
100
+ symbol: 'SubsystemGraphDocument',
101
+ purpose: 'type-only — framework / stereotype stay empty',
102
+ layer: 3,
103
+ },
104
+ ];
105
+
106
+ const reactUiEdges: SubsystemComponentEdge[] = [
107
+ {
108
+ id: 'http-to-create',
109
+ from: 'http-entry',
110
+ to: 'create-graph',
111
+ mechanism: 'calls',
112
+ },
113
+ {
114
+ id: 'sessions-uses-hook',
115
+ from: 'sessions-view',
116
+ to: 'drawings-host',
117
+ mechanism: 'uses',
118
+ },
119
+ {
120
+ id: 'analysis-uses-type',
121
+ from: 'analysis-view',
122
+ to: 'graph-doc-type',
123
+ mechanism: 'references',
124
+ },
125
+ {
126
+ id: 'sessions-to-analysis',
127
+ from: 'sessions-view',
128
+ to: 'analysis-view',
129
+ mechanism: 'feeds',
130
+ },
131
+ ];
132
+
133
+ export const ReactComponentsAndHooks: Story = {
134
+ render: () => (
135
+ <div style={{ width: '100%', height: '100vh' }}>
136
+ <SubsystemComponentGraph
137
+ title="Framework + stereotype"
138
+ description="construct stays language-shaped (function / type_alias). framework + stereotype label React units as component / hook without inventing a react_component construct. Empty fields mean language-only."
139
+ components={reactUiComponents}
140
+ edges={reactUiEdges}
141
+ />
142
+ </div>
143
+ ),
144
+ };
145
+
146
+ /** Nest-style stereotypes on the same ontology — controller / middleware / guard. */
147
+ const nestComponents: SubsystemComponent[] = [
148
+ {
149
+ id: 'graphs-controller',
150
+ name: 'SubsystemGraphsController',
151
+ construct: 'class',
152
+ file: 'src/graphs/graphs.controller.ts',
153
+ purl: 'pkg:github/acme/api',
154
+ symbol: 'SubsystemGraphsController',
155
+ purpose: 'Nest HTTP controller',
156
+ framework: 'nestjs',
157
+ stereotype: 'controller',
158
+ role: 'entry',
159
+ layer: 1,
160
+ },
161
+ {
162
+ id: 'auth-guard',
163
+ name: 'AuthGuard',
164
+ construct: 'class',
165
+ file: 'src/auth/auth.guard.ts',
166
+ purl: 'pkg:github/acme/api',
167
+ symbol: 'AuthGuard',
168
+ purpose: 'request guard',
169
+ framework: 'nestjs',
170
+ stereotype: 'guard',
171
+ layer: 1,
172
+ },
173
+ {
174
+ id: 'logging-mw',
175
+ name: 'LoggingMiddleware',
176
+ construct: 'function',
177
+ file: 'src/logging/logging.middleware.ts',
178
+ purl: 'pkg:github/acme/api',
179
+ symbol: 'LoggingMiddleware',
180
+ purpose: 'Express-style middleware function under Nest',
181
+ framework: 'nestjs',
182
+ stereotype: 'middleware',
183
+ layer: 1,
184
+ },
185
+ {
186
+ id: 'graphs-service',
187
+ name: 'GraphsService',
188
+ construct: 'class',
189
+ file: 'src/graphs/graphs.service.ts',
190
+ purl: 'pkg:github/acme/api',
191
+ symbol: 'GraphsService',
192
+ purpose: 'injectable service — stereotype without UI',
193
+ framework: 'nestjs',
194
+ stereotype: 'injectable',
195
+ layer: 2,
196
+ },
197
+ ];
198
+
199
+ const nestEdges: SubsystemComponentEdge[] = [
200
+ { id: 'mw-to-guard', from: 'logging-mw', to: 'auth-guard', mechanism: 'uses' },
201
+ { id: 'guard-to-ctrl', from: 'auth-guard', to: 'graphs-controller', mechanism: 'uses' },
202
+ { id: 'ctrl-to-svc', from: 'graphs-controller', to: 'graphs-service', mechanism: 'calls' },
203
+ ];
204
+
205
+ export const NestControllerStack: Story = {
206
+ render: () => (
207
+ <div style={{ width: '100%', height: '100vh' }}>
208
+ <SubsystemComponentGraph
209
+ title="Nest framework stereotypes"
210
+ description="Same optional fields work outside React: class/function constructs plus nestjs controller / guard / middleware / injectable stereotypes."
211
+ components={nestComponents}
212
+ edges={nestEdges}
213
+ />
214
+ </div>
215
+ ),
216
+ };
@@ -38,8 +38,8 @@ type Story = StoryObj<typeof meta>;
38
38
  // Targeted side-by-side spotlights: each construct's DATA next to its
39
39
  // RENDERING — no full graph shell. Three spotlights: nodes, mechanisms
40
40
  // (edges), and anatomy (declaration panels). The mapping these make visible:
41
- // construct:→ node anatomy + color, roletopology glyph, detail → drill-down,
42
- // mechanism → edge color/style.
41
+ // construct:→ node anatomy + color, framework+stereotypebadge label,
42
+ // role → topology glyph, detail → drill-down, mechanism → edge color/style.
43
43
  // ---------------------------------------------------------------------------
44
44
 
45
45
  /** Data snippet card — the raw object on the left of every row. */
@@ -209,6 +209,35 @@ const nodeSpotlights: Array<{ label: string; component: SubsystemComponent; note
209
209
  symbol: 'createSubsystemGraph',
210
210
  },
211
211
  },
212
+ {
213
+ label: 'framework: react · stereotype: component — still construct: function',
214
+ note: 'badge reads "react · component"; name wears <> ; color stays function',
215
+ component: {
216
+ id: 'analysis-view',
217
+ name: 'AnalysisView',
218
+ construct: 'function',
219
+ file: 'packages/trail-viewer/src/mainview/views/AnalysisView.tsx',
220
+ purl: corePurl,
221
+ symbol: 'AnalysisView',
222
+ framework: 'react',
223
+ stereotype: 'component',
224
+ purpose: 'renders a saved analysis as concept cards',
225
+ },
226
+ },
227
+ {
228
+ label: 'framework: react · stereotype: hook',
229
+ note: 'same construct, different stereotype — badge "react · hook", keeps ()',
230
+ component: {
231
+ id: 'drawings-host',
232
+ name: 'useDrawingsHost',
233
+ construct: 'function',
234
+ file: 'packages/trail-viewer/src/mainview/hooks/useDrawingsHost.ts',
235
+ purl: corePurl,
236
+ symbol: 'useDrawingsHost',
237
+ framework: 'react',
238
+ stereotype: 'hook',
239
+ },
240
+ },
212
241
  {
213
242
  label: 'role: entry on construct: function — boundary element',
214
243
  note: 'orange overrides construct:color; anatomy inherited from the function',
@@ -331,7 +360,13 @@ function NodeSpotlightsDemo() {
331
360
  {nodeSpotlights.map(({ component }) => {
332
361
  const on = activeIds.has(component.id);
333
362
  const color = constructColors[component.construct];
334
- const chip = component.role != null ? `${component.construct} · ${component.role}` : component.construct;
363
+ const chip = component.stereotype
364
+ ? component.framework
365
+ ? `${component.framework} · ${component.stereotype}`
366
+ : component.stereotype
367
+ : component.role != null
368
+ ? `${component.construct} · ${component.role}`
369
+ : component.construct;
335
370
  return (
336
371
  <button
337
372
  key={component.id}
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * FileDrawer — bottom panel that slides up from the bottom of the graph area
3
- * to show a file's contents. Opened by sidebar file-tree clicks (and any host
4
- * wiring); content is injected as children by the graph component.
3
+ * to show file / throughline code. Opened by sidebar file-tree clicks,
4
+ * declaration links, and throughline step focus; content is injected as
5
+ * children by the graph component.
5
6
  */
6
7
 
7
8
  import { useEffect } from 'react';
@@ -9,22 +10,22 @@ import type { ReactNode } from 'react';
9
10
  import { useTheme } from '@principal-ade/industry-theme';
10
11
  import { X } from 'lucide-react';
11
12
 
12
- /** Bottom panel that slides up from the bottom of the graph area to show a
13
- * file's contents opened by node clicks and sidebar file-tree clicks
14
- * alike. Sits in normal flow (canvas shrinks while open, nothing covered)
13
+ /** Bottom panel that slides up from the bottom of the graph area.
14
+ * Sits in normal flow (canvas shrinks while open, nothing covered)
15
15
  * and animates via height; stays mounted so open/close animates. */
16
16
  export function FileDrawer({
17
- file,
17
+ title,
18
18
  onClose,
19
19
  children,
20
20
  }: {
21
- file: string | null;
21
+ /** Drawer chrome title; `null` closes the drawer. */
22
+ title: string | null;
22
23
  onClose: () => void;
23
24
  children?: ReactNode;
24
25
  }) {
25
26
  const { theme } = useTheme();
26
27
  const muted = theme.colors.textMuted ?? theme.colors.textSecondary;
27
- const open = file !== null;
28
+ const open = title !== null;
28
29
 
29
30
  useEffect(() => {
30
31
  if (!open) return;
@@ -64,7 +65,7 @@ export function FileDrawer({
64
65
  }}
65
66
  >
66
67
  <span
67
- title={file ?? undefined}
68
+ title={title ?? undefined}
68
69
  style={{
69
70
  flex: 1,
70
71
  minWidth: 0,
@@ -76,7 +77,7 @@ export function FileDrawer({
76
77
  color: muted,
77
78
  }}
78
79
  >
79
- {file}
80
+ {title}
80
81
  </span>
81
82
  <button
82
83
  type="button"