# Plan — Mascot-driven UX overhaul

**Status:** planning; nothing implemented. Waiting for approval before touching code.

This is a full brand + interaction refresh, not a bag of small tweaks. The core insight: dddk currently has no mascot presence — it's a technically strong SDK with a generic look. The duck is only in the logo and nowhere in the actual product surface. This plan puts the duck **everywhere the user's attention is already going** (cursor, thinking, selection, greeting, proactive prompts, palette, inline AI) so every touchpoint feels like the same product.

All animation assets go through **gpt-image-2 low-quality + image-to-image** via `promo-cli`. Character consistency is solved by seeding each pose off a master reference, not by fighting the model at prompt-time.

---

## Meta · Design system decisions (I'm making these — push back if wrong)

### Locked decisions

1. **Character consistency source**: **gpt-image-2 image-to-image** (not hand-drawn SVG). Confirmed you're right that i2i solves the drift; SVG hand-drawn quality isn't good enough for the mascot moments. SVG only used where currentColor / infinite-scale really matters (FAB icon at very small sizes, cursor tip geometry).
2. **Master reference**: I'll produce **one canonical "master duck"** image first — 512 × 512 3/4 side view, transparent bg, brand palette. Every pose after that (thinking, waving, swimming, proactive) is i2i off this master. Guarantees consistency.
3. **Palette footer "powered by dotdotduck"**: **palette footer only**. Not on FAB (would clutter), not on inline AI (host's editing surface). Palette is a lookup UI — a subtle attribution row is expected there.
4. **First-visit greeting language re-trigger**: **no**. One lifetime greeting per visitor. Language switch mid-session re-fires ONCE only if the visitor hasn't seen the greeting in the new locale yet. Repeat visits, whatever language: nothing.
5. **Scroll animation** (you said you never saw it): the current `scroll_to` cursor-glyph swap is too subtle. Fix at Item 15 — turn the cursor INTO a mini swimming duck during scroll, so it's obvious the agent is moving.

### Assumptions I'll assume unless you object

- All animations respect `prefers-reduced-motion` — down to a still frame or a fade.
- All assets ship both single sprite sheet + WebP + APNG fallback for older Safari.
- Dark / light mode adapts: transparent PNG assets over `--dddk-bg-*` tokens, duck's outline / eyes remain visible in both (I'll design the master with a subtle dark ring so it reads on white AND on `#0f0f14`).

---

## Asset production pipeline (one-time setup, then batch)

`promo-cli` already has the gpt-image-2 wiring + API key. Sequence:

### Phase 0 — master reference

1. **Prompt**: `A cute cartoon duckling, side 3/4 view, big friendly eye, small orange bill, plump body, transparent background, thick clean line-art, warm minimal palette (soft yellow body, orange bill, dark grey outline), centred, 512×512`
2. Generate 4 candidates, pick the best.
3. Save as `assets/mascot/duck-master.png` — this is the single source of character.
4. Cost: 4 × $0.006 = $0.024.

### Phase 1 — pose set (i2i off master)

For each pose we need a still: `edit` API call with master as `image`, prompt = pose description. Poses needed for every downstream animation:

| Pose | Use case |
|---|---|
| Idle A (looking straight, calm) | FAB idle, cursor face |
| Idle B (looking left) | Cursor face when moving left |
| Idle C (looking right) | Cursor face when moving right |
| Waving hi (wing raised) | First-visit greeting |
| Bouncing up (mid-hop) | Thinking dots frame 2 |
| Landed (feet down) | Thinking dots frame 1 |
| Swimming stroke A (wings back) | Dwell top border, proactive swim-out |
| Swimming stroke B (wings mid) | Same |
| Swimming stroke C (wings forward) | Same |
| Thinking (chin on wing, "hmm") | Dwell bottom-right thinking duck |
| Rolling / tumbling | Scroll animation |
| Excited (both wings up, mouth open) | Proactive delivery landed |

**Batch cost**: 12 poses × 3 attempts avg = 36 × $0.006 = ~$0.22.

### Phase 2 — animation frames (i2i off each pose)

For each looping animation (thinking, swimming, waving, scroll-tumble) — generate 6–10 in-between frames off the endpoint poses.

Total additional frames: ~50. At $0.006 each × 2 attempts avg = ~$0.60.

### Phase 3 — post-processing

- Background removal (rembg or similar; automatable in a Node script) — some gpt-image-2 outputs still have faint bg.
- Trim, centre, unify canvas size.
- Assemble sprite sheets (one per animation) — one PNG per animation, N frames wide.
- Emit CSS with `background-position: -N*W` and `steps()` timing.

