@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.
- package/dist/components/ArchitectureMapHighlightLayers.d.ts +4 -1
- package/dist/components/ArchitectureMapHighlightLayers.d.ts.map +1 -1
- package/dist/components/ArchitectureMapHighlightLayers.js +133 -1
- package/dist/stories/sample-data.d.ts.map +1 -1
- package/dist/stories/sample-data.js +101 -258
- package/package.json +6 -10
- package/src/components/ArchitectureMapHighlightLayers.tsx +176 -0
- package/src/stories/ArchitectureMapGridLayout.stories.tsx +8 -7
- package/src/stories/ArchitectureMapHighlightLayers.stories.tsx +194 -1
- package/src/stories/sample-data.ts +129 -289
- package/dist/stories/ArchitectureMapGridLayout.stories.d.ts +0 -73
- package/dist/stories/ArchitectureMapGridLayout.stories.d.ts.map +0 -1
- package/dist/stories/ArchitectureMapGridLayout.stories.js +0 -345
- package/dist/stories/ArchitectureMapHighlightLayers.stories.d.ts +0 -78
- package/dist/stories/ArchitectureMapHighlightLayers.stories.d.ts.map +0 -1
- package/dist/stories/ArchitectureMapHighlightLayers.stories.js +0 -270
- package/dist/stories/CityViewWithReactFlow.stories.d.ts +0 -24
- package/dist/stories/CityViewWithReactFlow.stories.d.ts.map +0 -1
- package/dist/stories/CityViewWithReactFlow.stories.js +0 -778
|
@@ -46,6 +46,11 @@ export interface ArchitectureMapHighlightLayersProps {
|
|
|
46
46
|
onFileClick?: (path: string, type: 'file' | 'directory') => void;
|
|
47
47
|
enableZoom?: boolean;
|
|
48
48
|
|
|
49
|
+
// Animated zoom to directory
|
|
50
|
+
zoomToPath?: string | null; // When set, animates zoom to frame this directory/file
|
|
51
|
+
onZoomComplete?: () => void; // Called when zoom animation completes
|
|
52
|
+
zoomAnimationSpeed?: number; // Animation easing factor (0-1), default 0.12
|
|
53
|
+
|
|
49
54
|
// Display options
|
|
50
55
|
fullSize?: boolean;
|
|
51
56
|
showGrid?: boolean;
|
|
@@ -213,6 +218,9 @@ function ArchitectureMapHighlightLayersInner({
|
|
|
213
218
|
onDirectorySelect,
|
|
214
219
|
onFileClick,
|
|
215
220
|
enableZoom = false,
|
|
221
|
+
zoomToPath = null,
|
|
222
|
+
onZoomComplete,
|
|
223
|
+
zoomAnimationSpeed = 0.12,
|
|
216
224
|
fullSize = false,
|
|
217
225
|
showGrid = false,
|
|
218
226
|
showFileNames = false,
|
|
@@ -257,6 +265,16 @@ function ArchitectureMapHighlightLayersInner({
|
|
|
257
265
|
hasMouseMoved: false,
|
|
258
266
|
});
|
|
259
267
|
|
|
268
|
+
// Target zoom state for animated transitions
|
|
269
|
+
const [targetZoom, setTargetZoom] = useState<{
|
|
270
|
+
scale: number;
|
|
271
|
+
offsetX: number;
|
|
272
|
+
offsetY: number;
|
|
273
|
+
} | null>(null);
|
|
274
|
+
|
|
275
|
+
// Track the last zoomToPath to detect changes
|
|
276
|
+
const lastZoomToPathRef = useRef<string | null>(null);
|
|
277
|
+
|
|
260
278
|
useEffect(() => {
|
|
261
279
|
if (!enableZoom) {
|
|
262
280
|
setZoomState(prev => ({
|
|
@@ -267,9 +285,53 @@ function ArchitectureMapHighlightLayersInner({
|
|
|
267
285
|
isDragging: false,
|
|
268
286
|
hasMouseMoved: false,
|
|
269
287
|
}));
|
|
288
|
+
setTargetZoom(null);
|
|
270
289
|
}
|
|
271
290
|
}, [enableZoom]);
|
|
272
291
|
|
|
292
|
+
// Animation loop for smooth zoom transitions
|
|
293
|
+
useEffect(() => {
|
|
294
|
+
if (!targetZoom) return;
|
|
295
|
+
|
|
296
|
+
const animate = () => {
|
|
297
|
+
setZoomState(prev => {
|
|
298
|
+
const lerp = (a: number, b: number, t: number) => a + (b - a) * t;
|
|
299
|
+
const easing = zoomAnimationSpeed;
|
|
300
|
+
|
|
301
|
+
const newScale = lerp(prev.scale, targetZoom.scale, easing);
|
|
302
|
+
const newOffsetX = lerp(prev.offsetX, targetZoom.offsetX, easing);
|
|
303
|
+
const newOffsetY = lerp(prev.offsetY, targetZoom.offsetY, easing);
|
|
304
|
+
|
|
305
|
+
// Check if close enough to target (within 0.1% for scale, 0.5px for offset)
|
|
306
|
+
const scaleDone = Math.abs(newScale - targetZoom.scale) < 0.001;
|
|
307
|
+
const offsetXDone = Math.abs(newOffsetX - targetZoom.offsetX) < 0.5;
|
|
308
|
+
const offsetYDone = Math.abs(newOffsetY - targetZoom.offsetY) < 0.5;
|
|
309
|
+
|
|
310
|
+
if (scaleDone && offsetXDone && offsetYDone) {
|
|
311
|
+
// Animation complete - set exact target values
|
|
312
|
+
setTargetZoom(null);
|
|
313
|
+
onZoomComplete?.();
|
|
314
|
+
return {
|
|
315
|
+
...prev,
|
|
316
|
+
scale: targetZoom.scale,
|
|
317
|
+
offsetX: targetZoom.offsetX,
|
|
318
|
+
offsetY: targetZoom.offsetY,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
...prev,
|
|
324
|
+
scale: newScale,
|
|
325
|
+
offsetX: newOffsetX,
|
|
326
|
+
offsetY: newOffsetY,
|
|
327
|
+
};
|
|
328
|
+
});
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
const frameId = requestAnimationFrame(animate);
|
|
332
|
+
return () => cancelAnimationFrame(frameId);
|
|
333
|
+
}, [targetZoom, zoomState, zoomAnimationSpeed, onZoomComplete]);
|
|
334
|
+
|
|
273
335
|
const [hitTestCache, setHitTestCache] = useState<HitTestCache | null>(null);
|
|
274
336
|
|
|
275
337
|
const calculateCanvasResolution = (
|
|
@@ -329,6 +391,120 @@ function ArchitectureMapHighlightLayersInner({
|
|
|
329
391
|
return filteredCityData;
|
|
330
392
|
}, [subdirectoryMode?.enabled, subdirectoryMode?.autoCenter, cityData, filteredCityData]);
|
|
331
393
|
|
|
394
|
+
// Handle zoomToPath changes - calculate target zoom to frame the specified path
|
|
395
|
+
useEffect(() => {
|
|
396
|
+
// Skip if zoom is not enabled or path hasn't changed
|
|
397
|
+
if (!enableZoom || zoomToPath === lastZoomToPathRef.current) {
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
lastZoomToPathRef.current = zoomToPath;
|
|
402
|
+
|
|
403
|
+
// If zoomToPath is null, reset to default view
|
|
404
|
+
if (zoomToPath === null) {
|
|
405
|
+
setTargetZoom({
|
|
406
|
+
scale: 1,
|
|
407
|
+
offsetX: 0,
|
|
408
|
+
offsetY: 0,
|
|
409
|
+
});
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Need city data and canvas ref to calculate zoom
|
|
414
|
+
if (!filteredCityData || !canvasRef.current) {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Get actual display size - the canvas is resized to match this during render
|
|
419
|
+
// so canvas coordinates = display coordinates
|
|
420
|
+
const displayWidth = canvasRef.current.clientWidth || canvasSize.width;
|
|
421
|
+
const displayHeight = canvasRef.current.clientHeight || canvasSize.height;
|
|
422
|
+
|
|
423
|
+
if (!displayWidth || !displayHeight) {
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Find the target - first check districts, then buildings
|
|
428
|
+
const normalizedPath = zoomToPath.replace(/^\/+|\/+$/g, '');
|
|
429
|
+
const targetDistrict = filteredCityData.districts.find(
|
|
430
|
+
d => d.path === normalizedPath || d.path === zoomToPath,
|
|
431
|
+
);
|
|
432
|
+
const targetBuilding = filteredCityData.buildings.find(
|
|
433
|
+
b => b.path === normalizedPath || b.path === zoomToPath,
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
if (!targetDistrict && !targetBuilding) {
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Get the bounds to zoom to
|
|
441
|
+
let targetBounds: { minX: number; maxX: number; minZ: number; maxZ: number };
|
|
442
|
+
|
|
443
|
+
if (targetDistrict) {
|
|
444
|
+
targetBounds = targetDistrict.worldBounds;
|
|
445
|
+
} else if (targetBuilding) {
|
|
446
|
+
// Create bounds around the building with some padding
|
|
447
|
+
const [width, , depth] = targetBuilding.dimensions;
|
|
448
|
+
const padding = Math.max(width, depth) * 2;
|
|
449
|
+
targetBounds = {
|
|
450
|
+
minX: targetBuilding.position.x - width / 2 - padding,
|
|
451
|
+
maxX: targetBuilding.position.x + width / 2 + padding,
|
|
452
|
+
minZ: targetBuilding.position.z - depth / 2 - padding,
|
|
453
|
+
maxZ: targetBuilding.position.z + depth / 2 + padding,
|
|
454
|
+
};
|
|
455
|
+
} else {
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Use the same coordinate system as rendering
|
|
460
|
+
const coordinateData = canvasSizingData || filteredCityData;
|
|
461
|
+
const { scale: baseScale, offsetX: baseOffsetX, offsetZ: baseOffsetZ } = calculateScaleAndOffset(
|
|
462
|
+
coordinateData,
|
|
463
|
+
displayWidth,
|
|
464
|
+
displayHeight,
|
|
465
|
+
displayOptions.padding,
|
|
466
|
+
);
|
|
467
|
+
|
|
468
|
+
// Calculate target center in world coordinates
|
|
469
|
+
const targetCenterX = (targetBounds.minX + targetBounds.maxX) / 2;
|
|
470
|
+
const targetCenterZ = (targetBounds.minZ + targetBounds.maxZ) / 2;
|
|
471
|
+
|
|
472
|
+
// Calculate target size in screen coordinates (at base zoom)
|
|
473
|
+
const targetScreenWidth = (targetBounds.maxX - targetBounds.minX) * baseScale;
|
|
474
|
+
const targetScreenHeight = (targetBounds.maxZ - targetBounds.minZ) * baseScale;
|
|
475
|
+
|
|
476
|
+
// Calculate zoom scale to fit target with padding (80% of display)
|
|
477
|
+
const paddingFactor = 0.8;
|
|
478
|
+
const scaleToFitWidth = (displayWidth * paddingFactor) / targetScreenWidth;
|
|
479
|
+
const scaleToFitHeight = (displayHeight * paddingFactor) / targetScreenHeight;
|
|
480
|
+
const newZoomScale = Math.min(scaleToFitWidth, scaleToFitHeight, 5); // Cap at 5x
|
|
481
|
+
|
|
482
|
+
// Calculate the base screen position of target center (before zoom transform)
|
|
483
|
+
// This matches the worldToCanvas formula: ((x - bounds.minX) * scale + offsetX)
|
|
484
|
+
const baseScreenX = (targetCenterX - coordinateData.bounds.minX) * baseScale + baseOffsetX;
|
|
485
|
+
const baseScreenY = (targetCenterZ - coordinateData.bounds.minZ) * baseScale + baseOffsetZ;
|
|
486
|
+
|
|
487
|
+
// Calculate offset to center the target
|
|
488
|
+
// Full formula: screenPos = baseScreenPos * zoomScale + zoomOffset
|
|
489
|
+
// To center: displayCenter = baseScreen * zoomScale + zoomOffset
|
|
490
|
+
// Therefore: zoomOffset = displayCenter - baseScreen * zoomScale
|
|
491
|
+
const newOffsetX = displayWidth / 2 - baseScreenX * newZoomScale;
|
|
492
|
+
const newOffsetY = displayHeight / 2 - baseScreenY * newZoomScale;
|
|
493
|
+
|
|
494
|
+
setTargetZoom({
|
|
495
|
+
scale: newZoomScale,
|
|
496
|
+
offsetX: newOffsetX,
|
|
497
|
+
offsetY: newOffsetY,
|
|
498
|
+
});
|
|
499
|
+
}, [
|
|
500
|
+
zoomToPath,
|
|
501
|
+
enableZoom,
|
|
502
|
+
filteredCityData,
|
|
503
|
+
canvasSizingData,
|
|
504
|
+
canvasSize,
|
|
505
|
+
displayOptions.padding,
|
|
506
|
+
]);
|
|
507
|
+
|
|
332
508
|
// Build hit test cache with spatial indexing
|
|
333
509
|
const buildHitTestCache = useCallback(
|
|
334
510
|
(
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import React from 'react';
|
|
1
2
|
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
3
|
import {
|
|
3
4
|
CodeCityBuilderWithGrid,
|
|
@@ -24,7 +25,7 @@ interface CodebaseView {
|
|
|
24
25
|
name: string;
|
|
25
26
|
description: string;
|
|
26
27
|
overviewPath: string;
|
|
27
|
-
|
|
28
|
+
referenceGroups: {
|
|
28
29
|
[key: string]: {
|
|
29
30
|
files: string[];
|
|
30
31
|
coordinates: [number, number];
|
|
@@ -144,7 +145,7 @@ export const DefaultsTest: Story = {
|
|
|
144
145
|
name: 'Test Default UI Settings',
|
|
145
146
|
description: 'Tests default UI metadata settings when none are provided',
|
|
146
147
|
overviewPath: 'README.md',
|
|
147
|
-
|
|
148
|
+
referenceGroups: {
|
|
148
149
|
Source: {
|
|
149
150
|
files: ['src', 'lib'],
|
|
150
151
|
coordinates: [0, 0],
|
|
@@ -181,7 +182,7 @@ export const TwoByTwoGrid: Story = {
|
|
|
181
182
|
name: 'Two by Two Grid Layout',
|
|
182
183
|
description: 'A 2x2 grid layout organizing code into four quadrants',
|
|
183
184
|
overviewPath: 'README.md',
|
|
184
|
-
|
|
185
|
+
referenceGroups: {
|
|
185
186
|
Source: {
|
|
186
187
|
files: ['src', 'lib'],
|
|
187
188
|
coordinates: [0, 0],
|
|
@@ -227,7 +228,7 @@ export const ThreeByThreeGrid: Story = {
|
|
|
227
228
|
name: 'Three by Three Grid Layout',
|
|
228
229
|
description: 'A 3x3 grid layout for more granular organization',
|
|
229
230
|
overviewPath: 'README.md',
|
|
230
|
-
|
|
231
|
+
referenceGroups: {
|
|
231
232
|
Source: {
|
|
232
233
|
files: ['src'],
|
|
233
234
|
coordinates: [0, 0],
|
|
@@ -281,7 +282,7 @@ export const SparseGrid: Story = {
|
|
|
281
282
|
name: 'Sparse Grid Layout',
|
|
282
283
|
description: 'A grid with intentional gaps for visual organization',
|
|
283
284
|
overviewPath: 'README.md',
|
|
284
|
-
|
|
285
|
+
referenceGroups: {
|
|
285
286
|
Source: {
|
|
286
287
|
files: ['src', 'lib'],
|
|
287
288
|
coordinates: [0, 0],
|
|
@@ -315,7 +316,7 @@ export const GridWithHighlights: Story = {
|
|
|
315
316
|
name: 'Grid with Highlights',
|
|
316
317
|
description: 'Grid layout with custom highlight layers',
|
|
317
318
|
overviewPath: 'README.md',
|
|
318
|
-
|
|
319
|
+
referenceGroups: {
|
|
319
320
|
Source: {
|
|
320
321
|
files: ['src', 'lib'],
|
|
321
322
|
coordinates: [0, 0],
|
|
@@ -371,7 +372,7 @@ export const LargeGrid: Story = {
|
|
|
371
372
|
name: 'Large 5x5 Grid',
|
|
372
373
|
description: 'A large grid for complex projects',
|
|
373
374
|
overviewPath: 'README.md',
|
|
374
|
-
|
|
375
|
+
referenceGroups: {
|
|
375
376
|
'Core Source': { files: ['src/core'], coordinates: [0, 0] },
|
|
376
377
|
Components: { files: ['src/components'], coordinates: [1, 0] },
|
|
377
378
|
Utils: { files: ['src/utils'], coordinates: [2, 0] },
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
1
2
|
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
-
import { useState } from 'react';
|
|
3
3
|
|
|
4
4
|
import { ArchitectureMapHighlightLayers } from '../components/ArchitectureMapHighlightLayers';
|
|
5
5
|
import { HighlightLayer } from '../render/client/drawLayeredBuildings';
|
|
@@ -310,3 +310,196 @@ export const WithBorderRadius: Story = {
|
|
|
310
310
|
enableZoom: true,
|
|
311
311
|
},
|
|
312
312
|
};
|
|
313
|
+
|
|
314
|
+
// Story with animated zoom to directory
|
|
315
|
+
export const AnimatedZoomToDirectory: Story = {
|
|
316
|
+
render: function RenderAnimatedZoom() {
|
|
317
|
+
const [zoomToPath, setZoomToPath] = useState<string | null>(null);
|
|
318
|
+
const [isAnimating, setIsAnimating] = useState(false);
|
|
319
|
+
const cityData = createSampleCityData();
|
|
320
|
+
|
|
321
|
+
// Get unique top-level directories for navigation buttons
|
|
322
|
+
const topLevelDirs = Array.from(
|
|
323
|
+
new Set(
|
|
324
|
+
cityData.districts
|
|
325
|
+
.map(d => d.path.split('/')[0])
|
|
326
|
+
.filter(Boolean),
|
|
327
|
+
),
|
|
328
|
+
).sort();
|
|
329
|
+
|
|
330
|
+
const handleZoomTo = (path: string | null) => {
|
|
331
|
+
setIsAnimating(true);
|
|
332
|
+
setZoomToPath(path);
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const handleZoomComplete = () => {
|
|
336
|
+
setIsAnimating(false);
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
// Create highlight layer for the focused directory
|
|
340
|
+
const highlightLayers: HighlightLayer[] = zoomToPath
|
|
341
|
+
? [
|
|
342
|
+
{
|
|
343
|
+
id: 'zoom-focus',
|
|
344
|
+
name: 'Zoom Focus',
|
|
345
|
+
enabled: true,
|
|
346
|
+
color: '#3b82f6',
|
|
347
|
+
priority: 1,
|
|
348
|
+
items: [{ path: zoomToPath, type: 'directory' }],
|
|
349
|
+
},
|
|
350
|
+
]
|
|
351
|
+
: [];
|
|
352
|
+
|
|
353
|
+
return (
|
|
354
|
+
<div style={{ position: 'relative', width: '100%', height: '100%' }}>
|
|
355
|
+
<ArchitectureMapHighlightLayers
|
|
356
|
+
cityData={cityData}
|
|
357
|
+
fullSize={true}
|
|
358
|
+
enableZoom={true}
|
|
359
|
+
zoomToPath={zoomToPath}
|
|
360
|
+
onZoomComplete={handleZoomComplete}
|
|
361
|
+
zoomAnimationSpeed={0.1}
|
|
362
|
+
highlightLayers={highlightLayers}
|
|
363
|
+
onFileClick={(path, type) => {
|
|
364
|
+
if (type === 'directory') {
|
|
365
|
+
handleZoomTo(path);
|
|
366
|
+
}
|
|
367
|
+
}}
|
|
368
|
+
/>
|
|
369
|
+
{/* Navigation Controls */}
|
|
370
|
+
<div
|
|
371
|
+
style={{
|
|
372
|
+
position: 'absolute',
|
|
373
|
+
top: 20,
|
|
374
|
+
left: 20,
|
|
375
|
+
zIndex: 100,
|
|
376
|
+
display: 'flex',
|
|
377
|
+
flexDirection: 'column',
|
|
378
|
+
gap: '8px',
|
|
379
|
+
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
|
380
|
+
padding: '16px',
|
|
381
|
+
borderRadius: '8px',
|
|
382
|
+
maxWidth: '200px',
|
|
383
|
+
}}
|
|
384
|
+
>
|
|
385
|
+
<div
|
|
386
|
+
style={{
|
|
387
|
+
color: 'white',
|
|
388
|
+
fontFamily: 'monospace',
|
|
389
|
+
fontSize: '12px',
|
|
390
|
+
marginBottom: '8px',
|
|
391
|
+
}}
|
|
392
|
+
>
|
|
393
|
+
{isAnimating ? '🎬 Animating...' : '📍 Click to zoom'}
|
|
394
|
+
</div>
|
|
395
|
+
|
|
396
|
+
{/* Reset button */}
|
|
397
|
+
<button
|
|
398
|
+
onClick={() => handleZoomTo(null)}
|
|
399
|
+
disabled={isAnimating}
|
|
400
|
+
style={{
|
|
401
|
+
padding: '8px 12px',
|
|
402
|
+
backgroundColor: zoomToPath === null ? '#3b82f6' : '#374151',
|
|
403
|
+
color: 'white',
|
|
404
|
+
border: 'none',
|
|
405
|
+
borderRadius: '4px',
|
|
406
|
+
cursor: isAnimating ? 'not-allowed' : 'pointer',
|
|
407
|
+
fontFamily: 'monospace',
|
|
408
|
+
fontSize: '12px',
|
|
409
|
+
opacity: isAnimating ? 0.6 : 1,
|
|
410
|
+
}}
|
|
411
|
+
>
|
|
412
|
+
🏠 Reset View
|
|
413
|
+
</button>
|
|
414
|
+
|
|
415
|
+
{/* Directory buttons */}
|
|
416
|
+
{topLevelDirs.map(dir => (
|
|
417
|
+
<button
|
|
418
|
+
key={dir}
|
|
419
|
+
onClick={() => handleZoomTo(dir)}
|
|
420
|
+
disabled={isAnimating}
|
|
421
|
+
style={{
|
|
422
|
+
padding: '8px 12px',
|
|
423
|
+
backgroundColor: zoomToPath === dir ? '#3b82f6' : '#374151',
|
|
424
|
+
color: 'white',
|
|
425
|
+
border: 'none',
|
|
426
|
+
borderRadius: '4px',
|
|
427
|
+
cursor: isAnimating ? 'not-allowed' : 'pointer',
|
|
428
|
+
fontFamily: 'monospace',
|
|
429
|
+
fontSize: '12px',
|
|
430
|
+
textAlign: 'left',
|
|
431
|
+
opacity: isAnimating ? 0.6 : 1,
|
|
432
|
+
}}
|
|
433
|
+
>
|
|
434
|
+
📁 {dir}
|
|
435
|
+
</button>
|
|
436
|
+
))}
|
|
437
|
+
|
|
438
|
+
{/* Some nested directories */}
|
|
439
|
+
<div
|
|
440
|
+
style={{
|
|
441
|
+
borderTop: '1px solid #4b5563',
|
|
442
|
+
paddingTop: '8px',
|
|
443
|
+
marginTop: '4px',
|
|
444
|
+
}}
|
|
445
|
+
>
|
|
446
|
+
<div
|
|
447
|
+
style={{
|
|
448
|
+
color: '#9ca3af',
|
|
449
|
+
fontFamily: 'monospace',
|
|
450
|
+
fontSize: '10px',
|
|
451
|
+
marginBottom: '4px',
|
|
452
|
+
}}
|
|
453
|
+
>
|
|
454
|
+
Nested:
|
|
455
|
+
</div>
|
|
456
|
+
{['src/components', 'src/utils', 'tests/unit'].map(dir => (
|
|
457
|
+
<button
|
|
458
|
+
key={dir}
|
|
459
|
+
onClick={() => handleZoomTo(dir)}
|
|
460
|
+
disabled={isAnimating}
|
|
461
|
+
style={{
|
|
462
|
+
padding: '6px 10px',
|
|
463
|
+
backgroundColor: zoomToPath === dir ? '#3b82f6' : '#1f2937',
|
|
464
|
+
color: 'white',
|
|
465
|
+
border: 'none',
|
|
466
|
+
borderRadius: '4px',
|
|
467
|
+
cursor: isAnimating ? 'not-allowed' : 'pointer',
|
|
468
|
+
fontFamily: 'monospace',
|
|
469
|
+
fontSize: '11px',
|
|
470
|
+
textAlign: 'left',
|
|
471
|
+
marginBottom: '4px',
|
|
472
|
+
width: '100%',
|
|
473
|
+
opacity: isAnimating ? 0.6 : 1,
|
|
474
|
+
}}
|
|
475
|
+
>
|
|
476
|
+
📂 {dir}
|
|
477
|
+
</button>
|
|
478
|
+
))}
|
|
479
|
+
</div>
|
|
480
|
+
</div>
|
|
481
|
+
|
|
482
|
+
{/* Current zoom info */}
|
|
483
|
+
<div
|
|
484
|
+
style={{
|
|
485
|
+
position: 'absolute',
|
|
486
|
+
bottom: 20,
|
|
487
|
+
left: 20,
|
|
488
|
+
zIndex: 100,
|
|
489
|
+
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
|
490
|
+
padding: '12px',
|
|
491
|
+
borderRadius: '8px',
|
|
492
|
+
color: 'white',
|
|
493
|
+
fontFamily: 'monospace',
|
|
494
|
+
fontSize: '11px',
|
|
495
|
+
}}
|
|
496
|
+
>
|
|
497
|
+
<div>Current: {zoomToPath || '(root)'}</div>
|
|
498
|
+
<div style={{ color: '#9ca3af', marginTop: '4px' }}>
|
|
499
|
+
💡 Click directories in the map or use buttons above
|
|
500
|
+
</div>
|
|
501
|
+
</div>
|
|
502
|
+
</div>
|
|
503
|
+
);
|
|
504
|
+
},
|
|
505
|
+
};
|