Commit graph

4 commits

Author SHA1 Message Date
Simeon Schecter
ccbdb47162 Day 5+6 of the vertical slice: multi-size + per-row strips
Expands the slice from a single 300x250 banner to four IAB sizes
(300x600, 300x250, 728x90, 160x600) driven by a designer-authored
TypeSystem and a per-row strip review surface.

Layout engine
- TypeSystem with role-based typography (headline/subheadline/cta/legal)
  and piecewise size-class derivation: half_page / rectangle /
  leaderboard / skyscraper / mobile_banner.
- resolveLayout now derives per-size font/leading from the role +
  artboard size, then clamps to a legibility floor and emits a
  constraint_signal when copy does not fit at the floor.
- Four reference templates with character constraints per size.

AI pipeline (Shape B)
- One extract + one generate per feed row; generate returns per-size
  copy keyed by artboard_id plus a shared rationale block.
- Constraint-signal retry: orchestrator tightens per-(artboard, field)
  limits and re-calls generate before giving up.
- orchestrateRow returns specs[] + rationale + constraint_signals.

Review UI
- /review renders one strip per feed row, all four sizes side-by-side
  at true pixel dimensions, synced on a single GSAP master timeline.
- AiReasoningDrawer shows a per-size copy table, shared rationale, and
  any constraint signals that fired.
- /api/generate response grouped by row; /api/export accepts the same
  shape and writes exports/row-N/artboard_id.zip.

Render worker
- render-to-zip / render-many accept optional subdir + filename
  overrides so multi-size exports can be grouped by feed row.

Docs
- VERTICAL_SLICE and BUILD_SEQUENCE updated for the multi-size scope.
- RESOLVED_FEED.md documents the V1 Resolved Creative Feed proposal.
- SLICE_DEVIATIONS.md records where the slice diverges from V1.

Tests: 56 pass (28 layout-engine + 14 api-lib + 14 render-worker).
Web app: tsc clean, next build succeeds.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-18 14:22:26 -04:00
Simeon Schecter
bea0392d9c Day 4 of the vertical slice: render + export gate
Adds the render-and-export half of the slice: a Playwright-based render
worker that takes resolved BannerSpecs, builds self-contained HTML5 ad
units (absolute-positioned DOM, GSAP timeline, IAB clickTag, baked
typography + fonts), captures a backup PNG, and packages each into a zip
on disk. A new POST /api/export route drives it from /review via an
"Export ZIPs (N)" button.

What ships
- packages/render-worker (new package, promoted from the apps/ placeholder)
  - build-runtime-html: pure function producing the self-contained
    index.html. Inlines @font-face for Inter Regular/Bold, the resolved
    spec as JSON, GSAP's minified bundle, and a runtime IIFE that mirrors
    build-timeline.ts semantics (fade_in / fade_out / hold + padding tween).
    Layers with a fade_in event ship with opacity:0 inline to avoid a
    first-frame flash.
  - render-to-zip: stages temp dir, opens Playwright file://, awaits
    document.fonts.ready, pauses window.__tl and seeks to t=1.0s,
    screenshots backup.png, assembles JSZip on disk under
    exports/<safe(campaign_id)>/<safe(version_id)>.zip.
  - render-many: single Chromium, min(N,3) concurrent contexts, fonts
    + GSAP read once. Returns { exported, errors } so a single bad spec
    doesn't kill the batch.
  - safe-slug: sanitises path segments to [A-Za-z0-9_-], 'unknown' fallback.
  - 14 tests (8 build-runtime-html + 6 safe-slug). All green.
- apps/web/app/api/export/route.ts: validates each incoming spec with
  BannerSpecSchema (failures land in the errors list, not a 400), dynamic-
  imports the render-worker, calls renderMany, returns paths + byte sizes.
- apps/web/app/review/ReviewClient.tsx: export state machine (idle |
  exporting | done | error), "Export ZIPs (N)" button beside Play-All,
  success summary listing zip paths.
- apps/render-worker/ deleted; superseded by the package shape.

dropflow wasm wiring (Day-3 bug that surfaced on Day 4)
dropflow 0.5.1's wasm.js does `await environment.wasmLocator()` at module
top level. In Next.js 14.2.x, webpack's experiments.topLevelAwait flag is
ignored (regression vs 14.1.x), so any locator install that runs after
the dropflow chunk loads is too late — wasm.js's default locator throws
"Wasm location not configured" at module-init time.

Fix is a small pnpm patch on dropflow/dist/src/environment-browser.js,
which is imported by api.js BEFORE wasm.js via the `#register-default-
environment` browser-condition subpath. The patch installs a wasmLocator
that fetches /dropflow.wasm at exactly the right point in dropflow's
own initialization. patches/dropflow@0.5.1.patch is committed and wired
through pnpm.patchedDependencies.