**Total production cost**: ~$0.85 for all assets. **Total time**: ~4–6 hours (prompting iteration + post-processing + sprite assembly), possibly a bit more.

---

## The features

### 1 · FAB visible on every viewport by default

**Change**: default `fab: 'always'` (mobile + desktop). Config:
```ts
fab?:
  | false                                         // fully off
  | 'auto'                                        // legacy: mobile only
  | 'always'                                      // new default
  | { visible: 'always' | 'mobile' | 'never';
      anchor?: HTMLElement | string;              // host-owned button swap
      icon?: 'default' | 'monochrome' | HTMLElement;
      size?: number;                              // 48 mobile / 40 desktop
      position?: 'br' | 'bl' | 'tr' | 'tl';
      offset?: { x?: number; y?: number };
    };
```

**Sizes**: 48 mobile, 40 desktop (touch minimum vs less intrusion).
**Position**: bottom-right default; `bl / tr / tl` for hosts with their own bottom-right widget.
**Anchor swap**: unchanged from today.

### 2 · Palette footer: "powered by 🦆 dotdotduck ↗"

Footer row, rightmost:
```
↑↓ 上下移動   ⏎ 選取   esc 關閉                       🦆 powered by dotdotduck ↗
```

- 11 px, `--dddk-text-muted` colour, hover to `--dddk-text-primary`.
- Duck icon: same master silhouette in SVG form (16 × 16).
- Link: `https://dddk.perhapxin.com/` in new tab.
- Same treatment on `mountInline` variant.
- **Default on**; opt out with `palette: { poweredBy: false }`.
- Removed automatically when a commercial-license key is detected in `DotDotDuckConfig.license`.

### 3 · FAB icon — the master duck head

- Static PNG (from Phase 1, Idle A) scaled to FAB size.
- Sits inside a circular button with brand accent bg.
- Dark mode: duck asset over darker button bg; the built-in outline stays legible.
- Legacy `<circle>` "waiting for input" SVG stays for `prefers-reduced-motion`.

### 4 · Thinking indicator = three ducks bouncing

- Three copies of the master duck, staggered bounce animation.
- 8-frame sprite sheet per duck × 3 = 24 frames total, but we can reuse:
  - Frame 1 (Landed) + Frame 2 (Bouncing up) alternating with 3 offsets
- Sprite sheet: one PNG, three columns × 8 frames.
- CSS `background-position` cycling with `steps(8)`.
- Replaces the current `● ● ●` in `Subtitle.showIndicator('thinking')`, `'processing'`, `'executing'`.

### 5 · FAB animation set

CSS-only (no assets) + optional mascot reactions:

| State | Animation |
|---|---|
| **Idle** | Slow 2-px vertical float, 3 s loop |
| **Hover (desktop)** | Scale 1.0 → 1.05 + shadow deepen, 150 ms |
| **Press** | Scale 0.92 bounce, 90 ms + 90 ms |
| **Voice hold** | Existing ripple + duck's beak opens (asset swap: Idle A → Excited pose) |
| **Loading (agent running)** | Slow border pulse + duck asset swap to Thinking pose |
| **Success confirm** | 400 ms green check overlay above FAB |

All respect `prefers-reduced-motion` (freeze to Idle A pose).

### 6 · First-visit greeting

`localStorage: dddk:greeted:v1` gates a one-shot sequence:

```
T+0    ─ FAB fades in scale 0.6 → 1.0                    (400 ms)
T+400  ─ FAB grows 1.0 → 1.4                             (300 ms)
T+700  ─ Waving-hi animation × 2 loops                   (1200 ms)
T+700  ─ Speech bubble pops in above FAB, 3s auto-hide   (300 ms in / 3000 ms hold / 400 ms out)
T+4400 ─ FAB shrinks 1.4 → 1.0                           (300 ms)
```

- **Copy** (zh-TW / en):
  - `嗨！我是 dddk，需要幫忙隨時按我`
  - `Hi! I'm dddk — hit me any time.`
- **Click anywhere** during sequence → skip to end.
- **Analytics**: `agent_greeting_shown`, `agent_greeting_dismissed`.
- **Locale re-trigger**: fires ONCE per locale within first session only (uses `localStorage: dddk:greeted:v1:<locale>`). Repeat visits in any locale: no fire.

### 7 · Agent cursor redesign — dart + duck-face ball

Current cursor: minimal SVG arrow. Replace with:

- **Dart tip**: thin arrow (SVG path, `currentColor`), points at target — inherits rotate-toward-target for scroll/glide.
- **Circular ball behind tip**: 24 × 24 disc.
- **Ball fill**: master duck face (Idle A / Idle B / Idle C depending on horizontal movement direction).

