@principal-ai/file-city-react 0.5.40 → 0.5.41

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 (55) hide show
  1. package/dist/components/FileCity3D/FileCity3D.d.ts +8 -2
  2. package/dist/components/FileCity3D/FileCity3D.d.ts.map +1 -1
  3. package/dist/components/FileCity3D/FileCity3D.js +129 -40
  4. package/dist/components/FileCityExplorer/AddToAreaModal.d.ts +14 -0
  5. package/dist/components/FileCityExplorer/AddToAreaModal.d.ts.map +1 -0
  6. package/dist/components/FileCityExplorer/AddToAreaModal.js +140 -0
  7. package/dist/components/FileCityExplorer/AddToScopeModal.d.ts +14 -0
  8. package/dist/components/FileCityExplorer/AddToScopeModal.d.ts.map +1 -0
  9. package/dist/components/FileCityExplorer/AddToScopeModal.js +176 -0
  10. package/dist/components/FileCityExplorer/FileCityExplorer.d.ts +30 -0
  11. package/dist/components/FileCityExplorer/FileCityExplorer.d.ts.map +1 -0
  12. package/dist/components/FileCityExplorer/FileCityExplorer.js +1045 -0
  13. package/dist/components/FileCityExplorer/ScopeInfoOverlay.d.ts +10 -0
  14. package/dist/components/FileCityExplorer/ScopeInfoOverlay.d.ts.map +1 -0
  15. package/dist/components/FileCityExplorer/ScopeInfoOverlay.js +73 -0
  16. package/dist/components/FileCityExplorer/index.d.ts +3 -0
  17. package/dist/components/FileCityExplorer/index.d.ts.map +1 -0
  18. package/dist/components/FileCityExplorer/index.js +1 -0
  19. package/dist/components/FileCityExplorer/layers.d.ts +16 -0
  20. package/dist/components/FileCityExplorer/layers.d.ts.map +1 -0
  21. package/dist/components/FileCityExplorer/layers.js +61 -0
  22. package/dist/components/FileCityExplorer/model.d.ts +32 -0
  23. package/dist/components/FileCityExplorer/model.d.ts.map +1 -0
  24. package/dist/components/FileCityExplorer/model.js +14 -0
  25. package/dist/components/FileCityExplorer/pathConversion.d.ts +19 -0
  26. package/dist/components/FileCityExplorer/pathConversion.d.ts.map +1 -0
  27. package/dist/components/FileCityExplorer/pathConversion.js +26 -0
  28. package/dist/components/FileCityExplorer/scopeTreePaths.d.ts +21 -0
  29. package/dist/components/FileCityExplorer/scopeTreePaths.d.ts.map +1 -0
  30. package/dist/components/FileCityExplorer/scopeTreePaths.js +42 -0
  31. package/dist/components/FileCityExplorer/styles.d.ts +9 -0
  32. package/dist/components/FileCityExplorer/styles.d.ts.map +1 -0
  33. package/dist/components/FileCityExplorer/styles.js +28 -0
  34. package/dist/utils/folderElevatedPanels.d.ts +3 -1
  35. package/dist/utils/folderElevatedPanels.d.ts.map +1 -1
  36. package/dist/utils/folderElevatedPanels.js +5 -2
  37. package/package.json +2 -1
  38. package/src/components/FileCity3D/FileCity3D.tsx +200 -52
  39. package/src/components/FileCityExplorer/AddToAreaModal.tsx +273 -0
  40. package/src/components/FileCityExplorer/AddToScopeModal.tsx +320 -0
  41. package/src/components/FileCityExplorer/FileCityExplorer.tsx +1457 -0
  42. package/src/components/FileCityExplorer/ScopeInfoOverlay.tsx +229 -0
  43. package/src/components/FileCityExplorer/index.ts +2 -0
  44. package/src/components/FileCityExplorer/layers.ts +72 -0
  45. package/src/components/FileCityExplorer/model.ts +35 -0
  46. package/src/components/FileCityExplorer/pathConversion.ts +32 -0
  47. package/src/components/FileCityExplorer/scopeTreePaths.ts +52 -0
  48. package/src/components/FileCityExplorer/styles.ts +34 -0
  49. package/src/stories/2D3DComparison.stories.tsx +13 -2
  50. package/src/stories/ElevatedScopePanels.stories.tsx +295 -0
  51. package/src/stories/FileCity3D.stories.tsx +24 -3
  52. package/src/stories/FileCityExplorer.stories.tsx +2474 -0
  53. package/src/stories/FileCityExplorerComponent.stories.tsx +59 -0
  54. package/src/utils/folderElevatedPanels.ts +8 -2
  55. package/src/stories/ScopeOverlay.stories.tsx +0 -1610
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  import React, { useMemo, useRef, useState, useEffect, useCallback } from 'react';
11
+ import { useTheme } from '@principal-ade/industry-theme';
11
12
  import { Canvas, useFrame, ThreeEvent, useThree } from '@react-three/fiber';
