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

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,10 @@ 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;
99
103
  }
100
104
  /**
101
105
  * FileCity3D - 3D visualization of codebase structure
@@ -103,6 +107,6 @@ export interface FileCity3DProps {
103
107
  * Renders CityData as an interactive 3D city where buildings represent files
104
108
  * and their height corresponds to line count or file size.
105
109
  */
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;
110
+ 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, }: FileCity3DProps): import("react/jsx-runtime").JSX.Element;
107
111
  export default FileCity3D;
108
112
  //# 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;
@@ -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;
@@ -510,7 +516,17 @@ function BuildingIcons({ buildings, centerOffset, growProgress, heightScaling, l
510
516
  };
511
517
  })
512
518
  .filter(Boolean);
513
- }, [buildings, centerOffset, highlightLayers, isolationMode, hasActiveHighlights, heightScaling, linearScale, staggerIndices, staggerDelay]);
519
+ }, [
520
+ buildings,
521
+ centerOffset,
522
+ highlightLayers,
523
+ isolationMode,
524
+ hasActiveHighlights,
525
+ heightScaling,
526
+ linearScale,
527
+ staggerIndices,
528
+ staggerDelay,
529
+ ]);
514
530
  // Don't render if no progress yet
515
531
  if (growProgress < 0.1)
516
532
  return null;
@@ -528,7 +544,7 @@ function BuildingIcons({ buildings, centerOffset, growProgress, heightScaling, l
528
544
  return (_jsx(AnimatedIcon, { x: x, z: z, targetHeight: targetHeight, iconSize: iconSize, texture: texture, opacity: opacity, growProgress: growProgress, staggerDelayMs: staggerDelayMs, springDuration: springDuration }, building.path));
529
545
  }) }));
530
546
  }
531
- function DistrictFloor({ district, centerOffset, opacity, }) {
547
+ function DistrictFloor({ district, centerOffset, opacity }) {
532
548
  const { worldBounds } = district;
533
549
  const width = worldBounds.maxX - worldBounds.minX;
534
550
  const depth = worldBounds.maxZ - worldBounds.minZ;
@@ -642,9 +658,9 @@ function InfoPanel({ building }) {
642
658
  marginTop: 4,
643
659
  display: 'flex',
644
660
  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"] }))] })] }));
661
+ }, children: [building.lineCount !== undefined && (_jsxs("span", { children: [building.lineCount.toLocaleString(), " lines"] })), building.size !== undefined && _jsxs("span", { children: [(building.size / 1024).toFixed(1), " KB"] })] })] }));
646
662
  }
647
- function ControlsOverlay({ isFlat, onToggle, onResetCamera, }) {
663
+ function ControlsOverlay({ isFlat, onToggle, onResetCamera }) {
648
664
  const buttonStyle = {
649
665
  background: 'rgba(15, 23, 42, 0.9)',
650
666
  border: '1px solid #334155',
@@ -747,7 +763,7 @@ function CityScene({ cityData, onBuildingHover, onBuildingClick, hoveredBuilding
747
763
  const focusTarget = useMemo(() => {
748
764
  // Use camera focus directory for camera movement
749
765
  if (cameraFocusDirectory) {
750
- const focusedBuildings = cityData.buildings.filter((building) => isPathInDirectory(building.path, cameraFocusDirectory));
766
+ const focusedBuildings = cityData.buildings.filter(building => isPathInDirectory(building.path, cameraFocusDirectory));
751
767
  if (focusedBuildings.length === 0)
752
768
  return null;
753
769
  let minX = Infinity, maxX = -Infinity;
@@ -766,10 +782,11 @@ function CityScene({ cityData, onBuildingHover, onBuildingClick, hoveredBuilding
766
782
  const size = Math.max(maxX - minX, maxZ - minZ);
767
783
  return { x: centerX, z: centerZ, size };
768
784
  }
769
- // Priority 2: highlight layers
770
- if (!activeHighlights)
785
+ // Priority 2: highlight layers (only if no focusDirectory is pending)
786
+ // Don't focus on highlights if we're waiting for cameraFocusDirectory to catch up
787
+ if (!activeHighlights || focusDirectory)
771
788
  return null;
772
- const highlightedBuildings = cityData.buildings.filter((building) => {
789
+ const highlightedBuildings = cityData.buildings.filter(building => {
773
790
  const highlight = getHighlightForPath(building.path, highlightLayers);
774
791
  return highlight !== null;
775
792
  });
@@ -790,14 +807,21 @@ function CityScene({ cityData, onBuildingHover, onBuildingClick, hoveredBuilding
790
807
  const centerZ = (minZ + maxZ) / 2;
791
808
  const size = Math.max(maxX - minX, maxZ - minZ);
792
809
  return { x: centerX, z: centerZ, size };
793
- }, [cameraFocusDirectory, activeHighlights, cityData.buildings, highlightLayers, centerOffset, isPathInDirectory]);
810
+ }, [
811
+ cameraFocusDirectory,
812
+ focusDirectory,
813
+ activeHighlights,
814
+ cityData.buildings,
815
+ highlightLayers,
816
+ centerOffset,
817
+ isPathInDirectory,
818
+ ]);
794
819
  const staggerIndices = useMemo(() => {
795
820
  const centerX = (cityData.bounds.minX + cityData.bounds.maxX) / 2;
796
821
  const centerZ = (cityData.bounds.minZ + cityData.bounds.maxZ) / 2;
797
822
  const withDistance = cityData.buildings.map((b, originalIndex) => ({
798
823
  originalIndex,
799
- distance: Math.sqrt(Math.pow(b.position.x - centerX, 2) +
800
- Math.pow(b.position.z - centerZ, 2)),
824
+ distance: Math.sqrt(Math.pow(b.position.x - centerX, 2) + Math.pow(b.position.z - centerZ, 2)),
801
825
  }));
802
826
  withDistance.sort((a, b) => a.distance - b.distance);
803
827
  const indices = new Array(cityData.buildings.length);
@@ -809,13 +833,13 @@ function CityScene({ cityData, onBuildingHover, onBuildingClick, hoveredBuilding
809
833
  const hoveredIndex = useMemo(() => {
810
834
  if (!hoveredBuilding)
811
835
  return null;
812
- return cityData.buildings.findIndex((b) => b.path === hoveredBuilding.path);
836
+ return cityData.buildings.findIndex(b => b.path === hoveredBuilding.path);
813
837
  }, [hoveredBuilding, cityData.buildings]);
814
838
  // Calculate spring duration for animation sync
815
839
  const tension = animationConfig.tension || 120;
816
840
  const friction = animationConfig.friction || 14;
817
841
  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 })] }));
842
+ 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 })] }));
819
843
  }
