@principal-ai/file-city-react 0.5.37 → 0.5.38

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.
@@ -18,6 +18,7 @@ import {
18
18
  type HighlightLayer,
19
19
  type IsolationMode,
20
20
  } from '../components/FileCity3D';
21
+ import { createFileColorHighlightLayers } from '../utils/fileColorHighlightLayers';
21
22
 
22
23
  const meta: Meta<typeof FileCity3D> = {
23
24
  title: 'Components/FileCity3D',
@@ -2209,3 +2210,671 @@ export const FlatToGrownTransition: Story = {
2209
2210
  },
2210
2211
  },
2211
2212
  };
2213
+
2214
+ /**
2215
+ * Repository Profile - Simulates how FileCity3D is used in the electron-app's RepositoryProfilePanel
2216
+ *
2217
+ * Features:
2218
+ * - File suffix color layers (TypeScript, JavaScript, Python, etc.) using createFileColorHighlightLayers
2219
+ * - Git status overlays with borders (staged=green, modified=orange, untracked=blue, deleted=red)
2220
+ * - Toggle controls for layers
2221
+ * - Linear height scaling
2222
+ * - Background color theming
2223
+ * - Starts flat with manual grow button
2224
+ * - Uses real electron-app city data
2225
+ */
2226
+ const RepositoryProfileTemplate: React.FC = () => {
2227
+ const [showSuffixLayers, setShowSuffixLayers] = React.useState(true);
2228
+ const [showGitStatus, setShowGitStatus] = React.useState(true);
2229
+ const [gitStatusType, setGitStatusType] = React.useState<'clean' | 'working' | 'all'>('working');
2230
+
2231
+ // Sample git status files from electron-app
2232
+ const gitStatusFiles = {
2233
+ staged: ['src/renderer/panels/RepositoryProfilePanel.tsx', 'src/renderer/components/FileCity3D/FileCity3D.tsx'],
2234
+ modified: ['src/renderer/principal-window/index.tsx', 'src/main/stores/initialization.ts', 'src/renderer/App.tsx'],
2235
+ untracked: ['src/renderer/components/NewFeature.tsx', 'docs/NEW_ARCHITECTURE.md'],
2236
+ deleted: ['src/renderer/components/OldComponent.tsx'],
2237
+ };
2238
+
2239
+ // Create highlight layers
2240
+ const highlightLayers = React.useMemo(() => {
2241
+ const layers: HighlightLayer[] = [];
2242
+ const backgroundColor = '#1e293b'; // slate-800
2243
+ const hasGitChanges = showGitStatus && gitStatusType !== 'clean';
2244
+
2245
+ // 1. File suffix color layers (higher priority = rendered underneath)
2246
+ if (showSuffixLayers) {
2247
+ const fileSuffixLayers = createFileColorHighlightLayers((electronAppCityData as CityData).buildings);
2248
+
2249
+ // Dim suffix layers when git changes are present to help focus on git status
2250
+ const adjustedSuffixLayers = hasGitChanges
2251
+ ? fileSuffixLayers.map(layer => ({
2252
+ ...layer,
2253
+ opacity: (layer.opacity ?? 1.0) * 0.2,
2254
+ priority: layer.priority + 100,
2255
+ }))
2256
+ : fileSuffixLayers.map(layer => ({
2257
+ ...layer,
2258
+ priority: layer.priority + 100,
2259
+ }));
2260
+
2261
+ layers.push(...adjustedSuffixLayers);
2262
+ }
2263
+
2264
+ // 2. Git status layers with borders (lower priority = rendered on top)
2265
+ if (showGitStatus && gitStatusType !== 'clean') {
2266
+ // Staged files - green border
2267
+ if ((gitStatusType === 'all' || gitStatusType === 'working') && gitStatusFiles.staged.length > 0) {
2268
+ layers.push({
2269
+ id: 'git-staged',
2270
+ name: 'Staged Files',
2271
+ enabled: true,
2272
+ color: '#22c55e', // green-500
2273
+ priority: 4,
2274
+ items: gitStatusFiles.staged.map(path => ({
2275
+ type: 'file' as const,
2276
+ path,
2277
+ renderStrategy: 'border' as const
2278
+ })),
2279
+ opacity: 0.8,
2280
+ borderWidth: 4,
2281
+ });
2282
+ }
2283
+
2284
+ // Modified files - orange border
2285
+ if (gitStatusFiles.modified.length > 0) {
2286
+ layers.push({
2287
+ id: 'git-modified',
2288
+ name: 'Modified Files',
2289
+ enabled: true,
2290
+ color: '#f59e0b', // amber-500
2291
+ priority: 3,
2292
+ items: gitStatusFiles.modified.map(path => ({
2293
+ type: 'file' as const,
2294
+ path,
2295
+ renderStrategy: 'border' as const
2296
+ })),
2297
+ opacity: 0.8,
2298
+ borderWidth: 4,
2299
+ });
2300
+ }
2301
+
2302
+ // Untracked files - blue border
2303
+ if (gitStatusFiles.untracked.length > 0) {
2304
+ layers.push({
2305
+ id: 'git-untracked',
2306
+ name: 'Untracked Files',
2307
+ enabled: true,
2308
+ color: '#3b82f6', // blue-500
2309
+ priority: 2,
2310
+ items: gitStatusFiles.untracked.map(path => ({
2311
+ type: 'file' as const,
2312
+ path,
2313
+ renderStrategy: 'border' as const
2314
+ })),
2315
+ opacity: 0.8,
2316
+ borderWidth: 4,
2317
+ });
2318
+ }
2319
+
2320
+ // Deleted files - red border
2321
+ if (gitStatusType === 'all' && gitStatusFiles.deleted.length > 0) {
2322
+ layers.push({
2323
+ id: 'git-deleted',
2324
+ name: 'Deleted Files',
2325
+ enabled: true,
2326
+ color: '#ef4444', // red-500
2327
+ priority: 1,
2328
+ items: gitStatusFiles.deleted.map(path => ({
2329
+ type: 'file' as const,
2330
+ path,
2331
+ renderStrategy: 'border' as const
2332
+ })),
2333
+ opacity: 0.8,
2334
+ borderWidth: 4,
2335
+ });
2336
+ }
2337
+ }
2338
+
2339
+ return layers;
2340
+ }, [showSuffixLayers, showGitStatus, gitStatusType]);
2341
+
2342
+ return (
2343
+ <div
2344
+ style={{ height: '100vh', display: 'flex', flexDirection: 'column', backgroundColor: '#0f172a' }}
2345
+ >
2346
+ {/* Header area */}
2347
+ <div style={{ padding: 16, borderBottom: '1px solid #334155' }}>
2348
+ <h2 style={{ margin: 0, color: '#e2e8f0', fontSize: 18, fontFamily: 'system-ui, sans-serif' }}>
2349
+ Repository Profile Panel Layout
2350
+ </h2>
2351
+ </div>
2352
+
2353
+ {/* Main content area - split layout */}
2354
+ <div style={{ flex: 1, display: 'flex', gap: 16, padding: 16, overflow: 'hidden' }}>
2355
+ {/* Left side - Stats panel with controls */}
2356
+ <div
2357
+ style={{
2358
+ flex: 1,
2359
+ minWidth: 0,
2360
+ background: '#1e293b',
2361
+ border: '1px solid #334155',
2362
+ borderRadius: 8,
2363
+ padding: 16,
2364
+ overflow: 'auto',
2365
+ display: 'flex',
2366
+ flexDirection: 'column',
2367
+ gap: 16,
2368
+ }}
2369
+ >
2370
+ <div>
2371
+ <h3 style={{ margin: '0 0 16px', color: '#e2e8f0', fontSize: 14, fontFamily: 'system-ui, sans-serif', fontWeight: 600 }}>
2372
+ Repository Profile View
2373
+ </h3>
2374
+
2375
+ {/* File Type Colors Toggle */}
2376
+ <div style={{ marginBottom: 16 }}>
2377
+ <label
2378
+ style={{
2379
+ display: 'flex',
2380
+ alignItems: 'center',
2381
+ gap: 8,
2382
+ cursor: 'pointer',
2383
+ fontSize: 13,
2384
+ color: '#e2e8f0',
2385
+ fontFamily: 'system-ui, sans-serif',
2386
+ }}
2387
+ >
2388
+ <input
2389
+ type="checkbox"
2390
+ checked={showSuffixLayers}
2391
+ onChange={e => setShowSuffixLayers(e.target.checked)}
2392
+ style={{ cursor: 'pointer' }}
2393
+ />
2394
+ <span>Show File Type Colors</span>
2395
+ </label>
2396
+ <div style={{ fontSize: 11, color: '#64748b', marginLeft: 28, marginTop: 4, fontFamily: 'system-ui, sans-serif' }}>
2397
+ Color-codes files by extension (TS, JS, Python, etc.)
2398
+ </div>
2399
+ </div>
2400
+
2401
+ {/* Git Status Toggle */}
2402
+ <div style={{ marginBottom: 16 }}>
2403
+ <label
2404
+ style={{
2405
+ display: 'flex',
2406
+ alignItems: 'center',
2407
+ gap: 8,
2408
+ cursor: 'pointer',
2409
+ fontSize: 13,
2410
+ marginBottom: 8,
2411
+ color: '#e2e8f0',
2412
+ fontFamily: 'system-ui, sans-serif',
2413
+ }}
2414
+ >
2415
+ <input
2416
+ type="checkbox"
2417
+ checked={showGitStatus}
2418
+ onChange={e => setShowGitStatus(e.target.checked)}
2419
+ style={{ cursor: 'pointer' }}
2420
+ />
2421
+ <span>Show Git Status</span>
2422
+ </label>
2423
+
2424
+ {showGitStatus && (
2425
+ <div style={{ marginLeft: 28 }}>
2426
+ <div style={{ fontSize: 11, color: '#64748b', marginBottom: 6, fontFamily: 'system-ui, sans-serif' }}>
2427
+ Status Type:
2428
+ </div>
2429
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
2430
+ <label style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 12, cursor: 'pointer', color: '#e2e8f0', fontFamily: 'system-ui, sans-serif' }}>
2431
+ <input
2432
+ type="radio"
2433
+ checked={gitStatusType === 'clean'}
2434
+ onChange={() => setGitStatusType('clean')}
2435
+ style={{ cursor: 'pointer' }}
2436
+ />
2437
+ Clean (no changes)
2438
+ </label>
2439
+ <label style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 12, cursor: 'pointer', color: '#e2e8f0', fontFamily: 'system-ui, sans-serif' }}>
2440
+ <input
2441
+ type="radio"
2442
+ checked={gitStatusType === 'working'}
2443
+ onChange={() => setGitStatusType('working')}
2444
+ style={{ cursor: 'pointer' }}
2445
+ />
2446
+ Working Changes
2447
+ </label>
2448
+ <label style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 12, cursor: 'pointer', color: '#e2e8f0', fontFamily: 'system-ui, sans-serif' }}>
2449
+ <input
2450
+ type="radio"
2451
+ checked={gitStatusType === 'all'}
2452
+ onChange={() => setGitStatusType('all')}
2453
+ style={{ cursor: 'pointer' }}
2454
+ />
2455
+ All Status (+ staged & deleted)
2456
+ </label>
2457
+ </div>
2458
+ </div>
2459
+ )}
2460
+ </div>
2461
+
2462
+ {/* Legend */}
2463
+ {showGitStatus && gitStatusType !== 'clean' && (
2464
+ <div
2465
+ style={{
2466
+ marginTop: 16,
2467
+ paddingTop: 16,
2468
+ borderTop: '1px solid #334155',
2469
+ }}
2470
+ >
2471
+ <div style={{ fontSize: 11, color: '#64748b', marginBottom: 8, fontFamily: 'system-ui, sans-serif' }}>
2472
+ Git Status Legend:
2473
+ </div>
2474
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
2475
+ {gitStatusType === 'all' && (
2476
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 12, color: '#e2e8f0', fontFamily: 'system-ui, sans-serif' }}>
2477
+ <div
2478
+ style={{
2479
+ width: 16,
2480
+ height: 16,
2481
+ border: '3px solid #22c55e',
2482
+ borderRadius: 2,
2483
+ }}
2484
+ />
2485
+ <span>Staged ({gitStatusFiles.staged.length})</span>
2486
+ </div>
2487
+ )}
2488
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 12, color: '#e2e8f0', fontFamily: 'system-ui, sans-serif' }}>
2489
+ <div
2490
+ style={{
2491
+ width: 16,
2492
+ height: 16,
2493
+ border: '3px solid #f59e0b',
2494
+ borderRadius: 2,
2495
+ }}
2496
+ />
2497
+ <span>Modified ({gitStatusFiles.modified.length})</span>
2498
+ </div>
2499
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 12, color: '#e2e8f0', fontFamily: 'system-ui, sans-serif' }}>
2500
+ <div
2501
+ style={{
2502
+ width: 16,
2503
+ height: 16,
2504
+ border: '3px solid #3b82f6',
2505
+ borderRadius: 2,
2506
+ }}
2507
+ />
2508
+ <span>Untracked ({gitStatusFiles.untracked.length})</span>
2509
+ </div>
2510
+ {gitStatusType === 'all' && (
2511
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 12, color: '#e2e8f0', fontFamily: 'system-ui, sans-serif' }}>
2512
+ <div
2513
+ style={{
2514
+ width: 16,
2515
+ height: 16,
2516
+ border: '3px solid #ef4444',
2517
+ borderRadius: 2,
2518
+ }}
2519
+ />
2520
+ <span>Deleted ({gitStatusFiles.deleted.length})</span>
2521
+ </div>
2522
+ )}
2523
+ </div>
2524
+ </div>
2525
+ )}
2526
+
2527
+ {/* Active layers count */}
2528
+ <div
2529
+ style={{
2530
+ marginTop: 16,
2531
+ paddingTop: 16,
2532
+ borderTop: '1px solid #334155',
2533
+ fontSize: 11,
2534
+ color: '#64748b',
2535
+ fontFamily: 'system-ui, sans-serif',
2536
+ }}
2537
+ >
2538
+ Active Layers: {highlightLayers.length}
2539
+ </div>
2540
+ </div>
2541
+
2542
+ {/* Additional info */}
2543
+ <div
2544
+ style={{
2545
+ marginTop: 'auto',
2546
+ paddingTop: 16,
2547
+ borderTop: '1px solid #334155',
2548
+ }}
2549
+ >
2550
+ <div style={{ color: '#64748b', fontSize: 12, fontFamily: 'system-ui, sans-serif', lineHeight: 1.6 }}>
2551
+ <p style={{ margin: '0 0 8px' }}>In the actual RepositoryProfilePanel, this section also displays:</p>
2552
+ <ul style={{ margin: 0, paddingLeft: 20 }}>
2553
+ <li>Activity heatmap</li>
2554
+ <li>Contributor list</li>
2555
+ <li>Changed files details</li>
2556
+ <li>Commit playback controls</li>
2557
+ </ul>
2558
+ </div>
2559
+ </div>
2560
+ </div>
2561
+
2562
+ {/* Right side - File City 3D (constrained to square-ish aspect ratio) */}
2563
+ <div
2564
+ style={{
2565
+ flex: 1,
2566
+ minWidth: 0,
2567
+ height: '100%',
2568
+ borderRadius: 8,
2569
+ overflow: 'hidden',
2570
+ border: '1px solid #334155',
2571
+ backgroundColor: '#1e293b',
2572
+ position: 'relative',
2573
+ }}
2574
+ >
2575
+ <FileCity3D
2576
+ cityData={electronAppCityData as CityData}
2577
+ height="100%"
2578
+ width="100%"
2579
+ heightScaling="linear"
2580
+ linearScale={0.5}
2581
+ animation={{
2582
+ startFlat: true,
2583
+ autoStartDelay: null, // Manual control like in RepositoryProfilePanel
2584
+ }}
2585
+ showControls={true}
2586
+ backgroundColor="#1e293b" // slate-800
2587
+ highlightLayers={highlightLayers}
2588
+ style={{
2589
+ width: '100%',
2590
+ height: '100%',
2591
+ }}
2592
+ />
2593
+ </div>
2594
+ </div>
2595
+
2596
+ {/* Info banner - bottom */}
2597
+ <div
2598
+ style={{
2599
+ position: 'absolute',
2600
+ bottom: 0,
2601
+ left: 0,
2602
+ right: 0,
2603
+ zIndex: 100,
2604
+ background: 'rgba(15, 23, 42, 0.95)',
2605
+ borderTop: '1px solid #334155',
2606
+ padding: '12px 24px',
2607
+ color: '#94a3b8',
2608
+ fontFamily: 'system-ui, sans-serif',
2609
+ fontSize: 12,
2610
+ textAlign: 'center',
2611
+ }}
2612
+ >
2613
+ This story replicates how FileCity3D is used in the electron-app&apos;s RepositoryProfilePanel:
2614
+ file type colors + git status overlays with borders
2615
+ </div>
2616
+ </div>
2617
+ );
2618
+ };
2619
+
2620
+ export const RepositoryProfile: Story = {
2621
+ render: () => <RepositoryProfileTemplate />,
2622
+ parameters: {
2623
+ docs: {
2624
+ description: {
2625
+ story:
2626
+ 'Simulates how FileCity3D is used in the electron-app\'s RepositoryProfilePanel. ' +
2627
+ 'Shows file suffix color layers (using createFileColorHighlightLayers) and git status overlays with borders. ' +
2628
+ 'Demonstrates the layering system where git status borders render on top of dimmed file type colors.',
2629
+ },
2630
+ },
2631
+ },
2632
+ };
2633
+
2634
+ /**
2635
+ * Repository Profile - Async Data Loading Test
2636
+ *
2637
+ * Tests the camera initialization bug that caused electron-app to switch to 2D view.
2638
+ * Simulates the actual RepositoryProfilePanel behavior where city data loads asynchronously
2639
+ * after component mount (file tree fetch -> build city data -> setState).
2640
+ *
2641
+ * The bug: Camera would initialize at (0,0,0) before city bounds were calculated,
2642
+ * resulting in a black screen or wrong view position.
2643
+ */
2644
+ const AsyncDataLoadingTemplate: React.FC = () => {
2645
+ const [cityData, setCityData] = React.useState<CityData | null>(null);
2646
+ const [isLoading, setIsLoading] = React.useState(true);
2647
+ const [showSuffixLayers, setShowSuffixLayers] = React.useState(true);
2648
+
2649
+ // Simulate async data loading like RepositoryProfilePanel
2650
+ React.useEffect(() => {
2651
+ setIsLoading(true);
2652
+
2653
+ // Simulate network delay + processing time
2654
+ const timer = setTimeout(() => {
2655
+ console.log('[AsyncTest] Loading city data...');
2656
+ setCityData(electronAppCityData as CityData);
2657
+ setIsLoading(false);
2658
+ console.log('[AsyncTest] City data loaded, bounds:', (electronAppCityData as CityData).bounds);
2659
+ }, 1500); // 1.5s delay simulates file tree fetch + build time
2660
+
2661
+ return () => clearTimeout(timer);
2662
+ }, []);
2663
+
2664
+ // Create highlight layers
2665
+ const highlightLayers = React.useMemo(() => {
2666
+ if (!cityData) return [];
2667
+
2668
+ const layers: HighlightLayer[] = [];
2669
+
2670
+ if (showSuffixLayers) {
2671
+ const fileSuffixLayers = createFileColorHighlightLayers(cityData.buildings);
2672
+ layers.push(...fileSuffixLayers.map(layer => ({
2673
+ ...layer,
2674
+ priority: layer.priority + 100,
2675
+ })));
2676
+ }
2677
+
2678
+ return layers;
2679
+ }, [cityData, showSuffixLayers]);
2680
+
2681
+ return (
2682
+ <div
2683
+ style={{ height: '100vh', display: 'flex', flexDirection: 'column', backgroundColor: '#0f172a' }}
2684
+ >
2685
+ {/* Header */}
2686
+ <div style={{ padding: 16, borderBottom: '1px solid #334155' }}>
2687
+ <h2 style={{ margin: 0, color: '#e2e8f0', fontSize: 18, fontFamily: 'system-ui, sans-serif' }}>
2688
+ Async Data Loading Test - Camera Initialization Bug
2689
+ </h2>
2690
+ <p style={{ margin: '8px 0 0', color: '#94a3b8', fontSize: 13, fontFamily: 'system-ui, sans-serif' }}>
2691
+ Tests the camera bug that caused electron-app to switch from 3D to 2D view
2692
+ </p>
2693
+ </div>
2694
+
2695
+ {/* Main content */}
2696
+ <div style={{ flex: 1, display: 'flex', gap: 16, padding: 16, overflow: 'hidden' }}>
2697
+ {/* Left panel */}
2698
+ <div
2699
+ style={{
2700
+ flex: 1,
2701
+ minWidth: 0,
2702
+ background: '#1e293b',
2703
+ border: '1px solid #334155',
2704
+ borderRadius: 8,
2705
+ padding: 16,
2706
+ overflow: 'auto',
2707
+ display: 'flex',
2708
+ flexDirection: 'column',
2709
+ gap: 16,
2710
+ }}
2711
+ >
2712
+ <div>
2713
+ <h3 style={{ margin: '0 0 16px', color: '#e2e8f0', fontSize: 14, fontFamily: 'system-ui, sans-serif', fontWeight: 600 }}>
2714
+ Test Status
2715
+ </h3>
2716
+
2717
+ <div style={{ marginBottom: 16 }}>
2718
+ <div style={{ fontSize: 13, color: '#e2e8f0', marginBottom: 8, fontFamily: 'system-ui, sans-serif' }}>
2719
+ Loading State: <strong style={{ color: isLoading ? '#f59e0b' : '#22c55e' }}>
2720
+ {isLoading ? 'LOADING' : 'LOADED'}
2721
+ </strong>
2722
+ </div>
2723
+ <div style={{ fontSize: 13, color: '#e2e8f0', marginBottom: 8, fontFamily: 'system-ui, sans-serif' }}>
2724
+ City Data: <strong style={{ color: cityData ? '#22c55e' : '#64748b' }}>
2725
+ {cityData ? 'READY' : 'NULL'}
2726
+ </strong>
2727
+ </div>
2728
+ {cityData && (
2729
+ <div style={{ fontSize: 11, color: '#64748b', marginTop: 8, fontFamily: 'monospace' }}>
2730
+ Bounds: [{cityData.bounds.minX}, {cityData.bounds.maxX}] x [{cityData.bounds.minZ}, {cityData.bounds.maxZ}]
2731
+ </div>
2732
+ )}
2733
+ </div>
2734
+
2735
+ <div style={{ marginBottom: 16, paddingTop: 16, borderTop: '1px solid #334155' }}>
2736
+ <label
2737
+ style={{
2738
+ display: 'flex',
2739
+ alignItems: 'center',
2740
+ gap: 8,
2741
+ cursor: 'pointer',
2742
+ fontSize: 13,
2743
+ color: '#e2e8f0',
2744
+ fontFamily: 'system-ui, sans-serif',
2745
+ }}
2746
+ >
2747
+ <input
2748
+ type="checkbox"
2749
+ checked={showSuffixLayers}
2750
+ onChange={e => setShowSuffixLayers(e.target.checked)}
2751
+ style={{ cursor: 'pointer' }}
2752
+ />
2753
+ <span>Show File Type Colors</span>
2754
+ </label>
2755
+ </div>
2756
+
2757
+ <button
2758
+ onClick={() => {
2759
+ setCityData(null);
2760
+ setIsLoading(true);
2761
+ setTimeout(() => {
2762
+ setCityData(electronAppCityData as CityData);
2763
+ setIsLoading(false);
2764
+ }, 1500);
2765
+ }}
2766
+ style={{
2767
+ padding: '8px 16px',
2768
+ background: '#3b82f6',
2769
+ color: 'white',
2770
+ border: '1px solid #334155',
2771
+ borderRadius: 6,
2772
+ cursor: 'pointer',
2773
+ fontSize: 13,
2774
+ fontFamily: 'system-ui, sans-serif',
2775
+ }}
2776
+ >
2777
+ Reload Data (Test Again)
2778
+ </button>
2779
+ </div>
2780
+
2781
+ {/* Bug description */}
2782
+ <div
2783
+ style={{
2784
+ marginTop: 'auto',
2785
+ paddingTop: 16,
2786
+ borderTop: '1px solid #334155',
2787
+ }}
2788
+ >
2789
+ <div style={{ fontSize: 12, color: '#e2e8f0', marginBottom: 8, fontFamily: 'system-ui, sans-serif', fontWeight: 600 }}>
2790
+ The Bug:
2791
+ </div>
2792
+ <div style={{ color: '#94a3b8', fontSize: 12, fontFamily: 'system-ui, sans-serif', lineHeight: 1.6 }}>
2793
+ <p style={{ margin: '0 0 8px' }}>
2794
+ When FileCity3D mounts before cityData is available (or while it's null),
2795
+ the camera may initialize at position (0,0,0) instead of calculating the
2796
+ proper viewing position from city bounds.
2797
+ </p>
2798
+ <p style={{ margin: '8px 0 0' }}>
2799
+ This caused electron-app's RepositoryProfilePanel to show a black screen
2800
+ or wrong camera position, leading to a switch to the 2D view.
2801
+ </p>
2802
+ </div>
2803
+ </div>
2804
+ </div>
2805
+
2806
+ {/* Right panel - 3D view */}
2807
+ <div
2808
+ style={{
2809
+ flex: 1,
2810
+ minWidth: 0,
2811
+ height: '100%',
2812
+ borderRadius: 8,
2813
+ overflow: 'hidden',
2814
+ border: '1px solid #334155',
2815
+ backgroundColor: '#1e293b',
2816
+ position: 'relative',
2817
+ }}
2818
+ >
2819
+ {isLoading || !cityData ? (
2820
+ <div
2821
+ style={{
2822
+ width: '100%',
2823
+ height: '100%',
2824
+ display: 'flex',
2825
+ flexDirection: 'column',
2826
+ alignItems: 'center',
2827
+ justifyContent: 'center',
2828
+ color: '#94a3b8',
2829
+ gap: 16,
2830
+ }}
2831
+ >
2832
+ <div style={{ fontSize: 14, fontFamily: 'system-ui, sans-serif' }}>
2833
+ {isLoading ? 'Loading city data...' : 'No data'}
2834
+ </div>
2835
+ {isLoading && (
2836
+ <div style={{ fontSize: 12, color: '#64748b', fontFamily: 'system-ui, sans-serif' }}>
2837
+ Simulating async file tree fetch + build
2838
+ </div>
2839
+ )}
2840
+ </div>
2841
+ ) : (
2842
+ <FileCity3D
2843
+ cityData={cityData}
2844
+ height="100%"
2845
+ width="100%"
2846
+ heightScaling="linear"
2847
+ linearScale={0.5}
2848
+ animation={{
2849
+ startFlat: true,
2850
+ autoStartDelay: null,
2851
+ }}
2852
+ showControls={true}
2853
+ backgroundColor="#1e293b"
2854
+ highlightLayers={highlightLayers}
2855
+ style={{
2856
+ width: '100%',
2857
+ height: '100%',
2858
+ }}
2859
+ />
2860
+ )}
2861
+ </div>
2862
+ </div>
2863
+ </div>
2864
+ );
2865
+ };
2866
+
2867
+ export const AsyncDataLoadingTest: Story = {
2868
+ render: () => <AsyncDataLoadingTemplate />,
2869
+ parameters: {
2870
+ docs: {
2871
+ description: {
2872
+ story:
2873
+ 'Tests the camera initialization bug that caused electron-app to switch from FileCity3D to 2D view. ' +
2874
+ 'Simulates async data loading (file tree fetch -> build city data -> setState) to reproduce the race condition ' +
2875
+ 'where the camera would initialize at (0,0,0) before city bounds were available. ' +
2876
+ 'Click "Reload Data" to test the initialization sequence multiple times.',
2877
+ },
2878
+ },
2879
+ },
2880
+ };