@jkwd/inbase 0.1.6 → 0.1.8

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.
package/README.md CHANGED
@@ -7,6 +7,11 @@
7
7
 
8
8
  A first-person 3D map of a JavaScript or TypeScript codebase. Files become blocks, folders become walkable areas, and imports become lines in the air.
9
9
 
10
+ <p align="center">
11
+ <img src="docs/inbase-1.png" alt="First-person walk view of the codebase map" width="49%" />
12
+ <img src="docs/inbase-2.png" alt="Map view with 3D overlay" width="49%" />
13
+ </p>
14
+
10
15
  Install the package in a project, run `inbase init`, then `inbase run`. Cursor uses the installed skill so code changes go through the visual map.
11
16
 
12
17
  The npm package is `@jkwd/inbase` (npm blocks the unscoped name `inbase`). The command is still `inbase`.
@@ -62,6 +62,10 @@ export function discardInactiveDiffSessions(
62
62
  targetRoot?: string | null,
63
63
  waiterIds?: Iterable<string>,
64
64
  ): string[]
65
+ export function clearDiffSessions(
66
+ dataDir: string,
67
+ targetRoot?: string | null,
68
+ ): void
65
69
  export function listSessionIntents(
66
70
  dataDir: string,
67
71
  knownFileIds?: string[],
@@ -1154,6 +1154,22 @@ export function discardInactiveDiffSessions(
1154
1154
  return liveIds
1155
1155
  }
1156
1156
 
1157
+ export function clearDiffSessions(dataDir, targetRoot = null) {
1158
+ for (const sessionId of listStoredSessionIds(dataDir)) {
1159
+ discardStoredSession(dataDir, sessionId, targetRoot)
1160
+ }
1161
+ writeActiveSession(dataDir, null)
1162
+ writeBlueprintSession(dataDir, null)
1163
+
1164
+ const root = diffSessionsRoot(dataDir)
1165
+ fs.mkdirSync(root, { recursive: true })
1166
+ for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
1167
+ if (entry.name === '.gitkeep') continue
1168
+ fs.rmSync(path.join(root, entry.name), { recursive: true, force: true })
1169
+ }
1170
+ unstageDiffSessionArtifacts(dataDir, targetRoot)
1171
+ }
1172
+
1157
1173
  export function stopSession(dataDir, sessionId, targetRoot = null) {
1158
1174
  const safeId = assertSessionId(sessionId)
1159
1175
  writeStoppedMarker(dataDir, safeId)
@@ -1,7 +1,7 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
2
2
  import { Canvas } from '@react-three/fiber'
3
3
  import { emptyIntent, fetchAgentIntent, fetchAgentIntents, inspectTargetFile, performAgentAction, persistSessionBlueprint } from './agentIntent'
4
- import { fetchCodebase } from './codebase'
4
+ import { fetchCodebase, updateCodebase } from './codebase'
5
5
  import {
6
6
  layoutWorld,
7
7
  markCreatedFolders,
@@ -72,17 +72,35 @@ function intentSignature(intent: AgentIntent) {
72
72
  export default function App() {
73
73
  const [graph, setGraph] = useState<CodebaseGraph | null>(null)
74
74
  const [loadError, setLoadError] = useState<string | null>(null)
75
+ const [updatingModel, setUpdatingModel] = useState(false)
76
+ const updatingModelRef = useRef(false)
75
77
 
76
- const refreshGraph = useCallback(async () => {
77
- const next = await fetchCodebase()
78
+ const applyGraph = useCallback((next: CodebaseGraph | null, failed: string) => {
78
79
  if (next) {
79
80
  setGraph(next)
80
81
  setLoadError(null)
81
- return
82
+ return true
82
83
  }
83
- setLoadError((current) => current ?? 'Could not load the project map.')
84
+ setLoadError((current) => current ?? failed)
85
+ return false
84
86
  }, [])
85
87
 
88
+ const refreshGraph = useCallback(async () => {
89
+ applyGraph(await fetchCodebase(), 'Could not load the project map.')
90
+ }, [applyGraph])
91
+
92
+ const updateModel = useCallback(async () => {
93
+ if (updatingModelRef.current) return
94
+ updatingModelRef.current = true
95
+ setUpdatingModel(true)
96
+ try {
97
+ applyGraph(await updateCodebase(), 'Could not update the project map.')
98
+ } finally {
99
+ updatingModelRef.current = false
100
+ setUpdatingModel(false)
101
+ }
102
+ }, [applyGraph])
103
+
86
104
  useEffect(() => {
87
105
  void refreshGraph()
88
106
  }, [refreshGraph])
@@ -95,15 +113,26 @@ export default function App() {
95
113
  )
96
114
  }
97
115
 
98
- return <Explorer graph={graph} onRefreshGraph={refreshGraph} />
116
+ return (
117
+ <Explorer
118
+ graph={graph}
119
+ onRefreshGraph={refreshGraph}
120
+ onUpdateModel={updateModel}
121
+ updatingModel={updatingModel}
122
+ />
123
+ )
99
124
  }
100
125
 
101
126
  function Explorer({
102
127
  graph,
103
128
  onRefreshGraph,
129
+ onUpdateModel,
130
+ updatingModel,
104
131
  }: {
105
132
  graph: CodebaseGraph
106
133
  onRefreshGraph: () => Promise<void>
134
+ onUpdateModel: () => Promise<void>
135
+ updatingModel: boolean
107
136
  }) {
108
137
  const [intents, setIntents] = useState<AgentIntent[]>([])
109
138
  const [focusedSessionId, setFocusedSessionId] = useState<string | null>(null)
@@ -238,7 +267,6 @@ function Explorer({
238
267
  const [inspectTick, setInspectTick] = useState(0)
239
268
  const [flyTo, setFlyTo] = useState<FlyTo | null>(null)
240
269
  const [locked, setLocked] = useState(false)
241
- const [currentFolder, setCurrentFolder] = useState(graph.targetName)
242
270
  const [followLook, setFollowLook] = useState(false)
243
271
  const [importedBy, setImportedBy] = useState(false)
244
272
  const [changePathsOnly, setChangePathsOnly] = useState(false)
@@ -287,6 +315,7 @@ function Explorer({
287
315
  const toggleMap = useCallback(() => {
288
316
  if (mode === 'walk') {
289
317
  setLandAt(walkPos.current)
318
+ setFlyTo(null)
290
319
  setLocked(false)
291
320
  document.exitPointerLock()
292
321
  setMode('map')
@@ -322,6 +351,7 @@ function Explorer({
322
351
  (fileId: string, fly: boolean) => {
323
352
  const placed = layout.files[fileId]
324
353
  if (!placed) return
354
+ const from = walkPos.current
325
355
  const [x, z] = standInFront(placed)
326
356
  walkPos.current = [x, z]
327
357
  setAimedRelation(null)
@@ -330,6 +360,7 @@ function Explorer({
330
360
  fly
331
361
  ? {
332
362
  nonce: Date.now(),
363
+ from: [from[0], from[1]],
333
364
  lookAt: [placed.position[0], placed.position[1], placed.position[2]],
334
365
  }
335
366
  : null,
@@ -1099,7 +1130,6 @@ function Explorer({
1099
1130
  onSelect={selectFile}
1100
1131
  onSelectFolder={selectFolder}
1101
1132
  onLockedChange={setLocked}
1102
- onFolderChange={setCurrentFolder}
1103
1133
  onLand={landFromMap}
1104
1134
  onWalkPosition={rememberWalk}
1105
1135
  onContext={persistUserContext}
@@ -1145,7 +1175,6 @@ function Explorer({
1145
1175
  landAt={landAt}
1146
1176
  aimedRelation={aimedRelation}
1147
1177
  aimedFileId={aimedFileId}
1148
- currentFolder={currentFolder}
1149
1178
  intent={intent}
1150
1179
  intents={intents}
1151
1180
  focusedSessionId={focusedSessionId}
@@ -1156,6 +1185,8 @@ function Explorer({
1156
1185
  onWalk={openWalk}
1157
1186
  followLook={followLook}
1158
1187
  onToggleFollowLook={toggleFollowLook}
1188
+ onUpdateModel={onUpdateModel}
1189
+ updatingModel={updatingModel}
1159
1190
  importedBy={importedBy}
1160
1191
  onToggleImportedBy={toggleImportedBy}
1161
1192
  changePathsOnly={changePathsOnly}
@@ -1,14 +1,32 @@
1
1
  import type { CodebaseGraph } from './types'
2
2
 
3
+ function parseCodebase(data: unknown): CodebaseGraph | null {
4
+ if (
5
+ !data ||
6
+ typeof data !== 'object' ||
7
+ !Array.isArray((data as CodebaseGraph).files) ||
8
+ !Array.isArray((data as CodebaseGraph).folders)
9
+ ) {
10
+ return null
11
+ }
12
+ return data as CodebaseGraph
13
+ }
14
+
3
15
  export async function fetchCodebase(): Promise<CodebaseGraph | null> {
4
16
  try {
5
17
  const response = await fetch(`/api/codebase?t=${Date.now()}`)
6
18
  if (!response.ok) return null
7
- const data = (await response.json()) as CodebaseGraph
8
- if (!data || !Array.isArray(data.files) || !Array.isArray(data.folders)) {
9
- return null
10
- }
11
- return data
19
+ return parseCodebase(await response.json())
20
+ } catch {
21
+ return null
22
+ }
23
+ }
24
+
25
+ export async function updateCodebase(): Promise<CodebaseGraph | null> {
26
+ try {
27
+ const response = await fetch('/api/codebase', { method: 'POST' })
28
+ if (!response.ok) return null
29
+ return parseCodebase(await response.json())
12
30
  } catch {
13
31
  return null
14
32
  }
@@ -174,11 +174,22 @@ button {
174
174
  padding: 8px 12px;
175
175
  }
176
176
 
177
+ .hud-button:disabled {
178
+ opacity: 0.55;
179
+ cursor: default;
180
+ }
181
+
177
182
  .hud-button[data-active='true'] {
178
183
  border-color: #5d9ec4;
179
184
  color: #d7eef8;
180
185
  }
181
186
 
187
+ .hud-bottom-actions {
188
+ display: flex;
189
+ gap: 8px;
190
+ flex: none;
191
+ }
192
+
182
193
  .hud-chip,
183
194
  .hud-panel {
184
195
  background: rgba(25, 28, 34, 0.86);
@@ -240,6 +251,51 @@ button {
240
251
  background: rgba(25, 28, 34, 0.94);
241
252
  }
242
253
 
254
+ .map-you-are-here {
255
+ display: grid;
256
+ justify-items: center;
257
+ transform: translateY(-10px);
258
+ }
259
+
260
+ .map-you-are-here-pin {
261
+ width: 18px;
262
+ height: 18px;
263
+ background: #e8c36a;
264
+ border: 2px solid #fff8e4;
265
+ border-radius: 50% 50% 50% 0;
266
+ box-shadow:
267
+ 0 0 0 0 rgba(232, 195, 106, 0.7),
268
+ 0 2px 10px rgba(0, 0, 0, 0.55);
269
+ transform: rotate(-45deg);
270
+ animation: map-you-are-here-pulse 1.7s ease-out infinite;
271
+ }
272
+
273
+ .map-you-are-here-pin::after {
274
+ content: '';
275
+ position: absolute;
276
+ inset: 4px;
277
+ border-radius: 50%;
278
+ background: #1a1408;
279
+ }
280
+
281
+ @keyframes map-you-are-here-pulse {
282
+ 0% {
283
+ box-shadow:
284
+ 0 0 0 0 rgba(232, 195, 106, 0.75),
285
+ 0 2px 10px rgba(0, 0, 0, 0.55);
286
+ }
287
+ 70% {
288
+ box-shadow:
289
+ 0 0 0 12px rgba(232, 195, 106, 0),
290
+ 0 2px 10px rgba(0, 0, 0, 0.55);
291
+ }
292
+ 100% {
293
+ box-shadow:
294
+ 0 0 0 0 rgba(232, 195, 106, 0),
295
+ 0 2px 10px rgba(0, 0, 0, 0.55);
296
+ }
297
+ }
298
+
243
299
  .hud-bottom {
244
300
  position: absolute;
245
301
  left: 0;
@@ -256,12 +312,119 @@ button {
256
312
  pointer-events: auto;
257
313
  }
258
314
 
259
- .hud-hints {
315
+ .hud-instructions-overlay {
316
+ position: absolute;
317
+ inset: 0;
318
+ z-index: 20;
319
+ display: grid;
320
+ place-items: center;
321
+ background: rgba(25, 28, 34, 0.72);
322
+ pointer-events: auto;
323
+ }
324
+
325
+ .hud-instructions-card {
326
+ width: min(520px, calc(100% - 32px));
327
+ max-height: min(80dvh, 720px);
328
+ overflow: auto;
329
+ padding: 24px;
330
+ background: var(--vc-editor);
331
+ border: 1px solid var(--vc-border);
332
+ }
333
+
334
+ .hud-instructions-header {
335
+ display: flex;
336
+ align-items: center;
337
+ justify-content: space-between;
338
+ gap: 12px;
339
+ margin-bottom: 16px;
340
+ }
341
+
342
+ .hud-instructions-header h1 {
343
+ margin: 0;
344
+ font-size: 20px;
345
+ font-weight: 600;
346
+ }
347
+
348
+ .hud-instructions-sections {
349
+ display: flex;
350
+ flex-direction: column;
351
+ gap: 22px;
352
+ }
353
+
354
+ .hud-instructions-section h2 {
260
355
  display: flex;
356
+ align-items: center;
261
357
  gap: 8px;
262
- flex-wrap: wrap;
263
- color: #9aa3b2;
358
+ margin: 0 0 10px;
359
+ color: #8b95a5;
264
360
  font-size: 12px;
361
+ font-weight: 600;
362
+ letter-spacing: 0.08em;
363
+ text-transform: uppercase;
364
+ }
365
+
366
+ .hud-instructions-section[data-current='true'] h2 {
367
+ color: #d7eef8;
368
+ }
369
+
370
+ .hud-instructions-current {
371
+ padding: 2px 6px;
372
+ border: 1px solid #5d9ec4;
373
+ border-radius: 3px;
374
+ color: #d7eef8;
375
+ font-size: 10px;
376
+ letter-spacing: 0.04em;
377
+ }
378
+
379
+ .hud-instructions-list {
380
+ margin: 0;
381
+ padding: 0;
382
+ list-style: none;
383
+ display: flex;
384
+ flex-direction: column;
385
+ gap: 8px;
386
+ }
387
+
388
+ .hud-instructions-list .hud-instruction {
389
+ display: grid;
390
+ grid-template-columns: minmax(7.5rem, auto) 1fr;
391
+ align-items: center;
392
+ gap: 12px;
393
+ margin-top: 0;
394
+ color: #b7c0ce;
395
+ font-size: 14px;
396
+ line-height: 1.45;
397
+ }
398
+
399
+ .hud-instructions-list .hud-instruction:not(:has(.hud-instruction-keys)) {
400
+ grid-template-columns: 1fr;
401
+ }
402
+
403
+ .hud-instruction-keys {
404
+ display: flex;
405
+ flex-wrap: wrap;
406
+ justify-content: flex-end;
407
+ gap: 4px;
408
+ }
409
+
410
+ .hud-instructions-card kbd {
411
+ display: inline-flex;
412
+ align-items: center;
413
+ justify-content: center;
414
+ min-width: 1.6rem;
415
+ height: 1.6rem;
416
+ padding: 0 7px;
417
+ border: 1px solid #3a4250;
418
+ border-bottom-width: 2px;
419
+ border-radius: 4px;
420
+ background: var(--vc-surface);
421
+ color: #e7ebf2;
422
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
423
+ font-size: 11px;
424
+ font-weight: 600;
425
+ letter-spacing: 0.02em;
426
+ line-height: 1;
427
+ white-space: nowrap;
265
428
  }
266
429
 
267
430
  .hud-icon-row {
@@ -419,6 +582,8 @@ button {
419
582
 
420
583
  .hud-thumbnail {
421
584
  position: fixed;
585
+ display: flex;
586
+ flex-direction: column;
422
587
  right: 20px;
423
588
  bottom: 72px;
424
589
  left: auto;
@@ -431,6 +596,19 @@ button {
431
596
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
432
597
  }
433
598
 
599
+ .hud-right-stack .hud-thumbnail[data-maximized='true'],
600
+ .hud-thumbnail[data-maximized='true'] {
601
+ position: fixed;
602
+ top: 68px;
603
+ right: 20px;
604
+ bottom: 72px;
605
+ left: 20px;
606
+ z-index: 40;
607
+ width: auto;
608
+ max-width: none;
609
+ margin-top: 0;
610
+ }
611
+
434
612
  .hud-thumbnail-bar {
435
613
  display: flex;
436
614
  align-items: center;
@@ -555,13 +733,21 @@ button {
555
733
  position: relative;
556
734
  width: 100%;
557
735
  height: 220px;
736
+ min-height: 0;
737
+ flex: none;
558
738
  background: #000;
559
739
  touch-action: none;
560
740
  user-select: none;
561
741
  cursor: grab;
562
742
  }
563
743
 
564
- .hud-thumbnail-stage[data-panning='true'] {
744
+ .hud-thumbnail[data-maximized='true'] .hud-thumbnail-stage {
745
+ height: auto;
746
+ flex: 1 1 auto;
747
+ }
748
+
749
+ .hud-thumbnail-stage[data-panning='true'],
750
+ .hud-thumbnail-stage[data-orbiting='true'] {
565
751
  cursor: grabbing;
566
752
  }
567
753
 
@@ -183,18 +183,63 @@ function segmentDir(from: [number, number], to: [number, number]): [number, numb
183
183
  return [dx / length, dz / length]
184
184
  }
185
185
 
186
+ function signFontSize(label: string, lintelW: number) {
187
+ return Math.min(0.42, (lintelW - 0.55) / Math.max(label.length * 0.62, 3))
188
+ }
189
+
190
+ function GateSign({
191
+ z,
192
+ rotY,
193
+ signY,
194
+ lintelW,
195
+ label,
196
+ }: {
197
+ z: number
198
+ rotY: number
199
+ signY: number
200
+ lintelW: number
201
+ label: string
202
+ }) {
203
+ return (
204
+ <group>
205
+ <mesh position={[0, signY, z]}>
206
+ <boxGeometry args={[lintelW - 0.18, SIGN_H, SIGN_D]} />
207
+ <meshLambertMaterial color={SIGN_COLOR} />
208
+ </mesh>
209
+ <Suspense fallback={null}>
210
+ <Text
211
+ position={[0, signY, z + Math.sign(z) * (SIGN_D / 2 + 0.01)]}
212
+ rotation={[0, rotY, 0]}
213
+ fontSize={signFontSize(label, lintelW)}
214
+ color="#e7ebf2"
215
+ anchorX="center"
216
+ anchorY="middle"
217
+ maxWidth={lintelW - 0.4}
218
+ overflowWrap="break-word"
219
+ outlineWidth={0.02}
220
+ outlineColor="#07080b"
221
+ >
222
+ {label}
223
+ </Text>
224
+ </Suspense>
225
+ </group>
226
+ )
227
+ }
228
+
186
229
  function Gate({
187
230
  x,
188
231
  z,
189
232
  faceX,
190
233
  faceZ,
191
234
  label,
235
+ backLabel,
192
236
  }: {
193
237
  x: number
194
238
  z: number
195
239
  faceX: number
196
240
  faceZ: number
197
241
  label: string
242
+ backLabel: string
198
243
  }) {
199
244
  const rotationY = Math.atan2(faceX, faceZ)
200
245
  const postX = CONFIG.bridgeWidth / 2 + POST_W / 2
@@ -204,7 +249,6 @@ function Gate({
204
249
  const capY = POST_H + LINTEL_H + CAP_H / 2
205
250
  const signY = POST_H + LINTEL_H / 2
206
251
  const signZ = POST_D / 2 + SIGN_D / 2 + 0.01
207
- const fontSize = Math.min(0.42, (lintelW - 0.55) / Math.max(label.length * 0.62, 3))
208
252
 
209
253
  return (
210
254
  <group position={[x, 0, z]} rotation={[0, rotationY, 0]}>
@@ -224,25 +268,16 @@ function Gate({
224
268
  <boxGeometry args={[capW, CAP_H, POST_D + 0.08]} />
225
269
  <meshLambertMaterial color={CAP_COLOR} />
226
270
  </mesh>
227
- <mesh position={[0, signY, signZ]}>
228
- <boxGeometry args={[lintelW - 0.18, SIGN_H, SIGN_D]} />
229
- <meshLambertMaterial color={SIGN_COLOR} />
230
- </mesh>
231
- <Suspense fallback={null}>
232
- <Text
233
- position={[0, signY, signZ + SIGN_D / 2 + 0.01]}
234
- fontSize={fontSize}
235
- color="#e7ebf2"
236
- anchorX="center"
237
- anchorY="middle"
238
- maxWidth={lintelW - 0.4}
239
- overflowWrap="break-word"
240
- outlineWidth={0.02}
241
- outlineColor="#07080b"
242
- >
243
- {label}
244
- </Text>
245
- </Suspense>
271
+ <GateSign z={signZ} rotY={0} signY={signY} lintelW={lintelW} label={label} />
272
+ {backLabel ? (
273
+ <GateSign
274
+ z={-signZ}
275
+ rotY={Math.PI}
276
+ signY={signY}
277
+ lintelW={lintelW}
278
+ label={backLabel}
279
+ />
280
+ ) : null}
246
281
  </group>
247
282
  )
248
283
  }
@@ -292,6 +327,7 @@ export function Bridge({ bridge, folders }: BridgeProps) {
292
327
  faceX={-startDir[0]}
293
328
  faceZ={-startDir[1]}
294
329
  label={bridge.label}
330
+ backLabel={bridge.fromLabel}
295
331
  />
296
332
  )}
297
333
  {end && endDir && (
@@ -301,6 +337,7 @@ export function Bridge({ bridge, folders }: BridgeProps) {
301
337
  faceX={endDir[0]}
302
338
  faceZ={endDir[1]}
303
339
  label={bridge.fromLabel}
340
+ backLabel={bridge.label}
304
341
  />
305
342
  )}
306
343
  </group>
@@ -1,5 +1,5 @@
1
1
  import { useEffect, useLayoutEffect, useMemo, useRef } from 'react'
2
- import { useThree } from '@react-three/fiber'
2
+ import { useFrame, useThree } from '@react-three/fiber'
3
3
  import { Html, MapControls, OrthographicCamera } from '@react-three/drei'
4
4
  import * as THREE from 'three'
5
5
  import { folderAt, worldBounds } from '../layout'
@@ -280,7 +280,7 @@ export function MapView({
280
280
  key={folder.path}
281
281
  position={[folder.x, 14, folder.z + 1.35]}
282
282
  center
283
- zIndexRange={[1, 0]}
283
+ zIndexRange={[80, 50]}
284
284
  style={{ pointerEvents: 'none' }}
285
285
  >
286
286
  <div
@@ -307,7 +307,7 @@ export function MapView({
307
307
  </div>
308
308
  </Html>
309
309
  ))}
310
- {marker && <LandMarker marker={marker} />}
310
+ {enabled && marker && <LandMarker marker={marker} />}
311
311
  </>
312
312
  )
313
313
  }
@@ -323,12 +323,41 @@ function folderKindLabel(
323
323
  }
324
324
 
325
325
  function LandMarker({ marker }: { marker: [number, number] }) {
326
+ const camera = useThree((state) => state.camera)
327
+ const ring = useRef<THREE.Group>(null)
328
+
329
+ useFrame(() => {
330
+ if (!(camera instanceof THREE.OrthographicCamera) || !ring.current) return
331
+ const size = THREE.MathUtils.clamp(16 / Math.max(camera.zoom, 0.04), 2.4, 22)
332
+ ring.current.scale.setScalar(size)
333
+ })
334
+
326
335
  return (
327
- <group position={[marker[0], 0.2, marker[1]]}>
328
- <mesh rotation={[-Math.PI / 2, 0, 0]}>
329
- <ringGeometry args={[0.55, 0.85, 24]} />
330
- <meshBasicMaterial color="#e8c36a" side={THREE.DoubleSide} />
331
- </mesh>
336
+ <group position={[marker[0], 0.35, marker[1]]}>
337
+ <group ref={ring}>
338
+ <mesh rotation={[-Math.PI / 2, 0, 0]}>
339
+ <circleGeometry args={[0.62, 32]} />
340
+ <meshBasicMaterial color="#e8c36a" transparent opacity={0.28} />
341
+ </mesh>
342
+ <mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, 0.02, 0]}>
343
+ <ringGeometry args={[0.5, 0.72, 32]} />
344
+ <meshBasicMaterial color="#e8c36a" side={THREE.DoubleSide} />
345
+ </mesh>
346
+ <mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, 0.04, 0]}>
347
+ <circleGeometry args={[0.18, 20]} />
348
+ <meshBasicMaterial color="#fff6d4" />
349
+ </mesh>
350
+ </group>
351
+ <Html
352
+ center
353
+ zIndexRange={[20, 10]}
354
+ style={{ pointerEvents: 'none' }}
355
+ position={[0, 2.8, 0]}
356
+ >
357
+ <div className="map-you-are-here" role="img" aria-label="You are here">
358
+ <span className="map-you-are-here-pin" aria-hidden="true" />
359
+ </div>
360
+ </Html>
332
361
  </group>
333
362
  )
334
363
  }