banner_studio/INTERACTION_STANDARDS.md
Simeon Schecter 988a47c797 Initial commit: Day 1 + Day 2 of the vertical slice
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.
2026-05-15 10:25:21 -04:00

212 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.