Day 1 (monorepo + Node layout engine): - Turborepo + pnpm workspaces with apps/web, apps/render-worker, and packages for types, layout-engine, prompts, api-lib. - @banner-studio/types: BannerSpec contract, every layer kind, ResolvedLayer, zod schemas mirroring each interface. - @banner-studio/layout-engine: Dropflow WASM wrapper, text measurement, shrink-to-fit, push_siblings, resolveLayout. Snapshot-tested. Day 2 (browser parity + AI pipeline): - Layout engine ./browser subpath: same resolveLayout in the browser via Dropflow WASM build. Quarantined wasm-locator import (dropflow 0.5.1 exports gap). - Cross-group push_siblings bug fix: deltas now thread through group recursion via a shared accumulator; regression test added. - DEMO_TEMPLATE_300x250 promoted to packages/layout-engine/src/templates/. - @banner-studio/prompts: versioned extract + generate prompts with zod-defined tool schemas (claude-sonnet-4-6, forced tool-use). - @banner-studio/api-lib: CSV feed loader, extract/generate/route-node/ assemble agents, orchestrator returning fully-resolved BannerSpec. Generate agent retries on character-limit overflow. - apps/web (Next.js 14 App Router): /api/generate route, /parity diff page, promise-singleton browser engine init. - feeds/demo.csv with five hand-authored rows of varied length. - SLICE_DEVIATIONS.md documents the five intentional gaps from ARCHITECTURE.md with V1 reversal paths. Verified end-to-end: POST /api/generate against the live Claude API returns three resolved BannerSpecs and two honestly-skipped rows (overflow after two attempts). 26 unit + integration tests passing.
12 KiB
INTERACTION_STANDARDS.md
The canonical interaction reference for the banner production platform. These are non-negotiable. A developer who implements something different is fixing a bug, not making a design choice.
Conventions:
Cmdon macOS =Ctrlon Windows/Linux. The shortcuts below are written withCmdfor brevity; the implementation must bind both.Shift,Alt, andCmdmodifiers compose as written.- Every interaction is undoable except where explicitly noted.
- Behaviors apply across every editable surface in the app (template builder canvas, review grid edits, property panels) unless surface-specific overrides are called out.
Selection
| Action | Result |
|---|---|
| Single click on a layer | Select the layer |
| Single click on empty canvas | Deselect everything |
| Double click on a text layer | Enter text edit mode, cursor at click position |
| Double click on a group | Drill into the group (select first child); deeper double-clicks descend further |
Escape |
Exit text edit mode → exit nested group → deselect (cascading) |
Tab (with selection) |
Cycle to next sibling in z-order |
Shift + Tab |
Cycle to previous sibling in z-order |
Cmd + A |
Select all layers in the active artboard |
| Click + drag on empty canvas | Marquee select (V2 — out of slice) |
Shift + click |
Add to selection (multi-select) |
Cmd + click |
Toggle inclusion in selection |
| Right-click | Context menu — always a duplicate of UI affordances, never the only path |
Selection is always visible: every selected layer has a 1px outline in the system accent color, plus a bounding box for the multi-selection.
Multi-select and group operations
| Action | Result |
|---|---|
| Drag any selected layer | All selected layers move together, preserving relative position |
| Arrow keys with multi-select | Nudge all selected layers by 1px |
| Property panel edit with multi-select | Applies to all selected layers; conflicting values show "Mixed" and editing replaces |
| Resize handle with multi-select | Scales the bounding box; children scale proportionally |
| Align actions (left/center/right/top/middle/bottom) | Align selected layers relative to the bounding box, or to the artboard if only one layer is selected |
| Distribute (horizontal/vertical) | Available with 3+ layers selected |
Delete / Backspace |
Delete all selected layers (undoable, no confirmation) |
Cmd + G |
Group selected layers into a new GroupLayer with group_behavior: 'fixed' |
Cmd + Shift + G |
Ungroup the selected group, lifting children to the parent |
Cmd + L |
Lock selected layers (BaseLayer.locked = true) |
Cmd + Shift + L |
Unlock |
Cmd + ; |
Toggle visibility (BaseLayer.visible) |
Locked layers cannot be selected by click on the canvas — they must be selected via the layers panel. Hidden layers don't render and cannot be selected.
Movement
| Action | Result |
|---|---|
↑ / ↓ / ← / → |
Nudge 1px |
Shift + arrow |
Nudge 10px |
| Drag | Free move |
Shift + drag |
Constrain to nearest axis (horizontal or vertical) |
Alt + drag |
Duplicate and move — standard creative-tool behavior |
Cmd + drag |
Disable snapping for this gesture |
Snapping engages when the dragged element is within 4px of a snap target (artboard edge, safe zone, sibling edge or center, sibling baseline). Snap targets render a 1px guide before they engage. Cmd held during the drag suppresses all snapping.
Zoom
| Action | Result |
|---|---|
Cmd + scroll |
Zoom to cursor |
Cmd + = |
Zoom in |
Cmd + - |
Zoom out |
Cmd + 0 |
Fit all artboards in view |
Cmd + 1 |
100% (1:1 pixel) |
Cmd + 2 |
200% |
Space + drag |
Pan, even when a selection tool is active |
Zoom is continuous, not stepped. Browser zoom is disabled inside the editor — Cmd + scroll is always canvas zoom.
History
| Action | Result |
|---|---|
Cmd + Z |
Undo |
Cmd + Shift + Z |
Redo |
Cmd + Y |
Redo (alt) |
Undo is unlimited within the session, including in text edit mode, in the property panel, and in the review grid. Undo never has exceptions. This is treated as a correctness property of the application, not a feature.
History persists across reloads via the version log for major changes (every state change is auto-saved). In-session undo is fine-grained; cross-session undo operates at version granularity.
Text editing
| Action | Result |
|---|---|
| Double-click a text layer | Enter edit mode, cursor at click position |
Cmd + B |
Bold (toggles typography.font_weight within selection) |
Cmd + I |
Italic |
Cmd + U |
Underline |
Cmd + A |
Select all text in the layer |
Cmd + arrow |
Word-wise / line-wise cursor movement (standard OS behavior) |
Shift + arrow |
Extend selection |
Escape |
Commit edit and exit edit mode |
Tab |
Commit edit and select next layer in z-order |
Enter (in single-line text) |
Commit and exit |
Shift + Enter |
Line break within multi-line text |
Cmd + Z in edit mode |
Undo last text change (does not exit edit mode) |
Live text fit happens as the user types: Dropflow recalculates layout on every change, the canvas updates within 100ms, push_siblings cascades visibly. If the content exceeds the character limit, the layer renders with a limit-exceeded badge but does not block typing — the user sees the consequence and decides.
Copy / paste
| Action | Result |
|---|---|
Cmd + C |
Copy selected layer(s) with all properties |
Cmd + V |
Paste to the active artboard at the source layer's relative coordinates |
Cmd + Shift + V |
Paste in place — exact same coordinates as the source |
Cmd + D |
Duplicate in place, offset by (+10, +10) on the same artboard |
Cmd + X |
Cut (copy + delete) |
Paste always:
- Lands on the active artboard
- Preserves every visual property (typography, color, opacity, rotation, anchors)
- Preserves the layer's
behaviorrules (push_siblings, character constraints) - Re-anchors
push_siblings.layer_idreferences to copies of siblings if the siblings were copied too; otherwise the rule is preserved with the original target ID - Is undoable
Pasting across artboards of different sizes preserves the layer's pixel dimensions, not its proportional position — designers think in pixels for ad layouts.
Layer manipulation
| Action | Result |
|---|---|
Cmd + ] |
Bring forward one z-index |
Cmd + Shift + ] |
Bring to front |
Cmd + [ |
Send backward one z-index |
Cmd + Shift + [ |
Send to back |
| Drag in layers panel | Reorder z-index by dragging |
Cmd + G |
Group |
Cmd + Shift + G |
Ungroup |
Cmd + L / Cmd + Shift + L |
Lock / unlock |
Cmd + ; |
Toggle visibility |
Cmd + R |
Rename selected layer (focus name field in layers panel) |
The layers panel mirrors the canvas selection bidirectionally. Selecting in the panel selects on canvas; selecting on canvas highlights in the panel and auto-scrolls if needed.
Review grid shortcuts
| Action | Result |
|---|---|
→ / ← |
Next / previous banner |
↓ / ↑ |
Next / previous row in the grid |
Enter |
Open the focused banner detail (full-size view + AI reasoning panel) |
Escape |
Close detail / return to grid |
A |
Approve the focused banner |
R |
Reject the focused banner (prompts for one-line reason) |
G |
Regenerate the focused banner (shows pre-flight: overrides preserved, conflicts likely) |
E |
Edit the focused field (or activate inline edit if a field is in focus) |
Cmd + Enter |
Approve all banners in the campaign (only if all have status in_review and no unresolved conflicts) |
Space |
Play / pause animation on the focused banner |
? |
Show this shortcut sheet as an overlay |
Approval and rejection are bound to single keystrokes (A, R) because they are the most repeated actions in the review path. Regenerate is G deliberately — not Cmd+G, which is group — because regenerate is a single-banner action with a pre-flight modal that prevents accidental destruction.
The shortcut sheet (?) is the canonical onboarding aid for the review grid. Producers should not need a tutorial.
Touchpad and gesture
| Gesture | Result |
|---|---|
| Two-finger scroll | Pan the canvas (no scroll bars are visible during; the canvas is the document) |
| Pinch | Zoom to pinch center, continuous |
| Two-finger horizontal scroll on review grid | Pan horizontally through banners |
| Three-finger swipe | OS-level navigation (do not capture) |
| Double-tap with two fingers | Fit to view (Cmd + 0 equivalent) |
Browser-level pinch zoom is disabled in the editor and in the review grid — pinch is always canvas zoom. Two-finger scroll on the canvas pans, not page-scrolls. The canvas captures these gestures when the cursor is over it; outside the canvas, the page behaves normally.
Gesture conflict handling:
- If the cursor is over an inline text input or scrollable panel, two-finger scroll scrolls that element, not the canvas.
- Pinch over a scrollable panel still zooms the canvas (the page should never zoom; this is consistent everywhere in the editor).
Space + drag(panning shortcut) wins over gesture-based pan when both fire — keyboard intent is explicit.
Property panel
| Action | Result |
|---|---|
| Type in any numeric field | Live update on commit (blur or Enter) |
| Drag the field label | Scrub the numeric value (Photoshop-style) |
Shift + scrub |
10× speed |
Alt + scrub |
0.1× speed |
Tab |
Move to next field |
| Color swatch click | Open color picker; selection updates live as the picker moves |
Escape in a field |
Revert to the field's value before edit |
All property panel edits are undoable. The panel never shows a "Save" or "Apply" button.
Asset library (V1)
| Action | Result |
|---|---|
| Drag an asset onto a smart asset slot | Bind the asset's variant group to the slot |
| Drag an asset onto the canvas | Reject — assets must enter through smart asset slots, not free placement (anti-pattern #32) |
| Click an asset | Open asset detail (metadata, rights, approved crops) |
Cmd + F in the library |
Focus search |
| Filter pills (region, market, language, campaign type) | Multi-select OR within a pill, AND across pills |
Conflicts to resolve before build
Cmd + Lfor lock vs. layer-level locking. Anti-pattern document and FRUSTRATION_LIST.md both reference field-level locking (HumanOverride.locked). This document definesCmd + Las layer-level lock (BaseLayer.locked). These are two different locks. Confirm naming and keybinding to avoid user confusion — likely:Cmd + Llocks the layer; field-level lock is a toggle in the review grid's override pip menu, not a global keystroke.- Marquee select. Listed as V2 (out of slice) consistently with the source document. Confirm V1 includes it; if not, surface in the V1 acceptance criteria.
- Browser zoom disabled in the editor. Disabling browser zoom is technically tricky (browsers don't fully let you). Document the chosen approach (e.g., a CSS
touch-action: noneon the canvas wrapper + interceptingCmd + scroll) before implementation so behavior is consistent across Chrome/Safari/Firefox. Cmd + Yvs.Cmd + Shift + Zfor redo. Both bound. Adobe convention isCmd + Shift + Z; some users expectCmd + Y. Both work; no conflict — but document thatCmd + Yis an alias, not a separate action.- Tab cycles z-order vs. Tab in text edit.
Tabexits text edit mode and selects the next layer. This means Tab cannot insert a tab character into text. For ad copy this is fine (ad copy has no tabs), but flag in case a future text feature needs literal tabs.