@principal-ai/file-city-react 0.5.39 → 0.5.41

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 (59) hide show
  1. package/dist/components/FileCity3D/FileCity3D.d.ts +8 -2
  2. package/dist/components/FileCity3D/FileCity3D.d.ts.map +1 -1
  3. package/dist/components/FileCity3D/FileCity3D.js +129 -40
  4. package/dist/components/FileCityExplorer/AddToAreaModal.d.ts +14 -0
  5. package/dist/components/FileCityExplorer/AddToAreaModal.d.ts.map +1 -0
  6. package/dist/components/FileCityExplorer/AddToAreaModal.js +140 -0
  7. package/dist/components/FileCityExplorer/AddToScopeModal.d.ts +14 -0
  8. package/dist/components/FileCityExplorer/AddToScopeModal.d.ts.map +1 -0
  9. package/dist/components/FileCityExplorer/AddToScopeModal.js +176 -0
  10. package/dist/components/FileCityExplorer/FileCityExplorer.d.ts +30 -0
  11. package/dist/components/FileCityExplorer/FileCityExplorer.d.ts.map +1 -0
  12. package/dist/components/FileCityExplorer/FileCityExplorer.js +1045 -0
  13. package/dist/components/FileCityExplorer/ScopeInfoOverlay.d.ts +10 -0
  14. package/dist/components/FileCityExplorer/ScopeInfoOverlay.d.ts.map +1 -0
  15. package/dist/components/FileCityExplorer/ScopeInfoOverlay.js +73 -0
  16. package/dist/components/FileCityExplorer/index.d.ts +3 -0
  17. package/dist/components/FileCityExplorer/index.d.ts.map +1 -0
  18. package/dist/components/FileCityExplorer/index.js +1 -0
  19. package/dist/components/FileCityExplorer/layers.d.ts +16 -0
  20. package/dist/components/FileCityExplorer/layers.d.ts.map +1 -0
  21. package/dist/components/FileCityExplorer/layers.js +61 -0
  22. package/dist/components/FileCityExplorer/model.d.ts +32 -0
  23. package/dist/components/FileCityExplorer/model.d.ts.map +1 -0
  24. package/dist/components/FileCityExplorer/model.js +14 -0
  25. package/dist/components/FileCityExplorer/pathConversion.d.ts +19 -0
  26. package/dist/components/FileCityExplorer/pathConversion.d.ts.map +1 -0
  27. package/dist/components/FileCityExplorer/pathConversion.js +26 -0
  28. package/dist/components/FileCityExplorer/scopeTreePaths.d.ts +21 -0
  29. package/dist/components/FileCityExplorer/scopeTreePaths.d.ts.map +1 -0
  30. package/dist/components/FileCityExplorer/scopeTreePaths.js +42 -0
  31. package/dist/components/FileCityExplorer/styles.d.ts +9 -0
  32. package/dist/components/FileCityExplorer/styles.d.ts.map +1 -0
  33. package/dist/components/FileCityExplorer/styles.js +28 -0
  34. package/dist/index.d.ts +2 -0
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +2 -0
  37. package/dist/utils/folderElevatedPanels.d.ts +62 -0
  38. package/dist/utils/folderElevatedPanels.d.ts.map +1 -0
  39. package/dist/utils/folderElevatedPanels.js +130 -0
  40. package/package.json +2 -1
  41. package/src/components/FileCity3D/FileCity3D.tsx +200 -52
  42. package/src/components/FileCityExplorer/AddToAreaModal.tsx +273 -0
  43. package/src/components/FileCityExplorer/AddToScopeModal.tsx +320 -0
  44. package/src/components/FileCityExplorer/FileCityExplorer.tsx +1457 -0
  45. package/src/components/FileCityExplorer/ScopeInfoOverlay.tsx +229 -0
  46. package/src/components/FileCityExplorer/index.ts +2 -0
  47. package/src/components/FileCityExplorer/layers.ts +72 -0
  48. package/src/components/FileCityExplorer/model.ts +35 -0
  49. package/src/components/FileCityExplorer/pathConversion.ts +32 -0
  50. package/src/components/FileCityExplorer/scopeTreePaths.ts +52 -0
  51. package/src/components/FileCityExplorer/styles.ts +34 -0
  52. package/src/index.ts +8 -0
  53. package/src/stories/2D3DComparison.stories.tsx +13 -2
  54. package/src/stories/ElevatedScopePanels.stories.tsx +295 -0
  55. package/src/stories/FileCity3D.stories.tsx +24 -3
  56. package/src/stories/FileCityExplorer.stories.tsx +2474 -0
  57. package/src/stories/FileCityExplorerComponent.stories.tsx +59 -0
  58. package/src/utils/folderElevatedPanels.ts +176 -0
  59. package/src/stories/ScopeOverlay.stories.tsx +0 -1687
