minecraft-inventory 0.1.23 → 0.1.25

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
@@ -9,7 +9,7 @@ A flexible, scalable React library for rendering Minecraft inventory GUIs. Desig
9
9
  - `<img>`-rendered item textures with automatic `items/` → `blocks/` fallback (via PrismarineJS asset mirror by default)
10
10
  - Tooltips that follow the cursor, matching the original Minecraft style
11
11
  - Full keyboard support: number keys (1-9) to swap hotbar, Q to drop, double-click to collect, scroll wheel to pick/place
12
- - Mobile support: tap to open a context menu (take all / half / custom amount / drop)
12
+ - Mobile support: tap-to-focus inventory interactions plus long-press slot actions (pick half / custom amount / drop one / drop all)
13
13
  - Optional JEI (item browser) panel on the left or right
14
14
  - Bot connector layer — plugs into a mineflayer bot or any custom server connection
15
15
  - Demo connector with action logging for local development
@@ -414,13 +414,13 @@ const myConnector: InventoryConnector = {
414
414
 
415
415
  ## Mobile Support
416
416
 
417
- On touch devices, tapping a slot with no held item opens a context menu instead of immediately picking up the item. The menu appears to the side in landscape or below in portrait.
417
+ On touch devices, tapping a slot uses the mobile focus/swap flow. Long-pressing a populated slot with no held item opens a context menu. The menu appears to the side in landscape or below in portrait.
418
418
 
419
419
  Context menu options:
420
- - **Take All** — picks up the entire stack
421
- - **Take Half**picks up half
422
- - **Take Amount…**`window.prompt` to enter a custom quantity
423
- - **Drop** — drops the stack from the slot
420
+ - **Pick Half** — picks up half and highlights the selected slot
421
+ - **Pick Amount…**`window.prompt` to enter a custom quantity, remembering the last value entered
422
+ - **Drop One**drops a single item from the slot (same as `Q`)
423
+ - **Drop All** — drops the full stack from the slot (same as `Ctrl+Q`)
424
424
  - **Cancel**
425
425
 
426
426
  When you have a held item, tapping a slot places/transfers it (same as left-click on desktop).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minecraft-inventory",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "release": {
@@ -310,7 +310,7 @@ export function InventoryOverlay({
310
310
  lineHeight: 1,
311
311
  }}
312
312
  >
313
- INV 0.1.23
313
+ INV 0.1.25
314
314
  </a>
315
315
  )}
316
316
 
@@ -54,6 +54,8 @@ export function Slot({
54
54
  setPKeyActive,
55
55
  focusedSlot,
56
56
  setFocusedSlot,
57
+ mobilePickAmount,
58
+ setMobilePickAmount,
57
59
  dragEndedRef,
58
60
  noPlaceholders,
59
61
  } = useInventoryContext()
@@ -96,6 +98,13 @@ export function Slot({
96
98
  }
97
99
  }, [label, item, renderSize])
98
100
 
101
+ // Clean up long press timer on unmount
102
+ useEffect(() => {
103
+ return () => {
104
+ if (longPressTimerRef.current) clearTimeout(longPressTimerRef.current)
105
+ }
106
+ }, [])
107
+
99
108
  const isHovered = hoveredSlot === index
100
109
  const isDragTarget = dragSlots.includes(index)
101
110
  const dragPreviewEntry = dragPreview.get(index)
