@principal-ai/file-city-react 0.5.41 → 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.
@@ -1 +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,EAAE,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IACjE,iDAAiD;IACjD,mBAAmB,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IACtE;;;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,CA0CtB"}
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,CAsDhE;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,EAAE,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IACjE,iDAAiD;IACjD,mBAAmB,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IACtE;;;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,CA0CtB"}
@@ -34,6 +34,14 @@ export function buildFolderIndex(cityData) {
34
34
  directorySet.add(d.path);
35
35
  const dirs = Array.from(directorySet).sort();
36
36
  for (const dir of dirs) {
37
+ // The empty path represents the synthetic project root that some city
38
+ // builders emit at depth 0. Including it here would register '' as its
39
+ // own parent (slash<0 → parent=''), which makes the recursive `walk`
40
+ // in `buildFolderElevatedPanels` loop forever the moment the root node
41
+ // is marked expanded. Top-level real folders already live under the
42
+ // empty-string parent, so skipping the empty entry costs nothing.
43
+ if (dir === '')
44
+ continue;
37
45
  const slash = dir.lastIndexOf('/');
38
46
  const parent = slash >= 0 ? dir.slice(0, slash) : '';
39
47
  const arr = children.get(parent);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@principal-ai/file-city-react",
3
- "version": "0.5.41",
3
+ "version": "0.5.42",
4
4
  "type": "module",
5
5
  "description": "React components for File City visualization",
6
6
  "main": "dist/index.js",
@@ -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);