# 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: - `Cmd` on macOS = `Ctrl` on Windows/Linux. The shortcuts below are written with `Cmd` for brevity; the implementation must bind both. - `Shift`, `Alt`, and `Cmd` modifiers 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 `behavior` rules (push_siblings, character constraints) - Re-anchors `push_siblings.layer_id` references 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 + L` for lock vs. layer-level locking.** Anti-pattern document and FRUSTRATION_LIST.md both reference field-level locking (`HumanOverride.locked`). This document defines `Cmd + L` as layer-level lock (`BaseLayer.locked`). These are two different locks. Confirm naming and keybinding to avoid user confusion — likely: `Cmd + L` locks 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: none` on the canvas wrapper + intercepting `Cmd + scroll`) before implementation so behavior is consistent across Chrome/Safari/Firefox. - **`Cmd + Y` vs. `Cmd + Shift + Z` for redo.** Both bound. Adobe convention is `Cmd + Shift + Z`; some users expect `Cmd + Y`. Both work; no conflict — but document that `Cmd + Y` is an alias, not a separate action. - **Tab cycles z-order vs. Tab in text edit.** `Tab` exits 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.