@jkwd/inbase 0.1.13 → 0.1.16

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,6 +1,7 @@
1
1
  import type {
2
2
  AgentIntent,
3
3
  AgentIntentBundle,
4
+ BlueprintNote,
4
5
  PatchImport,
5
6
  PatchImportAddition,
6
7
  PatchSymbolAddition,
@@ -8,7 +9,11 @@ import type {
8
9
  UserCreatedIsland,
9
10
  WorkflowAction,
10
11
  } from './types'
11
- import { parseUserCreatedBlocks, parseUserCreatedIslands } from './userCreated'
12
+ import {
13
+ parseBlueprintNotes,
14
+ parseUserCreatedBlocks,
15
+ parseUserCreatedIslands,
16
+ } from './userCreated'
12
17
 
13
18
  function normalizeImports(value: unknown): PatchImport[] {
14
19
  if (!Array.isArray(value)) return []
@@ -98,12 +103,15 @@ export const emptyIntent: AgentIntent = {
98
103
  initialInstruction: null,
99
104
  creationMode: false,
100
105
  canEnterBlueprint: false,
106
+ blueprintHidden: false,
107
+ blueprintRevision: 0,
101
108
  blueprintSessionId: null,
102
109
  userCreatedBlocks: [],
103
110
  userCreatedIslands: [],
104
111
  blueprintFunctions: [],
105
112
  blueprintVariables: [],
106
113
  blueprintImports: [],
114
+ blueprintNotes: [],
107
115
  }
108
116
 
109
117
  function normalize(data: Partial<AgentIntent> | null | undefined): AgentIntent {
@@ -149,6 +157,9 @@ function normalize(data: Partial<AgentIntent> | null | undefined): AgentIntent {
149
157
  typeof data?.initialInstruction === 'string' ? data.initialInstruction : null,
150
158
  creationMode: Boolean(data?.creationMode),
151
159
  canEnterBlueprint: Boolean(data?.canEnterBlueprint),
160
+ blueprintHidden: Boolean(data?.blueprintHidden),
161
+ blueprintRevision:
162
+ typeof data?.blueprintRevision === 'number' ? data.blueprintRevision : 0,
152
163
  blueprintSessionId:
153
164
  typeof data?.blueprintSessionId === 'string'
154
165
  ? data.blueprintSessionId
@@ -158,18 +169,42 @@ function normalize(data: Partial<AgentIntent> | null | undefined): AgentIntent {
158
169
  blueprintFunctions: normalizeSymbolAdditions(data?.blueprintFunctions),
159
170
  blueprintVariables: normalizeSymbolAdditions(data?.blueprintVariables),
160
171
  blueprintImports: normalizeImportAdditions(data?.blueprintImports),
172
+ blueprintNotes: parseBlueprintNotes(data?.blueprintNotes),
173
+ }
174
+ }
175
+
176
+ function normalizeBlueprint(data: Partial<AgentIntentBundle['blueprint']> | null | undefined) {
177
+ return {
178
+ hidden: Boolean(data?.hidden),
179
+ revision: typeof data?.revision === 'number' ? data.revision : 0,
180
+ enabled: Boolean(data?.enabled),
181
+ userCreatedBlocks: parseUserCreatedBlocks(data?.userCreatedBlocks),
182
+ userCreatedIslands: parseUserCreatedIslands(data?.userCreatedIslands),
183
+ addedFunctions: normalizeSymbolAdditions(data?.addedFunctions),
184
+ addedVariables: normalizeSymbolAdditions(data?.addedVariables),
185
+ addedImports: normalizeImportAdditions(data?.addedImports),
186
+ notes: parseBlueprintNotes(data?.notes),
161
187
  }
162
188
  }
163
189
 
164
190
  export async function fetchAgentIntents(): Promise<AgentIntentBundle> {
165
191
  const query = new URLSearchParams({ t: String(Date.now()) })
166
192
  const response = await fetch(`/api/agent-intent?${query}`)
167
- if (!response.ok) return { focusedSessionId: null, nextAttachSessionId: null, intents: [] }
193
+ const emptyBlueprint = normalizeBlueprint(null)
194
+ if (!response.ok) {
195
+ return {
196
+ focusedSessionId: null,
197
+ nextAttachSessionId: null,
198
+ intents: [],
199
+ blueprint: emptyBlueprint,
200
+ }
201
+ }
168
202
  const data = (await response.json()) as {
169
203
  focusedSessionId?: string | null
170
204
  nextAttachSessionId?: string | null
171
205
  intents?: unknown
172
206
  sessionId?: string | null
207
+ blueprint?: Partial<AgentIntentBundle['blueprint']>
173
208
  } & Partial<AgentIntent>
174
209
  if (Array.isArray(data.intents)) {
175
210
  return {
@@ -182,6 +217,7 @@ export async function fetchAgentIntents(): Promise<AgentIntentBundle> {
182
217
  intents: data.intents
183
218
  .map((intent) => normalize(intent as Partial<AgentIntent>))
184
219
  .filter((intent) => Boolean(intent.sessionId)),
220
+ blueprint: normalizeBlueprint(data.blueprint),
185
221
  }
186
222
  }
187
223
  const intent = normalize(data)
@@ -189,6 +225,23 @@ export async function fetchAgentIntents(): Promise<AgentIntentBundle> {
189
225
  focusedSessionId: intent.sessionId,
190
226
  nextAttachSessionId: intent.awaitingAttach ? intent.sessionId : null,
191
227
  intents: intent.sessionId ? [intent] : [],
228
+ blueprint: normalizeBlueprint(data.blueprint ?? {
229
+ hidden: intent.blueprintHidden,
230
+ revision: intent.blueprintRevision,
231
+ enabled:
232
+ intent.userCreatedBlocks.length > 0 ||
233
+ intent.userCreatedIslands.length > 0 ||
234
+ intent.blueprintFunctions.length > 0 ||
235
+ intent.blueprintVariables.length > 0 ||
236
+ intent.blueprintImports.length > 0 ||
237
+ intent.blueprintNotes.length > 0,
238
+ userCreatedBlocks: intent.userCreatedBlocks,
239
+ userCreatedIslands: intent.userCreatedIslands,
240
+ addedFunctions: intent.blueprintFunctions,
241
+ addedVariables: intent.blueprintVariables,
242
+ addedImports: intent.blueprintImports,
243
+ notes: intent.blueprintNotes,
244
+ }),
192
245
  }
193
246
  }
194
247
 
@@ -219,6 +272,7 @@ export async function performAgentAction(
219
272
  addedFunctions?: PatchSymbolAddition[]
220
273
  addedVariables?: PatchSymbolAddition[]
221
274
  addedImports?: PatchImportAddition[]
275
+ notes?: BlueprintNote[]
222
276
  } = {},
223
277
  ) {
224
278
  const response = await fetch('/api/agent-intent', {
@@ -234,16 +288,17 @@ export async function performAgentAction(
234
288
  }
235
289
 
236
290
  export function persistSessionBlueprint(
237
- sessionId: string,
291
+ sessionId: string | null | undefined,
238
292
  payload: {
239
293
  userCreatedBlocks: UserCreatedBlock[]
240
294
  userCreatedIslands: UserCreatedIsland[]
241
295
  addedFunctions?: PatchSymbolAddition[]
242
296
  addedVariables?: PatchSymbolAddition[]
243
297
  addedImports?: PatchImportAddition[]
298
+ notes?: BlueprintNote[]
244
299
  },
245
300
  ) {
246
- fetch('/api/agent-intent', {
301
+ return fetch('/api/agent-intent', {
247
302
  method: 'POST',
248
303
  headers: { 'Content-Type': 'application/json' },
249
304
  body: JSON.stringify({
@@ -252,7 +307,34 @@ export function persistSessionBlueprint(
252
307
  ...payload,
253
308
  }),
254
309
  }).catch(() => {
255
- // Keep local drafts if the session handshake is no longer open.
310
+ // Keep local drafts if the visualizer could not save the shared blueprint.
311
+ })
312
+ }
313
+
314
+ export function persistBlueprintHidden(hidden: boolean) {
315
+ return fetch('/api/agent-intent', {
316
+ method: 'POST',
317
+ headers: { 'Content-Type': 'application/json' },
318
+ body: JSON.stringify({
319
+ action: 'blueprint_set_hidden',
320
+ hidden,
321
+ }),
322
+ })
323
+ }
324
+
325
+ export function persistBlueprintClear() {
326
+ return fetch('/api/agent-intent', {
327
+ method: 'POST',
328
+ headers: { 'Content-Type': 'application/json' },
329
+ body: JSON.stringify({ action: 'blueprint_clear' }),
330
+ })
331
+ }
332
+
333
+ export function persistBlueprintCleanup() {
334
+ return fetch('/api/agent-intent', {
335
+ method: 'POST',
336
+ headers: { 'Content-Type': 'application/json' },
337
+ body: JSON.stringify({ action: 'blueprint_cleanup' }),
256
338
  })
257
339
  }
258
340
 
@@ -188,6 +188,7 @@ button {
188
188
  display: flex;
189
189
  gap: 8px;
190
190
  flex: none;
191
+ flex-wrap: wrap;
191
192
  }
192
193
 
193
194
  .hud-chip,
@@ -1389,6 +1390,102 @@ button {
1389
1390
  border-color: #9ad8ff;
1390
1391
  }
1391
1392
 
1393
+ .hud-item-actions {
1394
+ display: flex;
1395
+ align-items: center;
1396
+ gap: 2px;
1397
+ flex: none;
1398
+ }
1399
+
1400
+ .hud-item-note {
1401
+ flex: none;
1402
+ padding: 2px 6px;
1403
+ color: #b7c0ce;
1404
+ background: transparent;
1405
+ border: 1px solid #3a4250;
1406
+ cursor: pointer;
1407
+ font-size: 11px;
1408
+ }
1409
+
1410
+ .hud-item-note:hover,
1411
+ .hud-item-note:focus-visible,
1412
+ .hud-item-note[data-open='true'] {
1413
+ border-color: #9ad8ff;
1414
+ color: #d7eef8;
1415
+ }
1416
+
1417
+ .hud-item-note[data-has-note='true'],
1418
+ .hud-button[data-has-note='true'] {
1419
+ border-color: #5d9ec4;
1420
+ color: #d7eef8;
1421
+ }
1422
+
1423
+ .hud-note-overlay {
1424
+ position: absolute;
1425
+ inset: 0;
1426
+ z-index: 22;
1427
+ display: grid;
1428
+ place-items: center;
1429
+ padding: 24px;
1430
+ background: rgba(12, 14, 18, 0.82);
1431
+ pointer-events: auto;
1432
+ }
1433
+
1434
+ .hud-note-card {
1435
+ width: min(1400px, 100%);
1436
+ height: min(92dvh, 100%);
1437
+ display: flex;
1438
+ flex-direction: column;
1439
+ gap: 16px;
1440
+ padding: 24px 24px 20px;
1441
+ background: var(--vc-editor);
1442
+ border: 1px solid var(--vc-border);
1443
+ }
1444
+
1445
+ .hud-note-header {
1446
+ display: flex;
1447
+ align-items: flex-start;
1448
+ justify-content: space-between;
1449
+ gap: 16px;
1450
+ flex: none;
1451
+ }
1452
+
1453
+ .hud-note-heading {
1454
+ min-width: 0;
1455
+ }
1456
+
1457
+ .hud-note-header h1 {
1458
+ margin: 0;
1459
+ font-size: 20px;
1460
+ font-weight: 600;
1461
+ }
1462
+
1463
+ .hud-note-subtitle {
1464
+ margin: 6px 0 0;
1465
+ color: #8b95a5;
1466
+ font-size: 13px;
1467
+ line-height: 1.4;
1468
+ word-break: break-all;
1469
+ }
1470
+
1471
+ .hud-note-field {
1472
+ width: 100%;
1473
+ min-height: 0;
1474
+ flex: 1;
1475
+ resize: none;
1476
+ box-sizing: border-box;
1477
+ padding: 16px;
1478
+ color: #e7ebf2;
1479
+ background: rgba(25, 28, 34, 0.96);
1480
+ border: 1px solid #3a4250;
1481
+ font: 15px/1.55 ui-monospace, SFMono-Regular, Menlo, monospace;
1482
+ outline: none;
1483
+ }
1484
+
1485
+ .hud-note-field:focus {
1486
+ border-color: #9ad8ff;
1487
+ }
1488
+
1392
1489
  .hud-add-row {
1393
1490
  display: flex;
1394
1491
  gap: 6px;
@@ -1441,11 +1538,6 @@ button {
1441
1538
  font-weight: 600;
1442
1539
  }
1443
1540
 
1444
- .block-name-overlay {
1445
- z-index: 20 !important;
1446
- pointer-events: auto;
1447
- }
1448
-
1449
1541
  .hud-name-gate {
1450
1542
  position: absolute;
1451
1543
  top: 42%;
@@ -1455,6 +1547,15 @@ button {
1455
1547
  z-index: 4;
1456
1548
  }
1457
1549
 
1550
+ .block-name-overlay {
1551
+ z-index: 90 !important;
1552
+ pointer-events: auto;
1553
+ }
1554
+
1555
+ .block-name-overlay-map .block-name-form {
1556
+ transform: translate(-50%, calc(-100% - 8px));
1557
+ }
1558
+
1458
1559
  .block-name-form {
1459
1560
  pointer-events: auto;
1460
1561
  }
@@ -0,0 +1,26 @@
1
+ let isolation = 0
2
+
3
+ export function beginKeyboardIsolation() {
4
+ isolation += 1
5
+ return () => {
6
+ isolation = Math.max(0, isolation - 1)
7
+ }
8
+ }
9
+
10
+ export function isKeyboardIsolated() {
11
+ return isolation > 0
12
+ }
13
+
14
+ export function isTypingInField(target: EventTarget | null) {
15
+ return (
16
+ target instanceof HTMLElement &&
17
+ (target.tagName === 'TEXTAREA' ||
18
+ target.tagName === 'INPUT' ||
19
+ target.tagName === 'SELECT' ||
20
+ target.isContentEditable)
21
+ )
22
+ }
23
+
24
+ export function shouldIgnoreShortcut(event: KeyboardEvent) {
25
+ return isKeyboardIsolated() || isTypingInField(event.target)
26
+ }
@@ -2,6 +2,7 @@ import { useEffect } from 'react'
2
2
  import { useThree } from '@react-three/fiber'
3
3
  import * as THREE from 'three'
4
4
  import { folderAt } from '../layout'
5
+ import { shouldIgnoreShortcut } from '../keyboard'
5
6
  import type { WorldLayout } from '../types'
6
7
 
7
8
  type BlockPlacerProps = {
@@ -19,16 +20,6 @@ const forward = new THREE.Vector3()
19
20
  const PLACE_MIN = 3
20
21
  const PLACE_MAX = 22
21
22
 
22
- function typingInField(target: EventTarget | null) {
23
- return (
24
- target instanceof HTMLElement &&
25
- (target.tagName === 'TEXTAREA' ||
26
- target.tagName === 'INPUT' ||
27
- target.tagName === 'SELECT' ||
28
- target.isContentEditable)
29
- )
30
- }
31
-
32
23
  function lookPoint(camera: THREE.Camera): { x: number; z: number } {
33
24
  raycaster.setFromCamera(ndc, camera)
34
25
  const reached = raycaster.ray.intersectPlane(ground, hit)
@@ -70,7 +61,7 @@ export function BlockPlacer({ enabled, layout, onPlace }: BlockPlacerProps) {
70
61
  useEffect(() => {
71
62
  const onKey = (event: KeyboardEvent) => {
72
63
  if (!enabled || event.repeat || event.code !== 'Space') return
73
- if (typingInField(event.target)) return
64
+ if (shouldIgnoreShortcut(event)) return
74
65
  event.preventDefault()
75
66
  const { x, z } = lookPoint(camera)
76
67
  const island =
@@ -193,11 +193,19 @@ export const FileBlock = memo(function FileBlock({
193
193
  )}
194
194
  {naming && onCommitName && onCancelName && (
195
195
  <Html
196
- position={[0, height / 2 + 0.42, 0]}
197
- center
196
+ position={
197
+ mapMode
198
+ ? [0, height / 2 + 0.04, -depth / 2]
199
+ : [0, height / 2 + 0.42, 0]
200
+ }
201
+ center={!mapMode}
198
202
  occlude={false}
199
- wrapperClass="block-name-overlay"
200
- zIndexRange={[100, 0]}
203
+ wrapperClass={
204
+ mapMode
205
+ ? 'block-name-overlay block-name-overlay-map'
206
+ : 'block-name-overlay'
207
+ }
208
+ zIndexRange={[120, 0]}
201
209
  >
202
210
  <NameInput
203
211
  placeholder="File name"
@@ -10,6 +10,7 @@ import {
10
10
  } from '../theme'
11
11
  import type { PlacedFolder } from '../types'
12
12
  import { MapSelectBorder } from './MapSelectBorder'
13
+ import { NameInput } from '../ui/NameInput'
13
14
 
14
15
  type FolderAreaProps = {
15
16
  folder: PlacedFolder
@@ -19,6 +20,8 @@ type FolderAreaProps = {
19
20
  highlightKind?: ChangeKind | null
20
21
  previewLabels?: boolean
21
22
  labelVisible?: boolean
23
+ onCommitName?: (name: string) => void
24
+ onCancelName?: () => void
22
25
  }
23
26
 
24
27
  export function FolderArea({
@@ -29,6 +32,8 @@ export function FolderArea({
29
32
  highlightKind = null,
30
33
  previewLabels = false,
31
34
  labelVisible = true,
35
+ onCommitName,
36
+ onCancelName,
32
37
  }: FolderAreaProps) {
33
38
  const added = Boolean(folder.added)
34
39
  const highlight = highlightKind ? CHANGE_HIGHLIGHT[highlightKind] : null
@@ -88,6 +93,21 @@ export function FolderArea({
88
93
  <meshBasicMaterial color={aisle} />
89
94
  </mesh>
90
95
  )}
96
+ {naming && mapMode && onCommitName && onCancelName && (
97
+ <Html
98
+ position={[0, 1.35, -folder.depth / 2 + 1.35]}
99
+ center
100
+ occlude={false}
101
+ wrapperClass="block-name-overlay"
102
+ zIndexRange={[120, 0]}
103
+ >
104
+ <NameInput
105
+ placeholder="Folder name"
106
+ onCommit={onCommitName}
107
+ onCancel={onCancelName}
108
+ />
109
+ </Html>
110
+ )}
91
111
  <Suspense fallback={null}>
92
112
  {!naming && previewLabels && labelVisible && (
93
113
  <Html
@@ -1,6 +1,7 @@
1
1
  import { useEffect } from 'react'
2
2
  import { useThree } from '@react-three/fiber'
3
3
  import { folderAt } from '../layout'
4
+ import { shouldIgnoreShortcut } from '../keyboard'
4
5
  import type { WorldLayout } from '../types'
5
6
 
6
7
  type IslandPlacerProps = {
@@ -9,23 +10,13 @@ type IslandPlacerProps = {
9
10
  onPlace: (parent: string) => void
10
11
  }
11
12
 
12
- function typingInField(target: EventTarget | null) {
13
- return (
14
- target instanceof HTMLElement &&
15
- (target.tagName === 'TEXTAREA' ||
16
- target.tagName === 'INPUT' ||
17
- target.tagName === 'SELECT' ||
18
- target.isContentEditable)
19
- )
20
- }
21
-
22
13
  export function IslandPlacer({ enabled, layout, onPlace }: IslandPlacerProps) {
23
14
  const { camera } = useThree()
24
15
 
25
16
  useEffect(() => {
26
17
  const onKey = (event: KeyboardEvent) => {
27
18
  if (!enabled || event.repeat || event.code !== 'KeyB') return
28
- if (typingInField(event.target)) return
19
+ if (shouldIgnoreShortcut(event)) return
29
20
  const island = folderAt(camera.position.x, camera.position.z, layout)
30
21
  if (!island) return
31
22
  event.preventDefault()
@@ -1,5 +1,4 @@
1
- import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
2
- import { createPortal } from 'react-dom'
1
+ import { useEffect, useLayoutEffect, useMemo, useRef } from 'react'
3
2
  import { useFrame, useThree } from '@react-three/fiber'
4
3
  import { Html, MapControls, OrthographicCamera } from '@react-three/drei'
5
4
  import * as THREE from 'three'
@@ -19,6 +18,7 @@ type MapViewProps = {
19
18
  marker: [number, number] | null
20
19
  highlightedFolders?: Partial<Record<string, ChangeKind>>
21
20
  selectedFolder?: string | null
21
+ namingFolderPath?: string | null
22
22
  onLand: (x: number, z: number) => void
23
23
  onSelect: (fileId: string | null) => void
24
24
  onSelectFolder: (folderPath: string | null) => void
@@ -32,6 +32,7 @@ export function MapView({
32
32
  marker,
33
33
  highlightedFolders,
34
34
  selectedFolder = null,
35
+ namingFolderPath = null,
35
36
  onLand,
36
37
  onSelect,
37
38
  onSelectFolder,
@@ -84,10 +85,9 @@ export function MapView({
84
85
  const element = gl.domElement
85
86
  element.style.cursor = 'grab'
86
87
 
87
- const isWalkClick = (event: PointerEvent | MouseEvent) => event.ctrlKey
88
+ const isWalkClick = (event: PointerEvent | MouseEvent) => event.altKey
88
89
 
89
- const isWalkButton = (event: PointerEvent) =>
90
- event.button === 0 || (event.ctrlKey && event.button === 2)
90
+ const isWalkButton = (event: PointerEvent) => event.button === 0
91
91
 
92
92
  const onDown = (event: PointerEvent) => {
93
93
  if (!isWalkButton(event)) return
@@ -184,7 +184,7 @@ export function MapView({
184
184
  }
185
185
 
186
186
  const onContextMenu = (event: MouseEvent) => {
187
- if (event.ctrlKey) {
187
+ if (event.altKey) {
188
188
  event.preventDefault()
189
189
  return
190
190
  }
@@ -314,6 +314,7 @@ export function MapView({
314
314
  folders={layout.folders}
315
315
  highlightedFolders={highlightedFolders}
316
316
  selectedFolder={selectedFolder}
317
+ namingFolderPath={namingFolderPath}
317
318
  />
318
319
  )}
319
320
  {enabled && marker && <LandMarker marker={marker} />}
@@ -323,6 +324,23 @@ export function MapView({
323
324
 
324
325
  const PROJECT = new THREE.Vector3()
325
326
  const MIN_FOLDER_LABEL_PX = 28
327
+ const FOLDER_ENTRANCE_Z = 1.35
328
+
329
+ function projectToScreen(
330
+ x: number,
331
+ y: number,
332
+ z: number,
333
+ camera: THREE.Camera,
334
+ width: number,
335
+ height: number,
336
+ ) {
337
+ PROJECT.set(x, y, z).project(camera)
338
+ return {
339
+ x: (PROJECT.x * 0.5 + 0.5) * width,
340
+ y: (-PROJECT.y * 0.5 + 0.5) * height,
341
+ behind: PROJECT.z < -1 || PROJECT.z > 1,
342
+ }
343
+ }
326
344
 
327
345
  function folderLabelClass(
328
346
  folder: PlacedFolder,
@@ -347,21 +365,50 @@ function MapFolderLabels({
347
365
  folders,
348
366
  highlightedFolders,
349
367
  selectedFolder,
368
+ namingFolderPath,
350
369
  }: {
351
370
  folders: Record<string, PlacedFolder>
352
371
  highlightedFolders?: Partial<Record<string, ChangeKind>>
353
372
  selectedFolder: string | null
373
+ namingFolderPath: string | null
354
374
  }) {
355
375
  const camera = useThree((state) => state.camera)
356
376
  const gl = useThree((state) => state.gl)
357
377
  const size = useThree((state) => state.size)
358
- const layerRef = useRef<HTMLDivElement>(null)
378
+ const layerRef = useRef<HTMLDivElement | null>(null)
359
379
  const items = useMemo(() => Object.values(folders), [folders])
360
- const [host, setHost] = useState<HTMLElement | null>(null)
361
380
 
362
381
  useLayoutEffect(() => {
363
- setHost(gl.domElement.parentElement)
364
- }, [gl])
382
+ const parent = gl.domElement.parentElement
383
+ if (!parent) return
384
+ const layer = document.createElement('div')
385
+ layer.className = 'map-folder-label-layer'
386
+ layer.style.cssText =
387
+ 'position:absolute;inset:0;overflow:hidden;pointer-events:none;z-index:80;background:transparent;'
388
+ for (const folder of items) {
389
+ const el = document.createElement('div')
390
+ el.className = folderLabelClass(folder, highlightedFolders, selectedFolder)
391
+ el.style.position = 'absolute'
392
+ el.style.top = '0'
393
+ el.style.left = '0'
394
+ el.style.visibility = 'hidden'
395
+ const name = document.createElement('span')
396
+ name.className = 'map-folder-name'
397
+ name.textContent = folderKindLabel(
398
+ folder.name,
399
+ highlightedFolders?.[folder.path] ?? null,
400
+ folder.added ?? false,
401
+ )
402
+ el.appendChild(name)
403
+ layer.appendChild(el)
404
+ }
405
+ parent.appendChild(layer)
406
+ layerRef.current = layer
407
+ return () => {
408
+ layer.remove()
409
+ layerRef.current = null
410
+ }
411
+ }, [gl, highlightedFolders, items, selectedFolder])
365
412
 
366
413
  useFrame(() => {
367
414
  const layer = layerRef.current
@@ -372,21 +419,31 @@ function MapFolderLabels({
372
419
  const el = nodes[i] as HTMLElement | undefined
373
420
  const folder = items[i]
374
421
  if (!el || !folder) continue
375
- PROJECT.set(folder.x, 14, folder.z + 1.35).project(camera)
376
- const x = (PROJECT.x * 0.5 + 0.5) * size.width
377
- const y = (-PROJECT.y * 0.5 + 0.5) * size.height
422
+ const screen = projectToScreen(
423
+ folder.x,
424
+ 14,
425
+ folder.z + FOLDER_ENTRANCE_Z,
426
+ camera,
427
+ size.width,
428
+ size.height,
429
+ )
430
+ const x = screen.x
431
+ const y = screen.y
378
432
  const span = Math.max(folder.width, folder.depth) * zoom
379
433
  const force =
380
434
  selectedFolder === folder.path ||
381
435
  Boolean(highlightedFolders?.[folder.path] || folder.added)
382
436
  const onScreen =
383
- PROJECT.z >= -1 &&
384
- PROJECT.z <= 1 &&
437
+ !screen.behind &&
385
438
  x > -120 &&
386
439
  x < size.width + 120 &&
387
440
  y > -40 &&
388
441
  y < size.height + 40
389
- if (!onScreen || (!force && span < MIN_FOLDER_LABEL_PX)) {
442
+ if (
443
+ folder.path === namingFolderPath ||
444
+ !onScreen ||
445
+ (!force && span < MIN_FOLDER_LABEL_PX)
446
+ ) {
390
447
  if (el.style.visibility !== 'hidden') el.style.visibility = 'hidden'
391
448
  continue
392
449
  }
@@ -401,27 +458,7 @@ function MapFolderLabels({
401
458
  }
402
459
  })
403
460
 
404
- if (!host) return null
405
-
406
- return createPortal(
407
- <div ref={layerRef} className="map-folder-label-layer">
408
- {items.map((folder) => (
409
- <div
410
- key={folder.path}
411
- className={folderLabelClass(folder, highlightedFolders, selectedFolder)}
412
- >
413
- <span className="map-folder-name">
414
- {folderKindLabel(
415
- folder.name,
416
- highlightedFolders?.[folder.path] ?? null,
417
- folder.added ?? false,
418
- )}
419
- </span>
420
- </div>
421
- ))}
422
- </div>,
423
- host,
424
- )
461
+ return null
425
462
  }
426
463
 
427
464
  function folderKindLabel(