@principal-ai/file-city-react 0.3.0 → 0.4.0

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.
@@ -12,6 +12,9 @@ export interface ArchitectureMapHighlightLayersProps {
12
12
  onDirectorySelect?: (directory: string | null) => void;
13
13
  onFileClick?: (path: string, type: 'file' | 'directory') => void;
14
14
  enableZoom?: boolean;
15
+ zoomToPath?: string | null;
16
+ onZoomComplete?: () => void;
17
+ zoomAnimationSpeed?: number;
15
18
  fullSize?: boolean;
16
19
  showGrid?: boolean;
17
20
  showFileNames?: boolean;
@@ -56,7 +59,7 @@ export interface ArchitectureMapHighlightLayersProps {
56
59
  buildingBorderRadius?: number;
57
60
  districtBorderRadius?: number;
58
61
  }
59
- declare function ArchitectureMapHighlightLayersInner({ cityData, highlightLayers, onLayerToggle, focusDirectory, rootDirectoryName, onDirectorySelect, onFileClick, enableZoom, fullSize, showGrid, showFileNames, className, selectiveRender, canvasBackgroundColor, hoverBorderColor, disableOpacityDimming, defaultDirectoryColor, defaultBuildingColor, subdirectoryMode, showLayerControls, showFileTypeIcons, showDirectoryLabels, transform, // Default to no rotation
62
+ declare function ArchitectureMapHighlightLayersInner({ cityData, highlightLayers, onLayerToggle, focusDirectory, rootDirectoryName, onDirectorySelect, onFileClick, enableZoom, zoomToPath, onZoomComplete, zoomAnimationSpeed, fullSize, showGrid, showFileNames, className, selectiveRender, canvasBackgroundColor, hoverBorderColor, disableOpacityDimming, defaultDirectoryColor, defaultBuildingColor, subdirectoryMode, showLayerControls, showFileTypeIcons, showDirectoryLabels, transform, // Default to no rotation
60
63
  onHover, buildingBorderRadius, districtBorderRadius, }: ArchitectureMapHighlightLayersProps): React.JSX.Element;
61
64
  export declare const ArchitectureMapHighlightLayers: typeof ArchitectureMapHighlightLayersInner;
62
65
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"ArchitectureMapHighlightLayers.d.ts","sourceRoot":"","sources":["../../src/components/ArchitectureMapHighlightLayers.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ArchitectureMapHighlightLayers.d.ts","sourceRoot":"","sources":["../../src/components/ArchitectureMapHighlightLayers.tsx"],"names":[],"mappings":"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"}
@@ -117,7 +117,7 @@ class SpatialGrid {
117
117
  this.grid.clear();
118
118
  }
119
119
  }
120
- function ArchitectureMapHighlightLayersInner({ cityData, highlightLayers = [], onLayerToggle, focusDirectory = null, rootDirectoryName, onDirectorySelect, onFileClick, enableZoom = false, fullSize = false, showGrid = false, showFileNames = false, className = '', selectiveRender, canvasBackgroundColor, hoverBorderColor, disableOpacityDimming = true, defaultDirectoryColor, defaultBuildingColor, subdirectoryMode, showLayerControls = false, showFileTypeIcons = true, showDirectoryLabels = true, transform = { rotation: 0 }, // Default to no rotation
120
+ function ArchitectureMapHighlightLayersInner({ cityData, highlightLayers = [], onLayerToggle, focusDirectory = null, rootDirectoryName, onDirectorySelect, onFileClick, enableZoom = false, zoomToPath = null, onZoomComplete, zoomAnimationSpeed = 0.12, fullSize = false, showGrid = false, showFileNames = false, className = '', selectiveRender, canvasBackgroundColor, hoverBorderColor, disableOpacityDimming = true, defaultDirectoryColor, defaultBuildingColor, subdirectoryMode, showLayerControls = false, showFileTypeIcons = true, showDirectoryLabels = true, transform = { rotation: 0 }, // Default to no rotation
121
121
  onHover, buildingBorderRadius = 0, districtBorderRadius = 0, }) {
122
122
  const { theme } = (0, industry_theme_1.useTheme)();
123
123
  // Use theme colors as defaults, with prop overrides
@@ -140,6 +140,10 @@ onHover, buildingBorderRadius = 0, districtBorderRadius = 0, }) {
140
140
  lastMousePos: { x: 0, y: 0 },
141
141
  hasMouseMoved: false,
142
142
  });
143
+ // Target zoom state for animated transitions
144
+ const [targetZoom, setTargetZoom] = (0, react_1.useState)(null);
145
+ // Track the last zoomToPath to detect changes
146
+ const lastZoomToPathRef = (0, react_1.useRef)(null);
143
147
  (0, react_1.useEffect)(() => {
144
148
  if (!enableZoom) {
145
149
  setZoomState(prev => ({
@@ -150,8 +154,46 @@ onHover, buildingBorderRadius = 0, districtBorderRadius = 0, }) {
150
154
  isDragging: false,
151
155
  hasMouseMoved: false,
152
156
  }));
157
+ setTargetZoom(null);
153
158
  }
154
159
  }, [enableZoom]);
160
+ // Animation loop for smooth zoom transitions
161
+ (0, react_1.useEffect)(() => {
162
+ if (!targetZoom)
163
+ return;
164
+ const animate = () => {
165
+ setZoomState(prev => {
166
+ const lerp = (a, b, t) => a + (b - a) * t;
167
+ const easing = zoomAnimationSpeed;
168
+ const newScale = lerp(prev.scale, targetZoom.scale, easing);
169
+ const newOffsetX = lerp(prev.offsetX, targetZoom.offsetX, easing);
170
+ const newOffsetY = lerp(prev.offsetY, targetZoom.offsetY, easing);
171
+ // Check if close enough to target (within 0.1% for scale, 0.5px for offset)
172
+ const scaleDone = Math.abs(newScale - targetZoom.scale) < 0.001;
173
+ const offsetXDone = Math.abs(newOffsetX - targetZoom.offsetX) < 0.5;
174
+ const offsetYDone = Math.abs(newOffsetY - targetZoom.offsetY) < 0.5;
175
+ if (scaleDone && offsetXDone && offsetYDone) {
176
+ // Animation complete - set exact target values
177
+ setTargetZoom(null);
178
+ onZoomComplete?.();
179
+ return {
180
+ ...prev,
181
+ scale: targetZoom.scale,
182
+ offsetX: targetZoom.offsetX,
183
+ offsetY: targetZoom.offsetY,
184
+ };
185
+ }
186
+ return {
187
+ ...prev,
188
+ scale: newScale,
189
+ offsetX: newOffsetX,
190
+ offsetY: newOffsetY,
191
+ };
192
+ });
193
+ };
194
+ const frameId = requestAnimationFrame(animate);
195
+ return () => cancelAnimationFrame(frameId);
196
+ }, [targetZoom, zoomState, zoomAnimationSpeed, onZoomComplete]);
155
197
  const [hitTestCache, setHitTestCache] = (0, react_1.useState)(null);
156
198
  const calculateCanvasResolution = (fileCount, _cityBounds) => {
157
199
  const minSize = 400;
@@ -188,6 +230,96 @@ onHover, buildingBorderRadius = 0, districtBorderRadius = 0, }) {
188
230
  }
