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

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 (56) 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/utils/folderElevatedPanels.d.ts +3 -1
  35. package/dist/utils/folderElevatedPanels.d.ts.map +1 -1
  36. package/dist/utils/folderElevatedPanels.js +13 -2
  37. package/package.json +2 -1
  38. package/src/components/FileCity3D/FileCity3D.tsx +200 -52
  39. package/src/components/FileCityExplorer/AddToAreaModal.tsx +273 -0
  40. package/src/components/FileCityExplorer/AddToScopeModal.tsx +320 -0
  41. package/src/components/FileCityExplorer/FileCityExplorer.tsx +1457 -0
  42. package/src/components/FileCityExplorer/ScopeInfoOverlay.tsx +229 -0
  43. package/src/components/FileCityExplorer/index.ts +2 -0
  44. package/src/components/FileCityExplorer/layers.ts +72 -0
  45. package/src/components/FileCityExplorer/model.ts +35 -0
  46. package/src/components/FileCityExplorer/pathConversion.ts +32 -0
  47. package/src/components/FileCityExplorer/scopeTreePaths.ts +52 -0
  48. package/src/components/FileCityExplorer/styles.ts +34 -0
  49. package/src/stories/2D3DComparison.stories.tsx +13 -2
  50. package/src/stories/ElevatedScopePanels.stories.tsx +295 -0
  51. package/src/stories/FileCity3D.stories.tsx +24 -3
  52. package/src/stories/FileCityExplorer.stories.tsx +2474 -0
  53. package/src/stories/FileCityExplorerComponent.stories.tsx +59 -0
  54. package/src/stories/LeaderLineSnippetOverlay.stories.tsx +306 -0
  55. package/src/utils/folderElevatedPanels.ts +15 -2
  56. package/src/stories/ScopeOverlay.stories.tsx +0 -1610
