Files
CableGUI/web
mAi b15913124a feat: frontend — frames + devices on SVG, tools, drag, inspector
Renders the slice-2 backend on the empty canvas from slice 1.

Canvas:
- Frames render as dashed-stroke rects with top-left label, slightly
  tinted fill. Devices render as solid-stroke rects with centred label
  in device.color.
- Selection halo via .selected class (stroke-width bump).
- Empty-state hint disappears once any geometry exists.

Tools (left sidebar + keyboard):
- F / + Frame  — rubber-band rect on the canvas. <80×60 cancels. On
  release, inline foreignObject namer → POST /api/projects/:pid/frames.
- D / + Device — single click places a 100×35 device centred at the
  click. Inline namer → POST devices. Drop-point determines initial
  frame_id via point-in-rect against all frames (smallest bbox wins).
- Esc cancels active tool / inline namer / clears selection.

Drag (pointer events + svg getScreenCTM):
- Devices: drag updates x/y live via transform, persists via
  PATCH .../devices/:id on pointerup. Also recomputes frame_id from
  drop point and includes "frame_id": null|<id> if it changed.
- Frames: dragging a frame moves its contained devices visually too;
  on pointerup, single PATCH for the frame + one PATCH per moved device.
  Children-batch is computed at pointerdown and only sent on release —
  no per-pointermove network traffic.

Inspector:
- Frame selection: name (debounced rename), x/y/w/h, device count,
  Delete button (confirm prompt — devices keep existing, frame_id → NULL
  via the schema's ON DELETE SET NULL).
- Device selection: name (debounced rename), colour picker
  (change-event PATCH, no debounce), x/y/w/h, current frame, Delete.
- Background click clears selection.

devicePatch wire format uses tri-state frame_id: key absent = leave,
key:null = clear, key:<int> = move. Frontend uses `null` explicitly
when a device drops outside all frames.
2026-05-15 18:22:49 +02:00
..