first commit
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
import { useState, type PointerEvent, type ReactNode } from "react";
|
||||
import type { FurnitureType } from "../types";
|
||||
|
||||
const CLICK_THRESHOLD_PX = 5;
|
||||
|
||||
interface Props {
|
||||
remaining: Record<FurnitureType, number>;
|
||||
onStartDrag: (type: FurnitureType, clientX: number, clientY: number) => void;
|
||||
onOpenMenu: (type: FurnitureType) => void;
|
||||
panel: ReactNode; // when set, replaces the tiles (type settings menu)
|
||||
}
|
||||
|
||||
const tile: React.CSSProperties = {
|
||||
width: "100%",
|
||||
height: 84,
|
||||
boxSizing: "border-box",
|
||||
background: "#fff",
|
||||
border: "1px solid #cfcfcf",
|
||||
borderRadius: 8,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 6,
|
||||
cursor: "grab",
|
||||
userSelect: "none",
|
||||
touchAction: "none",
|
||||
font: "12px system-ui, sans-serif",
|
||||
color: "#444",
|
||||
};
|
||||
|
||||
export function ChairIcon({ size = 36 }: { size?: number }) {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 36 36">
|
||||
<rect x="4" y="4" width="28" height="28" rx="3" fill="#8fb4d9" stroke="#3b3b3b" strokeWidth="1.5" />
|
||||
<rect x="4" y="4" width="28" height="8" rx="3" fill="#4a6f96" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function TableIcon({ size = 36 }: { size?: number }) {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 36 36">
|
||||
<rect x="2" y="9" width="32" height="18" rx="3" fill="#d9b382" stroke="#3b3b3b" strokeWidth="1.5" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
const MIN_W = 152;
|
||||
const MAX_W = 600;
|
||||
|
||||
interface TileProps {
|
||||
type: FurnitureType;
|
||||
remaining: number;
|
||||
onStartDrag: Props["onStartDrag"];
|
||||
onOpenMenu: Props["onOpenMenu"];
|
||||
}
|
||||
|
||||
/** Press + move > threshold = drag a new item out. Press + release without moving = open menu. */
|
||||
function Tile({ type, remaining, onStartDrag, onOpenMenu }: TileProps) {
|
||||
const empty = remaining <= 0;
|
||||
|
||||
const onPointerDown = (e: PointerEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
const { clientX: x0, clientY: y0 } = e;
|
||||
let dragging = false;
|
||||
const move = (ev: globalThis.PointerEvent) => {
|
||||
if (dragging || empty) return;
|
||||
if (Math.hypot(ev.clientX - x0, ev.clientY - y0) >= CLICK_THRESHOLD_PX) {
|
||||
dragging = true;
|
||||
cleanup();
|
||||
onStartDrag(type, ev.clientX, ev.clientY);
|
||||
}
|
||||
};
|
||||
const up = (ev: globalThis.PointerEvent) => {
|
||||
cleanup();
|
||||
// Out-of-stock tiles can't be dragged, so any release over the tile counts as a click.
|
||||
if (Math.hypot(ev.clientX - x0, ev.clientY - y0) < CLICK_THRESHOLD_PX || empty) {
|
||||
onOpenMenu(type);
|
||||
}
|
||||
};
|
||||
const cleanup = () => {
|
||||
window.removeEventListener("pointermove", move);
|
||||
window.removeEventListener("pointerup", up);
|
||||
};
|
||||
window.addEventListener("pointermove", move);
|
||||
window.addEventListener("pointerup", up);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
role="button"
|
||||
aria-label={`${type} (${remaining} remaining)`}
|
||||
style={{ ...tile, position: "relative", opacity: empty ? 0.45 : 1, filter: empty ? "grayscale(1)" : undefined, cursor: empty ? "pointer" : "grab" }}
|
||||
onPointerDown={onPointerDown}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 6,
|
||||
right: 6,
|
||||
minWidth: 20,
|
||||
height: 20,
|
||||
padding: "0 6px",
|
||||
boxSizing: "border-box",
|
||||
borderRadius: 10,
|
||||
background: empty ? "#888" : "#2b2b2b",
|
||||
color: "#fff",
|
||||
font: "600 11px/20px system-ui, sans-serif",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
{remaining}
|
||||
</span>
|
||||
{type === "chair" ? <ChairIcon /> : <TableIcon />}
|
||||
{type === "chair" ? "Chair" : "Table"}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Palette({ remaining, onStartDrag, onOpenMenu, panel }: Props) {
|
||||
const [width, setWidth] = useState(264);
|
||||
|
||||
const startResize = (e: PointerEvent) => {
|
||||
e.preventDefault();
|
||||
const startX = e.clientX;
|
||||
const startW = width;
|
||||
const move = (ev: globalThis.PointerEvent) =>
|
||||
setWidth(Math.min(MAX_W, Math.max(MIN_W, startW + ev.clientX - startX)));
|
||||
const up = () => {
|
||||
window.removeEventListener("pointermove", move);
|
||||
window.removeEventListener("pointerup", up);
|
||||
};
|
||||
window.addEventListener("pointermove", move);
|
||||
window.addEventListener("pointerup", up);
|
||||
};
|
||||
|
||||
return (
|
||||
<aside
|
||||
style={{
|
||||
position: "relative",
|
||||
width,
|
||||
boxSizing: "border-box",
|
||||
flexShrink: 0,
|
||||
padding: 16,
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(auto-fill, minmax(84px, 1fr))",
|
||||
alignContent: "start",
|
||||
gap: 12,
|
||||
borderRight: "1px solid #d4d4d4",
|
||||
background: "#ececec",
|
||||
}}
|
||||
>
|
||||
{panel ?? (["chair", "table"] as const).map((type) => (
|
||||
<Tile key={type} type={type} remaining={remaining[type]} onStartDrag={onStartDrag} onOpenMenu={onOpenMenu} />
|
||||
))}
|
||||
<div
|
||||
onPointerDown={startResize}
|
||||
style={{ position: "absolute", top: 0, right: -3, width: 6, height: "100%", cursor: "col-resize", touchAction: "none" }}
|
||||
/>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user