minecraft-inventory 0.1.0 → 0.1.1

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
@@ -130,7 +130,6 @@ import { InventoryOverlay, InventoryProvider, ScaleProvider, TextureProvider } f
130
130
  type="chest"
131
131
  showBackdrop // default: true (50% black)
132
132
  backdropColor="rgba(0,0,0,0.5)"
133
- showHotbar // default: true
134
133
  showJEI
135
134
  jeiItems={items}
136
135
  jeiPosition="right"
@@ -255,7 +254,24 @@ import { TextureProvider } from 'minecraft-inventory'
255
254
  </TextureProvider>
256
255
  ```
257
256
 
258
- > **Per-container texture version:** Each inventory definition can carry a `guiTextureVersion` string (e.g. `'1.21.4'`) that is passed to `getGuiTextureUrl` as the second argument. Older containers default to `'1.16.4'`; newer ones (crafter, new smithing table) use `'1.21.4'`. Specify it when [registering custom containers](#adding-new-inventory-types).
257
+ **Bundled GUI textures (mc-assets)** Use the optional `mc-assets` package to bundle container backgrounds locally and avoid remote requests. Generate the texture map from your inventory registry, then pass the config to `TextureProvider`:
258
+
259
+ ```bash
260
+ pnpm add mc-assets # optional peer dependency
261
+ ```
262
+
263
+ ```tsx
264
+ import { TextureProvider, InventoryOverlay } from 'minecraft-inventory'
265
+ import { localTexturesConfig } from './generated/localTextures'
266
+
267
+ // GUI container backgrounds (chest, furnace, etc.) load from bundled mc-assets;
268
+ // item/block textures still use the default remote URLs unless you override them.
269
+ <TextureProvider config={localTexturesConfig}>
270
+ <InventoryOverlay type="chest" ... />
271
+ </TextureProvider>
272
+ ```
273
+
274
+ The generated file imports every `backgroundTexture` from your registry, resolves each to `node_modules/mc-assets/dist/other-textures/<version>/...` (or `latest/` if that version is missing), and exports `allContainerPaths` (inventory name → short path) plus `localTexturesConfig.getGuiTextureUrl(path)` for use with `<TextureProvider config={...}>`. Re-run `pnpm gen:textures` after changing [inventory types](#adding-new-inventory-types).
259
275
 
260
276
  ---
261
277
 
@@ -265,20 +281,74 @@ The connector is the bridge between the GUI and a Minecraft server or bot.
265
281
 
266
282
  ### Mineflayer connector
267
283
 
268
- ```ts
269
- import { createMineflayerConnector } from 'minecraft-inventory'
270
- import mineflayer from 'mineflayer'
284
+ Use `createMineflayerConnector(bot)` to plug the GUI into a [mineflayer](https://github.com/PrismarineJS/mineflayer) bot. The connector turns slot clicks, drags, and drops into `bot.clickWindow()` (and plugin APIs for villager trades, enchantment table, anvil, beacon), and subscribes to mineflayer inventory events so the UI stays in sync.
271
285
 
272
- const bot = mineflayer.createBot({ ... })
286
+ **Example overlay when a container opens:**
273
287
 
274
- bot.on('windowOpen', (window) => {
275
- const connector = createMineflayerConnector(bot)
288
+ ```tsx
289
+ import React, { useState, useEffect } from 'react'
290
+ import { createRoot } from 'react-dom/client'
291
+ import mineflayer from 'mineflayer'
292
+ import {
293
+ InventoryProvider,
294
+ ScaleProvider,
295
+ TextureProvider,
296
+ InventoryOverlay,
297
+ createMineflayerConnector,
298
+ } from 'minecraft-inventory'
276
299
 
277
- // Mount <InventoryGUI connector={connector} type={window.type} />
300
+ const bot = mineflayer.createBot({
301
+ host: 'localhost',
302
+ port: 25565,
303
+ username: 'InventoryViewer',
278
304
  })
305
+
306
+ function App() {
307
+ const [connector, setConnector] = useState(null)
308
+ const [windowType, setWindowType] = useState(null)
309
+ const [open, setOpen] = useState(false)
310
+
311
+ useEffect(() => {
312
+ const onOpen = () => {
313
+ setConnector(createMineflayerConnector(bot))
314
+ setWindowType(bot.currentWindow?.type ?? 'generic_9x1')
315
+ setOpen(true)
316
+ }
317
+ const onClose = () => {
318
+ setOpen(false)
319
+ setConnector(null)
320
+ }
321
+
322
+ bot.on('windowOpen', onOpen)
323
+ bot.on('windowClose', onClose)
324
+ return () => {
325
+ bot.removeListener('windowOpen', onOpen)
326
+ bot.removeListener('windowClose', onClose)
327
+ }
328
+ }, [])
329
+
330
+ if (!open || !connector || !windowType) return null
331
+
332
+ return (
333
+ <TextureProvider>
334
+ <ScaleProvider scale={2}>
335
+ <InventoryProvider connector={connector}>
336
+ <InventoryOverlay
337
+ type={windowType}
338
+ onClose={() => bot.closeWindow(bot.currentWindow)}
339
+ showJEI
340
+ jeiItems={[]}
341
+ />
342
+ </InventoryProvider>
343
+ </ScaleProvider>
344
+ </TextureProvider>
345
+ )
346
+ }
347
+
348
+ createRoot(document.getElementById('root')).render(<App />)
279
349
  ```
280
350
 
281
- The connector translates GUI actions (slot clicks, drag, drop, etc.) into `bot.clickWindow()` calls and subscribes to mineflayer inventory events to keep the GUI in sync.
351
+ **Hotbar “open inventory” button:** If you render a hotbar with the `container` option (e.g. mobile open-inventory button), the connector handles the `open-inventory` action: it calls `openPlayerInventory()`, which opens the player inventory GUI, or the ridden entity’s inventory (e.g. llama) when mounted. No extra wiring needed once the connector is passed to `InventoryProvider`.
282
352
 
283
353
  ### Demo connector (for local testing)
284
354
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minecraft-inventory",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "release": {
@@ -17,6 +17,7 @@
17
17
  "@types/node": "^25.4.0",
18
18
  "@types/react": "^19.2.14",
19
19
  "@types/react-dom": "^19.2.3",
20
+ "mc-assets": "*",
20
21
  "minecraft-data": "^3.105.0",
21
22
  "typescript": "^5.9.3"
22
23
  },
@@ -25,13 +26,22 @@
25
26
  "react": "^19.2.4",
26
27
  "react-dom": "^19.2.4"
27
28
  },
29
+ "peerDependencies": {
30
+ "mc-assets": "*"
31
+ },
32
+ "peerDependenciesMeta": {
33
+ "mc-assets": {
34
+ "optional": true
35
+ }
36
+ },
28
37
  "repository": "https://github.com/zardoy/minecraft-inventory",
29
38
  "engines": {
30
39
  "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
31
40
  },
32
41
  "scripts": {
33
42
  "dev": "rsbuild dev",
34
- "build": "rsbuild build",
35
- "preview": "rsbuild preview"
43
+ "build": "pnpm gen:textures && rsbuild build",
44
+ "preview": "rsbuild preview",
45
+ "gen:textures": "node scripts/generate-texture-imports.mjs"
36
46
  }
37
47
  }
