Skip to content
Star on GitHub
Review and approveDesign review
Style
Accent
Sound

Built by DesignMe, free to use under the MIT licence.

Expert design and dev support for startups and enterprises.

Book a call

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

$npx designme add design-review
Pricing page v46 screens · sent for review by the design agent
In review
Checks
Review
Maya reviews once the checks pass
Waiting on checks
01

Install

Add it to your project with the CLI.

HTML, CSS and JavaScript

$npx designme add design-review

React

$npx designme add design-review --react

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

02

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

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} />
  );
}
04

Props

Everything DesignReview takes.

PropTypeWhat it does
titlerequiredstringWhat is up for review, like "Pricing page v4"
subtitlestringThe line under the title
iconIconName
checksrequiredReviewCheck[]
messagesrequiredReviewMessage[]The review so far, oldest first
phaserequiredReviewPhaseWhere it is: in review, approved (Ship turns on), shipping, then live or scheduled
notestringThe line beside the Ship button, like "Waiting on Maya"
waitingLabelstringShown in the review column before anyone has written
optionsShipOption[]The ways it can ship, picked from the menu beside the button
optionstring
defaultOptionstring
onOptionChange(key: string) => void
onShip(option: ShipOption) => void
doneLabelstringThe 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 }
classNamestring
05

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));
  });
});
06

Questions

What people ask about it.

Does it run the checks or send the review request?
No. It shows the checks, messages and phase you 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 a key, a label for the button and a detail for the menu. onShip gets the one that was picked.
07

More in Review and approve

Moments where a person checks, steers or signs off on the agent's work.

Change reviewAccept or reject each change the agent made, file by file.
Plan reviewCheck the agent's plan, edit steps, then let it run.
Permission gateThe agent asks before it touches something new.
Hand-off to a personThe agent hands a conversation to a person, with what they need to pick it up.
Decision promptThe agent asks a few quick questions before it starts.
Proposal cardA suggested change with confidence, alternatives and undo.
Design tunerTweak a component by hand or let the agent tune it.
Rewrite toolbarSelect a sentence and let the agent rewrite it in place.
All 14 in Review and approve →