**Modes**:
- `pointer` (default): dart + duck facing straight
- `scroll`: dart + duck face swaps to Rolling pose, ball rotates 360° during scroll motion (matches Item 15)
- `reading`: dart + duck face swaps to Thinking pose

**Trail**: when cursor moves > 100 px in one glide, leave a 3-dot ripple trail (opacity 0.3 → 0 over 300 ms). Only for the initial fly-in; anchored idle motion doesn't trail.

**Assets**: 3 duck-face variants (Idle A/B/C) + Rolling + Thinking = 5 static PNGs at 40 × 40.

### 8 · Dwell selection redesign

Currently: outset outline around the element.

**New treatment**:
- **Inner frame** (`box-shadow: inset 0 0 0 2px var(--dddk-accent)`) — sits INSIDE the element edges, not around. Cleaner on tight-fitting UI.
- **Above the selection** (absolutely positioned): 3 swimming ducks looping horizontally right-to-left across the width of the selection. Reuses the swimming stroke A/B/C sprite (3 frames per duck, 3 ducks staggered).
- **Bottom-right corner of selection**: single Thinking-pose duck (small, 32 × 32), gently bobbing.

**Analytics** (existing intent): `selection_pinned` fires; add `mascot_variant: 'swim'` metadata for observability.

**Trade-off**: for very small selections (< 120 px wide) the swimming ducks would look cramped — fall back to just the inner frame + a smaller single-duck-swimming icon top-left.

### 9 · Command palette — more possibilities

Current: filter list, submit. Additions:

**a. Ambient state (empty palette)**: a tiny master duck floats in the top-right of the input area, blinks occasionally, gently bobs. Signals "waiting for input" without being noisy.

**b. Empty results reaction**: no results → duck shrugs, subtitle line reads "找不到，換個字試試看" / "Nothing here — try different words". Reuses the master with a shrug-pose i2i.

**c. Voice-first opener**: option `palette.openWithVoice = true` opens palette with mic already on. Transcript streams into the filter input character-by-character.

**d. Category-based row shapes**: existing `lines` + `image` + new `mediaTop` — a small illustration renders above the row title. Useful for content-heavy hosts (recipes, products, docs).

**e. Split preview panel** (opt-in): when `palette.detailPreview = true`, hovering a row lights up a right-side detail pane (renders `PaletteItem.detail(): HTMLElement`). Book covers, order previews, doc snippet previews.

**f. Result-flying-in animation**: matched rows enter with a subtle stagger (rows 1 → N cascade, 40 ms each). Duck in top-right nods.

Everything (a–f) is opt-in per-host except (a) which is a default subtle touch.

### 10 · Inline AI upgrade

Current: floating menu → diff panel. Additions:

**a. Duck avatar in the menu header**: 16 × 16 master duck, gently blinks. Signal it's an AI menu, not just any dropdown.

**b. Action icon hover reactions**: each action's icon animates when hovered:
- Translate: `A` ↔ `文` swap
- Improve: sparkle burst
- Fix: red squiggle fades to green underline
- Shorten: horizontal squish
- Lengthen: horizontal stretch
- Tone: mask crossfades

**c. Processing state**: the "…" processing line replaced with a mini swimming duck (single-frame idle swim, ~24 × 24) with dots trailing.

**d. Diff panel refinement**:
- Rounder corners (12 px, currently 8-ish).
- Duck avatar in top-left of the diff panel header.
- Word-level diff: struck-through old text fades red → grey; new text fades highlighted-purple → normal (currently just shows both).
- **Streaming cursor**: existing blinking cursor gets a duck-shape variant during stream.

**e. Follow-up chat**: prior turn chips get a small duck bullet marker to make the history strip feel connected to the mascot.

### 11 · Voice hold — long-press Space

Current: subtitle bar shows "Listening…" + ripple around FAB.

**Additions**:
- FAB swap to Excited pose (beak open) — sprite swap on `voice_start`.
- **Waveform inside FAB**: 3 vertical bars, height driven by `AnalyserNode` FFT of the mic stream. Fallback to a 3-bar synthetic wobble when Analyser unavailable.
- Subtitle bar shows a mini swimming duck to the left of "Listening…" text.

### 12 · Subtitle bar refinement

Current: white / dark rounded box.

**Additions**:
- **Duck avatar** on the left (mini Idle A, 20 × 20), swaps to Thinking pose when `type: 'agent'` and the bar is streaming.
- **Wave line** beneath the text: subtle 1-px sine wave, animated (translateX loop), tinted at 15% opacity. Signals "the duck is talking."
- **Entry animation**: fade + 4 px slide-up instead of the current instant show.

