---
name: howto-filmstrips
description: >-
  Capture a real product UI how-to as a horizontal filmstrip on a CreatorNotes
  canvas: live CSS highlight on the click target, soft radial spotlight, then
  screenshot → upload → place. Use when the user wants a screenshot storyboard,
  click-path how-to, JTBD walkthrough, multi-path UI guide, or "show me how to
  do X in the app" with accurate click markers (not fat-marker sketches).
---

# How-to filmstrips (live UI capture on a canvas)

Turn a real product flow into a left-to-right filmstrip of screenshots on a CreatorNotes canvas. Each frame shows the full UI with a clear click (or type) target: **crimson highlight on the real element box** + **soft radial spotlight** dimming the rest.

This is the **pixel-accurate sibling** of `fat-marker-flows`. Use fat-marker for proposed / Shape Up sketches; use this skill for “how do I do this in the live product?”

---

## Design lock (do not invent a new look)

| Element | Spec |
|---------|------|
| Highlight colour | Classy crimson `#C43036` (outline, soft wash, CLICK/TYPE pill) |
| Spotlight | Soft **radial** fade, max opacity **~40%** (up to **~55%** on dark forms), long fade (`fade_mult ≈ 5.5`) |
| Done frame | Green `#2F9E6A` outline + `DONE` pill; lighter spotlight (~30%) |
| Captions | Canvas **richtext** under the image (same node), not Notes |
| Titles | Middle dot `·` as separator; **no em dashes** |
| Layout | One path = one horizontal row; multiple paths = stacked sections |
| Language | Match the product UI; prefer UK English in captions unless the product is US-only |

**Do not** use orange bullseyes, yellow highlighters, cyan brackets, or hard rectangular `box-shadow: 0 0 0 9999px` spotlights. The hard box-shadow gets clipped by parent `overflow` and often never appears in the PNG.

---

## When to use / not use

**Use when:** documenting a live click path, comparing entry points (Path A/B/C), workshop how-tos, JTBD storyboards from a real app.

**Do not use when:** the UI does not exist yet (use `fat-marker-flows`), or the user wants a polished Figma mock (use a design tool).

---

## Prerequisites

1. Browser automation available (Cursor browser MCP / CDP `Runtime.evaluate` + screenshot).
2. `cnotes` authenticated with a writable workspace (`cnotes auth status`, `cnotes workspace current`).
3. This skill folder installed (`cnotes skills install howto-filmstrips`) so scripts resolve under the agent skills dir.

---

## Step 0 — Scope the paths

1. Name the job (e.g. “Create a new contact”).
2. List each **entry path** separately (Path A, Path B…). One path = one row.
3. For each path, list 4–7 frames: each frame is **one action** (click or type) or a final Done state.
4. Write captions before capture: `N · Click …` / `N · Type …` / `N · Done · …`.

---

## Step 1 — Inject highlight helpers (before any click)

Read `scripts/inject-highlight.js` and evaluate its **entire file contents** in the page via CDP `Runtime.evaluate` (or equivalent). That installs:

- `window.__cnHowto.highlight(el, { label: 'CLICK'|'TYPE'|'DONE' })`
- `window.__cnHowto.clear()`
- `window.__cnHowto.findExact(text, opts)` — visible, sized matches; prefer menu-row / control size

The injector uses a **fixed full-viewport radial overlay** (not element `box-shadow`), so the spotlight survives screenshot and parent clipping.

**Calibrate visually:** after first inject, highlight one control and screenshot. Check that the crimson outline hugs the element box (not an inner span), the CLICK pill sits just above it and is legible, and the spotlight is a soft radial fade centred on the control — never a hard-edged rectangle. Adjust opacity only if the product chrome is unusually light or dark.

---

## Step 2 — Capture loop (per frame)

For each click/type frame:

1. **Find** the target with `__cnHowto.findExact` (exact label text) or a tight selector. Prefer the **largest visible** control that matches (menu row over inner text span).
2. **Highlight** with the right label (`CLICK` / `TYPE`).
3. **Screenshot** the viewport → save as `A1.png`, `A2.png`, …
4. **Clear** highlight.
5. **Perform** the action (click / type), wait for UI settle.
6. Repeat.

For the **Done** frame: land on the success state, highlight the result (e.g. contact name) with `{ label: 'DONE' }`, screenshot, clear.

### Accuracy rules

- Capture highlights **live** from element `getBoundingClientRect`. Do not guess pixel rings on old PNGs.
- If a frame teaches picking an option that is already selected, switch to another option first, then highlight the target and shoot — so the frame shows the switch, not a no-op.
- Retina / DPR: screenshots are often 2× CSS pixels. The injector uses CSS pixels for the overlay; if you fall back to `soft-spotlight.py`, pass `--dpr` (usually `2`).