@@ -3,16 +3,7 @@ import { useInventoryContext } from '../../context/InventoryContext'
3
3
  import { useScale } from '../../context/ScaleContext'
4
4
  import { ItemCanvas } from '../ItemCanvas'
5
5
  import { useMobile } from '../../hooks/useMobile'
6
-
7
- // Global mouse tracker — always knows cursor position, even before item is picked up
8
- const globalMouse = { x: -9999, y: -9999 }
9
-
10
- if (typeof window !== 'undefined') {
11
- window.addEventListener('mousemove', (e) => {
12
- globalMouse.x = e.clientX
13
- globalMouse.y = e.clientY
14
- }, { passive: true, capture: true })
15
- }
6
+ import { globalMouse } from '../../utils/globalMouse'
16
7
 
17
8
  /**
18
9
  * CursorItem — optimized to bypass React re-renders on mouse move.
@@ -1,17 +1,24 @@
1
1
  import React, { useCallback, useState } from 'react'
2
2
  import { useInventoryContext } from '../../context/InventoryContext'
3
3
  import { useScale } from '../../context/ScaleContext'
4
+ import { getInventoryType } from '../../registry'
4
5
  import { InventoryWindow } from '../InventoryWindow'
5
6
  import { CursorItem } from '../CursorItem'
6
- import { Hotbar } from '../Hotbar'
7
7
  import { JEI } from '../JEI'
8
8
  import type { JEIItem } from '../JEI'
9
+ import { Notes } from '../Notes'
10
+ import type { Note } from '../Notes'
9
11
  import { RecipeInventoryView } from '../RecipeGuide'
10
12
  import type { RecipeGuide, RecipeNavFrame } from '../../types'
11
13
 
12
14
  export interface InventoryOverlayProps {
13
15
  type: string
14
16
  title?: string
17
+ /**
18
+ * Extra properties forwarded to InventoryWindow. For the 'hotbar' type these
19
+ * control special features: `showOffhand` (1/0) and `container` (1/0).
20
+ */
21
+ properties?: Record<string, number>
15
22
  /** Show semi-transparent backdrop behind inventory (default: true) */
