minecraft-inventory 0.1.3 → 0.1.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minecraft-inventory",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "release": {
@@ -42,7 +42,7 @@
42
42
  },
43
43
  "scripts": {
44
44
  "dev": "rsbuild dev",
45
- "build": "pnpm gen:textures && rsbuild build",
45
+ "build": "pnpm gen:textures && node scripts/stamp-version.mjs && rsbuild build",
46
46
  "preview": "rsbuild preview",
47
47
  "gen:textures": "node scripts/generate-texture-imports.mjs"
48
48
  }
Binary file
Binary file
Binary file
@@ -19,8 +19,8 @@ export interface InventoryOverlayProps {
19
19
  * control special features: `showOffhand` (1/0) and `container` (1/0).
20
20
  */
21
21
  properties?: Record<string, number>
22
- /** Show semi-transparent backdrop behind inventory (default: true) */
23
- showBackdrop?: boolean
22
+ /** Hide semi-transparent backdrop behind inventory */
23
+ noBackdrop?: boolean
24
24
  /** Backdrop color (default: 'rgba(0,0,0,0.5)') */
25
25
  backdropColor?: string
26
26
  /** Called when clicking outside the inventory (or pressing Esc) */
@@ -48,14 +48,21 @@ export interface InventoryOverlayProps {
48
48
  className?: string
49
49
  style?: React.CSSProperties
50
50
  children?: React.ReactNode
51
+ /** Show yellow debug border around the overlay area */
51
52
  debugBounds?: boolean
53
+ /** Show red debug outline around the inventory background */
54
+ showDebug?: boolean
55
+ /** Override entity display rendering. Pass a function returning JSX, or null to hide. */
56
+ renderEntity?: ((width: number, height: number) => React.ReactNode) | null
57
+ /** Hide the "INV" version watermark (opt-out) */
58
+ noWatermark?: boolean
52
59
  }
53
60
 
54
61
  export function InventoryOverlay({
55
62
  type,
56
63
  title,
57
64
  properties,
58
- showBackdrop = true,
65
+ noBackdrop = false,
59
66
  backdropColor = 'rgba(0,0,0,0.5)',
60
67
  onClose,
61
68
  showJEI = false,
@@ -72,8 +79,11 @@ export function InventoryOverlay({
72
79
  style,
73
80
  children,
74
81
  debugBounds = false,
82
+ showDebug = false,
83
+ renderEntity,
84
+ noWatermark = false,
75
85
  }: InventoryOverlayProps) {
76
- const { heldItem, sendAction, setHeldItem } = useInventoryContext()
86
+ const { heldItem, sendAction, setHeldItem, focusedSlot, setFocusedSlot } = useInventoryContext()
77
87
  const { scale } = useScale()
78
88
 
79
89
  const def = getInventoryType(type)
@@ -114,13 +124,18 @@ export function InventoryOverlay({
114
124
 
115
125
  // Fires for any click that isn't stopped by an interactive panel (inventory, hotbar, JEI, etc.)
116
126
  const handleBackdropClick = useCallback(() => {
127
+ // Clicking the backdrop clears focused slot (regardless of heldItem)
128
+ if (focusedSlot !== null) {
129
+ setFocusedSlot(null)
130
+ return
131
+ }
117
132
  if (heldItem) {
118
133
  sendAction({ type: 'drop', slotIndex: -1, all: true })
119
134
  setHeldItem(null)
120
135
  } else {
121
136
  onClose?.()
122
137
  }
123
- }, [heldItem, sendAction, setHeldItem, onClose])
138
+ }, [heldItem, sendAction, setHeldItem, onClose, focusedSlot, setFocusedSlot])
124
139
 
125
140
  const jeiPanel = showJEI ? (
126
141
  <JEI
@@ -162,7 +177,7 @@ export function InventoryOverlay({
162
177
  inset: 0,
163
178
  width: '100%',
164
179
  height: '100%',
165
- background: showBackdrop ? backdropColor : 'transparent',
180
+ background: noBackdrop ? 'transparent' : backdropColor,
166
181
  cursor: 'default',
167
182
  zIndex: 1,
168
183
  ...style,
@@ -210,8 +225,15 @@ export function InventoryOverlay({
210
225
  }}
211
226
  >
212
227
  <div className="mc-inv-overlay-content" style={{ position: 'relative' }}>
213
- {/* Inventory / Recipe view — stopPropagation so clicks inside don't close the overlay */}
214
- <div className="mc-inv-overlay-window" onClick={(e) => e.stopPropagation()}>
228
+ {/* Inventory / Recipe view — clicks clear focused slot; slots stop propagation themselves */}
229
+ <div
230
+ className="mc-inv-overlay-window"
231
+ onClick={(e) => {
232
+ e.stopPropagation()
233
+ // Clicking the inventory background (not a slot) clears focused slot
234
+ if (focusedSlot !== null) setFocusedSlot(null)
235
+ }}
236
+ >
215
237
  {recipeNavStack.length > 0 ? (
216
238
  <RecipeInventoryView
217
239
  navStack={recipeNavStack}
@@ -220,7 +242,7 @@ export function InventoryOverlay({
220
242
  onPushFrame={handleRecipePushFrame}
221
243
  />
222
244
  ) : (
223
- <InventoryWindow type={type} title={title} properties={properties} showDebug={debugBounds} />
245
+ <InventoryWindow type={type} title={title} properties={properties} showDebug={showDebug} renderEntity={renderEntity} />
224
246
  )}
225
247
  </div>
226
248
  </div>
@@ -245,6 +267,31 @@ export function InventoryOverlay({
245
267
  {/* Extra children (overlay-level) */}
246
268
  {children}
247
269
 
270
+ {/* Watermark */}
271
+ {!noWatermark && (
272
+ <a
273
+ className="mc-inv-watermark"
274
+ href="https://www.npmjs.com/package/minecraft-inventory"
275
+ target="_blank"
276
+ rel="noopener noreferrer"
277
+ onClick={(e) => e.stopPropagation()}
278
+ style={{
279
+ position: 'absolute',
280
+ top: 4,
281
+ left: 4,
282
+ fontSize: 9,
283
+ fontFamily: "'Minecraftia', 'Minecraft', monospace",
284
+ color: 'rgba(255,255,255,0.3)',
285
+ textDecoration: 'none',
286
+ pointerEvents: 'auto',
287
+ zIndex: 1,
288
+ lineHeight: 1,
289
+ }}
290
+ >
291
+ INV 0.0.0
292
+ </a>
293
+ )}
294
+
248
295
  {/* Debug bounds */}
249
296
  {debugBounds && (
250
297
  <div
@@ -0,0 +1,102 @@
1
+ import React, { useState, useCallback, useRef, useEffect } from 'react'
2
+ import { useScale } from '../../context/ScaleContext'
3
+ import { useTextures } from '../../context/TextureContext'
4
+ import { useInventoryContext } from '../../context/InventoryContext'
5
+
6
+ interface AnvilInputProps {
7
+ x: number
8
+ y: number
9
+ width: number
10
+ height: number
11
+ }
12
+
13
+ export function AnvilInput({ x, y, width, height }: AnvilInputProps) {
14
+ const { scale } = useScale()
15
+ const textures = useTextures()
16
+ const { sendAction, windowState } = useInventoryContext()
17
+
18
+ const [text, setText] = useState('')
19
+ const [focused, setFocused] = useState(false)
20
+ const inputRef = useRef<HTMLInputElement>(null)
21
+
22
+ const hasInputItem = windowState?.slots.some((s) => s.index === 0 && s.item !== null) ?? false
23
+
24
+ const textFieldUrl = textures.getGuiTextureUrl(
25
+ hasInputItem
26
+ ? '1.21.11/textures/gui/sprites/container/anvil/text_field.png'
27
+ : '1.21.11/textures/gui/sprites/container/anvil/text_field_disabled.png',
28
+ )
29
+
30
+ useEffect(() => {
31
+ if (!hasInputItem) {
32
+ setText('')
33
+ setFocused(false)
34
+ }
35
+ }, [hasInputItem])
36
+
37
+ const handleChange = useCallback(
38
+ (e: React.ChangeEvent<HTMLInputElement>) => {
39
+ const val = e.target.value
40
+ setText(val)
41
+ sendAction({ type: 'rename', text: val })
42
+ },
43
+ [sendAction],
44
+ )
45
+
46
+ return (
47
+ <div
48
+ className="mc-inv-anvil-input"
49
+ style={{
50
+ position: 'absolute',
51
+ left: x * scale,
52
+ top: y * scale,
53
+ width: width * scale,
54
+ height: height * scale,
55
+ imageRendering: 'pixelated',
56
+ }}
57
+ >
58
+ <img
59
+ src={textFieldUrl}
60
+ alt=""
61
+ aria-hidden
62
+ draggable={false}
63
+ style={{
64
+ position: 'absolute',
65
+ inset: 0,
66
+ width: '100%',
67
+ height: '100%',
68
+ imageRendering: 'pixelated',
69
+ pointerEvents: 'none',
70
+ }}
71
+ />
72
+ <input
73
+ ref={inputRef}
74
+ type="text"
75
+ value={text}
76
+ onChange={handleChange}
77
+ onFocus={() => setFocused(true)}
78
+ onBlur={() => setFocused(false)}
79
+ disabled={!hasInputItem}
80
+ maxLength={35}
81
+ placeholder=""
82
+ className="mc-inv-anvil-rename-input"
83
+ style={{
84
+ position: 'absolute',
85
+ left: 2 * scale,
86
+ top: 1 * scale,
87
+ width: (width - 4) * scale,
88
+ height: (height - 2) * scale,
89
+ border: 'none',
90
+ outline: 'none',
91
+ background: 'transparent',
92
+ color: '#ffffff',
93
+ fontFamily: "'Minecraftia', 'Minecraft', monospace",
94
+ fontSize: 7 * scale,
95
+ padding: 0,
96
+ lineHeight: 1,
97
+ caretColor: focused ? '#ffffff' : 'transparent',
98
+ }}
99
+ />
100
+ </div>
101
+ )
102
+ }
@@ -0,0 +1,46 @@
1
+ import React from 'react'
2
+ import { useScale } from '../../context/ScaleContext'
3
+ import type { EntityDisplayArea } from '../../types'
4
+
5
+ interface EntityDisplayProps {
6
+ area: EntityDisplayArea
7
+ renderEntity?: ((width: number, height: number) => React.ReactNode) | null
8
+ }
9
+
10
+ export function EntityDisplay({ area, renderEntity }: EntityDisplayProps) {
11
+ const { scale } = useScale()
12
+
13
+ if (renderEntity === null) return null
14
+
15
+ const w = area.width * scale
16
+ const h = area.height * scale
17
+
18
+ return (
19
+ <div
20
+ className="mc-inv-entity-display"
21
+ style={{
22
+ position: 'absolute',
23
+ left: area.x * scale,
24
+ top: area.y * scale,
25
+ width: w,
26
+ height: h,
27
+ overflow: 'hidden',
28
+ }}
29
+ >
30
+ {renderEntity ? (
31
+ renderEntity(w, h)
32
+ ) : (
33
+ <div
34
+ className="mc-inv-entity-display-placeholder"
35
+ style={{
36
+ width: '100%',
37
+ height: '100%',
38
+ background: 'rgba(255, 0, 0, 0.15)',
39
+ border: '1px solid rgba(255, 0, 0, 0.4)',
40
+ boxSizing: 'border-box',
41
+ }}
42
+ />
43
+ )}
44
+ </div>
45
+ )
46
+ }
@@ -4,6 +4,22 @@ import { useScale } from '../../context/ScaleContext'
4
4
  import { MessageFormattedString } from '../Text/MessageFormattedString'
5
5
  import type { InventoryTypeDefinition } from '../../registry'
6
6
 
7
+ /**
8
+ * For generic_9xN (N < 6): CSS-stitch the 6-row generic_54 texture using two
9
+ * overlapping img elements with objectPosition to clip top and bottom portions.
10
+ * No canvas or async loading needed — works with any URL including remote ones.
11
+ *
12
+ * The source texture (generic_54.png, 176×222) fixed layout:
13
+ * y=0 ..16 — title bar (17px)
14
+ * y=17 ..124 — 6 container rows (6×18 = 108px)
15
+ * y=125 ..221 — player inventory section (97px)
16
+ *
17
+ * Output height = topH + playerH = N*18+17 + 97 = N*18+114.
18
+ * Registry backgroundHeight must equal N*18+114 for the div to match.
19
+ */
20
+ const SRC_PLAYER_Y = 17 + 6 * 18 // 125 — fixed position where player section starts
21
+ const PLAYER_H = 222 - SRC_PLAYER_Y // 97px — height of player section in generic_54.png
22
+
7
23
  interface InventoryBackgroundProps {
8
24
  definition: InventoryTypeDefinition
9
25
  children: React.ReactNode
@@ -21,13 +37,23 @@ export function InventoryBackground({
21
37
  const { scale } = useScale()
22
38
 
23
39
  const bgUrl = textures.getGuiTextureUrl(definition.backgroundTexture)
40
+ const isStitched = definition.containerRows != null && definition.containerRows < 6
41
+
24
42
  const w = definition.backgroundWidth * scale
25
43
  const h = definition.backgroundHeight * scale
26
44
 
27
- // Source dimensions from definition (e.g., 176x166) — clip to this region from texture
28
45
  const srcW = definition.backgroundWidth
29
46
  const srcH = definition.backgroundHeight
30
47
 
48
+ const sharedImgStyle: React.CSSProperties = {
49
+ display: 'block',
50
+ width: srcW,
51
+ imageRendering: 'pixelated',
52
+ pointerEvents: 'none',
53
+ userSelect: 'none',
54
+ objectFit: 'none',
55
+ }
56
+
31
57
  return (
32
58
  <div
33
59
  className="mc-inv-background"
@@ -41,40 +67,55 @@ export function InventoryBackground({
41
67
  outlineOffset: 0,
42
68
  }}
43
69
  >
44
- {/* Background texture wrapper — clips source to srcW×srcH, then scales */}
45
- <div
46
- className="mc-inv-background-wrapper"
47
- style={{
48
- position: 'absolute',
49
- top: 0,
50
- left: 0,
51
- width: srcW,
52
- height: srcH,
53
- overflow: 'hidden',
54
- transform: `scale(${scale})`,
55
- transformOrigin: 'top left',
56
- }}
57
- >
58
- {/* Background texture — render at natural size, clipped by wrapper overflow */}
59
- <img
60
- className="mc-inv-background-image"
61
- src={bgUrl}
62
- alt=""
63
- aria-hidden
70
+ {isStitched ? (
71
+ /* CSS two-part stitch: clips top (title+N rows) and bottom (player section)
72
+ from the same source image using objectPosition — no canvas/async needed. */
73
+ <div
74
+ className="mc-inv-background-wrapper"
64
75
  style={{
65
- display: 'block',
76
+ position: 'absolute',
77
+ top: 0,
78
+ left: 0,
79
+ transform: `scale(${scale})`,
80
+ transformOrigin: 'top left',
81
+ }}
82
+ >
83
+ {/* Top: title bar + N container rows */}
84
+ <div style={{ width: srcW, height: definition.containerRows! * 18 + 17, overflow: 'hidden' }}>
85
+ <img className="mc-inv-background-image" src={bgUrl} alt="" aria-hidden draggable={false}
86
+ style={{ ...sharedImgStyle, objectPosition: '0 0' }} />
87
+ </div>
88
+ {/* Bottom: player inventory section starting at SRC_PLAYER_Y in source */}
89
+ <div style={{ width: srcW, height: PLAYER_H, overflow: 'hidden' }}>
90
+ <img src={bgUrl} alt="" aria-hidden draggable={false}
91
+ style={{ ...sharedImgStyle, objectPosition: `0 -${SRC_PLAYER_Y}px` }} />
92
+ </div>
93
+ </div>
94
+ ) : (
95
+ /* Standard: clip source to srcW×srcH via overflow:hidden, then scale */
96
+ <div
97
+ className="mc-inv-background-wrapper"
98
+ style={{
99
+ position: 'absolute',
100
+ top: 0,
101
+ left: 0,
66
102
  width: srcW,
67
103
  height: srcH,
68
- imageRendering: 'pixelated',
69
- pointerEvents: 'none',
70
- userSelect: 'none',
71
- // Clip to top-left srcW×srcH region (if texture is larger)
72
- objectFit: 'none',
73
- objectPosition: '0 0',
104
+ overflow: 'hidden',
105
+ transform: `scale(${scale})`,
106
+ transformOrigin: 'top left',
74
107
  }}
75
- draggable={false}
76
- />
77
- </div>
108
+ >
109
+ <img
110
+ className="mc-inv-background-image"
111
+ src={bgUrl}
112
+ alt=""
113
+ aria-hidden
114
+ draggable={false}
115
+ style={{ ...sharedImgStyle, objectPosition: '0 0' }}
116
+ />
117
+ </div>
118
+ )}
78
119
 
79
120
  {/* Title */}
80
121
  {title !== undefined && (
@@ -10,6 +10,8 @@ import { ProgressBar } from './ProgressBar'
10
10
  import { VillagerTradeList } from './VillagerTradeList'
11
11
  import { EnchantmentOptions } from './EnchantmentOptions'
12
12
  import { HotbarExtras } from './HotbarExtras'
13
+ import { AnvilInput } from './AnvilInput'
14
+ import { EntityDisplay } from './EntityDisplay'
13
15
 
14
16
  interface InventoryWindowProps {
15
17
  type: string
@@ -20,6 +22,8 @@ interface InventoryWindowProps {
20
22
  style?: React.CSSProperties
21
23
  enableKeyboardShortcuts?: boolean
22
24
  showDebug?: boolean
25
+ /** Override entity display rendering. Pass a function returning JSX, or null to hide. */
26
+ renderEntity?: ((width: number, height: number) => React.ReactNode) | null
23
27
  }
24
28
 
25
29
  export function InventoryWindow({
@@ -31,6 +35,7 @@ export function InventoryWindow({
31
35
  style,
32
36
  enableKeyboardShortcuts = true,
33
37
  showDebug = false,
38
+ renderEntity,
34
39
  }: InventoryWindowProps) {
35
40
  const def = getInventoryType(type)
36
41
  const { windowState, getSlot } = useInventoryContext()
@@ -62,6 +67,7 @@ export function InventoryWindow({
62
67
  const isVillager = type === 'villager'
63
68
  const isEnchanting = type === 'enchanting_table'
64
69
  const isHotbar = type === 'hotbar'
70
+ const isAnvil = type === 'anvil'
65
71
 
66
72
  const resolveItem = (slotIndex: number) => {
67
73
  const fromProp = effectiveSlots.find((s) => s.index === slotIndex)
@@ -97,6 +103,11 @@ export function InventoryWindow({
97
103
  </div>
98
104
  ))}
99
105
 
106
+ {/* Entity display area */}
107
+ {def.entityDisplay && (
108
+ <EntityDisplay area={def.entityDisplay} renderEntity={renderEntity} />
109
+ )}
110
+
100
111
  {/* Progress bars */}
101
112
  {def.progressBars?.map((pb) => (
102
113
  <ProgressBar
@@ -119,6 +130,9 @@ export function InventoryWindow({
119
130
  />
120
131
  )}
121
132
 
133
+ {/* Anvil rename input */}
134
+ {isAnvil && <AnvilInput x={59} y={20} width={110} height={16} />}
135
+
122
136
  {/* Hotbar extras: active slot indicator, offhand slot, open-inventory button */}
123
137
  {isHotbar && (
124
138
  <HotbarExtras
@@ -7,8 +7,10 @@ import { useDataUrl, isTextureFailed } from '../../cache/textureCache'
7
7
  interface ItemCanvasProps {
8
8
  item: ItemStack
9
9
  size?: number
10
- showCount?: boolean
11
- showDurability?: boolean
10
+ /** Hide item count overlay */
11
+ noCount?: boolean
12
+ /** Hide durability bar */
13
+ noDurability?: boolean
12
14
  className?: string
13
15
  style?: React.CSSProperties
14
16
  }
@@ -24,8 +26,8 @@ function getDurabilityColor(current: number, max: number): string {
24
26
  export const ItemCanvas = memo(function ItemCanvas({
25
27
  item,
26
28
  size,
27
- showCount = true,
28
- showDurability = true,
29
+ noCount = false,
30
+ noDurability = false,
29
31
  className,
30
32
  style,
31
33
  }: ItemCanvasProps) {
@@ -34,7 +36,8 @@ export const ItemCanvas = memo(function ItemCanvas({
34
36
  const renderSize = size ?? contentSize
35
37
 
36
38
  const primaryUrl = textures.getItemTextureUrl(item)
37
- const fallbackUrl = item.name ? textures.getBlockTextureUrl(item) : null
39
+ // Skip the block-texture fallback when a textureKey override is already in use
40
+ const fallbackUrl = !item.textureKey && item.name ? textures.getBlockTextureUrl(item) : null
38
41
 
39
42
  // Load primary URL as cached data URL
40
43
  const primaryDataUrl = useDataUrl(primaryUrl)
@@ -54,7 +57,7 @@ export const ItemCanvas = memo(function ItemCanvas({
54
57
  : primaryFailed && !fallbackUrl
55
58
 
56
59
  const hasDurability =
57
- showDurability &&
60
+ !noDurability &&
58
61
  item.durability !== undefined &&
59
62
  item.maxDurability !== undefined &&
60
63
  item.durability < item.maxDurability
@@ -129,7 +132,7 @@ export const ItemCanvas = memo(function ItemCanvas({
129
132
  )}
130
133
 
131
134
  {/* Item count */}
132
- {showCount && item.count > 1 && (
135
+ {!noCount && item.count > 1 && (
133
136
  <span className="mc-inv-item-count" style={{
134
137
  position: 'absolute',
135
138
  bottom: 0,
@@ -143,6 +143,12 @@ export function JEI({
143
143
  // F / R / U keyboard handlers
144
144
  useEffect(() => {
145
145
  const handler = (e: KeyboardEvent) => {
146
+ // Shift+F: toggle favorites-only filter
147
+ if (e.code === 'KeyF' && e.shiftKey) {
148
+ setShowFavorites((v) => !v)
149
+ setPage(0)
150
+ return
151
+ }
146
152
  // F: toggle favorite for hovered JEI item
147
153
  if (e.code === 'KeyF' && hoveredSlot !== null && hoveredSlot < 0) {
148
154
  const item = slotToItemRef.current.get(hoveredSlot)
@@ -235,7 +241,7 @@ export function JEI({
235
241
  style={{
236
242
  padding: `${padding}px`,
237
243
  background: '#c6c6c6',
238
- border: `${scale}px solid #555555`,
244
+ // border: `${scale}px solid #555555`,
239
245
  flexShrink: 0,
240
246
  }}
241
247
  >