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.
@@ -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
- const cols = ITEMS_PER_ROW
87
- const panelWidth = width ?? cols * itemSize + padding * 2
88
- const rows = Math.floor((300 * scale) / itemSize)
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: React.WheelEvent<HTMLDivElement>) => {
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
- background: '#c6c6c6',
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 style={{ padding: `${padding}px`, borderBottom: `${scale}px solid #555555` }}>
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 style={{
203
- display: 'flex',
204
- alignItems: 'center',
205
- gap: scale,
206
- marginTop: 2 * scale,
207
- fontSize: 6 * scale,
208
- fontFamily: "'Minecraftia', 'Minecraft', monospace",
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 style={{
286
- position: 'absolute',
287
- top: 0,
288
- right: 0,
289
- fontSize: Math.round(5 * scale),
290
- color: '#ffcc00',
291
- lineHeight: 1,
292
- pointerEvents: 'none',
293
- textShadow: `0 0 ${scale}px rgba(0,0,0,0.8)`,
294
- }}>★</span>
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
+ }
@@ -0,0 +1,2 @@
1
+ export { Notes } from './Notes'
2
+ export type { Note } from './Notes'
@@ -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, mx: number, my: number) => void
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={(e) => {
53
- setTooltipPos({ x: e.clientX, y: e.clientY })
50
+ onMouseEnter={() => {
54
51
  setHovered(true)
55
- onHover(item, e.clientX, e.clientY)
52
+ onHover(item)
56
53
  }}
57
54
  onMouseLeave={() => {
58
55
  setHovered(false)
59
- onHover(null, 0, 0)
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 && createPortal(
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 mousePos = useRef({ x: 0, y: 0 })
57
- const [tooltipPos, setTooltipPos] = useState({ x: 0, y: 0 })
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: React.WheelEvent) => {
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) setTooltipPos({ x: rect.right, y: rect.top })
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 className={styles.emptyLabel} style={{ fontSize: Math.round(renderSize * 0.35) }}>
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} mouseX={tooltipPos.x} mouseY={tooltipPos.y} visible />
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={tooltipPos.x}
295
- y={tooltipPos.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'