@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,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
+ };