189
231
  return filteredCityData;
190
232
  }, [subdirectoryMode?.enabled, subdirectoryMode?.autoCenter, cityData, filteredCityData]);
233
+ // Handle zoomToPath changes - calculate target zoom to frame the specified path
234
+ (0, react_1.useEffect)(() => {
235
+ // Skip if zoom is not enabled or path hasn't changed
236
+ if (!enableZoom || zoomToPath === lastZoomToPathRef.current) {
237
+ return;
238
+ }
239
+ lastZoomToPathRef.current = zoomToPath;
240
+ // If zoomToPath is null, reset to default view
241
+ if (zoomToPath === null) {
242
+ setTargetZoom({
243
+ scale: 1,
244
+ offsetX: 0,
245
+ offsetY: 0,
246
+ });
247
+ return;
248
+ }
249
+ // Need city data and canvas ref to calculate zoom
250
+ if (!filteredCityData || !canvasRef.current) {
251
+ return;
252
+ }
253
+ // Get actual display size - the canvas is resized to match this during render
254
+ // so canvas coordinates = display coordinates
255
+ const displayWidth = canvasRef.current.clientWidth || canvasSize.width;
256
+ const displayHeight = canvasRef.current.clientHeight || canvasSize.height;
257
+ if (!displayWidth || !displayHeight) {
258
+ return;
259
+ }
260
+ // Find the target - first check districts, then buildings
261
+ const normalizedPath = zoomToPath.replace(/^\/+|\/+$/g, '');
262
+ const targetDistrict = filteredCityData.districts.find(d => d.path === normalizedPath || d.path === zoomToPath);
263
+ const targetBuilding = filteredCityData.buildings.find(b => b.path === normalizedPath || b.path === zoomToPath);
264
+ if (!targetDistrict && !targetBuilding) {
265
+ return;
266
+ }
267
+ // Get the bounds to zoom to
268
+ let targetBounds;
269
+ if (targetDistrict) {
270
+ targetBounds = targetDistrict.worldBounds;
271
+ }
272
+ else if (targetBuilding) {
273
+ // Create bounds around the building with some padding
274
+ const [width, , depth] = targetBuilding.dimensions;
275
+ const padding = Math.max(width, depth) * 2;
276
+ targetBounds = {
277
+ minX: targetBuilding.position.x - width / 2 - padding,
278
+ maxX: targetBuilding.position.x + width / 2 + padding,
279
+ minZ: targetBuilding.position.z - depth / 2 - padding,
280
+ maxZ: targetBuilding.position.z + depth / 2 + padding,
281
+ };
282
+ }
283
+ else {
284
+ return;
285
+ }
286
+ // Use the same coordinate system as rendering
287
+ const coordinateData = canvasSizingData || filteredCityData;
288
+ const { scale: baseScale, offsetX: baseOffsetX, offsetZ: baseOffsetZ } = calculateScaleAndOffset(coordinateData, displayWidth, displayHeight, displayOptions.padding);
289
+ // Calculate target center in world coordinates
290
+ const targetCenterX = (targetBounds.minX + targetBounds.maxX) / 2;
291
+ const targetCenterZ = (targetBounds.minZ + targetBounds.maxZ) / 2;
292
+ // Calculate target size in screen coordinates (at base zoom)
293
+ const targetScreenWidth = (targetBounds.maxX - targetBounds.minX) * baseScale;
294
+ const targetScreenHeight = (targetBounds.maxZ - targetBounds.minZ) * baseScale;
295
+ // Calculate zoom scale to fit target with padding (80% of display)
296
+ const paddingFactor = 0.8;
297
+ const scaleToFitWidth = (displayWidth * paddingFactor) / targetScreenWidth;
298
+ const scaleToFitHeight = (displayHeight * paddingFactor) / targetScreenHeight;
299
+ const newZoomScale = Math.min(scaleToFitWidth, scaleToFitHeight, 5); // Cap at 5x
300
+ // Calculate the base screen position of target center (before zoom transform)
301
+ // This matches the worldToCanvas formula: ((x - bounds.minX) * scale + offsetX)
302
+ const baseScreenX = (targetCenterX - coordinateData.bounds.minX) * baseScale + baseOffsetX;
303
+ const baseScreenY = (targetCenterZ - coordinateData.bounds.minZ) * baseScale + baseOffsetZ;
304
+ // Calculate offset to center the target
305
+ // Full formula: screenPos = baseScreenPos * zoomScale + zoomOffset
306
+ // To center: displayCenter = baseScreen * zoomScale + zoomOffset
307
+ // Therefore: zoomOffset = displayCenter - baseScreen * zoomScale
308
+ const newOffsetX = displayWidth / 2 - baseScreenX * newZoomScale;
309
+ const newOffsetY = displayHeight / 2 - baseScreenY * newZoomScale;
310
+ setTargetZoom({
311
+ scale: newZoomScale,
312
+ offsetX: newOffsetX,
313
+ offsetY: newOffsetY,
314
+ });
315
+ }, [
316
+ zoomToPath,
317
+ enableZoom,
318
+ filteredCityData,
319
+ canvasSizingData,
320
+ canvasSize,
321
+ displayOptions.padding,
322
+ ]);
191
323
  // Build hit test cache with spatial indexing
