@principal-ai/file-city-react 0.5.38 → 0.5.40

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.
@@ -63,6 +63,11 @@ export interface ElevatedScopePanel {
63
63
  label?: string;
64
64
  /** Hex color for the label (default white). */
65
65
  labelColor?: string;
66
+ /**
67
+ * Absolute label font size in world units. When omitted, falls back to a
68
+ * size derived from the panel's footprint. Always clamped to fit the tile.
69
+ */
70
+ labelSize?: number;
66
71
  /** Click handler. When set, the slab becomes interactive and shows a pointer cursor. */
67
72
  onClick?: () => void;
68
73
  }
@@ -1 +1 @@
1
- {"version":3,"file":"FileCity3D.d.ts","sourceRoot":"","sources":["../../../src/components/FileCity3D/FileCity3D.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"FileCity3D.d.ts","sourceRoot":"","sources":["../../../src/components/FileCity3D/FileCity3D.tsx"],"names":[],"mappings":"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"}
@@ -1692,8 +1692,11 @@ function CityScene({ cityData, onBuildingHover, onBuildingClick, hoveredBuilding
1692
1692
  const topY = y + t / 2;
1693
1693
  // Size text to the panel: roughly fit longest reasonable label,
1694
1694
  // clamped so tiny tiles still render legibly and huge ones don't
1695
- // get absurd.
1696
- const labelSize = Math.max(4, Math.min(24, Math.min(w, d) / 6));
1695
+ // get absurd. Callers may override via panel.labelSize, but we
1696
+ // still cap to the tile footprint so the label fits.
1697
+ const tileMax = Math.min(w, d) / 2;
1698
+ const requested = panel.labelSize ?? Math.min(w, d) / 6;
1699
+ const labelSize = Math.max(4, Math.min(tileMax, requested));
1697
1700
  const handleClick = panel.onClick
1698
1701
  ? (e) => {
1699
1702
  e.stopPropagation();
@@ -1711,7 +1714,7 @@ function CityScene({ cityData, onBuildingHover, onBuildingClick, hoveredBuilding
1711
1714
  document.body.style.cursor = '';
1712
1715
  }
1713
1716
  : undefined;
1714
- return (_jsxs("group", { children: [_jsxs("mesh", { position: [cx, y, cz], renderOrder: 10, onClick: handleClick, onPointerOver: handlePointerOver, onPointerOut: handlePointerOut, children: [_jsx("boxGeometry", { args: [w, t, d] }), _jsx("meshBasicMaterial", { color: panel.color, transparent: !isOpaque, opacity: opacity, depthWrite: isOpaque })] }), panel.label && (_jsxs(Text, { position: [cx, topY + 0.05, cz], rotation: [-Math.PI / 2, 0, 0], fontSize: labelSize, color: panel.labelColor ?? '#ffffff', anchorX: "center", anchorY: "middle", maxWidth: w * 0.9, textAlign: "center", renderOrder: 11, children: [panel.label, _jsx("meshBasicMaterial", { attach: "material", color: panel.labelColor ?? '#ffffff', depthWrite: false })] }))] }, panel.id));
1717
+ return (_jsxs("group", { children: [_jsxs("mesh", { position: [cx, y, cz], renderOrder: 10, onClick: handleClick, onPointerOver: handlePointerOver, onPointerOut: handlePointerOut, children: [_jsx("boxGeometry", { args: [w, t, d] }), _jsx("meshBasicMaterial", { color: panel.color, transparent: !isOpaque, opacity: opacity, depthWrite: isOpaque })] }), panel.label && (_jsxs(Text, { position: [cx, topY + 0.05, cz], rotation: [-Math.PI / 2, 0, 0], fontSize: labelSize, color: panel.labelColor ?? '#ffffff', anchorX: "center", anchorY: "middle", maxWidth: w * 0.9, textAlign: "center", renderOrder: 11, frustumCulled: false, children: [panel.label, _jsx("meshBasicMaterial", { attach: "material", color: panel.labelColor ?? '#ffffff', depthWrite: false, depthTest: false })] }))] }, panel.id));
1715
1718
  })] }));
