# Nexus Phase 12 — Promote-to-Project Transition

> **For agentic workers:** Use `superpowers:test-driven-development` per component. Commit atomically per logical unit.

**Goal:** Replace the current synchronous `assistantHandoff()` call on the ActionStrip's "Promote to project" button with the signature animated transition from `docs/specs/2026-04-11-nexus-layout-overhaul.md` §5.6 — the 700ms compress-and-rise where the chat thread compresses to a 30% ribbon at the top while the brainstormer panel rises into the bottom 70%, with an inset shadow ripple along the boundary.

**Source of truth:** spec §5.6 (promote-to-project transition) and §5.7 (origin chat preservation). Phase 9 components under `ui/src/components/assistant/` are the consumption context.

**Branch:** `nexus/design-system-migration`. Commit directly.

---

## Ownership boundaries

**You may create or modify ONLY:**

| Path | Action |
|---|---|
| `ui/src/components/assistant/PromoteTransition.tsx` | Create — the animation-owning component |
| `ui/src/components/assistant/PromoteTransition.test.tsx` | Create |
| `ui/src/components/assistant/BrainstormerPanel.tsx` | Create — the rising brainstormer form (goal, acceptance, gates, agents) |
| `ui/src/components/assistant/BrainstormerPanel.test.tsx` | Create |
| `ui/src/components/assistant/ActionStrip.tsx` | Modify — replace synchronous handoff with a `promoteState` flip that opens the PromoteTransition |
| `ui/src/pages/PersonalAssistant.tsx` | Modify minimally — render the PromoteTransition overlay when active |
| `ui/src/hooks/usePromoteToProject.ts` | Create — state machine + create-project mutation |
| `ui/src/hooks/usePromoteToProject.test.ts` | Create |

**You MUST NOT touch:**

- Any other `ui/src/components/assistant/**` file besides the two listed above
- `ui/src/components/frame/**`, `ui/src/components/Layout.tsx`
- `ui/src/pages/Projects.tsx`, `ui/src/pages/ProjectDetail.tsx`, `ui/src/components/projects/**`
- `ui/src/pages/ContentStudio.tsx`, `ui/src/pages/StudioWorkshopDetail.tsx`, `ui/src/components/studio/**`
- `ui/src/App.tsx`
- Any backend code

---

## Scope (strictly)

**In Phase 12:**

1. **Replace the existing synchronous promote handler** on `ActionStrip`'s "Promote to project" button with a state-machine-backed handler that transitions:
   - `idle` → (click) → `prompting` (the transition animation running, brainstormer rising)
   - `prompting` → (user confirms) → `creating` (API call to create project)
   - `creating` → `done` (project created, banner appears linking to new project, transition collapses, chat restores)
   - Any error → `error` (transition collapses, error toast, state resets to `idle`)
2. **Animate the transition** per spec §5.6:
   - 0–200ms: chat thread height compresses from 100% to 30% via CSS transform/grid or max-height + cubic-bezier(0.22, 1, 0.36, 1). Inset shadow (DESIGN.md Level 4) fades in along the bottom edge of the ribbon.
   - 200–500ms: BrainstormerPanel slides up from the bottom into the lower 70%
   - 500–700ms: small `SOURCE CONVERSATION` uppercase 1.4px-tracked silver label fades in above the compressed thread
3. **BrainstormerPanel form** — spec §5.6:
   - Goal (textarea)
   - Acceptance criteria (textarea, one per line)
   - Gates (list of checkboxes with default gate suggestions)
   - Agents (PM auto-assigned; Engineer template picker)
   - `[ Create project ]` volt-outline submit button at the bottom
   - Cancel button (returns to `idle` state)
4. **On successful create:**
   - Animate the collapse: brainstormer slides down, chat restores to full height (reverse of steps 2.1–2.2 but faster, ~300ms)
   - A persistent banner at the top of the chat reads `→ Project: <PROJECT-NAME>` with a link to `/<companyPrefix>/projects/<slug>/overview`
   - The user stays in Assistant — does NOT auto-navigate
5. **Implementation must be CSS-first, not a motion library.** Use CSS keyframes + transitions, not `motion/react`. Reasoning: one-off transition, no need for a new dependency. If you genuinely can't hand-roll the timing, report BLOCKED and the controller will reconsider adding Motion.

**NOT in Phase 12:**

- Server-side brainstormer changes. Reuse existing create-project endpoint.
- Changes to the project create flow semantics.
- Any changes to the Projects list or Project Detail pages — they just receive the new project normally.
- Mobile full-screen takeover for the transition — Phase 15 handles mobile variant.
- Reduced-motion fallback more elaborate than `prefers-reduced-motion: no animation`.

---

## File plan

| File | Responsibility | Est. lines |
|---|---|---|
| `ui/src/hooks/usePromoteToProject.ts` | State machine: `{state, startPrompting, confirm, cancel, error}`. `confirm(form)` calls `projectsApi.create` and transitions through states. | ~120 |
| `ui/src/hooks/usePromoteToProject.test.ts` | Tests each state transition, mocks `projectsApi.create` | ~140 |
| `ui/src/components/assistant/BrainstormerPanel.tsx` | The form that appears in the bottom 70% during `prompting`. Controlled form, `onConfirm(payload)` → calls hook. | ~180 |
| `ui/src/components/assistant/BrainstormerPanel.test.tsx` | Tests: renders fields, validation, submit payload | ~160 |
| `ui/src/components/assistant/PromoteTransition.tsx` | The animation container. Props: `state`, `children` (the chat thread ribbon), `panelChildren` (BrainstormerPanel). CSS-driven. | ~150 |
| `ui/src/components/assistant/PromoteTransition.test.tsx` | Tests: renders in idle/prompting/creating/done states, aria-live polite for status | ~140 |