@@ -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,306 @@
1
+ import { useLayoutEffect, useRef, useState } from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+
4
+ import { ArchitectureMapHighlightLayers } from '../components/ArchitectureMapHighlightLayers';
5
+ import type { CityData } from '../components/FileCity3D';
6
+ import { createFileColorHighlightLayers } from '../utils/fileColorHighlightLayers';
7
+ import authServerCityData from '../../../../assets/auth-server-city-data.json';
8
+
9
+ const meta = {
10
+ title: 'Prototypes/Leader Line Snippet Overlay',
11
+ parameters: { layout: 'fullscreen' },
12
+ } satisfies Meta;
13
+
14
+ export default meta;
15
+
16
+ // Mirrors the default `padding` inside ArchitectureMapHighlightLayers, so the
17
+ // world->screen math here lines up with what the canvas actually draws.
18
+ const CANVAS_PADDING = 20;
19
+
20
+ const TARGET_PATH = 'auth-server/src/app/api/auth/workos/callback/route.ts';
21
+
22
+ const SNIPPET = `export async function GET(req: Request) {
23
+ const url = new URL(req.url);
24
+ const code = url.searchParams.get('code');
25
+ if (!code) {
26
+ return NextResponse.redirect(new URL('/login', req.url));
27
+ }
28
+
29
+ const { user, sessionId } = await workos.userManagement
30
+ .authenticateWithCode({
31
+ clientId: env.WORKOS_CLIENT_ID,
32
+ code,
33
+ });
34
+
35
+ return setSessionCookie({ user, sessionId });
36
+ }`;
37
+
38
+ const PANEL_WIDTH = 360;
39
+
40
+ interface FitParams {
41
+ scale: number;
42
+ offsetX: number;
43
+ offsetZ: number;
44
+ }
45
+
46
+ // Replicates calculateScaleAndOffset from ArchitectureMapHighlightLayers.
47
+ function fitCityToBox(
48
+ bounds: CityData['bounds'],
49
+ width: number,
50
+ height: number,
51
+ padding: number,
52
+ ): FitParams {
53
+ const cityWidth = bounds.maxX - bounds.minX;
54
+ const cityDepth = bounds.maxZ - bounds.minZ;
55
+ const horizontalPadding = padding;
56
+ const verticalPadding = padding * 2;
57
+ const scaleX = (width - horizontalPadding) / cityDepth;
58
+ const scaleZ = (height - verticalPadding) / cityWidth;
59
+ const scale = Math.min(scaleX, scaleZ);
60
+ const scaledCityWidth = cityDepth * scale;
61
+ const scaledCityHeight = cityWidth * scale;
62
+ return {
63
+ scale,
64
+ offsetX: (width - scaledCityWidth) / 2,
65
+ offsetZ: (height - scaledCityHeight) / 2,
66
+ };
67
+ }
68
+
69
+ export const SingleLeaderLine: StoryObj = {
70
+ render: function RenderSingleLeaderLine() {
71
+ const cityData = authServerCityData as CityData;
72
+ const highlightLayers = createFileColorHighlightLayers(cityData.buildings);
73
+
74
+ const target = cityData.buildings.find(b => b.path === TARGET_PATH);
75
+
76
+ const stageRef = useRef<HTMLDivElement | null>(null);
77
+ const canvasWrapRef = useRef<HTMLDivElement | null>(null);
78
+ const panelRef = useRef<HTMLDivElement | null>(null);
79
+
80
+ const [layout, setLayout] = useState<{
81
+ stageW: number;
82
+ stageH: number;
83
+ anchor: { x: number; y: number } | null;
84
+ panel: { x: number; y: number; w: number; h: number } | null;
85
+ }>({ stageW: 0, stageH: 0, anchor: null, panel: null });
86
+
87
+ useLayoutEffect(() => {
88
+ const stage = stageRef.current;
89
+ const canvasWrap = canvasWrapRef.current;
90
+ const panel = panelRef.current;
91
+ if (!stage || !canvasWrap || !panel || !target) return;
92
+
93
+ const measure = () => {
94
+ const stageRect = stage.getBoundingClientRect();
95
+ const canvasRect = canvasWrap.getBoundingClientRect();
96
+ const panelRect = panel.getBoundingClientRect();
97
+
98
+ // Compute world->screen position for the target building inside the
99
+ // canvas wrapper, then translate to stage-local coords for the SVG.
100
+ const fit = fitCityToBox(
101
+ cityData.bounds,
102
+ canvasRect.width,
103
+ canvasRect.height,
104
+ CANVAS_PADDING,
105
+ );
106
+ const buildingX =
107
+ (target.position.x - cityData.bounds.minX) * fit.scale + fit.offsetX;
108
+ const buildingY =
109
+ (target.position.z - cityData.bounds.minZ) * fit.scale + fit.offsetZ;
110
+
111
+ const anchor = {
112
+ x: canvasRect.left - stageRect.left + buildingX,
113
+ y: canvasRect.top - stageRect.top + buildingY,
114
+ };
115
+ const panelLocal = {
116
+ x: panelRect.left - stageRect.left,
117
+ y: panelRect.top - stageRect.top,
118
+ w: panelRect.width,
119
+ h: panelRect.height,
120
+ };
121
+
122
+ setLayout({
123
+ stageW: stageRect.width,
124
+ stageH: stageRect.height,
125
+ anchor,
126
+ panel: panelLocal,
127
+ });
128
+ };
129
+
130
+ measure();
131
+ const ro = new ResizeObserver(measure);
132
+ ro.observe(stage);
133
+ ro.observe(canvasWrap);
134
+ ro.observe(panel);
135
+ window.addEventListener('resize', measure);
136
+ return () => {
137
+ ro.disconnect();
138
+ window.removeEventListener('resize', measure);
139
+ };
140
+ }, [cityData.bounds, target]);
141
+
142
+ if (!target) {
143
+ return (
144
+ <div style={{ padding: 24, color: '#f87171' }}>
145
+ Target building not found: {TARGET_PATH}
146
+ </div>
147
+ );
148
+ }
149
+
150
+ // Snippet panel anchor: left edge, vertically centered on the card.
151
+ const panelAnchor =
152
+ layout.panel != null
153
+ ? { x: layout.panel.x, y: layout.panel.y + layout.panel.h / 2 }
154
+ : null;
155
+
156
+ // Smooth S-curve from building -> panel using a horizontal cubic Bezier.
157
+ const path =
158
+ layout.anchor && panelAnchor
159
+ ? (() => {
160
+ const a = layout.anchor;
161
+ const b = panelAnchor;
162
+ const dx = Math.max(80, (b.x - a.x) * 0.5);
163
+ return `M ${a.x} ${a.y} C ${a.x + dx} ${a.y}, ${b.x - dx} ${b.y}, ${b.x} ${b.y}`;
164
+ })()
165
+ : null;
166
+
167
+ return (
168
+ <div
169
+ style={{
170
+ width: '100vw',
171
+ height: '100vh',
172
+ display: 'flex',
173
+ flexDirection: 'column',
174
+ backgroundColor: '#0f1419',
175
+ }}
176
+ >
177
+ <div
178
+ style={{
179
+ padding: '12px 16px',
180
+ backgroundColor: '#1f2937',
181
+ borderBottom: '1px solid #374151',
182
+ color: '#9ca3af',
183
+ fontSize: 13,
184
+ }}
185
+ >
186
+ Prototype: leader line from{' '}
187
+ <code style={{ color: '#e5e7eb' }}>{TARGET_PATH}</code> to a side-panel
188
+ snippet.
189
+ </div>
190
+
191
+ <div
192
+ ref={stageRef}
193
+ style={{
194
+ flex: 1,
195
+ position: 'relative',
196
+ display: 'flex',
197
+ minHeight: 0,
198
+ }}
199
+ >
200
+ <div
201
+ ref={canvasWrapRef}
202
+ style={{ flex: 1, position: 'relative', minWidth: 0 }}
203
+ >
204
+ <ArchitectureMapHighlightLayers
205
+ cityData={cityData}
206
+ highlightLayers={highlightLayers}
207
+ fullSize
208
+ canvasBackgroundColor="#0f1419"
209
+ defaultBuildingColor="#36454F"
210
+ defaultDirectoryColor="#111827"
211
+ enableZoom={false}
212
+ />
213
+ </div>
214
+
215
+ <div
216
+ style={{
217
+ width: PANEL_WIDTH,
218
+ padding: 24,
219
+ display: 'flex',
220
+ flexDirection: 'column',
221
+ justifyContent: 'center',
222
+ borderLeft: '1px solid #1f2937',
223
+ backgroundColor: '#0b0f14',
224
+ }}
225
+ >
226
+ <div
227
+ ref={panelRef}
228
+ style={{
229
+ backgroundColor: '#111827',
230
+ border: '1px solid #374151',
231
+ borderRadius: 8,
232
+ padding: 16,
233
+ boxShadow: '0 4px 16px rgba(0, 0, 0, 0.4)',
234
+ }}
235
+ >
236
+ <div
237
+ style={{
238
+ fontSize: 11,
239
+ color: '#9ca3af',
240
+ textTransform: 'uppercase',
241
+ letterSpacing: 0.5,
242
+ marginBottom: 8,
243
+ }}
244
+ >
245
+ callback / route.ts
246
+ </div>
247
+ <pre
248
+ style={{
249
+ margin: 0,
250
+ fontSize: 12,
251
+ lineHeight: 1.5,
252
+ color: '#e5e7eb',
253
+ fontFamily:
254
+ 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
255
+ whiteSpace: 'pre-wrap',
256
+ }}
257
+ >
258
+ {SNIPPET}
259
+ </pre>
260
+ </div>
261
+ </div>
262
+
263
+ {/* SVG overlay spans the whole stage so the line can cross columns. */}
264
+ <svg
265
+ width={layout.stageW}
266
+ height={layout.stageH}
267
+ style={{
268
+ position: 'absolute',
269
+ top: 0,
270
+ left: 0,
271
+ pointerEvents: 'none',
272
+ zIndex: 5,
273
+ }}
274
+ >
275
+ {path && layout.anchor && panelAnchor && (
276
+ <>
277
+ <path
278
+ d={path}
279
+ fill="none"
280
+ stroke="#fbbf24"
281
+ strokeWidth={1.5}
282
+ strokeDasharray="4 4"
283
+ opacity={0.85}
284
+ />
285
+ <circle
286
+ cx={layout.anchor.x}
287
+ cy={layout.anchor.y}
288
+ r={5}
289
+ fill="#fbbf24"
290
+ stroke="#0f1419"
291
+ strokeWidth={1.5}
292
+ />
293
+ <circle
294
+ cx={panelAnchor.x}
295
+ cy={panelAnchor.y}
296
+ r={3}
297
+ fill="#fbbf24"
298
+ />
299
+ </>
300
+ )}
301
+ </svg>
302
+ </div>
303
+ </div>
304
+ );
305
+ },
306
+ };
@@ -54,6 +54,13 @@ export function buildFolderIndex(cityData: CityData): FolderIndex {
54
54
  for (const d of cityData.districts) directorySet.add(d.path);
55
55
  const dirs = Array.from(directorySet).sort();
56
56
  for (const dir of dirs) {
57
+ // The empty path represents the synthetic project root that some city
58
+ // builders emit at depth 0. Including it here would register '' as its
59
+ // own parent (slash<0 → parent=''), which makes the recursive `walk`
60
+ // in `buildFolderElevatedPanels` loop forever the moment the root node
61
+ // is marked expanded. Top-level real folders already live under the
62
+ // empty-string parent, so skipping the empty entry costs nothing.
63
+ if (dir === '') continue;
57
64
  const slash = dir.lastIndexOf('/');
58
65
  const parent = slash >= 0 ? dir.slice(0, slash) : '';
59
66
  const arr = children.get(parent);
@@ -105,7 +112,9 @@ export interface BuildFolderElevatedPanelsOptions {
105
112
  */
106
113
  expandedFolders: ReadonlySet<string>;
107
114
  /** Toggle handler invoked when an umbrella tile is clicked. */
108
- onToggleFolder?: (folderPath: string) => void;
115
+ onToggleFolder?: (folderPath: string, event: MouseEvent) => void;
116
+ /** Double-click handler for an umbrella tile. */
117
+ onDoubleClickFolder?: (folderPath: string, event: MouseEvent) => void;
109
118
  /**
110
119
  * Scale label font size by descendant file count. Default true. When false,
111
120
  * the renderer's auto-sized label is used (size derived from tile footprint).
@@ -134,6 +143,7 @@ export function buildFolderElevatedPanels(
134
143
  cityData,
135
144
  expandedFolders,
136
145
  onToggleFolder,
146
+ onDoubleClickFolder,
137
147
  scaleLabelByFileCount = true,
138
148
  } = options;
139
149
  const index = options.index ?? buildFolderIndex(cityData);
@@ -161,7 +171,10 @@ export function buildFolderElevatedPanels(
161
171
  bounds,
162
172
  label,
163
173
  labelSize,
164
- onClick: onToggleFolder ? () => onToggleFolder(folderPath) : undefined,
174
+ onClick: onToggleFolder ? (event: MouseEvent) => onToggleFolder(folderPath, event) : undefined,
175
+ onDoubleClick: onDoubleClickFolder
176
+ ? (event: MouseEvent) => onDoubleClickFolder(folderPath, event)
177
+ : undefined,
165
178
  });
166
179
  };
167
180