192
324
  const buildHitTestCache = (0, react_1.useCallback)((cityData, scale, offsetX, offsetZ, zoomState, abstractedPaths) => {
193
325
  const spatialGrid = new SpatialGrid(cityData.bounds);
@@ -1 +1 @@
1
- {"version":3,"file":"sample-data.d.ts","sourceRoot":"","sources":["../../src/stories/sample-data.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAA8B,MAAM,iCAAiC,CAAC;AAGvF,wBAAgB,oBAAoB,IAAI,QAAQ,CAmP/C;AAGD,wBAAgB,yBAAyB,IAAI,QAAQ,CAmDpD"}
1
+ {"version":3,"file":"sample-data.d.ts","sourceRoot":"","sources":["../../src/stories/sample-data.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,QAAQ,EAGT,MAAM,iCAAiC,CAAC;AAqFzC,wBAAgB,oBAAoB,IAAI,QAAQ,CAmB/C;AAaD,wBAAgB,yBAAyB,IAAI,QAAQ,CAmBpD"}
@@ -2,267 +2,110 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createSampleCityData = createSampleCityData;
4
4
  exports.createSmallSampleCityData = createSmallSampleCityData;
5
- // Helper function to create sample city data for stories
5
+ const file_city_builder_1 = require("@principal-ai/file-city-builder");
6
+ // Sample file structure representing a typical project
7
+ const sampleFileStructure = [
8
+ // Source files
9
+ { path: 'src/index.ts', size: 1500 },
10
+ { path: 'src/App.tsx', size: 3200 },
11
+ { path: 'src/components/Header.tsx', size: 1800 },
12
+ { path: 'src/components/Footer.tsx', size: 1200 },
13
+ { path: 'src/components/Sidebar.tsx', size: 2100 },
14
+ { path: 'src/components/Card.tsx', size: 900 },
15
+ { path: 'src/components/Button.tsx', size: 600 },
16
+ { path: 'src/utils/helpers.ts', size: 2500 },
17
+ { path: 'src/utils/api.ts', size: 3100 },
18
+ { path: 'src/utils/validators.ts', size: 1400 },
19
+ { path: 'src/hooks/useAuth.ts', size: 800 },
20
+ { path: 'src/hooks/useData.ts', size: 1100 },
21
+ { path: 'src/styles/main.css', size: 4500 },
22
+ { path: 'src/styles/components.css', size: 2800 },
23
+ // Test files
24
+ { path: 'tests/unit/app.test.ts', size: 2200 },
25
+ { path: 'tests/unit/header.test.ts', size: 1600 },
26
+ { path: 'tests/unit/footer.test.tsx', size: 1400 },
27
+ { path: 'tests/integration/api.test.ts', size: 3400 },
28
+ { path: '__tests__/components.test.tsx', size: 2900 },
29
+ { path: '__tests__/utils.test.ts', size: 1900 },
30
+ // Config files
31
+ { path: 'package.json', size: 1200 },
32
+ { path: 'tsconfig.json', size: 800 },
33
+ { path: 'webpack.config.js', size: 2100 },
34
+ { path: '.eslintrc.js', size: 600 },
35
+ { path: '.prettierrc', size: 200 },
36
+ { path: 'README.md', size: 3500 },
37
+ // Documentation
38
+ { path: 'docs/README.md', size: 4200 },
39
+ { path: 'docs/API.md', size: 5100 },
40
+ { path: 'docs/CONTRIBUTING.md', size: 2300 },
41
+ // Build files
42
+ { path: 'dist/bundle.js', size: 45000 },
43
+ { path: 'dist/index.html', size: 800 },
44
+ { path: 'dist/styles.css', size: 12000 },
45
+ // Node modules (sample)
46
+ { path: 'node_modules/react/index.js', size: 8000 },
47
+ { path: 'node_modules/react/package.json', size: 1500 },
48
+ { path: 'node_modules/typescript/lib/typescript.js', size: 65000 },
49
+ { path: 'node_modules/@types/react/index.d.ts', size: 3200 },
50
+ // Deprecated files
51
+ { path: 'src/deprecated/OldComponent.tsx', size: 2400 },
52
+ { path: 'src/deprecated/LegacyAPI.ts', size: 3100 },
53
+ ];
54
+ // Convert file structure to FileInfo objects
55
+ function createFileInfoList(files) {
56
+ return files.map(file => ({
57
+ name: file.path.split('/').pop() || file.path,
58
+ path: file.path,
59
+ relativePath: file.path,
60
+ size: file.size,
61
+ extension: file.path.includes('.') ? '.' + (file.path.split('.').pop() || '') : '',
62
+ lastModified: new Date(),
63
+ isDirectory: false,
64
+ }));
65
+ }
66
+ // Cache the city data to avoid rebuilding on every render
67
+ let cachedCityData = null;
68
+ // Helper function to create sample city data for stories using the real treemap builder
6
69
  function createSampleCityData() {
7
- const buildings = [];
8
- const districts = [];
9
- // Define file structure
10
- const fileStructure = [
11
- // Source files
12
- { path: 'src/index.ts', size: 1500 },
13
- { path: 'src/App.tsx', size: 3200 },
14
- { path: 'src/components/Header.tsx', size: 1800 },
15
- { path: 'src/components/Footer.tsx', size: 1200 },
16
- { path: 'src/components/Sidebar.tsx', size: 2100 },
17
- { path: 'src/components/Card.tsx', size: 900 },
18
- { path: 'src/components/Button.tsx', size: 600 },
19
- { path: 'src/utils/helpers.ts', size: 2500 },
20
- { path: 'src/utils/api.ts', size: 3100 },
21
- { path: 'src/utils/validators.ts', size: 1400 },
22
- { path: 'src/hooks/useAuth.ts', size: 800 },
23
- { path: 'src/hooks/useData.ts', size: 1100 },
24
- { path: 'src/styles/main.css', size: 4500 },
25
- { path: 'src/styles/components.css', size: 2800 },
26
- // Test files
27
- { path: 'tests/unit/app.test.ts', size: 2200 },
28
- { path: 'tests/unit/header.test.ts', size: 1600 },
29
- { path: 'tests/unit/footer.test.tsx', size: 1400 },
30
- { path: 'tests/integration/api.test.ts', size: 3400 },
31
- { path: '__tests__/components.test.tsx', size: 2900 },
32
- { path: '__tests__/utils.test.ts', size: 1900 },
33
- // Config files
34
- { path: 'package.json', size: 1200 },
35
- { path: 'tsconfig.json', size: 800 },
36
- { path: 'webpack.config.js', size: 2100 },
37
- { path: '.eslintrc.js', size: 600 },
38
- { path: '.prettierrc', size: 200 },
39
- { path: 'README.md', size: 3500 },
40
- // Documentation
41
- { path: 'docs/README.md', size: 4200 },
42
- { path: 'docs/API.md', size: 5100 },
43
- { path: 'docs/CONTRIBUTING.md', size: 2300 },
44
- // Build files
45
- { path: 'dist/bundle.js', size: 45000 },
46
- { path: 'dist/index.html', size: 800 },
47
- { path: 'dist/styles.css', size: 12000 },
48
- // Node modules (sample)
49
- { path: 'node_modules/react/index.js', size: 8000 },
50
- { path: 'node_modules/react/package.json', size: 1500 },
51
- { path: 'node_modules/typescript/lib/typescript.js', size: 65000 },
52
- { path: 'node_modules/@types/react/index.d.ts', size: 3200 },
53
- // Deprecated files
54
- { path: 'src/deprecated/OldComponent.tsx', size: 2400 },
55
- { path: 'src/deprecated/LegacyAPI.ts', size: 3100 },
56
- ];
57
- // Create a simple grid layout
58
- let currentX = 0;
59
- let currentZ = 0;
60
- const spacing = 2;
61
- const maxPerRow = 10;
62
- let itemsInRow = 0;
63
- // Group files by directory
64
- const filesByDir = new Map();
65
- fileStructure.forEach(file => {
66
- const dir = file.path.includes('/') ? file.path.substring(0, file.path.lastIndexOf('/')) : '';
67
- if (!filesByDir.has(dir)) {
68
- filesByDir.set(dir, []);
69
- }
70
- const dirFiles = filesByDir.get(dir);
71
- if (dirFiles) {
72
- dirFiles.push(file);
73
- }
74
- });
75
- // Track district bounds
76
- const districtBounds = new Map();
77
- // Process each directory group
78
- const sortedDirs = Array.from(filesByDir.keys()).sort();
79
- let districtStartX = 0;
80
- sortedDirs.forEach(dir => {
81
- const files = filesByDir.get(dir);
82
- if (!files)
83
- return;
84
- const districtMinX = currentX;
85
- const districtMinZ = currentZ;
86
- files.forEach(file => {
87
- const extension = file.path.includes('.') ? '.' + (file.path.split('.').pop() || '') : '';
88
- // Calculate building dimensions based on file size
89
- const height = Math.log(file.size + 1) * 2;
90
- const width = Math.sqrt(file.size) / 10;
91
- const depth = width;
92
- buildings.push({
93
- path: file.path,
94
- position: {
95
- x: currentX + width / 2,
96
- y: height / 2,
97
- z: currentZ + depth / 2,
98
- },
99
- dimensions: [width, height, depth],
100
- type: 'file',
101
- fileExtension: extension,
102
- size: file.size,
103
- lastModified: new Date(),
104
- });
105
- // Update position for next building
106
- currentX += width + spacing;
107
- itemsInRow++;
108
- if (itemsInRow >= maxPerRow) {
109
- currentX = districtStartX;
110
- currentZ += depth + spacing;
111
- itemsInRow = 0;
112
- }
113
- });
114
- // Create district bounds
115
- if (dir) {
116
- const districtMaxX = currentX > districtMinX ? currentX : districtMinX + 10;
117
- const districtMaxZ = currentZ > districtMinZ ? currentZ + 5 : districtMinZ + 10;
118
- districtBounds.set(dir, {
119
- minX: districtMinX - 1,
120
- maxX: districtMaxX + 1,
121
- minZ: districtMinZ - 1,
122
- maxZ: districtMaxZ + 1,
123
- });
124
- // Move to next district area
125
- currentX = districtMaxX + spacing * 3;
126
- if (currentX > 100) {
127
- currentX = 0;
128
- currentZ = districtMaxZ + spacing * 3;
129
- }
130
- districtStartX = currentX;
131
- itemsInRow = 0;
132
- }
133
- });
134
- // Create districts from bounds
135
- const allPaths = new Set(districtBounds.keys());
136
- const processedPaths = new Set();
137
- // Helper to create all parent paths
138
- const getParentPaths = (path) => {
139
- const parts = path.split('/');
140
- const parents = [];
141
- for (let i = 1; i < parts.length; i++) {
142
- parents.push(parts.slice(0, i).join('/'));
143
- }
144
- return parents;
145
- };
146
- // Add all parent paths to the set
147
- allPaths.forEach(path => {
148
- getParentPaths(path).forEach(parent => allPaths.add(parent));
70
+ if (cachedCityData) {
71
+ return cachedCityData;
72
+ }
73
+ const fileInfos = createFileInfoList(sampleFileStructure);
74
+ const fileTree = (0, file_city_builder_1.buildFileSystemTreeFromFileInfoList)(fileInfos, 'sample-project');
75
+ const builder = new file_city_builder_1.CodeCityBuilderWithGrid();
76
+ cachedCityData = builder.buildCityFromFileSystem(fileTree, '', {
77
+ paddingTop: 2,
78
+ paddingBottom: 2,
79
+ paddingLeft: 2,
80
+ paddingRight: 2,
81
+ paddingInner: 1,
82
+ paddingOuter: 3,
149
83
  });
150
- // Create districts for all paths
151
- allPaths.forEach(path => {
152
- if (processedPaths.has(path))
153
- return;
154
- // Find all children of this path
155
- const children = Array.from(districtBounds.keys()).filter(p => p.startsWith(path + '/') && !p.slice(path.length + 1).includes('/'));
156
- let bounds;
157
- if (districtBounds.has(path)) {
158
- const pathBounds = districtBounds.get(path);
159
- if (!pathBounds)
160
- return;
161
- bounds = pathBounds;
162
- }
163
- else if (children.length > 0) {
164
- // Calculate bounds from children
165
- const childBounds = children
166
- .map(c => districtBounds.get(c))
167
- .filter((b) => b !== undefined);
168
- if (childBounds.length === 0)
169
- return;
170
- bounds = {
171
- minX: Math.min(...childBounds.map(c => c.minX)),
172
- maxX: Math.max(...childBounds.map(c => c.maxX)),
173
- minZ: Math.min(...childBounds.map(c => c.minZ)),
174
- maxZ: Math.max(...childBounds.map(c => c.maxZ)),
175
- };
176
- }
177
- else {
178
- return; // Skip if no bounds
179
- }
180
- const fileCount = buildings.filter(b => b.path === path || b.path.startsWith(path + '/')).length;
181
- districts.push({
182
- path,
183
- worldBounds: bounds,
184
- fileCount,
185
- type: 'directory',
186
- });
187
- processedPaths.add(path);
188
- });
189
- // Calculate overall bounds
190
- const allX = buildings.map(b => b.position.x);
191
- const allZ = buildings.map(b => b.position.z);
192
- const bounds = {
193
- minX: Math.min(...allX) - 5,
194
- maxX: Math.max(...allX) + 5,
195
- minZ: Math.min(...allZ) - 5,
196
- maxZ: Math.max(...allZ) + 5,
197
- };
198
- return {
199
- buildings,
200
- districts,
201
- bounds,
202
- metadata: {
203
- totalFiles: buildings.length,
204
- totalDirectories: districts.length,
205
- analyzedAt: new Date(),
206
- rootPath: '/',
207
- layoutConfig: {
208
- paddingTop: 2,
209
- paddingBottom: 2,
210
- paddingLeft: 2,
211
- paddingRight: 2,
212
- paddingInner: 1,
213
- paddingOuter: 3,
214
- },
215
- },
216
- };
84
+ return cachedCityData;
217
85
  }
86
+ // Smaller sample file structure
87
+ const smallFileStructure = [
88
+ { path: 'src/index.ts', size: 1500 },
89
+ { path: 'src/App.tsx', size: 3200 },
90
+ { path: 'src/utils/helpers.ts', size: 800 },
91
+ { path: 'package.json', size: 1200 },
92
+ ];
93
+ let cachedSmallCityData = null;
218
94
  // Create a smaller sample for performance testing
219
95
  function createSmallSampleCityData() {
220
- const buildings = [
221
- {
222
- path: 'index.ts',
223
- position: { x: 5, y: 3, z: 5 },
224
- dimensions: [4, 6, 4],
225
- type: 'file',
226
- fileExtension: '.ts',
227
- size: 1500,
228
- lastModified: new Date(),
229
- },
230
- {
231
- path: 'App.tsx',
232
- position: { x: 12, y: 4, z: 5 },
233
- dimensions: [5, 8, 5],
234
- type: 'file',
235
- fileExtension: '.tsx',
236
- size: 3200,
237
- lastModified: new Date(),
238
- },
239
- {
240
- path: 'utils/helpers.ts',
241
- position: { x: 5, y: 2.5, z: 15 },
242
- dimensions: [3, 5, 3],
243
- type: 'file',
244
- fileExtension: '.ts',
245
- size: 800,
246
- lastModified: new Date(),
247
- },
248
- ];
249
- const districts = [
250
- {
251
- path: 'utils',
252
- worldBounds: { minX: 2, maxX: 10, minZ: 12, maxZ: 20 },
253
- fileCount: 1,
254
- type: 'directory',
255
- },
256
- ];
257
- return {
258
- buildings,
259
- districts,
260
- bounds: { minX: 0, maxX: 20, minZ: 0, maxZ: 25 },
261
- metadata: {
262
- totalFiles: buildings.length,
263
- totalDirectories: districts.length,
264
- analyzedAt: new Date(),
265
- rootPath: '/',
266
- },
267
- };
96
+ if (cachedSmallCityData) {
97
+ return cachedSmallCityData;
98
+ }
99
+ const fileInfos = createFileInfoList(smallFileStructure);
100
+ const fileTree = (0, file_city_builder_1.buildFileSystemTreeFromFileInfoList)(fileInfos, 'small-sample');
101
+ const builder = new file_city_builder_1.CodeCityBuilderWithGrid();
102
+ cachedSmallCityData = builder.buildCityFromFileSystem(fileTree, '', {
103
+ paddingTop: 2,
104
+ paddingBottom: 2,
105
+ paddingLeft: 2,
106
+ paddingRight: 2,
107
+ paddingInner: 1,
108
+ paddingOuter: 3,
109
+ });
110
+ return cachedSmallCityData;
268
111
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@principal-ai/file-city-react",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "React components for File City visualization",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -21,17 +21,13 @@
21
21
  "react": "^19.0.0"
22
22
  },
23
23
  "devDependencies": {
24
- "@storybook/addon-essentials": "^7.6.0",
25
- "@storybook/addon-interactions": "^7.6.0",
26
- "@storybook/addon-links": "^7.6.0",
27
- "@storybook/blocks": "^7.6.0",
28
- "@storybook/react": "^7.6.0",
29
- "@storybook/react-vite": "^7.6.0",
30
- "@storybook/test": "^7.6.0",
31
- "@types/react": "^18.0.0",
24
+ "@storybook/addon-docs": "^10.1.2",
25
+ "@storybook/addon-links": "^10.1.2",
26
+ "@storybook/react-vite": "^10.1.2",
27
+ "@types/react": "^19.0.0",
32
28
  "react": "^19.1.1",
33
29
  "react-dom": "^19.1.1",
34
- "storybook": "^7.6.0",
30
+ "storybook": "^10.1.2",
35
31
  "typescript": "^5.0.0",
36
32
  "vite": "^5.0.0"
37
33
  },