# Demo walkthroughs — interactive click-through state

Demo views are wireframe states with internal `step` state — the user clicks a button (Accept / End / Save / Edit / etc.) and the view advances to the next step. Each step has its own callouts, swapped dynamically when the step changes.

This pattern is what makes the artifact feel like a "click-through prototype" instead of static slides.

## Why it's tricky

The tab navigation pattern uses a static `view.callouts` array per view. For demos, the callouts depend on internal state, which the App doesn't know about. The naive approach — let the demo component define `_CALLOUTS` and switch between them — doesn't work because the Callouts overlay is rendered by App, not the demo.

There are two failure modes you must avoid:

1. **No callouts ever render.** If the demo doesn't push its callouts to App, App reads `view.callouts: []` and renders nothing.
2. **Callouts disappear immediately on mount.** If App has a useEffect that resets dynamic callouts on view change (e.g. `useEffect(() => setDynamic(null), [active])`), it overrides the demo's mount-time push because parent useEffects run AFTER child useEffects. The demo sets the array, then App immediately nulls it. Result: zero callouts on screen.

Both happened during iteration. The fix below avoids both.

## The pattern (correct version)

### App side

```jsx
const App = () => {
  const [active, setActive] = React.useState(...);
  // demos push their per-step callouts here; null = use static view.callouts
  const [dynamicCallouts, setDynamicCallouts] = React.useState(null);
  // NO reset effect on [active] — demo's cleanup-on-unmount handles it

  const view = VIEWS[active];
  const Render = view.render;
  const effectiveCallouts = dynamicCallouts !== null ? dynamicCallouts : view.callouts;

  return (
    <div>
      <Tabs active={active} onSelect={setActive} />
      <div ref={stageRef} ...>
        <div className="absolute inset-0" style={{transform: `scale(${scale})`, transformOrigin: "top left"}}>
          {/* Static views ignore the prop; demos consume it */}
          <Render setCallouts={setDynamicCallouts} />
        </div>
        <Callouts hostRef={stageRef} scale={scale} callouts={effectiveCallouts} />
      </div>
    </div>
  );
};
```

### Demo side

```jsx
// Top-level array per demo — clear, easy to scan, matches step indices
const D1_STEP_CALLOUTS = [
  // step 0
  [
    { id: "d1s0-banner", side: "top", anchor: "top",
      label: "Inbound banner · 8s auto-accept countdown" },
  ],
  // step 1
  [
    { id: "d1s1-callpanel", side: "right", anchor: "right",
      label: "Live call panel · transcript + controls" },
    { id: "d1s1-end", side: "bottom", anchor: "bottom",
      label: "End → opens the wrap-up modal" },
  ],
  // step 2 — wrap-up modal
  [
    { id: "a-wrapmodal", side: "right", anchor: "right",
      label: "Wrap-up · same modal as the canonical S7 view" },
    { id: "d1s2-save", side: "bottom", anchor: "bottom",
      label: "Save & next loops back to home" },
  ],
  // step 3 — toast
  [
    { id: "d1s3-toast", side: "top", anchor: "top",
      label: "Demo complete · ↻ Restart to replay" },
  ],
];

const DemoInbound = ({setCallouts}) => {
  const [step, setStep] = React.useState(0);
  const restart = () => setStep(0);

  // Push the active step's callouts up. Cleanup-on-unmount nulls them so
  // switching to another view doesn't leak stale callouts.
  React.useEffect(() => {
    if (setCallouts) setCallouts(D1_STEP_CALLOUTS[step] || []);
    return () => { if (setCallouts) setCallouts(null); };
  }, [step, setCallouts]);

  if (step === 0) return <DemoStage step={0} total={4} label="Banner · awaiting accept" restart={restart}>
    <InboundBanner anchorId="d1s0-banner" onAccept={() => setStep(1)} />
    {/* dimmed shell underneath */}
  </DemoStage>;

  if (step === 1) return <DemoStage step={1} total={4} label="Active call · 02:14" restart={restart}>
    {/* call panel with anchorId="d1s1-callpanel"; End button with anchorId="d1s1-end" */}
  </DemoStage>;

  // ...
};
```

