minecraft-inventory 0.1.0

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.
Files changed (51) hide show
  1. package/README.md +566 -0
  2. package/package.json +37 -0
  3. package/src/InventoryGUI.tsx +108 -0
  4. package/src/cache/textureCache.ts +95 -0
  5. package/src/components/CursorItem/CursorItem.tsx +94 -0
  6. package/src/components/CursorItem/index.ts +1 -0
  7. package/src/components/HUD/HUD.tsx +11 -0
  8. package/src/components/HUD/index.ts +1 -0
  9. package/src/components/Hotbar/Hotbar.tsx +180 -0
  10. package/src/components/Hotbar/index.ts +1 -0
  11. package/src/components/InventoryOverlay/InventoryOverlay.tsx +196 -0
  12. package/src/components/InventoryOverlay/index.ts +2 -0
  13. package/src/components/InventoryWindow/EnchantmentOptions.tsx +109 -0
  14. package/src/components/InventoryWindow/InventoryBackground.tsx +110 -0
  15. package/src/components/InventoryWindow/InventoryWindow.tsx +120 -0
  16. package/src/components/InventoryWindow/ProgressBar.tsx +78 -0
  17. package/src/components/InventoryWindow/VillagerTradeList.tsx +136 -0
  18. package/src/components/InventoryWindow/index.ts +5 -0
  19. package/src/components/ItemCanvas/ItemCanvas.tsx +154 -0
  20. package/src/components/ItemCanvas/index.ts +1 -0
  21. package/src/components/JEI/JEI.module.css +37 -0
  22. package/src/components/JEI/JEI.tsx +303 -0
  23. package/src/components/JEI/index.ts +2 -0
  24. package/src/components/RecipeGuide/RecipeInventoryView.tsx +293 -0
  25. package/src/components/RecipeGuide/index.ts +1 -0
  26. package/src/components/Slot/Slot.module.css +111 -0
  27. package/src/components/Slot/Slot.tsx +363 -0
  28. package/src/components/Slot/index.ts +1 -0
  29. package/src/components/Text/MessageFormatted.css +5 -0
  30. package/src/components/Text/MessageFormatted.tsx +79 -0
  31. package/src/components/Text/MessageFormattedString.tsx +74 -0
  32. package/src/components/Text/chatUtils.ts +172 -0
  33. package/src/components/Tooltip/Tooltip.module.css +56 -0
  34. package/src/components/Tooltip/Tooltip.tsx +130 -0
  35. package/src/components/Tooltip/index.ts +1 -0
  36. package/src/connector/demo.ts +213 -0
  37. package/src/connector/index.ts +4 -0
  38. package/src/connector/mineflayer.ts +113 -0
  39. package/src/connector/types.ts +41 -0
  40. package/src/context/InventoryContext.tsx +157 -0
  41. package/src/context/ScaleContext.tsx +73 -0
  42. package/src/context/TextureContext.tsx +70 -0
  43. package/src/globals.d.ts +4 -0
  44. package/src/hooks/useKeyboardShortcuts.ts +41 -0
  45. package/src/hooks/useMobile.ts +28 -0
  46. package/src/index.tsx +65 -0
  47. package/src/mount.tsx +52 -0
  48. package/src/registry/index.ts +21 -0
  49. package/src/registry/inventories.ts +612 -0
  50. package/src/styles/tokens.css +47 -0
  51. package/src/types.ts +176 -0