@@ -247,19 +256,63 @@ export function Slot({
247
256
 
248
257
  // Mobile touch handlers
249
258
  const touchStartRef = useRef<{ x: number; y: number; time: number } | null>(null)
259
+ const longPressTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
260
+ const longPressFiredRef = useRef(false)
261
+
262
+ const cancelLongPress = useCallback(() => {
263
+ if (longPressTimerRef.current) {
264
+ clearTimeout(longPressTimerRef.current)
265
+ longPressTimerRef.current = null
266
+ }
267
+ }, [])
250
268
 
251
269
  const handleTouchStart = useCallback(
252
270
  (e: React.TouchEvent) => {
253
271
  if (!isMobile) return
254
272
  const touch = e.touches[0]
255
273
  touchStartRef.current = { x: touch.clientX, y: touch.clientY, time: Date.now() }
274
+ longPressFiredRef.current = false
275
+ cancelLongPress()
276
+ // Long press: open mobile menu after 400ms if item exists and no held item
277
+ if (item && !heldItem && !disabled) {
278
+ const startX = touch.clientX
279
+ const startY = touch.clientY
280
+ longPressTimerRef.current = setTimeout(() => {
281
+ longPressFiredRef.current = true
282
+ setMobileTouchPos({ x: startX, y: startY })
283
+ setMobileMenuOpen(true)
284
+ }, 400)
285
+ }
256
286
  },
257
- [isMobile],
287
+ [isMobile, item, heldItem, disabled, cancelLongPress],
288
+ )
289
+
290
+ const handleTouchMove = useCallback(
291
+ (e: React.TouchEvent) => {
292
+ if (!longPressTimerRef.current) return
293
+ const touch = e.touches[0]
294
+ const start = touchStartRef.current
295
+ if (!start) return
296
+ if (Math.abs(touch.clientX - start.x) > 10 || Math.abs(touch.clientY - start.y) > 10) {
297
+ cancelLongPress()
298
+ }
299
+ },
300
+ [cancelLongPress],
258
301
  )
259
302
 
260
303
  const handleTouchEnd = useCallback(
261
304
  (e: React.TouchEvent) => {
305
+ cancelLongPress()
262
306
  if (!isMobile || disabled) return
307
+ // If long press opened the menu, don't process the tap
308
+ if (longPressFiredRef.current) {
309
+ longPressFiredRef.current = false
310
+ e.stopPropagation()
311
+ e.preventDefault()
312
+ return
313
+ }
314
+ // If mobile menu is open, let menu buttons handle their own events
315
+ if (mobileMenuOpen) return
263
316
  const start = touchStartRef.current
264
317
  if (!start) return
265
318
  touchStartRef.current = null
@@ -280,6 +333,7 @@ export function Slot({
280
333
 
281
334
  if (heldItem) {
282
335
  // When holding an item, place it (standard behavior, no focus needed)
336
+ if (focusedSlot !== null) setFocusedSlot(null)
283
337
  sendAction({ type: 'click', slotIndex: index, button: 'left', mode: 'normal' })
284
338
  return
285
339
  }
@@ -297,38 +351,51 @@ export function Slot({
297
351
  setFocusedSlot(null)
298
352
  }
299
353
  },
300
- [isMobile, disabled, heldItem, sendAction, index, pKeyActive, setPKeyActive, focusedSlot, setFocusedSlot, onClickOverride],
354
+ [isMobile, disabled, heldItem, sendAction, index, pKeyActive, setPKeyActive, focusedSlot, setFocusedSlot, onClickOverride, cancelLongPress, mobileMenuOpen],
301
355
  )
302
356
 
303
- const handleMobilePickAll = useCallback(() => {
304
- setMobileMenuOpen(false)
305
- setShowTooltip(false)
306
- sendAction({ type: 'click', slotIndex: index, button: 'left', mode: 'normal' })
307
- }, [sendAction, index])
308
-
309
357
  const handleMobilePickHalf = useCallback(() => {
310
358
  setMobileMenuOpen(false)
311
359
  setShowTooltip(false)
360
+ setFocusedSlot(index)
312
361
  sendAction({ type: 'click', slotIndex: index, button: 'right', mode: 'normal' })
313
- }, [sendAction, index])
362
+ }, [sendAction, index, setFocusedSlot])
314
363
 
315
364
  const handleMobilePickCustom = useCallback(() => {
316
365
  if (!item) return
317
- const input = window.prompt(`Pick amount (max ${item.count}):`, String(item.count))
366
+ const input = window.prompt(`Pick amount (max ${item.count}):`, String(mobilePickAmount))
318
367
  const amount = parseInt(input ?? '', 10)
319
368
  if (isNaN(amount) || amount <= 0) return
369
+ setMobilePickAmount(amount)
320
370
  setMobileMenuOpen(false)
321
371
  setShowTooltip(false)
322
- for (let i = 0; i < Math.min(amount, item.count); i++) {
323
- sendAction({ type: 'click', slotIndex: index, button: 'right', mode: 'normal' })
372
+ setFocusedSlot(index)
373
+ const take = Math.min(amount, item.count)
374
+ if (take >= item.count) {
375
+ // Pick all: just left-click
376
+ sendAction({ type: 'click', slotIndex: index, button: 'left', mode: 'normal' })
377
+ } else {
378
+ // Pick up all, then put back (count - take) items one-by-one via right-click
379
+ sendAction({ type: 'click', slotIndex: index, button: 'left', mode: 'normal' })
380
+ for (let i = 0; i < item.count - take; i++) {
381
+ sendAction({ type: 'click', slotIndex: index, button: 'right', mode: 'normal' })
382
+ }
324
383
  }
325
- }, [item, sendAction, index])
384
+ }, [item, mobilePickAmount, sendAction, index, setFocusedSlot, setMobilePickAmount])
385
+
386
+ const handleMobileDropOne = useCallback(() => {
387
+ setMobileMenuOpen(false)
388
+ setShowTooltip(false)
389
+ setFocusedSlot(null)
390
+ sendAction({ type: 'drop', slotIndex: index, all: false })
391
+ }, [sendAction, index, setFocusedSlot])
326
392
 
327
- const handleMobileDrop = useCallback(() => {
393
+ const handleMobileDropAll = useCallback(() => {
328
394
  setMobileMenuOpen(false)
329
395
  setShowTooltip(false)
396
+ setFocusedSlot(null)
330
397
  sendAction({ type: 'drop', slotIndex: index, all: true })
331
- }, [sendAction, index])
398
+ }, [sendAction, index, setFocusedSlot])
332
399
 
333
400
  const closeMobileMenu = useCallback(() => {
334
401
  setMobileMenuOpen(false)
@@ -368,6 +435,7 @@ export function Slot({
368
435
  onDoubleClick={handleDoubleClick}
369
436
  onContextMenu={handleContextMenu}
370
437
  onTouchStart={handleTouchStart}
438
+ onTouchMove={handleTouchMove}
371
439
  onTouchEnd={handleTouchEnd}
372
440
  aria-label={
373
441
  label ??
@@ -462,15 +530,20 @@ export function Slot({
462
530
 
463
531
  {isMobile && mobileMenuOpen && item && (
464
532
  <>
465
- <div className={styles.mobileOverlay} onClick={closeMobileMenu} />
533
+ <div
534
+ className={styles.mobileOverlay}
535
+ onClick={closeMobileMenu}
536
+ onTouchStart={(e) => e.stopPropagation()}
537
+ onTouchEnd={(e) => { e.stopPropagation(); e.preventDefault(); closeMobileMenu() }}
538
+ />
466
539
  <MobileSlotMenu
467
540
  item={item}
468
541
  x={mobileTouchPos.x}
469
542
  y={mobileTouchPos.y}
470
- onPickAll={handleMobilePickAll}
471
543
  onPickHalf={handleMobilePickHalf}
472
544
  onPickCustom={handleMobilePickCustom}
473
- onDrop={handleMobileDrop}
545
+ onDropOne={handleMobileDropOne}
546
+ onDropAll={handleMobileDropAll}
474
547
  onClose={closeMobileMenu}
475
548
  />
476
549
  </>
@@ -483,14 +556,14 @@ interface MobileSlotMenuProps {
483
556
  item: ItemStack
484
557
  x: number
485
558
  y: number
486
- onPickAll(): void
487
559
  onPickHalf(): void
488
560
  onPickCustom(): void
489
- onDrop(): void
561
+ onDropOne(): void
562
+ onDropAll(): void
490
563
  onClose(): void
491
564
  }
492
565
 
493
- function MobileSlotMenu({ item, x, y, onPickAll, onPickHalf, onPickCustom, onDrop, onClose }: MobileSlotMenuProps) {
566
+ function MobileSlotMenu({ item, x, y, onPickHalf, onPickCustom, onDropOne, onDropAll, onClose }: MobileSlotMenuProps) {
494
567
  const { scale } = useScale()
495
568
  const menuRef = useRef<HTMLDivElement>(null)
496
569
  const [pos, setPos] = useState({ left: x, top: y })
@@ -509,10 +582,18 @@ function MobileSlotMenu({ item, x, y, onPickAll, onPickHalf, onPickCustom, onDro
509
582
  setPos({ left, top })
510
583
  }, [x, y])
511
584
 
585
+ // Wrapper to handle both touch and click, preventing event bubbling to the slot
586
+ const touchBtn = (handler: () => void) => ({
587
+ onTouchEnd: (e: React.TouchEvent) => { e.stopPropagation(); e.preventDefault(); handler() },
588
+ onClick: (e: React.MouseEvent) => { e.stopPropagation(); handler() },
589
+ })
590
+
512
591
  return (
513
592
  <div
514
593
  ref={menuRef}
515
594
  className={styles.mobileMenu}
595
+ onTouchStart={(e) => e.stopPropagation()}
596
+ onTouchEnd={(e) => e.stopPropagation()}
516
597
  style={{
517
598
  position: 'fixed',
518
599
  left: pos.left,
@@ -527,11 +608,11 @@ function MobileSlotMenu({ item, x, y, onPickAll, onPickHalf, onPickCustom, onDro
527
608
  <div className={styles.mobileMenuTitle}>
528
609
  {item.displayName ?? item.name ?? `Item #${item.type}`} ×{item.count}
529
610
  </div>
530
- <button className={styles.mobileBtn} onClick={onPickAll}>Take All ({item.count})</button>
531
- <button className={styles.mobileBtn} onClick={onPickHalf}>Take Half ({Math.ceil(item.count / 2)})</button>
532
- <button className={styles.mobileBtn} onClick={onPickCustom}>Take Amount…</button>
533
- <button className={[styles.mobileBtn, styles.mobileBtnDanger].join(' ')} onClick={onDrop}>Drop</button>
534
- <button className={styles.mobileBtn} onClick={onClose}>Cancel</button>
611
+ <button className={styles.mobileBtn} {...touchBtn(onPickHalf)}>Pick Half ({Math.ceil(item.count / 2)})</button>
612
+ <button className={styles.mobileBtn} {...touchBtn(onPickCustom)}>Pick Amount…</button>
613
+ <button className={[styles.mobileBtn, styles.mobileBtnDanger].join(' ')} {...touchBtn(onDropOne)}>Drop One</button>
614
+ <button className={[styles.mobileBtn, styles.mobileBtnDanger].join(' ')} {...touchBtn(onDropAll)}>Drop All</button>
615
+ <button className={styles.mobileBtn} {...touchBtn(onClose)}>Cancel</button>
535
616
  </div>
536
617
  )
537
618
  }
@@ -44,6 +44,9 @@ export interface InventoryContextValue {
44
44
  /** Pending first digit for P-key slot number entry */
45
45
  pKeyDigit: string
46
46
  setPKeyDigit: (d: string) => void
47
+ /** Last amount entered in the mobile pick-amount prompt */
48
+ mobilePickAmount: number
49
+ setMobilePickAmount: (amount: number) => void
47
50
  /** Ref set to true when a drag just ended; cleared on next mouseDown.
48
51
  * Used by Slot to suppress spurious click events that fire after endDrag. */
49
52
  dragEndedRef: React.MutableRefObject<boolean>
@@ -90,6 +93,7 @@ export function InventoryProvider({ connector, children, noDragSpread = false, n
90
93
  const [pKeyActive, setPKeyActive] = useState(false)
91
94
  const [focusedSlot, setFocusedSlot] = useState<number | null>(null)
92
95
  const [pKeyDigit, setPKeyDigit] = useState('')
96
+ const [mobilePickAmount, setMobilePickAmount] = useState(1)
93
97
 
94
98
  const connectorRef = useRef(connector)
95
99
  connectorRef.current = connector
@@ -320,6 +324,8 @@ export function InventoryProvider({ connector, children, noDragSpread = false, n
320
324
  setFocusedSlot,
321
325
  pKeyDigit,
322
326
  setPKeyDigit,
327
+ mobilePickAmount,
328
+ setMobilePickAmount,
323
329
  dragEndedRef,
324
330
  resolveEnchantmentName,
325
331
  }