820
844
  /**
821
845
  * FileCity3D - 3D visualization of codebase structure
@@ -823,8 +847,7 @@ function CityScene({ cityData, onBuildingHover, onBuildingClick, hoveredBuilding
823
847
  * Renders CityData as an interactive 3D city where buildings represent files
824
848
  * and their height corresponds to line count or file size.
825
849
  */
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();
850
+ 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', }) {
828
851
  const [hoveredBuilding, setHoveredBuilding] = useState(null);
829
852
  const [internalIsGrown, setInternalIsGrown] = useState(false);
830
853
  const animationConfig = useMemo(() => ({ ...DEFAULT_ANIMATION, ...animation }), [animation]);
@@ -854,12 +877,12 @@ export function FileCity3D({ cityData, width = '100%', height = 600, onBuildingC
854
877
  width,
855
878
  height,
856
879
  position: 'relative',
857
- background: theme.colors.background,
880
+ background: backgroundColor,
858
881
  overflow: 'hidden',
859
882
  display: 'flex',
860
883
  alignItems: 'center',
861
884
  justifyContent: 'center',
862
- color: theme.colors.textSecondary,
885
+ color: textColor,
863
886
  fontFamily: 'system-ui, sans-serif',
864
887
  fontSize: 14,
865
888
  ...style,
@@ -870,12 +893,12 @@ export function FileCity3D({ cityData, width = '100%', height = 600, onBuildingC
870
893
  width,
871
894
  height,
872
895
  position: 'relative',
873
- background: theme.colors.background,
896
+ background: backgroundColor,
874
897
  overflow: 'hidden',
875
898
  display: 'flex',
876
899
  alignItems: 'center',
877
900
  justifyContent: 'center',
878
- color: theme.colors.textSecondary,
901
+ color: textColor,
879
902
  fontFamily: 'system-ui, sans-serif',
880
903
  fontSize: 14,
881
904
  ...style,
@@ -885,7 +908,7 @@ export function FileCity3D({ cityData, width = '100%', height = 600, onBuildingC
885
908
  width,
886
909
  height,
887
910
  position: 'relative',
888
- background: theme.colors.background,
911
+ background: backgroundColor,
889
912
  overflow: 'hidden',
890
913
  ...style,
891
914
  }, children: [_jsx(Canvas, { shadows: true, style: {
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.10",
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",
@@ -7,22 +7,11 @@
7
7
  * Supports animated transition from 2D (flat) to 3D (grown buildings).
8
8
  */
9
9
 
10
- import React, {
11
- useMemo,
12
- useRef,
13
- useState,
14
- useEffect,
15
- useCallback,
16
- } from 'react';
10
+ import React, { useMemo, useRef, useState, useEffect, useCallback } from 'react';
17
11
  import { Canvas, useFrame, ThreeEvent, useThree } from '@react-three/fiber';
18
- import { useTheme } from '@principal-ade/industry-theme';
12
+
19
13
  import { animated, useSpring, config } from '@react-spring/three';
20
- import {
21
- OrbitControls,
22
- PerspectiveCamera,
23
- Text,
24
- RoundedBox,
25
- } from '@react-three/drei';
14
+ import { OrbitControls, PerspectiveCamera, Text, RoundedBox } from '@react-three/drei';
26
15
  import { getFileConfig } from '@principal-ai/file-city-builder';
27
16
  import type {
28
17
  CityData,
@@ -159,7 +148,7 @@ function isCodeFile(extension: string): boolean {
159
148
  function calculateBuildingHeight(
160
149
  building: CityBuilding,
161
150
  scaling: HeightScaling = 'logarithmic',
162
- linearScale: number = 0.05
151
+ linearScale: number = 0.05,
163
152
  ): number {
164
153
  const minHeight = 2;
165
154
 
@@ -194,15 +183,23 @@ function calculateBuildingHeight(
194
183
  const LUCIDE_ICONS: Record<string, string> = {
195
184
  Atom: '<circle cx="12" cy="12" r="1"/><path d="M20.2 20.2c2.04-2.03.02-7.36-4.5-11.9-4.54-4.52-9.87-6.54-11.9-4.5-2.04 2.03-.02 7.36 4.5 11.9 4.54 4.52 9.87 6.54 11.9 4.5Z"/><path d="M15.7 15.7c4.52-4.54 6.54-9.87 4.5-11.9-2.03-2.04-7.36-.02-11.9 4.5-4.52 4.54-6.54 9.87-4.5 11.9 2.03 2.04 7.36.02 11.9-4.5Z"/>',
196
185
  Lock: '<rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>',
197
- EyeOff: '<path d="M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49"/><path d="M14.084 14.158a3 3 0 0 1-4.242-4.242"/><path d="M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143"/><path d="m2 2 20 20"/>',
186
+ EyeOff:
187
+ '<path d="M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49"/><path d="M14.084 14.158a3 3 0 0 1-4.242-4.242"/><path d="M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143"/><path d="m2 2 20 20"/>',
198
188
  Key: '<path d="M2.586 17.414A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814a6.5 6.5 0 1 0-4-4z"/><circle cx="16.5" cy="7.5" r=".5" fill="currentColor"/>',
199
- GitBranch: '<line x1="6" x2="6" y1="3" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/>',
200
- TestTube: '<path d="M14.5 2v17.5c0 1.4-1.1 2.5-2.5 2.5c-1.4 0-2.5-1.1-2.5-2.5V2"/><path d="M8.5 2h7"/><path d="M14.5 16h-5"/>',
201
- FlaskConical: '<path d="M10 2v7.527a2 2 0 0 1-.211.896L4.72 20.55a1 1 0 0 0 .9 1.45h12.76a1 1 0 0 0 .9-1.45l-5.069-10.127A2 2 0 0 1 14 9.527V2"/><path d="M8.5 2h7"/><path d="M7 16h10"/>',
202
- BookText: '<path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"/><path d="M8 11h8"/><path d="M8 7h6"/>',
203
- BookOpen: '<path d="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/>',
204
- ScrollText: '<path d="M15 12h-5"/><path d="M15 8h-5"/><path d="M19 17V5a2 2 0 0 0-2-2H4"/><path d="M8 21h12a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1H11a1 1 0 0 0-1 1v1a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v2a1 1 0 0 0 1 1h3"/>',
205
- Settings: '<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/>',
189
+ GitBranch:
190
+ '<line x1="6" x2="6" y1="3" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/>',
191
+ TestTube:
192
+ '<path d="M14.5 2v17.5c0 1.4-1.1 2.5-2.5 2.5c-1.4 0-2.5-1.1-2.5-2.5V2"/><path d="M8.5 2h7"/><path d="M14.5 16h-5"/>',
193
+ FlaskConical:
194
+ '<path d="M10 2v7.527a2 2 0 0 1-.211.896L4.72 20.55a1 1 0 0 0 .9 1.45h12.76a1 1 0 0 0 .9-1.45l-5.069-10.127A2 2 0 0 1 14 9.527V2"/><path d="M8.5 2h7"/><path d="M7 16h10"/>',
195
+ BookText:
196
+ '<path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"/><path d="M8 11h8"/><path d="M8 7h6"/>',
197
+ BookOpen:
198
+ '<path d="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/>',
199
+ ScrollText:
200
+ '<path d="M15 12h-5"/><path d="M15 8h-5"/><path d="M19 17V5a2 2 0 0 0-2-2H4"/><path d="M8 21h12a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1H11a1 1 0 0 0-1 1v1a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v2a1 1 0 0 0 1 1h3"/>',
201
+ Settings:
202
+ '<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/>',
206
203
  Home: '<path d="M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8"/><path d="M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>',
207
204
  };
208
205
 
@@ -283,7 +280,7 @@ function getColorForFile(building: CityBuilding): string {
283
280
  */
284
281
  function getHighlightForPath(
285
282
  path: string,
286
- layers: HighlightLayer[]
283
+ layers: HighlightLayer[],
287
284
  ): { color: string; opacity: number } | null {
288
285
  for (const layer of layers) {
289
286
  if (!layer.enabled) continue;
@@ -292,10 +289,7 @@ function getHighlightForPath(
292
289
  if (item.type === 'file' && item.path === path) {
293
290
  return { color: layer.color, opacity: layer.opacity ?? 1 };
294
291
  }
295
- if (
296
- item.type === 'directory' &&
297
- (path === item.path || path.startsWith(item.path + '/'))
298
- ) {
292
+ if (item.type === 'directory' && (path === item.path || path.startsWith(item.path + '/'))) {
299
293
  return { color: layer.color, opacity: layer.opacity ?? 1 };
300
294
  }
301
295
  }
@@ -304,7 +298,7 @@ function getHighlightForPath(
304
298
  }
305
299
 
306
300
  function hasActiveHighlights(layers: HighlightLayer[]): boolean {
307
- return layers.some((layer) => layer.enabled && layer.items.length > 0);
301
+ return layers.some(layer => layer.enabled && layer.items.length > 0);
308
302
  }
309
303
 
310
304
  // Animated RoundedBox wrapper
@@ -336,7 +330,14 @@ interface BuildingEdgesProps {
336
330
  heightMultipliersRef: React.MutableRefObject<Float32Array | null>;
337
331
  }
338
332
 
339
- function BuildingEdges({ buildings, growProgress, minHeight, baseOffset, springDuration, heightMultipliersRef }: BuildingEdgesProps) {
333
+ function BuildingEdges({
334
+ buildings,
335
+ growProgress,
336
+ minHeight,
337
+ baseOffset,
338
+ springDuration,
339
+ heightMultipliersRef,
340
+ }: BuildingEdgesProps) {
340
341
  const meshRef = useRef<THREE.InstancedMesh>(null);
341
342
  const startTimeRef = useRef<number | null>(null);
342
343
  const tempObject = useMemo(() => new THREE.Object3D(), []);
@@ -346,7 +347,7 @@ function BuildingEdges({ buildings, growProgress, minHeight, baseOffset, springD
346
347
 
347
348
  // Pre-compute edge data
348
349
  const edgeData = useMemo(() => {
349
- return buildings.flatMap((data) => {
350
+ return buildings.flatMap(data => {
350
351
  const { width, depth, x, z, fullHeight, staggerDelayMs, buildingIndex } = data;
351
352
  const halfW = width / 2;
352
353
  const halfD = depth / 2;
@@ -471,7 +472,10 @@ function InstancedBuildings({
471
472
  // Initialize height multiplier arrays
472
473
  useEffect(() => {
473
474
  if (buildings.length > 0) {
474
- if (!heightMultipliersRef.current || heightMultipliersRef.current.length !== buildings.length) {
475
+ if (
476
+ !heightMultipliersRef.current ||
477
+ heightMultipliersRef.current.length !== buildings.length
478
+ ) {
475
479
  heightMultipliersRef.current = new Float32Array(buildings.length).fill(1);
476
480
  targetMultipliersRef.current = new Float32Array(buildings.length).fill(1);
477
481
  }
@@ -525,7 +529,14 @@ function InstancedBuildings({
525
529
  staggerDelayMs,
526
530
  };
527
531
  });
528
- }, [buildings, centerOffset, heightScaling, linearScale, staggerIndices, animationConfig.staggerDelay]);
532
+ }, [
533
+ buildings,
534
+ centerOffset,
535
+ heightScaling,
536
+ linearScale,
537
+ staggerIndices,
538
+ animationConfig.staggerDelay,
539
+ ]);
529
540
 
530
541
  const minHeight = 0.3;
531
542
  const baseOffset = 0.2;
@@ -588,7 +599,8 @@ function InstancedBuildings({
588
599
  // Animate height multiplier towards target
589
600
  const currentMultiplier = heightMultipliersRef.current![instanceIndex];
590
601
  const targetMultiplier = targetMultipliersRef.current![instanceIndex];
591
- const newMultiplier = currentMultiplier + (targetMultiplier - currentMultiplier) * collapseSpeed;
602
+ const newMultiplier =
603
+ currentMultiplier + (targetMultiplier - currentMultiplier) * collapseSpeed;
592
604
  heightMultipliersRef.current![instanceIndex] = newMultiplier;
593
605
 
594
606
  // Calculate grow animation progress
@@ -646,7 +658,7 @@ function InstancedBuildings({
646
658
  onHover?.(data.building);
647
659
  }
648
660
  },
649
- [buildingData, onHover]
661
+ [buildingData, onHover],
650
662
  );
651
663
 
652
664
  const handlePointerOut = useCallback(() => {
@@ -661,7 +673,7 @@ function InstancedBuildings({
661
673
  onClick?.(data.building);
662
674
  }
663
675
  },
664
- [buildingData, onClick]
676
+ [buildingData, onClick],
665
677
  );
666
678
 
667
679
  if (buildingData.length === 0) return null;
@@ -784,11 +796,7 @@ function AnimatedIcon({
784
796
  });
785
797
 
786
798
  return (
787
- <sprite
788
- ref={spriteRef}
789
- position={[x, 0, z]}
790
- scale={[iconSize, iconSize, 1]}
791
- >
799
+ <sprite ref={spriteRef} position={[x, 0, z]} scale={[iconSize, iconSize, 1]}>
792
800
  <spriteMaterial
793
801
  ref={materialRef}
794
802
  map={texture}
@@ -849,49 +857,61 @@ function BuildingIcons({
849
857
  };
850
858
  })
851
859
  .filter(Boolean) as Array<{
852
- building: CityBuilding;
853
- config: FileConfigResult;
854
- x: number;
855
- z: number;
856
- targetHeight: number;
857
- shouldDim: boolean;
858
- staggerDelayMs: number;
859
- }>;
860
- }, [buildings, centerOffset, highlightLayers, isolationMode, hasActiveHighlights, heightScaling, linearScale, staggerIndices, staggerDelay]);
860
+ building: CityBuilding;
861
+ config: FileConfigResult;
862
+ x: number;
863
+ z: number;
864
+ targetHeight: number;
865
+ shouldDim: boolean;
866
+ staggerDelayMs: number;
867
+ }>;
868
+ }, [
869
+ buildings,
870
+ centerOffset,
871
+ highlightLayers,
872
+ isolationMode,
873
+ hasActiveHighlights,
874
+ heightScaling,
875
+ linearScale,
876
+ staggerIndices,
877
+ staggerDelay,
878
+ ]);
861
879
 
862
880
  // Don't render if no progress yet
863
881
  if (growProgress < 0.1) return null;
864
882
 
865
883
  return (
866
884
  <>
867
- {buildingsWithIcons.map(({ building, config, x, z, targetHeight, shouldDim, staggerDelayMs }) => {
868
- const icon = config.icon!;
869
- const texture = getIconTexture(icon.name, icon.color || '#ffffff');
870
- if (!texture) return null;
871
-
872
- // Icon size based on building dimensions
873
- const [width] = building.dimensions;
874
- const baseSize = Math.max(width * 0.8, 6);
875
- const heightBoost = Math.min(targetHeight / 20, 3);
876
- const iconSize = (baseSize + heightBoost) * (icon.size || 1);
877
-
878
- const opacity = shouldDim && isolationMode === 'transparent' ? 0.3 : 1;
879
-
880
- return (
881
- <AnimatedIcon
882
- key={building.path}
883
- x={x}
884
- z={z}
885
- targetHeight={targetHeight}
886
- iconSize={iconSize}
887
- texture={texture}
888
- opacity={opacity}
889
- growProgress={growProgress}
890
- staggerDelayMs={staggerDelayMs}
891
- springDuration={springDuration}
892
- />
893
- );
894
- })}
885
+ {buildingsWithIcons.map(
886
+ ({ building, config, x, z, targetHeight, shouldDim, staggerDelayMs }) => {
887
+ const icon = config.icon!;
888
+ const texture = getIconTexture(icon.name, icon.color || '#ffffff');
889
+ if (!texture) return null;
890
+
891
+ // Icon size based on building dimensions
892
+ const [width] = building.dimensions;
893
+ const baseSize = Math.max(width * 0.8, 6);
894
+ const heightBoost = Math.min(targetHeight / 20, 3);
895
+ const iconSize = (baseSize + heightBoost) * (icon.size || 1);
896
+
897
+ const opacity = shouldDim && isolationMode === 'transparent' ? 0.3 : 1;
898
+
899
+ return (
900
+ <AnimatedIcon
901
+ key={building.path}
902
+ x={x}
903
+ z={z}
904
+ targetHeight={targetHeight}
905
+ iconSize={iconSize}
906
+ texture={texture}
907
+ opacity={opacity}
908
+ growProgress={growProgress}
909
+ staggerDelayMs={staggerDelayMs}
910
+ springDuration={springDuration}
911
+ />
912
+ );
913
+ },
914
+ )}
895
915
  </>
896
916
  );
897
917
  }
@@ -903,11 +923,7 @@ interface DistrictFloorProps {
903
923
  opacity: number;
904
924
  }
905
925
 
906
- function DistrictFloor({
907
- district,
908
- centerOffset,
909
- opacity,
910
- }: DistrictFloorProps) {
926
+ function DistrictFloor({ district, centerOffset, opacity }: DistrictFloorProps) {
911
927
  const { worldBounds } = district;
912
928
  const width = worldBounds.maxX - worldBounds.minX;
913
929
  const depth = worldBounds.maxZ - worldBounds.minZ;
@@ -921,15 +937,8 @@ function DistrictFloor({
921
937
 
922
938
  return (
923
939
  <group position={[centerX, 0, centerZ]}>
924
- <lineSegments
925
- rotation={[-Math.PI / 2, 0, 0]}
926
- position={[0, floorY, 0]}
927
- renderOrder={-1}
928
- >
929
- <edgesGeometry
930
- args={[new THREE.PlaneGeometry(width, depth)]}
931
- attach="geometry"
932
- />
940
+ <lineSegments rotation={[-Math.PI / 2, 0, 0]} position={[0, floorY, 0]} renderOrder={-1}>
941
+ <edgesGeometry args={[new THREE.PlaneGeometry(width, depth)]} attach="geometry" />
933
942
  <lineBasicMaterial color="#475569" depthWrite={false} />
934
943
  </lineSegments>
935
944
 
@@ -955,7 +964,7 @@ function DistrictFloor({
955
964
  interface FocusTarget {
956
965
  x: number;
957
966
  z: number;
958
- size: number; // Approximate size of the focused area
967
+ size: number; // Approximate size of the focused area
959
968
  }
960
969
 
961
970
  interface AnimatedCameraProps {
@@ -1074,8 +1083,7 @@ function InfoPanel({ building }: InfoPanelProps) {
1074
1083
 
1075
1084
  const fileName = building.path.split('/').pop();
1076
1085
  const dirPath = building.path.split('/').slice(0, -1).join('/');
1077
- const rawExt =
1078
- building.fileExtension || building.path.split('.').pop() || '';
1086
+ const rawExt = building.fileExtension || building.path.split('.').pop() || '';
1079
1087
  const ext = rawExt.replace(/^\./, '');
1080
1088
  const isCode = isCodeFile(ext);
1081
1089
 
@@ -1110,9 +1118,7 @@ function InfoPanel({ building }: InfoPanelProps) {
1110
1118
  {building.lineCount !== undefined && (
1111
1119
  <span>{building.lineCount.toLocaleString()} lines</span>
1112
1120
  )}
1113
- {building.size !== undefined && (
1114
- <span>{(building.size / 1024).toFixed(1)} KB</span>
1115
- )}
1121
+ {building.size !== undefined && <span>{(building.size / 1024).toFixed(1)} KB</span>}
1116
1122
  </div>
1117
1123
  </div>
1118
1124
  );
@@ -1125,11 +1131,7 @@ interface ControlsOverlayProps {
1125
1131
  onResetCamera: () => void;
1126
1132
  }
1127
1133
 
1128
- function ControlsOverlay({
1129
- isFlat,
1130
- onToggle,
1131
- onResetCamera,
1132
- }: ControlsOverlayProps) {
1134
+ function ControlsOverlay({ isFlat, onToggle, onResetCamera }: ControlsOverlayProps) {
1133
1135
  const buttonStyle = {
1134
1136
  background: 'rgba(15, 23, 42, 0.9)',
1135
1137
  border: '1px solid #334155',
@@ -1196,18 +1198,15 @@ function CityScene({
1196
1198
  x: (cityData.bounds.minX + cityData.bounds.maxX) / 2,
1197
1199
  z: (cityData.bounds.minZ + cityData.bounds.maxZ) / 2,
1198
1200
  }),
1199
- [cityData.bounds]
1201
+ [cityData.bounds],
1200
1202
  );
1201
1203
 
1202
1204
  const citySize = Math.max(
1203
1205
  cityData.bounds.maxX - cityData.bounds.minX,
1204
- cityData.bounds.maxZ - cityData.bounds.minZ
1206
+ cityData.bounds.maxZ - cityData.bounds.minZ,
1205
1207
  );
1206
1208
 
1207
- const activeHighlights = useMemo(
1208
- () => hasActiveHighlights(highlightLayers),
1209
- [highlightLayers]
1210
- );
1209
+ const activeHighlights = useMemo(() => hasActiveHighlights(highlightLayers), [highlightLayers]);
1211
1210
 
1212
1211
  // Helper to check if a path is inside a directory
1213
1212
  const isPathInDirectory = useCallback((path: string, directory: string) => {
@@ -1291,14 +1290,16 @@ function CityScene({
1291
1290
  const focusTarget = useMemo((): FocusTarget | null => {
1292
1291
  // Use camera focus directory for camera movement
1293
1292
  if (cameraFocusDirectory) {
1294
- const focusedBuildings = cityData.buildings.filter((building) =>
1295
- isPathInDirectory(building.path, cameraFocusDirectory)
1293
+ const focusedBuildings = cityData.buildings.filter(building =>
1294
+ isPathInDirectory(building.path, cameraFocusDirectory),
1296
1295
  );
1297
1296
 
1298
1297
  if (focusedBuildings.length === 0) return null;
1299
1298
 
1300
- let minX = Infinity, maxX = -Infinity;
1301
- let minZ = Infinity, maxZ = -Infinity;
1299
+ let minX = Infinity,
1300
+ maxX = -Infinity;
1301
+ let minZ = Infinity,
1302
+ maxZ = -Infinity;
1302
1303
 
1303
1304
  for (const building of focusedBuildings) {
1304
1305
  const x = building.position.x - centerOffset.x;
@@ -1318,18 +1319,21 @@ function CityScene({
1318
1319
  return { x: centerX, z: centerZ, size };
1319
1320
  }
1320
1321
 
1321
- // Priority 2: highlight layers
1322
- if (!activeHighlights) return null;
1322
+ // Priority 2: highlight layers (only if no focusDirectory is pending)
1323
+ // Don't focus on highlights if we're waiting for cameraFocusDirectory to catch up
1324
+ if (!activeHighlights || focusDirectory) return null;
1323
1325
 
1324
- const highlightedBuildings = cityData.buildings.filter((building) => {
1326
+ const highlightedBuildings = cityData.buildings.filter(building => {
1325
1327
  const highlight = getHighlightForPath(building.path, highlightLayers);
1326
1328
  return highlight !== null;
1327
1329
  });
1328
1330
 
1329
1331
  if (highlightedBuildings.length === 0) return null;
1330
1332
 
1331
- let minX = Infinity, maxX = -Infinity;
1332
- let minZ = Infinity, maxZ = -Infinity;
1333
+ let minX = Infinity,
1334
+ maxX = -Infinity;
1335
+ let minZ = Infinity,
1336
+ maxZ = -Infinity;
1333
1337
 
1334
1338
  for (const building of highlightedBuildings) {
1335
1339
  const x = building.position.x - centerOffset.x;
@@ -1347,7 +1351,15 @@ function CityScene({
1347
1351
  const size = Math.max(maxX - minX, maxZ - minZ);
1348
1352
 
1349
1353
  return { x: centerX, z: centerZ, size };
1350
- }, [cameraFocusDirectory, activeHighlights, cityData.buildings, highlightLayers, centerOffset, isPathInDirectory]);
1354
+ }, [
1355
+ cameraFocusDirectory,
1356
+ focusDirectory,
1357
+ activeHighlights,
1358
+ cityData.buildings,
1359
+ highlightLayers,
1360
+ centerOffset,
1361
+ isPathInDirectory,
1362
+ ]);
1351
1363
 
1352
1364
  const staggerIndices = useMemo(() => {
1353
1365
  const centerX = (cityData.bounds.minX + cityData.bounds.maxX) / 2;
@@ -1356,8 +1368,7 @@ function CityScene({
1356
1368
  const withDistance = cityData.buildings.map((b, originalIndex) => ({
1357
1369
  originalIndex,
1358
1370
  distance: Math.sqrt(
1359
- Math.pow(b.position.x - centerX, 2) +
1360
- Math.pow(b.position.z - centerZ, 2)
1371
+ Math.pow(b.position.x - centerX, 2) + Math.pow(b.position.z - centerZ, 2),
1361
1372
  ),
1362
1373
  }));
1363
1374
 
@@ -1373,7 +1384,7 @@ function CityScene({
1373
1384
 
1374
1385
  const hoveredIndex = useMemo(() => {
1375
1386
  if (!hoveredBuilding) return null;
1376
- return cityData.buildings.findIndex((b) => b.path === hoveredBuilding.path);
1387
+ return cityData.buildings.findIndex(b => b.path === hoveredBuilding.path);
1377
1388
  }, [hoveredBuilding, cityData.buildings]);
1378
1389
 
1379
1390
  // Calculate spring duration for animation sync
@@ -1386,10 +1397,7 @@ function CityScene({
1386
1397
  <AnimatedCamera citySize={citySize} isFlat={growProgress === 0} focusTarget={focusTarget} />
1387
1398
 
1388
1399
  <ambientLight intensity={1.2} />
1389
- <hemisphereLight
1390
- args={['#ddeeff', '#667788', 0.8]}
1391
- position={[0, citySize, 0]}
1392
- />
1400
+ <hemisphereLight args={['#ddeeff', '#667788', 0.8]} position={[0, citySize, 0]} />
1393
1401
  <directionalLight
1394
1402
  position={[citySize, citySize * 1.5, citySize * 0.5]}
1395
1403
  intensity={2}
@@ -1400,12 +1408,9 @@ function CityScene({
1400
1408
  position={[-citySize * 0.5, citySize * 0.8, -citySize * 0.5]}
1401
1409
  intensity={1}
1402
1410
  />
1403
- <directionalLight
1404
- position={[citySize * 0.3, citySize, citySize]}
1405
- intensity={0.6}
1406
- />
1411
+ <directionalLight position={[citySize * 0.3, citySize, citySize]} intensity={0.6} />
1407
1412
 
1408
- {cityData.districts.map((district) => (
1413
+ {cityData.districts.map(district => (
1409
1414
  <DistrictFloor
1410
1415
  key={district.path}
1411
1416
  district={district}
@@ -1492,6 +1497,10 @@ export interface FileCity3DProps {
1492
1497
  focusDirectory?: string | null;
1493
1498
  /** Callback when user clicks on a district to navigate */
1494
1499
  onDirectorySelect?: (directory: string | null) => void;
1500
+ /** Background color for the canvas container */
1501
+ backgroundColor?: string;
1502
+ /** Text color for secondary/placeholder text */
1503
+ textColor?: string;
1495
1504
  }
1496
1505
 
1497
1506
  /**
@@ -1521,20 +1530,15 @@ export function FileCity3D({
1521
1530
  linearScale = 0.05,
1522
1531
  focusDirectory = null,
1523
1532
  onDirectorySelect,
1533
+ backgroundColor = '#0f172a',
1534
+ textColor = '#94a3b8',
1524
1535
  }: FileCity3DProps) {
1525
- const { theme } = useTheme();
1526
- const [hoveredBuilding, setHoveredBuilding] = useState<CityBuilding | null>(
1527
- null
1528
- );
1536
+ const [hoveredBuilding, setHoveredBuilding] = useState<CityBuilding | null>(null);
1529
1537
  const [internalIsGrown, setInternalIsGrown] = useState(false);
1530
1538
 
1531
- const animationConfig = useMemo(
1532
- () => ({ ...DEFAULT_ANIMATION, ...animation }),
1533
- [animation]
1534
- );
1539
+ const animationConfig = useMemo(() => ({ ...DEFAULT_ANIMATION, ...animation }), [animation]);
1535
1540
 
1536
- const isGrown =
1537
- externalIsGrown !== undefined ? externalIsGrown : internalIsGrown;
1541
+ const isGrown = externalIsGrown !== undefined ? externalIsGrown : internalIsGrown;
1538
1542
  const setIsGrown = (value: boolean) => {
1539
1543
  setInternalIsGrown(value);
1540
1544
  onGrowChange?.(value);
@@ -1566,12 +1570,12 @@ export function FileCity3D({
1566
1570
  width,
1567
1571
  height,
1568
1572
  position: 'relative',
1569
- background: theme.colors.background,
1573
+ background: backgroundColor,
1570
1574
  overflow: 'hidden',
1571
1575
  display: 'flex',
1572
1576
  alignItems: 'center',
1573
1577
  justifyContent: 'center',
1574
- color: theme.colors.textSecondary,
1578
+ color: textColor,
1575
1579
  fontFamily: 'system-ui, sans-serif',
1576
1580
  fontSize: 14,
1577
1581
  ...style,
@@ -1590,12 +1594,12 @@ export function FileCity3D({
1590
1594
  width,
1591
1595
  height,
1592
1596
  position: 'relative',
1593
- background: theme.colors.background,
1597
+ background: backgroundColor,
1594
1598
  overflow: 'hidden',
1595
1599
  display: 'flex',
1596
1600
  alignItems: 'center',
1597
1601
  justifyContent: 'center',
1598
- color: theme.colors.textSecondary,
1602
+ color: textColor,
1599
1603
  fontFamily: 'system-ui, sans-serif',
1600
1604
  fontSize: 14,
1601
1605
  ...style,
@@ -1613,7 +1617,7 @@ export function FileCity3D({
1613
1617
  width,
1614
1618
  height,
1615
1619
  position: 'relative',
1616
- background: theme.colors.background,
1620
+ background: backgroundColor,
1617
1621
  overflow: 'hidden',
1618
1622
  ...style,
1619
1623
  }}
@@ -1644,11 +1648,7 @@ export function FileCity3D({
1644
1648
  </Canvas>
1645
1649
  <InfoPanel building={hoveredBuilding} />
1646
1650
  {showControls && (
1647
- <ControlsOverlay
1648
- isFlat={!isGrown}
1649
- onToggle={handleToggle}
1650
- onResetCamera={resetCamera}
1651
- />
1651
+ <ControlsOverlay isFlat={!isGrown} onToggle={handleToggle} onResetCamera={resetCamera} />
1652
1652
  )}
1653
1653
  </div>
1654
1654
  );
@@ -1,18 +1,17 @@
1
1
  import React from 'react';
2
2
  import type { Meta, StoryObj } from '@storybook/react';
3
- import { ThemeProvider } from '@principal-ade/industry-theme';
4
- import { FileCity3D, type CityData, type CityBuilding, type CityDistrict, type HighlightLayer, type IsolationMode } from '../components/FileCity3D';
3
+ import {
4
+ FileCity3D,
5
+ type CityData,
6
+ type CityBuilding,
7
+ type CityDistrict,
8
+ type HighlightLayer,
9
+ type IsolationMode,
10
+ } from '../components/FileCity3D';
5
11
 
6
12
  const meta: Meta<typeof FileCity3D> = {
7
13
  title: 'Components/FileCity3D',
8
14
  component: FileCity3D,
9
- decorators: [
10
- (Story) => (
11
- <ThemeProvider>
12
- <Story />
13
- </ThemeProvider>
14
- ),
15
- ],
16
15
  parameters: {
17
16
  layout: 'fullscreen',
18
17
  },
@@ -40,7 +39,7 @@ function generateBuildings(
40
39
  startX: number,
41
40
  startZ: number,
42
41
  areaWidth: number,
43
- areaDepth: number
42
+ areaDepth: number,
44
43
  ): CityBuilding[] {
45
44
  const buildings: CityBuilding[] = [];
46
45
  const allExtensions = [...CODE_EXTENSIONS, ...NON_CODE_EXTENSIONS];
@@ -56,9 +55,7 @@ function generateBuildings(
56
55
  const lineCount = isCode
57
56
  ? Math.floor(Math.exp(Math.random() * Math.log(3000 - 20) + Math.log(20)))
58
57
  : undefined;
59
- const size = isCode
60
- ? lineCount! * 40
61
- : Math.floor(Math.random() * 200000) + 1000;
58
+ const size = isCode ? lineCount! * 40 : Math.floor(Math.random() * 200000) + 1000;
62
59
 
63
60
  buildings.push({
64
61
  path: `${basePath}/file${i}.${ext}`,
@@ -92,28 +89,44 @@ const sampleCityData: CityData = {
92
89
  worldBounds: { minX: -2, maxX: 42, minZ: -2, maxZ: 42 },
93
90
  fileCount: 12,
94
91
  type: 'directory',
95
- label: { text: 'src', bounds: { minX: -2, maxX: 42, minZ: 42, maxZ: 46 }, position: 'bottom' },
92
+ label: {
93
+ text: 'src',
94
+ bounds: { minX: -2, maxX: 42, minZ: 42, maxZ: 46 },
95
+ position: 'bottom',
96
+ },
96
97
  },
97
98
  {
98
99
  path: 'src/components',
99
100
  worldBounds: { minX: 48, maxX: 82, minZ: -2, maxZ: 32 },
100
101
  fileCount: 8,
101
102
  type: 'directory',
102
- label: { text: 'components', bounds: { minX: 48, maxX: 82, minZ: 32, maxZ: 36 }, position: 'bottom' },
103
+ label: {
104
+ text: 'components',
105
+ bounds: { minX: 48, maxX: 82, minZ: 32, maxZ: 36 },
106
+ position: 'bottom',
107
+ },
103
108
  },
104
109
  {
105
110
  path: 'src/utils',
106
111
  worldBounds: { minX: 48, maxX: 77, minZ: 38, maxZ: 67 },
107
112
  fileCount: 6,
108
113
  type: 'directory',
109
- label: { text: 'utils', bounds: { minX: 48, maxX: 77, minZ: 67, maxZ: 71 }, position: 'bottom' },
114
+ label: {
115
+ text: 'utils',
116
+ bounds: { minX: 48, maxX: 77, minZ: 67, maxZ: 71 },
117
+ position: 'bottom',
118
+ },
110
119
  },
111
120
  {
112
121
  path: 'tests',
113
122
  worldBounds: { minX: -2, maxX: 32, minZ: 48, maxZ: 72 },
114
123
  fileCount: 5,
115
124
  type: 'directory',
116
- label: { text: 'tests', bounds: { minX: -2, maxX: 32, minZ: 72, maxZ: 76 }, position: 'bottom' },
125
+ label: {
126
+ text: 'tests',
127
+ bounds: { minX: -2, maxX: 32, minZ: 72, maxZ: 76 },
128
+ position: 'bottom',
129
+ },
117
130
  },
118
131
  ],
119
132
  bounds: { minX: -5, maxX: 85, minZ: -5, maxZ: 80 },
@@ -164,7 +177,11 @@ function generateLargeCityData(): CityData {
164
177
  buildings,
165
178
  districts,
166
179
  bounds: { minX: -10, maxX: totalSize + 10, minZ: -10, maxZ: totalSize + 10 },
167
- metadata: { totalFiles: buildings.length, totalDirectories: districts.length, rootPath: '/large-project' },
180
+ metadata: {
181
+ totalFiles: buildings.length,
182
+ totalDirectories: districts.length,
183
+ rootPath: '/large-project',
184
+ },
168
185
  };
169
186
  }
170
187
 
@@ -211,7 +228,11 @@ function generateMonorepoCityData(): CityData {
211
228
  buildings,
212
229
  districts,
213
230
  bounds: { minX: -10, maxX: 175, minZ: -10, maxZ: 110 },
214
- metadata: { totalFiles: buildings.length, totalDirectories: districts.length, rootPath: '/monorepo' },
231
+ metadata: {
232
+ totalFiles: buildings.length,
233
+ totalDirectories: districts.length,
234
+ rootPath: '/monorepo',
235
+ },
215
236
  };
216
237
  }
217
238
 
@@ -345,7 +366,7 @@ export const WithClickHandler: Story = {
345
366
  args: {
346
367
  cityData: sampleCityData,
347
368
  height: '100vh',
348
- onBuildingClick: (building) => {
369
+ onBuildingClick: building => {
349
370
  console.log('Clicked building:', building.path);
350
371
  alert(`Clicked: ${building.path}`);
351
372
  },
@@ -497,14 +518,16 @@ const authServerTourSteps: TourStep[] = [
497
518
  {
498
519
  id: 'overview',
499
520
  title: 'Welcome to Auth Server',
500
- description: 'This is the authentication server for Principal ADE. Let\'s explore its architecture.',
521
+ description:
522
+ "This is the authentication server for Principal ADE. Let's explore its architecture.",
501
523
  highlightLayers: [],
502
524
  isolationMode: 'none' as const,
503
525
  },
504
526
  {
505
527
  id: 'workos-auth',
506
528
  title: 'WorkOS Authentication',
507
- description: 'The core authentication flow using WorkOS. Handles OAuth callbacks, token exchange, and verification.',
529
+ description:
530
+ 'The core authentication flow using WorkOS. Handles OAuth callbacks, token exchange, and verification.',
508
531
  highlightLayers: [
509
532
  {
510
533
  id: 'workos',
@@ -656,7 +679,9 @@ const AuthServerTourTemplate: React.FC = () => {
656
679
  };
657
680
 
658
681
  return (
659
- <div style={{ height: '100vh', display: 'flex', flexDirection: 'column', position: 'relative' }}>
682
+ <div
683
+ style={{ height: '100vh', display: 'flex', flexDirection: 'column', position: 'relative' }}
684
+ >
660
685
  {/* 3D City */}
661
686
  <FileCity3D
662
687
  cityData={authServerCityData as CityData}
@@ -713,7 +738,15 @@ const AuthServerTourTemplate: React.FC = () => {
713
738
  </button>
714
739
 
715
740
  {/* Step content - center */}
716
- <div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 8 }}>
741
+ <div
742
+ style={{
743
+ flex: 1,
744
+ display: 'flex',
745
+ flexDirection: 'column',
746
+ alignItems: 'center',
747
+ gap: 8,
748
+ }}
749
+ >
717
750
  {/* Step indicators */}
718
751
  <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
719
752
  {authServerTourSteps.map((s, i) => (
@@ -725,7 +758,8 @@ const AuthServerTourTemplate: React.FC = () => {
725
758
  height: i === currentStep ? 12 : 10,
726
759
  borderRadius: '50%',
727
760
  border: i === currentStep ? '2px solid #3b82f6' : 'none',
728
- background: i === currentStep ? '#3b82f6' : i < currentStep ? '#22c55e' : '#475569',
761
+ background:
762
+ i === currentStep ? '#3b82f6' : i < currentStep ? '#22c55e' : '#475569',
729
763
  cursor: 'pointer',
730
764
  padding: 0,
731
765
  transition: 'all 0.2s',
@@ -745,16 +779,35 @@ const AuthServerTourTemplate: React.FC = () => {
745
779
  </div>
746
780
 
747
781
  {/* Description */}
748
- <p style={{ margin: 0, fontSize: 13, color: '#94a3b8', textAlign: 'center', maxWidth: 600 }}>
782
+ <p
783
+ style={{
784
+ margin: 0,
785
+ fontSize: 13,
786
+ color: '#94a3b8',
787
+ textAlign: 'center',
788
+ maxWidth: 600,
789
+ }}
790
+ >
749
791
  {step.description}
750
792
  </p>
751
793
 
752
794
  {/* Isolation mode indicator */}
753
795
  <div style={{ fontSize: 11, color: '#64748b' }}>
754
- Isolation: <code style={{ color: '#94a3b8', background: '#1e293b', padding: '2px 6px', borderRadius: 4 }}>{step.isolationMode}</code>
796
+ Isolation:{' '}
797
+ <code
798
+ style={{
799
+ color: '#94a3b8',
800
+ background: '#1e293b',
801
+ padding: '2px 6px',
802
+ borderRadius: 4,
803
+ }}
804
+ >
805
+ {step.isolationMode}
806
+ </code>
755
807
  {step.highlightLayers.length > 0 && (
756
808
  <span style={{ marginLeft: 8 }}>
757
- • {step.highlightLayers.length} layer{step.highlightLayers.length > 1 ? 's' : ''} active
809
+ • {step.highlightLayers.length} layer{step.highlightLayers.length > 1 ? 's' : ''}{' '}
810
+ active
758
811
  </span>
759
812
  )}
760
813
  </div>
@@ -806,7 +859,9 @@ const DirectorySelectionTemplate: React.FC = () => {
806
859
  }, []);
807
860
 
808
861
  return (
809
- <div style={{ height: '100vh', display: 'flex', flexDirection: 'column', position: 'relative' }}>
862
+ <div
863
+ style={{ height: '100vh', display: 'flex', flexDirection: 'column', position: 'relative' }}
864
+ >
810
865
  {/* 3D City */}
811
866
  <FileCity3D
812
867
  cityData={authServerCityData as CityData}
@@ -822,12 +877,12 @@ const DirectorySelectionTemplate: React.FC = () => {
822
877
  friction: 14,
823
878
  }}
824
879
  showControls={true}
825
- onBuildingClick={(building) => {
880
+ onBuildingClick={building => {
826
881
  // Extract directory from building path
827
882
  const parts = building.path.split('/');
828
883
  if (parts.length >= 2) {
829
884
  const dir = parts.slice(0, 2).join('/');
830
- setFocusDirectory(prev => prev === dir ? null : dir);
885
+ setFocusDirectory(prev => (prev === dir ? null : dir));
831
886
  }
832
887
  }}
833
888
  />
@@ -869,7 +924,7 @@ const DirectorySelectionTemplate: React.FC = () => {
869
924
  {directories.map(dir => (
870
925
  <button
871
926
  key={dir}
872
- onClick={() => setFocusDirectory(prev => prev === dir ? null : dir)}
927
+ onClick={() => setFocusDirectory(prev => (prev === dir ? null : dir))}
873
928
  style={{
874
929
  padding: '8px 16px',
875
930
  background: focusDirectory === dir ? '#3b82f6' : '#334155',
@@ -887,7 +942,17 @@ const DirectorySelectionTemplate: React.FC = () => {
887
942
  </div>
888
943
  {focusDirectory && (
889
944
  <div style={{ marginTop: 12, fontSize: 14 }}>
890
- Focused: <code style={{ color: '#3b82f6', background: '#1e293b', padding: '4px 8px', borderRadius: 4 }}>{focusDirectory}</code>
945
+ Focused:{' '}
946
+ <code
947
+ style={{
948
+ color: '#3b82f6',
949
+ background: '#1e293b',
950
+ padding: '4px 8px',
951
+ borderRadius: 4,
952
+ }}
953
+ >
954
+ {focusDirectory}
955
+ </code>
891
956
  </div>
892
957
  )}
893
958
  </div>