@@ -0,0 +1,59 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+
3
+ import { FileCityExplorer, type ProjectArea } from '../components/FileCityExplorer';
4
+ import type { CityData } from '../components/FileCity3D';
5
+
6
+ import electronAppCityData from '../../../../assets/electron-app-city-data.json';
7
+
8
+ /**
9
+ * Side-by-side test of the extracted `<FileCityExplorer>` component against
10
+ * the original story-template implementation in
11
+ * `FileCityExplorer.stories.tsx`. The two should look and behave identically;
12
+ * differences indicate regressions in the extraction.
13
+ *
14
+ * Persistence is intentionally namespaced to a separate `localStorage` key
15
+ * (`file-city.scope-overlay-component`) so the two stories don't fight over
16
+ * the same scopes/areas state.
17
+ */
18
+
19
+ const meta: Meta<typeof FileCityExplorer> = {
20
+ title: 'Experiments/FileCityExplorer (Component)',
21
+ component: FileCityExplorer,
22
+ parameters: { layout: 'fullscreen' },
23
+ };
24
+
25
+ export default meta;
26
+ type Story = StoryObj<typeof FileCityExplorer>;
27
+
28
+ const DEFAULT_AREAS: ProjectArea[] = [
29
+ {
30
+ name: 'Documentation',
31
+ description: 'Project docs, READMEs, and design notes — not OTEL-instrumented.',
32
+ paths: ['docs'],
33
+ },
34
+ {
35
+ name: 'Build & tooling',
36
+ description: 'Build scripts, bundler config, and developer tooling.',
37
+ paths: ['scripts', 'build'],
38
+ },
39
+ ];
40
+
41
+ export const Default: Story = {
42
+ render: () => (
43
+ <FileCityExplorer
44
+ cityData={electronAppCityData as CityData}
45
+ packageRoot="electron-app/"
46
+ initialAreas={DEFAULT_AREAS}
47
+ persistKey="file-city.scope-overlay-component"
48
+ />
49
+ ),
50
+ parameters: {
51
+ docs: {
52
+ description: {
53
+ story:
54
+ 'Extracted `<FileCityExplorer>` component over the electron-app city. ' +
55
+ 'Should behave identically to the original story (FileCityExplorer / Default).',
56
+ },
57
+ },
58
+ },
59
+ };
@@ -0,0 +1,176 @@
1
+ import type { CityData } from '@principal-ai/file-city-builder';
2
+ import type { ElevatedScopePanel } from '../components/FileCity3D';
3
+
4
+ const FOLDER_PALETTE = [
5
+ '#3b82f6',
6
+ '#22c55e',
7
+ '#f59e0b',
8
+ '#ec4899',
9
+ '#8b5cf6',
10
+ '#06b6d4',
11
+ '#ef4444',
12
+ '#14b8a6',
13
+ '#a855f7',
14
+ '#eab308',
15
+ ];
16
+
17
+ /**
18
+ * Stable color for a folder path, picked from a small palette via a string
19
+ * hash. Two folders at the same depth get visibly different colors; the
20
+ * same folder always gets the same color across renders.
21
+ */
22
+ export function hashFolderColor(path: string): string {
23
+ let h = 0;
24
+ for (let i = 0; i < path.length; i++) {
25
+ h = (h * 31 + path.charCodeAt(i)) | 0;
26
+ }
27
+ return FOLDER_PALETTE[Math.abs(h) % FOLDER_PALETTE.length];
28
+ }
29
+
30
+ interface Bounds {
31
+ minX: number;
32
+ maxX: number;
33
+ minZ: number;
34
+ maxZ: number;
35
+ }
36
+
37
+ interface FolderIndex {
38
+ /** Parent directory → list of immediate child directories. Top-level folders live under '' (empty string). */
39
+ children: Map<string, string[]>;
40
+ /** Folder path → world bounds spanning every descendant district. */
41
+ bounds: Map<string, Bounds>;
42
+ /** Folder path → number of descendant files. */
43
+ fileCounts: Map<string, number>;
44
+ }
45
+
46
+ /**
47
+ * Precompute the data structures `buildFolderElevatedPanels` needs from a
48
+ * `CityData`. Cache this when the city data is stable to avoid redoing the
49
+ * O(districts × depth) walk on every render.
50
+ */
51
+ export function buildFolderIndex(cityData: CityData): FolderIndex {
52
+ const children = new Map<string, string[]>();
53
+ const directorySet = new Set<string>();
54
+ for (const d of cityData.districts) directorySet.add(d.path);
55
+ const dirs = Array.from(directorySet).sort();
56
+ for (const dir of dirs) {
57
+ const slash = dir.lastIndexOf('/');
58
+ const parent = slash >= 0 ? dir.slice(0, slash) : '';
59
+ const arr = children.get(parent);
60
+ if (arr) arr.push(dir);
61
+ else children.set(parent, [dir]);
62
+ }
63
+
64
+ const bounds = new Map<string, Bounds>();
65
+ for (const district of cityData.districts) {
66
+ const b = district.worldBounds;
67
+ let path = district.path;
68
+ while (path) {
69
+ const cur = bounds.get(path);
70
+ if (!cur) {
71
+ bounds.set(path, { minX: b.minX, maxX: b.maxX, minZ: b.minZ, maxZ: b.maxZ });
72
+ } else {
73
+ if (b.minX < cur.minX) cur.minX = b.minX;
74
+ if (b.maxX > cur.maxX) cur.maxX = b.maxX;
75
+ if (b.minZ < cur.minZ) cur.minZ = b.minZ;
76
+ if (b.maxZ > cur.maxZ) cur.maxZ = b.maxZ;
77
+ }
78
+ const slash = path.lastIndexOf('/');
79
+ if (slash < 0) break;
80
+ path = path.slice(0, slash);
81
+ }
82
+ }
83
+
84
+ const fileCounts = new Map<string, number>();
85
+ for (const b of cityData.buildings) {
86
+ let path = b.path;
87
+ const slash = path.lastIndexOf('/');
88
+ path = slash >= 0 ? path.slice(0, slash) : '';
89
+ while (path) {
90
+ fileCounts.set(path, (fileCounts.get(path) ?? 0) + 1);
91
+ const s = path.lastIndexOf('/');
92
+ if (s < 0) break;
93
+ path = path.slice(0, s);
94
+ }
95
+ }
96
+
97
+ return { children, bounds, fileCounts };
98
+ }
99
+
100
+ export interface BuildFolderElevatedPanelsOptions {
101
+ cityData: CityData;
102
+ /**
103
+ * Set of folder paths that are currently expanded in the file tree. A folder
104
+ * not in this set is treated as collapsed.
105
+ */
106
+ expandedFolders: ReadonlySet<string>;
107
+ /** Toggle handler invoked when an umbrella tile is clicked. */
108
+ onToggleFolder?: (folderPath: string, event: MouseEvent) => void;
109
+ /** Double-click handler for an umbrella tile. */
110
+ onDoubleClickFolder?: (folderPath: string, event: MouseEvent) => void;
111
+ /**
112
+ * Scale label font size by descendant file count. Default true. When false,
113
+ * the renderer's auto-sized label is used (size derived from tile footprint).
114
+ */
115
+ scaleLabelByFileCount?: boolean;
116
+ /**
117
+ * Pre-built index from `buildFolderIndex(cityData)`. Pass when you cache the
118
+ * city's index across renders to avoid recomputing it.
119
+ */
120
+ index?: FolderIndex;
121
+ }
122
+
123
+ /**
124
+ * Walks the folder hierarchy from the top-level folders of a `CityData`. For
125
+ * each folder:
126
+ * - if expanded → recurse into child folders
127
+ * - if collapsed → emit one elevated panel covering the union of every
128
+ * descendant district's world bounds, colored via `hashFolderColor`.
129
+ *
130
+ * Mirror of the scope-tree expansion behavior, applied to file-tree folders.
131
+ */
132
+ export function buildFolderElevatedPanels(
133
+ options: BuildFolderElevatedPanelsOptions,
134
+ ): ElevatedScopePanel[] {
135
+ const {
136
+ cityData,
137
+ expandedFolders,
138
+ onToggleFolder,
139
+ onDoubleClickFolder,
140
+ scaleLabelByFileCount = true,
141
+ } = options;
142
+ const index = options.index ?? buildFolderIndex(cityData);
143
+
144
+ const panels: ElevatedScopePanel[] = [];
145
+
146
+ const walk = (folderPath: string): void => {
147
+ if (expandedFolders.has(folderPath)) {
148
+ const kids = index.children.get(folderPath) ?? [];
149
+ for (const child of kids) walk(child);
150
+ return;
151
+ }
152
+ const bounds = index.bounds.get(folderPath);
153
+ if (!bounds) return;
154
+ const label = folderPath.split('/').pop() ?? folderPath;
155
+ const fileCount = index.fileCounts.get(folderPath) ?? 0;
156
+ const labelSize = scaleLabelByFileCount
157
+ ? Math.max(12, Math.min(240, 8 + Math.sqrt(fileCount) * 5))
158
+ : undefined;
159
+ panels.push({
160
+ id: `folder::${folderPath}`,
161
+ color: hashFolderColor(folderPath),
162
+ height: 4,
163
+ thickness: 2,
164
+ bounds,
165
+ label,
166
+ labelSize,
167
+ onClick: onToggleFolder ? (event: MouseEvent) => onToggleFolder(folderPath, event) : undefined,
168
+ onDoubleClick: onDoubleClickFolder
169
+ ? (event: MouseEvent) => onDoubleClickFolder(folderPath, event)
170
+ : undefined,
171
+ });
172
+ };
173
+
174
+ for (const top of index.children.get('') ?? []) walk(top);
175
+ return panels;
176
+ }