12
13
 
13
14
  import { useSpring } from '@react-spring/three';
@@ -27,10 +28,11 @@ import type { ThreeElements } from '@react-three/fiber';
27
28
  import { resolveVisualizationIntent } from '../../utils/visualizationResolution';
28
29
 
29
30
  // Extend JSX with Three.js elements
31
+ /* eslint-disable react/no-unknown-property */
30
32
  declare module 'react' {
31
33
  // eslint-disable-next-line @typescript-eslint/no-namespace
32
34
  namespace JSX {
33
- // eslint-disable-next-line @typescript-eslint/no-empty-object-type
35
+ // eslint-disable-next-line @typescript-eslint/no-empty-interface
34
36
  interface IntrinsicElements extends ThreeElements {}
35
37
  }
36
38
  }
@@ -93,8 +95,14 @@ export interface ElevatedScopePanel {
93
95
  * size derived from the panel's footprint. Always clamped to fit the tile.
94
96
  */
95
97
  labelSize?: number;
98
+ /** Optional secondary label rendered above the main label in a smaller font. */
99
+ displayLabel?: string;
100
+ /** Hex color for the display label (default `labelColor` or white). */
101
+ displayLabelColor?: string;
96
102
  /** Click handler. When set, the slab becomes interactive and shows a pointer cursor. */
97
- onClick?: () => void;
103
+ onClick?: (event: MouseEvent) => void;
104
+ /** Double-click handler. The slab becomes interactive (pointer cursor) when either onClick or onDoubleClick is set. */
105
+ onDoubleClick?: (event: MouseEvent) => void;
98
106
  }
99
107
 
100
108
  /** Pattern for files that should render flat (e.g., lock files, generated files) */
