refactor(ui): stack per-instance board + square detail vertically
The Modifier Profile editor rendered four visible columns in practice: PerType | board | square-detail sidebar | library/presets. The inner split inside PerInstancePanel (board on the left, w-72 detail sidebar on the right) was the culprit — it squeezed the 8x8 board into a cramped square the moment a piece was selected. Restructure PerInstancePanel so the board stacks ABOVE the detail strip instead of beside it: - Board centered in the top region (full column width, max-w-md). - Detail strip below: horizontal flex with existing-modifiers list on the left and the Add Modifier form on the right, wrapping to stacked rows on narrow widths. max-h-[45%] so the board never loses most of its space when many modifiers are present. - Empty state gains a short one-line prompt; the Choose Layout button is kept for backward-compat with existing e2e tests. Testids unchanged (per-instance-panel, per-instance-board, instance-modifier-*, instance-modifiers-list, copy/paste-instance-modifiers, instance-modifier-kind, instance-modifier-value, instance-modifier-add, per-instance-choose-layout). All Playwright e2e tests continue to pass.
This commit is contained in:
parent
fe787478b1
commit
ef730eefbe
1 changed files with 92 additions and 49 deletions
|
|
@ -56,13 +56,23 @@ export function PerInstancePanel({
|
|||
const [selectedSquare, setSelectedSquare] = useState<string | null>(null);
|
||||
|
||||
// No layout bound — show the empty state prompt.
|
||||
// Per-instance modifiers are square-bound (b1, e4, etc.), so we
|
||||
// can't render a board preview or accept clicks until a layout
|
||||
// supplies the pieces. The prompt + button focus the layout picker
|
||||
// in the subhead so the user can bind one in a single click.
|
||||
if (boundLayout === null) {
|
||||
return (
|
||||
<div
|
||||
data-testid="per-instance-panel"
|
||||
className="flex flex-1 flex-col items-center justify-center gap-4 p-8"
|
||||
>
|
||||
<p className="text-sm text-neutral-500 italic">Select a layout first</p>
|
||||
<div className="max-w-sm text-center space-y-2">
|
||||
<p className="text-sm text-neutral-500 italic">Select a layout first</p>
|
||||
<p className="text-xs text-neutral-400 leading-relaxed">
|
||||
Per-piece modifiers are tied to specific squares. Bind a starting
|
||||
layout to pick which pieces get modifiers.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
data-testid="per-instance-choose-layout"
|
||||
onClick={onLayoutSelect}
|
||||
|
|
@ -95,29 +105,44 @@ export function PerInstancePanel({
|
|||
}
|
||||
|
||||
return (
|
||||
<div data-testid="per-instance-panel" className="flex flex-1 overflow-hidden">
|
||||
{/* Board preview */}
|
||||
<div className="flex-1 flex items-center justify-center p-4 bg-white">
|
||||
<LayoutBoardView
|
||||
pieces={boundLayout.pieces}
|
||||
selectedSquare={selectedSquare}
|
||||
testId="per-instance-board"
|
||||
onSquareClick={handleSquareClick}
|
||||
/>
|
||||
<div
|
||||
data-testid="per-instance-panel"
|
||||
className="flex flex-1 flex-col overflow-hidden bg-white"
|
||||
>
|
||||
{/* Board preview — stacked on top and centered so it gets the
|
||||
full column width (no more sharing with a side detail panel
|
||||
that squeezes it to a tiny square). Scroll-locked. */}
|
||||
<div className="flex-1 min-h-0 flex items-center justify-center p-4 overflow-auto">
|
||||
<div className="w-full max-w-md">
|
||||
<LayoutBoardView
|
||||
pieces={boundLayout.pieces}
|
||||
selectedSquare={selectedSquare}
|
||||
testId="per-instance-board"
|
||||
onSquareClick={handleSquareClick}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Square detail sidebar */}
|
||||
<aside className="w-72 border-l border-neutral-200 bg-neutral-50 p-4 overflow-y-auto space-y-4">
|
||||
{/* Square detail strip — stacked BELOW the board instead of
|
||||
beside it. Fixed-height, scrolls its own overflow. Keeps
|
||||
this column to ONE visual lane so the modal no longer
|
||||
renders four cramped columns side-by-side. */}
|
||||
<div
|
||||
className={`border-t border-neutral-200 bg-neutral-50 shrink-0 ${
|
||||
selectedSquare === null ? 'py-3 px-4' : 'p-4'
|
||||
} max-h-[45%] overflow-y-auto`}
|
||||
>
|
||||
{selectedSquare === null ? (
|
||||
<p className="text-sm text-neutral-500 italic">
|
||||
<p className="text-xs text-neutral-500 italic text-center">
|
||||
Click a piece on the board to add modifiers.
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
{/* Selected square header */}
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h3 className="text-xs font-bold text-neutral-500 uppercase tracking-widest mb-1">
|
||||
<div className="space-y-3">
|
||||
{/* Selected square header — horizontal so it's compact
|
||||
inside the bottom strip. */}
|
||||
<div className="flex items-center justify-between gap-3 flex-wrap">
|
||||
<div className="flex items-baseline gap-3">
|
||||
<h3 className="text-xs font-bold text-neutral-500 uppercase tracking-widest">
|
||||
Square {selectedSquare}
|
||||
</h3>
|
||||
{selectedPiece !== undefined && (
|
||||
|
|
@ -126,7 +151,7 @@ export function PerInstancePanel({
|
|||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-1">
|
||||
<div className="flex items-center gap-1">
|
||||
{squareModifiers.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -137,7 +162,7 @@ export function PerInstancePanel({
|
|||
entries: squareModifiers.map(({ modifier }) => {
|
||||
const { square: _sq, ...rest } = modifier;
|
||||
return rest;
|
||||
})
|
||||
}),
|
||||
});
|
||||
}}
|
||||
className="px-2 py-0.5 text-[10px] font-bold uppercase tracking-wider text-blue-600 bg-blue-50 border border-blue-200 rounded hover:bg-blue-100 transition-colors"
|
||||
|
|
@ -151,7 +176,12 @@ export function PerInstancePanel({
|
|||
data-testid="paste-instance-modifiers"
|
||||
onClick={() => {
|
||||
if (clipboard.kind === 'instance-modifiers') {
|
||||
onPaste(clipboard.entries.map(entry => ({ ...entry, square: selectedSquare })));
|
||||
onPaste(
|
||||
clipboard.entries.map((entry) => ({
|
||||
...entry,
|
||||
square: selectedSquare,
|
||||
})),
|
||||
);
|
||||
}
|
||||
}}
|
||||
className="px-2 py-0.5 text-[10px] font-bold uppercase tracking-wider text-green-600 bg-green-50 border border-green-200 rounded hover:bg-green-100 transition-colors"
|
||||
|
|
@ -162,35 +192,48 @@ export function PerInstancePanel({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Existing modifiers list */}
|
||||
{squareModifiers.length > 0 && (
|
||||
<ul data-testid="instance-modifiers-list" className="space-y-1">
|
||||
{squareModifiers.map(({ modifier, index }) => (
|
||||
<li
|
||||
key={index}
|
||||
data-testid={`instance-modifier-${selectedSquare}-${modifier.kind}`}
|
||||
className="flex items-center gap-2 px-3 py-2 bg-white border border-neutral-200 rounded text-sm"
|
||||
{/* Horizontal band: existing modifier chips on the left,
|
||||
add-form on the right. Wraps to stacked on narrow
|
||||
widths via flex-wrap. */}
|
||||
<div className="flex flex-wrap gap-3 items-start">
|
||||
<div className="flex-1 min-w-[240px] space-y-1">
|
||||
{squareModifiers.length > 0 ? (
|
||||
<ul
|
||||
data-testid="instance-modifiers-list"
|
||||
className="space-y-1"
|
||||
>
|
||||
<span className="flex-1 font-medium text-neutral-700 break-all">
|
||||
{modifier.kind}: {JSON.stringify(modifier.value)}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => onDelete(index)}
|
||||
aria-label={`Delete ${modifier.kind} modifier`}
|
||||
className="shrink-0 text-red-500 hover:text-red-700 text-xs font-bold leading-none"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
{/* Add modifier form */}
|
||||
<AddModifierForm square={selectedSquare} onAdd={onAdd} />
|
||||
</>
|
||||
{squareModifiers.map(({ modifier, index }) => (
|
||||
<li
|
||||
key={index}
|
||||
data-testid={`instance-modifier-${selectedSquare}-${modifier.kind}`}
|
||||
className="flex items-center gap-2 px-3 py-1.5 bg-white border border-neutral-200 rounded text-xs"
|
||||
>
|
||||
<span className="flex-1 font-medium text-neutral-700 break-all">
|
||||
{modifier.kind}: {JSON.stringify(modifier.value)}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => onDelete(index)}
|
||||
aria-label={`Delete ${modifier.kind} modifier`}
|
||||
className="shrink-0 text-red-500 hover:text-red-700 text-xs font-bold leading-none"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p className="text-[11px] text-neutral-400 italic py-1">
|
||||
No modifiers on this square yet.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="w-full sm:w-[280px] shrink-0">
|
||||
<AddModifierForm square={selectedSquare} onAdd={onAdd} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -309,7 +352,7 @@ function AddModifierForm({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2 border-t border-neutral-200 pt-3">
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-xs font-bold text-neutral-500 uppercase tracking-widest">
|
||||
Add Modifier
|
||||
</h4>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue