feat(floorplan-editor): polish height slider + add hand tool for canvas pan

Two related polish improvements after the swatch-column → vertical-
slider swap.

Slider
- Wider track (18 px, was 14 px) for a more comfortable click area
  with the same on-screen footprint.
- Min / max chips above and below the rail (HEIGHT_BRUSH_MIN /
  _MAX) so users know which end is high and which is low without
  hovering to discover.
- Thumb now uses a warm amber radial gradient (#fff7c4 → #facc15
  → #ca8a04) on a dark brown border with a soft drop shadow + inset
  highlight, instead of the flat yellow disc. Hover adds a white
  ring; drag swaps it for a darker ring — clear gesture feedback.
- Track gains a hover/drag glow (inset white seam + amber outline
  via boxShadow) so you can tell the slider has focus before you
  even click.

Hand tool (canvas pan)
- New FloorplanToolbar button (FaHandPaper, sticky toggle, emerald
  fill when active) ties to a new  state lifted into
  FloorplanEditorView. When the hand is active, plain left-click
  + drag pans the canvas instead of brushing tiles. Cursor flips
  to grab / grabbing accordingly.
- FloorplanCanvasSVG's isPanGesture predicate becomes:
  middle-mouse  OR  Shift+left-click  OR  (panMode && left-click).
  Shift / middle still work whether or not the hand is on so power
  users keep their muscle memory.
- No change to the reducer (panMode is a canvas-level UI flag, not
  a brush action — keeps state/types tight).
This commit is contained in:
simoleo89
2026-05-24 21:32:49 +02:00
committed by simoleo89
parent abf43d86c3
commit 12d24719cf
4 changed files with 97 additions and 49 deletions
@@ -9,6 +9,13 @@ import { TILE_SIZE, MAX_NUM_TILE_PER_AXIS } from '../state/constants';
type Props = {
state: FloorplanState;
dispatch: Dispatch<FloorplanAction>;
/**
* When true, left-click + drag pans the canvas instead of
* brushing. Driven by the hand-tool toggle in the toolbar.
* Shift+drag and middle-mouse drag always pan regardless of
* this flag.
*/
panMode?: boolean;
};
const VIEWBOX_W = 2048;
@@ -76,7 +83,7 @@ const computeRoomBounds = (state: FloorplanState): { x: number; y: number; w: nu
};
};
export const FloorplanCanvasSVG: FC<Props> = ({ state, dispatch }) =>
export const FloorplanCanvasSVG: FC<Props> = ({ state, dispatch, panMode }) =>
{
const svgRef = useRef<SVGSVGElement | null>(null);
const [ zoom, setZoom ] = useState(1);
@@ -204,14 +211,24 @@ export const FloorplanCanvasSVG: FC<Props> = ({ state, dispatch }) =>
};
}, [ visW ]);
const isPanGesture = (e: ReactPointerEvent): boolean => e.button === 1 || (e.button === 0 && e.shiftKey);
// Pan gestures: middle-mouse, Shift+left-click, and (when the
// hand-tool is active) plain left-click. The hand-tool toggle
// is the toolbar affordance — Shift / middle still work even
// when the hand isn't on, so power users keep their muscle
// memory.
const isPanGesture = (e: ReactPointerEvent): boolean =>
e.button === 1
|| (e.button === 0 && e.shiftKey)
|| (e.button === 0 && Boolean(panMode));
const cursorClass = isPanning ? 'cursor-grabbing' : panMode ? 'cursor-grab' : '';
return (
<div className="relative w-full h-full">
<svg
ref={ svgRef }
viewBox={ viewBox }
className={ `w-full h-full select-none rounded-md border border-zinc-300 bg-[url('@/assets/images/floorplaneditor/canvas_floor_pattern.png')] bg-repeat [image-rendering:pixelated] ${ isPanning ? 'cursor-grabbing' : '' }` }
className={ `w-full h-full select-none rounded-md border border-zinc-300 bg-[url('@/assets/images/floorplaneditor/canvas_floor_pattern.png')] bg-repeat [image-rendering:pixelated] ${ cursorClass }` }
onWheel={ onWheel }
onPointerDown={ e =>
{