### 13 · `ask_user_choice` styling

Current: plain option list.

**Additions**:
- Each choice row: small duck bullet on the left, animated in with a 60 ms stagger.
- Selected choice: duck jumps in place once (Bouncing pose).
- Free-text row (`Other`): mini pencil-holding duck icon.

### 14 · Proactive prompt = duck swims out of the FAB

**This is the delight moment.** Sequence when a proactive prompt fires:

```
T+0    ─ FAB ripples (concentric water rings)              (400 ms)
T+400  ─ Master duck swims OUT of the FAB along a
         curved bezier path toward the subtitle-bar's
         resting position                                  (900 ms)
T+400  ─ Trail of small ripples along the path
T+1300 ─ Duck arrives, speech bubble containing the
         proactive prompt fades in above it                (300 ms)
T+1600 ─ User answers OR dismisses:
         - Accept  → duck does a happy hop + returns
                     swimming back to FAB
         - Reject  → duck deflates slightly + dives
                     back into FAB
         - Timeout → duck sighs, swims back
                                                            (900 ms return)
```

**Analytics**: existing proactive intent events plus new `mascot_swim_shown`, `mascot_swim_completed` for observability of the animation itself.

**Path**: pre-computed bezier from FAB current position to the subtitle anchor. Reduced-motion → straight-line 150 ms fade in with no swimming, no dive.

### 15 · Scroll animation upgrade

The current `scroll_to` swaps the cursor glyph to a mouse-wheel icon — you never noticed because the swap is minimal and the traversal is fast.

**New treatment**:
- On `scroll_to`, cursor swaps to a **Rolling / Tumbling duck pose** (Phase 1 asset).
- Cursor's rotate transform matches scroll direction (down → cursor spins clockwise, up → counterclockwise).
- Trail: 5 small ripples along the scroll path, fade out 400 ms behind.
- On landing: cursor pulses once (arrival beat), duck asset swaps back to Idle A.

**Why this reads**: rolling/tumbling is a universal "movement in progress" metaphor; scroll bar users understand it instantly. Duck-in-scroll-mode is also just charming.

---

## Ship order + estimates

Assume all approved. Grouped into two waves so you can gate-check between them.

### Wave 1 — Foundation (all mascot assets + core surfaces)

1. **Master duck reference** (Phase 0)                                  30 min
2. **Batch pose generation** (Phase 1 — 12 poses)                       1 h
3. **Post-processing + sprite assembly**                                2 h
4. **Item 1** — FAB default everywhere                                  3 h
5. **Item 3** — FAB icon = duck head                                    1 h
6. **Item 4** — Three-ducks thinking indicator                          3 h
7. **Item 7** — Cursor redesign (dart + duck ball)                      5 h
8. **Item 12** — Subtitle bar duck avatar + wave line                   3 h
9. **Item 15** — Scroll = rolling duck                                  2 h

**Wave 1 total: ~20 h + $0.30 in gpt-image-2 usage**.
Ship, live-test on dddk.perhapxin.com, take screenshots, adjust before Wave 2.

### Wave 2 — Delight moments

10. **Item 5** — FAB animation set (swaps in Wave 1 poses)             3 h
11. **Item 6** — First-visit greeting                                   3 h
12. **Item 8** — Dwell selection redesign                              4 h
13. **Item 14** — Proactive swim-out                                    5 h
14. **Item 9** — Palette upgrades (a–c mandatory, d–f opt-in)          6 h
15. **Item 10** — Inline AI upgrade                                     5 h
16. **Item 11** — Voice hold FAB waveform                              2 h
17. **Item 13** — `ask_user_choice` mascot bullets                     2 h
18. **Item 2** — Palette powered-by footer                             2 h

**Wave 2 total: ~32 h + $0.55 in gpt-image-2 usage**.

**Grand total: ~52 h + <$1 image gen**. Realistically that's 2–3 weeks of focused work.

---

## Sign-off checklist

- [ ] Approve the meta locked decisions (character source, master ref, powered-by location, greeting re-trigger, scroll fix approach).
- [ ] Approve Wave 1 → 2 gating (ship Wave 1 first, review live, then Wave 2).
- [ ] Approve total budget (~$1 gpt-image-2, ~52 h dev).
- [ ] Approve OR strike any Item 1–15.
- [ ] Add any Item you want that I didn't cover.

Once you strike anything you don't want and confirm the rest, I'll start Phase 0 (master duck) immediately and post it back for you to bless before batch-generating the pose set.