Supporting changes in the layout-engine to keep the static module graph
dropflow-free where possible:
- browser/wasm-locator.ts: dynamic-imports dropflow/environment.js.
- browser/dropflow-wrapper.ts: dynamic-imports dropflow itself; uses an
  inline structural type instead of `import type * as DropflowNs`.
- core/measure.ts: removed eager dropflow import; receives the namespace
  via setDropflow() from whichever wrapper inits first.
- apps/web/lib/engine.ts: dynamic-imports the layout-engine browser
  barrel so dropflow doesn't enter /review's synchronous graph.
- BannerCanvas.tsx + ParityClient.tsx: pull DEMO_TEMPLATE_300x250 from
  the dropflow-free @banner-studio/layout-engine/templates subpath
  rather than /browser. ParityClient also dynamic-imports resolveLayout.

Other plumbing
- next.config.mjs: server externals for playwright / playwright-core /
  chromium-bidi / jszip / @banner-studio/render-worker so webpack doesn't
  try to bundle dynamic CJS requires it can't trace.
- SLICE_DEVIATIONS.md: added §6 (render-worker as a package not an app),
  §7 (hero image remote-referenced, not bundled), §8 (CM360 click-macro
  deferred — IAB clickTag only).

Acceptance gate (Day 4)
- pnpm -r test: 40 tests across 4 packages green.
- pnpm -r build: clean, /api/export present in the Next route table.
- Manual smoke: /review → Export ZIPs → real .zip files in
  exports/demo-campaign/. Unzipped index.html opens in Chrome, animates,
  and the clickTag opens example.com/<row>.url in a new tab.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-16 13:29:01 -04:00
Simeon Schecter
2842c5c2c6 Day 3 of the vertical slice: /review page (Konva grid + GSAP + drawer)
Producer-visible demo gate. /review hits POST /api/generate on mount, lays
the resulting BannerSpecs into a grid of Konva canvases, plays each on its
own GSAP timeline, and opens a right-side drawer showing AI reasoning +
the resolved spec JSON when a card is clicked.

UI:
- apps/web/app/review/page.tsx: thin shell, lazy-loads ReviewClient via
  next/dynamic(ssr:false). Mirrors /parity (Konva touches window; dropflow
  uses top-level await — neither survives SSG).
- ReviewClient.tsx: init → loading → ready | error state machine. Calls
  ensureBrowserEngine() then fetch('/api/generate'). Reconstructs feed-row
  order by slotting skipped/error rows at their declared rowIndex and
  filling the gaps with the ok specs in arrival order.
- components/BannerCanvas.tsx: pure Konva renderer. Gray Rect fallback,
  hero Image with manual cover-fit (Konva has no native object-fit:cover),
  text layers wrapped in <Group ref> keyed by layer_id so GSAP can tween
  attrs.opacity. Typography is looked up from DEMO_TEMPLATE_300x250 since
  ResolvedLayer doesn't carry it.
- components/BannerCard.tsx: one card per spec. Builds its GSAP timeline
  on mount inside a requestAnimationFrame (so Konva refs are populated),
  registers a restart fn upstream, kills the timeline on unmount.
- components/BannerGrid.tsx: feed-row ordered grid. Skipped/error rows get
  gray placeholder cards with status pills so producers see exactly which
  row was which.
- components/AiReasoningDrawer.tsx: slide-in 420px drawer. Closes on X,
  backdrop click, or Escape. Four labeled reasoning sections + collapsible
  <details> with pretty-printed BannerSpec JSON.
- lib/build-timeline.ts: pure factory. fade_in/fade_out map to gsap tweens
  on node.attrs.opacity; hold is a no-op; one timeline-level onUpdate
  drives stage.batchDraw(). Initial opacity(0) is set before play to avoid
  a first-frame flash.
- lib/typography-lookup.ts: walks the unresolved template (incl. group
  children) for TypographySpec by layer_id.
- styles.module.css: scoped grid/card/drawer styles.

Other:
- apps/web/package.json: konva@^9, react-konva@^18.2.10, gsap@^3.12.
- apps/web/app/page.tsx: /review listed as "review grid (live)".

Verified: pnpm -F @banner-studio/web build is clean (/review in route
table at 1.21 kB). All 26 Day-2 tests still pass. Dev server boots and
serves /review at HTTP 200 with the expected CSR bail-out.

End-of-day gate (VERTICAL_SLICE.md:154): A producer-shaped user could see
this and immediately understand what the product does. They see 5 banners
generated from 5 feed rows, animating, with reasoning visible.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-15 14:01:59 -04:00
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