# 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: ` with a link to `//projects//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 `` as an overlay that wraps the chat thread when `promoteState !== "idle"`, render `` 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); // ... ``` PersonalAssistant.tsx renders the transition overlay: ```tsx {promote.state.kind !== "idle" && promote.state.kind !== "done" && ( )} {promote.state.kind === "done" && (
→ Project: {promote.state.projectName}
)} ``` ### 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: ` 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