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
|
|
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
|
|
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
|
-
- **
|
|
421
|
-
- **
|
|
422
|
-
- **
|
|
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
|
@@ -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(
|
|
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
|
-
|
|
323
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
561
|
+
onDropOne(): void
|
|
562
|
+
onDropAll(): void
|
|
490
563
|
onClose(): void
|
|
491
564
|
}
|
|
492
565
|
|
|
493
|
-
function MobileSlotMenu({ item, x, y,
|
|
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}
|
|
531
|
-
<button className={styles.mobileBtn}
|
|
532
|
-
<button className={styles.mobileBtn}
|
|
533
|
-
<button className={[styles.mobileBtn, styles.mobileBtnDanger].join(' ')}
|
|
534
|
-
<button className={styles.mobileBtn}
|
|
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
|
}
|