@principal-ai/file-city-react 0.5.22 → 0.5.24

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.
@@ -0,0 +1,527 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+
4
+ import { ArchitectureMapHighlightLayers } from '../components/ArchitectureMapHighlightLayers';
5
+ import { FileCity3D, type HighlightLayer, type IsolationMode, type CityData } from '../components/FileCity3D';
6
+ import { createFileColorHighlightLayers } from '../utils/fileColorHighlightLayers';
7
+ import authServerCityData from '../../../../assets/auth-server-city-data.json';
8
+
9
+ const meta = {
10
+ title: 'Comparison/2D vs 3D',
11
+ parameters: {
12
+ layout: 'fullscreen',
13
+ },
14
+ } satisfies Meta;
15
+
16
+ export default meta;
17
+
18
+ type ViewMode = '2d' | '3d-flat' | '3d-grown';
19
+
20
+ export const ViewModeSwitch: StoryObj = {
21
+ render: function RenderViewModeSwitch() {
22
+ const [viewMode, setViewMode] = useState<ViewMode>('2d');
23
+ const [overlayOpacity, setOverlayOpacity] = useState(1);
24
+ const [hideOverlay, setHideOverlay] = useState(false);
25
+ const cityData = authServerCityData as CityData;
26
+ const highlightLayers = createFileColorHighlightLayers(cityData.buildings);
27
+
28
+ const handleViewModeChange = (mode: ViewMode) => {
29
+ if (mode !== '2d' && viewMode === '2d') {
30
+ // Switching from 2D to 3D - reset overlay state before changing mode
31
+ setOverlayOpacity(1);
32
+ setHideOverlay(false);
33
+ }
34
+ setViewMode(mode);
35
+ };
36
+
37
+ // Fade out overlay after switching to 3D
38
+ useEffect(() => {
39
+ if (viewMode !== '2d') {
40
+ // Wait for 3D to render, then fade out
41
+ const fadeTimer = setTimeout(() => {
42
+ setOverlayOpacity(0);
43
+ }, 100);
44
+ // Remove overlay after fade completes
45
+ const removeTimer = setTimeout(() => {
46
+ setHideOverlay(true);
47
+ }, 400);
48
+ return () => {
49
+ clearTimeout(fadeTimer);
50
+ clearTimeout(removeTimer);
51
+ };
52
+ } else {
53
+ setHideOverlay(false);
54
+ setOverlayOpacity(1);
55
+ }
56
+ }, [viewMode]);
57
+
58
+ return (
59
+ <div style={{ width: '100vw', height: '100vh', display: 'flex', flexDirection: 'column' }}>
60
+ <div
61
+ style={{
62
+ padding: '12px 16px',
63
+ backgroundColor: '#1f2937',
64
+ borderBottom: '1px solid #374151',
65
+ display: 'flex',
66
+ gap: '8px',
67
+ alignItems: 'center',
68
+ }}
69
+ >
70
+ <span style={{ color: '#9ca3af', marginRight: '8px', fontSize: '14px' }}>View Mode:</span>
71
+ {(['2d', '3d-flat', '3d-grown'] as ViewMode[]).map(mode => (
72
+ <button
73
+ key={mode}
74
+ onClick={() => handleViewModeChange(mode)}
75
+ style={{
76
+ padding: '6px 12px',
77
+ borderRadius: '6px',
78
+ border: 'none',
79
+ cursor: 'pointer',
80
+ fontSize: '13px',
81
+ fontWeight: 500,
82
+ backgroundColor: viewMode === mode ? '#3b82f6' : '#374151',
83
+ color: viewMode === mode ? '#ffffff' : '#d1d5db',
84
+ transition: 'all 0.15s ease',
85
+ }}
86
+ >
87
+ {mode === '2d' ? '2D Canvas' : mode === '3d-flat' ? '3D (Flat)' : '3D (Grown)'}
88
+ </button>
89
+ ))}
90
+ <span style={{ color: '#6b7280', fontSize: '12px', marginLeft: '16px' }}>
91
+ Compare initial render between 2D canvas and 3D flat view
92
+ </span>
93
+ </div>
94
+
95
+ <div style={{ flex: 1, backgroundColor: '#0f1419', position: 'relative' }}>
96
+ {/* 3D layer (behind) */}
97
+ {viewMode !== '2d' && (
98
+ <FileCity3D
99
+ cityData={cityData}
100
+ highlightLayers={highlightLayers}
101
+ width="100%"
102
+ height="100%"
103
+ isGrown={viewMode === '3d-grown'}
104
+ showControls={true}
105
+ backgroundColor="#0f1419"
106
+ />
107
+ )}
108
+
109
+ {/* 2D layer (on top, fades out when switching to 3D) */}
110
+ {(viewMode === '2d' || !hideOverlay) && (
111
+ <div
112
+ style={{
113
+ position: 'absolute',
114
+ top: 0,
115
+ left: 0,
116
+ right: 0,
117
+ bottom: 0,
118
+ opacity: viewMode === '2d' ? 1 : overlayOpacity,
119
+ transition: 'opacity 300ms ease-out',
120
+ pointerEvents: viewMode === '2d' ? 'auto' : 'none',
121
+ }}
122
+ >
123
+ <ArchitectureMapHighlightLayers
124
+ cityData={cityData}
125
+ highlightLayers={highlightLayers}
126
+ fullSize={true}
127
+ canvasBackgroundColor="#0f1419"
128
+ defaultBuildingColor="#36454F"
129
+ defaultDirectoryColor="#111827"
130
+ enableZoom={viewMode === '2d'}
131
+ />
132
+ </div>
133
+ )}
134
+ </div>
135
+ </div>
136
+ );
137
+ },
138
+ };
139
+
140
+ export const SideBySide: StoryObj = {
141
+ render: function RenderSideBySide() {
142
+ const cityData = authServerCityData as CityData;
143
+ const highlightLayers = createFileColorHighlightLayers(cityData.buildings);
144
+
145
+ return (
146
+ <div style={{ width: '100vw', height: '100vh', display: 'flex', flexDirection: 'column' }}>
147
+ <div
148
+ style={{
149
+ padding: '12px 16px',
150
+ backgroundColor: '#1f2937',
151
+ borderBottom: '1px solid #374151',
152
+ }}
153
+ >
154
+ <span style={{ color: '#9ca3af', fontSize: '14px' }}>
155
+ Side-by-side comparison: 2D Canvas (left) vs 3D Flat (right)
156
+ </span>
157
+ </div>
158
+
159
+ <div style={{ flex: 1, display: 'flex' }}>
160
+ <div
161
+ style={{
162
+ flex: 1,
163
+ borderRight: '1px solid #374151',
164
+ display: 'flex',
165
+ flexDirection: 'column',
166
+ }}
167
+ >
168
+ <div
169
+ style={{
170
+ padding: '8px 12px',
171
+ backgroundColor: '#111827',
172
+ borderBottom: '1px solid #374151',
173
+ color: '#9ca3af',
174
+ fontSize: '12px',
175
+ fontWeight: 500,
176
+ }}
177
+ >
178
+ 2D CANVAS
179
+ </div>
180
+ <div style={{ flex: 1, backgroundColor: '#0f1419' }}>
181
+ <ArchitectureMapHighlightLayers
182
+ cityData={cityData}
183
+ highlightLayers={highlightLayers}
184
+ fullSize={true}
185
+ canvasBackgroundColor="#0f1419"
186
+ defaultBuildingColor="#36454F"
187
+ defaultDirectoryColor="#111827"
188
+ enableZoom={true}
189
+ />
190
+ </div>
191
+ </div>
192
+
193
+ <div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
194
+ <div
195
+ style={{
196
+ padding: '8px 12px',
197
+ backgroundColor: '#111827',
198
+ borderBottom: '1px solid #374151',
199
+ color: '#9ca3af',
200
+ fontSize: '12px',
201
+ fontWeight: 500,
202
+ }}
203
+ >
204
+ 3D FLAT
205
+ </div>
206
+ <div style={{ flex: 1, backgroundColor: '#0f1419' }}>
207
+ <FileCity3D
208
+ cityData={cityData}
209
+ highlightLayers={highlightLayers}
210
+ width="100%"
211
+ height="100%"
212
+ isGrown={false}
213
+ showControls={false}
214
+ backgroundColor="#0f1419"
215
+ />
216
+ </div>
217
+ </div>
218
+ </div>
219
+ </div>
220
+ );
221
+ },
222
+ };
223
+
224
+ // Test scenarios for highlight comparison
225
+ // No explicit isolationMode - let resolution determine everything
226
+ interface TestScenario {
227
+ id: string;
228
+ name: string;
229
+ description: string;
230
+ focusDirectory: string | null;
231
+ focusColor?: string | null;
232
+ highlightLayers: HighlightLayer[];
233
+ }
234
+
235
+ const testScenarios: TestScenario[] = [
236
+ {
237
+ id: 'S1-baseline',
238
+ name: 'S1: Baseline',
239
+ description: 'Full city view, no focus, no highlights - all file colors shown',
240
+ focusDirectory: null,
241
+ highlightLayers: [],
242
+ },
243
+ {
244
+ id: 'S2-focus-only',
245
+ name: 'S2: Focus Only (src)',
246
+ description: 'Camera zooms to src, file colors filtered to focus area',
247
+ focusDirectory: 'auth-server/src',
248
+ focusColor: '#3b82f6',
249
+ highlightLayers: [],
250
+ },
251
+ {
252
+ id: 'S2b-focus-only-tests',
253
+ name: 'S2b: Focus Only (bruno)',
254
+ description: 'Camera zooms to bruno directory',
255
+ focusDirectory: 'auth-server/bruno',
256
+ focusColor: '#22c55e',
257
+ highlightLayers: [],
258
+ },
259
+ {
260
+ id: 'S3-highlight-only',
261
+ name: 'S3: Highlight Only',
262
+ description: 'Full view with highlight layer, file colors filtered to highlight area',
263
+ focusDirectory: null,
264
+ highlightLayers: [
265
+ {
266
+ id: 'api-layer',
267
+ name: 'API Routes',
268
+ enabled: true,
269
+ color: '#22c55e',
270
+ items: [{ path: 'auth-server/src/app/api', type: 'directory' as const }],
271
+ },
272
+ ],
273
+ },
274
+ {
275
+ id: 'S4-focus-highlight-same',
276
+ name: 'S4: Focus + Highlight (same directory)',
277
+ description: 'Focus and highlight on same directory',
278
+ focusDirectory: 'auth-server/src/app/api',
279
+ focusColor: '#3b82f6',
280
+ highlightLayers: [
281
+ {
282
+ id: 'api-layer',
283
+ name: 'API Routes',
284
+ enabled: true,
285
+ color: '#3b82f6',
286
+ items: [{ path: 'auth-server/src/app/api', type: 'directory' as const }],
287
+ },
288
+ ],
289
+ },
290
+ {
291
+ id: 'S5-focus-highlight-subset',
292
+ name: 'S5: Focus + Highlight (subset)',
293
+ description: 'Focus on src, highlight only lib subset',
294
+ focusDirectory: 'auth-server/src',
295
+ focusColor: '#3b82f6',
296
+ highlightLayers: [
297
+ {
298
+ id: 'lib-layer',
299
+ name: 'Libraries',
300
+ enabled: true,
301
+ color: '#8b5cf6',
302
+ items: [{ path: 'auth-server/src/lib', type: 'directory' as const }],
303
+ },
304
+ ],
305
+ },
306
+ {
307
+ id: 'S6-multiple-highlights-focus',
308
+ name: 'S6: Multiple Highlights (with focus)',
309
+ description: 'Two highlight layers within focused area',
310
+ focusDirectory: 'auth-server/src',
311
+ focusColor: '#3b82f6',
312
+ highlightLayers: [
313
+ {
314
+ id: 'api-layer',
315
+ name: 'API Routes',
316
+ enabled: true,
317
+ color: '#22c55e',
318
+ items: [{ path: 'auth-server/src/app/api', type: 'directory' as const }],
319
+ },
320
+ {
321
+ id: 'lib-layer',
322
+ name: 'Libraries',
323
+ enabled: true,
324
+ color: '#f59e0b',
325
+ items: [{ path: 'auth-server/src/lib', type: 'directory' as const }],
326
+ },
327
+ ],
328
+ },
329
+ {
330
+ id: 'S7-multiple-highlights-no-focus',
331
+ name: 'S7: Multiple Highlights (no focus)',
332
+ description: 'Two highlight layers, file colors filtered to both areas',
333
+ focusDirectory: null,
334
+ highlightLayers: [
335
+ {
336
+ id: 'api-layer',
337
+ name: 'API Routes',
338
+ enabled: true,
339
+ color: '#3b82f6',
340
+ items: [{ path: 'auth-server/src/app/api', type: 'directory' as const }],
341
+ },
342
+ {
343
+ id: 'bruno-layer',
344
+ name: 'Bruno Tests',
345
+ enabled: true,
346
+ color: '#ef4444',
347
+ items: [{ path: 'auth-server/bruno', type: 'directory' as const }],
348
+ },
349
+ ],
350
+ },
351
+ {
352
+ id: 'S8-single-file',
353
+ name: 'S8: Single File Highlight',
354
+ description: 'Highlight a single file',
355
+ focusDirectory: null,
356
+ highlightLayers: [
357
+ {
358
+ id: 'single-file-layer',
359
+ name: 'Single File',
360
+ enabled: true,
361
+ color: '#ec4899',
362
+ items: [{ path: 'auth-server/src/app/api/auth/workos/callback/route.ts', type: 'file' as const }],
363
+ },
364
+ ],
365
+ },
366
+ {
367
+ id: 'S9-multiple-files',
368
+ name: 'S9: Multiple Files Highlight',
369
+ description: 'Highlight multiple individual files',
370
+ focusDirectory: null,
371
+ highlightLayers: [
372
+ {
373
+ id: 'files-layer',
374
+ name: 'Selected Files',
375
+ enabled: true,
376
+ color: '#14b8a6',
377
+ items: [
378
+ { path: 'auth-server/src/app/api/auth/workos/callback/route.ts', type: 'file' as const },
379
+ { path: 'auth-server/src/app/api/auth/workos/verify/route.ts', type: 'file' as const },
380
+ { path: 'auth-server/src/app/api/auth/workos/token/route.ts', type: 'file' as const },
381
+ ],
382
+ },
383
+ ],
384
+ },
385
+ ];
386
+
387
+ export const ScenarioComparison: StoryObj = {
388
+ render: function RenderScenarioComparison() {
389
+ const [currentScenarioIndex, setCurrentScenarioIndex] = useState(0);
390
+ const scenario = testScenarios[currentScenarioIndex];
391
+ const cityData = authServerCityData as CityData;
392
+
393
+ // Base file color layers - resolution will filter these based on highlights/focus
394
+ const fileColorLayers = createFileColorHighlightLayers(cityData.buildings);
395
+
396
+ return (
397
+ <div style={{ width: '100vw', height: '100vh', display: 'flex', flexDirection: 'column' }}>
398
+ {/* Scenario selector */}
399
+ <div
400
+ style={{
401
+ padding: '12px 16px',
402
+ backgroundColor: '#1f2937',
403
+ borderBottom: '1px solid #374151',
404
+ display: 'flex',
405
+ gap: '8px',
406
+ alignItems: 'center',
407
+ flexWrap: 'wrap',
408
+ }}
409
+ >
410
+ <span style={{ color: '#9ca3af', fontSize: '14px', marginRight: '8px' }}>Scenario:</span>
411
+ {testScenarios.map((s, index) => (
412
+ <button
413
+ key={s.id}
414
+ onClick={() => setCurrentScenarioIndex(index)}
415
+ style={{
416
+ padding: '4px 8px',
417
+ borderRadius: '4px',
418
+ border: 'none',
419
+ cursor: 'pointer',
420
+ fontSize: '11px',
421
+ fontWeight: 500,
422
+ backgroundColor: currentScenarioIndex === index ? '#3b82f6' : '#374151',
423
+ color: currentScenarioIndex === index ? '#ffffff' : '#d1d5db',
424
+ }}
425
+ >
426
+ {s.name}
427
+ </button>
428
+ ))}
429
+ </div>
430
+
431
+ {/* Scenario info */}
432
+ <div
433
+ style={{
434
+ padding: '8px 16px',
435
+ backgroundColor: '#111827',
436
+ borderBottom: '1px solid #374151',
437
+ fontSize: '12px',
438
+ color: '#9ca3af',
439
+ }}
440
+ >
441
+ <strong>{scenario.name}</strong>: {scenario.description}
442
+ {scenario.focusDirectory && (
443
+ <span style={{ marginLeft: '12px', color: '#22c55e' }}>
444
+ Focus: {scenario.focusDirectory}
445
+ </span>
446
+ )}
447
+ {scenario.highlightLayers.length > 0 && (
448
+ <span style={{ marginLeft: '12px', color: '#3b82f6' }}>
449
+ Layers: {scenario.highlightLayers.map(l => l.name).join(', ')}
450
+ </span>
451
+ )}
452
+ </div>
453
+
454
+ {/* Side by side comparison */}
455
+ <div style={{ flex: 1, display: 'flex' }}>
456
+ <div
457
+ style={{
458
+ flex: 1,
459
+ borderRight: '1px solid #374151',
460
+ display: 'flex',
461
+ flexDirection: 'column',
462
+ }}
463
+ >
464
+ <div
465
+ style={{
466
+ padding: '8px 12px',
467
+ backgroundColor: '#111827',
468
+ borderBottom: '1px solid #374151',
469
+ color: '#9ca3af',
470
+ fontSize: '12px',
471
+ fontWeight: 500,
472
+ }}
473
+ >
474
+ 2D CANVAS
475
+ </div>
476
+ <div style={{ flex: 1, backgroundColor: '#0f1419' }}>
477
+ <ArchitectureMapHighlightLayers
478
+ cityData={cityData}
479
+ highlightLayers={scenario.highlightLayers}
480
+ fileColorLayers={fileColorLayers}
481
+ zoomToPath={scenario.focusDirectory}
482
+ focusColor={scenario.focusColor}
483
+ allowZoomToPath={true}
484
+ zoomAnimationSpeed={0.12}
485
+ fullSize={true}
486
+ canvasBackgroundColor="#0f1419"
487
+ defaultBuildingColor="#36454F"
488
+ defaultDirectoryColor="#111827"
489
+ enableZoom={true}
490
+ />
491
+ </div>
492
+ </div>
493
+
494
+ <div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
495
+ <div
496
+ style={{
497
+ padding: '8px 12px',
498
+ backgroundColor: '#111827',
499
+ borderBottom: '1px solid #374151',
500
+ color: '#9ca3af',
501
+ fontSize: '12px',
502
+ fontWeight: 500,
503
+ }}
504
+ >
505
+ 3D
506
+ </div>
507
+ <div style={{ flex: 1, backgroundColor: '#0f1419' }}>
508
+ <FileCity3D
509
+ cityData={cityData}
510
+ highlightLayers={scenario.highlightLayers}
511
+ fileColorLayers={fileColorLayers}
512
+ focusDirectory={scenario.focusDirectory}
513
+ focusColor={scenario.focusColor}
514
+ width="100%"
515
+ height="100%"
516
+ isGrown={true}
517
+ showControls={true}
518
+ backgroundColor="#0f1419"
519
+ />
520
+ </div>
521
+ </div>
522
+ </div>
523
+ </div>
524
+ );
525
+ },
526
+ };
527
+
@@ -6,67 +6,69 @@ import {
6
6
  import { FileInfo } from '@principal-ai/repository-abstraction';
7
7
 
8
8
  // Sample file structure representing a typical project
9
- const sampleFileStructure: Array<{ path: string; size: number }> = [
9
+ // lineCount is estimated as size / 35 (avg ~35 bytes per line)
10
+ const sampleFileStructure: Array<{ path: string; size: number; lineCount: number }> = [
10
11
  // Source files
11
- { path: 'src/index.ts', size: 1500 },
12
- { path: 'src/App.tsx', size: 3200 },
13
- { path: 'src/components/Header.tsx', size: 1800 },
14
- { path: 'src/components/Footer.tsx', size: 1200 },
15
- { path: 'src/components/Sidebar.tsx', size: 2100 },
16
- { path: 'src/components/Card.tsx', size: 900 },
17
- { path: 'src/components/Button.tsx', size: 600 },
18
- { path: 'src/utils/helpers.ts', size: 2500 },
19
- { path: 'src/utils/api.ts', size: 3100 },
20
- { path: 'src/utils/validators.ts', size: 1400 },
21
- { path: 'src/hooks/useAuth.ts', size: 800 },
22
- { path: 'src/hooks/useData.ts', size: 1100 },
23
- { path: 'src/styles/main.css', size: 4500 },
24
- { path: 'src/styles/components.css', size: 2800 },
12
+ { path: 'src/index.ts', size: 1500, lineCount: 45 },
13
+ { path: 'src/App.tsx', size: 3200, lineCount: 95 },
14
+ { path: 'src/components/Header.tsx', size: 1800, lineCount: 55 },
15
+ { path: 'src/components/Footer.tsx', size: 1200, lineCount: 35 },
16
+ { path: 'src/components/Sidebar.tsx', size: 2100, lineCount: 65 },
17
+ { path: 'src/components/Card.tsx', size: 900, lineCount: 28 },
18
+ { path: 'src/components/Button.tsx', size: 600, lineCount: 20 },
19
+ { path: 'src/utils/helpers.ts', size: 2500, lineCount: 75 },
20
+ { path: 'src/utils/api.ts', size: 3100, lineCount: 92 },
21
+ { path: 'src/utils/validators.ts', size: 1400, lineCount: 42 },
22
+ { path: 'src/hooks/useAuth.ts', size: 800, lineCount: 25 },
23
+ { path: 'src/hooks/useData.ts', size: 1100, lineCount: 34 },
24
+ { path: 'src/styles/main.css', size: 4500, lineCount: 180 },
25
+ { path: 'src/styles/components.css', size: 2800, lineCount: 112 },
25
26
 
26
27
  // 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 },
28
+ { path: 'tests/unit/app.test.ts', size: 2200, lineCount: 68 },
29
+ { path: 'tests/unit/header.test.ts', size: 1600, lineCount: 50 },
30
+ { path: 'tests/unit/footer.test.tsx', size: 1400, lineCount: 44 },
31
+ { path: 'tests/integration/api.test.ts', size: 3400, lineCount: 105 },
32
+ { path: '__tests__/components.test.tsx', size: 2900, lineCount: 90 },
33
+ { path: '__tests__/utils.test.ts', size: 1900, lineCount: 58 },
33
34
 
34
35
  // Config files
35
- { path: 'package.json', size: 1200 },
36
- { path: 'tsconfig.json', size: 800 },
37
- { path: 'webpack.config.js', size: 2100 },
38
- { path: '.eslintrc.js', size: 600 },
39
- { path: '.prettierrc', size: 200 },
40
- { path: 'README.md', size: 3500 },
36
+ { path: 'package.json', size: 1200, lineCount: 45 },
37
+ { path: 'tsconfig.json', size: 800, lineCount: 30 },
38
+ { path: 'webpack.config.js', size: 2100, lineCount: 65 },
39
+ { path: '.eslintrc.js', size: 600, lineCount: 22 },
40
+ { path: '.prettierrc', size: 200, lineCount: 8 },
41
+ { path: 'README.md', size: 3500, lineCount: 120 },
41
42
 
42
43
  // Documentation
43
- { path: 'docs/README.md', size: 4200 },
44
- { path: 'docs/API.md', size: 5100 },
45
- { path: 'docs/CONTRIBUTING.md', size: 2300 },
44
+ { path: 'docs/README.md', size: 4200, lineCount: 140 },
45
+ { path: 'docs/API.md', size: 5100, lineCount: 170 },
46
+ { path: 'docs/CONTRIBUTING.md', size: 2300, lineCount: 80 },
46
47
 
47
48
  // Build files
48
- { path: 'dist/bundle.js', size: 45000 },
49
- { path: 'dist/index.html', size: 800 },
50
- { path: 'dist/styles.css', size: 12000 },
49
+ { path: 'dist/bundle.js', size: 45000, lineCount: 1500 },
50
+ { path: 'dist/index.html', size: 800, lineCount: 30 },
51
+ { path: 'dist/styles.css', size: 12000, lineCount: 480 },
51
52
 
52
53
  // Node modules (sample)
53
- { path: 'node_modules/react/index.js', size: 8000 },
54
- { path: 'node_modules/react/package.json', size: 1500 },
55
- { path: 'node_modules/typescript/lib/typescript.js', size: 65000 },
56
- { path: 'node_modules/@types/react/index.d.ts', size: 3200 },
54
+ { path: 'node_modules/react/index.js', size: 8000, lineCount: 250 },
55
+ { path: 'node_modules/react/package.json', size: 1500, lineCount: 55 },
56
+ { path: 'node_modules/typescript/lib/typescript.js', size: 65000, lineCount: 2200 },
57
+ { path: 'node_modules/@types/react/index.d.ts', size: 3200, lineCount: 100 },
57
58
 
58
59
  // Deprecated files
59
- { path: 'src/deprecated/OldComponent.tsx', size: 2400 },
60
- { path: 'src/deprecated/LegacyAPI.ts', size: 3100 },
60
+ { path: 'src/deprecated/OldComponent.tsx', size: 2400, lineCount: 72 },
61
+ { path: 'src/deprecated/LegacyAPI.ts', size: 3100, lineCount: 95 },
61
62
  ];
62
63
 
63
64
  // Convert file structure to FileInfo objects
64
- function createFileInfoList(files: Array<{ path: string; size: number }>): FileInfo[] {
65
+ function createFileInfoList(files: Array<{ path: string; size: number; lineCount: number }>): FileInfo[] {
65
66
  return files.map(file => ({
66
67
  name: file.path.split('/').pop() || file.path,
67
68
  path: file.path,
68
69
  relativePath: file.path,
69
70
  size: file.size,
71
+ lineCount: file.lineCount,
70
72
  extension: file.path.includes('.') ? '.' + (file.path.split('.').pop() || '') : '',
71
73
  lastModified: new Date(),
72
74
  isDirectory: false,
@@ -99,11 +101,11 @@ export function createSampleCityData(): CityData {
99
101
  }
100
102
 
101
103
  // Smaller sample file structure
102
- const smallFileStructure: Array<{ path: string; size: number }> = [
103
- { path: 'src/index.ts', size: 1500 },
104
- { path: 'src/App.tsx', size: 3200 },
105
- { path: 'src/utils/helpers.ts', size: 800 },
106
- { path: 'package.json', size: 1200 },
104
+ const smallFileStructure: Array<{ path: string; size: number; lineCount: number }> = [
105
+ { path: 'src/index.ts', size: 1500, lineCount: 45 },
106
+ { path: 'src/App.tsx', size: 3200, lineCount: 95 },
107
+ { path: 'src/utils/helpers.ts', size: 800, lineCount: 25 },
108
+ { path: 'package.json', size: 1200, lineCount: 45 },
107
109
  ];
108
110
 
109
111
  let cachedSmallCityData: CityData | null = null;