minecraft-inventory 0.1.26 → 0.1.28

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.26",
3
+ "version": "0.1.28",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "release": {
@@ -133,15 +133,26 @@ export function InventoryOverlay({
133
133
  }, [jeiOnGetRecipes, jeiOnGetUsages, pushRecipeFrame])
134
134
 
135
135
  // Fires for any click that isn't stopped by an interactive panel (inventory, hotbar, JEI, etc.)
136
- const handleBackdropClick = useCallback(() => {
137
- // Clicking the backdrop clears focused slot (regardless of heldItem)
138
- if (focusedSlot !== null) {
139
- setFocusedSlot(null)
140
- return
141
- }
136
+ const handleBackdropClick = useCallback((e: React.MouseEvent) => {
137
+ // Only handle left (drop all) and right (drop one) mouse buttons
138
+ if (e.button !== 0 && e.button !== 2) return
142
139
  if (heldItem) {
143
- sendAction({ type: 'drop', slotIndex: -1, all: true })
144
- setHeldItem(null)
140
+ const dropAll = e.button === 0 // LMB = drop all, RMB = drop one
141
+ sendAction({ type: 'drop', slotIndex: -1, all: dropAll })
142
+ if (dropAll) {
143
+ setHeldItem(null)
144
+ } else {
145
+ // Right click: drop one, keep rest on cursor
146
+ if (heldItem.count > 1) {
147
+ setHeldItem({ ...heldItem, count: heldItem.count - 1 })
148
+ } else {
149
+ setHeldItem(null)
150
+ }
151
+ }
152
+ // Also clear focused slot if any
153
+ if (focusedSlot !== null) setFocusedSlot(null)
154
+ } else if (focusedSlot !== null) {
155
+ setFocusedSlot(null)
145
156
  } else {
146
157
  onClose?.()
147
158
  }
@@ -183,7 +194,8 @@ export function InventoryOverlay({
183
194
  <>
184
195
  <div
185
196
  className={['mc-inv-overlay', className].filter(Boolean).join(' ')}
186
- onClick={handleBackdropClick}
197
+ onMouseDown={handleBackdropClick}
198
+ onContextMenu={(e) => e.preventDefault()}
187
199
  style={{
188
200
  position: 'absolute',
189
201
  inset: 0,
@@ -217,6 +229,7 @@ export function InventoryOverlay({
217
229
  {showJEI && jeiPosition === 'left' && (
218
230
  <div
219
231
  className="mc-inv-overlay-jei mc-inv-overlay-jei-left"
232
+ onMouseDown={(e) => e.stopPropagation()}
220
233
  onClick={(e) => e.stopPropagation()}
221
234
  style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', width: '100%' }}
222
235
  >
@@ -237,12 +250,14 @@ export function InventoryOverlay({
237
250
  display: 'flex',
238
251
  justifyContent: 'center',
239
252
  alignItems: 'center',
253
+ pointerEvents: 'none',
240
254
  }}
241
255
  >
242
- <div className="mc-inv-overlay-content" style={{ position: 'relative' }}>
256
+ <div className="mc-inv-overlay-content" style={{ position: 'relative', pointerEvents: 'auto' }}>
243
257
  {/* Inventory / Recipe view — clicks clear focused slot; slots stop propagation themselves */}
244
258
  <div
245
259
  className="mc-inv-overlay-window"
260
+ onMouseDown={(e) => e.stopPropagation()}
246
261
  onClick={(e) => {
247
262
  e.stopPropagation()
248
263
  // Clicking the inventory background (not a slot) clears focused slot
@@ -274,6 +289,7 @@ export function InventoryOverlay({
274
289
  {showJEI && jeiPosition === 'right' && (
275
290
  <div
276
291
  className="mc-inv-overlay-side mc-inv-overlay-side-right"
292
+ onMouseDown={(e) => e.stopPropagation()}
277
293
  onClick={(e) => e.stopPropagation()}
278
294
  style={{ ...sidePanelBase, right: sideGapPx, pointerEvents: 'auto' }}
279
295
  >
@@ -310,7 +326,7 @@ export function InventoryOverlay({
310
326
  lineHeight: 1,
311
327
  }}
312
328
  >
313
- INV 0.1.26
329
+ INV 0.1.28
314
330
  </a>
315
331
  )}
316
332
 
@@ -1,8 +1,11 @@
1
1
  import React, { useState, useCallback, useRef, useEffect } from 'react'
2
+ import { useFocusInputShortcut } from '../../hooks/useFocusInputShortcut'
2
3
  import { useScale } from '../../context/ScaleContext'
3
4
  import { useTextures } from '../../context/TextureContext'
4
5
  import { useInventoryContext } from '../../context/InventoryContext'
5
6
 
7
+ const ANVIL_FOCUS_KEYS = ['KeyE'] as const
8
+
6
9
  interface AnvilInputProps {
7
10
  x: number
8
11
  y: number
@@ -21,6 +24,8 @@ export function AnvilInput({ x, y, width, height }: AnvilInputProps) {
21
24
 
22
25
  const hasInputItem = windowState?.slots.some((s) => s.index === 0 && s.item !== null) ?? false
23
26
 
27
+ useFocusInputShortcut(inputRef, ANVIL_FOCUS_KEYS, hasInputItem)
28
+
24
29
  const textFieldUrl = textures.getGuiTextureUrl(
25
30
  hasInputItem
26
31
  ? '1.21.11/textures/gui/sprites/container/anvil/text_field.png'
@@ -2,6 +2,7 @@ import React, { useState, useMemo, useCallback, useEffect, useLayoutEffect, useR
2
2
  import type { ItemStack, RecipeGuide, RecipeNavFrame, BlockTextureRender } from '../../types'
3
3
  import { useScale } from '../../context/ScaleContext'
4
4
  import { useInventoryContext } from '../../context/InventoryContext'
5
+ import { useSlashFocusInput } from '../../hooks/useFocusInputShortcut'
5
6
  import { Slot } from '../Slot'
6
7
  import { StarIcon } from './StarIcon'
7
8
  import styles from './JEI.module.css'
@@ -88,6 +89,8 @@ export function JEI({
88
89
  // Self-measured dimensions so the panel can be width:100% and fill its container
89
90
  const rootRef = useRef<HTMLDivElement>(null)
90
91
  const gridRef = useRef<HTMLDivElement>(null)
92
+ const searchInputRef = useRef<HTMLInputElement>(null)
93
+ useSlashFocusInput(searchInputRef, true)
91
94
  const [measuredCols, setMeasuredCols] = useState(ITEMS_PER_ROW)
92
95
  const [measuredRows, setMeasuredRows] = useState(5)
93
96
 
@@ -250,6 +253,7 @@ export function JEI({
250
253
  }}
251
254
  >
252
255
  <input
256
+ ref={searchInputRef}
253
257
  type="text"
254
258
  value={search}
255
259
  onChange={handleSearchChange}
@@ -755,13 +755,39 @@ export function createMineflayerConnector(bot: MineflayerBot, options?: Mineflay
755
755
  }
756
756
  logActionEvent('connector.action.success', action)
757
757
  } else if (action.type === 'drop') {
758
- logHelperIntent(action, 'bot.clickWindow', {
759
- slot: action.slotIndex,
760
- mouseButton: action.all ? 1 : 0,
761
- mode: 4,
762
- })
763
- await bot.clickWindow(action.slotIndex, action.all ? 1 : 0, 4)
764
- onSetSlot()
758
+ if (action.slotIndex === -1) {
759
+ // Drop cursor item by clicking outside the window: slot=-999, mode=0
760
+ // Left click (all=true) drops entire stack, right click (all=false) drops one
761
+ if (!ext._client) {
762
+ logActionEvent('connector.action.skipped', action, { reason: 'missing_client' })
763
+ return
764
+ }
765
+ const windowId = bot.currentWindow ? bot.currentWindow.id : 0
766
+ const mouseButton = action.all ? 0 : 1
767
+ const packet = {
768
+ windowId,
769
+ stateId: dragStateId,
770
+ slot: -999,
771
+ mouseButton,
772
+ mode: 0,
773
+ changedSlots: [],
774
+ cursorItem: { present: false } as any,
775
+ }
776
+ logPacketWrite('window_click', packet)
777
+ ext._client.write('window_click', packet)
778
+ dragStateId++
779
+ // Skip onSetSlot() — mineflayer's selectedItem is not updated yet.
780
+ // The server will send set_slot/window_items to sync the cursor state.
781
+ } else {
782
+ // Drop from specific slot via Q key: mode=4
783
+ logHelperIntent(action, 'bot.clickWindow', {
784
+ slot: action.slotIndex,
785
+ mouseButton: action.all ? 1 : 0,
786
+ mode: 4,
787
+ })
788
+ await bot.clickWindow(action.slotIndex, action.all ? 1 : 0, 4)
789
+ onSetSlot()
790
+ }
765
791
  logActionEvent('connector.action.success', action)
766
792
  } else if (action.type === 'close') {
767
793
  if (win) {
@@ -0,0 +1,46 @@
1
+ import { useEffect, useMemo, type RefObject } from 'react'
2
+
3
+ function isTypingTarget(el: EventTarget | null): boolean {
4
+ if (!el || !(el instanceof HTMLElement)) return false
5
+ const tag = el.tagName
6
+ if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true
7
+ if (el.isContentEditable) return true
8
+ return false
9
+ }
10
+
11
+ const SLASH_KEYS = ['Slash', 'NumpadDivide'] as const
12
+
13
+ /**
14
+ * Focus the input when one of the given keys is pressed; prevents the key from being typed.
15
+ * Skips when focus is in another input/textarea/select/contenteditable (same idea as useCloseOnWClick).
16
+ */
17
+ export function useFocusInputShortcut(
18
+ inputRef: RefObject<HTMLInputElement | null>,
19
+ keyCodes: readonly string[],
20
+ enabled = true,
21
+ ) {
22
+ const codesSet = useMemo(() => new Set(keyCodes), [keyCodes])
23
+
24
+ useEffect(() => {
25
+ if (!enabled) return
26
+
27
+ const onKeyDown = (e: KeyboardEvent) => {
28
+ if (!codesSet.has(e.code)) return
29
+ if (e.repeat) return
30
+ if (e.ctrlKey || e.metaKey || e.altKey) return
31
+ if (isTypingTarget(e.target)) return
32
+ const el = inputRef.current
33
+ if (!el || el.disabled) return
34
+ e.preventDefault()
35
+ el.focus()
36
+ }
37
+
38
+ window.addEventListener('keydown', onKeyDown)
39
+ return () => window.removeEventListener('keydown', onKeyDown)
40
+ }, [enabled, inputRef, codesSet])
41
+ }
42
+
43
+ /** `/` or numpad `/` — focus JEI search (when JEI is mounted). */
44
+ export function useSlashFocusInput(inputRef: RefObject<HTMLInputElement | null>, enabled = true) {
45
+ useFocusInputShortcut(inputRef, SLASH_KEYS, enabled)
46
+ }