@principal-ai/file-city-react 0.3.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.
Files changed (51) hide show
  1. package/dist/builder/cityDataUtils.d.ts +15 -0
  2. package/dist/builder/cityDataUtils.d.ts.map +1 -0
  3. package/dist/builder/cityDataUtils.js +348 -0
  4. package/dist/components/ArchitectureMapHighlightLayers.d.ts +63 -0
  5. package/dist/components/ArchitectureMapHighlightLayers.d.ts.map +1 -0
  6. package/dist/components/ArchitectureMapHighlightLayers.js +1040 -0
  7. package/dist/components/CityViewWithReactFlow.d.ts +14 -0
  8. package/dist/components/CityViewWithReactFlow.d.ts.map +1 -0
  9. package/dist/components/CityViewWithReactFlow.js +266 -0
  10. package/dist/config/files.json +996 -0
  11. package/dist/hooks/useCodeCityData.d.ts +21 -0
  12. package/dist/hooks/useCodeCityData.d.ts.map +1 -0
  13. package/dist/hooks/useCodeCityData.js +57 -0
  14. package/dist/index.d.ts +14 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +29 -0
  17. package/dist/render/client/drawLayeredBuildings.d.ts +51 -0
  18. package/dist/render/client/drawLayeredBuildings.d.ts.map +1 -0
  19. package/dist/render/client/drawLayeredBuildings.js +650 -0
  20. package/dist/stories/ArchitectureMapGridLayout.stories.d.ts +73 -0
  21. package/dist/stories/ArchitectureMapGridLayout.stories.d.ts.map +1 -0
  22. package/dist/stories/ArchitectureMapGridLayout.stories.js +345 -0
  23. package/dist/stories/ArchitectureMapHighlightLayers.stories.d.ts +78 -0
  24. package/dist/stories/ArchitectureMapHighlightLayers.stories.d.ts.map +1 -0
  25. package/dist/stories/ArchitectureMapHighlightLayers.stories.js +270 -0
  26. package/dist/stories/CityViewWithReactFlow.stories.d.ts +24 -0
  27. package/dist/stories/CityViewWithReactFlow.stories.d.ts.map +1 -0
  28. package/dist/stories/CityViewWithReactFlow.stories.js +778 -0
  29. package/dist/stories/sample-data.d.ts +4 -0
  30. package/dist/stories/sample-data.d.ts.map +1 -0
  31. package/dist/stories/sample-data.js +268 -0
  32. package/dist/types/react-types.d.ts +17 -0
  33. package/dist/types/react-types.d.ts.map +1 -0
  34. package/dist/types/react-types.js +4 -0
  35. package/dist/utils/fileColorHighlightLayers.d.ts +86 -0
  36. package/dist/utils/fileColorHighlightLayers.d.ts.map +1 -0
  37. package/dist/utils/fileColorHighlightLayers.js +283 -0
  38. package/package.json +49 -0
  39. package/src/builder/cityDataUtils.ts +430 -0
  40. package/src/components/ArchitectureMapHighlightLayers.tsx +1518 -0
  41. package/src/components/CityViewWithReactFlow.tsx +365 -0
  42. package/src/config/files.json +996 -0
  43. package/src/hooks/useCodeCityData.ts +82 -0
  44. package/src/index.ts +64 -0
  45. package/src/render/client/drawLayeredBuildings.ts +946 -0
  46. package/src/stories/ArchitectureMapGridLayout.stories.tsx +410 -0
  47. package/src/stories/ArchitectureMapHighlightLayers.stories.tsx +312 -0
  48. package/src/stories/CityViewWithReactFlow.stories.tsx +787 -0
  49. package/src/stories/sample-data.ts +301 -0
  50. package/src/types/react-types.ts +18 -0
  51. package/src/utils/fileColorHighlightLayers.ts +378 -0
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@principal-ai/file-city-react",
3
+ "version": "0.3.0",
4
+ "description": "React components for File City visualization",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "dev": "tsc --watch",
10
+ "clean": "rm -rf dist",
11
+ "storybook": "storybook dev -p 6006",
12
+ "build-storybook": "storybook build"
13
+ },
14
+ "dependencies": {
15
+ "@principal-ade/industry-theme": "^0.1.3",
16
+ "@principal-ai/alexandria-core-library": "^0.1.36",
17
+ "@principal-ai/file-city-builder": "^0.3.0",
18
+ "reactflow": "^11.11.4"
19
+ },
20
+ "peerDependencies": {
21
+ "react": "^19.0.0"
22
+ },
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",
32
+ "react": "^19.1.1",
33
+ "react-dom": "^19.1.1",
34
+ "storybook": "^7.6.0",
35
+ "typescript": "^5.0.0",
36
+ "vite": "^5.0.0"
37
+ },
38
+ "files": [
39
+ "dist",
40
+ "src"
41
+ ],
42
+ "keywords": [
43
+ "file-city",
44
+ "visualization",
45
+ "react",
46
+ "architecture"
47
+ ],
48
+ "license": "MIT"
49
+ }
@@ -0,0 +1,430 @@
1
+ import {
2
+ CityBuilding,
3
+ CityData,
4
+ CityDistrict,
5
+ SelectiveRenderOptions,
6
+ } from '@principal-ai/file-city-builder';
7
+
8
+ // Utility functions for selective rendering
9
+ export const filterCityDataForSelectiveRender = (
10
+ cityData: CityData,
11
+ selectiveRender?: SelectiveRenderOptions,
12
+ ): CityData => {
13
+ if (!selectiveRender || selectiveRender.mode === 'all') {
14
+ return cityData;
15
+ }
16
+
17
+ const { mode, directories, rootDirectory, showParentContext } = selectiveRender;
18
+
19
+ switch (mode) {
20
+ case 'filter':
21
+ return filterCityData(cityData, directories || new Set());
22
+
23
+ case 'focus':
24
+ // Focus mode doesn't filter data, just affects rendering
25
+ return cityData;
26
+
27
+ case 'drilldown':
28
+ return drilldownCityData(cityData, rootDirectory || '', showParentContext || false);
29
+
30
+ default:
31
+ return cityData;
32
+ }
33
+ };
34
+
35
+ const filterCityData = (cityData: CityData, visibleDirectories: Set<string>): CityData => {
36
+ const filteredBuildings = cityData.buildings.filter(building => {
37
+ // Check if building is in any of the visible directories
38
+ return Array.from(visibleDirectories).some(
39
+ dir => building.path.startsWith(dir + '/') || building.path === dir,
40
+ );
41
+ });
42
+
43
+ const filteredDistricts = cityData.districts.filter(district => {
44
+ // Include district if it's in the visible set or is a parent of a visible directory
45
+ return Array.from(visibleDirectories).some(
46
+ dir =>
47
+ district.path === dir ||
48
+ dir.startsWith(district.path + '/') ||
49
+ district.path.startsWith(dir + '/'),
50
+ );
51
+ });
52
+
53
+ return {
54
+ ...cityData,
55
+ buildings: filteredBuildings,
56
+ districts: filteredDistricts,
57
+ bounds: recalculateBounds(filteredBuildings, filteredDistricts),
58
+ };
59
+ };
60
+
61
+ const drilldownCityData = (
62
+ cityData: CityData,
63
+ rootDirectory: string,
64
+ showParentContext: boolean,
65
+ ): CityData => {
66
+ if (!rootDirectory) return cityData;
67
+
68
+ // Filter buildings to only those within the root directory
69
+ const filteredBuildings = cityData.buildings
70
+ .filter(
71
+ building => building.path.startsWith(rootDirectory + '/') || building.path === rootDirectory,
72
+ )
73
+ .map(building => ({
74
+ ...building,
75
+ // Adjust path to be relative to the new root
76
+ path: building.path.startsWith(rootDirectory + '/')
77
+ ? building.path.substring(rootDirectory.length + 1)
78
+ : building.path,
79
+ }));
80
+
81
+ // Filter districts to only those within the root directory
82
+ const filteredDistricts = cityData.districts
83
+ .filter(
84
+ district => district.path.startsWith(rootDirectory + '/') || district.path === rootDirectory,
85
+ )
86
+ .map(district => ({
87
+ ...district,
88
+ // Adjust path to be relative to the new root
89
+ path: district.path.startsWith(rootDirectory + '/')
90
+ ? district.path.substring(rootDirectory.length + 1)
91
+ : district.path === rootDirectory
92
+ ? ''
93
+ : district.path,
94
+ }));
95
+
96
+ // If showing parent context, add immediate parent district
97
+ if (showParentContext && rootDirectory) {
98
+ const parentPath = rootDirectory.split('/').slice(0, -1).join('/');
99
+ const parentDistrict = cityData.districts.find(d => d.path === parentPath);
100
+ if (parentDistrict) {
101
+ filteredDistricts.unshift({
102
+ ...parentDistrict,
103
+ path: '../' + parentDistrict.path.split('/').pop(),
104
+ });
105
+ }
106
+ }
107
+
108
+ return {
109
+ ...cityData,
110
+ buildings: filteredBuildings,
111
+ districts: filteredDistricts,
112
+ bounds: recalculateBounds(filteredBuildings, filteredDistricts),
113
+ metadata: {
114
+ ...cityData.metadata,
115
+ rootPath: rootDirectory,
116
+ },
117
+ };
118
+ };
119
+
120
+ const recalculateBounds = (buildings: CityBuilding[], districts: CityDistrict[]) => {
121
+ if (buildings.length === 0 && districts.length === 0) {
122
+ return { minX: 0, maxX: 100, minZ: 0, maxZ: 100 };
123
+ }
124
+
125
+ const allX = [
126
+ ...buildings.map(b => b.position.x),
127
+ ...districts.flatMap(d => [d.worldBounds.minX, d.worldBounds.maxX]),
128
+ ];
129
+ const allZ = [
130
+ ...buildings.map(b => b.position.z),
131
+ ...districts.flatMap(d => [d.worldBounds.minZ, d.worldBounds.maxZ]),
132
+ ];
133
+
134
+ return {
135
+ minX: Math.min(...allX),
136
+ maxX: Math.max(...allX),
137
+ minZ: Math.min(...allZ),
138
+ maxZ: Math.max(...allZ),
139
+ };
140
+ };
141
+
142
+ /**
143
+ * Filter city data to only include a subdirectory and optionally remap coordinates
144
+ * to make the subdirectory the new origin (0,0)
145
+ */
146
+ export const filterCityDataForSubdirectory = (
147
+ cityData: CityData,
148
+ subdirectoryPath: string,
149
+ autoCenter: boolean = true,
150
+ ): CityData => {
151
+ if (!subdirectoryPath) return cityData;
152
+
153
+ // Normalize path - remove leading/trailing slashes
154
+ const normalizedPath = subdirectoryPath.replace(/^\/+|\/+$/g, '');
155
+
156
+ // Filter buildings to only those within the subdirectory
157
+ const filteredBuildings = cityData.buildings.filter(
158
+ building =>
159
+ building.path === normalizedPath ||
160
+ building.path.startsWith(normalizedPath + '/') ||
161
+ building.path.startsWith(`/${normalizedPath}`),
162
+ );
163
+
164
+ // Filter districts to only those within the subdirectory
165
+ const filteredDistricts = cityData.districts.filter(
166
+ district =>
167
+ district.path === normalizedPath ||
168
+ district.path.startsWith(normalizedPath + '/') ||
169
+ district.path.startsWith(`/${normalizedPath}`),
170
+ );
171
+
172
+ // Find the subdirectory district to get its bounds
173
+ const subdirectoryDistrict = cityData.districts.find(d => d.path === normalizedPath);
174
+
175
+ if (!subdirectoryDistrict && filteredBuildings.length === 0) {
176
+ // Subdirectory not found or empty
177
+ return {
178
+ ...cityData,
179
+ buildings: [],
180
+ districts: [],
181
+ bounds: { minX: 0, maxX: 100, minZ: 0, maxZ: 100 },
182
+ metadata: {
183
+ ...cityData.metadata,
184
+ rootPath: normalizedPath,
185
+ },
186
+ };
187
+ }
188
+
189
+ // Calculate the bounds of the subdirectory content
190
+ const contentBounds = recalculateBounds(filteredBuildings, filteredDistricts);
191
+
192
+ let remappedBuildings: CityBuilding[];
193
+ let remappedDistricts: CityDistrict[];
194
+ let newBounds: { minX: number; maxX: number; minZ: number; maxZ: number };
195
+
196
+ if (autoCenter) {
197
+ // Calculate offset to make subdirectory origin (0,0)
198
+ const offsetX = -contentBounds.minX;
199
+ const offsetZ = -contentBounds.minZ;
200
+
201
+ // Remap building coordinates relative to subdirectory origin
202
+ remappedBuildings = filteredBuildings.map(building => ({
203
+ ...building,
204
+ position: {
205
+ ...building.position,
206
+ x: building.position.x + offsetX,
207
+ z: building.position.z + offsetZ,
208
+ },
209
+ // Make path relative to subdirectory
210
+ path:
211
+ building.path === normalizedPath
212
+ ? building.path.split('/').pop() || building.path
213
+ : building.path.substring(normalizedPath.length + 1),
214
+ }));
215
+
216
+ // Remap district coordinates relative to subdirectory origin
217
+ remappedDistricts = filteredDistricts.map(district => ({
218
+ ...district,
219
+ worldBounds: {
220
+ minX: district.worldBounds.minX + offsetX,
221
+ maxX: district.worldBounds.maxX + offsetX,
222
+ minZ: district.worldBounds.minZ + offsetZ,
223
+ maxZ: district.worldBounds.maxZ + offsetZ,
224
+ },
225
+ // Make path relative to subdirectory
226
+ path:
227
+ district.path === normalizedPath
228
+ ? '' // The subdirectory itself becomes the root
229
+ : district.path.substring(normalizedPath.length + 1),
230
+ }));
231
+
232
+ // New bounds with origin at (0,0)
233
+ newBounds = {
234
+ minX: 0,
235
+ maxX: contentBounds.maxX - contentBounds.minX,
236
+ minZ: 0,
237
+ maxZ: contentBounds.maxZ - contentBounds.minZ,
238
+ };
239
+ } else {
240
+ // Preserve original coordinates when autoCenter is disabled
241
+ remappedBuildings = filteredBuildings.map(building => ({
242
+ ...building,
243
+ // Make path relative to subdirectory
244
+ path:
245
+ building.path === normalizedPath
246
+ ? building.path.split('/').pop() || building.path
247
+ : building.path.substring(normalizedPath.length + 1),
248
+ }));
249
+
250
+ remappedDistricts = filteredDistricts.map(district => ({
251
+ ...district,
252
+ // Make path relative to subdirectory
253
+ path:
254
+ district.path === normalizedPath
255
+ ? '' // The subdirectory itself becomes the root
256
+ : district.path.substring(normalizedPath.length + 1),
257
+ }));
258
+
259
+ // Preserve original full city bounds to maintain spatial context
260
+ newBounds = cityData.bounds;
261
+ }
262
+
263
+ return {
264
+ ...cityData,
265
+ buildings: remappedBuildings,
266
+ districts: remappedDistricts,
267
+ bounds: newBounds,
268
+ metadata: {
269
+ ...cityData.metadata,
270
+ rootPath: normalizedPath,
271
+ totalFiles: filteredBuildings.length,
272
+ totalDirectories: filteredDistricts.length,
273
+ },
274
+ };
275
+ };
276
+
277
+ /**
278
+ * Filter city data based on multiple directory filters with include/exclude modes
279
+ */
280
+ export const filterCityDataForMultipleDirectories = (
281
+ cityData: CityData,
282
+ filters: Array<{ path: string; mode: 'include' | 'exclude' }>,
283
+ autoCenter: boolean = true,
284
+ combineMode: 'union' | 'intersection' = 'union',
285
+ ): CityData => {
286
+ if (!filters || filters.length === 0) return cityData;
287
+
288
+ // Normalize all filter paths
289
+ const normalizedFilters = filters.map(filter => ({
290
+ ...filter,
291
+ path: filter.path.replace(/^\/+|\/+$/g, ''),
292
+ }));
293
+
294
+ const includeFilters = normalizedFilters.filter(f => f.mode === 'include');
295
+ const excludeFilters = normalizedFilters.filter(f => f.mode === 'exclude');
296
+
297
+ // Helper function to check if a path matches a filter
298
+ const matchesFilter = (itemPath: string, filterPath: string): boolean => {
299
+ const normalizedItemPath = itemPath.replace(/^\/+/, '');
300
+ return normalizedItemPath === filterPath || normalizedItemPath.startsWith(filterPath + '/');
301
+ };
302
+
303
+ // Filter buildings based on include/exclude rules
304
+ const filteredBuildings = cityData.buildings.filter(building => {
305
+ // Check excludes first (they take precedence)
306
+ for (const filter of excludeFilters) {
307
+ if (matchesFilter(building.path, filter.path)) {
308
+ return false;
309
+ }
310
+ }
311
+
312
+ // If we have includes, must match based on combine mode
313
+ if (includeFilters.length > 0) {
314
+ if (combineMode === 'union') {
315
+ // Union: match any include filter
316
+ return includeFilters.some(filter => matchesFilter(building.path, filter.path));
317
+ } else {
318
+ // Intersection: must match all include filters (rare use case)
319
+ return includeFilters.every(filter => matchesFilter(building.path, filter.path));
320
+ }
321
+ }
322
+
323
+ // No includes means include everything (except excludes)
324
+ return true;
325
+ });
326
+
327
+ // Filter districts with the same logic
328
+ const filteredDistricts = cityData.districts.filter(district => {
329
+ // Check excludes first
330
+ for (const filter of excludeFilters) {
331
+ if (matchesFilter(district.path, filter.path)) {
332
+ return false;
333
+ }
334
+ }
335
+
336
+ // Check if district contains any included content
337
+ if (includeFilters.length > 0) {
338
+ if (combineMode === 'union') {
339
+ // Include district if it matches any filter or contains matching content
340
+ return includeFilters.some(
341
+ filter =>
342
+ matchesFilter(district.path, filter.path) || matchesFilter(filter.path, district.path), // District is parent of filter
343
+ );
344
+ } else {
345
+ // Intersection mode for districts is complex - simplify to union behavior
346
+ return includeFilters.some(
347
+ filter =>
348
+ matchesFilter(district.path, filter.path) || matchesFilter(filter.path, district.path),
349
+ );
350
+ }
351
+ }
352
+
353
+ return true;
354
+ });
355
+
356
+ // If nothing matches, return empty city
357
+ if (filteredBuildings.length === 0 && filteredDistricts.length === 0) {
358
+ return {
359
+ ...cityData,
360
+ buildings: [],
361
+ districts: [],
362
+ bounds: { minX: 0, maxX: 100, minZ: 0, maxZ: 100 },
363
+ metadata: {
364
+ ...cityData.metadata,
365
+ rootPath: filters.map(f => `${f.mode}:${f.path}`).join(', '),
366
+ },
367
+ };
368
+ }
369
+
370
+ // Calculate bounds and optionally recenter
371
+ const contentBounds = recalculateBounds(filteredBuildings, filteredDistricts);
372
+
373
+ if (autoCenter) {
374
+ // Calculate offset to center the content
375
+ const offsetX = -contentBounds.minX;
376
+ const offsetZ = -contentBounds.minZ;
377
+
378
+ // Remap coordinates
379
+ const remappedBuildings = filteredBuildings.map(building => ({
380
+ ...building,
381
+ position: {
382
+ ...building.position,
383
+ x: building.position.x + offsetX,
384
+ z: building.position.z + offsetZ,
385
+ },
386
+ }));
387
+
388
+ const remappedDistricts = filteredDistricts.map(district => ({
389
+ ...district,
390
+ worldBounds: {
391
+ minX: district.worldBounds.minX + offsetX,
392
+ maxX: district.worldBounds.maxX + offsetX,
393
+ minZ: district.worldBounds.minZ + offsetZ,
394
+ maxZ: district.worldBounds.maxZ + offsetZ,
395
+ },
396
+ }));
397
+
398
+ return {
399
+ ...cityData,
400
+ buildings: remappedBuildings,
401
+ districts: remappedDistricts,
402
+ bounds: {
403
+ minX: 0,
404
+ maxX: contentBounds.maxX - contentBounds.minX,
405
+ minZ: 0,
406
+ maxZ: contentBounds.maxZ - contentBounds.minZ,
407
+ },
408
+ metadata: {
409
+ ...cityData.metadata,
410
+ rootPath: filters.map(f => `${f.mode}:${f.path}`).join(', '),
411
+ totalFiles: remappedBuildings.length,
412
+ totalDirectories: remappedDistricts.length,
413
+ },
414
+ };
415
+ } else {
416
+ // Return filtered data with original coordinates and bounds
417
+ return {
418
+ ...cityData,
419
+ buildings: filteredBuildings,
420
+ districts: filteredDistricts,
421
+ bounds: cityData.bounds, // Preserve original bounds when autoCenter is false
422
+ metadata: {
423
+ ...cityData.metadata,
424
+ rootPath: filters.map(f => `${f.mode}:${f.path}`).join(', '),
425
+ totalFiles: filteredBuildings.length,
426
+ totalDirectories: filteredDistricts.length,
427
+ },
428
+ };
429
+ }
430
+ };