### Fallback if the in-page spotlight is weak

On very dark UIs the radial overlay can look faint. After capture, run:

```bash
python3 "<skillDir>/scripts/soft-spotlight.py" \
  --in A3.png --out A3.png \
  --cx 276 --cy 219 --dpr 2 \
  --opacity 0.55
```

`--cx/--cy` are **CSS-pixel** centres from `getBoundingClientRect` (the script multiplies by `--dpr`).

---

## Step 3 — Upload and place on the canvas

```bash
WS=<workspaceId>
CANVAS=<canvasId>
OUT=/tmp/howto-filmstrip
mkdir -p "$OUT"

cnotes operations begin -w "$WS" --prompt "How-to filmstrip: <job>" --json >/dev/null

# Upload in frame order; collect storageIds
> "$OUT/ids.txt"
for f in A1 A2 A3 A4 A5 A6; do
  sid=$(cnotes files upload "$OUT/$f.png" -w "$WS" --json \
    | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('storageId') or d.get('id') or '')")
  echo "$sid" >> "$OUT/ids.txt"
done
```

Build a place spec: header + one **section per path**, each with a **grid** of richtext cells (image markdown + caption). Prefer `columns: N` matching frame count (e.g. 6). Use `size: "medium"` for frames.

Image URL form inside richtext:

```markdown
![Short alt](/api/images/convex/<storageId>)

1 · Click Create new +
```

Clear existing storyboard nodes if rebuilding the same canvas:

```bash
cnotes canvas get "$CANVAS" -w "$WS" --json | python3 -c "
import sys,json
d=json.load(sys.stdin)
ids=[n['id'] for n in (d.get('allNodes') or [])]
for s in d.get('sectionNodes') or []:
  if s['id'] not in ids: ids.append(s['id'])
open('/tmp/howto-remove.json','w').write(json.dumps(ids))
"
cnotes canvas agent-run wrap --canvas "$CANVAS" --prompt "Clear before how-to rebuild" -- \
  cnotes canvas bulk-remove "$CANVAS" --nodes "$(cat /tmp/howto-remove.json)" -w "$WS" --json
```

Place:

```bash
cnotes canvas agent-run wrap --canvas "$CANVAS" --prompt "Place how-to filmstrip" -- \
  cnotes canvas place "$CANVAS" --spec "$OUT/unified.json" -w "$WS" --json
cnotes operations end -w "$WS" --json
```

### Multi-path layout sketch

```json
{
  "root": {
    "kind": "stack",
    "axis": "vertical",
    "gap": "spacious",
    "align": "start",
    "items": [
      { "kind": "item", "type": "richtext", "size": "large",
        "content": "# How to …\n\nSame job, several entry points. Read each row left to right." },
      {
        "kind": "section",
        "name": "Path A · …",
        "child": {
          "kind": "stack",
          "axis": "vertical",
          "gap": "tight",
          "align": "start",
          "items": [
            { "kind": "item", "type": "richtext", "size": "small",
              "content": "**Path A** · …" },
            { "kind": "grid", "columns": 6, "gap": "medium", "items": [/* frames */] }
          ]
        }
      }
    ]
  }
}
```

---

## Gotchas (learned the hard way)

- **`box-shadow: 9999px` spotlights get clipped** and vanish from screenshots. Always use the fixed radial overlay from `inject-highlight.js`.
- **Wrong element:** loose regexes match sibling labels (`/save/i` also matches “Save and close”). Prefer exact text + min width/height + prefer larger hit targets.
- **CLICK pill vs outline mismatch:** place the pill from the highlighted element’s box after layout; re-highlight after fixing helpers if the first shot drifted.
- **Dark forms:** bump spotlight opacity toward 0.55 so the fade reads.
- **DPR:** post-process centres must be CSS × devicePixelRatio.
- **`place` appends below existing content** unless you bulk-remove first when rebuilding.
- **Verify images by node count / positions**, not by grepping `![` in stored content (TipTap converts markdown images).
- **Do not commit secrets** from the product under test; use throwaway trial data in screenshots.

---

## Pairing with fat-marker-flows

| Skill | Fidelity | Source |
|-------|----------|--------|
| `fat-marker-flows` | Rough Shape Up sketches | Image model |
| `howto-filmstrips` | Real product UI | Browser capture + CSS |

Same canvas can hold a fat-marker “proposed” row and a how-to “as shipped” row — keep the visual language of each skill intact so readers never confuse sketch with screenshot.