16
23
  showBackdrop?: boolean
17
24
  /** Backdrop color (default: 'rgba(0,0,0,0.5)') */
@@ -24,18 +31,30 @@ export interface InventoryOverlayProps {
24
31
  jeiPosition?: 'left' | 'right'
25
32
  jeiOnGetRecipes?: (item: JEIItem) => RecipeGuide[]
26
33
  jeiOnGetUsages?: (item: JEIItem) => RecipeGuide[]
27
- /** Show hotbar below inventory */
28
- showHotbar?: boolean
34
+ /** Enable Notes sidebar (uses localStorage by default if callbacks not provided) */
35
+ enableNotes?: boolean
36
+ /** Callback to get notes. If not provided and enableNotes is true, uses localStorage. */
37
+ notesOnGet?: () => Note[] | Promise<Note[]>
38
+ /** Callback to save notes. If not provided and enableNotes is true, uses localStorage. */
39
+ notesOnSave?: (notes: Note[]) => void | Promise<void>
40
+ /** Storage key for localStorage (default: 'mc-inv-notes') */
41
+ notesStorageKey?: string
42
+ /**
43
+ * Content shown in the left side-panel area (e.g. custom panels).
44
+ * Width is auto-computed as (overlayWidth - inventoryWidth) / 2 - padding.
45
+ * Note: If enableNotes is true, Notes will be shown in addition to leftPanel.
46
+ */
47
+ leftPanel?: React.ReactNode
29
48
  className?: string
30
49
  style?: React.CSSProperties
31
- /** Style applied to the inner content container (around inventory + JEI) */
32
- contentStyle?: React.CSSProperties
33
50
  children?: React.ReactNode
51
+ debugBounds?: boolean
34
52
  }
35
53
 
