@principal-ai/file-city-react 0.5.9 → 0.5.11

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.
@@ -96,6 +96,12 @@ export interface FileCity3DProps {
96
96
  focusDirectory?: string | null;
97
97
  /** Callback when user clicks on a district to navigate */
98
98
  onDirectorySelect?: (directory: string | null) => void;
99
+ /** Background color for the canvas container */
100
+ backgroundColor?: string;
101
+ /** Text color for secondary/placeholder text */
102
+ textColor?: string;
103
+ /** Currently selected building (controlled by host) */
104
+ selectedBuilding?: CityBuilding | null;
99
105
  }
100
106
  /**
101
107
  * FileCity3D - 3D visualization of codebase structure
@@ -103,6 +109,6 @@ export interface FileCity3DProps {
103
109
  * Renders CityData as an interactive 3D city where buildings represent files
104
110
  * and their height corresponds to line count or file size.
105
111
  */
106
- export declare function FileCity3D({ cityData, width, height, onBuildingClick, className, style, animation, isGrown: externalIsGrown, onGrowChange, showControls, highlightLayers, isolationMode, dimOpacity, isLoading, loadingMessage, emptyMessage, heightScaling, linearScale, focusDirectory, onDirectorySelect, }: FileCity3DProps): import("react/jsx-runtime").JSX.Element;
112
+ export declare function FileCity3D({ cityData, width, height, onBuildingClick, className, style, animation, isGrown: externalIsGrown, onGrowChange, showControls, highlightLayers, isolationMode, dimOpacity, isLoading, loadingMessage, emptyMessage, heightScaling, linearScale, focusDirectory, onDirectorySelect, backgroundColor, textColor, selectedBuilding, }: FileCity3DProps): import("react/jsx-runtime").JSX.Element;
107
113
  export default FileCity3D;
108
114
  //# sourceMappingURL=FileCity3D.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"FileCity3D.d.ts","sourceRoot":"","sources":["../../../src/components/FileCity3D/FileCity3D.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"FileCity3D.d.ts","sourceRoot":"","sources":["../../../src/components/FileCity3D/FileCity3D.tsx"],"names":[],"mappings":"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"}
@@ -7,11 +7,10 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
7
7
  *
8
8
  * Supports animated transition from 2D (flat) to 3D (grown buildings).
9
9
  */
10
- import { useMemo, useRef, useState, useEffect, useCallback, } from 'react';
10
+ import { useMemo, useRef, useState, useEffect, useCallback } from 'react';
11
11
  import { Canvas, useFrame, useThree } from '@react-three/fiber';
12
- import { useTheme } from '@principal-ade/industry-theme';
13
12
  import { animated, useSpring } from '@react-spring/three';
14
- import { OrbitControls, PerspectiveCamera, Text, RoundedBox, } from '@react-three/drei';
13
+ import { OrbitControls, PerspectiveCamera, Text, RoundedBox } from '@react-three/drei';
15
14
  import { getFileConfig } from '@principal-ai/file-city-builder';
16
15
  import * as THREE from 'three';
