@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.
- package/dist/components/FileCity3D/FileCity3D.d.ts +8 -2
- package/dist/components/FileCity3D/FileCity3D.d.ts.map +1 -1
- package/dist/components/FileCity3D/FileCity3D.js +129 -40
- package/dist/components/FileCityExplorer/AddToAreaModal.d.ts +14 -0
- package/dist/components/FileCityExplorer/AddToAreaModal.d.ts.map +1 -0
- package/dist/components/FileCityExplorer/AddToAreaModal.js +140 -0
- package/dist/components/FileCityExplorer/AddToScopeModal.d.ts +14 -0
- package/dist/components/FileCityExplorer/AddToScopeModal.d.ts.map +1 -0
- package/dist/components/FileCityExplorer/AddToScopeModal.js +176 -0
- package/dist/components/FileCityExplorer/FileCityExplorer.d.ts +30 -0
- package/dist/components/FileCityExplorer/FileCityExplorer.d.ts.map +1 -0
- package/dist/components/FileCityExplorer/FileCityExplorer.js +1045 -0
- package/dist/components/FileCityExplorer/ScopeInfoOverlay.d.ts +10 -0
- package/dist/components/FileCityExplorer/ScopeInfoOverlay.d.ts.map +1 -0
- package/dist/components/FileCityExplorer/ScopeInfoOverlay.js +73 -0
- package/dist/components/FileCityExplorer/index.d.ts +3 -0
- package/dist/components/FileCityExplorer/index.d.ts.map +1 -0
- package/dist/components/FileCityExplorer/index.js +1 -0
- package/dist/components/FileCityExplorer/layers.d.ts +16 -0
- package/dist/components/FileCityExplorer/layers.d.ts.map +1 -0
- package/dist/components/FileCityExplorer/layers.js +61 -0
- package/dist/components/FileCityExplorer/model.d.ts +32 -0
- package/dist/components/FileCityExplorer/model.d.ts.map +1 -0
- package/dist/components/FileCityExplorer/model.js +14 -0
- package/dist/components/FileCityExplorer/pathConversion.d.ts +19 -0
- package/dist/components/FileCityExplorer/pathConversion.d.ts.map +1 -0
- package/dist/components/FileCityExplorer/pathConversion.js +26 -0
- package/dist/components/FileCityExplorer/scopeTreePaths.d.ts +21 -0
- package/dist/components/FileCityExplorer/scopeTreePaths.d.ts.map +1 -0
- package/dist/components/FileCityExplorer/scopeTreePaths.js +42 -0
- package/dist/components/FileCityExplorer/styles.d.ts +9 -0
- package/dist/components/FileCityExplorer/styles.d.ts.map +1 -0
- package/dist/components/FileCityExplorer/styles.js +28 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/utils/folderElevatedPanels.d.ts +62 -0
- package/dist/utils/folderElevatedPanels.d.ts.map +1 -0
- package/dist/utils/folderElevatedPanels.js +130 -0
- package/package.json +2 -1
- package/src/components/FileCity3D/FileCity3D.tsx +200 -52
- package/src/components/FileCityExplorer/AddToAreaModal.tsx +273 -0
- package/src/components/FileCityExplorer/AddToScopeModal.tsx +320 -0
- package/src/components/FileCityExplorer/FileCityExplorer.tsx +1457 -0
- package/src/components/FileCityExplorer/ScopeInfoOverlay.tsx +229 -0
- package/src/components/FileCityExplorer/index.ts +2 -0
- package/src/components/FileCityExplorer/layers.ts +72 -0
- package/src/components/FileCityExplorer/model.ts +35 -0
- package/src/components/FileCityExplorer/pathConversion.ts +32 -0
- package/src/components/FileCityExplorer/scopeTreePaths.ts +52 -0
- package/src/components/FileCityExplorer/styles.ts +34 -0
- package/src/index.ts +8 -0
- package/src/stories/2D3DComparison.stories.tsx +13 -2
- package/src/stories/ElevatedScopePanels.stories.tsx +295 -0
- package/src/stories/FileCity3D.stories.tsx +24 -3
- package/src/stories/FileCityExplorer.stories.tsx +2474 -0
- package/src/stories/FileCityExplorerComponent.stories.tsx +59 -0
- package/src/utils/folderElevatedPanels.ts +176 -0
- 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
|
+
}
|