@principal-ai/file-city-react 0.5.44 → 0.5.45
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/components/FileCity3D/FileCity3D.d.ts +7 -0
- package/dist/components/FileCity3D/FileCity3D.d.ts.map +1 -1
- package/dist/components/FileCity3D/FileCity3D.js +41 -1
- package/dist/components/FileCity3D/index.d.ts +1 -1
- package/dist/components/FileCity3D/index.d.ts.map +1 -1
- package/dist/components/FileCity3D/index.js +1 -1
- package/package.json +2 -1
- package/src/components/FileCity3D/FileCity3D.tsx +52 -1
- package/src/components/FileCity3D/index.ts +2 -0
- package/src/stories/CameraOffsetForOverlays.stories.tsx +470 -0
- package/src/stories/SequenceDiagramOverlay.stories.tsx +427 -0
- package/src/stories/WorkflowSequenceDiagramPrototype.stories.tsx +754 -0
|
@@ -0,0 +1,754 @@
|
|
|
1
|
+
import { useMemo, useState } from 'react';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
|
+
|
|
4
|
+
import '@xyflow/react/dist/style.css';
|
|
5
|
+
import { WorkflowSequenceDiagram } from '@principal-ai/principal-view-react';
|
|
6
|
+
import { getEventTemplateString } from '@principal-ai/principal-view-core';
|
|
7
|
+
import type {
|
|
8
|
+
ExtendedCanvas,
|
|
9
|
+
ExtendedCanvasNode,
|
|
10
|
+
WorkflowScenario,
|
|
11
|
+
} from '@principal-ai/principal-view-core';
|
|
12
|
+
|
|
13
|
+
import { FileCity3D } from '../components/FileCity3D';
|
|
14
|
+
import type { CityData, HighlightLayer } from '../components/FileCity3D';
|
|
15
|
+
import authServerCityData from '../../../../assets/auth-server-city-data.json';
|
|
16
|
+
|
|
17
|
+
const meta = {
|
|
18
|
+
title: 'Prototypes/Workflow Sequence Diagram (3D Overlay)',
|
|
19
|
+
parameters: { layout: 'fullscreen' },
|
|
20
|
+
} satisfies Meta;
|
|
21
|
+
|
|
22
|
+
export default meta;
|
|
23
|
+
|
|
24
|
+
const HAPPY_PATH: WorkflowScenario = {
|
|
25
|
+
id: 'authenticated',
|
|
26
|
+
priority: 1,
|
|
27
|
+
description: 'User authenticates via WorkOS and a CLI room token is minted',
|
|
28
|
+
template: {
|
|
29
|
+
summary: 'Successful auth for {{user.login}}',
|
|
30
|
+
events: {
|
|
31
|
+
'request.middleware.intercepted': '{{http.method}} {{http.route}}',
|
|
32
|
+
'workos.start.requested': 'Redirecting to WorkOS',
|
|
33
|
+
'workos.callback.received': 'Callback received (code: {{code}})',
|
|
34
|
+
'workos.token.exchanged':
|
|
35
|
+
'Exchanged code for token (status: {{response.status}})',
|
|
36
|
+
'session.created': 'Session minted for {{user.login}}',
|
|
37
|
+
'org.membership.checked': 'Org membership verified ({{org}})',
|
|
38
|
+
'cli.room_token.minted': 'CLI room token issued',
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const UNAUTHENTICATED: WorkflowScenario = {
|
|
44
|
+
id: 'unauthenticated',
|
|
45
|
+
priority: 2,
|
|
46
|
+
description: 'No valid session — middleware short-circuits the request',
|
|
47
|
+
template: {
|
|
48
|
+
summary: 'Not authenticated: {{reason}}',
|
|
49
|
+
events: {
|
|
50
|
+
'request.middleware.intercepted': '{{http.method}} {{http.route}}',
|
|
51
|
+
'session.lookup': 'Looking up session (has_cookie: {{has_cookie}})',
|
|
52
|
+
'session.unauthenticated': 'No valid session: {{reason}}',
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const ERROR_PATH: WorkflowScenario = {
|
|
58
|
+
id: 'error',
|
|
59
|
+
priority: 3,
|
|
60
|
+
description: 'WorkOS rejected the callback code',
|
|
61
|
+
template: {
|
|
62
|
+
summary: 'Auth failed: {{error.message}}',
|
|
63
|
+
events: {
|
|
64
|
+
'request.middleware.intercepted': '{{http.method}} {{http.route}}',
|
|
65
|
+
'workos.start.requested': 'Redirecting to WorkOS',
|
|
66
|
+
'workos.callback.received': 'Callback received (code: {{code}})',
|
|
67
|
+
'workos.token.exchanged':
|
|
68
|
+
'Token exchange failed (status: {{response.status}})',
|
|
69
|
+
'workos.error': '{{error.type}}: {{error.message}}',
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const SCENARIOS: WorkflowScenario[] = [
|
|
75
|
+
HAPPY_PATH,
|
|
76
|
+
UNAUTHENTICATED,
|
|
77
|
+
ERROR_PATH,
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Inline OTEL canvas covering every event referenced by the scenarios above.
|
|
82
|
+
* The diagram uses `otel.scope` to assign swimlanes; on selection we read
|
|
83
|
+
* `otel.files` (primary instrumentation site) and `otel.references` (related
|
|
84
|
+
* code) to drive two distinctly-colored highlight layers.
|
|
85
|
+
*/
|
|
86
|
+
const eventNode = (
|
|
87
|
+
id: string,
|
|
88
|
+
label: string,
|
|
89
|
+
eventName: string,
|
|
90
|
+
scope: string,
|
|
91
|
+
files: string[],
|
|
92
|
+
references: string[] = [],
|
|
93
|
+
): ExtendedCanvasNode =>
|
|
94
|
+
({
|
|
95
|
+
id,
|
|
96
|
+
type: 'otel-event',
|
|
97
|
+
x: 0,
|
|
98
|
+
y: 0,
|
|
99
|
+
width: 220,
|
|
100
|
+
height: 80,
|
|
101
|
+
label,
|
|
102
|
+
event: { name: eventName },
|
|
103
|
+
otel: { scope, files, references, status: 'implemented' },
|
|
104
|
+
}) as ExtendedCanvasNode;
|
|
105
|
+
|
|
106
|
+
const AUTH_CANVAS: ExtendedCanvas = {
|
|
107
|
+
name: 'Auth Server (prototype)',
|
|
108
|
+
nodes: [
|
|
109
|
+
eventNode(
|
|
110
|
+
'middleware-intercept',
|
|
111
|
+
'Middleware Intercepts',
|
|
112
|
+
'request.middleware.intercepted',
|
|
113
|
+
'middleware',
|
|
114
|
+
['auth-server/src/middleware.ts'],
|
|
115
|
+
['auth-server/src/lib/auth-session-manager.ts'],
|
|
116
|
+
),
|
|
117
|
+
eventNode(
|
|
118
|
+
'workos-start',
|
|
119
|
+
'Start WorkOS Auth',
|
|
120
|
+
'workos.start.requested',
|
|
121
|
+
'workos',
|
|
122
|
+
['auth-server/src/app/api/auth/workos/start/route.ts'],
|
|
123
|
+
),
|
|
124
|
+
eventNode(
|
|
125
|
+
'workos-callback',
|
|
126
|
+
'Callback Received',
|
|
127
|
+
'workos.callback.received',
|
|
128
|
+
'workos',
|
|
129
|
+
['auth-server/src/app/api/auth/workos/callback/route.ts'],
|
|
130
|
+
),
|
|
131
|
+
eventNode(
|
|
132
|
+
'workos-token',
|
|
133
|
+
'Exchange Code → Token',
|
|
134
|
+
'workos.token.exchanged',
|
|
135
|
+
'workos',
|
|
136
|
+
['auth-server/src/app/api/auth/workos/token/route.ts'],
|
|
137
|
+
['auth-server/src/lib/token-store.ts'],
|
|
138
|
+
),
|
|
139
|
+
eventNode(
|
|
140
|
+
'workos-error',
|
|
141
|
+
'WorkOS Error',
|
|
142
|
+
'workos.error',
|
|
143
|
+
'workos',
|
|
144
|
+
['auth-server/src/app/api/auth/workos/callback/route.ts'],
|
|
145
|
+
),
|
|
146
|
+
eventNode(
|
|
147
|
+
'session-lookup',
|
|
148
|
+
'Look Up Session',
|
|
149
|
+
'session.lookup',
|
|
150
|
+
'session',
|
|
151
|
+
['auth-server/src/lib/auth-session-manager.ts'],
|
|
152
|
+
['auth-server/src/middleware.ts'],
|
|
153
|
+
),
|
|
154
|
+
eventNode(
|
|
155
|
+
'session-unauth',
|
|
156
|
+
'No Session',
|
|
157
|
+
'session.unauthenticated',
|
|
158
|
+
'session',
|
|
159
|
+
['auth-server/src/middleware.ts'],
|
|
160
|
+
),
|
|
161
|
+
eventNode(
|
|
162
|
+
'session-created',
|
|
163
|
+
'Create Session',
|
|
164
|
+
'session.created',
|
|
165
|
+
'session',
|
|
166
|
+
['auth-server/src/lib/auth-session-manager.ts'],
|
|
167
|
+
['auth-server/src/lib/token-store.ts'],
|
|
168
|
+
),
|
|
169
|
+
eventNode(
|
|
170
|
+
'org-membership',
|
|
171
|
+
'Check Org Membership',
|
|
172
|
+
'org.membership.checked',
|
|
173
|
+
'session',
|
|
174
|
+
['auth-server/src/lib/org-membership.ts'],
|
|
175
|
+
),
|
|
176
|
+
eventNode(
|
|
177
|
+
'cli-room-token',
|
|
178
|
+
'Mint CLI Room Token',
|
|
179
|
+
'cli.room_token.minted',
|
|
180
|
+
'cli',
|
|
181
|
+
['auth-server/src/app/api/auth/cli/room-token/route.ts'],
|
|
182
|
+
[
|
|
183
|
+
'auth-server/src/lib/token-store.ts',
|
|
184
|
+
'auth-server/src/lib/auth-session-manager.ts',
|
|
185
|
+
],
|
|
186
|
+
),
|
|
187
|
+
],
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const FILE_HIGHLIGHT_COLOR = '#22d3ee'; // cyan — primary instrumentation site
|
|
191
|
+
const REFERENCE_HIGHLIGHT_COLOR = '#a78bfa'; // violet — auxiliary references
|
|
192
|
+
|
|
193
|
+
function PathList({
|
|
194
|
+
label,
|
|
195
|
+
color,
|
|
196
|
+
paths,
|
|
197
|
+
}: {
|
|
198
|
+
label: string;
|
|
199
|
+
color: string;
|
|
200
|
+
paths: string[];
|
|
201
|
+
}) {
|
|
202
|
+
return (
|
|
203
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
|
204
|
+
<div
|
|
205
|
+
style={{
|
|
206
|
+
display: 'flex',
|
|
207
|
+
alignItems: 'center',
|
|
208
|
+
gap: 6,
|
|
209
|
+
fontSize: 10,
|
|
210
|
+
textTransform: 'uppercase',
|
|
211
|
+
letterSpacing: 1.2,
|
|
212
|
+
color: '#9ca3af',
|
|
213
|
+
}}
|
|
214
|
+
>
|
|
215
|
+
<span
|
|
216
|
+
style={{
|
|
217
|
+
width: 8,
|
|
218
|
+
height: 8,
|
|
219
|
+
borderRadius: 2,
|
|
220
|
+
backgroundColor: color,
|
|
221
|
+
display: 'inline-block',
|
|
222
|
+
}}
|
|
223
|
+
/>
|
|
224
|
+
<span>{label}</span>
|
|
225
|
+
</div>
|
|
226
|
+
{paths.map(path => (
|
|
227
|
+
<code
|
|
228
|
+
key={path}
|
|
229
|
+
style={{
|
|
230
|
+
fontSize: 11,
|
|
231
|
+
color: '#e5e7eb',
|
|
232
|
+
fontFamily:
|
|
233
|
+
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
|
|
234
|
+
wordBreak: 'break-all',
|
|
235
|
+
}}
|
|
236
|
+
>
|
|
237
|
+
{path}
|
|
238
|
+
</code>
|
|
239
|
+
))}
|
|
240
|
+
</div>
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const FONT =
|
|
245
|
+
'ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif';
|
|
246
|
+
|
|
247
|
+
const OVERLAY_HEIGHT_PCT = 50;
|
|
248
|
+
const COLLAPSE_MS = 320;
|
|
249
|
+
|
|
250
|
+
type Story = StoryObj;
|
|
251
|
+
|
|
252
|
+
export const Default: Story = {
|
|
253
|
+
render: () => {
|
|
254
|
+
const cityData = useMemo(() => authServerCityData as CityData, []);
|
|
255
|
+
|
|
256
|
+
const [scenarioId, setScenarioId] = useState(SCENARIOS[0].id);
|
|
257
|
+
const scenario = useMemo(
|
|
258
|
+
() => SCENARIOS.find(s => s.id === scenarioId) ?? SCENARIOS[0],
|
|
259
|
+
[scenarioId],
|
|
260
|
+
);
|
|
261
|
+
const [selectedEventIndex, setSelectedEventIndex] = useState<
|
|
262
|
+
number | undefined
|
|
263
|
+
>(undefined);
|
|
264
|
+
|
|
265
|
+
const [opacity, setOpacity] = useState(1);
|
|
266
|
+
const [interactive, setInteractive] = useState(true);
|
|
267
|
+
const [collapsed, setCollapsed] = useState(false);
|
|
268
|
+
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
269
|
+
|
|
270
|
+
const eventNameToPaths = useMemo(() => {
|
|
271
|
+
const m = new Map<string, { files: string[]; references: string[] }>();
|
|
272
|
+
for (const node of AUTH_CANVAS.nodes ?? []) {
|
|
273
|
+
if (node.type !== 'otel-event') continue;
|
|
274
|
+
const name = node.event?.name;
|
|
275
|
+
if (!name) continue;
|
|
276
|
+
m.set(name, {
|
|
277
|
+
files: node.otel?.files ?? [],
|
|
278
|
+
references: node.otel?.references ?? [],
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
return m;
|
|
282
|
+
}, []);
|
|
283
|
+
|
|
284
|
+
const eventNames = useMemo(
|
|
285
|
+
() => Object.keys(scenario.template.events ?? {}),
|
|
286
|
+
[scenario],
|
|
287
|
+
);
|
|
288
|
+
const selectedEventName =
|
|
289
|
+
selectedEventIndex !== undefined
|
|
290
|
+
? eventNames[selectedEventIndex]
|
|
291
|
+
: undefined;
|
|
292
|
+
const selectedEventEntry =
|
|
293
|
+
selectedEventName !== undefined
|
|
294
|
+
? scenario.template.events?.[selectedEventName]
|
|
295
|
+
: undefined;
|
|
296
|
+
const selectedEventTemplate = selectedEventEntry
|
|
297
|
+
? getEventTemplateString(selectedEventEntry)
|
|
298
|
+
: undefined;
|
|
299
|
+
const selectedPaths =
|
|
300
|
+
selectedEventName !== undefined
|
|
301
|
+
? (eventNameToPaths.get(selectedEventName) ?? {
|
|
302
|
+
files: [],
|
|
303
|
+
references: [],
|
|
304
|
+
})
|
|
305
|
+
: { files: [], references: [] };
|
|
306
|
+
|
|
307
|
+
const highlightLayers = useMemo<HighlightLayer[]>(() => {
|
|
308
|
+
const layers: HighlightLayer[] = [];
|
|
309
|
+
// References render first / underneath so primary files stay visually dominant.
|
|
310
|
+
if (selectedPaths.references.length > 0) {
|
|
311
|
+
layers.push({
|
|
312
|
+
id: 'workflow-selection-references',
|
|
313
|
+
name: 'Workflow References',
|
|
314
|
+
enabled: true,
|
|
315
|
+
color: REFERENCE_HIGHLIGHT_COLOR,
|
|
316
|
+
opacity: 0.7,
|
|
317
|
+
borderWidth: 3,
|
|
318
|
+
priority: 90,
|
|
319
|
+
items: selectedPaths.references.map(path => ({
|
|
320
|
+
path,
|
|
321
|
+
type: 'file' as const,
|
|
322
|
+
renderStrategy: 'fill' as const,
|
|
323
|
+
})),
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
if (selectedPaths.files.length > 0) {
|
|
327
|
+
layers.push({
|
|
328
|
+
id: 'workflow-selection-files',
|
|
329
|
+
name: 'Workflow Files',
|
|
330
|
+
enabled: true,
|
|
331
|
+
color: FILE_HIGHLIGHT_COLOR,
|
|
332
|
+
opacity: 0.9,
|
|
333
|
+
borderWidth: 4,
|
|
334
|
+
priority: 100,
|
|
335
|
+
items: selectedPaths.files.map(path => ({
|
|
336
|
+
path,
|
|
337
|
+
type: 'file' as const,
|
|
338
|
+
renderStrategy: 'fill' as const,
|
|
339
|
+
})),
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
return layers;
|
|
343
|
+
}, [selectedPaths]);
|
|
344
|
+
|
|
345
|
+
return (
|
|
346
|
+
<div
|
|
347
|
+
style={{
|
|
348
|
+
position: 'fixed',
|
|
349
|
+
inset: 0,
|
|
350
|
+
backgroundColor: '#0b0f14',
|
|
351
|
+
overflow: 'hidden',
|
|
352
|
+
}}
|
|
353
|
+
>
|
|
354
|
+
{/* 2D city background (flat mode) */}
|
|
355
|
+
<div style={{ position: 'absolute', inset: 0 }}>
|
|
356
|
+
<FileCity3D
|
|
357
|
+
cityData={cityData}
|
|
358
|
+
width="100%"
|
|
359
|
+
height="100%"
|
|
360
|
+
isGrown={false}
|
|
361
|
+
backgroundColor="#0b0f14"
|
|
362
|
+
highlightLayers={highlightLayers}
|
|
363
|
+
/>
|
|
364
|
+
</div>
|
|
365
|
+
|
|
366
|
+
{/* Semi-transparent workflow diagram overlay — bottom half, collapsible */}
|
|
367
|
+
<div
|
|
368
|
+
style={{
|
|
369
|
+
position: 'absolute',
|
|
370
|
+
bottom: 0,
|
|
371
|
+
left: 0,
|
|
372
|
+
right: 0,
|
|
373
|
+
height: `${OVERLAY_HEIGHT_PCT}vh`,
|
|
374
|
+
transform: collapsed
|
|
375
|
+
? 'translateY(calc(100% - 36px))'
|
|
376
|
+
: 'translateY(0)',
|
|
377
|
+
transition: `transform ${COLLAPSE_MS}ms cubic-bezier(0.32, 0.72, 0, 1)`,
|
|
378
|
+
pointerEvents: 'none',
|
|
379
|
+
zIndex: 30,
|
|
380
|
+
}}
|
|
381
|
+
>
|
|
382
|
+
{/* Drawer handle / collapse toggle pinned to the top edge */}
|
|
383
|
+
<div
|
|
384
|
+
style={{
|
|
385
|
+
position: 'absolute',
|
|
386
|
+
left: 0,
|
|
387
|
+
right: 0,
|
|
388
|
+
top: 0,
|
|
389
|
+
height: 36,
|
|
390
|
+
display: 'flex',
|
|
391
|
+
alignItems: 'center',
|
|
392
|
+
justifyContent: 'flex-end',
|
|
393
|
+
paddingRight: 16,
|
|
394
|
+
pointerEvents: 'auto',
|
|
395
|
+
}}
|
|
396
|
+
>
|
|
397
|
+
<button
|
|
398
|
+
onClick={() => setCollapsed(c => !c)}
|
|
399
|
+
style={{
|
|
400
|
+
display: 'flex',
|
|
401
|
+
alignItems: 'center',
|
|
402
|
+
gap: 8,
|
|
403
|
+
padding: '6px 14px',
|
|
404
|
+
backgroundColor: 'rgba(17, 24, 39, 0.92)',
|
|
405
|
+
border: '1px solid #374151',
|
|
406
|
+
borderRadius: 999,
|
|
407
|
+
color: '#e5e7eb',
|
|
408
|
+
fontSize: 11,
|
|
409
|
+
fontFamily: FONT,
|
|
410
|
+
fontWeight: 600,
|
|
411
|
+
letterSpacing: 0.4,
|
|
412
|
+
textTransform: 'uppercase',
|
|
413
|
+
cursor: 'pointer',
|
|
414
|
+
boxShadow: '0 4px 14px rgba(0,0,0,0.45)',
|
|
415
|
+
}}
|
|
416
|
+
>
|
|
417
|
+
<span
|
|
418
|
+
style={{
|
|
419
|
+
display: 'inline-block',
|
|
420
|
+
transform: collapsed ? 'rotate(0deg)' : 'rotate(180deg)',
|
|
421
|
+
transition: `transform ${COLLAPSE_MS}ms ease`,
|
|
422
|
+
fontSize: 13,
|
|
423
|
+
lineHeight: 1,
|
|
424
|
+
}}
|
|
425
|
+
>
|
|
426
|
+
▲
|
|
427
|
+
</span>
|
|
428
|
+
<span>{collapsed ? 'Show workflow' : 'Hide workflow'}</span>
|
|
429
|
+
</button>
|
|
430
|
+
</div>
|
|
431
|
+
|
|
432
|
+
{/* Diagram pane */}
|
|
433
|
+
<div
|
|
434
|
+
style={{
|
|
435
|
+
position: 'absolute',
|
|
436
|
+
top: 36,
|
|
437
|
+
left: 0,
|
|
438
|
+
right: 0,
|
|
439
|
+
bottom: 0,
|
|
440
|
+
backgroundColor: '#0b0f14',
|
|
441
|
+
borderTop: '1px solid #1f2937',
|
|
442
|
+
opacity: collapsed ? 0 : opacity,
|
|
443
|
+
pointerEvents: interactive && !collapsed ? 'auto' : 'none',
|
|
444
|
+
transition: `opacity ${COLLAPSE_MS}ms ease`,
|
|
445
|
+
}}
|
|
446
|
+
>
|
|
447
|
+
<WorkflowSequenceDiagram
|
|
448
|
+
scenario={scenario}
|
|
449
|
+
canvas={AUTH_CANVAS}
|
|
450
|
+
width="100%"
|
|
451
|
+
height="100%"
|
|
452
|
+
showControls={false}
|
|
453
|
+
showBackground={false}
|
|
454
|
+
selectedEventIndex={selectedEventIndex}
|
|
455
|
+
onEventIndexChange={setSelectedEventIndex}
|
|
456
|
+
/>
|
|
457
|
+
</div>
|
|
458
|
+
</div>
|
|
459
|
+
|
|
460
|
+
{/* Settings gear button — always visible, toggles the panel */}
|
|
461
|
+
<button
|
|
462
|
+
onClick={() => setSettingsOpen(o => !o)}
|
|
463
|
+
aria-label={settingsOpen ? 'Hide settings' : 'Show settings'}
|
|
464
|
+
style={{
|
|
465
|
+
position: 'absolute',
|
|
466
|
+
top: 16,
|
|
467
|
+
left: 16,
|
|
468
|
+
zIndex: 60,
|
|
469
|
+
width: 36,
|
|
470
|
+
height: 36,
|
|
471
|
+
display: 'flex',
|
|
472
|
+
alignItems: 'center',
|
|
473
|
+
justifyContent: 'center',
|
|
474
|
+
backgroundColor: settingsOpen
|
|
475
|
+
? 'rgba(34, 211, 238, 0.18)'
|
|
476
|
+
: 'rgba(17, 24, 39, 0.92)',
|
|
477
|
+
border: `1px solid ${settingsOpen ? '#22d3ee' : '#374151'}`,
|
|
478
|
+
borderRadius: 8,
|
|
479
|
+
color: settingsOpen ? '#22d3ee' : '#e5e7eb',
|
|
480
|
+
cursor: 'pointer',
|
|
481
|
+
boxShadow: '0 4px 14px rgba(0,0,0,0.45)',
|
|
482
|
+
transition:
|
|
483
|
+
'background-color 160ms ease, border-color 160ms ease, color 160ms ease',
|
|
484
|
+
}}
|
|
485
|
+
>
|
|
486
|
+
<svg width={18} height={18} viewBox="0 0 24 24" fill="none">
|
|
487
|
+
<path
|
|
488
|
+
d="M12 15.5a3.5 3.5 0 100-7 3.5 3.5 0 000 7z"
|
|
489
|
+
stroke="currentColor"
|
|
490
|
+
strokeWidth={1.7}
|
|
491
|
+
/>
|
|
492
|
+
<path
|
|
493
|
+
d="M19.4 13.6a7.7 7.7 0 000-3.2l1.7-1.3-1.8-3.1-2 .8a7.7 7.7 0 00-2.8-1.6l-.3-2.1h-3.6l-.3 2.1a7.7 7.7 0 00-2.8 1.6l-2-.8-1.8 3.1 1.7 1.3a7.7 7.7 0 000 3.2l-1.7 1.3 1.8 3.1 2-.8a7.7 7.7 0 002.8 1.6l.3 2.1h3.6l.3-2.1a7.7 7.7 0 002.8-1.6l2 .8 1.8-3.1-1.7-1.3z"
|
|
494
|
+
stroke="currentColor"
|
|
495
|
+
strokeWidth={1.7}
|
|
496
|
+
strokeLinejoin="round"
|
|
497
|
+
/>
|
|
498
|
+
</svg>
|
|
499
|
+
</button>
|
|
500
|
+
|
|
501
|
+
{/* Floating control panel */}
|
|
502
|
+
<div
|
|
503
|
+
style={{
|
|
504
|
+
position: 'absolute',
|
|
505
|
+
top: 16,
|
|
506
|
+
left: 64,
|
|
507
|
+
zIndex: 50,
|
|
508
|
+
backgroundColor: 'rgba(17, 24, 39, 0.92)',
|
|
509
|
+
border: '1px solid #374151',
|
|
510
|
+
borderRadius: 10,
|
|
511
|
+
padding: '14px 16px',
|
|
512
|
+
color: '#e5e7eb',
|
|
513
|
+
fontFamily: FONT,
|
|
514
|
+
fontSize: 12,
|
|
515
|
+
display: 'flex',
|
|
516
|
+
flexDirection: 'column',
|
|
517
|
+
gap: 10,
|
|
518
|
+
minWidth: 260,
|
|
519
|
+
boxShadow: '0 8px 24px rgba(0,0,0,0.45)',
|
|
520
|
+
transformOrigin: 'top left',
|
|
521
|
+
transform: settingsOpen
|
|
522
|
+
? 'translateX(0) scale(1)'
|
|
523
|
+
: 'translateX(-8px) scale(0.96)',
|
|
524
|
+
opacity: settingsOpen ? 1 : 0,
|
|
525
|
+
pointerEvents: settingsOpen ? 'auto' : 'none',
|
|
526
|
+
transition: 'opacity 180ms ease, transform 180ms ease',
|
|
527
|
+
}}
|
|
528
|
+
>
|
|
529
|
+
<div
|
|
530
|
+
style={{
|
|
531
|
+
fontSize: 10,
|
|
532
|
+
textTransform: 'uppercase',
|
|
533
|
+
letterSpacing: 1.2,
|
|
534
|
+
color: '#9ca3af',
|
|
535
|
+
}}
|
|
536
|
+
>
|
|
537
|
+
Workflow Diagram Overlay
|
|
538
|
+
</div>
|
|
539
|
+
|
|
540
|
+
<label style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
|
541
|
+
<span
|
|
542
|
+
style={{
|
|
543
|
+
fontSize: 10,
|
|
544
|
+
textTransform: 'uppercase',
|
|
545
|
+
letterSpacing: 1.2,
|
|
546
|
+
color: '#9ca3af',
|
|
547
|
+
}}
|
|
548
|
+
>
|
|
549
|
+
Scenario
|
|
550
|
+
</span>
|
|
551
|
+
<select
|
|
552
|
+
value={scenarioId}
|
|
553
|
+
onChange={e => {
|
|
554
|
+
setScenarioId(e.target.value);
|
|
555
|
+
setSelectedEventIndex(undefined);
|
|
556
|
+
}}
|
|
557
|
+
style={{
|
|
558
|
+
backgroundColor: 'rgba(11, 15, 20, 0.92)',
|
|
559
|
+
color: '#e5e7eb',
|
|
560
|
+
border: '1px solid #374151',
|
|
561
|
+
borderRadius: 6,
|
|
562
|
+
padding: '6px 8px',
|
|
563
|
+
fontSize: 12,
|
|
564
|
+
fontFamily: FONT,
|
|
565
|
+
cursor: 'pointer',
|
|
566
|
+
}}
|
|
567
|
+
>
|
|
568
|
+
{SCENARIOS.map(s => (
|
|
569
|
+
<option key={s.id} value={s.id}>
|
|
570
|
+
{s.id}
|
|
571
|
+
</option>
|
|
572
|
+
))}
|
|
573
|
+
</select>
|
|
574
|
+
<span style={{ fontSize: 10, color: '#9ca3af', lineHeight: 1.4 }}>
|
|
575
|
+
{scenario.description}
|
|
576
|
+
</span>
|
|
577
|
+
</label>
|
|
578
|
+
|
|
579
|
+
<label style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
|
580
|
+
<span style={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
581
|
+
<span>Opacity</span>
|
|
582
|
+
<span style={{ color: '#9ca3af' }}>{opacity.toFixed(2)}</span>
|
|
583
|
+
</span>
|
|
584
|
+
<input
|
|
585
|
+
type="range"
|
|
586
|
+
min={0}
|
|
587
|
+
max={1}
|
|
588
|
+
step={0.01}
|
|
589
|
+
value={opacity}
|
|
590
|
+
onChange={e => setOpacity(parseFloat(e.target.value))}
|
|
591
|
+
style={{ width: '100%' }}
|
|
592
|
+
/>
|
|
593
|
+
</label>
|
|
594
|
+
|
|
595
|
+
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
596
|
+
<input
|
|
597
|
+
type="checkbox"
|
|
598
|
+
checked={interactive}
|
|
599
|
+
onChange={e => setInteractive(e.target.checked)}
|
|
600
|
+
/>
|
|
601
|
+
<span>Interactive (capture pointer)</span>
|
|
602
|
+
</label>
|
|
603
|
+
|
|
604
|
+
<div style={{ fontSize: 10, color: '#6b7280', lineHeight: 1.4 }}>
|
|
605
|
+
Click an event to inspect its template and highlight the matching
|
|
606
|
+
file in the city below. Use the handle at the seam to slide the
|
|
607
|
+
diagram down.
|
|
608
|
+
</div>
|
|
609
|
+
</div>
|
|
610
|
+
|
|
611
|
+
{/* Right-side event inspector overlay */}
|
|
612
|
+
<div
|
|
613
|
+
style={{
|
|
614
|
+
position: 'absolute',
|
|
615
|
+
top: 16,
|
|
616
|
+
right: 16,
|
|
617
|
+
zIndex: 50,
|
|
618
|
+
width: 320,
|
|
619
|
+
maxHeight: 'calc(100vh - 32px)',
|
|
620
|
+
overflowY: 'auto',
|
|
621
|
+
backgroundColor: 'rgba(17, 24, 39, 0.92)',
|
|
622
|
+
border: '1px solid #374151',
|
|
623
|
+
borderRadius: 10,
|
|
624
|
+
padding: '14px 16px',
|
|
625
|
+
color: '#e5e7eb',
|
|
626
|
+
fontFamily: FONT,
|
|
627
|
+
fontSize: 12,
|
|
628
|
+
display: 'flex',
|
|
629
|
+
flexDirection: 'column',
|
|
630
|
+
gap: 10,
|
|
631
|
+
boxShadow: '0 8px 24px rgba(0,0,0,0.45)',
|
|
632
|
+
transformOrigin: 'top right',
|
|
633
|
+
transform: selectedEventName
|
|
634
|
+
? 'translateX(0) scale(1)'
|
|
635
|
+
: 'translateX(8px) scale(0.96)',
|
|
636
|
+
opacity: selectedEventName ? 1 : 0,
|
|
637
|
+
pointerEvents: selectedEventName ? 'auto' : 'none',
|
|
638
|
+
transition: 'opacity 180ms ease, transform 180ms ease',
|
|
639
|
+
}}
|
|
640
|
+
>
|
|
641
|
+
<div
|
|
642
|
+
style={{
|
|
643
|
+
display: 'flex',
|
|
644
|
+
alignItems: 'baseline',
|
|
645
|
+
justifyContent: 'space-between',
|
|
646
|
+
gap: 8,
|
|
647
|
+
}}
|
|
648
|
+
>
|
|
649
|
+
<span
|
|
650
|
+
style={{
|
|
651
|
+
fontSize: 10,
|
|
652
|
+
textTransform: 'uppercase',
|
|
653
|
+
letterSpacing: 1.2,
|
|
654
|
+
color: '#9ca3af',
|
|
655
|
+
}}
|
|
656
|
+
>
|
|
657
|
+
Event
|
|
658
|
+
</span>
|
|
659
|
+
{selectedEventIndex !== undefined && (
|
|
660
|
+
<span style={{ fontSize: 10, color: '#6b7280' }}>
|
|
661
|
+
{selectedEventIndex + 1} / {eventNames.length}
|
|
662
|
+
</span>
|
|
663
|
+
)}
|
|
664
|
+
</div>
|
|
665
|
+
<code
|
|
666
|
+
style={{
|
|
667
|
+
fontSize: 12,
|
|
668
|
+
color: '#22d3ee',
|
|
669
|
+
fontFamily:
|
|
670
|
+
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
|
|
671
|
+
wordBreak: 'break-all',
|
|
672
|
+
}}
|
|
673
|
+
>
|
|
674
|
+
{selectedEventName}
|
|
675
|
+
</code>
|
|
676
|
+
|
|
677
|
+
<div
|
|
678
|
+
style={{
|
|
679
|
+
marginTop: 4,
|
|
680
|
+
paddingTop: 10,
|
|
681
|
+
borderTop: '1px solid #1f2937',
|
|
682
|
+
display: 'flex',
|
|
683
|
+
flexDirection: 'column',
|
|
684
|
+
gap: 6,
|
|
685
|
+
}}
|
|
686
|
+
>
|
|
687
|
+
<span
|
|
688
|
+
style={{
|
|
689
|
+
fontSize: 10,
|
|
690
|
+
textTransform: 'uppercase',
|
|
691
|
+
letterSpacing: 1.2,
|
|
692
|
+
color: '#9ca3af',
|
|
693
|
+
}}
|
|
694
|
+
>
|
|
695
|
+
Template
|
|
696
|
+
</span>
|
|
697
|
+
<code
|
|
698
|
+
style={{
|
|
699
|
+
fontSize: 12,
|
|
700
|
+
lineHeight: 1.5,
|
|
701
|
+
color: '#e5e7eb',
|
|
702
|
+
fontFamily:
|
|
703
|
+
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
|
|
704
|
+
wordBreak: 'break-word',
|
|
705
|
+
whiteSpace: 'pre-wrap',
|
|
706
|
+
backgroundColor: 'rgba(11, 15, 20, 0.6)',
|
|
707
|
+
border: '1px solid #1f2937',
|
|
708
|
+
borderRadius: 6,
|
|
709
|
+
padding: '8px 10px',
|
|
710
|
+
}}
|
|
711
|
+
>
|
|
712
|
+
{selectedEventTemplate}
|
|
713
|
+
</code>
|
|
714
|
+
</div>
|
|
715
|
+
|
|
716
|
+
<div
|
|
717
|
+
style={{
|
|
718
|
+
marginTop: 4,
|
|
719
|
+
paddingTop: 10,
|
|
720
|
+
borderTop: '1px solid #1f2937',
|
|
721
|
+
display: 'flex',
|
|
722
|
+
flexDirection: 'column',
|
|
723
|
+
gap: 8,
|
|
724
|
+
}}
|
|
725
|
+
>
|
|
726
|
+
{selectedPaths.files.length === 0 &&
|
|
727
|
+
selectedPaths.references.length === 0 ? (
|
|
728
|
+
<span style={{ fontSize: 10, color: '#6b7280' }}>
|
|
729
|
+
No files or references mapped for this event.
|
|
730
|
+
</span>
|
|
731
|
+
) : (
|
|
732
|
+
<>
|
|
733
|
+
{selectedPaths.files.length > 0 && (
|
|
734
|
+
<PathList
|
|
735
|
+
label="Files"
|
|
736
|
+
color={FILE_HIGHLIGHT_COLOR}
|
|
737
|
+
paths={selectedPaths.files}
|
|
738
|
+
/>
|
|
739
|
+
)}
|
|
740
|
+
{selectedPaths.references.length > 0 && (
|
|
741
|
+
<PathList
|
|
742
|
+
label="References"
|
|
743
|
+
color={REFERENCE_HIGHLIGHT_COLOR}
|
|
744
|
+
paths={selectedPaths.references}
|
|
745
|
+
/>
|
|
746
|
+
)}
|
|
747
|
+
</>
|
|
748
|
+
)}
|
|
749
|
+
</div>
|
|
750
|
+
</div>
|
|
751
|
+
</div>
|
|
752
|
+
);
|
|
753
|
+
},
|
|
754
|
+
};
|