@@ -640,7 +648,7 @@ function BorderHighlights({
640
648
  const animStartTime = startTimeRef.current ?? currentTime;
641
649
 
642
650
  borderEdgeData.forEach((edge, idx) => {
643
- const { x, z, fullHeight, staggerDelayMs, buildingIndex, color, opacity, borderWidth, edgeType, width, depth } = edge;
651
+ const { x, z, fullHeight, staggerDelayMs, buildingIndex, color, borderWidth, edgeType, width, depth } = edge;
644
652
 
645
653
  // Get height multiplier from shared ref (for collapse animation)
646
654
  const heightMultiplier = heightMultipliersRef.current?.[buildingIndex] ?? 1;
@@ -715,7 +723,7 @@ interface InstancedBuildingsProps {
715
723
  buildings: CityBuilding[];
716
724
  centerOffset: { x: number; z: number };
717
725
  onHover?: (building: CityBuilding | null) => void;
718
- onClick?: (building: CityBuilding) => void;
726
+ onClick?: (building: CityBuilding, event: MouseEvent) => void;
719
727
  hoveredIndex: number | null;
720
728
  selectedIndex: number | null;
721
729
  growProgress: number;
@@ -1013,7 +1021,7 @@ function InstancedBuildings({
1013
1021
  e.stopPropagation();
1014
1022
  if (e.instanceId !== undefined && e.instanceId < buildingData.length) {
1015
1023
  const data = buildingData[e.instanceId];
1016
- onClick?.(data.building);
1024
+ onClick?.(data.building, e.nativeEvent);
1017
1025
  }
1018
1026
  },
1019
1027
  [buildingData, onClick],
@@ -1707,6 +1715,75 @@ const AnimatedCamera = React.memo(function AnimatedCamera({
1707
1715
  // eslint-disable-next-line react-hooks/exhaustive-deps
1708
1716
  }, [isFlat]); // Only animate when isFlat changes, not when focusTarget/citySize/etc change
1709
1717
 
1718
+ // Animate the camera when focusTarget changes (works in both 2D and 3D).
1719
+ // - 3D + target: frame the directory using the same math as the 2D→3D path
1720
+ // - 3D + null: ease back to the overview position
1721
+ // - 2D + target: pan top-down camera over the directory and zoom to fit it
1722
+ // - 2D + null: return to the centered top-down overview
1723
+ useEffect(() => {
1724
+ if (!hasAppliedInitial.current) return;
1725
+
1726
+ let newPos: {
1727
+ x: number;
1728
+ y: number;
1729
+ z: number;
1730
+ targetX: number;
1731
+ targetY: number;
1732
+ targetZ: number;
1733
+ };
1734
+
1735
+ if (isFlat) {
1736
+ if (focusTarget) {
1737
+ const perspCam = camera as THREE.PerspectiveCamera;
1738
+ const aspect = perspCam.aspect || 1;
1739
+ const fovRad = (50 * Math.PI) / 180;
1740
+ const tanHalfFov = Math.tan(fovRad / 2);
1741
+ const effectiveAspect = Math.min(1, aspect);
1742
+ // Same framing math as calculateFlatCameraHeight, but using the focus
1743
+ // region's size instead of citySize so the directory fills the view.
1744
+ const height = (focusTarget.size / (2 * tanHalfFov * effectiveAspect)) * 1.08;
1745
+ newPos = {
1746
+ x: focusTarget.x,
1747
+ y: height,
1748
+ z: focusTarget.z + 0.001,
1749
+ targetX: focusTarget.x,
1750
+ targetY: 0,
1751
+ targetZ: focusTarget.z,
1752
+ };
1753
+ } else {
1754
+ newPos = getInitial2DPosition();
1755
+ }
1756
+ } else if (focusTarget) {
1757
+ newPos = {
1758
+ x: focusTarget.x,
1759
+ y: Math.max(focusTarget.size * 1.5, 40),
1760
+ z: focusTarget.z + Math.max(focusTarget.size * 2, 50),
1761
+ targetX: focusTarget.x,
1762
+ targetY: 0,
1763
+ targetZ: focusTarget.z,
1764
+ };
1765
+ } else {
1766
+ newPos = {
1767
+ x: 0,
1768
+ y: maxBuildingHeight > 0 ? Math.max(citySize * 1.1, maxBuildingHeight * 2.5) : citySize * 1.1,
1769
+ z: citySize * 1.3,
1770
+ targetX: 0,
1771
+ targetY: 0,
1772
+ targetZ: 0,
1773
+ };
1774
+ }
1775
+
1776
+ api.start({
1777
+ camX: newPos.x,
1778
+ camY: newPos.y,
1779
+ camZ: newPos.z,
1780
+ lookX: newPos.targetX,
1781
+ lookY: newPos.targetY,
1782
+ lookZ: newPos.targetZ,
1783
+ });
1784
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1785
+ }, [focusTarget, isFlat]);
1786
+
1710
1787
  // Update camera each frame
1711
1788
  useFrame(() => {
1712
1789
  frameCount.current++;
@@ -1814,11 +1891,6 @@ const AnimatedCamera = React.memo(function AnimatedCamera({
1814
1891
  }
1815
1892
  // Handle position animation
1816
1893
  else if (isAnimatingRef.current) {
1817
- const springY = camY.get();
1818
- const currentY = camera.position.y;
1819
- if (Math.abs(springY - currentY) > 1) {
1820
- console.log('[useFrame] Spring animating - springY:', springY, 'currentY:', currentY);
1821
- }
1822
1894
  camera.position.set(camX.get(), camY.get(), camZ.get());
1823
1895
  controlsRef.current.target.set(lookX.get(), lookY.get(), lookZ.get());
1824
1896
  controlsRef.current.update();
@@ -2188,13 +2260,15 @@ const AnimatedCamera = React.memo(function AnimatedCamera({
2188
2260
  </>
2189
2261
  );
2190
2262
  }, (prevProps, nextProps) => {
2191
- // Custom comparison: only re-render if isFlat, citySize, maxBuildingHeight, or cameraControls changes
2192
- // Skip re-render when only focusTarget changes (handled internally by useEffect on isFlat)
2263
+ // Custom comparison: re-render when isFlat, citySize, maxBuildingHeight,
2264
+ // cameraControls, or focusTarget change. focusTarget is included so the
2265
+ // useEffect that animates the camera on focus changes actually fires.
2193
2266
  return (
2194
2267
  prevProps.isFlat === nextProps.isFlat &&
2195
2268
  prevProps.citySize === nextProps.citySize &&
2196
2269
  prevProps.maxBuildingHeight === nextProps.maxBuildingHeight &&
2197
- prevProps.cameraControls === nextProps.cameraControls
2270
+ prevProps.cameraControls === nextProps.cameraControls &&
2271
+ prevProps.focusTarget === nextProps.focusTarget
2198
2272
  );
2199
2273
  });
2200
2274
 
@@ -2204,6 +2278,7 @@ interface InfoPanelProps {
2204
2278
  }
2205
2279
 
2206
2280
  function InfoPanel({ building }: InfoPanelProps) {
2281
+ const { theme } = useTheme();
2207
2282
  if (!building) return null;
2208
2283
 
2209
2284
  const fileName = building.path.split('/').pop();
@@ -2215,26 +2290,26 @@ function InfoPanel({ building }: InfoPanelProps) {
2215
2290
  position: 'absolute',
2216
2291
  bottom: 16,
2217
2292
  left: 60,
2218
- background: 'rgba(15, 23, 42, 0.9)',
2219
- border: '1px solid #334155',
2220
- borderRadius: 8,
2293
+ background: `color-mix(in oklab, ${theme.colors.background} 90%, transparent)`,
2294
+ border: `1px solid ${theme.colors.border}`,
2295
+ borderRadius: theme.radii[4],
2221
2296
  padding: '12px 16px',
2222
- color: '#e2e8f0',
2223
- fontSize: 14,
2224
- fontFamily: 'monospace',
2297
+ color: theme.colors.text,
2298
+ fontSize: theme.fontSizes[1],
2299
+ fontFamily: theme.fonts.monospace,
2225
2300
  maxWidth: 400,
2226
2301
  pointerEvents: 'none',
2227
2302
  }}
2228
2303
  >
2229
- <div style={{ fontWeight: 600, marginBottom: 4 }}>{fileName}</div>
2230
- <div style={{ color: '#94a3b8', fontSize: 12 }}>{dirPath}</div>
2304
+ <div style={{ fontWeight: theme.fontWeights.semibold, marginBottom: 4 }}>{fileName}</div>
2305
+ <div style={{ color: theme.colors.textMuted, fontSize: theme.fontSizes[0] }}>{dirPath}</div>
2231
2306
  <div
2232
2307
  style={{
2233
- color: '#64748b',
2308
+ color: theme.colors.textTertiary,
2234
2309
  fontSize: 11,
2235
2310
  marginTop: 4,
2236
2311
  display: 'flex',
2237
- gap: 12,
2312
+ gap: theme.space[3],
2238
2313
  }}
2239
2314
  >
2240
2315
  {building.lineCount !== undefined && (
@@ -2255,63 +2330,65 @@ interface ControlsOverlayProps {
2255
2330
  }
2256
2331
 
2257
2332
  function ControlsOverlay({ isFlat, onToggle, onResetCamera, onLookDown }: ControlsOverlayProps) {
2333
+ const { theme } = useTheme();
2258
2334
  const buttonStyle = {
2259
- background: 'rgba(15, 23, 42, 0.9)',
2260
- border: '1px solid #334155',
2261
- borderRadius: 8,
2335
+ background: `color-mix(in oklab, ${theme.colors.background} 90%, transparent)`,
2336
+ border: `1px solid ${theme.colors.border}`,
2337
+ borderRadius: theme.radii[4],
2262
2338
  padding: '10px',
2263
- color: '#e2e8f0',
2264
- fontSize: 14,
2339
+ color: theme.colors.text,
2340
+ fontSize: theme.fontSizes[1],
2265
2341
  cursor: 'pointer',
2266
2342
  display: 'flex',
2267
2343
  alignItems: 'center',
2268
2344
  justifyContent: 'center',
2269
2345
  width: 40,
2270
2346
  height: 40,
2271
- fontWeight: 500,
2347
+ fontWeight: theme.fontWeights.medium,
2272
2348
  };
2273
2349
 
2274
2350
  return (
2275
2351
  <>
2276
- {/* 2D/3D Toggle - Top Left */}
2352
+ {/* 2D/3D Toggle - Bottom Right (moved from top-left to leave room
2353
+ for story-level overlays like the focus-directory readout). */}
2277
2354
  <button
2278
2355
  onClick={onToggle}
2279
2356
  style={{
2280
2357
  ...buttonStyle,
2281
2358
  position: 'absolute',
2282
- top: 8,
2283
- left: 8,
2359
+ bottom: 8,
2360
+ right: 8,
2284
2361
  }}
2285
2362
  >
2286
2363
  {isFlat ? '3D' : '2D'}
2287
2364
  </button>
2288
2365
 
2289
- {/* Reset Camera - Top Right */}
2366
+ {/* Look Down - Bottom Left */}
2290
2367
  <button
2291
- onClick={onResetCamera}
2368
+ onClick={onLookDown}
2292
2369
  style={{
2293
2370
  ...buttonStyle,
2294
2371
  position: 'absolute',
2295
- top: 8,
2296
- right: 8,
2372
+ bottom: 8,
2373
+ left: 8,
2297
2374
  }}
2298
- title="Reset View"
2375
+ title="Look down"
2299
2376
  >
2300
-
2377
+
2301
2378
  </button>
2302
2379
 
2303
- {/* Look Down - Bottom Left */}
2380
+ {/* Reset Camera - Bottom Left (right of Look Down) */}
2304
2381
  <button
2305
- onClick={onLookDown}
2382
+ onClick={onResetCamera}
2306
2383
  style={{
2307
2384
  ...buttonStyle,
2308
2385
  position: 'absolute',
2309
2386
  bottom: 8,
2310
- left: 8,
2387
+ left: 56,
2311
2388
  }}
2312
- title="Look down"
2389
+ title="Reset View"
2313
2390
  >
2314
-
2391
+
2315
2392
  </button>
2316
2393
  </>
2317
2394
  );
@@ -2321,7 +2398,7 @@ function ControlsOverlay({ isFlat, onToggle, onResetCamera, onLookDown }: Contro
2321
2398
  interface CitySceneProps {
2322
2399
  cityData: CityData;
2323
2400
  onBuildingHover?: (building: CityBuilding | null) => void;
2324
- onBuildingClick?: (building: CityBuilding) => void;
2401
+ onBuildingClick?: (building: CityBuilding, event: MouseEvent) => void;
2325
2402
  hoveredBuilding: CityBuilding | null;
2326
2403
  selectedBuilding: CityBuilding | null;
2327
2404
  growProgress: number;
@@ -2472,14 +2549,32 @@ function CityScene({
2472
2549
 
2473
2550
  // Case 3: Switching between directories (dirA -> dirB)
2474
2551
  if (prevFocus !== null && focusDirectory !== null) {
2475
- // Phase 1: Zoom camera out
2552
+ // Direct transition when the two directories are visually adjacent:
2553
+ // - parent ↔ child (one is a prefix of the other)
2554
+ // - immediate siblings (same parent folder)
2555
+ // In both cases the new directory is already in or near the current
2556
+ // view, so a zoom-out detour would feel like extra travel.
2557
+ const isDescendant = focusDirectory.startsWith(prevFocus + '/');
2558
+ const isAncestor = prevFocus.startsWith(focusDirectory + '/');
2559
+ const parentOf = (p: string) => {
2560
+ const i = p.lastIndexOf('/');
2561
+ return i >= 0 ? p.slice(0, i) : '';
2562
+ };
2563
+ const isSibling = parentOf(prevFocus) === parentOf(focusDirectory);
2564
+ if (isDescendant || isAncestor || isSibling) {
2565
+ setBuildingFocusDirectory(focusDirectory);
2566
+ setBuildingFocusColor(focusColor ?? null);
2567
+ setCameraFocusDirectory(focusDirectory);
2568
+ return;
2569
+ }
2570
+
2571
+ // Unrelated branches — keep the 3-phase out/in transition so the
2572
+ // long camera flight stays legible.
2476
2573
  setCameraFocusDirectory(null);
2477
- // Phase 2: After zoom-out, collapse/expand buildings with new color
2478
2574
  const timer1 = setTimeout(() => {
2479
2575
  setBuildingFocusDirectory(focusDirectory);
2480
2576
  setBuildingFocusColor(focusColor ?? null);
2481
2577
  }, 500);
2482
- // Phase 3: After collapse settles, zoom camera into new directory
2483
2578
  const timer2 = setTimeout(() => {
2484
2579
  setCameraFocusDirectory(focusDirectory);
2485
2580
  }, 1100); // 500ms zoom-out + 600ms collapse
@@ -2677,19 +2772,26 @@ function CityScene({
2677
2772
  const requested = panel.labelSize ?? Math.min(w, d) / 6;
2678
2773
  const labelSize = Math.max(4, Math.min(tileMax, requested));
2679
2774
 
2775
+ const interactive = Boolean(panel.onClick || panel.onDoubleClick);
2680
2776
  const handleClick = panel.onClick
2681
2777
  ? (e: ThreeEvent<MouseEvent>) => {
2682
2778
  e.stopPropagation();
2683
- panel.onClick!();
2779
+ panel.onClick!(e.nativeEvent);
2684
2780
  }
2685
2781
  : undefined;
2686
- const handlePointerOver = panel.onClick
2782
+ const handleDoubleClick = panel.onDoubleClick
2783
+ ? (e: ThreeEvent<MouseEvent>) => {
2784
+ e.stopPropagation();
2785
+ panel.onDoubleClick!(e.nativeEvent);
2786
+ }
2787
+ : undefined;
2788
+ const handlePointerOver = interactive
2687
2789
  ? (e: ThreeEvent<PointerEvent>) => {
2688
2790
  e.stopPropagation();
2689
2791
  document.body.style.cursor = 'pointer';
2690
2792
  }
2691
2793
  : undefined;
2692
- const handlePointerOut = panel.onClick
2794
+ const handlePointerOut = interactive
2693
2795
  ? () => {
2694
2796
  document.body.style.cursor = '';
2695
2797
  }
@@ -2701,6 +2803,7 @@ function CityScene({
2701
2803
  position={[cx, y, cz]}
2702
2804
  renderOrder={10}
2703
2805
  onClick={handleClick}
2806
+ onDoubleClick={handleDoubleClick}
2704
2807
  onPointerOver={handlePointerOver}
2705
2808
  onPointerOut={handlePointerOut}
2706
2809
  >
@@ -2712,9 +2815,54 @@ function CityScene({
2712
2815
  depthWrite={isOpaque}
2713
2816
  />
2714
2817
  </mesh>
2818
+ {panel.displayLabel && (
2819
+ <>
2820
+ <Text
2821
+ position={[cx, topY + 0.05, cz - labelSize * 0.6]}
2822
+ rotation={[-Math.PI / 2, 0, 0]}
2823
+ fontSize={labelSize}
2824
+ color={panel.displayLabelColor ?? panel.labelColor ?? '#ffffff'}
2825
+ anchorX="center"
2826
+ anchorY="middle"
2827
+ maxWidth={w * 0.9}
2828
+ textAlign="center"
2829
+ renderOrder={11}
2830
+ frustumCulled={false}
2831
+ >
2832
+ {panel.displayLabel}
2833
+ <meshBasicMaterial
2834
+ attach="material"
2835
+ color={panel.displayLabelColor ?? panel.labelColor ?? '#ffffff'}
2836
+ depthWrite={false}
2837
+ depthTest={false}
2838
+ />
2839
+ </Text>
2840
+ {/* Underline rendered as a thin plane just below the
2841
+ displayLabel. Width approximated from character count
2842
+ (~0.55em advance) and clamped to the panel footprint. */}
2843
+ <mesh
2844
+ position={[cx, topY + 0.06, cz - labelSize * 0.05]}
2845
+ rotation={[-Math.PI / 2, 0, 0]}
2846
+ renderOrder={11}
2847
+ >
2848
+ <planeGeometry
2849
+ args={[
2850
+ Math.min(w * 0.9, panel.displayLabel.length * labelSize * 0.55),
2851
+ labelSize * 0.06,
2852
+ ]}
2853
+ />
2854
+ <meshBasicMaterial
2855
+ color={panel.displayLabelColor ?? panel.labelColor ?? '#ffffff'}
2856
+ depthWrite={false}
2857
+ depthTest={false}
2858
+ transparent
2859
+ />
2860
+ </mesh>
2861
+ </>
2862
+ )}
2715
2863
  {panel.label && (
2716
2864
  <Text
2717
- position={[cx, topY + 0.05, cz]}
2865
+ position={[cx, topY + 0.05, cz + (panel.displayLabel ? labelSize * 0.6 : 0)]}
2718
2866
  rotation={[-Math.PI / 2, 0, 0]}
2719
2867
  fontSize={labelSize}
2720
2868
  color={panel.labelColor ?? '#ffffff'}
@@ -2753,7 +2901,7 @@ export interface FileCity3DProps {
2753
2901
  /** Height of the container */
2754
2902
  height?: number | string;
2755
2903
  /** Callback when a building is clicked */
2756
- onBuildingClick?: (building: CityBuilding) => void;
2904
+ onBuildingClick?: (building: CityBuilding, event: MouseEvent) => void;
2757
2905
  /** CSS class name */
2758
2906
  className?: string;
2759
2907
  /** Inline styles */