36
54
  export function InventoryOverlay({
37
55
  type,
38
56
  title,
57
+ properties,
39
58
  showBackdrop = true,
40
59
  backdropColor = 'rgba(0,0,0,0.5)',
41
60
  onClose,
@@ -44,15 +63,26 @@ export function InventoryOverlay({
44
63
  jeiPosition = 'right',
45
64
  jeiOnGetRecipes,
46
65
  jeiOnGetUsages,
47
- showHotbar = true,
66
+ enableNotes = false,
67
+ notesOnGet,
68
+ notesOnSave,
69
+ notesStorageKey,
70
+ leftPanel,
48
71
  className,
49
72
  style,
50
- contentStyle,
51
73
  children,
74
+ debugBounds = false,
52
75
  }: InventoryOverlayProps) {
53
76
  const { heldItem, sendAction, setHeldItem } = useInventoryContext()
54
77
  const { scale } = useScale()
55
78
 
79
+ const def = getInventoryType(type)
80
+ const invUnscaledW = def?.backgroundWidth ?? 176
81
+ const sideGapPx = 5 * scale // gap from overlay edge and from inventory edge
82
+
83
+ // Full height for side panels = overlay height minus edge gaps
84
+ const sidePanelHeight = `calc(100% - ${sideGapPx * 2}px)`
85
+
56
86
  // Recipe navigation stack — when non-empty, RecipeInventoryView replaces InventoryWindow
57
87
  const [recipeNavStack, setRecipeNavStack] = useState<RecipeNavFrame[]>([])
58
88
 
@@ -102,6 +132,26 @@ export function InventoryOverlay({
102
132
  />
103
133
  ) : null
104
134
 
135
+ const notesPanel = enableNotes ? (
136
+ <Notes
137
+ onGetNotes={notesOnGet}
138
+ onSaveNotes={notesOnSave}
139
+ storageKey={notesStorageKey}
140
+ />
141
+ ) : null
142
+
143
+ // Full height side panels; width fills the space between overlay edge and inventory center
144
+ const sidePanelBase: React.CSSProperties = {
145
+ position: 'absolute',
146
+ top: sideGapPx,
147
+ bottom: sideGapPx,
148
+ width: `calc(100% / 2 - ${invUnscaledW * scale}px / 2 - ${sideGapPx}px * 2)`,
149
+ height: sidePanelHeight,
150
+ zIndex: 5,
151
+ display: 'flex',
152
+ flexDirection: 'column',
153
+ }
154
+
105
155
  return (
106
156
  <>
107
157
  <div
@@ -110,39 +160,56 @@ export function InventoryOverlay({
110
160
  style={{
111
161
  position: 'absolute',
112
162
  inset: 0,
163
+ width: '100%',
164
+ height: '100%',
113
165
  background: showBackdrop ? backdropColor : 'transparent',
114
166
  cursor: 'default',
115
167
  zIndex: 1,
116
168
  ...style,
117
169
  }}
118
170
  >
119
- {/* Centered anchor inventory always stays centered */}
120
- <div
121
- className="mc-inv-overlay-center"
122
- style={{
123
- position: 'absolute',
124
- left: '50%',
125
- top: '50%',
126
- transform: 'translate(-50%, -50%)',
127
- }}
128
- >
129
- <div className="mc-inv-overlay-content" style={{ position: 'relative', ...contentStyle }}>
130
- {/* JEI on left — stopPropagation so clicks on JEI don't close the overlay */}
171
+ {/* ── Left side panel (leftPanel prop, Notes, and/or JEI-left) ── */}
172
+ {(leftPanel || enableNotes || (showJEI && jeiPosition === 'left')) && (
173
+ <div
174
+ className="mc-inv-overlay-side mc-inv-overlay-side-left"
175
+ onClick={(e) => e.stopPropagation()}
176
+ style={{ ...sidePanelBase, left: sideGapPx, pointerEvents: 'auto' }}
177
+ >
178
+ {enableNotes && (
179
+ <div className="mc-inv-overlay-notes" style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
180
+ {notesPanel}
181
+ </div>
182
+ )}
183
+ {leftPanel && (
184
+ <div style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
185
+ {leftPanel}
186
+ </div>
187
+ )}
131
188
  {showJEI && jeiPosition === 'left' && (
132
189
  <div
133
190
  className="mc-inv-overlay-jei mc-inv-overlay-jei-left"
134
- onClick={(e) => e.stopPropagation()}
135
- style={{
136
- position: 'absolute',
137
- right: `calc(100% + ${4 * scale}px)`,
138
- top: 0,
139
- zIndex: 5,
140
- }}
191
+ style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', width: '100%' }}
141
192
  >
142
193
  {jeiPanel}
143
194
  </div>
144
195
  )}
196
+ </div>
197
+ )}
145
198
 
199
+ {/* ── Inventory centered anchor — flex centering, no transform ── */}
200
+ <div
201
+ className="mc-inv-overlay-center"
202
+ style={{
203
+ position: 'absolute',
204
+ inset: 0,
205
+ width: '100%',
206
+ height: '100%',
207
+ display: 'flex',
208
+ justifyContent: 'center',
209
+ alignItems: 'center',
210
+ }}
211
+ >
212
+ <div className="mc-inv-overlay-content" style={{ position: 'relative' }}>
146
213
  {/* Inventory / Recipe view — stopPropagation so clicks inside don't close the overlay */}
147
214
  <div className="mc-inv-overlay-window" onClick={(e) => e.stopPropagation()}>
148
215
  {recipeNavStack.length > 0 ? (
@@ -153,41 +220,44 @@ export function InventoryOverlay({
153
220
  onPushFrame={handleRecipePushFrame}
154
221
  />
155
222
  ) : (
156
- <InventoryWindow type={type} title={title} />
223
+ <InventoryWindow type={type} title={title} properties={properties} />
157
224
  )}
158
225
  </div>
226
+ </div>
227
+ </div>
159
228
 
160
- {/* Hotbar stopPropagation */}
161
- {showHotbar && (
162
- <div
163
- className="mc-inv-overlay-hotbar"
164
- onClick={(e) => e.stopPropagation()}
165
- style={{ marginTop: 8 * scale }}
166
- >
167
- <Hotbar />
168
- </div>
169
- )}
229
+ {/* ── Right side panel (JEI-right) ── */}
230
+ {showJEI && jeiPosition === 'right' && (
231
+ <div
232
+ className="mc-inv-overlay-side mc-inv-overlay-side-right"
233
+ onClick={(e) => e.stopPropagation()}
234
+ style={{ ...sidePanelBase, right: sideGapPx, pointerEvents: 'auto' }}
235
+ >
236
+ <div
237
+ className="mc-inv-overlay-jei mc-inv-overlay-jei-right"
238
+ style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', width: '100%' }}
239
+ >
240
+ {jeiPanel}
241
+ </div>
242
+ </div>
243
+ )}
170
244
 
171
- {/* JEI on right — stopPropagation */}
172
- {showJEI && jeiPosition === 'right' && (
173
- <div
174
- className="mc-inv-overlay-jei mc-inv-overlay-jei-right"
175
- onClick={(e) => e.stopPropagation()}
176
- style={{
177
- position: 'absolute',
178
- left: `calc(100% + ${4 * scale}px)`,
179
- top: 0,
180
- zIndex: 5,
181
- }}
182
- >
183
- {jeiPanel}
184
- </div>
185
- )}
245
+ {/* Extra children (overlay-level) */}
246
+ {children}
186
247
 
187
- {/* Extra children (notes, etc.) */}
188
- {children}
189
- </div>
190
- </div>
248
+ {/* Debug bounds */}
249
+ {debugBounds && (
250
+ <div
251
+ className="mc-inv-overlay-debug-marker"
252
+ style={{
253
+ position: 'absolute',
254
+ inset: 0,
255
+ border: '2px solid yellow',
256
+ pointerEvents: 'none',
257
+ boxSizing: 'border-box',
258
+ }}
259
+ />
260
+ )}
191
261
  </div>
192
262
 
193
263
  <CursorItem />
@@ -0,0 +1,180 @@
1
+ import React from 'react'
2
+ import { useInventoryContext } from '../../context/InventoryContext'
3
+ import { useScale } from '../../context/ScaleContext'
4
+ import { useTextures } from '../../context/TextureContext'
5
+ import { Slot } from '../Slot'
6
+ import type { ItemStack } from '../../types'
7
+
8
+ interface HotbarExtrasProps {
9
+ showOffhand: boolean
10
+ container: boolean
11
+ offhandItem: ItemStack | null
12
+ }
13
+
14
+ /** Renders a sprite from the widgets.png using the same inner-div+transform approach as InventoryBackground */
15
+ function WidgetSprite({
16
+ url,
17
+ srcX,
18
+ srcY,
19
+ srcW,
20
+ srcH,
21
+ scale,
22
+ style,
23
+ }: {
24
+ url: string
25
+ srcX: number
26
+ srcY: number
27
+ srcW: number
28
+ srcH: number
29
+ scale: number
30
+ style?: React.CSSProperties
31
+ }) {
32
+ return (
33
+ <div
34
+ style={{
35
+ position: 'absolute',
36
+ width: srcW * scale,
37
+ height: srcH * scale,
38
+ overflow: 'hidden',
39
+ imageRendering: 'pixelated',
40
+ pointerEvents: 'none',
41
+ ...style,
42
+ }}
43
+ >
44
+ <div
45
+ style={{
46
+ width: srcW,
47
+ height: srcH,
48
+ overflow: 'hidden',
49
+ transform: `scale(${scale})`,
50
+ transformOrigin: 'top left',
51
+ }}
52
+ >
53
+ <img
54
+ src={url}
55
+ alt=""
56
+ aria-hidden
57
+ draggable={false}
58
+ style={{
59
+ position: 'absolute',
60
+ top: -srcY,
61
+ left: -srcX,
62
+ imageRendering: 'pixelated',
63
+ userSelect: 'none',
64
+ }}
65
+ />
66
+ </div>
67
+ </div>
68
+ )
69
+ }
70
+
71
+ /**
72
+ * Hotbar-specific extras rendered inside InventoryBackground alongside the regular slots.
73
+ * Mirrors layouts.mjs Hotbar children:
74
+ * - Active slot indicator (always)
75
+ * - Offhand slot left of the strip (when showOffhand)
76
+ * - Open-inventory button right of the strip (when container)
77
+ */
78
+ export function HotbarExtras({ showOffhand, container, offhandItem }: HotbarExtrasProps) {
79
+ const { playerState, sendAction } = useInventoryContext()
80
+ const { scale } = useScale()
81
+ const textures = useTextures()
82
+
83
+ const activeSlot = playerState?.activeHotbarSlot ?? 0
84
+ const hotbarUrl = textures.getGuiTextureUrl('1.15/textures/gui/widgets.png')
85
+
86
+ // Sprite regions within widgets.png (native pixels):
87
+ // Hotbar strip : [0, 0, 182, 22]
88
+ // Active box : [0, 22, 24, 24]
89
+ // Offhand box : [24, 22, 24, 24]
90
+ const GAP = 2
91
+
92
+ return (
93
+ <>
94
+ {/* ── Active slot indicator ── */}
95
+ <WidgetSprite
96
+ url={hotbarUrl}
97
+ srcX={0}
98
+ srcY={22}
99
+ srcW={24}
100
+ srcH={24}
101
+ scale={scale}
102
+ style={{
103
+ left: (activeSlot * 20 - 1) * scale,
104
+ top: -1 * scale,
105
+ zIndex: 2,
106
+ }}
107
+ />
108
+
109
+ {/* ── Offhand slot (left of hotbar) ── */}
110
+ {showOffhand && (
111
+ <div
112
+ className="mc-inv-hotbar-offhand"
113
+ style={{
114
+ position: 'absolute',
115
+ left: -(24 + GAP) * scale,
116
+ top: 0,
117
+ width: 24 * scale,
118
+ height: 22 * scale,
119
+ }}
120
+ >
121
+ <WidgetSprite
122
+ url={hotbarUrl}
123
+ srcX={24}
124
+ srcY={22}
125
+ srcW={24}
126
+ srcH={22}
127
+ scale={scale}
128
+ style={{ left: 0, top: 0 }}
129
+ />
130
+ {/* Item at slot 45 (offhand) */}
131
+ <div style={{ position: 'absolute', top: 3 * scale, left: 3 * scale }}>
132
+ <Slot index={45} item={offhandItem} size={16 * scale} noBackground />
133
+ </div>
134
+ </div>
135
+ )}
136
+
137
+ {/* ── Open-inventory button (right of hotbar) ── */}
138
+ {container && (
139
+ <div
140
+ className="mc-inv-hotbar-open-inv"
141
+ title="Open inventory"
142
+ style={{
143
+ position: 'absolute',
144
+ left: (182 + GAP) * scale,
145
+ top: 0,
146
+ width: 24 * scale,
147
+ height: 22 * scale,
148
+ cursor: 'pointer',
149
+ }}
150
+ onClick={() => sendAction({ type: 'open-inventory' })}
151
+ >
152
+ <WidgetSprite
153
+ url={hotbarUrl}
154
+ srcX={24}
155
+ srcY={22}
156
+ srcW={24}
157
+ srcH={22}
158
+ scale={scale}
159
+ style={{ left: 0, top: 0 }}
160
+ />
161
+ {/* Three white dots centered */}
162
+ {[0, 1, 2].map((i) => (
163
+ <div
164
+ key={i}
165
+ style={{
166
+ position: 'absolute',
167
+ left: (6 + i * 4) * scale,
168
+ top: 10 * scale,
169
+ width: 2 * scale,
170
+ height: 2 * scale,
171
+ background: 'white',
172
+ pointerEvents: 'none',
173
+ }}
174
+ />
175
+ ))}
176
+ </div>
177
+ )}
178
+ </>
179
+ )
180
+ }
@@ -18,7 +18,7 @@ export function InventoryBackground({
18
18
  const textures = useTextures()
19
19
  const { scale } = useScale()
20
20
 
21
- const bgUrl = textures.getGuiTextureUrl(definition.backgroundTexture, definition.guiTextureVersion)
21
+ const bgUrl = textures.getGuiTextureUrl(definition.backgroundTexture)
22
22
  const w = definition.backgroundWidth * scale
23
23
  const h = definition.backgroundHeight * scale
24
24
 
@@ -80,8 +80,8 @@ export function InventoryBackground({
80
80
  className="mc-inv-background-title"
81
81
  style={{
82
82
  position: 'absolute',
83
- top: 6 * scale,
84
- left: 8 * scale,
83
+ top: (6 + (definition.titleOffset?.y ?? 0)) * scale,
84
+ left: (8 + (definition.titleOffset?.x ?? 0)) * scale,
85
85
  fontSize: 7 * scale,
86
86
  fontFamily: "'Minecraftia', 'Minecraft', monospace",
87
87
  pointerEvents: 'none',
@@ -9,6 +9,7 @@ import { InventoryBackground } from './InventoryBackground'
9
9
  import { ProgressBar } from './ProgressBar'
10
10
  import { VillagerTradeList } from './VillagerTradeList'
11
11
  import { EnchantmentOptions } from './EnchantmentOptions'
12
+ import { HotbarExtras } from './HotbarExtras'
12
13
 
13
14
  interface InventoryWindowProps {
14
15
  type: string
@@ -58,6 +59,7 @@ export function InventoryWindow({
58
59
  const effectiveTitle = type === 'player' ? undefined : (title ?? windowState?.title ?? def.title)
59
60
  const isVillager = type === 'villager'
60
61
  const isEnchanting = type === 'enchanting_table'
62
+ const isHotbar = type === 'hotbar'
61
63
 
62
64
  const resolveItem = (slotIndex: number) => {
63
65
  const fromProp = effectiveSlots.find((s) => s.index === slotIndex)
@@ -114,6 +116,15 @@ export function InventoryWindow({
114
116
  y={14}
115
117
  />
116
118
  )}
119
+
120
+ {/* Hotbar extras: active slot indicator, offhand slot, open-inventory button */}
121
+ {isHotbar && (
122
+ <HotbarExtras
123
+ showOffhand={Boolean(effectiveProperties.showOffhand)}
124
+ container={Boolean(effectiveProperties.container)}
125
+ offhandItem={resolveItem(45)}
126
+ />
127
+ )}
117
128
  </InventoryBackground>
118
129
  </div>
119
130
  )