@jkwd/inbase 0.1.5 → 0.1.7

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.
@@ -8,6 +8,8 @@ import {
8
8
  standInFront,
9
9
  relationTravelTarget,
10
10
  withPreviewGraph,
11
+ filterGraphToChangePaths,
12
+ mapPointOntoFolder,
11
13
  } from './layout'
12
14
  import { World } from './scene/World'
13
15
  import { HUD } from './ui/HUD'
@@ -49,6 +51,7 @@ function intentSignature(intent: AgentIntent) {
49
51
  updatedAt: intent.updatedAt,
50
52
  status: intent.status,
51
53
  phase: intent.phase,
54
+ stalledWait: intent.stalledWait,
52
55
  sessionId: intent.sessionId,
53
56
  creationMode: intent.creationMode,
54
57
  diffId: intent.diffId,
@@ -61,6 +64,8 @@ function intentSignature(intent: AgentIntent) {
61
64
  addedFunctions: intent.addedFunctions,
62
65
  addedVariables: intent.addedVariables,
63
66
  addedImports: intent.addedImports,
67
+ changedFunctions: intent.changedFunctions,
68
+ changedVariables: intent.changedVariables,
64
69
  })
65
70
  }
66
71
 
@@ -166,6 +171,59 @@ function Explorer({
166
171
  if (previewing) markCreatedFolders(world, intent.createFolders ?? [])
167
172
  return withUserCreatedLayout(world, userBlocks, userIslands)
168
173
  }, [intent.createFolders, previewGraph, previewing, userBlocks, userIslands])
174
+ const changeFileIds = useMemo(() => {
175
+ const ids = new Set<string>()
176
+ for (const id of intent.files) ids.add(id)
177
+ for (const id of intent.creates) ids.add(id)
178
+ for (const id of intent.deletes) ids.add(id)
179
+ for (const block of userBlocks) ids.add(block.id)
180
+ for (const item of blueprintFunctions) ids.add(item.file)
181
+ for (const item of blueprintVariables) ids.add(item.file)
182
+ for (const item of blueprintImports) ids.add(item.file)
183
+ return [...ids]
184
+ }, [
185
+ blueprintFunctions,
186
+ blueprintImports,
187
+ blueprintVariables,
188
+ intent.creates,
189
+ intent.deletes,
190
+ intent.files,
191
+ userBlocks,
192
+ ])
193
+ const changeFolderPaths = useMemo(() => {
194
+ const paths = new Set<string>()
195
+ for (const path of intent.createFolders ?? []) paths.add(path)
196
+ for (const island of userIslands) {
197
+ if (island.path) paths.add(island.path)
198
+ else if (island.id) paths.add(island.id)
199
+ }
200
+ return [...paths]
201
+ }, [intent.createFolders, userIslands])
202
+ const hasChangeSet =
203
+ changeFileIds.length > 0 || changeFolderPaths.length > 0
204
+ const changePathGraph = useMemo(() => {
205
+ if (!hasChangeSet) return displayGraph
206
+ return filterGraphToChangePaths(
207
+ displayGraph,
208
+ changeFileIds,
209
+ changeFolderPaths,
210
+ )
211
+ }, [changeFileIds, changeFolderPaths, displayGraph, hasChangeSet])
212
+ const changePathLayout = useMemo(() => {
213
+ if (!hasChangeSet) return layout
214
+ const world = layoutWorld(changePathGraph)
215
+ markCreatedFolders(world, [
216
+ ...(intent.createFolders ?? []),
217
+ ...userIslands.map((island) => island.path || island.id),
218
+ ])
219
+ return world
220
+ }, [
221
+ changePathGraph,
222
+ hasChangeSet,
223
+ intent.createFolders,
224
+ layout,
225
+ userIslands,
226
+ ])
169
227
  const [mode, setMode] = useState<ViewMode>('map')