17
16
  const DEFAULT_ANIMATION = {
@@ -182,8 +181,7 @@ function getHighlightForPath(path, layers) {
182
181
  if (item.type === 'file' && item.path === path) {
183
182
  return { color: layer.color, opacity: layer.opacity ?? 1 };
184
183
  }
185
- if (item.type === 'directory' &&
186
- (path === item.path || path.startsWith(item.path + '/'))) {
184
+ if (item.type === 'directory' && (path === item.path || path.startsWith(item.path + '/'))) {
187
185
  return { color: layer.color, opacity: layer.opacity ?? 1 };
188
186
  }
189
187
  }
@@ -191,13 +189,13 @@ function getHighlightForPath(path, layers) {
191
189
  return null;
192
190
  }
193
191
  function hasActiveHighlights(layers) {
194
- return layers.some((layer) => layer.enabled && layer.items.length > 0);
192
+ return layers.some(layer => layer.enabled && layer.items.length > 0);
195
193
  }
196
194
  // Animated RoundedBox wrapper
197
195
  const AnimatedRoundedBox = animated(RoundedBox);
198
196
  // Animated meshStandardMaterial for opacity transitions
199
197
  const AnimatedMeshStandardMaterial = animated('meshStandardMaterial');
200
- function BuildingEdges({ buildings, growProgress, minHeight, baseOffset, springDuration, heightMultipliersRef }) {
198
+ function BuildingEdges({ buildings, growProgress, minHeight, baseOffset, springDuration, heightMultipliersRef, }) {
201
199
  const meshRef = useRef(null);
202
200
  const startTimeRef = useRef(null);
203
201
  const tempObject = useMemo(() => new THREE.Object3D(), []);
@@ -205,7 +203,7 @@ function BuildingEdges({ buildings, growProgress, minHeight, baseOffset, springD
205
203
  const numEdges = buildings.length * 4;
206
204
  // Pre-compute edge data
207
205
  const edgeData = useMemo(() => {
208
- return buildings.flatMap((data) => {
206
+ return buildings.flatMap(data => {
209
207
  const { width, depth, x, z, fullHeight, staggerDelayMs, buildingIndex } = data;
210
208
  const halfW = width / 2;
211
209
  const halfD = depth / 2;
@@ -261,7 +259,7 @@ function isPathInDirectory(path, directory) {
261
259
  return true;
262
260
  return path === directory || path.startsWith(directory + '/');
263
261
  }
264
- function InstancedBuildings({ buildings, centerOffset, onHover, onClick, hoveredIndex, growProgress, animationConfig, heightScaling, linearScale, staggerIndices, focusDirectory, highlightLayers, isolationMode, }) {
262
+ function InstancedBuildings({ buildings, centerOffset, onHover, onClick, hoveredIndex, selectedIndex, growProgress, animationConfig, heightScaling, linearScale, staggerIndices, focusDirectory, highlightLayers, isolationMode, }) {
265
263
  const meshRef = useRef(null);
266
264
  const startTimeRef = useRef(null);
267
265
  const tempObject = useMemo(() => new THREE.Object3D(), []);
@@ -276,7 +274,8 @@ function InstancedBuildings({ buildings, centerOffset, onHover, onClick, hovered
276
274
  // Initialize height multiplier arrays
277
275
  useEffect(() => {
278
276
  if (buildings.length > 0) {
279
- if (!heightMultipliersRef.current || heightMultipliersRef.current.length !== buildings.length) {
277
+ if (!heightMultipliersRef.current ||
278
+ heightMultipliersRef.current.length !== buildings.length) {
280
279
  heightMultipliersRef.current = new Float32Array(buildings.length).fill(1);
281
280
  targetMultipliersRef.current = new Float32Array(buildings.length).fill(1);
282
281
  }
@@ -323,7 +322,14 @@ function InstancedBuildings({ buildings, centerOffset, onHover, onClick, hovered
323
322
  staggerDelayMs,
324
323
  };
325
324
  });
326
- }, [buildings, centerOffset, heightScaling, linearScale, staggerIndices, animationConfig.staggerDelay]);
325
+ }, [
326
+ buildings,
327
+ centerOffset,
328
+ heightScaling,
329
+ linearScale,
330
+ staggerIndices,
331
+ animationConfig.staggerDelay,
332
+ ]);
327
333
  const minHeight = 0.3;
328
334
  const baseOffset = 0.2;
329
335
  const tension = animationConfig.tension || 120;
@@ -389,7 +395,8 @@ function InstancedBuildings({ buildings, centerOffset, onHover, onClick, hovered
389
395
  const height = animProgress * fullHeight * newMultiplier + minHeight;
390
396
  const yPosition = height / 2 + baseOffset;
391
397
  const isHovered = hoveredIndex === data.index;
392
- const scale = isHovered ? 1.05 : 1;
398
+ const isSelected = selectedIndex === data.index;
399
+ const scale = isSelected ? 1.08 : isHovered ? 1.05 : 1;
393
400
  tempObject.position.set(x, yPosition, z);
394
401
  tempObject.scale.set(width * scale, height, depth * scale);
395
402
  tempObject.updateMatrix();
@@ -404,7 +411,10 @@ function InstancedBuildings({ buildings, centerOffset, onHover, onClick, hovered
404
411
  tempColor.g = tempColor.g * (1 - grayAmount) + gray * grayAmount;
405
412
  tempColor.b = tempColor.b * (1 - grayAmount) + gray * grayAmount;
406
413
  }
407
- if (isHovered) {
414
+ if (isSelected) {
415
+ tempColor.multiplyScalar(1.4);
416
+ }
417
+ else if (isHovered) {
408
418
  tempColor.multiplyScalar(1.2);
409
419
  }
410
420
  meshRef.current.setColorAt(instanceIndex, tempColor);
@@ -413,6 +423,8 @@ function InstancedBuildings({ buildings, centerOffset, onHover, onClick, hovered
413
423
  if (meshRef.current.instanceColor) {
414
424
  meshRef.current.instanceColor.needsUpdate = true;
415
425
  }
426
+ // Update bounding sphere for raycasting as buildings grow/animate
427
+ meshRef.current.computeBoundingSphere();
416
428
  });
417
429
  const handlePointerMove = useCallback((e) => {
418
430
  e.stopPropagation();
@@ -476,7 +488,7 @@ function AnimatedIcon({ x, z, targetHeight, iconSize, texture, opacity, growProg
476
488
  materialRef.current.opacity = opacity * animProgress;
477
489
  }
478
490
  });
479
- return (_jsx("sprite", { ref: spriteRef, position: [x, 0, z], scale: [iconSize, iconSize, 1], children: _jsx("spriteMaterial", { ref: materialRef, map: texture, transparent: true, opacity: 0, depthTest: true, depthWrite: false }) }));
491
+ return (_jsx("sprite", { ref: spriteRef, position: [x, 0, z], scale: [iconSize, iconSize, 1], raycast: () => null, children: _jsx("spriteMaterial", { ref: materialRef, map: texture, transparent: true, opacity: 0, depthTest: true, depthWrite: false }) }));
480
492
  }
481
493
  function BuildingIcons({ buildings, centerOffset, growProgress, heightScaling, linearScale, highlightLayers, isolationMode, hasActiveHighlights, staggerIndices, springDuration, staggerDelay, }) {
482
494
  // Pre-compute buildings with icons
@@ -510,7 +522,17 @@ function BuildingIcons({ buildings, centerOffset, growProgress, heightScaling, l
510
522
  };
511
523
  })
512
524
  .filter(Boolean);
513
- }, [buildings, centerOffset, highlightLayers, isolationMode, hasActiveHighlights, heightScaling, linearScale, staggerIndices, staggerDelay]);
525
+ }, [
526
+ buildings,
527
+ centerOffset,
528
+ highlightLayers,
529
+ isolationMode,
530
+ hasActiveHighlights,
531
+ heightScaling,
532
+ linearScale,
533
+ staggerIndices,
534
+ staggerDelay,
535
+ ]);
514
536
  // Don't render if no progress yet
515
537
  if (growProgress < 0.1)
516
538
  return null;
@@ -528,7 +550,7 @@ function BuildingIcons({ buildings, centerOffset, growProgress, heightScaling, l
528
550
  return (_jsx(AnimatedIcon, { x: x, z: z, targetHeight: targetHeight, iconSize: iconSize, texture: texture, opacity: opacity, growProgress: growProgress, staggerDelayMs: staggerDelayMs, springDuration: springDuration }, building.path));
529
551
  }) }));
530
552
  }
531
- function DistrictFloor({ district, centerOffset, opacity, }) {
553
+ function DistrictFloor({ district, centerOffset, opacity }) {
532
554
  const { worldBounds } = district;
533
555
  const width = worldBounds.maxX - worldBounds.minX;
534
556
  const depth = worldBounds.maxZ - worldBounds.minZ;
@@ -547,6 +569,7 @@ function AnimatedCamera({ citySize, isFlat, focusTarget }) {
547
569
  const { camera } = useThree();
548
570
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
549
571
  const controlsRef = useRef(null);
572
+ const isAnimatingRef = useRef(true);
550
573
  // Animated camera position and target
551
574
  const targetPos = useMemo(() => {
552
575
  if (focusTarget) {
@@ -583,10 +606,17 @@ function AnimatedCamera({ citySize, isFlat, focusTarget }) {
583
606
  lookY: targetPos.targetY,
584
607
  lookZ: targetPos.targetZ,
585
608
  config: { tension: 60, friction: 20 },
609
+ onStart: () => {
610
+ isAnimatingRef.current = true;
611
+ },
612
+ onRest: () => {
613
+ isAnimatingRef.current = false;
614
+ },
586
615
  });
587
- // Update camera each frame based on spring values
616
+ // Update camera each frame only while spring is animating
617
+ // Once animation settles, let OrbitControls handle user interaction
588
618
  useFrame(() => {
589
- if (!controlsRef.current)
619
+ if (!controlsRef.current || !isAnimatingRef.current)
590
620
  return;
591
621
  camera.position.set(camX.get(), camY.get(), camZ.get());
592
622
  controlsRef.current.target.set(lookX.get(), lookY.get(), lookZ.get());
@@ -642,9 +672,9 @@ function InfoPanel({ building }) {
642
672
  marginTop: 4,
643
673
  display: 'flex',
644
674
  gap: 12,
645
- }, children: [building.lineCount !== undefined && (_jsxs("span", { children: [building.lineCount.toLocaleString(), " lines"] })), building.size !== undefined && (_jsxs("span", { children: [(building.size / 1024).toFixed(1), " KB"] }))] })] }));
675
+ }, children: [building.lineCount !== undefined && (_jsxs("span", { children: [building.lineCount.toLocaleString(), " lines"] })), building.size !== undefined && _jsxs("span", { children: [(building.size / 1024).toFixed(1), " KB"] })] })] }));
646
676
  }
647
- function ControlsOverlay({ isFlat, onToggle, onResetCamera, }) {
677
+ function ControlsOverlay({ isFlat, onToggle, onResetCamera }) {
648
678
  const buttonStyle = {
649
679
  background: 'rgba(15, 23, 42, 0.9)',
650
680
  border: '1px solid #334155',
@@ -665,7 +695,7 @@ function ControlsOverlay({ isFlat, onToggle, onResetCamera, }) {
665
695
  gap: 8,
666
696
  }, children: [_jsx("button", { onClick: onResetCamera, style: buttonStyle, children: "Reset View" }), _jsx("button", { onClick: onToggle, style: buttonStyle, children: isFlat ? 'Grow to 3D' : 'Flatten to 2D' })] }));
667
697
  }
668
- function CityScene({ cityData, onBuildingHover, onBuildingClick, hoveredBuilding, growProgress, animationConfig, highlightLayers, isolationMode, heightScaling, linearScale, focusDirectory, }) {
698
+ function CityScene({ cityData, onBuildingHover, onBuildingClick, hoveredBuilding, selectedBuilding, growProgress, animationConfig, highlightLayers, isolationMode, heightScaling, linearScale, focusDirectory, }) {
669
699
  const centerOffset = useMemo(() => ({
670
700
  x: (cityData.bounds.minX + cityData.bounds.maxX) / 2,
671
701
  z: (cityData.bounds.minZ + cityData.bounds.maxZ) / 2,
@@ -747,7 +777,7 @@ function CityScene({ cityData, onBuildingHover, onBuildingClick, hoveredBuilding
747
777
  const focusTarget = useMemo(() => {
748
778
  // Use camera focus directory for camera movement
749
779
  if (cameraFocusDirectory) {
750
- const focusedBuildings = cityData.buildings.filter((building) => isPathInDirectory(building.path, cameraFocusDirectory));
780
+ const focusedBuildings = cityData.buildings.filter(building => isPathInDirectory(building.path, cameraFocusDirectory));
751
781
  if (focusedBuildings.length === 0)
752
782
  return null;
753
783
  let minX = Infinity, maxX = -Infinity;
@@ -766,10 +796,11 @@ function CityScene({ cityData, onBuildingHover, onBuildingClick, hoveredBuilding
766
796
  const size = Math.max(maxX - minX, maxZ - minZ);
767
797
  return { x: centerX, z: centerZ, size };
768
798
  }
769
- // Priority 2: highlight layers
770
- if (!activeHighlights)
799
+ // Priority 2: highlight layers (only if no focusDirectory is pending)
800
+ // Don't focus on highlights if we're waiting for cameraFocusDirectory to catch up
801
+ if (!activeHighlights || focusDirectory)
771
802
  return null;
772
- const highlightedBuildings = cityData.buildings.filter((building) => {
803
+ const highlightedBuildings = cityData.buildings.filter(building => {
773
804
  const highlight = getHighlightForPath(building.path, highlightLayers);
774
805
  return highlight !== null;
775
806
  });
@@ -790,14 +821,21 @@ function CityScene({ cityData, onBuildingHover, onBuildingClick, hoveredBuilding
790
821
  const centerZ = (minZ + maxZ) / 2;
791
822
  const size = Math.max(maxX - minX, maxZ - minZ);
792
823
  return { x: centerX, z: centerZ, size };
793
- }, [cameraFocusDirectory, activeHighlights, cityData.buildings, highlightLayers, centerOffset, isPathInDirectory]);
824
+ }, [
825
+ cameraFocusDirectory,
826
+ focusDirectory,
827
+ activeHighlights,
828
+ cityData.buildings,
829
+ highlightLayers,
830
+ centerOffset,
831
+ isPathInDirectory,
832
+ ]);
794
833
  const staggerIndices = useMemo(() => {
795
834
  const centerX = (cityData.bounds.minX + cityData.bounds.maxX) / 2;
796
835
  const centerZ = (cityData.bounds.minZ + cityData.bounds.maxZ) / 2;
797
836
  const withDistance = cityData.buildings.map((b, originalIndex) => ({
798
837
  originalIndex,
799
- distance: Math.sqrt(Math.pow(b.position.x - centerX, 2) +
800
- Math.pow(b.position.z - centerZ, 2)),
838
+ distance: Math.sqrt(Math.pow(b.position.x - centerX, 2) + Math.pow(b.position.z - centerZ, 2)),
801
839
  }));
802
840
  withDistance.sort((a, b) => a.distance - b.distance);
803
841
  const indices = new Array(cityData.buildings.length);
@@ -809,13 +847,18 @@ function CityScene({ cityData, onBuildingHover, onBuildingClick, hoveredBuilding
809
847
  const hoveredIndex = useMemo(() => {
810
848
  if (!hoveredBuilding)
811
849
  return null;
812
- return cityData.buildings.findIndex((b) => b.path === hoveredBuilding.path);
850
+ return cityData.buildings.findIndex(b => b.path === hoveredBuilding.path);
813
851
  }, [hoveredBuilding, cityData.buildings]);
852
+ const selectedIndex = useMemo(() => {
853
+ if (!selectedBuilding)
854
+ return null;
855
+ return cityData.buildings.findIndex(b => b.path === selectedBuilding.path);
856
+ }, [selectedBuilding, cityData.buildings]);
814
857
  // Calculate spring duration for animation sync
815
858
  const tension = animationConfig.tension || 120;
816
859
  const friction = animationConfig.friction || 14;
817
860
  const springDuration = Math.sqrt(1 / (tension * 0.001)) * friction * 20;
818
- return (_jsxs(_Fragment, { children: [_jsx(AnimatedCamera, { citySize: citySize, isFlat: growProgress === 0, focusTarget: focusTarget }), _jsx("ambientLight", { intensity: 1.2 }), _jsx("hemisphereLight", { args: ['#ddeeff', '#667788', 0.8], position: [0, citySize, 0] }), _jsx("directionalLight", { position: [citySize, citySize * 1.5, citySize * 0.5], intensity: 2, castShadow: true, "shadow-mapSize": [2048, 2048] }), _jsx("directionalLight", { position: [-citySize * 0.5, citySize * 0.8, -citySize * 0.5], intensity: 1 }), _jsx("directionalLight", { position: [citySize * 0.3, citySize, citySize], intensity: 0.6 }), cityData.districts.map((district) => (_jsx(DistrictFloor, { district: district, centerOffset: centerOffset, opacity: 1 }, district.path))), _jsx(InstancedBuildings, { buildings: cityData.buildings, centerOffset: centerOffset, onHover: onBuildingHover, onClick: onBuildingClick, hoveredIndex: hoveredIndex, growProgress: growProgress, animationConfig: animationConfig, heightScaling: heightScaling, linearScale: linearScale, staggerIndices: staggerIndices, focusDirectory: buildingFocusDirectory, highlightLayers: highlightLayers, isolationMode: isolationMode }), _jsx(BuildingIcons, { buildings: cityData.buildings, centerOffset: centerOffset, growProgress: growProgress, heightScaling: heightScaling, linearScale: linearScale, highlightLayers: highlightLayers, isolationMode: isolationMode, hasActiveHighlights: activeHighlights, staggerIndices: staggerIndices, springDuration: springDuration, staggerDelay: animationConfig.staggerDelay || 15 })] }));
861
+ return (_jsxs(_Fragment, { children: [_jsx(AnimatedCamera, { citySize: citySize, isFlat: growProgress === 0, focusTarget: focusTarget }), _jsx("ambientLight", { intensity: 1.2 }), _jsx("hemisphereLight", { args: ['#ddeeff', '#667788', 0.8], position: [0, citySize, 0] }), _jsx("directionalLight", { position: [citySize, citySize * 1.5, citySize * 0.5], intensity: 2, castShadow: true, "shadow-mapSize": [2048, 2048] }), _jsx("directionalLight", { position: [-citySize * 0.5, citySize * 0.8, -citySize * 0.5], intensity: 1 }), _jsx("directionalLight", { position: [citySize * 0.3, citySize, citySize], intensity: 0.6 }), cityData.districts.map(district => (_jsx(DistrictFloor, { district: district, centerOffset: centerOffset, opacity: 1 }, district.path))), _jsx(InstancedBuildings, { buildings: cityData.buildings, centerOffset: centerOffset, onHover: onBuildingHover, onClick: onBuildingClick, hoveredIndex: hoveredIndex, selectedIndex: selectedIndex, growProgress: growProgress, animationConfig: animationConfig, heightScaling: heightScaling, linearScale: linearScale, staggerIndices: staggerIndices, focusDirectory: buildingFocusDirectory, highlightLayers: highlightLayers, isolationMode: isolationMode }), _jsx(BuildingIcons, { buildings: cityData.buildings, centerOffset: centerOffset, growProgress: growProgress, heightScaling: heightScaling, linearScale: linearScale, highlightLayers: highlightLayers, isolationMode: isolationMode, hasActiveHighlights: activeHighlights, staggerIndices: staggerIndices, springDuration: springDuration, staggerDelay: animationConfig.staggerDelay || 15 })] }));
819
862
  }
820
863
  /**
821
864
  * FileCity3D - 3D visualization of codebase structure
@@ -823,8 +866,7 @@ function CityScene({ cityData, onBuildingHover, onBuildingClick, hoveredBuilding
823
866
  * Renders CityData as an interactive 3D city where buildings represent files
824
867
  * and their height corresponds to line count or file size.
825
868
  */
826
- export function FileCity3D({ cityData, width = '100%', height = 600, onBuildingClick, className, style, animation, isGrown: externalIsGrown, onGrowChange, showControls = true, highlightLayers = [], isolationMode = 'transparent', dimOpacity = 0.15, isLoading = false, loadingMessage = 'Loading file city...', emptyMessage = 'No file tree data available', heightScaling = 'logarithmic', linearScale = 0.05, focusDirectory = null, onDirectorySelect, }) {
827
- const { theme } = useTheme();
869
+ export function FileCity3D({ cityData, width = '100%', height = 600, onBuildingClick, className, style, animation, isGrown: externalIsGrown, onGrowChange, showControls = true, highlightLayers = [], isolationMode = 'transparent', dimOpacity = 0.15, isLoading = false, loadingMessage = 'Loading file city...', emptyMessage = 'No file tree data available', heightScaling = 'logarithmic', linearScale = 0.05, focusDirectory = null, onDirectorySelect, backgroundColor = '#0f172a', textColor = '#94a3b8', selectedBuilding = null, }) {
828
870
  const [hoveredBuilding, setHoveredBuilding] = useState(null);
829
871
  const [internalIsGrown, setInternalIsGrown] = useState(false);
830
872
  const animationConfig = useMemo(() => ({ ...DEFAULT_ANIMATION, ...animation }), [animation]);
@@ -854,12 +896,12 @@ export function FileCity3D({ cityData, width = '100%', height = 600, onBuildingC
854
896
  width,
855
897
  height,
856
898
  position: 'relative',
857
- background: theme.colors.background,
899
+ background: backgroundColor,
858
900
  overflow: 'hidden',
859
901
  display: 'flex',
860
902
  alignItems: 'center',
861
903
  justifyContent: 'center',
862
- color: theme.colors.textSecondary,
904
+ color: textColor,
863
905
  fontFamily: 'system-ui, sans-serif',
864
906
  fontSize: 14,
865
907
  ...style,
@@ -870,12 +912,12 @@ export function FileCity3D({ cityData, width = '100%', height = 600, onBuildingC
870
912
  width,
871
913
  height,
872
914
  position: 'relative',
873
- background: theme.colors.background,
915
+ background: backgroundColor,
874
916
  overflow: 'hidden',
875
917
  display: 'flex',
876
918
  alignItems: 'center',
877
919
  justifyContent: 'center',
878
- color: theme.colors.textSecondary,
920
+ color: textColor,
879
921
  fontFamily: 'system-ui, sans-serif',
880
922
  fontSize: 14,
881
923
  ...style,
@@ -885,7 +927,7 @@ export function FileCity3D({ cityData, width = '100%', height = 600, onBuildingC
885
927
  width,
886
928
  height,
887
929
  position: 'relative',
888
- background: theme.colors.background,
930
+ background: backgroundColor,
889
931
  overflow: 'hidden',
890
932
  ...style,
891
933
  }, children: [_jsx(Canvas, { shadows: true, style: {
@@ -894,6 +936,6 @@ export function FileCity3D({ cityData, width = '100%', height = 600, onBuildingC
894
936
  left: 0,
895
937
  width: '100%',
896
938
  height: '100%',
897
- }, children: _jsx(CityScene, { cityData: cityData, onBuildingHover: setHoveredBuilding, onBuildingClick: onBuildingClick, hoveredBuilding: hoveredBuilding, growProgress: growProgress, animationConfig: animationConfig, highlightLayers: highlightLayers, isolationMode: isolationMode, heightScaling: heightScaling, linearScale: linearScale, focusDirectory: focusDirectory }) }), _jsx(InfoPanel, { building: hoveredBuilding }), showControls && (_jsx(ControlsOverlay, { isFlat: !isGrown, onToggle: handleToggle, onResetCamera: resetCamera }))] }));
939
+ }, children: _jsx(CityScene, { cityData: cityData, onBuildingHover: setHoveredBuilding, onBuildingClick: onBuildingClick, hoveredBuilding: hoveredBuilding, selectedBuilding: selectedBuilding, growProgress: growProgress, animationConfig: animationConfig, highlightLayers: highlightLayers, isolationMode: isolationMode, heightScaling: heightScaling, linearScale: linearScale, focusDirectory: focusDirectory }) }), _jsx(InfoPanel, { building: selectedBuilding || hoveredBuilding }), showControls && (_jsx(ControlsOverlay, { isFlat: !isGrown, onToggle: handleToggle, onResetCamera: resetCamera }))] }));
898
940
  }
899
941
  export default FileCity3D;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@principal-ai/file-city-react",
3
- "version": "0.5.9",
3
+ "version": "0.5.11",
4
4
  "type": "module",
5
5
  "description": "React components for File City visualization",
6
6
  "main": "dist/index.js",
@@ -22,13 +22,11 @@
22
22
  "three": "^0.175.0"
23
23
  },
24
24
  "peerDependencies": {
25
- "@principal-ade/industry-theme": ">=0.1.3",
26
25
  "@principal-ai/alexandria-core-library": ">=0.1.36",
27
26
  "@principal-ai/file-city-builder": ">=0.4.5",
28
27
  "react": "^19.0.0"
29
28
  },
30
29
  "devDependencies": {
31
- "@principal-ade/industry-theme": "^0.1.8",
32
30
  "@principal-ai/alexandria-core-library": "^0.3.2",
33
31
  "@principal-ai/file-city-builder": "^0.4.5",
34
32
  "@storybook/addon-docs": "^10.1.2",