@principal-ai/file-city-react 0.5.2 → 0.5.4

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,16 @@
1
+ /**
2
+ * FileCity3D - 3D visualization component
3
+ */
4
+
5
+ export { FileCity3D, resetCamera } from './FileCity3D';
6
+ export type {
7
+ FileCity3DProps,
8
+ AnimationConfig,
9
+ HighlightLayer,
10
+ HighlightItem,
11
+ IsolationMode,
12
+ HeightScaling,
13
+ CityData,
14
+ CityBuilding,
15
+ CityDistrict,
16
+ } from './FileCity3D';
package/src/index.ts CHANGED
@@ -70,3 +70,14 @@ export {
70
70
 
71
71
  // Re-export theme utilities for consumers
72
72
  export { ThemeProvider, useTheme } from '@principal-ade/industry-theme';
73
+
74
+ // 3D visualization component
75
+ export { FileCity3D, resetCamera } from './components/FileCity3D';
76
+ export type {
77
+ FileCity3DProps,
78
+ AnimationConfig,
79
+ HighlightLayer as FileCity3DHighlightLayer,
80
+ HighlightItem as FileCity3DHighlightItem,
81
+ IsolationMode,
82
+ HeightScaling,
83
+ } from './components/FileCity3D';
@@ -0,0 +1,480 @@
1
+ import React from 'react';
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 } from '../components/FileCity3D';
5
+
6
+ const meta: Meta<typeof FileCity3D> = {
7
+ title: 'Components/FileCity3D',
8
+ component: FileCity3D,
9
+ decorators: [
10
+ (Story) => (
11
+ <ThemeProvider>
12
+ <Story />
13
+ </ThemeProvider>
14
+ ),
15
+ ],
16
+ parameters: {
17
+ layout: 'fullscreen',
18
+ },
19
+ argTypes: {
20
+ width: { control: 'text' },
21
+ height: { control: 'number' },
22
+ showControls: { control: 'boolean' },
23
+ heightScaling: { control: 'select', options: ['logarithmic', 'linear'] },
24
+ isolationMode: { control: 'select', options: ['none', 'transparent', 'collapse', 'hide'] },
25
+ dimOpacity: { control: { type: 'range', min: 0, max: 1, step: 0.05 } },
26
+ },
27
+ };
28
+
29
+ export default meta;
30
+ type Story = StoryObj<typeof FileCity3D>;
31
+
32
+ // Code extensions use lineCount for height
33
+ const CODE_EXTENSIONS = ['ts', 'tsx', 'js', 'jsx', 'py', 'rs', 'go', 'java'];
34
+ const NON_CODE_EXTENSIONS = ['json', 'css', 'md', 'yaml', 'svg', 'png'];
35
+
36
+ // Helper to generate sample buildings
37
+ function generateBuildings(
38
+ basePath: string,
39
+ count: number,
40
+ startX: number,
41
+ startZ: number,
42
+ areaWidth: number,
43
+ areaDepth: number
44
+ ): CityBuilding[] {
45
+ const buildings: CityBuilding[] = [];
46
+ const allExtensions = [...CODE_EXTENSIONS, ...NON_CODE_EXTENSIONS];
47
+ const cols = Math.ceil(Math.sqrt(count));
48
+
49
+ for (let i = 0; i < count; i++) {
50
+ const col = i % cols;
51
+ const row = Math.floor(i / cols);
52
+ const ext = allExtensions[i % allExtensions.length];
53
+ const isCode = CODE_EXTENSIONS.includes(ext);
54
+
55
+ // Code files: logarithmic distribution of line counts (20-3000 lines)
56
+ const lineCount = isCode
57
+ ? Math.floor(Math.exp(Math.random() * Math.log(3000 - 20) + Math.log(20)))
58
+ : undefined;
59
+ const size = isCode
60
+ ? lineCount! * 40
61
+ : Math.floor(Math.random() * 200000) + 1000;
62
+
63
+ buildings.push({
64
+ path: `${basePath}/file${i}.${ext}`,
65
+ position: {
66
+ x: startX + (col / cols) * areaWidth + areaWidth / cols / 2,
67
+ y: 0,
68
+ z: startZ + (row / cols) * areaDepth + areaDepth / cols / 2,
69
+ },
70
+ dimensions: [(areaWidth / cols) * 0.7, 10, (areaDepth / cols) * 0.7],
71
+ type: 'file',
72
+ fileExtension: ext,
73
+ size,
74
+ lineCount,
75
+ });
76
+ }
77
+
78
+ return buildings;
79
+ }
80
+
81
+ // Sample city data
82
+ const sampleCityData: CityData = {
83
+ buildings: [
84
+ ...generateBuildings('src', 12, 0, 0, 40, 40),
85
+ ...generateBuildings('src/components', 8, 50, 0, 30, 30),
86
+ ...generateBuildings('src/utils', 6, 50, 40, 25, 25),
87
+ ...generateBuildings('tests', 5, 0, 50, 30, 20),
88
+ ],
89
+ districts: [
90
+ {
91
+ path: 'src',
92
+ worldBounds: { minX: -2, maxX: 42, minZ: -2, maxZ: 42 },
93
+ fileCount: 12,
94
+ type: 'directory',
95
+ label: { text: 'src', bounds: { minX: -2, maxX: 42, minZ: 42, maxZ: 46 }, position: 'bottom' },
96
+ },
97
+ {
98
+ path: 'src/components',
99
+ worldBounds: { minX: 48, maxX: 82, minZ: -2, maxZ: 32 },
100
+ fileCount: 8,
101
+ type: 'directory',
102
+ label: { text: 'components', bounds: { minX: 48, maxX: 82, minZ: 32, maxZ: 36 }, position: 'bottom' },
103
+ },
104
+ {
105
+ path: 'src/utils',
106
+ worldBounds: { minX: 48, maxX: 77, minZ: 38, maxZ: 67 },
107
+ fileCount: 6,
108
+ type: 'directory',
109
+ label: { text: 'utils', bounds: { minX: 48, maxX: 77, minZ: 67, maxZ: 71 }, position: 'bottom' },
110
+ },
111
+ {
112
+ path: 'tests',
113
+ worldBounds: { minX: -2, maxX: 32, minZ: 48, maxZ: 72 },
114
+ fileCount: 5,
115
+ type: 'directory',
116
+ label: { text: 'tests', bounds: { minX: -2, maxX: 32, minZ: 72, maxZ: 76 }, position: 'bottom' },
117
+ },
118
+ ],
119
+ bounds: { minX: -5, maxX: 85, minZ: -5, maxZ: 80 },
120
+ metadata: { totalFiles: 31, totalDirectories: 4, rootPath: '/project' },
121
+ };
122
+
123
+ // Large city for stress testing
124
+ function generateLargeCityData(): CityData {
125
+ const buildings: CityBuilding[] = [];
126
+ const districts: CityDistrict[] = [];
127
+ const gridSize = 5;
128
+ const dirSize = 40;
129
+ const filesPerDir = 15;
130
+
131
+ for (let row = 0; row < gridSize; row++) {
132
+ for (let col = 0; col < gridSize; col++) {
133
+ const dirPath = `dir_${row}_${col}`;
134
+ const startX = col * (dirSize + 10);
135
+ const startZ = row * (dirSize + 10);
136
+
137
+ buildings.push(...generateBuildings(dirPath, filesPerDir, startX, startZ, dirSize, dirSize));
138
+ districts.push({
139
+ path: dirPath,
140
+ worldBounds: {
141
+ minX: startX - 2,
142
+ maxX: startX + dirSize + 2,
143
+ minZ: startZ - 2,
144
+ maxZ: startZ + dirSize + 2,
145
+ },
146
+ fileCount: filesPerDir,
147
+ type: 'directory',
148
+ label: {
149
+ text: dirPath,
150
+ bounds: {
151
+ minX: startX - 2,
152
+ maxX: startX + dirSize + 2,
153
+ minZ: startZ + dirSize + 2,
154
+ maxZ: startZ + dirSize + 6,
155
+ },
156
+ position: 'bottom',
157
+ },
158
+ });
159
+ }
160
+ }
161
+
162
+ const totalSize = gridSize * (dirSize + 10);
163
+ return {
164
+ buildings,
165
+ districts,
166
+ bounds: { minX: -10, maxX: totalSize + 10, minZ: -10, maxZ: totalSize + 10 },
167
+ metadata: { totalFiles: buildings.length, totalDirectories: districts.length, rootPath: '/large-project' },
168
+ };
169
+ }
170
+
171
+ // Monorepo layout
172
+ function generateMonorepoCityData(): CityData {
173
+ const buildings: CityBuilding[] = [];
174
+ const districts: CityDistrict[] = [];
175
+
176
+ const packages = [
177
+ { name: 'packages/core', files: 20, x: 0, z: 0, w: 50, d: 50 },
178
+ { name: 'packages/cli', files: 10, x: 60, z: 0, w: 35, d: 35 },
179
+ { name: 'packages/react', files: 15, x: 60, z: 45, w: 40, d: 40 },
180
+ { name: 'packages/server', files: 8, x: 0, z: 60, w: 30, d: 30 },
181
+ { name: 'apps/web', files: 25, x: 110, z: 0, w: 55, d: 55 },
182
+ { name: 'apps/docs', files: 12, x: 110, z: 65, w: 40, d: 35 },
183
+ ];
184
+
185
+ for (const pkg of packages) {
186
+ buildings.push(...generateBuildings(pkg.name, pkg.files, pkg.x, pkg.z, pkg.w, pkg.d));
187
+ districts.push({
188
+ path: pkg.name,
189
+ worldBounds: {
190
+ minX: pkg.x - 2,
191
+ maxX: pkg.x + pkg.w + 2,
192
+ minZ: pkg.z - 2,
193
+ maxZ: pkg.z + pkg.d + 2,
194
+ },
195
+ fileCount: pkg.files,
196
+ type: 'directory',
197
+ label: {
198
+ text: pkg.name.split('/').pop() || pkg.name,
199
+ bounds: {
200
+ minX: pkg.x - 2,
201
+ maxX: pkg.x + pkg.w + 2,
202
+ minZ: pkg.z + pkg.d + 2,
203
+ maxZ: pkg.z + pkg.d + 6,
204
+ },
205
+ position: 'bottom',
206
+ },
207
+ });
208
+ }
209
+
210
+ return {
211
+ buildings,
212
+ districts,
213
+ bounds: { minX: -10, maxX: 175, minZ: -10, maxZ: 110 },
214
+ metadata: { totalFiles: buildings.length, totalDirectories: districts.length, rootPath: '/monorepo' },
215
+ };
216
+ }
217
+
218
+ /**
219
+ * Default view - starts fully grown in 3D mode
220
+ */
221
+ export const Default: Story = {
222
+ args: {
223
+ cityData: sampleCityData,
224
+ height: '100vh',
225
+ },
226
+ };
227
+
228
+ /**
229
+ * Animated intro - starts flat (2D), then grows into 3D with a ripple effect
230
+ */
231
+ export const AnimatedIntro: Story = {
232
+ args: {
233
+ cityData: sampleCityData,
234
+ height: '100vh',
235
+ animation: {
236
+ startFlat: true,
237
+ autoStartDelay: 800,
238
+ staggerDelay: 20,
239
+ tension: 100,
240
+ friction: 12,
241
+ },
242
+ },
243
+ };
244
+
245
+ /**
246
+ * Manual control - starts flat, use button to trigger growth
247
+ */
248
+ export const ManualControl: Story = {
249
+ args: {
250
+ cityData: sampleCityData,
251
+ height: '100vh',
252
+ animation: {
253
+ startFlat: true,
254
+ autoStartDelay: null,
255
+ staggerDelay: 15,
256
+ },
257
+ showControls: true,
258
+ },
259
+ };
260
+
261
+ /**
262
+ * Fast animation - snappy growth effect
263
+ */
264
+ export const FastAnimation: Story = {
265
+ args: {
266
+ cityData: sampleCityData,
267
+ height: '100vh',
268
+ animation: {
269
+ startFlat: true,
270
+ autoStartDelay: 500,
271
+ staggerDelay: 8,
272
+ tension: 200,
273
+ friction: 18,
274
+ },
275
+ },
276
+ };
277
+
278
+ /**
279
+ * Slow dramatic reveal
280
+ */
281
+ export const SlowDramatic: Story = {
282
+ args: {
283
+ cityData: sampleCityData,
284
+ height: '100vh',
285
+ animation: {
286
+ startFlat: true,
287
+ autoStartDelay: 1000,
288
+ staggerDelay: 40,
289
+ tension: 60,
290
+ friction: 8,
291
+ },
292
+ },
293
+ };
294
+
295
+ /**
296
+ * Large city with animation - 375 buildings
297
+ */
298
+ export const LargeCityAnimated: Story = {
299
+ args: {
300
+ cityData: generateLargeCityData(),
301
+ height: '100vh',
302
+ animation: {
303
+ startFlat: true,
304
+ autoStartDelay: 600,
305
+ staggerDelay: 5,
306
+ tension: 150,
307
+ friction: 16,
308
+ },
309
+ },
310
+ };
311
+
312
+ /**
313
+ * Monorepo layout with animation
314
+ */
315
+ export const MonorepoAnimated: Story = {
316
+ args: {
317
+ cityData: generateMonorepoCityData(),
318
+ height: '100vh',
319
+ animation: {
320
+ startFlat: true,
321
+ autoStartDelay: 700,
322
+ staggerDelay: 12,
323
+ tension: 120,
324
+ friction: 14,
325
+ },
326
+ },
327
+ };
328
+
329
+ /**
330
+ * Static 3D view (no animation)
331
+ */
332
+ export const Static3D: Story = {
333
+ args: {
334
+ cityData: sampleCityData,
335
+ height: '100vh',
336
+ animation: { startFlat: false },
337
+ showControls: false,
338
+ },
339
+ };
340
+
341
+ /**
342
+ * With click handler
343
+ */
344
+ export const WithClickHandler: Story = {
345
+ args: {
346
+ cityData: sampleCityData,
347
+ height: '100vh',
348
+ onBuildingClick: (building) => {
349
+ console.log('Clicked building:', building.path);
350
+ alert(`Clicked: ${building.path}`);
351
+ },
352
+ },
353
+ };
354
+
355
+ /**
356
+ * Isolation - transparent mode
357
+ */
358
+ export const IsolationTransparent: Story = {
359
+ args: {
360
+ cityData: sampleCityData,
361
+ height: '100vh',
362
+ isolationMode: 'transparent',
363
+ dimOpacity: 0.1,
364
+ highlightLayers: [
365
+ {
366
+ id: 'focus',
367
+ name: 'Focus Layer',
368
+ enabled: true,
369
+ color: '#22c55e',
370
+ items: [{ path: 'src', type: 'directory' as const }],
371
+ },
372
+ ],
373
+ },
374
+ };
375
+
376
+ /**
377
+ * Isolation - collapse mode
378
+ */
379
+ export const IsolationCollapse: Story = {
380
+ args: {
381
+ cityData: sampleCityData,
382
+ height: '100vh',
383
+ isolationMode: 'collapse',
384
+ highlightLayers: [
385
+ {
386
+ id: 'focus',
387
+ name: 'Focus Layer',
388
+ enabled: true,
389
+ color: '#3b82f6',
390
+ items: [{ path: 'src/components', type: 'directory' as const }],
391
+ },
392
+ ],
393
+ },
394
+ };
395
+
396
+ /**
397
+ * Isolation - hide mode
398
+ */
399
+ export const IsolationHide: Story = {
400
+ args: {
401
+ cityData: sampleCityData,
402
+ height: '100vh',
403
+ isolationMode: 'hide',
404
+ highlightLayers: [
405
+ {
406
+ id: 'focus',
407
+ name: 'Focus Layer',
408
+ enabled: true,
409
+ color: '#f59e0b',
410
+ items: [{ path: 'tests', type: 'directory' as const }],
411
+ },
412
+ ],
413
+ },
414
+ };
415
+
416
+ /**
417
+ * Multiple highlight layers
418
+ */
419
+ export const MultipleHighlights: Story = {
420
+ args: {
421
+ cityData: sampleCityData,
422
+ height: '100vh',
423
+ isolationMode: 'transparent',
424
+ dimOpacity: 0.08,
425
+ highlightLayers: [
426
+ {
427
+ id: 'src',
428
+ name: 'Source',
429
+ enabled: true,
430
+ color: '#22c55e',
431
+ items: [{ path: 'src', type: 'directory' as const }],
432
+ },
433
+ {
434
+ id: 'tests',
435
+ name: 'Tests',
436
+ enabled: true,
437
+ color: '#ef4444',
438
+ items: [{ path: 'tests', type: 'directory' as const }],
439
+ },
440
+ ],
441
+ },
442
+ };
443
+
444
+ /**
445
+ * Animated intro with highlight
446
+ */
447
+ export const AnimatedWithHighlight: Story = {
448
+ args: {
449
+ cityData: sampleCityData,
450
+ height: '100vh',
451
+ animation: {
452
+ startFlat: true,
453
+ autoStartDelay: 800,
454
+ staggerDelay: 20,
455
+ },
456
+ isolationMode: 'transparent',
457
+ dimOpacity: 0.15,
458
+ highlightLayers: [
459
+ {
460
+ id: 'components',
461
+ name: 'Components',
462
+ enabled: true,
463
+ color: '#8b5cf6',
464
+ items: [{ path: 'src/components', type: 'directory' as const }],
465
+ },
466
+ ],
467
+ },
468
+ };
469
+
470
+ /**
471
+ * Linear height scaling
472
+ */
473
+ export const LinearHeightScaling: Story = {
474
+ args: {
475
+ cityData: sampleCityData,
476
+ height: '100vh',
477
+ heightScaling: 'linear',
478
+ linearScale: 0.5,
479
+ },
480
+ };