Skip to content

Commit f2c266a

Browse files
committed
style(editor): restore the item tooltip and preview
1 parent 8e7351a commit f2c266a

5 files changed

Lines changed: 154 additions & 23 deletions

File tree

resources/css/editor-extras.css

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,3 +201,12 @@
201201
.preview-line {
202202
white-space: pre-wrap;
203203
}
204+
205+
/* The canvas header gained a size selector, so it lays its two parts out
206+
instead of letting the control overlap the label. */
207+
.canvas-header {
208+
display: flex;
209+
align-items: center;
210+
justify-content: space-between;
211+
gap: 12px;
212+
}

resources/js/components/visual/ItemEditor.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { AttributeEditor } from './AttributeEditor';
44
import { ConditionEditor } from './ConditionEditor';
55
import { FormattedInput } from './FormattedInput';
66
import { ItemIcon } from './ItemIcon';
7+
import { ItemPreview } from './ItemPreview';
78
import type { VisualItem } from '../../editor/model';
89

910
interface ItemEditorProps {
@@ -43,6 +44,10 @@ export function ItemEditor({ slot, item, serverVersion, onChange, onRemove }: It
4344
</div>
4445

4546
<div className="item-editor-body">
47+
<div className="item-editor-section">
48+
<ItemPreview item={item} serverVersion={serverVersion} />
49+
</div>
50+
4651
<div className="item-editor-section">
4752
<div className="editor-field">
4853
<label htmlFor="item-material">Material</label>
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { useState } from 'react';
2+
import { getMaterialImageUrl } from '../../editor/materials';
3+
import { getPlayerHeadImageUrl } from '../../editor/heads';
4+
import { parseMiniMessage } from '../../editor/miniMessage';
5+
import type { VisualItem } from '../../editor/model';
6+
7+
interface ItemPreviewProps {
8+
item: VisualItem;
9+
serverVersion: string | null;
10+
}
11+
12+
/** How the item will look in game, beside the fields that define it. */
13+
export function ItemPreview({ item, serverVersion }: ItemPreviewProps) {
14+
const [broken, setBroken] = useState(false);
15+
const lore = Array.isArray(item.lore) ? item.lore : [];
16+
const attributeCount = countAttributes(item);
17+
18+
const source =
19+
item.material === 'PLAYER_HEAD'
20+
? getPlayerHeadImageUrl(typeof item.value === 'string' ? item.value : null)
21+
: getMaterialImageUrl(item.material, serverVersion);
22+
23+
return (
24+
<div className="item-preview-container">
25+
<div className="item-preview-image-wrapper">
26+
{broken ? (
27+
<div className="item-preview-fallback">📦</div>
28+
) : (
29+
<img
30+
src={source}
31+
alt={item.material}
32+
className="item-preview-image"
33+
onError={() => setBroken(true)}
34+
/>
35+
)}
36+
{item.amount > 1 && <span className="item-preview-amount">{item.amount}</span>}
37+
</div>
38+
39+
<div className="item-preview-details">
40+
{typeof item.name === 'string' && item.name !== '' ? (
41+
<div
42+
className="item-preview-name"
43+
dangerouslySetInnerHTML={{ __html: parseMiniMessage(item.name) }}
44+
/>
45+
) : (
46+
<div className="item-preview-name" style={{ color: '#fff' }}>
47+
{item.material}
48+
</div>
49+
)}
50+
51+
{lore.map((line, index) => (
52+
<div
53+
key={`${index}-${line}`}
54+
className="item-preview-lore-line"
55+
dangerouslySetInnerHTML={{ __html: parseMiniMessage(line) }}
56+
/>
57+
))}
58+
59+
{attributeCount > 0 && (
60+
<div className="item-preview-flags">
61+
{attributeCount} attribute{attributeCount === 1 ? '' : 's'}
62+
</div>
63+
)}
64+
</div>
65+
</div>
66+
);
67+
}
68+
69+
function countAttributes(item: VisualItem): number {
70+
const attributes = item.attributes;
71+
72+
if (attributes === undefined) {
73+
return 0;
74+
}
75+
76+
const flags = attributes.flags ?? {};
77+
const enchantments = attributes.enchantments ?? {};
78+
79+
return Object.keys(flags).length + Object.keys(enchantments).length;
80+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { formatMaterialName } from '../../editor/materials';
2+
import { parseMiniMessage } from '../../editor/miniMessage';
3+
import type { VisualItem } from '../../editor/model';
4+
5+
interface ItemTooltipProps {
6+
item: VisualItem;
7+
x: number;
8+
y: number;
9+
}
10+
11+
/** The in game tooltip, so a name and its lore can be judged as the player sees them. */
12+
export function ItemTooltip({ item, x, y }: ItemTooltipProps) {
13+
const lore = Array.isArray(item.lore) ? item.lore : [];
14+
15+
return (
16+
<div className="item-tooltip" style={{ left: x + 16, top: y + 16 }}>
17+
<div className="item-tooltip-content">
18+
{typeof item.name === 'string' && item.name !== '' ? (
19+
<div
20+
className="item-tooltip-name"
21+
dangerouslySetInnerHTML={{ __html: parseMiniMessage(item.name) }}
22+
/>
23+
) : (
24+
<div className="item-tooltip-name" style={{ color: '#FFFFFF' }}>
25+
{formatMaterialName(item.material)}
26+
</div>
27+
)}
28+
29+
{lore.length > 0 && (
30+
<div className="item-tooltip-lore">
31+
{lore.map((line, index) => (
32+
<div
33+
key={`${index}-${line}`}
34+
className="item-tooltip-lore-line"
35+
dangerouslySetInnerHTML={{ __html: parseMiniMessage(line) }}
36+
/>
37+
))}
38+
</div>
39+
)}
40+
41+
<div className="item-tooltip-material">{item.material}</div>
42+
</div>
43+
</div>
44+
);
45+
}

resources/js/components/visual/MenuCanvas.tsx

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useState, type DragEvent } from 'react';
2-
import { parseMiniMessage } from '../../editor/miniMessage';
32
import { ItemIcon } from './ItemIcon';
3+
import { ItemTooltip } from './ItemTooltip';
44
import type { VisualItem } from '../../editor/model';
55

66
const MATERIAL_DRAG_TYPE = 'application/x-bluemenu-material';
@@ -32,6 +32,7 @@ export function MenuCanvas({
3232
onContextMenu,
3333
}: MenuCanvasProps) {
3434
const [hoveredSlot, setHoveredSlot] = useState<number | null>(null);
35+
const [tooltip, setTooltip] = useState<{ item: VisualItem; x: number; y: number } | null>(null);
3536

3637
const handleDrop = (event: DragEvent<HTMLDivElement>, slot: number): void => {
3738
event.preventDefault();
@@ -53,7 +54,10 @@ export function MenuCanvas({
5354
};
5455

5556
return (
56-
<div className="menu-canvas" style={{ gridTemplateColumns: 'repeat(9, 60px)' }}>
57+
<>
58+
{tooltip !== null && <ItemTooltip item={tooltip.item} x={tooltip.x} y={tooltip.y} />}
59+
60+
<div className="menu-canvas" style={{ gridTemplateColumns: 'repeat(9, 60px)' }}>
5761
{Array.from({ length: size }, (_unused, slot) => {
5862
const item = items[slot];
5963

@@ -81,7 +85,13 @@ export function MenuCanvas({
8185
onDragEnter={() => setHoveredSlot(slot)}
8286
onDragLeave={() => setHoveredSlot(current => (current === slot ? null : current))}
8387
onDrop={event => handleDrop(event, slot)}
84-
title={tooltipFor(item, slot)}
88+
onMouseEnter={event =>
89+
item !== undefined && setTooltip({ item, x: event.clientX, y: event.clientY })
90+
}
91+
onMouseMove={event =>
92+
item !== undefined && setTooltip({ item, x: event.clientX, y: event.clientY })
93+
}
94+
onMouseLeave={() => setTooltip(null)}
8595
aria-label={item === undefined ? `Empty slot ${slot}` : `${item.material} in slot ${slot}`}
8696
>
8797
<span className="slot-number">{slot}</span>
@@ -96,25 +106,7 @@ export function MenuCanvas({
96106
</div>
97107
);
98108
})}
99-
</div>
109+
</div>
110+
</>
100111
);
101112
}
102-
103-
function tooltipFor(item: VisualItem | undefined, slot: number): string {
104-
if (item === undefined) {
105-
return `Slot ${slot}`;
106-
}
107-
108-
// The name carries MiniMessage tags, which are noise in a native tooltip.
109-
const name = typeof item.name === 'string' ? stripMarkup(item.name) : item.material;
110-
111-
return `${name} (slot ${slot})`;
112-
}
113-
114-
function stripMarkup(name: string): string {
115-
const rendered = parseMiniMessage(name);
116-
const container = document.createElement('div');
117-
container.innerHTML = rendered;
118-
119-
return container.textContent ?? name;
120-
}

0 commit comments

Comments
 (0)