@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.
@@ -0,0 +1,427 @@
1
+ import { useCallback, useMemo, useState } from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+
4
+ import '@xyflow/react/dist/style.css';
5
+ import { SequenceDiagramRenderer } from '@principal-ai/principal-view-react';
6
+ import type {
7
+ SequenceEvent,
8
+ SequenceEdge,
9
+ } from '@principal-ai/principal-view-react';
10
+
11
+ import { FileCity3D } from '../components/FileCity3D';
12
+ import type { CityData, HighlightLayer } from '../components/FileCity3D';
13
+ import authServerCityData from '../../../../assets/auth-server-city-data.json';
14
+
15
+ const meta = {
16
+ title: 'Prototypes/Sequence Diagram Overlay (3D)',
17
+ parameters: { layout: 'fullscreen' },
18
+ } satisfies Meta;
19
+
20
+ export default meta;
21
+
22
+ /**
23
+ * Hand-tagged auth-server flow: each event carries a `sourcePath` pointing
24
+ * at a real building in the auth-server city data. Clicking a step in the
25
+ * diagram drives a HighlightLayer on the matching building below.
26
+ */
27
+ const AUTH_FLOW: { events: SequenceEvent[]; edges: SequenceEdge[] } = {
28
+ events: [
29
+ {
30
+ id: '1',
31
+ name: 'request.middleware.intercepted',
32
+ label: 'Middleware Intercepts',
33
+ sourcePath: 'auth-server/src/middleware.ts',
34
+ },
35
+ {
36
+ id: '2',
37
+ name: 'workos.start.requested',
38
+ label: 'Start WorkOS Auth',
39
+ sourcePath: 'auth-server/src/app/api/auth/workos/start/route.ts',
40
+ },
41
+ {
42
+ id: '3',
43
+ name: 'workos.callback.received',
44
+ label: 'Callback Received',
45
+ sourcePath: 'auth-server/src/app/api/auth/workos/callback/route.ts',
46
+ },
47
+ {
48
+ id: '4',
49
+ name: 'workos.token.exchanged',
50
+ label: 'Exchange Code → Token',
51
+ sourcePath: 'auth-server/src/app/api/auth/workos/token/route.ts',
52
+ },
53
+ {
54
+ id: '5',
55
+ name: 'token.stored',
56
+ label: 'Persist Token',
57
+ sourcePath: 'auth-server/src/lib/token-store.ts',
58
+ },
59
+ {
60
+ id: '6',
61
+ name: 'session.created',
62
+ label: 'Create Session',
63
+ sourcePath: 'auth-server/src/lib/auth-session-manager.ts',
64
+ },
65
+ {
66
+ id: '7',
67
+ name: 'org.membership.checked',
68
+ label: 'Check Org Membership',
69
+ sourcePath: 'auth-server/src/lib/org-membership.ts',
70
+ },
71
+ {
72
+ id: '8',
73
+ name: 'cli.room-token.minted',
74
+ label: 'Mint CLI Room Token',
75
+ sourcePath: 'auth-server/src/app/api/auth/cli/room-token/route.ts',
76
+ },
77
+ {
78
+ id: '9',
79
+ name: 'workos.token.verified',
80
+ label: 'Verify Token',
81
+ sourcePath: 'auth-server/src/app/api/auth/workos/verify/route.ts',
82
+ },
83
+ {
84
+ id: '10',
85
+ name: 'user.fetched',
86
+ label: 'Fetch User',
87
+ sourcePath: 'auth-server/src/app/api/auth/user/route.ts',
88
+ },
89
+ ],
90
+ edges: [
91
+ { id: 'e1', fromEvent: '1', toEvent: '2' },
92
+ { id: 'e2', fromEvent: '2', toEvent: '3' },
93
+ { id: 'e3', fromEvent: '3', toEvent: '4' },
94
+ { id: 'e4', fromEvent: '4', toEvent: '5' },
95
+ { id: 'e5', fromEvent: '5', toEvent: '6' },
96
+ { id: 'e6', fromEvent: '6', toEvent: '7' },
97
+ { id: 'e7', fromEvent: '7', toEvent: '8' },
98
+ { id: 'e8', fromEvent: '8', toEvent: '9' },
99
+ { id: 'e9', fromEvent: '9', toEvent: '10' },
100
+ ],
101
+ };
102
+
103
+ type Story = StoryObj;
104
+
105
+ const OVERLAY_HEIGHT_PCT = 50;
106
+ const COLLAPSE_MS = 320;
107
+
108
+ export const Default: Story = {
109
+ render: () => {
110
+ const cityData = useMemo(() => authServerCityData as CityData, []);
111
+ const [opacity, setOpacity] = useState(1);
112
+ const [interactive, setInteractive] = useState(true);
113
+ const [collapsed, setCollapsed] = useState(false);
114
+ const [settingsOpen, setSettingsOpen] = useState(false);
115
+ const [selectedEventId, setSelectedEventId] = useState<string | null>(
116
+ null,
117
+ );
118
+
119
+ const eventById = useMemo(() => {
120
+ const m = new Map<string, SequenceEvent>();
121
+ for (const e of AUTH_FLOW.events) m.set(e.id, e);
122
+ return m;
123
+ }, []);
124
+
125
+ const selectedPath = useMemo(() => {
126
+ if (!selectedEventId) return null;
127
+ return eventById.get(selectedEventId)?.sourcePath ?? null;
128
+ }, [selectedEventId, eventById]);
129
+
130
+ const highlightLayers = useMemo<HighlightLayer[]>(() => {
131
+ if (!selectedPath) return [];
132
+ return [
133
+ {
134
+ id: 'sequence-selection',
135
+ name: 'Sequence Selection',
136
+ enabled: true,
137
+ color: '#22d3ee',
138
+ opacity: 0.9,
139
+ borderWidth: 4,
140
+ priority: 100,
141
+ items: [
142
+ {
143
+ path: selectedPath,
144
+ type: 'file',
145
+ renderStrategy: 'fill',
146
+ },
147
+ ],
148
+ },
149
+ ];
150
+ }, [selectedPath]);
151
+
152
+ const handleNodeClick = useCallback((nodeId: string) => {
153
+ setSelectedEventId(prev => (prev === nodeId ? null : nodeId));
154
+ }, []);
155
+
156
+ return (
157
+ <div
158
+ style={{
159
+ position: 'fixed',
160
+ inset: 0,
161
+ backgroundColor: '#0b0f14',
162
+ overflow: 'hidden',
163
+ }}
164
+ >
165
+ {/* 2D city background (flat mode) */}
166
+ <div style={{ position: 'absolute', inset: 0 }}>
167
+ <FileCity3D
168
+ cityData={cityData}
169
+ width="100%"
170
+ height="100%"
171
+ isGrown={false}
172
+ backgroundColor="#0b0f14"
173
+ highlightLayers={highlightLayers}
174
+ />
175
+ </div>
176
+
177
+ {/* Semi-transparent sequence diagram overlay — bottom half, collapsible */}
178
+ <div
179
+ style={{
180
+ position: 'absolute',
181
+ bottom: 0,
182
+ left: 0,
183
+ right: 0,
184
+ height: `${OVERLAY_HEIGHT_PCT}vh`,
185
+ transform: collapsed
186
+ ? 'translateY(calc(100% - 36px))'
187
+ : 'translateY(0)',
188
+ transition: `transform ${COLLAPSE_MS}ms cubic-bezier(0.32, 0.72, 0, 1)`,
189
+ pointerEvents: 'none',
190
+ zIndex: 30,
191
+ }}
192
+ >
193
+ {/* Drawer handle / collapse toggle pinned to the top edge */}
194
+ <div
195
+ style={{
196
+ position: 'absolute',
197
+ left: 0,
198
+ right: 0,
199
+ top: 0,
200
+ height: 36,
201
+ display: 'flex',
202
+ alignItems: 'center',
203
+ justifyContent: 'flex-end',
204
+ paddingRight: 16,
205
+ pointerEvents: 'auto',
206
+ }}
207
+ >
208
+ <button
209
+ onClick={() => setCollapsed(c => !c)}
210
+ style={{
211
+ display: 'flex',
212
+ alignItems: 'center',
213
+ gap: 8,
214
+ padding: '6px 14px',
215
+ backgroundColor: 'rgba(17, 24, 39, 0.92)',
216
+ border: '1px solid #374151',
217
+ borderRadius: 999,
218
+ color: '#e5e7eb',
219
+ fontSize: 11,
220
+ fontFamily:
221
+ 'ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif',
222
+ fontWeight: 600,
223
+ letterSpacing: 0.4,
224
+ textTransform: 'uppercase',
225
+ cursor: 'pointer',
226
+ boxShadow: '0 4px 14px rgba(0,0,0,0.45)',
227
+ }}
228
+ >
229
+ <span
230
+ style={{
231
+ display: 'inline-block',
232
+ transform: collapsed ? 'rotate(0deg)' : 'rotate(180deg)',
233
+ transition: `transform ${COLLAPSE_MS}ms ease`,
234
+ fontSize: 13,
235
+ lineHeight: 1,
236
+ }}
237
+ >
238
+
239
+ </span>
240
+ <span>{collapsed ? 'Show sequence' : 'Hide sequence'}</span>
241
+ </button>
242
+ </div>
243
+
244
+ {/* Diagram pane */}
245
+ <div
246
+ style={{
247
+ position: 'absolute',
248
+ top: 36,
249
+ left: 0,
250
+ right: 0,
251
+ bottom: 0,
252
+ backgroundColor: '#0b0f14',
253
+ borderTop: '1px solid #1f2937',
254
+ opacity: collapsed ? 0 : opacity,
255
+ pointerEvents: interactive && !collapsed ? 'auto' : 'none',
256
+ transition: `opacity ${COLLAPSE_MS}ms ease`,
257
+ }}
258
+ >
259
+ <SequenceDiagramRenderer
260
+ events={AUTH_FLOW.events}
261
+ edges={AUTH_FLOW.edges}
262
+ width="100%"
263
+ height="100%"
264
+ showControls={false}
265
+ showBackground={false}
266
+ stickyHeaders
267
+ selectedNodeId={selectedEventId ?? undefined}
268
+ onNodeClick={handleNodeClick}
269
+ />
270
+ </div>
271
+ </div>
272
+
273
+ {/* Settings gear button — always visible, toggles the panel */}
274
+ <button
275
+ onClick={() => setSettingsOpen(o => !o)}
276
+ aria-label={settingsOpen ? 'Hide settings' : 'Show settings'}
277
+ style={{
278
+ position: 'absolute',
279
+ top: 16,
280
+ left: 16,
281
+ zIndex: 60,
282
+ width: 36,
283
+ height: 36,
284
+ display: 'flex',
285
+ alignItems: 'center',
286
+ justifyContent: 'center',
287
+ backgroundColor: settingsOpen
288
+ ? 'rgba(34, 211, 238, 0.18)'
289
+ : 'rgba(17, 24, 39, 0.92)',
290
+ border: `1px solid ${settingsOpen ? '#22d3ee' : '#374151'}`,
291
+ borderRadius: 8,
292
+ color: settingsOpen ? '#22d3ee' : '#e5e7eb',
293
+ cursor: 'pointer',
294
+ boxShadow: '0 4px 14px rgba(0,0,0,0.45)',
295
+ transition:
296
+ 'background-color 160ms ease, border-color 160ms ease, color 160ms ease',
297
+ }}
298
+ >
299
+ <svg width={18} height={18} viewBox="0 0 24 24" fill="none">
300
+ <path
301
+ d="M12 15.5a3.5 3.5 0 100-7 3.5 3.5 0 000 7z"
302
+ stroke="currentColor"
303
+ strokeWidth={1.7}
304
+ />
305
+ <path
306
+ 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"
307
+ stroke="currentColor"
308
+ strokeWidth={1.7}
309
+ strokeLinejoin="round"
310
+ />
311
+ </svg>
312
+ </button>
313
+
314
+ {/* Floating control panel */}
315
+ <div
316
+ style={{
317
+ position: 'absolute',
318
+ top: 16,
319
+ left: 64,
320
+ zIndex: 50,
321
+ backgroundColor: 'rgba(17, 24, 39, 0.92)',
322
+ border: '1px solid #374151',
323
+ borderRadius: 10,
324
+ padding: '14px 16px',
325
+ color: '#e5e7eb',
326
+ fontFamily:
327
+ 'ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif',
328
+ fontSize: 12,
329
+ display: 'flex',
330
+ flexDirection: 'column',
331
+ gap: 10,
332
+ minWidth: 240,
333
+ boxShadow: '0 8px 24px rgba(0,0,0,0.45)',
334
+ transformOrigin: 'top left',
335
+ transform: settingsOpen
336
+ ? 'translateX(0) scale(1)'
337
+ : 'translateX(-8px) scale(0.96)',
338
+ opacity: settingsOpen ? 1 : 0,
339
+ pointerEvents: settingsOpen ? 'auto' : 'none',
340
+ transition:
341
+ 'opacity 180ms ease, transform 180ms ease',
342
+ }}
343
+ >
344
+ <div
345
+ style={{
346
+ fontSize: 10,
347
+ textTransform: 'uppercase',
348
+ letterSpacing: 1.2,
349
+ color: '#9ca3af',
350
+ }}
351
+ >
352
+ Sequence Diagram Overlay
353
+ </div>
354
+
355
+ <label
356
+ style={{ display: 'flex', flexDirection: 'column', gap: 4 }}
357
+ >
358
+ <span style={{ display: 'flex', justifyContent: 'space-between' }}>
359
+ <span>Opacity</span>
360
+ <span style={{ color: '#9ca3af' }}>{opacity.toFixed(2)}</span>
361
+ </span>
362
+ <input
363
+ type="range"
364
+ min={0}
365
+ max={1}
366
+ step={0.01}
367
+ value={opacity}
368
+ onChange={e => setOpacity(parseFloat(e.target.value))}
369
+ style={{ width: '100%' }}
370
+ />
371
+ </label>
372
+
373
+ <label
374
+ style={{ display: 'flex', alignItems: 'center', gap: 8 }}
375
+ >
376
+ <input
377
+ type="checkbox"
378
+ checked={interactive}
379
+ onChange={e => setInteractive(e.target.checked)}
380
+ />
381
+ <span>Interactive (capture pointer)</span>
382
+ </label>
383
+
384
+ <div style={{ fontSize: 10, color: '#6b7280', lineHeight: 1.4 }}>
385
+ Click an event to highlight the matching file in the city below.
386
+ Use the handle at the seam to slide the diagram down.
387
+ </div>
388
+
389
+ {selectedPath && (
390
+ <div
391
+ style={{
392
+ marginTop: 4,
393
+ paddingTop: 10,
394
+ borderTop: '1px solid #1f2937',
395
+ display: 'flex',
396
+ flexDirection: 'column',
397
+ gap: 4,
398
+ }}
399
+ >
400
+ <div
401
+ style={{
402
+ fontSize: 10,
403
+ textTransform: 'uppercase',
404
+ letterSpacing: 1.2,
405
+ color: '#9ca3af',
406
+ }}
407
+ >
408
+ Selected
409
+ </div>
410
+ <code
411
+ style={{
412
+ fontSize: 11,
413
+ color: '#e5e7eb',
414
+ fontFamily:
415
+ 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
416
+ wordBreak: 'break-all',
417
+ }}
418
+ >
419
+ {selectedPath}
420
+ </code>
421
+ </div>
422
+ )}
423
+ </div>
424
+ </div>
425
+ );
426
+ },
427
+ };