minecraft-inventory 0.1.0 → 0.1.2
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 +80 -10
- package/package.json +20 -8
- package/src/components/CursorItem/CursorItem.tsx +1 -10
- package/src/components/InventoryOverlay/InventoryOverlay.tsx +126 -56
- package/src/components/InventoryWindow/HotbarExtras.tsx +180 -0
- package/src/components/InventoryWindow/InventoryBackground.tsx +3 -3
- package/src/components/InventoryWindow/InventoryWindow.tsx +11 -0
- package/src/components/JEI/JEI.tsx +98 -29
- package/src/components/Notes/Notes.tsx +237 -0
- package/src/components/Notes/index.ts +2 -0
- package/src/components/RecipeGuide/RecipeInventoryView.tsx +5 -15
- package/src/components/Slot/Slot.tsx +49 -17
- package/src/components/Text/MessageFormatted.tsx +1 -1
- package/src/components/Tooltip/Tooltip.module.css +14 -14
- package/src/components/Tooltip/Tooltip.tsx +31 -24
- package/src/connector/demo.ts +4 -0
- package/src/connector/mineflayer.ts +145 -1
- package/src/connector/types.ts +39 -9
- package/src/context/TextureContext.tsx +17 -11
- package/src/generated/localTextures.ts +112 -0
- package/src/globals.d.ts +4 -0
- package/src/index.tsx +3 -2
- package/src/registry/index.ts +1 -0
- package/src/registry/inventories.ts +203 -195
- package/src/types.ts +4 -2
- package/src/utils/globalMouse.ts +14 -0
- package/src/components/Hotbar/Hotbar.tsx +0 -180
- package/src/components/Hotbar/index.ts +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState, useMemo, useCallback, useEffect, useRef } from 'react'
|
|
1
|
+
import React, { useState, useMemo, useCallback, useEffect, useLayoutEffect, useRef } from 'react'
|
|
2
2
|
import type { ItemStack, RecipeGuide, RecipeNavFrame } from '../../types'
|
|
3
3
|
import { useScale } from '../../context/ScaleContext'
|
|
4
4
|
import { useInventoryContext } from '../../context/InventoryContext'
|
|
@@ -16,7 +16,12 @@ export interface JEIItem {
|
|
|
16
16
|
interface JEIProps {
|
|
17
17
|
items?: JEIItem[]
|
|
18
18
|
position?: 'left' | 'right'
|
|
19
|
+
/** Override number of item columns (default: ITEMS_PER_ROW=6). Set by InventoryOverlay from slot-grid calc. */
|
|
20
|
+
cols?: number
|
|
21
|
+
/** Override panel width in px. If omitted, derived from cols. */
|
|
19
22
|
width?: number
|
|
23
|
+
/** Max height in px for the panel. Controls rows visible. */
|
|
24
|
+
maxHeight?: number
|
|
20
25
|
onItemClick?: (item: JEIItem) => void
|
|
21
26
|
onItemMiddleClick?: (item: JEIItem) => void
|
|
22
27
|
/** Called when R is pressed while hovering an item — return list of recipes to display */
|
|
@@ -55,7 +60,9 @@ function saveFavorites(favs: Set<string>) {
|
|
|
55
60
|
export function JEI({
|
|
56
61
|
items = [],
|
|
57
62
|
position = 'right',
|
|
63
|
+
cols: colsProp,
|
|
58
64
|
width,
|
|
65
|
+
maxHeight,
|
|
59
66
|
onItemClick,
|
|
60
67
|
onItemMiddleClick,
|
|
61
68
|
onGetRecipes,
|
|
@@ -73,6 +80,38 @@ export function JEI({
|
|
|
73
80
|
// Map from negative slot index → JEI item (to enable F-key and R/U on hover)
|
|
74
81
|
const slotToItemRef = useRef<Map<number, JEIItem>>(new Map())
|
|
75
82
|
|
|
83
|
+
// Self-measured dimensions so the panel can be width:100% and fill its container
|
|
84
|
+
const rootRef = useRef<HTMLDivElement>(null)
|
|
85
|
+
const gridRef = useRef<HTMLDivElement>(null)
|
|
86
|
+
const [measuredCols, setMeasuredCols] = useState(ITEMS_PER_ROW)
|
|
87
|
+
const [measuredRows, setMeasuredRows] = useState(5)
|
|
88
|
+
|
|
89
|
+
useLayoutEffect(() => {
|
|
90
|
+
const root = rootRef.current
|
|
91
|
+
const grid = gridRef.current
|
|
92
|
+
if (!root || !grid) return
|
|
93
|
+
|
|
94
|
+
// Track latest sizes across both entries
|
|
95
|
+
const sizes = { rootW: 0, gridH: 0 }
|
|
96
|
+
|
|
97
|
+
const ro = new ResizeObserver((entries) => {
|
|
98
|
+
for (const entry of entries) {
|
|
99
|
+
if (entry.target === (root as unknown as Element)) {
|
|
100
|
+
sizes.rootW = entry.contentRect.width
|
|
101
|
+
} else if (entry.target === (grid as unknown as Element)) {
|
|
102
|
+
sizes.gridH = entry.contentRect.height
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const innerPad = PADDING * scale * 2
|
|
106
|
+
setMeasuredCols(Math.max(1, Math.floor((sizes.rootW - innerPad) / contentSize)))
|
|
107
|
+
setMeasuredRows(Math.max(1, Math.floor(sizes.gridH / contentSize)))
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
ro.observe(root as unknown as Element)
|
|
111
|
+
ro.observe(grid as unknown as Element)
|
|
112
|
+
return () => ro.disconnect()
|
|
113
|
+
}, [scale, contentSize])
|
|
114
|
+
|
|
76
115
|
const pushRecipeFrame = useCallback((targetItem: JEIItem, mode: 'recipes' | 'usages') => {
|
|
77
116
|
const getter = mode === 'recipes' ? onGetRecipes : onGetUsages
|
|
78
117
|
if (!getter || !onPushRecipeFrame) return
|
|
@@ -83,9 +122,12 @@ export function JEI({
|
|
|
83
122
|
|
|
84
123
|
const padding = PADDING * scale
|
|
85
124
|
const itemSize = contentSize
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
125
|
+
// cols: explicit prop > internal measurement > default
|
|
126
|
+
const cols = colsProp ?? measuredCols
|
|
127
|
+
// panelWidth: explicit prop > 100% (container controls width)
|
|
128
|
+
const panelWidth = width ?? '100%'
|
|
129
|
+
// rows: explicit maxHeight override > internal measurement
|
|
130
|
+
const rows = maxHeight ? Math.max(1, Math.floor(maxHeight / itemSize)) : measuredRows
|
|
89
131
|
const itemsPerPage = cols * rows
|
|
90
132
|
|
|
91
133
|
const toggleFavorite = useCallback((key: string) => {
|
|
@@ -149,7 +191,7 @@ export function JEI({
|
|
|
149
191
|
}, [])
|
|
150
192
|
|
|
151
193
|
const handleWheel = useCallback(
|
|
152
|
-
(e:
|
|
194
|
+
(e: WheelEvent) => {
|
|
153
195
|
e.preventDefault()
|
|
154
196
|
setPage((prev) => {
|
|
155
197
|
if (e.deltaY > 0) return Math.min(prev + 1, totalPages - 1)
|
|
@@ -159,6 +201,14 @@ export function JEI({
|
|
|
159
201
|
[totalPages],
|
|
160
202
|
)
|
|
161
203
|
|
|
204
|
+
// Non-passive wheel listener so preventDefault() actually suppresses page scroll.
|
|
205
|
+
useEffect(() => {
|
|
206
|
+
const el = gridRef.current
|
|
207
|
+
if (!el) return
|
|
208
|
+
el.addEventListener('wheel', handleWheel, { passive: false })
|
|
209
|
+
return () => el.removeEventListener('wheel', handleWheel)
|
|
210
|
+
}, [handleWheel])
|
|
211
|
+
|
|
162
212
|
// Build slot→item map on each render
|
|
163
213
|
slotToItemRef.current.clear()
|
|
164
214
|
visibleItems.forEach((jeiItem, i) => {
|
|
@@ -168,18 +218,27 @@ export function JEI({
|
|
|
168
218
|
|
|
169
219
|
return (
|
|
170
220
|
<div
|
|
221
|
+
ref={rootRef}
|
|
171
222
|
className={['mc-inv-root', styles.jei, className].filter(Boolean).join(' ')}
|
|
172
223
|
style={{
|
|
173
224
|
width: panelWidth,
|
|
174
|
-
|
|
175
|
-
border: `${scale}px solid #555555`,
|
|
225
|
+
maxHeight: maxHeight ?? undefined,
|
|
176
226
|
display: 'flex',
|
|
177
227
|
flexDirection: 'column',
|
|
228
|
+
overflow: 'hidden',
|
|
178
229
|
...style,
|
|
179
230
|
}}
|
|
180
231
|
>
|
|
181
|
-
{/* Search + pagination controls */}
|
|
182
|
-
<div
|
|
232
|
+
{/* Search + pagination controls — background only here */}
|
|
233
|
+
<div
|
|
234
|
+
className="mc-inv-jei-header"
|
|
235
|
+
style={{
|
|
236
|
+
padding: `${padding}px`,
|
|
237
|
+
background: '#c6c6c6',
|
|
238
|
+
border: `${scale}px solid #555555`,
|
|
239
|
+
flexShrink: 0,
|
|
240
|
+
}}
|
|
241
|
+
>
|
|
183
242
|
<input
|
|
184
243
|
type="text"
|
|
185
244
|
value={search}
|
|
@@ -199,14 +258,17 @@ export function JEI({
|
|
|
199
258
|
}}
|
|
200
259
|
/>
|
|
201
260
|
{/* Prev / Next / Favorites below search bar */}
|
|
202
|
-
<div
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
261
|
+
<div
|
|
262
|
+
className="mc-inv-jei-pagination"
|
|
263
|
+
style={{
|
|
264
|
+
display: 'flex',
|
|
265
|
+
alignItems: 'center',
|
|
266
|
+
gap: scale,
|
|
267
|
+
marginTop: 2 * scale,
|
|
268
|
+
fontSize: 6 * scale,
|
|
269
|
+
fontFamily: "'Minecraftia', 'Minecraft', monospace",
|
|
270
|
+
}}
|
|
271
|
+
>
|
|
210
272
|
<button
|
|
211
273
|
className={styles.pageBtn}
|
|
212
274
|
onClick={() => setPage((p) => Math.max(0, p - 1))}
|
|
@@ -216,7 +278,7 @@ export function JEI({
|
|
|
216
278
|
>
|
|
217
279
|
◀
|
|
218
280
|
</button>
|
|
219
|
-
<span style={{ flex: 1, textAlign: 'center', color: '#404040' }}>
|
|
281
|
+
<span className="mc-inv-jei-page-counter" style={{ flex: 1, textAlign: 'center', color: '#404040' }}>
|
|
220
282
|
{page + 1} / {Math.max(1, totalPages)}
|
|
221
283
|
</span>
|
|
222
284
|
<button
|
|
@@ -241,6 +303,8 @@ export function JEI({
|
|
|
241
303
|
|
|
242
304
|
{/* Item grid — no slot backgrounds, just raw items */}
|
|
243
305
|
<div
|
|
306
|
+
ref={gridRef}
|
|
307
|
+
className="mc-inv-jei-grid"
|
|
244
308
|
style={{
|
|
245
309
|
position: 'relative',
|
|
246
310
|
display: 'flex',
|
|
@@ -251,7 +315,6 @@ export function JEI({
|
|
|
251
315
|
alignContent: 'flex-start',
|
|
252
316
|
overflowY: 'hidden',
|
|
253
317
|
}}
|
|
254
|
-
onWheel={handleWheel}
|
|
255
318
|
>
|
|
256
319
|
{visibleItems.map((jeiItem, i) => {
|
|
257
320
|
const itemStack: ItemStack = {
|
|
@@ -266,6 +329,7 @@ export function JEI({
|
|
|
266
329
|
return (
|
|
267
330
|
<div
|
|
268
331
|
key={`${jeiItem.type}-${jeiItem.metadata ?? 0}-${i}`}
|
|
332
|
+
className="mc-inv-jei-item-wrapper"
|
|
269
333
|
style={{ position: 'relative' }}
|
|
270
334
|
>
|
|
271
335
|
<Slot
|
|
@@ -282,16 +346,21 @@ export function JEI({
|
|
|
282
346
|
}}
|
|
283
347
|
/>
|
|
284
348
|
{isFav && (
|
|
285
|
-
<span
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
349
|
+
<span
|
|
350
|
+
className="mc-inv-jei-favorite-star"
|
|
351
|
+
style={{
|
|
352
|
+
position: 'absolute',
|
|
353
|
+
top: 0,
|
|
354
|
+
right: 0,
|
|
355
|
+
fontSize: Math.round(5 * scale),
|
|
356
|
+
color: '#ffcc00',
|
|
357
|
+
lineHeight: 1,
|
|
358
|
+
pointerEvents: 'none',
|
|
359
|
+
textShadow: `0 0 ${scale}px rgba(0,0,0,0.8)`,
|
|
360
|
+
}}
|
|
361
|
+
>
|
|
362
|
+
★
|
|
363
|
+
</span>
|
|
295
364
|
)}
|
|
296
365
|
</div>
|
|
297
366
|
)
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import React, { useState, useCallback, useEffect } from 'react'
|
|
2
|
+
import { useScale } from '../../context/ScaleContext'
|
|
3
|
+
|
|
4
|
+
export interface Note {
|
|
5
|
+
id: number
|
|
6
|
+
text: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface NotesProps {
|
|
10
|
+
/** Callback to get notes. If not provided, uses localStorage. */
|
|
11
|
+
onGetNotes?: () => Note[] | Promise<Note[]>
|
|
12
|
+
/** Callback to save notes. If not provided, uses localStorage. */
|
|
13
|
+
onSaveNotes?: (notes: Note[]) => void | Promise<void>
|
|
14
|
+
/** Storage key for localStorage (default: 'mc-inv-notes') */
|
|
15
|
+
storageKey?: string
|
|
16
|
+
className?: string
|
|
17
|
+
style?: React.CSSProperties
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const DEFAULT_STORAGE_KEY = 'mc-inv-notes'
|
|
21
|
+
|
|
22
|
+
function loadNotesFromStorage(key: string): Note[] {
|
|
23
|
+
try {
|
|
24
|
+
const raw = localStorage.getItem(key)
|
|
25
|
+
return raw ? JSON.parse(raw) : []
|
|
26
|
+
} catch {
|
|
27
|
+
return []
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function saveNotesToStorage(key: string, notes: Note[]) {
|
|
32
|
+
try {
|
|
33
|
+
localStorage.setItem(key, JSON.stringify(notes))
|
|
34
|
+
} catch {}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function Notes({
|
|
38
|
+
onGetNotes,
|
|
39
|
+
onSaveNotes,
|
|
40
|
+
storageKey = DEFAULT_STORAGE_KEY,
|
|
41
|
+
className,
|
|
42
|
+
style,
|
|
43
|
+
}: NotesProps) {
|
|
44
|
+
const { scale } = useScale()
|
|
45
|
+
const [notes, setNotes] = useState<Note[]>([])
|
|
46
|
+
const [draft, setDraft] = useState('')
|
|
47
|
+
const [loading, setLoading] = useState(true)
|
|
48
|
+
|
|
49
|
+
// Load notes on mount
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
let cancelled = false
|
|
52
|
+
setLoading(true)
|
|
53
|
+
|
|
54
|
+
const loadNotes = async () => {
|
|
55
|
+
if (onGetNotes) {
|
|
56
|
+
try {
|
|
57
|
+
const loaded = await onGetNotes()
|
|
58
|
+
if (!cancelled) {
|
|
59
|
+
setNotes(loaded)
|
|
60
|
+
setLoading(false)
|
|
61
|
+
}
|
|
62
|
+
} catch (err) {
|
|
63
|
+
console.error('Failed to load notes:', err)
|
|
64
|
+
if (!cancelled) {
|
|
65
|
+
setNotes([])
|
|
66
|
+
setLoading(false)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
// Use localStorage
|
|
71
|
+
const loaded = loadNotesFromStorage(storageKey)
|
|
72
|
+
if (!cancelled) {
|
|
73
|
+
setNotes(loaded)
|
|
74
|
+
setLoading(false)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
loadNotes()
|
|
80
|
+
return () => {
|
|
81
|
+
cancelled = true
|
|
82
|
+
}
|
|
83
|
+
}, [onGetNotes, storageKey])
|
|
84
|
+
|
|
85
|
+
const saveNotes = useCallback(async (newNotes: Note[]) => {
|
|
86
|
+
setNotes(newNotes)
|
|
87
|
+
if (onSaveNotes) {
|
|
88
|
+
try {
|
|
89
|
+
await onSaveNotes(newNotes)
|
|
90
|
+
} catch (err) {
|
|
91
|
+
console.error('Failed to save notes:', err)
|
|
92
|
+
}
|
|
93
|
+
} else {
|
|
94
|
+
// Use localStorage
|
|
95
|
+
saveNotesToStorage(storageKey, newNotes)
|
|
96
|
+
}
|
|
97
|
+
}, [onSaveNotes, storageKey])
|
|
98
|
+
|
|
99
|
+
const addNote = useCallback(() => {
|
|
100
|
+
const text = draft.trim()
|
|
101
|
+
if (!text) return
|
|
102
|
+
const next = [...notes, { id: Date.now(), text }]
|
|
103
|
+
saveNotes(next)
|
|
104
|
+
setDraft('')
|
|
105
|
+
}, [draft, notes, saveNotes])
|
|
106
|
+
|
|
107
|
+
const removeNote = useCallback((id: number) => {
|
|
108
|
+
const next = notes.filter((n) => n.id !== id)
|
|
109
|
+
saveNotes(next)
|
|
110
|
+
}, [notes, saveNotes])
|
|
111
|
+
|
|
112
|
+
const fs = Math.max(9, Math.round(7 * scale))
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<div
|
|
116
|
+
className={['mc-inv-root', 'mc-inv-notes', className].filter(Boolean).join(' ')}
|
|
117
|
+
style={{
|
|
118
|
+
width: '100%',
|
|
119
|
+
height: '100%',
|
|
120
|
+
fontFamily: "'Minecraftia', 'Minecraft', monospace",
|
|
121
|
+
fontSize: fs,
|
|
122
|
+
display: 'flex',
|
|
123
|
+
flexDirection: 'column',
|
|
124
|
+
minHeight: 0,
|
|
125
|
+
...style,
|
|
126
|
+
}}
|
|
127
|
+
>
|
|
128
|
+
<div style={{
|
|
129
|
+
padding: `${3 * scale}px ${4 * scale}px`,
|
|
130
|
+
color: '#404040',
|
|
131
|
+
fontWeight: 'bold',
|
|
132
|
+
flexShrink: 0,
|
|
133
|
+
}}>
|
|
134
|
+
Current Tasks
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
{/* Task list */}
|
|
138
|
+
<div style={{ overflowY: 'auto', padding: `${2 * scale}px`, minHeight: 0 }}>
|
|
139
|
+
{loading ? (
|
|
140
|
+
<div style={{ color: '#888', fontSize: Math.round(6 * scale), textAlign: 'center', padding: `${4 * scale}px` }}>
|
|
141
|
+
Loading...
|
|
142
|
+
</div>
|
|
143
|
+
) : notes.length === 0 ? (
|
|
144
|
+
<div style={{ color: '#888', fontSize: Math.round(6 * scale), textAlign: 'center', padding: `${4 * scale}px` }}>
|
|
145
|
+
No tasks yet
|
|
146
|
+
</div>
|
|
147
|
+
) : (
|
|
148
|
+
notes.map((note) => (
|
|
149
|
+
<div key={note.id} style={{
|
|
150
|
+
display: 'flex',
|
|
151
|
+
alignItems: 'flex-start',
|
|
152
|
+
gap: scale,
|
|
153
|
+
marginBottom: 2 * scale,
|
|
154
|
+
padding: `${2 * scale}px`,
|
|
155
|
+
}}>
|
|
156
|
+
<span style={{ flex: 1, color: '#222', wordBreak: 'break-word', lineHeight: 1.3 }}>
|
|
157
|
+
{note.text}
|
|
158
|
+
</span>
|
|
159
|
+
<button
|
|
160
|
+
onClick={() => removeNote(note.id)}
|
|
161
|
+
style={{
|
|
162
|
+
background: '#8b0000',
|
|
163
|
+
color: '#fff',
|
|
164
|
+
border: 'none',
|
|
165
|
+
cursor: 'pointer',
|
|
166
|
+
fontSize: Math.round(6 * scale),
|
|
167
|
+
padding: `0 ${2 * scale}px`,
|
|
168
|
+
fontFamily: 'inherit',
|
|
169
|
+
flexShrink: 0,
|
|
170
|
+
}}
|
|
171
|
+
>
|
|
172
|
+
×
|
|
173
|
+
</button>
|
|
174
|
+
</div>
|
|
175
|
+
))
|
|
176
|
+
)}
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
{/* Add task input */}
|
|
180
|
+
<div style={{
|
|
181
|
+
padding: `${2 * scale}px`,
|
|
182
|
+
display: 'flex',
|
|
183
|
+
gap: scale,
|
|
184
|
+
flexShrink: 0,
|
|
185
|
+
alignItems: 'flex-end',
|
|
186
|
+
width: '100%',
|
|
187
|
+
boxSizing: 'border-box',
|
|
188
|
+
// borderTop: `${scale}px solid #555555`,
|
|
189
|
+
}}>
|
|
190
|
+
<textarea
|
|
191
|
+
value={draft}
|
|
192
|
+
rows={2}
|
|
193
|
+
onChange={(e) => {
|
|
194
|
+
// Strip newlines — behave like a single-line input but with word wrap
|
|
195
|
+
setDraft(e.target.value.replace(/[\r\n]/g, ''))
|
|
196
|
+
}}
|
|
197
|
+
onKeyDown={(e) => {
|
|
198
|
+
if (e.code === 'Enter') { e.preventDefault(); addNote() }
|
|
199
|
+
}}
|
|
200
|
+
placeholder="Add task…"
|
|
201
|
+
style={{
|
|
202
|
+
flex: 1,
|
|
203
|
+
minWidth: 0,
|
|
204
|
+
resize: 'none',
|
|
205
|
+
background: '#8b8b8b',
|
|
206
|
+
border: `${scale}px solid #373737`,
|
|
207
|
+
color: '#fff',
|
|
208
|
+
fontSize: Math.round(6 * scale),
|
|
209
|
+
padding: `${scale}px`,
|
|
210
|
+
fontFamily: 'inherit',
|
|
211
|
+
outline: 'none',
|
|
212
|
+
lineHeight: 1.4,
|
|
213
|
+
boxSizing: 'border-box',
|
|
214
|
+
overflowY: 'hidden',
|
|
215
|
+
}}
|
|
216
|
+
/>
|
|
217
|
+
<button
|
|
218
|
+
onClick={addNote}
|
|
219
|
+
disabled={!draft.trim()}
|
|
220
|
+
style={{
|
|
221
|
+
background: '#404040',
|
|
222
|
+
color: '#fff',
|
|
223
|
+
border: `${scale}px solid #666`,
|
|
224
|
+
cursor: draft.trim() ? 'pointer' : 'not-allowed',
|
|
225
|
+
fontSize: Math.round(6 * scale),
|
|
226
|
+
padding: `${scale}px ${2 * scale}px`,
|
|
227
|
+
fontFamily: 'inherit',
|
|
228
|
+
flexShrink: 0,
|
|
229
|
+
opacity: draft.trim() ? 1 : 0.5,
|
|
230
|
+
}}
|
|
231
|
+
>
|
|
232
|
+
+
|
|
233
|
+
</button>
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
)
|
|
237
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import React, { useState, useCallback, useEffect, useRef } from 'react'
|
|
2
|
-
import { createPortal } from 'react-dom'
|
|
3
2
|
import type { RecipeNavFrame, ItemStack } from '../../types'
|
|
4
3
|
import { ItemCanvas } from '../ItemCanvas'
|
|
5
4
|
import { Tooltip } from '../Tooltip'
|
|
@@ -27,9 +26,8 @@ function RecipeItemCell({
|
|
|
27
26
|
x: number
|
|
28
27
|
y: number
|
|
29
28
|
size: number
|
|
30
|
-
onHover: (item: ItemStack | null
|
|
29
|
+
onHover: (item: ItemStack | null) => void
|
|
31
30
|
}) {
|
|
32
|
-
const [tooltipPos, setTooltipPos] = useState({ x: 0, y: 0 })
|
|
33
31
|
const [hovered, setHovered] = useState(false)
|
|
34
32
|
|
|
35
33
|
if (!item) {
|
|
@@ -49,25 +47,17 @@ function RecipeItemCell({
|
|
|
49
47
|
return (
|
|
50
48
|
<div
|
|
51
49
|
style={{ position: 'absolute', left: x, top: y, width: size, height: size, cursor: 'default' }}
|
|
52
|
-
onMouseEnter={(
|
|
53
|
-
setTooltipPos({ x: e.clientX, y: e.clientY })
|
|
50
|
+
onMouseEnter={() => {
|
|
54
51
|
setHovered(true)
|
|
55
|
-
onHover(item
|
|
52
|
+
onHover(item)
|
|
56
53
|
}}
|
|
57
54
|
onMouseLeave={() => {
|
|
58
55
|
setHovered(false)
|
|
59
|
-
onHover(null
|
|
60
|
-
}}
|
|
61
|
-
onMouseMove={(e) => {
|
|
62
|
-
setTooltipPos({ x: e.clientX, y: e.clientY })
|
|
63
|
-
onHover(item, e.clientX, e.clientY)
|
|
56
|
+
onHover(null)
|
|
64
57
|
}}
|
|
65
58
|
>
|
|
66
59
|
<ItemCanvas item={item} size={size} style={{ position: 'absolute', top: 0, left: 0, pointerEvents: 'none' }} />
|
|
67
|
-
{hovered &&
|
|
68
|
-
<Tooltip item={item} mouseX={tooltipPos.x} mouseY={tooltipPos.y} visible />,
|
|
69
|
-
document.body
|
|
70
|
-
)}
|
|
60
|
+
{hovered && <Tooltip item={item} visible />}
|
|
71
61
|
</div>
|
|
72
62
|
)
|
|
73
63
|
}
|
|
@@ -53,13 +53,40 @@ export function Slot({
|
|
|
53
53
|
const { contentSize } = useScale()
|
|
54
54
|
const isMobile = useMobile()
|
|
55
55
|
const slotRef = useRef<HTMLDivElement>(null)
|
|
56
|
-
const
|
|
57
|
-
const [
|
|
56
|
+
const labelRef = useRef<HTMLDivElement>(null)
|
|
57
|
+
const [mobileTouchPos, setMobileTouchPos] = useState({ x: 0, y: 0 })
|
|
58
58
|
const [showTooltip, setShowTooltip] = useState(false)
|
|
59
59
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
|
60
|
+
const [labelFontSize, setLabelFontSize] = useState<number | undefined>(undefined)
|
|
60
61
|
// Slot div = item content area. Slot is already positioned inside the texture border by InventoryWindow.
|
|
61
62
|
const renderSize = size ?? contentSize
|
|
62
63
|
|
|
64
|
+
// Measure label text and scale font size if it exceeds slot bounds
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
if (!label || item) {
|
|
67
|
+
setLabelFontSize(undefined)
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
const baseFontSize = Math.round(renderSize * 0.35)
|
|
71
|
+
const canvas = document.createElement('canvas')
|
|
72
|
+
const ctx = canvas.getContext('2d')
|
|
73
|
+
if (!ctx) {
|
|
74
|
+
setLabelFontSize(baseFontSize)
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
// Use the same font family as the label
|
|
78
|
+
ctx.font = `${baseFontSize}px 'Minecraft', monospace`
|
|
79
|
+
const textWidth = ctx.measureText(label).width
|
|
80
|
+
const maxWidth = renderSize * 0.9 // Leave 10% padding on each side
|
|
81
|
+
if (textWidth <= maxWidth) {
|
|
82
|
+
setLabelFontSize(baseFontSize)
|
|
83
|
+
} else {
|
|
84
|
+
// Scale down proportionally
|
|
85
|
+
const scaleFactor = maxWidth / textWidth
|
|
86
|
+
setLabelFontSize(Math.max(Math.round(baseFontSize * scaleFactor), Math.round(renderSize * 0.2))) // Min 20% of slot size
|
|
87
|
+
}
|
|
88
|
+
}, [label, item, renderSize])
|
|
89
|
+
|
|
63
90
|
const isHovered = hoveredSlot === index
|
|
64
91
|
const isDragTarget = dragSlots.includes(index)
|
|
65
92
|
|
|
@@ -72,7 +99,6 @@ export function Slot({
|
|
|
72
99
|
const handleMouseEnter = useCallback((e: React.MouseEvent) => {
|
|
73
100
|
if (isMobile) return
|
|
74
101
|
setHoveredSlot(index)
|
|
75
|
-
setTooltipPos({ x: e.clientX, y: e.clientY })
|
|
76
102
|
setShowTooltip(true)
|
|
77
103
|
if (isDragging) addDragSlot(index)
|
|
78
104
|
}, [isMobile, index, setHoveredSlot, isDragging, addDragSlot])
|
|
@@ -83,12 +109,6 @@ export function Slot({
|
|
|
83
109
|
setShowTooltip(false)
|
|
84
110
|
}, [isMobile, setHoveredSlot])
|
|
85
111
|
|
|
86
|
-
const handleMouseMove = useCallback((e: React.MouseEvent) => {
|
|
87
|
-
if (isMobile) return
|
|
88
|
-
mousePos.current = { x: e.clientX, y: e.clientY }
|
|
89
|
-
setTooltipPos({ x: e.clientX, y: e.clientY })
|
|
90
|
-
}, [isMobile])
|
|
91
|
-
|
|
92
112
|
const handleMouseDown = useCallback(
|
|
93
113
|
(e: React.MouseEvent) => {
|
|
94
114
|
if (isMobile || disabled) return
|
|
@@ -145,7 +165,7 @@ export function Slot({
|
|
|
145
165
|
}, [])
|
|
146
166
|
|
|
147
167
|
const handleWheel = useCallback(
|
|
148
|
-
(e:
|
|
168
|
+
(e: WheelEvent) => {
|
|
149
169
|
if (isMobile || disabled) return
|
|
150
170
|
if (!item && !heldItem) return
|
|
151
171
|
e.preventDefault()
|
|
@@ -162,6 +182,16 @@ export function Slot({
|
|
|
162
182
|
[isMobile, disabled, item, heldItem, sendAction, index, onClickOverride],
|
|
163
183
|
)
|
|
164
184
|
|
|
185
|
+
// Attach wheel listener as non-passive so preventDefault() is effective.
|
|
186
|
+
// React 17+ registers wheel events at the root as passive, which prevents
|
|
187
|
+
// calling preventDefault() from within React's onWheel synthetic handler.
|
|
188
|
+
useEffect(() => {
|
|
189
|
+
const el = slotRef.current
|
|
190
|
+
if (!el) return
|
|
191
|
+
el.addEventListener('wheel', handleWheel, { passive: false })
|
|
192
|
+
return () => el.removeEventListener('wheel', handleWheel)
|
|
193
|
+
}, [handleWheel])
|
|
194
|
+
|
|
165
195
|
// Mobile touch handlers
|
|
166
196
|
const touchStartRef = useRef<{ x: number; y: number; time: number } | null>(null)
|
|
167
197
|
|
|
@@ -187,7 +217,7 @@ export function Slot({
|
|
|
187
217
|
sendAction({ type: 'click', slotIndex: index, button: 'left', mode: 'normal' })
|
|
188
218
|
} else if (item) {
|
|
189
219
|
const rect = slotRef.current?.getBoundingClientRect()
|
|
190
|
-
if (rect)
|
|
220
|
+
if (rect) setMobileTouchPos({ x: rect.right, y: rect.top })
|
|
191
221
|
setMobileMenuOpen(true)
|
|
192
222
|
setShowTooltip(true)
|
|
193
223
|
}
|
|
@@ -253,12 +283,10 @@ export function Slot({
|
|
|
253
283
|
}}
|
|
254
284
|
onMouseEnter={handleMouseEnter}
|
|
255
285
|
onMouseLeave={handleMouseLeave}
|
|
256
|
-
onMouseMove={handleMouseMove}
|
|
257
286
|
onMouseDown={handleMouseDown}
|
|
258
287
|
onMouseUp={handleMouseUp}
|
|
259
288
|
onDoubleClick={handleDoubleClick}
|
|
260
289
|
onContextMenu={handleContextMenu}
|
|
261
|
-
onWheel={handleWheel}
|
|
262
290
|
onTouchStart={handleTouchStart}
|
|
263
291
|
onTouchEnd={handleTouchEnd}
|
|
264
292
|
aria-label={
|
|
@@ -277,13 +305,17 @@ export function Slot({
|
|
|
277
305
|
)}
|
|
278
306
|
|
|
279
307
|
{!item && label && (
|
|
280
|
-
<div
|
|
308
|
+
<div
|
|
309
|
+
ref={labelRef}
|
|
310
|
+
className={styles.emptyLabel}
|
|
311
|
+
style={{ fontSize: labelFontSize ?? Math.round(renderSize * 0.35) }}
|
|
312
|
+
>
|
|
281
313
|
{label}
|
|
282
314
|
</div>
|
|
283
315
|
)}
|
|
284
316
|
|
|
285
317
|
{item && showTooltip && !mobileMenuOpen && (
|
|
286
|
-
<Tooltip item={item}
|
|
318
|
+
<Tooltip item={item} visible />
|
|
287
319
|
)}
|
|
288
320
|
|
|
289
321
|
{isMobile && mobileMenuOpen && item && (
|
|
@@ -291,8 +323,8 @@ export function Slot({
|
|
|
291
323
|
<div className={styles.mobileOverlay} onClick={closeMobileMenu} />
|
|
292
324
|
<MobileSlotMenu
|
|
293
325
|
item={item}
|
|
294
|
-
x={
|
|
295
|
-
y={
|
|
326
|
+
x={mobileTouchPos.x}
|
|
327
|
+
y={mobileTouchPos.y}
|
|
296
328
|
onPickAll={handleMobilePickAll}
|
|
297
329
|
onPickHalf={handleMobilePickHalf}
|
|
298
330
|
onPickCustom={handleMobilePickCustom}
|
|
@@ -2,7 +2,7 @@ import { ComponentProps } from 'react'
|
|
|
2
2
|
import { MessageFormatOptions, MessageFormatPart } from './chatUtils'
|
|
3
3
|
import './MessageFormatted.css'
|
|
4
4
|
|
|
5
|
-
export const MessagePart = ({ part, formatOptions, ...props }: { part: MessageFormatPart, formatOptions?: MessageFormatOptions } & ComponentProps<'span'>) => {
|
|
5
|
+
export const MessagePart = ({ part, formatOptions, ...props }: { part: MessageFormatPart, formatOptions?: MessageFormatOptions } & Pick<ComponentProps<'span'>, 'style' | 'className' | 'children'>) => {
|
|
6
6
|
|
|
7
7
|
const { color: _color, italic, bold, underlined, strikethrough, text, clickEvent, hoverEvent, obfuscated } = part
|
|
8
8
|
const color = _color ?? 'white'
|