170
228
  const [landAt, setLandAt] = useState<[number, number]>([
171
229
  layout.spawn[0],
@@ -176,11 +234,14 @@ function Explorer({
176
234
  const [selectedTick, setSelectedTick] = useState(0)
177
235
  const [selectedFolder, setSelectedFolder] = useState<string | null>(null)
178
236
  const [aimedRelation, setAimedRelation] = useState<AimedRelation | null>(null)
237
+ const [aimedFileId, setAimedFileId] = useState<string | null>(null)
238
+ const [inspectTick, setInspectTick] = useState(0)
179
239
  const [flyTo, setFlyTo] = useState<FlyTo | null>(null)
180
240
  const [locked, setLocked] = useState(false)
181
241
  const [currentFolder, setCurrentFolder] = useState(graph.targetName)
182
242
  const [followLook, setFollowLook] = useState(false)
183
243
  const [importedBy, setImportedBy] = useState(false)
244
+ const [changePathsOnly, setChangePathsOnly] = useState(false)
184
245
  const lastIntentSig = useRef<string | null>(null)
185
246
  const viewedDiffId = useRef<Record<string, string | null>>({})
186
247
  const browsingHistory = useRef<Record<string, boolean>>({})
@@ -241,6 +302,22 @@ function Explorer({
241
302
  setMode('walk')
242
303
  }, [])
243
304
 
305
+ const landFromMap = useCallback(
306
+ (x: number, z: number) => {
307
+ if (!changePathsOnly || !hasChangeSet) {
308
+ land(x, z)
309
+ return
310
+ }
311
+ const mapped = mapPointOntoFolder(x, z, changePathLayout, layout, [
312
+ layout.spawn[0],
313
+ layout.spawn[2],
314
+ ])
315
+ if (!mapped) return
316
+ land(mapped[0], mapped[1])
317
+ },
318
+ [changePathLayout, changePathsOnly, hasChangeSet, land, layout],
319
+ )
320
+
244
321
  const travelToFile = useCallback(
245
322
  (fileId: string, fly: boolean) => {
246
323
  const placed = layout.files[fileId]
@@ -274,7 +351,7 @@ function Explorer({
274
351
  async (
275
352
  sessionId: string,
276
353
  action: WorkflowAction,
277
- options: { instruction?: string; step?: number } = {},
354
+ options: { instruction?: string; step?: number; stepByStep?: boolean } = {},
278
355
  ) => {
279
356
  const current = intents.find((item) => item.sessionId === sessionId)
280
357
  if (!current?.sessionId) return
@@ -305,7 +382,12 @@ function Explorer({
305
382
  browsingHistory.current[sessionId] = false
306
383
  lastIntentSig.current = null
307
384
  applyIntent(next, sessionId)
308
- if (action === 'invoke' || action === 'continue' || action === 'stop') {
385
+ if (
386
+ action === 'invoke' ||
387
+ action === 'continue' ||
388
+ action === 'stop' ||
389
+ action === 'set_step_by_step'
390
+ ) {
309
391
  await onRefreshGraph()
310
392
  }
311
393
  } catch {
@@ -631,11 +713,39 @@ function Explorer({
631
713
  [creationMode],
632
714
  )
633
715
 
716
+ const inspectBlock = useCallback(
717
+ (fileId: string) => {
718
+ selectFile(fileId)
719
+ setInspectTick((tick) => tick + 1)
720
+ },
721
+ [selectFile],
722
+ )
723
+
634
724
  const selectFolder = useCallback((folderPath: string | null) => {
635
725
  setSelectedFolder(folderPath)
636
726
  if (folderPath) setSelectedId(null)
637
727
  }, [])
638
728
 
729
+ useEffect(() => {
730
+ if (!changePathsOnly || !hasChangeSet) return
731
+ if (selectedFolder && !changePathLayout.folders[selectedFolder]) {
732
+ setSelectedFolder(null)
733
+ }
734
+ if (
735
+ selectedId &&
736
+ !changePathGraph.files.some((file) => file.id === selectedId)
737
+ ) {
738
+ setSelectedId(null)
739
+ }
740
+ }, [
741
+ changePathGraph.files,
742
+ changePathLayout.folders,
743
+ changePathsOnly,
744
+ hasChangeSet,
745
+ selectedFolder,
746
+ selectedId,
747
+ ])
748
+
639
749
  const addBlueprintFunction = useCallback(
640
750
  (fileId: string, rawName: string) => {
641
751
  if (!creationMode || fileId.startsWith('draft:')) return false
@@ -850,6 +960,11 @@ function Explorer({
850
960
  setImportedBy((current) => !current)
851
961
  }, [])
852
962
 
963
+ const toggleChangePathsOnly = useCallback(() => {
964
+ if (!hasChangeSet) return
965
+ setChangePathsOnly((current) => !current)
966
+ }, [hasChangeSet])
967
+
853
968
  useEffect(() => {
854
969
  const onKey = (event: KeyboardEvent) => {
855
970
  if (event.repeat || event.code !== 'KeyK') return
@@ -870,6 +985,27 @@ function Explorer({
870
985
  return () => window.removeEventListener('keydown', onKey)
871
986
  }, [toggleImportedBy])
872
987
 
988
+ useEffect(() => {
989
+ if (mode !== 'map' || !hasChangeSet) return
990
+ const onKey = (event: KeyboardEvent) => {
991
+ if (event.repeat || event.code !== 'KeyC') return
992
+ const target = event.target
993
+ if (
994
+ target instanceof HTMLElement &&
995
+ (target.tagName === 'TEXTAREA' ||
996
+ target.tagName === 'INPUT' ||
997
+ target.tagName === 'SELECT' ||
998
+ target.isContentEditable)
999
+ ) {
1000
+ return
1001
+ }
1002
+ event.preventDefault()
1003
+ toggleChangePathsOnly()
1004
+ }
1005
+ window.addEventListener('keydown', onKey)
1006
+ return () => window.removeEventListener('keydown', onKey)
1007
+ }, [hasChangeSet, mode, toggleChangePathsOnly])
1008
+
873
1009
  useEffect(() => {
874
1010
  let cancelled = false
875
1011
  const poll = async () => {
@@ -964,7 +1100,7 @@ function Explorer({
964
1100
  onSelectFolder={selectFolder}
965
1101
  onLockedChange={setLocked}
966
1102
  onFolderChange={setCurrentFolder}
967
- onLand={land}
1103
+ onLand={landFromMap}
968
1104
  onWalkPosition={rememberWalk}
969
1105
  onContext={persistUserContext}
970
1106
  plannedIds={plannedIds}
@@ -976,6 +1112,8 @@ function Explorer({
976
1112
  flyTo={flyTo}
977
1113
  aimedRelation={aimedRelation}
978
1114
  onAimRelation={setAimedRelation}
1115
+ onAimFile={setAimedFileId}
1116
+ onInspect={inspectBlock}
979
1117
  onTravelTo={flyAlongRelation}
980
1118
  importedBy={importedBy}
981
1119
  namingId={namingId}
@@ -986,6 +1124,12 @@ function Explorer({
986
1124
  onCancelName={cancelBlockName}
987
1125
  userCreatedBlocks={userBlocks}
988
1126
  userCreatedIslands={userIslands}
1127
+ mapGraph={
1128
+ changePathsOnly && hasChangeSet ? changePathGraph : null
1129
+ }
1130
+ mapLayout={
1131
+ changePathsOnly && hasChangeSet ? changePathLayout : null
1132
+ }
989
1133
  />
990
1134
  </Canvas>
991
1135
  </div>
@@ -996,9 +1140,11 @@ function Explorer({
996
1140
  locked={locked}
997
1141
  selectedId={selectedId}
998
1142
  selectedTick={selectedTick}
1143
+ inspectTick={inspectTick}
999
1144
  selectedFolder={selectedFolder}
1000
1145
  landAt={landAt}
1001
1146
  aimedRelation={aimedRelation}
1147
+ aimedFileId={aimedFileId}
1002
1148
  currentFolder={currentFolder}
1003
1149
  intent={intent}
1004
1150
  intents={intents}
@@ -1012,6 +1158,9 @@ function Explorer({
1012
1158
  onToggleFollowLook={toggleFollowLook}
1013
1159
  importedBy={importedBy}
1014
1160
  onToggleImportedBy={toggleImportedBy}
1161
+ changePathsOnly={changePathsOnly}
1162
+ hasChangeSet={hasChangeSet}
1163
+ onToggleChangePathsOnly={toggleChangePathsOnly}
1015
1164
  naming={naming}
1016
1165
  namingIsland={Boolean(namingIslandId)}
1017
1166
  onCommitIslandName={(name) => {
@@ -1032,6 +1181,7 @@ function Explorer({
1032
1181
  onMapAddFile={placeBlockOnFolder}
1033
1182
  onMapAddFolder={placeIslandOnFolder}
1034
1183
  onInspectFile={inspectFile}
1184
+ onInspectBlock={inspectBlock}
1035
1185
  plannedIds={plannedIds}
1036
1186
  createdIds={plannedCreates}
1037
1187
  deletedIds={deletedIds}
@@ -52,6 +52,7 @@ export const emptyIntent: AgentIntent = {
52
52
  feature: null,
53
53
  steps: [],
54
54
  step: null,
55
+ stepByStep: true,
55
56
  files: [],
56
57
  creates: [],
57
58
  deletes: [],
@@ -61,6 +62,8 @@ export const emptyIntent: AgentIntent = {
61
62
  addedFunctions: [],
62
63
  addedVariables: [],
63
64
  addedImports: [],
65
+ changedFunctions: [],
66
+ changedVariables: [],
64
67
  reason: null,
65
68
  sessionId: null,
66
69
  diffId: null,
@@ -71,6 +74,7 @@ export const emptyIntent: AgentIntent = {
71
74
  preview: false,
72
75
  phase: null,
73
76
  working: false,
77
+ stalledWait: false,
74
78
  creationMode: false,
75
79
  canEnterBlueprint: false,
76
80
  blueprintSessionId: null,
@@ -89,6 +93,7 @@ function normalize(data: Partial<AgentIntent> | null | undefined): AgentIntent {
89
93
  feature: data?.feature ?? null,
90
94
  steps: Array.isArray(data?.steps) ? data.steps : [],
91
95
  step: typeof data?.step === 'number' ? data.step : null,
96
+ stepByStep: data?.stepByStep !== false,
92
97
  files: Array.isArray(data?.files) ? data.files : [],
93
98
  creates: Array.isArray(data?.creates) ? data.creates : [],
94
99
  deletes: Array.isArray(data?.deletes) ? data.deletes : [],
@@ -101,6 +106,8 @@ function normalize(data: Partial<AgentIntent> | null | undefined): AgentIntent {
101
106
  addedFunctions: normalizeSymbolAdditions(data?.addedFunctions),
102
107
  addedVariables: normalizeSymbolAdditions(data?.addedVariables),
103
108
  addedImports: normalizeImportAdditions(data?.addedImports),
109
+ changedFunctions: normalizeSymbolAdditions(data?.changedFunctions),
110
+ changedVariables: normalizeSymbolAdditions(data?.changedVariables),
104
111
  reason: data?.reason ?? null,
105
112
  sessionId: data?.sessionId ?? null,
106
113
  diffId: data?.diffId ?? null,
@@ -111,6 +118,7 @@ function normalize(data: Partial<AgentIntent> | null | undefined): AgentIntent {
111
118
  preview: Boolean(data?.preview),
112
119
  phase: data?.phase ?? null,
113
120
  working: Boolean(data?.working),
121
+ stalledWait: Boolean(data?.stalledWait),
114
122
  creationMode: Boolean(data?.creationMode),
115
123
  canEnterBlueprint: Boolean(data?.canEnterBlueprint),
116
124
  blueprintSessionId:
@@ -171,6 +179,7 @@ export async function performAgentAction(
171
179
  diffId?: string
172
180
  instruction?: string
173
181
  step?: number
182
+ stepByStep?: boolean
174
183
  userCreatedBlocks?: UserCreatedBlock[]
175
184
  userCreatedIslands?: UserCreatedIsland[]
176
185
  addedFunctions?: PatchSymbolAddition[]
@@ -471,6 +471,58 @@ button {
471
471
  white-space: nowrap;
472
472
  }
473
473
 
474
+ .hud-mode-switch {
475
+ display: flex;
476
+ align-items: center;
477
+ justify-content: space-between;
478
+ gap: 10px;
479
+ margin: 0 0 10px;
480
+ color: #b7c0ce;
481
+ font-size: 12px;
482
+ }
483
+
484
+ .hud-mode-hint {
485
+ margin: -4px 0 10px;
486
+ color: #8b95a5;
487
+ font-size: 11px;
488
+ line-height: 1.4;
489
+ }
490
+
491
+ .hud-switch {
492
+ position: relative;
493
+ flex: none;
494
+ width: 32px;
495
+ height: 18px;
496
+ padding: 0;
497
+ cursor: pointer;
498
+ pointer-events: auto;
499
+ border-radius: 999px;
500
+ background: #2a313c;
501
+ border: 1px solid #3a4250;
502
+ }
503
+
504
+ .hud-switch::after {
505
+ content: '';
506
+ position: absolute;
507
+ top: 2px;
508
+ left: 2px;
509
+ width: 12px;
510
+ height: 12px;
511
+ border-radius: 50%;
512
+ background: #8b95a5;
513
+ transition: transform 0.12s ease;
514
+ }
515
+
516
+ .hud-switch[aria-checked='true'] {
517
+ background: #1d4a3a;
518
+ border-color: #3dff78;
519
+ }
520
+
521
+ .hud-switch[aria-checked='true']::after {
522
+ transform: translateX(14px);
523
+ background: #3dff78;
524
+ }
525
+
474
526
  .hud-panel-info .hud-panel-chrome-title {
475
527
  color: #e7ebf2;
476
528
  font-size: 16px;
@@ -505,33 +557,73 @@ button {
505
557
  height: 220px;
506
558
  background: #000;
507
559
  touch-action: none;
560
+ user-select: none;
561
+ cursor: grab;
562
+ }
563
+
564
+ .hud-thumbnail-stage[data-panning='true'] {
565
+ cursor: grabbing;
508
566
  }
509
567
 
510
568
  .hud-thumbnail-stage canvas {
511
569
  display: block;
512
570
  }
513
571
 
572
+ .hud-thumbnail-hint {
573
+ position: absolute;
574
+ left: 8px;
575
+ bottom: 6px;
576
+ z-index: 2;
577
+ pointer-events: none;
578
+ color: rgba(231, 235, 242, 0.55);
579
+ font-size: 9px;
580
+ letter-spacing: 0.02em;
581
+ }
582
+
583
+ .hud-thumbnail-nav {
584
+ position: absolute;
585
+ right: 6px;
586
+ bottom: 6px;
587
+ z-index: 2;
588
+ display: flex;
589
+ gap: 4px;
590
+ }
591
+
592
+ .hud-thumbnail-nav .hud-button {
593
+ width: 24px;
594
+ height: 24px;
595
+ padding: 0;
596
+ color: #e7ebf2;
597
+ font-size: 14px;
598
+ line-height: 1;
599
+ background: rgba(18, 20, 24, 0.88);
600
+ }
601
+
514
602
  .thumbnail-block-label,
515
603
  .thumbnail-folder-label {
516
604
  pointer-events: none;
517
605
  user-select: none;
518
- white-space: nowrap;
606
+ overflow: hidden;
607
+ max-width: 72px;
519
608
  color: #ffffff;
520
- font-weight: 800;
521
- letter-spacing: 0.02em;
522
- line-height: 1.1;
609
+ font-weight: 600;
610
+ letter-spacing: 0.01em;
611
+ line-height: 1.15;
612
+ text-overflow: ellipsis;
613
+ white-space: nowrap;
523
614
  text-shadow:
524
- 0 0 4px #000,
615
+ 0 0 3px #000,
525
616
  0 1px 2px #000,
526
- 0 0 10px rgba(0, 0, 0, 0.85);
617
+ 0 0 8px rgba(0, 0, 0, 0.85);
527
618
  }
528
619
 
529
620
  .thumbnail-block-label {
530
- font-size: 13px;
621
+ font-size: 9px;
531
622
  }
532
623
 
533
624
  .thumbnail-folder-label {
534
- font-size: 15px;
625
+ font-size: 10px;
626
+ max-width: 96px;
535
627
  }
536
628
 
537
629
  .hud-panel-planned {
@@ -870,19 +962,19 @@ button {
870
962
  }
871
963
 
872
964
  .hud-file-add,
873
- .hud-panel-planned .hud-section-title.hud-section-title-add {
965
+ .hud-section-title.hud-section-title-add {
874
966
  color: #3dff78;
875
967
  font-weight: 600;
876
968
  }
877
969
 
878
970
  .hud-file-edit,
879
- .hud-panel-planned .hud-section-title.hud-section-title-edit {
971
+ .hud-section-title.hud-section-title-edit {
880
972
  color: #5cb8ff;
881
973
  font-weight: 600;
882
974
  }
883
975
 
884
976
  .hud-file-remove,
885
- .hud-panel-planned .hud-section-title.hud-section-title-remove {
977
+ .hud-section-title.hud-section-title-remove {
886
978
  color: #ff5a72;
887
979
  font-weight: 600;
888
980
  }
@@ -170,6 +170,51 @@ export function folderParent(folderPath: string) {
170
170
  return folderPath.includes('/') ? folderPath.split('/').slice(0, -1).join('/') : '.'
171
171
  }
172
172
 
173
+ export function changeRelatedFolderPaths(
174
+ graph: CodebaseGraph,
175
+ fileIds: Iterable<string>,
176
+ extraFolders: Iterable<string> = [],
177
+ ) {
178
+ const root = graph.root || '.'
179
+ const paths = new Set<string>([root])
180
+ const filesById = new Map(graph.files.map((file) => [file.id, file]))
181
+
182
+ const addAncestors = (start: string | null | undefined) => {
183
+ let current = start
184
+ while (current) {
185
+ paths.add(current)
186
+ current = folderParent(current)
187
+ }
188
+ }
189
+
190
+ for (const id of fileIds) {
191
+ const file = filesById.get(id)
192
+ addAncestors(file?.folder ?? folderOfFile(id))
193
+ }
194
+ for (const folder of extraFolders) addAncestors(folder)
195
+ return paths
196
+ }
197
+
198
+ export function filterGraphToChangePaths(
199
+ graph: CodebaseGraph,
200
+ fileIds: Iterable<string>,
201
+ extraFolders: Iterable<string> = [],
202
+ ): CodebaseGraph {
203
+ const keepFiles = new Set(fileIds)
204
+ const keepFolders = changeRelatedFolderPaths(graph, fileIds, extraFolders)
205
+ return {
206
+ ...graph,
207
+ files: graph.files.filter((file) => keepFiles.has(file.id)),
208
+ folders: graph.folders
209
+ .filter((folder) => keepFolders.has(folder.path))
210
+ .map((folder) => ({
211
+ ...folder,
212
+ files: folder.files.filter((id) => keepFiles.has(id)),
213
+ children: folder.children.filter((path) => keepFolders.has(path)),
214
+ })),
215
+ }
216
+ }
217
+
173
218
  function folderName(folderPath: string, rootName: string) {
174
219
  if (!folderPath || folderPath === '.') return rootName
175
220
  return folderPath.split('/').pop() ?? folderPath
@@ -434,7 +479,7 @@ export function layoutWorld(graph: CodebaseGraph): WorldLayout {
434
479
  export function folderAt(
435
480
  x: number,
436
481
  z: number,
437
- layout: WorldLayout,
482
+ layout: Pick<WorldLayout, 'folders'>,
438
483
  ): PlacedFolder | null {
439
484
  let current: PlacedFolder | null = null
440
485
  for (const folder of Object.values(layout.folders)) {
@@ -445,6 +490,25 @@ export function folderAt(
445
490
  return current
446
491
  }
447
492
 
493
+ export function mapPointOntoFolder(
494
+ x: number,
495
+ z: number,
496
+ fromLayout: Pick<WorldLayout, 'folders'>,
497
+ toLayout: Pick<WorldLayout, 'folders'>,
498
+ fallback: [number, number] | null = null,
499
+ ): [number, number] | null {
500
+ const folder = folderAt(x, z, fromLayout)
501
+ if (!folder) return fallback
502
+ const next = toLayout.folders[folder.path]
503
+ if (!next) return fallback
504
+ const relX = folder.width ? (x - folder.x) / folder.width : 0
505
+ const relZ = folder.depth ? (z - folder.z) / folder.depth : 0
506
+ return [
507
+ next.x + Math.min(Math.max(relX, -0.45), 0.45) * next.width,
508
+ next.z + Math.min(Math.max(relZ, 0), 1) * next.depth,
509
+ ]
510
+ }
511
+
448
512
  function distanceToSegment(
449
513
  px: number,
450
514
  pz: number,
@@ -1,10 +1,11 @@
1
1
  import { Suspense } from 'react'
2
2
  import { Text } from '@react-three/drei'
3
- import { CONFIG } from '../theme'
4
- import type { PlacedBridge } from '../types'
3
+ import { folderAt } from '../layout'
4
+ import { CONFIG, folderAisleColor } from '../theme'
5
+ import type { PlacedBridge, PlacedFolder } from '../types'
5
6
 
6
7
  const WALK_Y = 0.02
7
- const LAND_COLOR = '#2a3340'
8
+ const LAND_COLOR = 'hsl(210, 6%, 40%)'
8
9
  const SPAN_COLOR = '#232e3a'
9
10
  const CORNER_SIZE = CONFIG.bridgeWidth
10
11
 
@@ -22,6 +23,19 @@ const SIGN_COLOR = '#323a46'
22
23
 
23
24
  type BridgeProps = {
24
25
  bridge: PlacedBridge
26
+ folders?: Record<string, PlacedFolder>
27
+ }
28
+
29
+ function landColorAt(
30
+ x: number,
31
+ z: number,
32
+ folders: Record<string, PlacedFolder> | undefined,
33
+ ) {
34
+ if (!folders) return LAND_COLOR
35
+ const folder = folderAt(x, z, { folders })
36
+ if (!folder) return LAND_COLOR
37
+ if (folder.added) return '#23485a'
38
+ return folderAisleColor(folder.path)
25
39
  }
26
40
 
27
41
  type Segment = {
@@ -233,7 +247,7 @@ function Gate({
233
247
  )
234
248
  }
235
249
 
236
- export function Bridge({ bridge }: BridgeProps) {
250
+ export function Bridge({ bridge, folders }: BridgeProps) {
237
251
  const segments = segmentsFromPoints(bridge.points)
238
252
  const corners = cornersFromPoints(bridge.points)
239
253
  const start = bridge.points[0]
@@ -252,7 +266,11 @@ export function Bridge({ bridge }: BridgeProps) {
252
266
  z={piece.z}
253
267
  width={piece.width}
254
268
  depth={piece.depth}
255
- color={piece.color}
269
+ color={
270
+ piece.color === LAND_COLOR
271
+ ? landColorAt(piece.x, piece.z, folders)
272
+ : piece.color
273
+ }
256
274
  />
257
275
  ))}
258
276
 
@@ -29,6 +29,7 @@ type FileBlockProps = {
29
29
  mapMode?: boolean
30
30
  highlightMapChange?: boolean
31
31
  previewLabels?: boolean
32
+ labelVisible?: boolean
32
33
  onCommitName?: (name: string) => void
33
34
  onCancelName?: () => void
34
35
  }
@@ -53,6 +54,7 @@ export function FileBlock({
53
54
  mapMode = false,
54
55
  highlightMapChange = mapMode,
55
56
  previewLabels = false,
57
+ labelVisible = true,
56
58
  onCommitName,
57
59
  onCancelName,
58
60
  }: FileBlockProps) {
@@ -164,15 +166,17 @@ export function FileBlock({
164
166
  {showLabels && (
165
167
  <Suspense fallback={null}>
166
168
  {previewLabels ? (
167
- <Html
168
- position={[0, height / 2 + 0.55, 0]}
169
- center
170
- occlude={false}
171
- style={{ pointerEvents: 'none' }}
172
- zIndexRange={[20, 0]}
173
- >
174
- <div className="thumbnail-block-label">{label}</div>
175
- </Html>
169
+ labelVisible && (
170
+ <Html
171
+ position={[0, height / 2 + 0.4, 0]}
172
+ center
173
+ occlude={false}
174
+ style={{ pointerEvents: 'none' }}
175
+ zIndexRange={[20, 0]}
176
+ >
177
+ <div className="thumbnail-block-label">{label}</div>
178
+ </Html>
179
+ )
176
180
  ) : (
177
181
  <>
178
182
  <Billboard position={[0, height / 2 + (planned || highlight ? 0.55 : 0.38), 0]}>