@@ -0,0 +1,110 @@
1
+ import React from 'react'
2
+ import { useTextures } from '../../context/TextureContext'
3
+ import { useScale } from '../../context/ScaleContext'
4
+ import { MessageFormattedString } from '../Text/MessageFormattedString'
5
+ import type { InventoryTypeDefinition } from '../../registry'
6
+
7
+ interface InventoryBackgroundProps {
8
+ definition: InventoryTypeDefinition
9
+ children: React.ReactNode
10
+ title?: string
11
+ }
12
+
13
+ export function InventoryBackground({
14
+ definition,
15
+ children,
16
+ title,
17
+ }: InventoryBackgroundProps) {
18
+ const textures = useTextures()
19
+ const { scale } = useScale()
20
+
21
+ const bgUrl = textures.getGuiTextureUrl(definition.backgroundTexture, definition.guiTextureVersion)
22
+ const w = definition.backgroundWidth * scale
23
+ const h = definition.backgroundHeight * scale
24
+
25
+ // Source dimensions from definition (e.g., 176x166) — clip to this region from texture
26
+ const srcW = definition.backgroundWidth
27
+ const srcH = definition.backgroundHeight
28
+
29
+ return (
30
+ <div
31
+ className="mc-inv-background"
32
+ style={{
33
+ position: 'relative',
34
+ width: w,
35
+ height: h,
36
+ imageRendering: 'pixelated',
37
+ display: 'inline-block',
38
+ outline: '1px solid rgba(255, 0, 0, 0.7)',
39
+ outlineOffset: 0,
40
+ }}
41
+ >
42
+ {/* Background texture wrapper — clips source to srcW×srcH, then scales */}
43
+ <div
44
+ className="mc-inv-background-wrapper"
45
+ style={{
46
+ position: 'absolute',
47
+ top: 0,
48
+ left: 0,
49
+ width: srcW,
50
+ height: srcH,
51
+ overflow: 'hidden',
52
+ transform: `scale(${scale})`,
53
+ transformOrigin: 'top left',
54
+ }}
55
+ >
56
+ {/* Background texture — render at natural size, clipped by wrapper overflow */}
57
+ <img
58
+ className="mc-inv-background-image"
59
+ src={bgUrl}
60
+ alt=""
61
+ aria-hidden
62
+ style={{
63
+ display: 'block',
64
+ width: srcW,
65
+ height: srcH,
66
+ imageRendering: 'pixelated',
67
+ pointerEvents: 'none',
68
+ userSelect: 'none',
69
+ // Clip to top-left srcW×srcH region (if texture is larger)
70
+ objectFit: 'none',
71
+ objectPosition: '0 0',
72
+ }}
73
+ draggable={false}
74
+ />
75
+ </div>
76
+
77
+ {/* Title */}
78
+ {title !== undefined && (
79
+ <div
80
+ className="mc-inv-background-title"
81
+ style={{
82
+ position: 'absolute',
83
+ top: 6 * scale,
84
+ left: 8 * scale,
85
+ fontSize: 7 * scale,
86
+ fontFamily: "'Minecraftia', 'Minecraft', monospace",
87
+ pointerEvents: 'none',
88
+ lineHeight: 1,
89
+ }}
90
+ >
91
+ <MessageFormattedString text={title} fallbackColor="#404040" />
92
+ </div>
93
+ )}
94
+
95
+ {/* Slot layer */}
96
+ <div
97
+ className="mc-inv-background-slots"
98
+ style={{
99
+ position: 'absolute',
100
+ top: 0,
101
+ left: 0,
102
+ width: w,
103
+ height: h,
104
+ }}
105
+ >
106
+ {children}
107
+ </div>
108
+ </div>
109
+ )
110
+ }
@@ -0,0 +1,120 @@
1
+ import React, { useMemo } from 'react'
2
+ import type { SlotState } from '../../types'
3
+ import { getInventoryType } from '../../registry'
4
+ import { useInventoryContext } from '../../context/InventoryContext'
5
+ import { useScale } from '../../context/ScaleContext'
6
+ import { useKeyboardShortcuts } from '../../hooks/useKeyboardShortcuts'
7
+ import { Slot } from '../Slot'
8
+ import { InventoryBackground } from './InventoryBackground'
9
+ import { ProgressBar } from './ProgressBar'
10
+ import { VillagerTradeList } from './VillagerTradeList'
11
+ import { EnchantmentOptions } from './EnchantmentOptions'
12
+
13
+ interface InventoryWindowProps {
14
+ type: string
15
+ title?: string
16
+ slots?: SlotState[]
17
+ properties?: Record<string, number>
18
+ className?: string
19
+ style?: React.CSSProperties
20
+ enableKeyboardShortcuts?: boolean
21
+ }
22
+
23
+ export function InventoryWindow({
24
+ type,
25
+ title,
26
+ slots: slotsProp,
27
+ properties = {},
28
+ className,
29
+ style,
30
+ enableKeyboardShortcuts = true,
31
+ }: InventoryWindowProps) {
32
+ const def = getInventoryType(type)
33
+ const { windowState, getSlot } = useInventoryContext()
34
+ const { scale, slotSize, contentSize } = useScale()
35
+ // borderPx removed from slot positioning: registry coords already point to the item area
36
+ const borderPx = 0
37
+
38
+ useKeyboardShortcuts(enableKeyboardShortcuts)
39
+
40
+ const effectiveSlots = useMemo(() => {
41
+ if (slotsProp) return slotsProp
42
+ return windowState?.slots ?? []
43
+ }, [slotsProp, windowState])
44
+
45
+ const effectiveProperties = useMemo(() => {
46
+ return { ...properties, ...(windowState?.properties ?? {}) }
47
+ }, [properties, windowState])
48
+
49
+ if (!def) {
50
+ return (
51
+ <div style={{ color: 'red', padding: 8 }}>
52
+ Unknown inventory type: {type}
53
+ </div>
54
+ )
55
+ }
56
+
57
+ // Player inventory: don't show title (it's just the player's own inventory)
58
+ const effectiveTitle = type === 'player' ? undefined : (title ?? windowState?.title ?? def.title)
59
+ const isVillager = type === 'villager'
60
+ const isEnchanting = type === 'enchanting_table'
61
+
62
+ const resolveItem = (slotIndex: number) => {
63
+ const fromProp = effectiveSlots.find((s) => s.index === slotIndex)
64
+ if (fromProp) return fromProp.item
65
+ return getSlot(slotIndex)?.item ?? null
66
+ }
67
+
68
+ return (
69
+ <div
70
+ className={['mc-inv-root', className].filter(Boolean).join(' ')}
71
+ style={{ position: 'relative', display: 'inline-block', ...style }}
72
+ >
73
+ <InventoryBackground definition={def} title={effectiveTitle}>
74
+ {/* Render slots */}
75
+ {def.slots.map((slotDef) => (
76
+ <div
77
+ key={slotDef.index}
78
+ className="mc-inv-slot-wrapper"
79
+ style={{
80
+ position: 'absolute',
81
+ // Offset by borderPx to land inside the texture's slot cell (skip the 1px border)
82
+ left: slotDef.x * scale + borderPx,
83
+ top: slotDef.y * scale + borderPx,
84
+ }}
85
+ >
86
+ <Slot
87
+ index={slotDef.index}
88
+ item={resolveItem(slotDef.index)}
89
+ size={slotDef.size ? slotDef.size * scale - 2 * borderPx : contentSize}
90
+ resultSlot={slotDef.resultSlot}
91
+ label={slotDef.label}
92
+ />
93
+ </div>
94
+ ))}
95
+
96
+ {/* Progress bars */}
97
+ {def.progressBars?.map((pb) => (
98
+ <ProgressBar
99
+ key={pb.id}
100
+ definition={pb}
101
+ properties={effectiveProperties}
102
+ backgroundTexture={def.backgroundTexture}
103
+ />
104
+ ))}
105
+
106
+ {/* Villager trades */}
107
+ {isVillager && <VillagerTradeList />}
108
+
109
+ {/* Enchanting options */}
110
+ {isEnchanting && (
111
+ <EnchantmentOptions
112
+ properties={effectiveProperties}
113
+ x={60}
114
+ y={14}
115
+ />
116
+ )}
117
+ </InventoryBackground>
118
+ </div>
119
+ )
120
+ }
@@ -0,0 +1,78 @@
1
+ import React from 'react'
2
+ import { useTextures } from '../../context/TextureContext'
3
+ import { useScale } from '../../context/ScaleContext'
4
+ import type { ProgressBar as ProgressBarDef } from '../../types'
5
+
6
+ interface ProgressBarProps {
7
+ definition: ProgressBarDef
8
+ properties: Record<string, number>
9
+ backgroundTexture: string
10
+ }
11
+
12
+ export function ProgressBar({ definition, properties, backgroundTexture }: ProgressBarProps) {
13
+ const textures = useTextures()
14
+ const { scale } = useScale()
15
+
16
+ const value = definition.getValue(properties)
17
+ const max = definition.getMax(properties)
18
+ const ratio = max > 0 ? Math.min(1, Math.max(0, value / max)) : 0
19
+
20
+ const bgUrl = textures.getGuiTextureUrl(backgroundTexture)
21
+ const x = definition.x * scale
22
+ const y = definition.y * scale
23
+ const w = definition.width * scale
24
+ const h = definition.height * scale
25
+ const srcX = definition.textureX
26
+ const srcY = definition.textureY
27
+
28
+ let clipPath: string
29
+ let clipWidth = w
30
+ let clipHeight = h
31
+
32
+ switch (definition.direction) {
33
+ case 'right':
34
+ clipWidth = w * ratio
35
+ break
36
+ case 'left':
37
+ clipWidth = w * ratio
38
+ break
39
+ case 'up':
40
+ clipHeight = h * ratio
41
+ break
42
+ case 'down':
43
+ clipHeight = h * ratio
44
+ break
45
+ }
46
+
47
+ if (ratio <= 0) return null
48
+
49
+ return (
50
+ <div
51
+ style={{
52
+ position: 'absolute',
53
+ left: x,
54
+ top: y,
55
+ width: clipWidth,
56
+ height: clipHeight,
57
+ overflow: 'hidden',
58
+ imageRendering: 'pixelated',
59
+ pointerEvents: 'none',
60
+ }}
61
+ >
62
+ <img
63
+ src={bgUrl}
64
+ alt=""
65
+ aria-hidden
66
+ style={{
67
+ position: 'absolute',
68
+ left: -srcX * scale,
69
+ top: -srcY * scale,
70
+ imageRendering: 'pixelated',
71
+ display: 'block',
72
+ maxWidth: 'none',
73
+ }}
74
+ draggable={false}
75
+ />
76
+ </div>
77
+ )
78
+ }
@@ -0,0 +1,136 @@
1
+ import React, { useState } from 'react'
2
+ import type { TradeOffer } from '../../types'
3
+ import { useInventoryContext } from '../../context/InventoryContext'
4
+ import { useScale } from '../../context/ScaleContext'
5
+ import { ItemCanvas } from '../ItemCanvas'
6
+ import { Tooltip } from '../Tooltip'
7
+
8
+ interface TradeRowProps {
9
+ trade: TradeOffer
10
+ index: number
11
+ selected: boolean
12
+ onSelect(): void
13
+ }
14
+
15
+ function TradeRow({ trade, index, selected, onSelect }: TradeRowProps) {
16
+ const { scale } = useScale()
17
+ const { sendAction } = useInventoryContext()
18
+ const [hoveredItem, setHoveredItem] = useState<'in1' | 'in2' | 'out' | null>(null)
19
+ const slotSize = 16 * scale
20
+
21
+ const handleClick = () => {
22
+ onSelect()
23
+ sendAction({ type: 'trade', tradeIndex: index })
24
+ }
25
+
26
+ return (
27
+ <div
28
+ onClick={handleClick}
29
+ style={{
30
+ display: 'flex',
31
+ alignItems: 'center',
32
+ gap: scale * 2,
33
+ padding: `${scale * 2}px`,
34
+ background: selected ? '#555555' : trade.disabled ? '#222222' : '#404040',
35
+ border: `${scale}px solid ${selected ? '#aaaaaa' : '#666666'}`,
36
+ cursor: trade.disabled ? 'not-allowed' : 'pointer',
37
+ opacity: trade.disabled ? 0.6 : 1,
38
+ width: '100%',
39
+ boxSizing: 'border-box',
40
+ }}
41
+ >
42
+ {/* Input 1 */}
43
+ <div
44
+ style={{ position: 'relative' }}
45
+ onMouseEnter={() => setHoveredItem('in1')}
46
+ onMouseLeave={() => setHoveredItem(null)}
47
+ >
48
+ <ItemCanvas item={trade.inputItem1} size={slotSize} />
49
+ {hoveredItem === 'in1' && (
50
+ <Tooltip item={trade.inputItem1} visible />
51
+ )}
52
+ </div>
53
+
54
+ {/* Input 2 */}
55
+ {trade.inputItem2 && (
56
+ <div
57
+ style={{ position: 'relative' }}
58
+ onMouseEnter={() => setHoveredItem('in2')}
59
+ onMouseLeave={() => setHoveredItem(null)}
60
+ >
61
+ <ItemCanvas item={trade.inputItem2} size={slotSize} />
62
+ {hoveredItem === 'in2' && (
63
+ <Tooltip item={trade.inputItem2} visible />
64
+ )}
65
+ </div>
66
+ )}
67
+
68
+ {/* Arrow */}
69
+ <div style={{ color: '#aaaaaa', fontSize: scale * 8 }}>→</div>
70
+
71
+ {/* Output */}
72
+ <div
73
+ style={{ position: 'relative' }}
74
+ onMouseEnter={() => setHoveredItem('out')}
75
+ onMouseLeave={() => setHoveredItem(null)}
76
+ >
77
+ <ItemCanvas item={trade.outputItem} size={slotSize} />
78
+ {hoveredItem === 'out' && (
79
+ <Tooltip item={trade.outputItem} visible />
80
+ )}
81
+ </div>
82
+
83
+ {/* Uses */}
84
+ <div
85
+ style={{
86
+ marginLeft: 'auto',
87
+ fontSize: scale * 6,
88
+ color: trade.uses >= trade.maxUses ? '#ff5555' : '#aaaaaa',
89
+ whiteSpace: 'nowrap',
90
+ }}
91
+ >
92
+ {trade.uses}/{trade.maxUses}
93
+ </div>
94
+ </div>
95
+ )
96
+ }
97
+
98
+ export function VillagerTradeList() {
99
+ const { windowState } = useInventoryContext()
100
+ const { scale } = useScale()
101
+ const [selectedIndex, setSelectedIndex] = useState(0)
102
+
103
+ const villagerState = windowState as (typeof windowState & {
104
+ trades?: TradeOffer[]
105
+ }) | null
106
+
107
+ const trades = villagerState?.trades ?? []
108
+ if (trades.length === 0) return null
109
+
110
+ return (
111
+ <div
112
+ style={{
113
+ position: 'absolute',
114
+ left: 4 * scale,
115
+ top: 18 * scale,
116
+ width: 100 * scale,
117
+ maxHeight: 130 * scale,
118
+ overflowY: 'auto',
119
+ display: 'flex',
120
+ flexDirection: 'column',
121
+ gap: scale,
122
+ scrollbarWidth: 'thin',
123
+ }}
124
+ >
125
+ {trades.map((trade, i) => (
126
+ <TradeRow
127
+ key={i}
128
+ trade={trade}
129
+ index={i}
130
+ selected={selectedIndex === i}
131
+ onSelect={() => setSelectedIndex(i)}
132
+ />
133
+ ))}
134
+ </div>
135
+ )
136
+ }
@@ -0,0 +1,5 @@
1
+ export { InventoryWindow } from './InventoryWindow'
2
+ export { InventoryBackground } from './InventoryBackground'
3
+ export { ProgressBar } from './ProgressBar'
4
+ export { VillagerTradeList } from './VillagerTradeList'
5
+ export { EnchantmentOptions } from './EnchantmentOptions'
@@ -0,0 +1,154 @@
1
+ import React, { useState, useEffect, memo } from 'react'
2
+ import type { ItemStack } from '../../types'
3
+ import { useTextures } from '../../context/TextureContext'
4
+ import { useScale } from '../../context/ScaleContext'
5
+ import { useDataUrl, isTextureFailed } from '../../cache/textureCache'
6
+
7
+ interface ItemCanvasProps {
8
+ item: ItemStack
9
+ size?: number
10
+ showCount?: boolean
11
+ showDurability?: boolean
12
+ className?: string
13
+ style?: React.CSSProperties
14
+ }
15
+
16
+ function getDurabilityColor(current: number, max: number): string {
17
+ const ratio = current / max
18
+ if (ratio > 0.6) return '#55ff55'
19
+ if (ratio > 0.3) return '#ffff55'
20
+ return '#ff5555'
21
+ }
22
+
23
+ /** Renders a single item: texture as <img>, count as <span>, durability as CSS bars. */
24
+ export const ItemCanvas = memo(function ItemCanvas({
25
+ item,
26
+ size,
27
+ showCount = true,
28
+ showDurability = true,
29
+ className,
30
+ style,
31
+ }: ItemCanvasProps) {
32
+ const textures = useTextures()
33
+ const { contentSize, pixelSize } = useScale()
34
+ const renderSize = size ?? contentSize
35
+
36
+ const primaryUrl = textures.getItemTextureUrl(item)
37
+ const fallbackUrl = item.name ? textures.getBlockTextureUrl(item) : null
38
+
39
+ // Load primary URL as cached data URL
40
+ const primaryDataUrl = useDataUrl(primaryUrl)
41
+ const primaryFailed = primaryDataUrl === null || isTextureFailed(primaryUrl)
42
+
43
+ // Load fallback only once primary is known to have failed
44
+ const [loadFallback, setLoadFallback] = useState(false)
45
+ useEffect(() => {
46
+ if (primaryFailed && fallbackUrl) setLoadFallback(true)
47
+ else setLoadFallback(false)
48
+ }, [primaryFailed, fallbackUrl])
49
+ const fallbackDataUrl = useDataUrl(loadFallback ? fallbackUrl : null)
50
+
51
+ const src = loadFallback ? fallbackDataUrl : primaryDataUrl
52
+ const failed = loadFallback
53
+ ? (fallbackDataUrl === null || isTextureFailed(fallbackUrl ?? ''))
54
+ : primaryFailed && !fallbackUrl
55
+
56
+ const hasDurability =
57
+ showDurability &&
58
+ item.durability !== undefined &&
59
+ item.maxDurability !== undefined &&
60
+ item.durability < item.maxDurability
61
+
62
+ const barColor = hasDurability
63
+ ? getDurabilityColor(item.durability!, item.maxDurability!)
64
+ : '#55ff55'
65
+
66
+ // Font size for count: ~38% of slot size, min 7px, shadow offset = 1 scaled pixel
67
+ const countFontSize = Math.max(7, Math.round(renderSize * 0.38))
68
+ const shadow = Math.max(1, Math.round(pixelSize))
69
+
70
+ return (
71
+ <div
72
+ className={['mc-inv-item', className].filter(Boolean).join(' ')}
73
+ style={{
74
+ position: 'relative',
75
+ width: renderSize,
76
+ height: renderSize,
77
+ flexShrink: 0,
78
+ ...style,
79
+ }}
80
+ >
81
+ {failed ? (
82
+ /* Missing texture — magenta/black checkerboard */
83
+ <div className="mc-inv-item-placeholder" style={{
84
+ width: '100%',
85
+ height: '100%',
86
+ background: 'linear-gradient(45deg, #8b008b 25%, #000 25%, #000 50%, #8b008b 50%, #8b008b 75%, #000 75%)',
87
+ backgroundSize: `${renderSize / 2}px ${renderSize / 2}px`,
88
+ }} />
89
+ ) : src ? (
90
+ /* Loaded — render cached data URL; no onError needed */
91
+ <img
92
+ className="mc-inv-item-image"
93
+ src={src}
94
+ alt=""
95
+ aria-hidden
96
+ draggable={false}
97
+ style={{
98
+ width: '100%',
99
+ height: '100%',
100
+ imageRendering: 'pixelated',
101
+ display: 'block',
102
+ pointerEvents: 'none',
103
+ }}
104
+ />
105
+ ) : null /* still loading — render nothing (slot stays empty) */}
106
+
107
+ {/* Durability bar */}
108
+ {hasDurability && (
109
+ <>
110
+ <div className="mc-inv-item-durability-bg" style={{
111
+ position: 'absolute',
112
+ bottom: 1,
113
+ left: 1,
114
+ width: 'calc(100% - 2px)',
115
+ height: Math.max(1, Math.round(renderSize * 0.063)),
116
+ background: '#000',
117
+ pointerEvents: 'none',
118
+ }} />
119
+ <div className="mc-inv-item-durability-bar" style={{
120
+ position: 'absolute',
121
+ bottom: 1,
122
+ left: 1,
123
+ width: `${(item.durability! / item.maxDurability!) * 100}%`,
124
+ height: Math.max(1, Math.round(renderSize * 0.063)),
125
+ background: barColor,
126
+ pointerEvents: 'none',
127
+ }} />
128
+ </>
129
+ )}
130
+
131
+ {/* Item count */}
132
+ {showCount && item.count > 1 && (
133
+ <span className="mc-inv-item-count" style={{
134
+ position: 'absolute',
135
+ bottom: 0,
136
+ right: 0,
137
+ fontSize: countFontSize,
138
+ fontFamily: "'Minecraftia', 'Minecraft', monospace",
139
+ fontWeight: 'bold',
140
+ color: '#ffffff',
141
+ lineHeight: 1,
142
+ pointerEvents: 'none',
143
+ // Double shadow trick: dark shadow at +1,+1 for Minecraft count text look
144
+ textShadow: `${shadow}px ${shadow}px 0 rgba(0,0,0,0.6)`,
145
+ userSelect: 'none',
146
+ // Prevent wrapping
147
+ whiteSpace: 'nowrap',
148
+ }}>
149
+ {item.count}
150
+ </span>
151
+ )}
152
+ </div>
153
+ )
154
+ })
@@ -0,0 +1 @@
1
+ export { ItemCanvas } from './ItemCanvas'
@@ -0,0 +1,37 @@
1
+ .jei {
2
+ font-family: 'Minecraft', monospace;
3
+ image-rendering: pixelated;
4
+ }
5
+
6
+ .searchInput::placeholder {
7
+ color: rgba(255, 255, 255, 0.5);
8
+ }
9
+
10
+ .pageBtn {
11
+ background: #404040;
12
+ color: #ffffff;
13
+ border: 1px solid #666666;
14
+ cursor: pointer;
15
+ font-family: inherit;
16
+ font-size: inherit;
17
+ transition: background 0.1s;
18
+ }
19
+
20
+ .pageBtn:hover:not(:disabled) {
21
+ background: #555555;
22
+ }
23
+
24
+ .pageBtn:disabled {
25
+ opacity: 0.4;
26
+ cursor: not-allowed;
27
+ }
28
+
29
+ .pageBtnActive {
30
+ background: #8b6914;
31
+ color: #ffcc00;
32
+ border-color: #ffcc00;
33
+ }
34
+
35
+ .pageBtnActive:hover:not(:disabled) {
36
+ background: #a07820;
37
+ }