import React, { useState, useCallback, useEffect, useRef } from 'react'
import type { RecipeNavFrame, RecipeGuide, ItemStack } from '../../types'
import { ItemCanvas } from '../ItemCanvas'
import { Tooltip } from '../Tooltip'
import { useScale } from '../../context/ScaleContext'
import { useTextures } from '../../context/TextureContext'
import { getInventoryType } from '../../registry'
function isTypingTarget(target: EventTarget | null): boolean {
if (!target || !(target instanceof HTMLElement)) return false
const tag = target.tagName
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true
if (target.isContentEditable) return true
return false
}
interface RecipeInventoryViewProps {
navStack: RecipeNavFrame[]
onBack: () => void
onGuideIndexChange: (idx: number) => void
/** Called when R or U is pressed while hovering a recipe item (nested navigation) */
onPushFrame: (item: RecipeNavFrame['item'], mode: 'recipes' | 'usages') => void
}
function minecraftWikiUrlForName(nameOrDisplay: string): string {
const slug = nameOrDisplay.replace(/ /g, '_')
return `https://minecraft.wiki/w/${encodeURIComponent(slug)}`
}
/** Slot item with a Tooltip, used inside the recipe view */
function RecipeItemCell({
item,
x,
y,
size,
onHover,
onRecipeNavigate,
}: {
item: ItemStack | null
x: number
y: number
size: number
onHover: (item: ItemStack | null) => void
onRecipeNavigate?: (item: ItemStack, mode: 'recipes' | 'usages') => void
}) {
const [hovered, setHovered] = useState(false)
if (!item) {
return (
)
}
return (
{
setHovered(true)
onHover(item)
}}
onMouseLeave={() => {
setHovered(false)
onHover(null)
}}
onClick={(e) => {
e.stopPropagation()
onRecipeNavigate?.(item, 'recipes')
}}
onContextMenu={(e) => {
e.preventDefault()
e.stopPropagation()
onRecipeNavigate?.(item, 'usages')
}}
>
{hovered && }
)
}
/**
* Renders the current recipe guide using the actual inventory background layout
* (crafting_table for crafting, furnace for smelting). Replaces the main
* InventoryWindow while a recipe is being browsed.
*/
export function RecipeInventoryView({
navStack,
onBack,
onGuideIndexChange,
onPushFrame,
}: RecipeInventoryViewProps) {
const { scale, contentSize } = useScale()
// Registry coords already point to item area — no border offset needed
const borderPx = 0
const frame = navStack[navStack.length - 1]
const guide = frame.guides[frame.guideIndex]
const hoveredItemRef = useRef(null)
const recipeViewRootRef = useRef(null)
const handleHoverItem = useCallback((item: ItemStack | null) => {
if (!item) { hoveredItemRef.current = null; return }
hoveredItemRef.current = {
type: item.type,
name: item.name ?? '',
displayName: item.displayName ?? item.name ?? `Item #${item.type}`,
count: item.count,
metadata: item.metadata,
}
}, [])
const handleRecipeItemNavigate = useCallback(
(item: ItemStack, mode: 'recipes' | 'usages') => {
onPushFrame(
{
type: item.type,
name: item.name ?? '',
displayName: item.displayName ?? item.name ?? `Item #${item.type}`,
count: item.count,
metadata: item.metadata,
},
mode,
)
},
[onPushFrame],
)
const wikiHref = minecraftWikiUrlForName(frame.item.name || frame.item.displayName || 'Unknown')
// R / U for nested navigation on hovered recipe items
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (e.code !== 'KeyR' && e.code !== 'KeyU') return
if (isTypingTarget(e.target)) return
const item = hoveredItemRef.current
if (!item) return
onPushFrame(item, e.code === 'KeyR' ? 'recipes' : 'usages')
}
window.addEventListener('keydown', handler)
return () => window.removeEventListener('keydown', handler)
}, [onPushFrame])
// Escape / Backspace = go back one frame (Backspace on root frame closes recipe view)
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (e.code !== 'Escape' && e.code !== 'Backspace') return
if (isTypingTarget(e.target)) return
onBack()
if (e.code === 'Backspace') e.preventDefault()
}
window.addEventListener('keydown', handler)
return () => window.removeEventListener('keydown', handler)
}, [onBack])
const isMultiFrame = navStack.length > 1
const isCustom = guide.type === 'custom'
// Determine layout type and get definition (not needed for custom)
const layoutType = guide.type === 'smelting' ? 'furnace' : 'crafting_table'
const def = getInventoryType(layoutType)!
// Container-only slots (no player inv / hotbar)
const containerSlots = isCustom ? [] : def.slots.filter(
(s) => s.group !== 'inventory' && s.group !== 'hotbar',
)
// Map recipe items to slot indices
const slotItems = new Map()
if (guide.type === 'crafting') {
slotItems.set(0, guide.result ?? null)
const ingr = guide.ingredients ?? []
for (let i = 0; i < 9; i++) {
slotItems.set(i + 1, ingr[i] ?? null)
}
} else if (guide.type === 'smelting') {
slotItems.set(0, guide.input ?? null)
slotItems.set(1, guide.fuel ?? null)
slotItems.set(2, guide.result ?? null)
}
const totalGuides = frame.guides.length
const navFontSize = Math.max(6, Math.round(6 * scale))
const navPx = Math.max(1, Math.round(scale))
const handleWheel = useCallback(
(e: WheelEvent) => {
if (totalGuides <= 1) return
e.preventDefault()
const idx = frame.guideIndex
if (e.deltaY > 0) {
onGuideIndexChange(Math.min(idx + 1, totalGuides - 1))
} else {
onGuideIndexChange(Math.max(idx - 1, 0))
}
},
[totalGuides, frame.guideIndex, onGuideIndexChange],
)
useEffect(() => {
const el = recipeViewRootRef.current
if (!el || totalGuides <= 1) return
el.addEventListener('wheel', handleWheel, { passive: false })
return () => el.removeEventListener('wheel', handleWheel)
}, [handleWheel, totalGuides])
return (
{/* ── Navigation bar ABOVE the inventory background ── */}
{isCustom ? (
/* ── Compact description card for non-craftable items ── */
) : (
/* ── Cropped inventory background with container slots only (no player inv) ── */
)}
{/* ── Hint below ── */}
{!isCustom && (
Hover ingredient + R / U, or left / right-click (usages)
)}
)
}
/**
* Cropped recipe background — shows only the container area (no player inventory).
* Crafting: crops to 80px height (title bar + 3×3 grid + result).
* Smelting: crops to 84px height (input + fuel + result + arrow).
*/
function CroppedRecipeBackground({
guide,
containerSlots,
slotItems,
scale,
navPx,
contentSize,
onHoverItem,
onRecipeNavigate,
}: {
guide: RecipeGuide
containerSlots: Array<{ index: number; x: number; y: number; size?: number; group?: string }>
slotItems: Map
scale: number
navPx: number
contentSize: number
onHoverItem: (item: ItemStack | null) => void
onRecipeNavigate?: (item: ItemStack, mode: 'recipes' | 'usages') => void
}) {
const textures = useTextures()
const layoutType = guide.type === 'smelting' ? 'furnace' : 'crafting_table'
const def = getInventoryType(layoutType)!
const bgUrl = textures.getGuiTextureUrl(def.backgroundTexture)
// Crop height: crafting=80, furnace=84 (matches old lib's CraftingTableGuide slice)
const CROP_H = guide.type === 'smelting' ? 84 : 80
const SRC_W = def.backgroundWidth
const fontSize = Math.max(6, Math.round(6 * scale))
return (
{/* Cropped background texture */}
{/* Title */}
{guide.title}
{guide.description && (
({guide.description})
)}
{/* Slot items */}
{containerSlots.map((slotDef) => {
const item = slotItems.get(slotDef.index) ?? null
return (
)
})}
)
}
/**
* Compact description card for non-craftable items (replaces full inventory background).
* Mirrors the old canvas-based GenericDescription layout: cropped crafting_table header,
* gray content area, item icon, description text, and a Minecraft Wiki link.
*/
function DescriptionCard({
guide,
itemName,
scale,
}: {
guide: RecipeGuide
itemName: string
scale: number
}) {
const textures = useTextures()
const craftingDef = getInventoryType('crafting_table')!
const bgUrl = textures.getGuiTextureUrl(craftingDef.backgroundTexture)
const CARD_W = 176
const CARD_H = 120
const TITLE_H = 15
const fontSize = Math.max(6, Math.round(6 * scale))
const iconSize = 16 * scale
return (
{/* Cropped crafting_table texture as background header */}
{/* Gray content overlay (below title bar) */}
{/* Title */}
{guide.title}
{/* Item icon */}
{/* Description text */}
{guide.description && (
{guide.description}
)}
)
}
function NavBtn({
scale,
onClick,
disabled,
title,
children,
}: {
scale: number
onClick: () => void
disabled?: boolean
title?: string
children: React.ReactNode
}) {
return (
)
}