1716
1719
  }
1717
1720
  /**
package/dist/index.d.ts CHANGED
@@ -16,6 +16,8 @@ export { ThemeProvider, useTheme } from '@principal-ade/industry-theme';
16
16
  export { FileCity3D, resetCamera, DEFAULT_FLAT_PATTERNS } from './components/FileCity3D';
17
17
  export type { FileCity3DProps, AnimationConfig, HighlightLayer as FileCity3DHighlightLayer, IsolationMode, HeightScaling, FlatPattern, ElevatedScopePanel, } from './components/FileCity3D';
18
18
  export type { HighlightLayer as FileCity3DHL } from './components/FileCity3D';
19
+ export { buildFolderElevatedPanels, buildFolderIndex, hashFolderColor, } from './utils/folderElevatedPanels';
20
+ export type { BuildFolderElevatedPanelsOptions } from './utils/folderElevatedPanels';
19
21
  export { resolveVisualizationIntent } from './utils/visualizationResolution';
20
22
  export type { VisualizationIntent, ResolvedVisualizationState, } from './utils/visualizationResolution';
21
23
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,8BAA8B,EAC9B,KAAK,mCAAmC,GACzC,MAAM,6CAA6C,CAAC;AAGrD,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,SAAS,EACd,KAAK,cAAc,EACnB,UAAU,GACX,MAAM,sCAAsC,CAAC;AAG9C,OAAO,EAAE,KAAK,mBAAmB,EAAE,KAAK,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAGvF,OAAO,EACL,gCAAgC,EAChC,6BAA6B,EAC7B,oCAAoC,GACrC,MAAM,yBAAyB,CAAC;AAGjC,OAAO,EACL,8BAA8B,EAC9B,yBAAyB,EACzB,mBAAmB,GACpB,MAAM,kCAAkC,CAAC;AAE1C,YAAY,EACV,gBAAgB,EAChB,gBAAgB,EAChB,qBAAqB,EACrB,kBAAkB,GACnB,MAAM,kCAAkC,CAAC;AAG1C,OAAO,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAGzF,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAG7F,YAAY,EACV,QAAQ,EACR,YAAY,EACZ,YAAY,EACZ,sBAAsB,EACtB,QAAQ,EACR,UAAU,GACX,MAAM,iCAAiC,CAAC;AAGzC,OAAO,EAAE,uBAAuB,EAAE,MAAM,iCAAiC,CAAC;AAG1E,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,YAAY,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAG7F,YAAY,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AAGhE,OAAO,EACL,qBAAqB,EACrB,KAAK,0BAA0B,GAChC,MAAM,oCAAoC,CAAC;AAG5C,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,+BAA+B,CAAC;AAGxE,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AACzF,YAAY,EACV,eAAe,EACf,eAAe,EACf,cAAc,IAAI,wBAAwB,EAC1C,aAAa,EACb,aAAa,EACb,WAAW,EACX,kBAAkB,GACnB,MAAM,yBAAyB,CAAC;AAIjC,YAAY,EAAE,cAAc,IAAI,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAI9E,OAAO,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAC;AAC7E,YAAY,EACV,mBAAmB,EACnB,0BAA0B,GAC3B,MAAM,iCAAiC,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,8BAA8B,EAC9B,KAAK,mCAAmC,GACzC,MAAM,6CAA6C,CAAC;AAGrD,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,SAAS,EACd,KAAK,cAAc,EACnB,UAAU,GACX,MAAM,sCAAsC,CAAC;AAG9C,OAAO,EAAE,KAAK,mBAAmB,EAAE,KAAK,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAGvF,OAAO,EACL,gCAAgC,EAChC,6BAA6B,EAC7B,oCAAoC,GACrC,MAAM,yBAAyB,CAAC;AAGjC,OAAO,EACL,8BAA8B,EAC9B,yBAAyB,EACzB,mBAAmB,GACpB,MAAM,kCAAkC,CAAC;AAE1C,YAAY,EACV,gBAAgB,EAChB,gBAAgB,EAChB,qBAAqB,EACrB,kBAAkB,GACnB,MAAM,kCAAkC,CAAC;AAG1C,OAAO,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAGzF,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAG7F,YAAY,EACV,QAAQ,EACR,YAAY,EACZ,YAAY,EACZ,sBAAsB,EACtB,QAAQ,EACR,UAAU,GACX,MAAM,iCAAiC,CAAC;AAGzC,OAAO,EAAE,uBAAuB,EAAE,MAAM,iCAAiC,CAAC;AAG1E,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,YAAY,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAG7F,YAAY,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AAGhE,OAAO,EACL,qBAAqB,EACrB,KAAK,0BAA0B,GAChC,MAAM,oCAAoC,CAAC;AAG5C,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,+BAA+B,CAAC;AAGxE,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AACzF,YAAY,EACV,eAAe,EACf,eAAe,EACf,cAAc,IAAI,wBAAwB,EAC1C,aAAa,EACb,aAAa,EACb,WAAW,EACX,kBAAkB,GACnB,MAAM,yBAAyB,CAAC;AAIjC,YAAY,EAAE,cAAc,IAAI,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAG9E,OAAO,EACL,yBAAyB,EACzB,gBAAgB,EAChB,eAAe,GAChB,MAAM,8BAA8B,CAAC;AACtC,YAAY,EAAE,gCAAgC,EAAE,MAAM,8BAA8B,CAAC;AAIrF,OAAO,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAC;AAC7E,YAAY,EACV,mBAAmB,EACnB,0BAA0B,GAC3B,MAAM,iCAAiC,CAAC"}
package/dist/index.js CHANGED
@@ -20,6 +20,8 @@ export { CityViewWithReactFlow, } from './components/CityViewWithReactFlow';
20
20
  export { ThemeProvider, useTheme } from '@principal-ade/industry-theme';
21
21
  // 3D visualization component
22
22
  export { FileCity3D, resetCamera, DEFAULT_FLAT_PATTERNS } from './components/FileCity3D';
23
+ // Folder-driven elevated panels (file-tree expansion → 3D umbrella tiles)
24
+ export { buildFolderElevatedPanels, buildFolderIndex, hashFolderColor, } from './utils/folderElevatedPanels';
23
25
  // Visualization resolution utilities
24
26
  // See docs/VISUALIZATION_STATE_RESOLUTION.md for documentation
25
27
  export { resolveVisualizationIntent } from './utils/visualizationResolution';
@@ -0,0 +1,60 @@
1
+ import type { CityData } from '@principal-ai/file-city-builder';
2
+ import type { ElevatedScopePanel } from '../components/FileCity3D';
3
+ /**
4
+ * Stable color for a folder path, picked from a small palette via a string
5
+ * hash. Two folders at the same depth get visibly different colors; the
6
+ * same folder always gets the same color across renders.
7
+ */
8
+ export declare function hashFolderColor(path: string): string;
9
+ interface Bounds {
10
+ minX: number;
11
+ maxX: number;
12
+ minZ: number;
13
+ maxZ: number;
14
+ }
15
+ interface FolderIndex {
16
+ /** Parent directory → list of immediate child directories. Top-level folders live under '' (empty string). */
17
+ children: Map<string, string[]>;
18
+ /** Folder path → world bounds spanning every descendant district. */
19
+ bounds: Map<string, Bounds>;
20
+ /** Folder path → number of descendant files. */
21
+ fileCounts: Map<string, number>;
22
+ }
23
+ /**
24
+ * Precompute the data structures `buildFolderElevatedPanels` needs from a
25
+ * `CityData`. Cache this when the city data is stable to avoid redoing the
26
+ * O(districts × depth) walk on every render.
27
+ */
28
+ export declare function buildFolderIndex(cityData: CityData): FolderIndex;
29
+ export interface BuildFolderElevatedPanelsOptions {
30
+ cityData: CityData;
31
+ /**
32
+ * Set of folder paths that are currently expanded in the file tree. A folder
33
+ * not in this set is treated as collapsed.
34
+ */
35
+ expandedFolders: ReadonlySet<string>;
36
+ /** Toggle handler invoked when an umbrella tile is clicked. */
37
+ onToggleFolder?: (folderPath: string) => void;
38
+ /**
39
+ * Scale label font size by descendant file count. Default true. When false,
40
+ * the renderer's auto-sized label is used (size derived from tile footprint).
41
+ */
42
+ scaleLabelByFileCount?: boolean;
43
+ /**
44
+ * Pre-built index from `buildFolderIndex(cityData)`. Pass when you cache the
45
+ * city's index across renders to avoid recomputing it.
46
+ */
47
+ index?: FolderIndex;
48
+ }
49
+ /**
50
+ * Walks the folder hierarchy from the top-level folders of a `CityData`. For
51
+ * each folder:
52
+ * - if expanded → recurse into child folders
53
+ * - if collapsed → emit one elevated panel covering the union of every
54
+ * descendant district's world bounds, colored via `hashFolderColor`.
55
+ *
56
+ * Mirror of the scope-tree expansion behavior, applied to file-tree folders.
57
+ */
58
+ export declare function buildFolderElevatedPanels(options: BuildFolderElevatedPanelsOptions): ElevatedScopePanel[];
59
+ export {};
60
+ //# sourceMappingURL=folderElevatedPanels.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"folderElevatedPanels.d.ts","sourceRoot":"","sources":["../../src/utils/folderElevatedPanels.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AAChE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAenE;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAMpD;AAED,UAAU,MAAM;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,UAAU,WAAW;IACnB,8GAA8G;IAC9G,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IAChC,qEAAqE;IACrE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B,gDAAgD;IAChD,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,GAAG,WAAW,CA+ChE;AAED,MAAM,WAAW,gCAAgC;IAC/C,QAAQ,EAAE,QAAQ,CAAC;IACnB;;;OAGG;IACH,eAAe,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACrC,+DAA+D;IAC/D,cAAc,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9C;;;OAGG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC;;;OAGG;IACH,KAAK,CAAC,EAAE,WAAW,CAAC;CACrB;AAED;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,gCAAgC,GACxC,kBAAkB,EAAE,CAsCtB"}
@@ -0,0 +1,127 @@
1
+ const FOLDER_PALETTE = [
2
+ '#3b82f6',
3
+ '#22c55e',
4
+ '#f59e0b',
5
+ '#ec4899',
6
+ '#8b5cf6',
7
+ '#06b6d4',
8
+ '#ef4444',
9
+ '#14b8a6',
10
+ '#a855f7',
11
+ '#eab308',
12
+ ];
13
+ /**
14
+ * Stable color for a folder path, picked from a small palette via a string
15
+ * hash. Two folders at the same depth get visibly different colors; the
16
+ * same folder always gets the same color across renders.
17
+ */
18
+ export function hashFolderColor(path) {
19
+ let h = 0;
20
+ for (let i = 0; i < path.length; i++) {
21
+ h = (h * 31 + path.charCodeAt(i)) | 0;
22
+ }
23
+ return FOLDER_PALETTE[Math.abs(h) % FOLDER_PALETTE.length];
24
+ }
25
+ /**
26
+ * Precompute the data structures `buildFolderElevatedPanels` needs from a
27
+ * `CityData`. Cache this when the city data is stable to avoid redoing the
28
+ * O(districts × depth) walk on every render.
29
+ */
30
+ export function buildFolderIndex(cityData) {
31
+ const children = new Map();
32
+ const directorySet = new Set();
33
+ for (const d of cityData.districts)
34
+ directorySet.add(d.path);
35
+ const dirs = Array.from(directorySet).sort();
36
+ for (const dir of dirs) {
37
+ const slash = dir.lastIndexOf('/');
38
+ const parent = slash >= 0 ? dir.slice(0, slash) : '';
39
+ const arr = children.get(parent);
40
+ if (arr)
41
+ arr.push(dir);
42
+ else
43
+ children.set(parent, [dir]);
44
+ }
45
+ const bounds = new Map();
46
+ for (const district of cityData.districts) {
47
+ const b = district.worldBounds;
48
+ let path = district.path;
49
+ while (path) {
50
+ const cur = bounds.get(path);
51
+ if (!cur) {
52
+ bounds.set(path, { minX: b.minX, maxX: b.maxX, minZ: b.minZ, maxZ: b.maxZ });
53
+ }
54
+ else {
55
+ if (b.minX < cur.minX)
56
+ cur.minX = b.minX;
57
+ if (b.maxX > cur.maxX)
58
+ cur.maxX = b.maxX;
59
+ if (b.minZ < cur.minZ)
60
+ cur.minZ = b.minZ;
61
+ if (b.maxZ > cur.maxZ)
62
+ cur.maxZ = b.maxZ;
63
+ }
64
+ const slash = path.lastIndexOf('/');
65
+ if (slash < 0)
66
+ break;
67
+ path = path.slice(0, slash);
68
+ }
69
+ }
70
+ const fileCounts = new Map();
71
+ for (const b of cityData.buildings) {
72
+ let path = b.path;
73
+ const slash = path.lastIndexOf('/');
74
+ path = slash >= 0 ? path.slice(0, slash) : '';
75
+ while (path) {
76
+ fileCounts.set(path, (fileCounts.get(path) ?? 0) + 1);
77
+ const s = path.lastIndexOf('/');
78
+ if (s < 0)
79
+ break;
80
+ path = path.slice(0, s);
81
+ }
82
+ }
83
+ return { children, bounds, fileCounts };
84
+ }
85
+ /**
86
+ * Walks the folder hierarchy from the top-level folders of a `CityData`. For
87
+ * each folder:
88
+ * - if expanded → recurse into child folders
89
+ * - if collapsed → emit one elevated panel covering the union of every
90
+ * descendant district's world bounds, colored via `hashFolderColor`.
91
+ *
92
+ * Mirror of the scope-tree expansion behavior, applied to file-tree folders.
93
+ */
94
+ export function buildFolderElevatedPanels(options) {
95
+ const { cityData, expandedFolders, onToggleFolder, scaleLabelByFileCount = true, } = options;
96
+ const index = options.index ?? buildFolderIndex(cityData);
97
+ const panels = [];
98
+ const walk = (folderPath) => {
99
+ if (expandedFolders.has(folderPath)) {
100
+ const kids = index.children.get(folderPath) ?? [];
101
+ for (const child of kids)
102
+ walk(child);
103
+ return;
104
+ }
105
+ const bounds = index.bounds.get(folderPath);
106
+ if (!bounds)
107
+ return;
108
+ const label = folderPath.split('/').pop() ?? folderPath;
109
+ const fileCount = index.fileCounts.get(folderPath) ?? 0;
110
+ const labelSize = scaleLabelByFileCount
111
+ ? Math.max(12, Math.min(240, 8 + Math.sqrt(fileCount) * 5))
112
+ : undefined;
113
+ panels.push({
114
+ id: `folder::${folderPath}`,
115
+ color: hashFolderColor(folderPath),
116
+ height: 4,
117
+ thickness: 2,
118
+ bounds,
119
+ label,
120
+ labelSize,
121
+ onClick: onToggleFolder ? () => onToggleFolder(folderPath) : undefined,
122
+ });
123
+ };
124
+ for (const top of index.children.get('') ?? [])
125
+ walk(top);
126
+ return panels;
127
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@principal-ai/file-city-react",
3
- "version": "0.5.38",
3
+ "version": "0.5.40",
4
4
  "type": "module",
5
5
  "description": "React components for File City visualization",
6
6
  "main": "dist/index.js",
@@ -88,6 +88,11 @@ export interface ElevatedScopePanel {
88
88
  label?: string;
89
89
  /** Hex color for the label (default white). */
90
90
  labelColor?: string;
91
+ /**
92
+ * Absolute label font size in world units. When omitted, falls back to a
93
+ * size derived from the panel's footprint. Always clamped to fit the tile.
94
+ */
95
+ labelSize?: number;
91
96
  /** Click handler. When set, the slab becomes interactive and shows a pointer cursor. */
92
97
  onClick?: () => void;
93
98
  }
@@ -2666,8 +2671,11 @@ function CityScene({
2666
2671
  const topY = y + t / 2;
2667
2672
  // Size text to the panel: roughly fit longest reasonable label,
2668
2673
  // clamped so tiny tiles still render legibly and huge ones don't
2669
- // get absurd.
2670
- const labelSize = Math.max(4, Math.min(24, Math.min(w, d) / 6));
2674
+ // get absurd. Callers may override via panel.labelSize, but we
2675
+ // still cap to the tile footprint so the label fits.
2676
+ const tileMax = Math.min(w, d) / 2;
2677
+ const requested = panel.labelSize ?? Math.min(w, d) / 6;
2678
+ const labelSize = Math.max(4, Math.min(tileMax, requested));
2671
2679
 
2672
2680
  const handleClick = panel.onClick
2673
2681
  ? (e: ThreeEvent<MouseEvent>) => {
@@ -2715,12 +2723,14 @@ function CityScene({
2715
2723
  maxWidth={w * 0.9}
2716
2724
  textAlign="center"
2717
2725
  renderOrder={11}
2726
+ frustumCulled={false}
2718
2727
  >
2719
2728
  {panel.label}
2720
2729
  <meshBasicMaterial
2721
2730
  attach="material"
2722
2731
  color={panel.labelColor ?? '#ffffff'}
2723
2732
  depthWrite={false}
2733
+ depthTest={false}
2724
2734
  />
2725
2735
  </Text>
2726
2736
  )}
package/src/index.ts CHANGED
@@ -87,6 +87,14 @@ export type {
87
87
  // with the 2D HighlightLayer from drawLayeredBuildings
88
88
  export type { HighlightLayer as FileCity3DHL } from './components/FileCity3D';
89
89
 
90
+ // Folder-driven elevated panels (file-tree expansion → 3D umbrella tiles)
91
+ export {
92
+ buildFolderElevatedPanels,
93
+ buildFolderIndex,
94
+ hashFolderColor,
95
+ } from './utils/folderElevatedPanels';
96
+ export type { BuildFolderElevatedPanelsOptions } from './utils/folderElevatedPanels';
97
+
90
98
  // Visualization resolution utilities
91
99
  // See docs/VISUALIZATION_STATE_RESOLUTION.md for documentation
92
100
  export { resolveVisualizationIntent } from './utils/visualizationResolution';
@@ -14,6 +14,7 @@ import {
14
14
  type HighlightLayer,
15
15
  } from '../components/FileCity3D';
16
16
  import { createFileColorHighlightLayers } from '../utils/fileColorHighlightLayers';
17
+ import { buildFolderElevatedPanels, buildFolderIndex } from '../utils/folderElevatedPanels';
17
18
 
18
19
  import electronAppCityData from '../../../../assets/electron-app-city-data.json';
19
20
 
@@ -116,6 +117,13 @@ const ELECTRON_DISTRICTS_BY_PATH: Map<string, CityDistrict> = new Map(
116
117
  (electronAppCityData as CityData).districts.map(d => [d.path, d]),
117
118
  );
118
119
 
120
+ /**
121
+ * Pre-built folder index (children, bounds, file counts) for the static
122
+ * electron-app city. Cached once at module load so the story doesn't
123
+ * recompute on every render.
124
+ */
125
+ const ELECTRON_FOLDER_INDEX = buildFolderIndex(electronAppCityData as CityData);
126
+
119
127
  // ---------------------------------------------------------------------------
120
128
  // Scope tree paths
121
129
  // ---------------------------------------------------------------------------
@@ -1079,6 +1087,42 @@ const SingleScopeTemplate: React.FC = () => {
1079
1087
  return panels.length > 0 ? panels : undefined;
1080
1088
  }, [activeTab, scopes, scopeTreeModel, treeExpansion]);
1081
1089
 
1090
+ // Track which folders are expanded in the file tree. The file-tree tab's
1091
+ // elevated panels mirror this: a collapsed folder shows one umbrella tile
1092
+ // covering every descendant district; expanding the folder reveals its
1093
+ // sub-folder tiles (or the buildings themselves at the leaves).
1094
+ const folderTreeExpansion = useFileTreeSelector(
1095
+ treeModel,
1096
+ React.useCallback((model: FileTree) => {
1097
+ const expanded = new Set<string>();
1098
+ for (const dir of ELECTRON_DIRECTORIES) {
1099
+ const item = model.getItem(dir);
1100
+ if (item?.isDirectory() && item.isExpanded()) expanded.add(dir);
1101
+ }
1102
+ return { expanded };
1103
+ }, []),
1104
+ React.useCallback((prev, next) => {
1105
+ if (prev.expanded.size !== next.expanded.size) return false;
1106
+ for (const k of prev.expanded) if (!next.expanded.has(k)) return false;
1107
+ return true;
1108
+ }, []),
1109
+ );
1110
+
1111
+ // Elevated folder panels — driven by the file tree's expansion state.
1112
+ const folderElevatedPanels = React.useMemo<ElevatedScopePanel[] | undefined>(() => {
1113
+ if (activeTab !== 'files') return undefined;
1114
+ const panels = buildFolderElevatedPanels({
1115
+ cityData: electronAppCityData as CityData,
1116
+ expandedFolders: folderTreeExpansion.expanded,
1117
+ onToggleFolder: (folderPath) => {
1118
+ const item = treeModel.getItem(folderPath);
1119
+ if (item?.isDirectory()) item.toggle();
1120
+ },
1121
+ index: ELECTRON_FOLDER_INDEX,
1122
+ });
1123
+ return panels.length > 0 ? panels : undefined;
1124
+ }, [activeTab, treeModel, folderTreeExpansion]);
1125
+
1082
1126
  const openAddModal = React.useCallback((prefillScopeId?: string) => {
1083
1127
  setModalScopeId(prefillScopeId ?? '');
1084
1128
  setModalNamespaceName('');
@@ -1484,7 +1528,7 @@ const SingleScopeTemplate: React.FC = () => {
1484
1528
  linearScale={0.5}
1485
1529
  focusDirectory={focusDirectory}
1486
1530
  highlightLayers={cityHighlightLayers}
1487
- elevatedScopePanels={cityElevatedPanels}
1531
+ elevatedScopePanels={cityElevatedPanels ?? folderElevatedPanels}
1488
1532
  animation={{
1489
1533
  startFlat: true,
1490
1534
  autoStartDelay: null,
@@ -1495,6 +1539,37 @@ const SingleScopeTemplate: React.FC = () => {
1495
1539
  showControls={true}
1496
1540
  />
1497
1541
 
1542
+ {/* Debug overlay — current focusDirectory */}
1543
+ <div
1544
+ style={{
1545
+ position: 'absolute',
1546
+ top: 16,
1547
+ left: 16,
1548
+ padding: '8px 12px',
1549
+ background: 'rgba(15, 23, 42, 0.92)',
1550
+ border: '1px solid #334155',
1551
+ borderRadius: 6,
1552
+ color: '#e2e8f0',
1553
+ fontFamily: 'system-ui, sans-serif',
1554
+ fontSize: 12,
1555
+ zIndex: 100,
1556
+ maxWidth: 480,
1557
+ }}
1558
+ >
1559
+ <div style={sectionLabelStyle}>focusDirectory</div>
1560
+ <div
1561
+ style={{
1562
+ marginTop: 4,
1563
+ fontFamily: 'monospace',
1564
+ fontSize: 12,
1565
+ color: focusDirectory ? '#fde68a' : '#64748b',
1566
+ wordBreak: 'break-all',
1567
+ }}
1568
+ >
1569
+ {focusDirectory ?? '(null)'}
1570
+ </div>
1571
+ </div>
1572
+
1498
1573
  {/* Info overlay — driven by scope tree selection */}
1499
1574
  {scopeInfo && <ScopeInfoOverlay info={scopeInfo} />}
1500
1575
  </div>
@@ -0,0 +1,170 @@
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) => void;
109
+ /**
110
+ * Scale label font size by descendant file count. Default true. When false,
111
+ * the renderer's auto-sized label is used (size derived from tile footprint).
112
+ */
113
+ scaleLabelByFileCount?: boolean;
114
+ /**
115
+ * Pre-built index from `buildFolderIndex(cityData)`. Pass when you cache the
116
+ * city's index across renders to avoid recomputing it.
117
+ */
118
+ index?: FolderIndex;
119
+ }
120
+
121
+ /**
122
+ * Walks the folder hierarchy from the top-level folders of a `CityData`. For
123
+ * each folder:
124
+ * - if expanded → recurse into child folders
125
+ * - if collapsed → emit one elevated panel covering the union of every
126
+ * descendant district's world bounds, colored via `hashFolderColor`.
127
+ *
128
+ * Mirror of the scope-tree expansion behavior, applied to file-tree folders.
129
+ */
130
+ export function buildFolderElevatedPanels(
131
+ options: BuildFolderElevatedPanelsOptions,
132
+ ): ElevatedScopePanel[] {
133
+ const {
134
+ cityData,
135
+ expandedFolders,
136
+ onToggleFolder,
137
+ scaleLabelByFileCount = true,
138
+ } = options;
139
+ const index = options.index ?? buildFolderIndex(cityData);
140
+
141
+ const panels: ElevatedScopePanel[] = [];
142
+
143
+ const walk = (folderPath: string): void => {
144
+ if (expandedFolders.has(folderPath)) {
145
+ const kids = index.children.get(folderPath) ?? [];
146
+ for (const child of kids) walk(child);
147
+ return;
148
+ }
149
+ const bounds = index.bounds.get(folderPath);
150
+ if (!bounds) return;
151
+ const label = folderPath.split('/').pop() ?? folderPath;
152
+ const fileCount = index.fileCounts.get(folderPath) ?? 0;
153
+ const labelSize = scaleLabelByFileCount
154
+ ? Math.max(12, Math.min(240, 8 + Math.sqrt(fileCount) * 5))
155
+ : undefined;
156
+ panels.push({
157
+ id: `folder::${folderPath}`,
158
+ color: hashFolderColor(folderPath),
159
+ height: 4,
160
+ thickness: 2,
161
+ bounds,
162
+ label,
163
+ labelSize,
164
+ onClick: onToggleFolder ? () => onToggleFolder(folderPath) : undefined,
165
+ });
166
+ };
167
+
168
+ for (const top of index.children.get('') ?? []) walk(top);
169
+ return panels;
170
+ }