The cleanup function `return () => setCallouts(null)` is what makes view switches clean. Order on view change:
1. Old demo unmounts → cleanup runs → `setDynamicCallouts(null)`
2. New view mounts
3. If new view is another demo: its useEffect fires → pushes its step-0 callouts
4. If new view is static: nothing pushes; `dynamicCallouts` stays null; App falls back to `view.callouts`

No race, no stale callouts.

## DemoStage wrapper

The demo's outer wrapper that adds the step-indicator strip at the top of the canvas:

```jsx
const DemoStage = ({step, total, label, restart, children}) => (
  <div className="absolute top-0 left-0 origin-top-left rounded-md border border-border overflow-hidden bg-white" style={{width: 1920, height: 1080}}>
    {/* step indicator strip — 32px tall, sits above the wireframe content */}
    <div className="flex items-center gap-3 h-8 px-5 bg-amber-50/60 border-b border-amber-200 text-[12px]">
      <span className="font-semibold text-amber-800 uppercase tracking-[0.06em]">Step {step + 1} of {total}</span>
      <span className="text-zinc-500">·</span>
      <span className="text-zinc-700">{label}</span>
      <button onClick={restart} className="ml-auto inline-flex items-center gap-1 text-amber-700 hover:underline">
        <I name="rotate-ccw" className="w-3 h-3" /> Restart
      </button>
    </div>
    {/* content gets 1080 - 32 = 1048 px of height */}
    {children}
  </div>
);
```

When the children include a Topbar (h-16 = 64px) and a flex shell, give the inner flex `style={{height: 1080 - 32 - 64}}` to account for both the step strip and the topbar.

## Reusable demo sub-components

Don't refactor static state components to accept a million props. Keep the static path simple. For demos, build small `Demo*` variants that accept the action callbacks and anchor IDs:

- `DemoInboundBanner({onAccept, anchorId})` — green slide-down with a clickable Accept
- `DemoVoiceCenter({onEnd, direction, customerName, caseId, title, elapsed, anchorIds})` — call panel with End button wired up
- `DemoOutboundRingingCenter({onConnect, customerName, caseId, anchorId})` — pulsing ringing card
- `DemoRightRailWithCall({onCall, customer, phone, anchorIds})` — the right rail with a Call button
- `DemoCompleteToast({restart, anchorId})` — top-right toast with restart link

For the wrap-up modal: **don't fork**. The S7 `WrapUpModal` should accept `onSave`, `onHome`, `headline`, `subline`, `saveAnchorId` props (all optional). S7 calls it with no props (existing behavior); demos pass their own. One source of truth — what supervisors see in S7 is what agents see at the end of every demo.

## Anchor ID convention

For demo anchors, use `d{N}s{step}-{element}` — e.g. `d1s2-save`, `d2s0-call`. This makes IDs unique across all 14+ views and easy to grep.

For the wrap-up modal, demos REUSE the canonical S7 anchors (`a-wrapmodal`, `a-summary`, `a-axes`, `a-merge`, `a-status`) plus their own save-button anchor. The modal renders only one at a time; no ID conflict.

## Inject-active-row pattern

When a demo accepts a new inbound (call, chat, email), the inbox should reflect the new case. The shared `LeftCaseList` accepts an `injectActive` prop:

```jsx
<LeftCaseList
  activeCaseId="CS-4861"
  injectActive={{id: "CS-4861", name: "Chan · live call", channel: "phone"}}
/>
```

The injected row prepends to the Open section, the Open count bumps by 1, and the row gets the `active` highlight when `activeCaseId` matches. The existing rows below stay (modeling that the customer might have OTHER cases too — e.g. a separate email).

This avoids the trap of "the highlighted case in the inbox is the wrong channel" — accepting a phone call but highlighting an existing email row.

## Step indicator UX

A small but important touch: the step indicator strip sits at the very top of the canvas (above the topbar), in a faint amber band, with the format `STEP N OF M · {label} · ↻ Restart`. It says enough to orient a viewer who jumped in mid-flow without reading the surrounding context. Don't let `{label}` exceed ~30 characters or it pushes Restart off-screen at smaller scales.
