@jkwd/inbase 0.1.1 → 0.1.4

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.
@@ -21,6 +21,8 @@ body,
21
21
  .stage {
22
22
  position: absolute;
23
23
  inset: 0;
24
+ z-index: 0;
25
+ background: #000;
24
26
  }
25
27
 
26
28
  * {
@@ -35,7 +37,7 @@ button {
35
37
  position: fixed;
36
38
  inset: 0;
37
39
  pointer-events: none;
38
- z-index: 2;
40
+ z-index: 10;
39
41
  }
40
42
 
41
43
  .hud-top,
@@ -243,6 +245,7 @@ button {
243
245
  left: 0;
244
246
  right: 0;
245
247
  bottom: 0;
248
+ z-index: 3;
246
249
  display: flex;
247
250
  align-items: center;
248
251
  justify-content: space-between;
@@ -306,11 +309,15 @@ button {
306
309
  position: absolute;
307
310
  top: 68px;
308
311
  right: 20px;
312
+ z-index: 4;
309
313
  width: min(320px, calc(100% - 40px));
310
- max-height: calc(100dvh - 132px);
311
- overflow: auto;
314
+ height: auto;
315
+ max-height: calc(100dvh - 168px);
316
+ overflow-x: hidden;
317
+ overflow-y: auto;
312
318
  overscroll-behavior: contain;
313
- padding: 16px;
319
+ padding: 16px 16px 20px;
320
+ background: var(--vc-chrome);
314
321
  }
315
322
 
316
323
  .hud-panel-info {
@@ -352,18 +359,31 @@ button {
352
359
 
353
360
  .hud-steps {
354
361
  margin: 10px 0 0;
355
- padding-left: 18px;
362
+ padding: 0;
363
+ list-style: none;
356
364
  color: #8b95a5;
357
365
  }
358
366
 
359
367
  .hud-steps li {
360
- margin: 0 0 6px;
368
+ display: grid;
369
+ grid-template-columns: 1.6em minmax(0, 1fr);
370
+ align-items: start;
371
+ justify-content: start;
372
+ gap: 8px;
373
+ margin: 0 0 8px;
374
+ }
375
+
376
+ .hud-step-index {
377
+ font-variant-numeric: tabular-nums;
378
+ line-height: 1.4;
361
379
  }
362
380
 
363
381
  .hud-step-main {
364
- display: inline-flex;
382
+ display: flex;
383
+ flex-wrap: wrap;
365
384
  align-items: baseline;
366
385
  gap: 8px;
386
+ min-width: 0;
367
387
  }
368
388
 
369
389
  .hud-steps li[data-next='true'] {
@@ -488,6 +508,39 @@ button {
488
508
  opacity: 0.35;
489
509
  }
490
510
 
511
+ .hud-fold {
512
+ margin-top: 12px;
513
+ }
514
+
515
+ .hud-fold-summary {
516
+ cursor: pointer;
517
+ list-style: none;
518
+ display: flex;
519
+ align-items: center;
520
+ gap: 8px;
521
+ user-select: none;
522
+ }
523
+
524
+ .hud-fold-summary::-webkit-details-marker,
525
+ .hud-fold-summary::marker {
526
+ display: none;
527
+ content: '';
528
+ }
529
+
530
+ .hud-fold-summary::before {
531
+ content: '';
532
+ width: 0;
533
+ height: 0;
534
+ border-top: 4px solid transparent;
535
+ border-bottom: 4px solid transparent;
536
+ border-left: 6px solid currentColor;
537
+ transition: transform 0.12s ease;
538
+ }
539
+
540
+ .hud-fold[open] > .hud-fold-summary::before {
541
+ transform: rotate(90deg);
542
+ }
543
+
491
544
  .hud-button-approve {
492
545
  border-color: #9ad8ff;
493
546
  color: #d7eef8;
@@ -514,12 +567,17 @@ button {
514
567
  word-break: break-all;
515
568
  }
516
569
 
570
+ .hud-inspect {
571
+ margin: 10px 0 0;
572
+ width: 100%;
573
+ }
574
+
517
575
  .hud-panel ul {
518
576
  margin: 8px 0 0;
519
577
  padding-left: 18px;
520
578
  }
521
579
 
522
- .hud-panel li {
580
+ .hud-panel ul li {
523
581
  margin: 0 0 4px;
524
582
  display: flex;
525
583
  align-items: center;
@@ -547,6 +605,21 @@ button {
547
605
  border-color: #8a4a4a;
548
606
  }
549
607
 
608
+ .hud-item-inspect {
609
+ flex: none;
610
+ padding: 2px 6px;
611
+ color: #d7eef8;
612
+ background: transparent;
613
+ border: 1px solid #3a4250;
614
+ cursor: pointer;
615
+ font-size: 11px;
616
+ }
617
+
618
+ .hud-item-inspect:hover,
619
+ .hud-item-inspect:focus-visible {
620
+ border-color: #9ad8ff;
621
+ }
622
+
550
623
  .hud-add-row {
551
624
  display: flex;
552
625
  gap: 6px;
@@ -95,7 +95,7 @@ export function FileBlock({
95
95
  onCancelName,
96
96
  }: FileBlockProps) {
97
97
  const [width, height, depth] = placed.size
98
- const highlight = changeKind ? CHANGE_HIGHLIGHT[changeKind] : null
98
+ const highlight = changeKind && mapMode ? CHANGE_HIGHLIGHT[changeKind] : null
99
99
  const color = added || file.userCreated ? '#7ec8e8' : fileColor(file.language)
100
100
  const muted = dimColor(color, 0.32)
101
101
  const walkSelected = selected && !mapMode
@@ -8,6 +8,7 @@ type FolderAreaProps = {
8
8
  folder: PlacedFolder
9
9
  naming?: boolean
10
10
  selected?: boolean
11
+ mapMode?: boolean
11
12
  highlightKind?: ChangeKind | null
12
13
  }
13
14
 
@@ -15,10 +16,12 @@ export function FolderArea({
15
16
  folder,
16
17
  naming = false,
17
18
  selected = false,
19
+ mapMode = false,
18
20
  highlightKind = null,
19
21
  }: FolderAreaProps) {
20
22
  const added = Boolean(folder.added)
21
- const highlight = highlightKind ? CHANGE_HIGHLIGHT[highlightKind] : null
23
+ const highlight =
24
+ mapMode && highlightKind ? CHANGE_HIGHLIGHT[highlightKind] : null
22
25
  const addedOnly = added && !highlight
23
26
  const outline = highlight ? highlight.color : addedOnly ? '#7ec8e8' : null
24
27
  const color = highlight
@@ -48,7 +51,7 @@ export function FolderArea({
48
51
  <planeGeometry args={[folder.width, folder.depth]} />
49
52
  <meshBasicMaterial color={color} />
50
53
  </mesh>
51
- {selected && (
54
+ {selected && mapMode && (
52
55
  <MapSelectBorder
53
56
  width={folder.width}
54
57
  depth={folder.depth}
@@ -34,33 +34,46 @@ export function MapView({
34
34
  const bounds = useMemo(() => worldBounds(layout), [layout])
35
35
  const drag = useRef({ x: 0, y: 0, moved: false, active: false })
36
36
  const sized = size.width > 16 && size.height > 16
37
+ const hudReserve = 88
38
+ const topReserve = 28
39
+ const viewWidth = Math.max(size.width, 1)
40
+ const viewHeight = Math.max(size.height - hudReserve - topReserve, 1)
37
41
 
38
42
  const fitZoom = sized
39
43
  ? Math.min(
40
- size.width / Math.max(bounds.width + 24, 1),
41
- size.height / Math.max(bounds.depth + 24, 1),
44
+ viewWidth / Math.max(bounds.width + 36, 1),
45
+ viewHeight / Math.max(bounds.depth + 36, 1),
42
46
  )
43
47
  : 8
44
- const zoom = Math.max(fitZoom * 0.92, 1)
48
+ const zoom = Math.max(fitZoom * 0.92, 0.08)
49
+ // Ortho up is -Z, so +Z is down the screen. Shift the view so the map sits
50
+ // above the bottom HUD instead of centering under it.
51
+ const cz = bounds.cz + hudReserve / 2 / zoom
45
52
 
46
53
  useLayoutEffect(() => {
47
54
  if (!enabled || !(camera instanceof THREE.OrthographicCamera)) return
48
55
  camera.up.set(0, 0, -1)
49
- camera.position.set(bounds.cx, 120, bounds.cz)
50
- camera.lookAt(bounds.cx, 0, bounds.cz)
56
+ camera.position.set(bounds.cx, 120, cz)
57
+ camera.lookAt(bounds.cx, 0, cz)
51
58
  camera.near = 1
52
59
  camera.far = 2000
53
60
  camera.zoom = zoom
54
61
  camera.updateProjectionMatrix()
55
- }, [bounds.cx, bounds.cz, camera, enabled, zoom])
62
+ }, [bounds.cx, camera, cz, enabled, zoom])
56
63
 
57
64
  useEffect(() => {
58
65
  if (!enabled) return
59
66
  const element = gl.domElement
60
67
  element.style.cursor = 'grab'
61
68
 
69
+ const isWalkClick = (event: PointerEvent | MouseEvent) =>
70
+ event.shiftKey || event.ctrlKey || event.metaKey
71
+
72
+ const isWalkButton = (event: PointerEvent) =>
73
+ event.button === 0 || (event.ctrlKey && event.button === 2)
74
+
62
75
  const onDown = (event: PointerEvent) => {
63
- if (event.button !== 0) return
76
+ if (!isWalkButton(event)) return
64
77
  drag.current = { x: event.clientX, y: event.clientY, moved: false, active: true }
65
78
  element.style.cursor = 'grabbing'
66
79
  }
@@ -86,11 +99,43 @@ export function MapView({
86
99
  return { raycaster, relationHit, fileHit }
87
100
  }
88
101
 
102
+ const landAt = (x: number, z: number) => {
103
+ const lock = element.requestPointerLock()
104
+ if (lock && typeof lock.catch === 'function') {
105
+ void lock.catch(() => {})
106
+ }
107
+ onLand(x, z)
108
+ }
109
+
110
+ const landAtPointer = (clientX: number, clientY: number, allowIsland: boolean) => {
111
+ const pick = pickAt(clientX, clientY)
112
+ if (!pick) return false
113
+ const { raycaster, relationHit, fileHit } = pick
114
+
115
+ const hit = new THREE.Vector3()
116
+ const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0)
117
+ if (!raycaster.ray.intersectPlane(plane, hit)) return false
118
+
119
+ const folder = folderAt(hit.x, hit.z, layout)
120
+ if (allowIsland && folder) {
121
+ landAt(hit.x, hit.z)
122
+ return true
123
+ }
124
+ if (relationHit || fileHit) return false
125
+
126
+ landAt(hit.x, hit.z)
127
+ return true
128
+ }
129
+
89
130
  const onUp = (event: PointerEvent) => {
90
131
  element.style.cursor = 'grab'
91
132
  const startedOnCanvas = drag.current.active
92
133
  drag.current.active = false
93
- if (!startedOnCanvas || event.button !== 0 || drag.current.moved) return
134
+ if (!startedOnCanvas || !isWalkButton(event) || drag.current.moved) return
135
+
136
+ if (isWalkClick(event) && landAtPointer(event.clientX, event.clientY, true)) {
137
+ return
138
+ }
94
139
 
95
140
  const pick = pickAt(event.clientX, event.clientY)
96
141
  if (!pick) return
@@ -123,34 +168,20 @@ export function MapView({
123
168
  onSelectFolder(null)
124
169
  }
125
170
 
126
- const onDblClick = (event: MouseEvent) => {
127
- if (event.button !== 0 || drag.current.moved) return
128
- const pick = pickAt(event.clientX, event.clientY)
129
- if (!pick) return
130
- const { raycaster, relationHit, fileHit } = pick
131
- if (relationHit || fileHit) return
132
-
133
- const hit = new THREE.Vector3()
134
- const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0)
135
- if (raycaster.ray.intersectPlane(plane, hit)) {
136
- const lock = element.requestPointerLock()
137
- if (lock && typeof lock.catch === 'function') {
138
- void lock.catch(() => {})
139
- }
140
- onLand(hit.x, hit.z)
141
- }
171
+ const onContextMenu = (event: MouseEvent) => {
172
+ if (event.ctrlKey) event.preventDefault()
142
173
  }
143
174
 
144
175
  element.addEventListener('pointerdown', onDown)
145
176
  window.addEventListener('pointermove', onMove)
146
177
  window.addEventListener('pointerup', onUp)
147
- element.addEventListener('dblclick', onDblClick)
178
+ element.addEventListener('contextmenu', onContextMenu)
148
179
  return () => {
149
180
  element.style.cursor = ''
150
181
  element.removeEventListener('pointerdown', onDown)
151
182
  window.removeEventListener('pointermove', onMove)
152
183
  window.removeEventListener('pointerup', onUp)
153
- element.removeEventListener('dblclick', onDblClick)
184
+ element.removeEventListener('contextmenu', onContextMenu)
154
185
  }
155
186
  }, [
156
187
  camera,
@@ -169,7 +200,7 @@ export function MapView({
169
200
  {enabled && (
170
201
  <OrthographicCamera
171
202
  makeDefault
172
- position={[bounds.cx, 120, bounds.cz]}
203
+ position={[bounds.cx, 120, cz]}
173
204
  zoom={zoom}
174
205
  near={1}
175
206
  far={2000}
@@ -183,9 +214,9 @@ export function MapView({
183
214
  dampingFactor={0.12}
184
215
  screenSpacePanning
185
216
  zoomSpeed={1.15}
186
- minZoom={Math.max(fitZoom * 0.45, 1)}
217
+ minZoom={Math.max(fitZoom * 0.35, 0.05)}
187
218
  maxZoom={Math.max(fitZoom * 10, 20)}
188
- target={[bounds.cx, 0, bounds.cz]}
219
+ target={[bounds.cx, 0, cz]}
189
220
  />
190
221
  )}
191
222
  {enabled &&
@@ -194,6 +225,7 @@ export function MapView({
194
225
  key={folder.path}
195
226
  position={[folder.x, 14, folder.z + 1.35]}
196
227
  center
228
+ zIndexRange={[1, 0]}
197
229
  style={{ pointerEvents: 'none' }}
198
230
  >
199
231
  <div
@@ -9,7 +9,7 @@ import { UserContextTracker } from './UserContextTracker'
9
9
  import { BlockPlacer } from './BlockPlacer'
10
10
  import { IslandPlacer } from './IslandPlacer'
11
11
  import { folderOfFile, filesImporting } from '../layout'
12
- import { EDITOR_GREY } from '../theme'
12
+ import { WORLD_VOID } from '../theme'
13
13
  import type { ChangeKind } from '../theme'
14
14
  import type {
15
15
  CodebaseGraph,
@@ -161,18 +161,14 @@ export function World({
161
161
 
162
162
  return (
163
163
  <>
164
- <color attach="background" args={[EDITOR_GREY.editor]} />
165
- {!mapping && <fog attach="fog" args={[EDITOR_GREY.editor, 38, 160]} />}
164
+ <color attach="background" args={[WORLD_VOID]} />
165
+ {!mapping && <fog attach="fog" args={[WORLD_VOID, 38, 160]} />}
166
166
  <hemisphereLight args={['#d7e2ee', '#2a3038', mapping ? 1.1 : 0.85]} />
167
167
  <directionalLight
168
168
  position={mapping ? [8, 60, 8] : [12, 22, 8]}
169
169
  intensity={mapping ? 1.35 : 0.55}
170
170
  />
171
171
  <ambientLight intensity={mapping ? 0.7 : 0.42} />
172
- <mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, -0.06, 40]}>
173
- <planeGeometry args={[400, 400]} />
174
- <meshBasicMaterial color={EDITOR_GREY.chrome} />
175
- </mesh>
176
172
  <MapView
177
173
  layout={layout}
178
174
  enabled={mapping}
@@ -191,6 +187,7 @@ export function World({
191
187
  folder={folder}
192
188
  naming={folder.path === namingIslandId}
193
189
  selected={folder.path === selectedFolder}
190
+ mapMode={mapping}
194
191
  highlightKind={highlightedFolders[folder.path] ?? null}
195
192
  />
196
193
  ))}
@@ -25,6 +25,9 @@ export const EDITOR_GREY = {
25
25
  surface: '#272c36',
26
26
  }
27
27
 
28
+ /** Infinite map/walk backdrop. Keep distinct from chrome HUD greys. */
29
+ export const WORLD_VOID = '#000000'
30
+
28
31
  export type ChangeKind = 'add' | 'edit' | 'remove'
29
32
 
30
33
  export const CHANGE_HIGHLIGHT: Record<