Review and approveDesign review
Built by DesignMe, free to use under the MIT licence.
Expert design and dev support for startups and enterprises.
Book a callDesign review
Checks run, a teammate asks for a change, the agent makes it, and you ship. Free and MIT licensed, as HTML or a typed React component, in two styles with light and dark mode.
Install
Add it to your project with the CLI.
HTML, CSS and JavaScript
npx designme add design-reviewReact
npx designme add design-review --reactThe CLI copies the source into your project, so you own it and can change anything. The HTML version lands in ./designme with the shared core and a demo.html. The React version lands in src/components/designme and needs React 19; it works in Next.js, Vite and Astro.
About
What it is for and what it does.
Design review follows something the agent made on its way out. Automated checks run first, then a teammate reviews it, asks for a change and the agent makes it, and once they approve, a split button lets you ship it to everyone, to a share of visitors first, or on a set day.
- When a person has to sign off on the agent's work before it goes live.
- When the review is a conversation: the reviewer asks for a change, the agent answers with what it did.
- When automated checks and a human review both gate a release, and people should see them side by side.
- When shipping has options, like a gradual rollout or a scheduled launch.
checksrun on the left, each with astate(wait,run,okorbad) and a resultnoteon the right.messageshold the review, oldest first. Each has anauthor(an avatar, or the agent's mark withagent: true), an optionalverdicttag with a tone, an icon or a spinner, and thetextthey wrote.phasemoves fromreviewtoapproved,shipping, thenliveorscheduled. The tag in the header follows it, and the Ship button only turns on once it isapproved.- The menu beside Ship picks how it goes out.
optionsreplace the defaults (to everyone, to 10% first, on Monday), andoption/defaultOption/onOptionChangekeep the pick. onShipgets the picked option. Once the phase isliveorscheduled, the buttons give way todoneLabel.- At wide sizes the checks and the review sit side by side; in a narrow container they stack.
- The review column is an
aria-live="polite"region, so new messages are read out as they arrive. - Ship and the menu button are real buttons, disabled until the review is approved. The menu button is labelled "How it ships", says whether its menu is open, and Escape closes the menu.
- Each shipping option is a menu item that marks the picked one with
aria-checked. - From the keyboard, Enter, Space or the down arrow opens a menu with the focus on its picked item, or its first (the up arrow, on its last); the arrow keys move through it and wrap, Home and End jump to the ends, Tab closes it and moves on, and closing it gives the focus back to its button.
Usage
Import it and drive it from your own state.
Import
import { DesignReview } from '@/components/designme/design-review/DesignReview';Example
The demo above as a React component, 50 lines you can copy.
'use client';
// The animated preview from designme.dev, rebuilt from the React component: an example of driving it from your own state.
import { useState } from 'react';
import { DesignReview, type ReviewCheck, type ReviewMessage, type ReviewPhase, type ShipOption } from './DesignReview';
import { useScript, useWait } from '../lib/useScript';
/* ---------- Design review: three checks pass, Maya asks for one change, the agent makes it, she approves, you ship ---------- */
const CHECKS: [label: string, result: string][] = [['Visual diff', '2 of 6 screens changed'], ['Library tokens', 'All from the library'], ['Contrast', 'AA on every screen']];
const MAYA = { name: 'Maya Kim', initials: 'MK', color: 'var(--dm-av-l)' };
const AGENT = { name: 'Design agent', agent: true };
const DONE: Record<string, string> = { all: 'Live for every visitor', some: 'Live for 10% of visitors. Everyone tomorrow', later: 'Ships on Monday at 09:00' };
export default function DesignReviewDemo() {
const [checks, setChecks] = useState<ReviewCheck[]>(CHECKS.map(([label]) => ({ label, state: 'wait' })));
const [messages, setMessages] = useState<ReviewMessage[]>([]);
const [phase, setPhase] = useState<ReviewPhase>('review');
const [note, setNote] = useState('Waiting on checks');
const [done, setDone] = useState('');
const wait = useWait();
useScript(async ({ wait }) => {
await wait(500);
for (let i = 0; i < CHECKS.length; i++) {
setChecks(cs => cs.map((c, k) => k === i ? { ...c, state: 'run' } : c));
await wait(650 + i * 120);
setChecks(cs => cs.map((c, k) => k === i ? { ...c, state: 'ok', note: CHECKS[i][1] } : c));
}
setNote('Waiting on Maya'); await wait(900);
setMessages([{ author: MAYA, verdict: { label: 'Changes requested', tone: 'warn' }, text: 'Start the plan toggle on yearly. Most people pick it.' }]);
setNote('The agent is on it'); await wait(1200);
setMessages(ms => [...ms, { author: AGENT, verdict: { label: 'Updating', busy: true } }]);
await wait(1300);
setMessages(ms => [ms[0], { author: AGENT, verdict: { label: 'Updated' }, text: 'The toggle starts on yearly on all 6 screens.' }]);
await wait(1100);
setMessages(ms => [...ms, { author: MAYA, verdict: { label: 'Approved', tone: 'ok', icon: 'check' } }]);
setNote('Approved by Maya'); setPhase('approved');
});
const ship = async (o: ShipOption) => {
setPhase('shipping'); setNote('Publishing 6 screens');
await wait(1000);
setDone(DONE[o.key]); setPhase(o.key === 'later' ? 'scheduled' : 'live');
};
return (
<DesignReview title="Pricing page v4" subtitle="6 screens · sent for review by the design agent" checks={checks} messages={messages}
phase={phase} note={note} waitingLabel="Maya reviews once the checks pass" onShip={ship} doneLabel={done} />
);
}
Props
Everything DesignReview takes.
| Prop | Type | What it does |
|---|---|---|
titlerequired | string | What is up for review, like "Pricing page v4" |
subtitle | string | The line under the title |
icon | IconName | |
checksrequired | ReviewCheck[] | |
messagesrequired | ReviewMessage[] | The review so far, oldest first |
phaserequired | ReviewPhase | Where it is: in review, approved (Ship turns on), shipping, then live or scheduled |
note | string | The line beside the Ship button, like "Waiting on Maya" |
waitingLabel | string | Shown in the review column before anyone has written |
options | ShipOption[] | The ways it can ship, picked from the menu beside the button |
option | string | |
defaultOption | string | |
onOptionChange | (key: string) => void | |
onShip | (option: ShipOption) => void | |
doneLabel | string | The line that replaces the buttons once it is live or scheduled, like "Live for every visitor" |
labels | { checks?: string; review?: string; inReview?: string; approved?: string; live?: string; scheduled?: string; shipping?: string } | |
className | string |
Code
The full source, as the CLI installs it.
DesignReview.tsxReact · 161 lines
'use client';
import { cx } from '../lib/cx';
import { useControllable } from '../lib/hooks';
import { Icon, type IconName } from '../lib/icons';
import { Avatar, enter, Spin, Swap } from '../lib/motion';
import { usePopover } from '../lib/popover';
import '../designme.css';
import './design-review.css';
export type ReviewCheckState = 'wait' | 'run' | 'ok' | 'bad';
export interface ReviewCheck {
/** What is checked, like "Visual diff" */
label: string;
/** wait (not run yet), run (running now), ok or bad */
state: ReviewCheckState;
/** The result on the right, like "2 of 6 screens changed" */
note?: string;
}
export interface ReviewMessage {
/** Who wrote it. `agent` draws the agent's mark instead of an avatar */
author: { name: string; initials?: string; color?: string; agent?: boolean };
/** The tag beside the name, like Changes requested or Approved. `busy` puts a spinner in it */
verdict?: { label: string; tone?: 'ok' | 'warn' | 'bad' | 'neutral'; icon?: IconName; busy?: boolean };
/** What they said, under the name */
text?: string;
}
export interface ShipOption {
key: string;
/** The button's label when it is picked, like "Ship to 10% first" */
label: string;
/** The quiet line under it in the menu */
detail?: string;
}
export type ReviewPhase = 'review' | 'approved' | 'shipping' | 'live' | 'scheduled';
export interface DesignReviewProps {
/** What is up for review, like "Pricing page v4" */
title: string;
/** The line under the title */
subtitle?: string;
icon?: IconName;
checks: ReviewCheck[];
/** The review so far, oldest first */
messages: ReviewMessage[];
/** Where it is: in review, approved (Ship turns on), shipping, then live or scheduled */
phase: ReviewPhase;
/** The line beside the Ship button, like "Waiting on Maya" */
note?: string;
/** Shown in the review column before anyone has written */
waitingLabel?: string;
/** The ways it can ship, picked from the menu beside the button */
options?: ShipOption[];
option?: string;
defaultOption?: string;
onOptionChange?: (key: string) => void;
onShip?: (option: ShipOption) => void;
/** The line that replaces the buttons once it is live or scheduled, like "Live for every visitor" */
doneLabel?: string;
labels?: { checks?: string; review?: string; inReview?: string; approved?: string; live?: string; scheduled?: string; shipping?: string };
className?: string;
}
const SHIP: ShipOption[] = [
{ key: 'all', label: 'Ship to everyone', detail: 'Every visitor sees it now' },
{ key: 'some', label: 'Ship to 10% first', detail: 'Everyone else after a day' },
{ key: 'later', label: 'Ship on Monday', detail: 'At 09:00 your time' }
];
function Glyph({ state }: { state: ReviewCheckState }) {
const g = state === 'run' ? <Spin /> : state === 'ok' ? <span style={{ color: 'var(--dm-ok)', display: 'grid' }}><Icon name="ok" size="xs" /></span>
: state === 'bad' ? <span style={{ color: 'var(--dm-bad)', display: 'grid' }}><Icon name="alert" size="xs" /></span> : <span className="dm-drv-ring" />;
return <span key={state} className={state === 'wait' ? undefined : 'dm-enter'} style={{ display: 'grid' }}>{g}</span>;
}
/** Something the agent made, on its way out: checks run, a teammate reviews and the agent answers, then you pick how it ships. */
export function DesignReview({
title, subtitle, icon = 'palette', checks, messages, phase, note, waitingLabel = 'The review starts once the checks pass',
options = SHIP, option, defaultOption, onOptionChange, onShip, doneLabel, labels = {}, className
}: DesignReviewProps) {
const [pick, setPick] = useControllable(option, defaultOption ?? options[0].key, onOptionChange);
const menu = usePopover({ align: 'end' });
const chosen = options.find(o => o.key === pick) ?? options[0];
const L = { checks: 'Checks', review: 'Review', inReview: 'In review', approved: 'Approved', live: 'Live', scheduled: 'Scheduled', shipping: 'Shipping', ...labels };
const done = phase === 'live' || phase === 'scheduled';
const tag = phase === 'review' ? ['dm-b', L.inReview] : phase === 'live' ? ['dm-ok', L.live] : phase === 'scheduled' ? ['dm-b', L.scheduled] : ['dm-ok', L.approved];
const locked = phase !== 'approved';
return (
<div data-dm="" className={cx('dm-drv-box', className)} style={{ containerType: 'inline-size', width: '100%' }}>
<div className={cx('dm-drv dm-panel', phase === 'live' && 'dm-live')}>
<div className="dm-drv-h">
<span className="dm-drv-ic"><Icon name={icon} /></span>
<div className="dm-drv-ht"><b>{title}</b>{subtitle && <span className="dm-muted">{subtitle}</span>}</div>
<span className={cx('dm-tag dm-drv-tag', tag[0])}><Swap>{tag[1]}</Swap></span>
</div>
<div className="dm-drv-b">
<div className="dm-drv-col">
<span className="dm-drv-lb">{L.checks}</span>
<div className="dm-drv-checks">
{checks.map((c, i) => (
<div key={i} className="dm-drv-c">
<span className="dm-g"><Glyph state={c.state} /></span>
<span className="dm-t">{c.label}</span>
<Swap className="dm-d">{c.note ?? (c.state === 'run' ? 'Running' : '')}</Swap>
</div>
))}
</div>
</div>
<div className="dm-drv-col">
<span className="dm-drv-lb">{L.review}</span>
<div className="dm-drv-rv" aria-live="polite">
{messages.length ? messages.map((m, i) => (
<div key={i} className="dm-drv-m dm-enter">
{m.author.agent
? <span className="dm-drv-ag"><Icon name="spark" size="xs" /></span>
: <Avatar initials={m.author.initials ?? m.author.name.split(' ').map(w => w[0]).join('').slice(0, 2)} color={m.author.color ?? 'var(--dm-av-l)'} />}
<div className="dm-drv-mb">
<span className="dm-drv-mn">
<b>{m.author.name}</b>
{m.verdict && (
<span className={cx('dm-tag', m.verdict.busy && 'dm-drv-wk', m.verdict.tone && m.verdict.tone !== 'neutral' && 'dm-' + m.verdict.tone)}>
{m.verdict.busy ? <Spin /> : m.verdict.icon && <Icon name={m.verdict.icon} size="xs" />}{m.verdict.label}
</span>
)}
</span>
{m.text && <p key={m.text} {...enter()}>{m.text}</p>}
</div>
</div>
)) : <span className="dm-muted dm-drv-wt">{waitingLabel}</span>}
</div>
</div>
</div>
<div className="dm-drv-f">
{done ? (
<span className="dm-drv-done dm-enter"><Icon name={phase === 'scheduled' ? 'clock' : 'ok'} /><span>{doneLabel ?? chosen.label}</span></span>
) : <>
<Swap className="dm-muted dm-drv-st">{note ?? ''}</Swap>
<div className={cx('dm-drv-mg', !locked && 'dm-enter')}>
<button type="button" className="dm-pill dm-accent dm-sm dm-drv-go" disabled={locked} onClick={() => onShip?.(chosen)}>
<Swap>{phase === 'shipping' ? L.shipping : chosen.label}</Swap>
</button>
<button type="button" className="dm-pill dm-accent dm-sm dm-drv-dd" aria-label="How it ships" disabled={locked} {...menu.trigger}><Icon name="chev-d" size="xs" /></button>
</div>
<div {...menu.pop} className={cx(menu.pop.className, 'dm-drv-pop')} role="menu">
{options.map(o => (
<button type="button" key={o.key} className="dm-mi" role="menuitemradio" aria-checked={o.key === chosen.key} onClick={() => { setPick(o.key); menu.closeSoon(); }}>
<span>{o.label}{o.detail && <span className="dm-d">{o.detail}</span>}</span><span className="dm-ck"><Icon name="check" size="xs" /></span>
</button>
))}
</div>
</>}
</div>
</div>
</div>
);
}
design-review.cssReact · 42 lines
/* Generated from designme.dev by scripts/sync.mjs. Edit the site, then sync. */
/* ---------- Design review ---------- */
.dm-drv{padding:var(--dm-s-4);display:flex;flex-direction:column;gap:var(--dm-s-3)}
.dm-drv-h{display:flex;align-items:center;gap:10px}
.dm-drv-ic{width:32px;height:32px;border-radius:10px;background:var(--dm-pb-bg);color:var(--dm-pb);display:grid;place-items:center;flex:none;transition:background .4s,color .4s}
.dm-drv.dm-live .dm-drv-ic{background:var(--dm-ok-bg);color:var(--dm-ok)}
.dm-drv-ht{display:flex;flex-direction:column;min-width:0;flex:1}
.dm-drv-ht b{font-weight:500;font-size:13.5px}
.dm-drv-ht span{font-size:11.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.dm-drv-b{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:14px 18px;align-items:start}
@container (max-width:600px){.dm-drv-b{grid-template-columns:minmax(0,1fr)}}
.dm-drv-col{display:flex;flex-direction:column;gap:var(--dm-s-2);min-width:0}
.dm-drv-lb{font-size:11.5px;color:var(--dm-muted);font-weight:500}
.dm-drv-wt{font-size:12.5px;padding:3px 0}
.dm-drv-checks{display:flex;flex-direction:column}
/* the checks are a plain list, flush with the column, with a hairline between them and the same room above and below each */
.dm-drv-c{display:flex;align-items:center;gap:10px;padding:var(--dm-s-2) 0;font-size:12.5px}
.dm-drv-c+.dm-drv-c{border-top:1px solid color-mix(in srgb,var(--dm-line) 50%,transparent)}
.dm-drv-c .dm-g{width:16px;height:16px;display:grid;place-items:center;flex:none}
.dm-drv-c .dm-t{flex:1;min-width:0}
.dm-drv-c .dm-d{color:var(--dm-muted);font-size:12px;text-align:right}
.dm-drv-ring{width:12px;height:12px;border-radius:50%;box-shadow:inset 0 0 0 1.5px color-mix(in srgb,var(--dm-faint) 50%,transparent)}
.dm-drv-rv{display:flex;flex-direction:column;gap:10px;min-height:150px}
@container (max-width:600px){.dm-drv-rv{min-height:0}}
.dm-drv-m{display:flex;align-items:flex-start;gap:9px;font-size:12.5px}
.dm-drv-m .dm-av{width:22px;height:22px;margin-top:1px}
.dm-drv-ag{width:22px;height:22px;border-radius:50%;background:var(--dm-chip);color:var(--dm-fg2);display:grid;place-items:center;flex:none;margin-top:1px}
.dm-drv-mb{display:flex;flex-direction:column;gap:3px;min-width:0;flex:1}
.dm-drv-mn{display:flex;align-items:center;gap:7px;min-height:22px;flex-wrap:wrap}
.dm-drv-mn b{font-weight:500}
.dm-drv-mb p{margin:0;color:var(--dm-fg2);line-height:1.45}
/* a hairline sets the status and the Ship button apart from the review, with the same room above and below them */
.dm-drv-f{display:flex;align-items:center;gap:10px;position:relative;padding-top:var(--dm-s-4);border-top:1px solid color-mix(in srgb,var(--dm-line) 50%,transparent)}
.dm-drv-st{flex:1;min-width:0;font-size:12.5px}
.dm-drv-mg{display:flex;gap:1px;flex:none}
.dm-drv-go{border-radius:var(--dm-r-pill) 0 0 var(--dm-r-pill);padding-right:var(--dm-s-3)}
.dm-drv-dd{border-radius:0 var(--dm-r-pill) var(--dm-r-pill) 0;padding:0 9px}
.dm-drv-pop{min-width:230px}
.dm-drv-done{display:flex;align-items:center;gap:var(--dm-s-2);font-size:13px;flex:1}
.dm-drv-done svg{color:var(--dm-ok)}
.dm-drv-wk .dm-spin{width:9px;height:9px;border-width:1.3px}
design-review.htmlHTML · 21 lines
<div class="card wide" data-c="design-review">
<div class="stage-in">
<div class="drv panel">
<div class="drv-h"><span class="drv-ic"><svg class="i s"><use href="#i-palette"/></svg></span><div class="drv-ht"><b>Pricing page v4</b><span class="muted">6 screens · sent for review by the design agent</span></div><span class="tag b drv-tag">In review</span></div>
<div class="drv-b">
<div class="drv-col"><span class="drv-lb">Checks</span><div class="drv-checks"></div></div>
<div class="drv-col"><span class="drv-lb">Review</span><div class="drv-rv" aria-live="polite"><span class="muted drv-wt">Maya reviews once the checks pass</span></div></div>
</div>
<div class="drv-f">
<span class="muted drv-st">Waiting on checks</span>
<div class="drv-mg"><button class="pill accent sm drv-go" disabled>Ship to everyone</button><button class="pill accent sm drv-dd" aria-label="How it ships" data-pop=".drv-pop" data-pop-align="end" disabled><svg class="i xs"><use href="#i-chev-d"/></svg></button></div>
<div class="pop drv-pop" role="menu">
<button class="mi" data-close data-m="Ship to everyone" aria-checked="true"><span>Ship to everyone<span class="d">Every visitor sees it now</span></span><span class="ck"><svg class="i xs"><use href="#i-check"/></svg></span></button>
<button class="mi" data-close data-m="Ship to 10% first" aria-checked="false"><span>Ship to 10% first<span class="d">Everyone else after a day</span></span><span class="ck"><svg class="i xs"><use href="#i-check"/></svg></span></button>
<button class="mi" data-close data-m="Ship on Monday" aria-checked="false"><span>Ship on Monday<span class="d">At 09:00 your time</span></span><span class="ck"><svg class="i xs"><use href="#i-check"/></svg></span></button>
</div>
</div>
</div>
</div>
</div>
design-review.cssHTML · 41 lines
/* ---------- Design review ---------- */
.drv{padding:var(--s-4);display:flex;flex-direction:column;gap:var(--s-3)}
.drv-h{display:flex;align-items:center;gap:10px}
.drv-ic{width:32px;height:32px;border-radius:10px;background:var(--pb-bg);color:var(--pb);display:grid;place-items:center;flex:none;transition:background .4s,color .4s}
.drv.live .drv-ic{background:var(--ok-bg);color:var(--ok)}
.drv-ht{display:flex;flex-direction:column;min-width:0;flex:1}
.drv-ht b{font-weight:500;font-size:13.5px}
.drv-ht span{font-size:11.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.drv-b{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:14px 18px;align-items:start}
@container (max-width:600px){.drv-b{grid-template-columns:minmax(0,1fr)}}
.drv-col{display:flex;flex-direction:column;gap:var(--s-2);min-width:0}
.drv-lb{font-size:11.5px;color:var(--muted);font-weight:500}
.drv-wt{font-size:12.5px;padding:3px 0}
.drv-checks{display:flex;flex-direction:column}
/* the checks are a plain list, flush with the column, with a hairline between them and the same room above and below each */
.drv-c{display:flex;align-items:center;gap:10px;padding:var(--s-2) 0;font-size:12.5px}
.drv-c+.drv-c{border-top:1px solid color-mix(in srgb,var(--line) 50%,transparent)}
.drv-c .g{width:16px;height:16px;display:grid;place-items:center;flex:none}
.drv-c .t{flex:1;min-width:0}
.drv-c .d{color:var(--muted);font-size:12px;text-align:right}
.drv-ring{width:12px;height:12px;border-radius:50%;box-shadow:inset 0 0 0 1.5px color-mix(in srgb,var(--faint) 50%,transparent)}
.drv-rv{display:flex;flex-direction:column;gap:10px;min-height:150px}
@container (max-width:600px){.drv-rv{min-height:0}}
.drv-m{display:flex;align-items:flex-start;gap:9px;font-size:12.5px}
.drv-m .av{width:22px;height:22px;margin-top:1px}
.drv-ag{width:22px;height:22px;border-radius:50%;background:var(--chip);color:var(--fg2);display:grid;place-items:center;flex:none;margin-top:1px}
.drv-mb{display:flex;flex-direction:column;gap:3px;min-width:0;flex:1}
.drv-mn{display:flex;align-items:center;gap:7px;min-height:22px;flex-wrap:wrap}
.drv-mn b{font-weight:500}
.drv-mb p{margin:0;color:var(--fg2);line-height:1.45}
/* a hairline sets the status and the Ship button apart from the review, with the same room above and below them */
.drv-f{display:flex;align-items:center;gap:10px;position:relative;padding-top:var(--s-4);border-top:1px solid color-mix(in srgb,var(--line) 50%,transparent)}
.drv-st{flex:1;min-width:0;font-size:12.5px}
.drv-mg{display:flex;gap:1px;flex:none}
.drv-go{border-radius:var(--r-pill) 0 0 var(--r-pill);padding-right:var(--s-3)}
.drv-dd{border-radius:0 var(--r-pill) var(--r-pill) 0;padding:0 9px}
.drv-pop{min-width:230px}
.drv-done{display:flex;align-items:center;gap:var(--s-2);font-size:13px;flex:1}
.drv-done svg{color:var(--ok)}
.drv-wk .spin{width:9px;height:9px;border-width:1.3px}
design-review.jsHTML · 36 lines
DM.def('design-review', (r, c) => {
r.innerHTML = r.innerHTML.replace('{palette}', I('palette')).replace('{chev}', I('chev-d','xs')).replace(/\{chk\}/g, I('check','xs'));
const C = [['Visual diff','2 of 6 screens changed'],['Library tokens','All from the library'],['Contrast','AA on every screen']];
const box = $('.drv-checks', r), rv = $('.drv-rv', r), go = $('.drv-go', r), dd = $('.drv-dd', r), st = $('.drv-st', r), tag = $('.drv-tag', r), root = $('.drv', r);
box.innerHTML = C.map(x => `<div class="drv-c"><span class="g"><span class="drv-ring"></span></span><span class="t">${x[0]}</span><span class="d"></span></div>`).join('');
const rows = $$('.drv-c', box);
const DONE = { 'Ship to everyone':'Live for every visitor', 'Ship to 10% first':'Live for 10% of visitors. Everyone tomorrow', 'Ship on Monday':'Ships on Monday at 09:00' };
// one message in the review: who, a tag, and what they said
const msg = (who, tg, text) => { const m = html(`<div class="drv-m">${who === 'agent' ? `<span class="drv-ag">${I('spark','xs')}</span>` : AV('MK','var(--av-l)')}<div class="drv-mb"><span class="drv-mn"><b>${who === 'agent' ? 'Design agent' : 'Maya Kim'}</b>${tg}</span>${text ? `<p>${text}</p>` : ''}</div></div>`); $('.drv-wt', rv)?.remove(); rv.appendChild(m); enter(m); return m; };
$$('.drv-pop .mi', r).forEach(m => m.addEventListener('click', () => { $$('.drv-pop .mi', r).forEach(x => x.setAttribute('aria-checked', x === m)); swap(go, m.dataset.m); }));
go.onclick = () => c.run(async () => {
const how = go.textContent; go.disabled = dd.disabled = true; swap(go, 'Shipping'); swap(st, 'Publishing 6 screens'); await c.wait(1000);
const later = how === 'Ship on Monday';
root.classList.toggle('live', !later); tag.className = 'tag ' + (later ? 'b' : 'ok') + ' drv-tag'; swap(tag, later ? 'Scheduled' : 'Live');
const f = $('.drv-f', r); f.innerHTML = `<span class="drv-done">${I(later ? 'clock' : 'ok')}<span>${DONE[how]}</span></span>`; enter(f.firstElementChild);
});
c.run(async () => {
await c.wait(500);
for (let i = 0; i < rows.length; i++){
const g = $('.g', rows[i]); g.innerHTML = '<span class="spin"></span>'; swap($('.d', rows[i]), 'Running');
await c.wait(650 + i * 120);
g.innerHTML = `<span style="color:var(--ok);display:grid">${I('ok','xs')}</span>`; enter(g.firstElementChild); swap($('.d', rows[i]), C[i][1]);
}
swap(st, 'Waiting on Maya'); await c.wait(900);
msg('maya', '<span class="tag warn">Changes requested</span>', 'Start the plan toggle on yearly. Most people pick it.');
swap(st, 'The agent is on it'); await c.wait(1200);
const a = msg('agent', '<span class="tag drv-wk"><span class="spin"></span>Updating</span>', '');
await c.wait(1300);
swap($('.drv-wk', a), 'Updated'); const p = html('<p>The toggle starts on yearly on all 6 screens.</p>'); $('.drv-mb', a).appendChild(p); enter(p);
await c.wait(1100);
msg('maya', `<span class="tag ok">${I('check','xs')}Approved</span>`, '');
swap(st, 'Approved by Maya'); tag.className = 'tag ok drv-tag'; swap(tag, 'Approved');
go.disabled = dd.disabled = false; enter($('.drv-mg', r));
});
});
Questions
What people ask about it.
- Does it run the checks or send the review request?
- No. It shows the
checks,messagesandphaseyou pass. Your code or agent runs the checks, collects the review and moves the phase along. - How is it different from Launch checklist?
- Launch checklist is the agent checking its own work and fixing what fails. Design review adds a person: a teammate reviews, the agent answers, and shipping waits for their approval.
- Can I change the ways it ships?
- Yes. Pass your own
options, each with akey, alabelfor the button and adetailfor the menu.onShipgets the one that was picked.
More in Review and approve
Moments where a person checks, steers or signs off on the agent's work.