Modifications:
- `ui/src/components/assistant/ActionStrip.tsx`: swap sync `onPromote` for `usePromoteToProject().startPrompting`, pass the hook's state down
- `ui/src/pages/PersonalAssistant.tsx`: mount `<PromoteTransition>` as an overlay that wraps the chat thread when `promoteState !== "idle"`, render `<BrainstormerPanel>` inside it

---

## Implementation notes

### State machine shape

```ts
type PromoteState =
  | { kind: "idle" }
  | { kind: "prompting"; conversationId: string }
  | { kind: "creating"; conversationId: string; payload: BrainstormerPayload }
  | { kind: "done"; projectSlug: string; projectName: string }
  | { kind: "error"; message: string };

interface BrainstormerPayload {
  goal: string;
  acceptanceCriteria: string[];
  gates: string[];
  engineerTemplateId?: string;
}
```

### Animation — target keyframes

```css
/* Spec §5.6 — 700ms total. Prefers-reduced-motion: instant swap. */

.promote-chat-ribbon {
  /* idle → prompting: collapses from 100% to 30% over 200ms */
  transition: max-height 200ms cubic-bezier(0.22, 1, 0.36, 1);
  max-height: 100%;
  overflow: hidden;
}
.promote-chat-ribbon[data-state="prompting"],
.promote-chat-ribbon[data-state="creating"] {
  max-height: 30vh;
  box-shadow: inset 0 -16px 24px -12px rgba(0, 0, 0, 0.55);
}

.promote-panel {
  /* 200ms delay, 300ms rise */
  transition: transform 300ms cubic-bezier(0.22, 1, 0.36, 1),
              opacity 300ms ease-out;
  transform: translateY(100%);
  opacity: 0;
}
.promote-panel[data-state="prompting"],
.promote-panel[data-state="creating"] {
  transform: translateY(0);
  opacity: 1;
  transition-delay: 200ms;
}

.source-conversation-label {
  /* 500ms delay, 200ms fade */
  transition: opacity 200ms ease-out;
  opacity: 0;
}
.source-conversation-label[data-state="prompting"],
.source-conversation-label[data-state="creating"] {
  opacity: 1;
  transition-delay: 500ms;
}

@media (prefers-reduced-motion: reduce) {
  .promote-chat-ribbon,
  .promote-panel,
  .source-conversation-label {
    transition: none !important;
  }
}
```

Inline these via Tailwind arbitrary values where possible. If keyframes need to live in a global stylesheet, add them to `ui/src/index.css` under a clearly-scoped block — but prefer inline Tailwind.

### Integration with Phase 9's ActionStrip

Phase 9 wired `onPromote` to call `assistantHandoff()` synchronously. Phase 12 replaces that:

```tsx
// ActionStrip.tsx — change
const promote = usePromoteToProject(activeConversationId);
// ...
<button onClick={promote.startPrompting} disabled={!canPromote || promote.state.kind !== "idle"}>
  ⊕ Promote to project
</button>
```

PersonalAssistant.tsx renders the transition overlay:

```tsx
{promote.state.kind !== "idle" && promote.state.kind !== "done" && (
  <PromoteTransition state={promote.state.kind}>
    <BrainstormerPanel
      conversationId={promote.state.conversationId}
      onConfirm={promote.confirm}
      onCancel={promote.cancel}
      isCreating={promote.state.kind === "creating"}
    />
  </PromoteTransition>
)}
{promote.state.kind === "done" && (
  <div className="border-l-2 border-primary bg-card p-3 mb-4">
    → Project: <Link to={`.../${promote.state.projectSlug}/overview`}>{promote.state.projectName}</Link>
  </div>
)}
```

### Accessibility

- `aria-live="polite"` on the transition container so screen readers announce state changes
- The BrainstormerPanel form fields have labels
- ESC while in `prompting` or `creating` triggers `promote.cancel()`
- Focus traps inside the BrainstormerPanel while active

---

## Acceptance criteria

1. Clicking `⊕ Promote to project` on an active Assistant conversation triggers the 700ms animation per spec §5.6
2. The chat thread compresses to 30%, brainstormer rises into 70%, source-conversation label fades in — in that order
3. The BrainstormerPanel accepts Goal / Acceptance Criteria / Gates / Engineer Template, validates the Goal field as non-empty, submits via the existing project-create endpoint
4. On success: transition collapses, chat restores, a `→ Project: <NAME>` banner appears at the top of the chat, user stays in Assistant
5. On error: transition collapses, error toast shown, chat restores, promote button becomes clickable again
6. ESC cancels the prompting state and restores chat
7. `prefers-reduced-motion: reduce` disables the transition — brainstormer snaps in and out instead of animating
8. Tests pass: `npx vitest run src/components/assistant/PromoteTransition.test.tsx src/components/assistant/BrainstormerPanel.test.tsx src/hooks/usePromoteToProject.test.ts`
9. Typecheck clean on Phase 12 files
10. No file outside declared ownership modified

---

## Report format

- Status
- Commit SHAs
- Files created / modified
- Tests added / passing
- Typecheck result
- Animation approach (CSS-only vs keyframes-file)
- Concerns, deviations, self-review findings
