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>
471 lines
7.2 KiB
CSS
471 lines
7.2 KiB
CSS
/* /review page styles. Scoped via CSS modules so we don't leak grid/drawer
|
|
rules into other routes. */
|
|
|
|
.page {
|
|
padding: 24px 32px 64px;
|
|
font-family: Inter, system-ui, sans-serif;
|
|
}
|
|
|
|
.header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: 24px;
|
|
gap: 16px;
|
|
}
|
|
|
|
.title {
|
|
font-size: 22px;
|
|
margin: 0;
|
|
}
|
|
|
|
.subtitle {
|
|
color: #555;
|
|
font-size: 13px;
|
|
margin: 4px 0 0;
|
|
}
|
|
|
|
.headerButtons {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.playAllBtn {
|
|
background: #0e1116;
|
|
color: #fff;
|
|
border: 0;
|
|
padding: 10px 16px;
|
|
border-radius: 6px;
|
|
font-size: 13px;
|
|
cursor: pointer;
|
|
font-family: inherit;
|
|
}
|
|
|
|
.playAllBtn:hover {
|
|
background: #1c2128;
|
|
}
|
|
|
|
.playAllBtn:disabled {
|
|
background: #999;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.exportBtn {
|
|
background: transparent;
|
|
color: #0e1116;
|
|
border: 1px solid #0e1116;
|
|
padding: 9px 15px;
|
|
border-radius: 6px;
|
|
font-size: 13px;
|
|
cursor: pointer;
|
|
font-family: inherit;
|
|
}
|
|
|
|
.exportBtn:hover {
|
|
background: #0e1116;
|
|
color: #fff;
|
|
}
|
|
|
|
.exportBtn:disabled {
|
|
border-color: #999;
|
|
color: #999;
|
|
background: transparent;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.exportSummary {
|
|
background: #f5f8fc;
|
|
border: 1px solid #d6e0ec;
|
|
border-radius: 6px;
|
|
padding: 12px 16px;
|
|
margin-bottom: 24px;
|
|
font-size: 12px;
|
|
font-family: ui-monospace, monospace;
|
|
color: #222;
|
|
}
|
|
|
|
.exportSummary ul {
|
|
margin: 6px 0 12px;
|
|
padding-left: 20px;
|
|
}
|
|
|
|
.exportSummary li {
|
|
margin: 2px 0;
|
|
word-break: break-all;
|
|
}
|
|
|
|
.exportSummary code {
|
|
background: #fff;
|
|
padding: 1px 4px;
|
|
border-radius: 3px;
|
|
border: 1px solid #e5ecf3;
|
|
}
|
|
|
|
.exportBytes {
|
|
color: #666;
|
|
}
|
|
|
|
.grid {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 16px;
|
|
}
|
|
|
|
/* Per-row strips: vertical stack of strips, each holds all four sizes
|
|
side-by-side at true pixel dimensions in a horizontally-scrollable row. */
|
|
.strips {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 20px;
|
|
}
|
|
|
|
.strip {
|
|
border: 1px solid #d9d9d9;
|
|
border-radius: 8px;
|
|
background: #fff;
|
|
cursor: pointer;
|
|
transition: box-shadow 120ms ease, border-color 120ms ease;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.strip:hover {
|
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
|
border-color: #bfbfbf;
|
|
}
|
|
|
|
.stripPlaceholder {
|
|
cursor: default;
|
|
}
|
|
|
|
.stripPlaceholder:hover {
|
|
box-shadow: none;
|
|
border-color: #d9d9d9;
|
|
}
|
|
|
|
.stripHeader {
|
|
padding: 10px 14px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
border-bottom: 1px solid #eee;
|
|
background: #fafafa;
|
|
}
|
|
|
|
.stripRowLabel {
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.04em;
|
|
color: #555;
|
|
}
|
|
|
|
.stripProductName {
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
color: #222;
|
|
flex: 1;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.stripCanvases {
|
|
display: flex;
|
|
gap: 16px;
|
|
padding: 16px;
|
|
align-items: flex-start;
|
|
background: #f5f5f5;
|
|
overflow-x: auto;
|
|
}
|
|
|
|
.stripCanvasCell {
|
|
position: relative;
|
|
background: #fff;
|
|
border: 1px solid #e5e5e5;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.stripSizeLabel {
|
|
position: absolute;
|
|
top: -8px;
|
|
left: 6px;
|
|
background: #0e1116;
|
|
color: #fff;
|
|
font-size: 10px;
|
|
font-weight: 500;
|
|
padding: 1px 6px;
|
|
border-radius: 3px;
|
|
font-family: ui-monospace, monospace;
|
|
}
|
|
|
|
.stripPlaceholderBody {
|
|
padding: 24px;
|
|
color: #888;
|
|
font-size: 13px;
|
|
text-align: center;
|
|
background: #f9f9f9;
|
|
}
|
|
|
|
.pillSignal {
|
|
background: #fff4d6;
|
|
color: #8a5a00;
|
|
}
|
|
|
|
.copyTable {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
font-size: 11px;
|
|
font-family: ui-monospace, monospace;
|
|
}
|
|
|
|
.copyTable th,
|
|
.copyTable td {
|
|
border: 1px solid #eee;
|
|
padding: 4px 6px;
|
|
text-align: left;
|
|
vertical-align: top;
|
|
word-break: break-word;
|
|
}
|
|
|
|
.copyTable th {
|
|
background: #fafafa;
|
|
font-weight: 600;
|
|
color: #555;
|
|
text-transform: uppercase;
|
|
font-size: 10px;
|
|
letter-spacing: 0.04em;
|
|
}
|
|
|
|
.copyTableSize {
|
|
font-weight: 600;
|
|
color: #0e1116;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.card {
|
|
width: 300px;
|
|
border: 1px solid #d9d9d9;
|
|
border-radius: 6px;
|
|
overflow: hidden;
|
|
background: #fff;
|
|
cursor: pointer;
|
|
transition: box-shadow 120ms ease;
|
|
}
|
|
|
|
.card:hover {
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
|
|
}
|
|
|
|
.cardCanvasWrap {
|
|
background: #f5f5f5;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 300px;
|
|
height: 250px;
|
|
}
|
|
|
|
.cardFooter {
|
|
padding: 8px 10px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 8px;
|
|
font-size: 12px;
|
|
border-top: 1px solid #eee;
|
|
}
|
|
|
|
.cardName {
|
|
color: #222;
|
|
font-weight: 500;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.pill {
|
|
padding: 2px 8px;
|
|
border-radius: 10px;
|
|
font-size: 11px;
|
|
font-weight: 500;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.pillOk {
|
|
background: #e6f4ea;
|
|
color: #137333;
|
|
}
|
|
|
|
.pillSkipped {
|
|
background: #fef7e0;
|
|
color: #b06000;
|
|
}
|
|
|
|
.pillError {
|
|
background: #fce8e6;
|
|
color: #c5221f;
|
|
}
|
|
|
|
.skippedCanvas {
|
|
width: 300px;
|
|
height: 250px;
|
|
background: #f5f5f5;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: #888;
|
|
font-size: 12px;
|
|
padding: 12px;
|
|
text-align: center;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.spinner {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 80px 0;
|
|
gap: 12px;
|
|
color: #555;
|
|
}
|
|
|
|
.spinnerRing {
|
|
width: 32px;
|
|
height: 32px;
|
|
border: 3px solid #ddd;
|
|
border-top-color: #0e1116;
|
|
border-radius: 50%;
|
|
animation: spin 0.9s linear infinite;
|
|
}
|
|
|
|
@keyframes spin {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
.errorBox {
|
|
padding: 16px;
|
|
background: #fce8e6;
|
|
color: #c5221f;
|
|
border-radius: 6px;
|
|
font-family: ui-monospace, monospace;
|
|
font-size: 12px;
|
|
white-space: pre-wrap;
|
|
}
|
|
|
|
/* Drawer */
|
|
|
|
.backdrop {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(0, 0, 0, 0.25);
|
|
z-index: 49;
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
transition: opacity 200ms ease;
|
|
}
|
|
|
|
.backdropOpen {
|
|
opacity: 1;
|
|
pointer-events: auto;
|
|
}
|
|
|
|
.drawer {
|
|
position: fixed;
|
|
top: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
width: 420px;
|
|
max-width: 100vw;
|
|
background: #fff;
|
|
z-index: 50;
|
|
box-shadow: -4px 0 16px rgba(0, 0, 0, 0.15);
|
|
transform: translateX(100%);
|
|
transition: transform 200ms ease;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.drawerOpen {
|
|
transform: translateX(0);
|
|
}
|
|
|
|
.drawerHeader {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 16px 20px;
|
|
border-bottom: 1px solid #eee;
|
|
}
|
|
|
|
.drawerTitle {
|
|
font-size: 15px;
|
|
font-weight: 600;
|
|
margin: 0;
|
|
}
|
|
|
|
.drawerClose {
|
|
background: transparent;
|
|
border: 0;
|
|
font-size: 20px;
|
|
cursor: pointer;
|
|
color: #555;
|
|
line-height: 1;
|
|
padding: 4px 8px;
|
|
}
|
|
|
|
.drawerClose:hover {
|
|
color: #000;
|
|
}
|
|
|
|
.drawerBody {
|
|
padding: 16px 20px;
|
|
overflow-y: auto;
|
|
flex: 1;
|
|
}
|
|
|
|
.drawerSection {
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.drawerSectionLabel {
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.04em;
|
|
color: #888;
|
|
margin: 0 0 4px;
|
|
}
|
|
|
|
.drawerSectionBody {
|
|
font-size: 13px;
|
|
color: #222;
|
|
line-height: 1.5;
|
|
margin: 0;
|
|
white-space: pre-wrap;
|
|
}
|
|
|
|
.drawerDetails {
|
|
margin-top: 20px;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.drawerDetails summary {
|
|
cursor: pointer;
|
|
color: #555;
|
|
font-weight: 500;
|
|
padding: 4px 0;
|
|
}
|
|
|
|
.drawerJson {
|
|
background: #0e1116;
|
|
color: #cde;
|
|
padding: 12px;
|
|
border-radius: 4px;
|
|
overflow: auto;
|
|
font-size: 11px;
|
|
line-height: 1.4;
|
|
max-height: 50vh;
|
|
margin: 8px 0 0;
|
|
}
|