# Nexus Phase 10 — Studio Mode Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:test-driven-development`. Commit atomically per logical unit. **Goal:** Refactor ContentStudio from a 7-tab page into an **8-card workshop grid** that folds in the existing ConvertPage as the 8th workshop. Add a freeform prompt input at the bottom of the Studio home that routes to a workshop based on intent. Build a workshop detail layout (two columns: params left, preview right) for when a user selects a workshop. **Source of truth:** `docs/specs/2026-04-11-nexus-layout-overhaul.md` **§6** (Mode 2 — Studio). Read §6.1–§6.5 before starting. DESIGN.md governs visuals. **Branch:** `nexus/design-system-migration`. Commit directly. --- ## Ownership boundaries **You may create or modify ONLY:** | Path | Action | |---|---| | `ui/src/pages/ContentStudio.tsx` | Modify (major rewrite: from tabs to workshop grid) | | `ui/src/pages/StudioWorkshopDetail.tsx` | Create (new routed view for a single workshop) | | `ui/src/components/studio/**` | Create (new subdir for Phase 10 components) | | `ui/src/pages/ConvertPage.tsx` | MAY read and reference, but DO NOT DELETE. Phase 16 handles dead-file deletion. Leave the file in place; it just stops being linked from the rail. | **You MUST NOT touch:** - `ui/src/App.tsx` — routing is controller-owned. If you need new sub-routes for workshops (e.g. `/content-studio/:workshopSlug`), report them in your final report; the controller wires them up after Wave 2. - Existing content generator backends under `server/` or `packages/`. Phase 10 is purely a frontend IA reshape. - Any file outside `ui/src/` except as noted. - `ui/src/components/Layout.tsx`, `ui/src/components/frame/*` — Phase 8 territory, do not touch. - Other phases' owned paths (`ui/src/pages/PersonalAssistant.tsx`, `ui/src/pages/Projects.tsx`, `ui/src/pages/ProjectDetail.tsx`, `ui/src/components/assistant/**`, `ui/src/components/projects/**`). **Existing code you may reuse (read-only):** - The 7 existing content-generator subcomponents that the current `ContentStudio.tsx` references for its tabs. Read `ContentStudio.tsx` first to find them. You'll reuse them as the body content of each workshop's detail view, unchanged. - `ui/src/pages/ConvertPage.tsx` — you'll embed its main content (not the whole page shell) as the body of the Convert workshop detail view. Do not delete the file; the route path `/convert` still works in Phase 10 for backwards compatibility. - `ui/src/components/frame/*` — Phase 8 patterns (test shape, semantic tokens, focus-visible styles, cn helper, slide-overs if you need any). --- ## Scope (strictly) **In Phase 10:** 1. **Studio home rewrite** — replace the 7-tab layout in `ContentStudio.tsx` with a grid of **8 workshop cards**: - `diagrams`, `icons`, `themes`, `wallpapers`, `documents`, `brand-kits`, `social`, `convert` - 3-column grid on `>= 1024px`, 2-column on `>= 640px`, 1-column below - Card: 1px charcoal border, 8px radius, transparent fill on idle, `bg-card` near-black on hover, 24px padding - Title in Inter 700 uppercase 24px, subtitle in silver 14px, Lucide icon in volt top-right 2. **Convert workshop fold-in** — the 8th card routes to the existing ConvertPage body rendered inside a Studio workshop detail shell. The `/convert` top-level route must continue to work (because users might have it bookmarked) but the IconRail's Studio destination is the canonical path forward. 3. **Workshop detail view** at `StudioWorkshopDetail.tsx` — a new routed view that renders one workshop at a time: - Two-column layout on `>= 1024px`: left = params/prompt input (40% width), right = preview (60% width) - Mobile stacks to a single column - Top of page shows `STUDIO / WORKSHOP-NAME` as the page title (the actual mode-breadcrumb is in TopStrip from Phase 8; this is an in-page heading) - Action bar at the bottom of the right column: `Save`, `Export`, `Send to Assistant` 4. **StudioPromptBar** — freeform text input at the bottom of the Studio home. Placeholder: `Or just describe it: "I need a 1920×1080 wallpaper of …"`. Submits to a classifier (§6.5 of the spec) that maps intent to a workshop slug. If routing succeeds, navigate to `/content-studio/{workshopSlug}` with the prompt pre-filled. If routing fails, the prompt falls through to the Assistant as a new conversation message (see §Implementation notes). 5. **Workshop definitions** — a single source-of-truth data structure that both the grid and the detail view consume. **NOT in Phase 10:** - Any backend changes to content generators. - The freeform prompt classifier itself — use a simple regex/keyword matching stub for Phase 10 (see §Implementation notes). A real LLM classifier is out of scope. - Recipe integration (v1.8 planned; not part of Wave 2). - Deleting `ConvertPage.tsx`. Phase 16 cleanup handles dead files. - Editing `App.tsx` routes — report needed routes to the controller. - Mobile polish beyond "grid collapses to 1 column" — Phase 15 handles mobile. --- ## File plan ### Create | File | Responsibility | Est. lines | |---|---|---| | `ui/src/components/studio/workshops.ts` | Single source-of-truth data: array of 8 workshops with `slug`, `title`, `subtitle`, `icon`, `componentKey`. Exported for consumption by both the grid and the detail view. | ~80 | | `ui/src/components/studio/workshops.test.ts` | Tests: all 8 workshops present, slugs unique, icons are valid Lucide imports, each `componentKey` maps to a real component | ~60 | | `ui/src/components/studio/WorkshopCard.tsx` | Single card for the grid. Props: `workshop`, `onClick`. Renders title, subtitle, icon, hover state. | ~70 | | `ui/src/components/studio/WorkshopCard.test.tsx` | Tests: renders workshop data, click fires callback, hover state applies correctly | ~90 | | `ui/src/components/studio/WorkshopGrid.tsx` | Responsive grid of WorkshopCards. Takes `workshops` array and `onSelect` callback. | ~60 | | `ui/src/components/studio/WorkshopGrid.test.tsx` | Tests: renders N cards, onSelect fires with correct workshop slug, responsive class application | ~80 | | `ui/src/components/studio/StudioPromptBar.tsx` | Freeform text input + submit button. Calls `classifyIntent` helper and either navigates or calls `onFallbackToAssistant`. | ~80 | | `ui/src/components/studio/StudioPromptBar.test.tsx` | Tests: renders input, classifier routing, fallback path | ~110 | | `ui/src/components/studio/classifyIntent.ts` | Pure function. Given a prompt string, returns `{ slug: string; prefilledPrompt: string } \| null`. Keyword-matching only in Phase 10. | ~70 | | `ui/src/components/studio/classifyIntent.test.ts` | Parameterized tests: "diagram of X" → diagrams, "wallpaper of Y" → wallpapers, "convert pdf to docx" → convert, "make me a logo" → brand-kits, "random chat" → null | ~80 | | `ui/src/pages/StudioWorkshopDetail.tsx` | Routed view. Reads `:workshopSlug` from params, looks up in workshops data, renders two-column layout with the workshop's existing generator component on the right and a params/prompt input on the left. Action bar at the bottom: Save / Export / Send to Assistant. | ~180 | | `ui/src/pages/StudioWorkshopDetail.test.tsx` | Tests: renders workshop by slug, 404 fallback for unknown slug, action bar renders | ~120 | ### Modify | File | Change | |---|---| | `ui/src/pages/ContentStudio.tsx` | Replace the 7-tab layout with the new WorkshopGrid + StudioPromptBar. Remove tab state and tab container. Preserve any cross-cutting concerns (e.g. error boundaries, query client usage) that the existing page has. | **Do not create or modify any other files.** --- ## Implementation notes ### Workshop data structure ```ts // ui/src/components/studio/workshops.ts import { Sparkles, Layers, Palette, Image, FileText, Award, Share2, Repeat } from "lucide-react"; import type { ComponentType } from "react"; export type WorkshopSlug = | "diagrams" | "icons" | "themes" | "wallpapers" | "documents" | "brand-kits" | "social" | "convert"; export interface WorkshopDefinition { slug: WorkshopSlug; title: string; // Inter 700 uppercase, e.g. "DIAGRAMS" subtitle: string; // Inter 400 silver, single line icon: ComponentType<{ className?: string }>; // Lucide icon /** * Key for looking up the generator component. The detail view maps * this to an existing component from ContentStudio's current tab * implementations. Phase 10 does NOT rewrite any generator internals. */ componentKey: string; } export const WORKSHOPS: WorkshopDefinition[] = [ { slug: "diagrams", title: "DIAGRAMS", subtitle: "Mermaid → rendered SVG", icon: Sparkles, componentKey: "diagram-renderer" }, { slug: "icons", title: "ICONS", subtitle: "SVG sets from description", icon: Layers, componentKey: "icon-renderer" }, { slug: "themes", title: "THEMES", subtitle: "Color → full palette", icon: Palette, componentKey: "theme-renderer" }, { slug: "wallpapers", title: "WALLPAPERS", subtitle: "Desktop, mobile, banners", icon: Image, componentKey: "wallpaper-renderer" }, { slug: "documents", title: "DOCUMENTS", subtitle: "PDF reports, invoices", icon: FileText, componentKey: "document-renderer" }, { slug: "brand-kits", title: "BRAND KITS", subtitle: "Full brand identity", icon: Award, componentKey: "brand-renderer" }, { slug: "social", title: "SOCIAL", subtitle: "Platform-ready posts", icon: Share2, componentKey: "social-renderer" }, { slug: "convert", title: "CONVERT", subtitle: "File format conversion", icon: Repeat, componentKey: "convert" }, ]; ``` Pick Lucide icons that fit. If a more appropriate icon exists for a slug, use it. The above is a sensible default. ### The `componentKey` → real component mapping Phase 10 does NOT rewrite any generator. For each workshop, the detail view renders the existing generator component as-is. You must read the current `ContentStudio.tsx` to find those components. They'll be something like ``, ``, etc. Build a map: ```ts // In StudioWorkshopDetail.tsx (or a small helper module within studio/) const WORKSHOP_COMPONENTS: Record = { "diagram-renderer": DiagramGeneratorTab, "icon-renderer": IconGeneratorTab, // ... etc. "convert": ConvertPageBody, // see Convert fold-in notes below }; ``` If a generator doesn't currently exist in `ContentStudio.tsx`'s tabs (e.g. if the current ContentStudio has only 6 tabs and we're adding Social as a future extension), render a `` inline-component that shows `"Coming soon"` for that slug. Do NOT stub out a fake generator. ### Convert fold-in `ui/src/pages/ConvertPage.tsx` currently exists and is routed via `/convert`. Phase 10 needs the Convert workshop body rendered inside the Studio workshop detail shell, but the existing `/convert` route MUST continue to work (backwards compat). Strategy: 1. Extract the inner content of `ConvertPage.tsx` into a reusable component `ConvertPageBody`. Do this by **reading** `ConvertPage.tsx` and identifying the JSX that's the actual convert UI (the file uploader, format selector, conversion runner) vs. the page-level shell. 2. If `ConvertPage.tsx` is already structured so the inner content is a single child component, just import and reuse that component — no refactor needed. 3. If `ConvertPage.tsx` is a monolithic component and extracting `ConvertPageBody` requires a real refactor, take the smallest possible subset: wrap the whole `` in the workshop detail and accept that there's a small amount of redundant chrome. Flag as a concern. 4. Leave `ConvertPage.tsx` in place; the `/convert` route keeps rendering it directly. ### StudioPromptBar fallback to Assistant When the classifier returns `null`, the prompt should fall through to the Assistant as a new conversation. The cleanest implementation is: 1. Navigate to `//assistant?prompt=`. 2. The Assistant page (Phase 9's work) reads the `prompt` query param on mount and pre-fills the input. **BUT:** Phase 9's `PersonalAssistant.tsx` rewrite may not include query-param pre-fill logic. Coordinate: add a note to your final report saying "StudioPromptBar fallback assumes Phase 9 will honor `?prompt=` query param on the Assistant route". Phase 9's plan should also have this noted; if it doesn't, the controller will coordinate. For Phase 10's implementation, go ahead and navigate with the query param. If Phase 9 doesn't honor it yet, the worst case is the prompt is dropped and the user lands on an empty assistant — acceptable degradation. ### classifyIntent — Phase 10 rules Simple keyword-based classifier. Pure function, no state, no async. ```ts export function classifyIntent(prompt: string): { slug: WorkshopSlug; prefilledPrompt: string } | null { const lower = prompt.toLowerCase().trim(); if (!lower) return null; // Convert intent — "convert X to Y", "from X to Y", explicit formats if (/\bconvert\b|from\s+\w+\s+to\s+\w+|\bpdf\b|\bdocx?\b|\bmd\b/i.test(lower)) { return { slug: "convert", prefilledPrompt: prompt }; } // Wallpaper intent if (/wallpaper|background\s+image|desktop\s+bg/i.test(lower)) { return { slug: "wallpapers", prefilledPrompt: prompt }; } // Diagram intent if (/diagram|flowchart|sequence|mermaid|architecture/i.test(lower)) { return { slug: "diagrams", prefilledPrompt: prompt }; } // Icon intent if (/\bicon\b|icon\s+set|svg\s+icon/i.test(lower)) { return { slug: "icons", prefilledPrompt: prompt }; } // Theme intent if (/\btheme\b|color\s+palette|palette/i.test(lower)) { return { slug: "themes", prefilledPrompt: prompt }; } // Document intent if (/pdf|document|invoice|report|one-?pager/i.test(lower)) { return { slug: "documents", prefilledPrompt: prompt }; } // Brand intent if (/brand|logo|identity|style\s+guide/i.test(lower)) { return { slug: "brand-kits", prefilledPrompt: prompt }; } // Social intent if (/social|tweet|post|instagram|linkedin|twitter|x\.com/i.test(lower)) { return { slug: "social", prefilledPrompt: prompt }; } return null; // unclassified → fall through to Assistant } ``` Test exhaustively with the parameterized test pattern from Phase 8's `ModeBreadcrumb.test.tsx`. ### ContentStudio.tsx target structure ```tsx export function ContentStudio() { const navigate = useNavigate(); const { selectedCompanyId, selectedCompany } = useCompany(); const prefix = selectedCompany?.issuePrefix ?? ""; const handleSelectWorkshop = (slug: WorkshopSlug) => { navigate(`/${prefix}/content-studio/${slug}`); }; const handleFallbackToAssistant = (prompt: string) => { navigate(`/${prefix}/assistant?prompt=${encodeURIComponent(prompt)}`); }; return (

STUDIO

Eight workshops. Pick one or describe what you need.

navigate(`/${prefix}/content-studio/${slug}?prompt=${encodeURIComponent(prefilled)}`)} onFallbackToAssistant={handleFallbackToAssistant} />
); } ``` Preserve any existing cross-cutting concerns (error boundaries, query client, auth checks) from the current ContentStudio. ### StudioWorkshopDetail routing **You are NOT allowed to edit App.tsx.** You'll need sub-routes for Studio workshops, e.g. `//content-studio/:workshopSlug`. The existing App.tsx currently only has `//content-studio` without a sub-route param. **Workaround for Phase 10:** render StudioWorkshopDetail as a child of ContentStudio, not as a separate route. ContentStudio checks for a `:workshopSlug` path segment via `useLocation()` (not `useParams()`, because the App.tsx routes don't define the param). If there's a slug, render StudioWorkshopDetail; otherwise render the grid. ```tsx // ContentStudio.tsx pseudocode export function ContentStudio() { const location = useLocation(); const match = location.pathname.match(/\/content-studio\/([^/]+)/); const workshopSlug = match?.[1]; if (workshopSlug) { return ; } return ; } ``` This is hacky but correct for Phase 10. Report to the controller: "New App.tsx routes needed: `content-studio/:workshopSlug`". The controller will replace the pathname-matching with a proper route param after Wave 2. --- ## Acceptance criteria Phase 10 is complete when: 1. Loading `/NEX/content-studio` renders an 8-card grid with the new workshop definitions — no tab strip visible anywhere on the page. 2. Each card navigates to `/NEX/content-studio/{slug}` when clicked. 3. Loading `/NEX/content-studio/{slug}` renders the workshop detail view with the existing generator component on the right and a params panel on the left. 4. Loading `/NEX/content-studio/convert` renders the ConvertPage body inside the workshop detail shell. 5. Loading `/NEX/convert` (the legacy route) still works — backwards compat. 6. The StudioPromptBar at the bottom of the grid view accepts text input and routes correctly: - `"diagram of the auth flow"` → navigates to `/NEX/content-studio/diagrams?prompt=...` - `"convert this pdf to markdown"` → navigates to `/NEX/content-studio/convert?prompt=...` - `"random musing"` → navigates to `/NEX/assistant?prompt=...` (fallback) 7. All new components have tests using the Phase 8 manual createRoot + act pattern. 8. `npx vitest run src/components/studio/ src/pages/StudioWorkshopDetail.test.tsx` passes. 9. `npx tsc --noEmit 2>&1 | grep -E "studio/|ContentStudio\.tsx|StudioWorkshopDetail\.tsx"` returns no errors. 10. The `/convert` top-level route still renders the ConvertPage. 11. No file outside the declared ownership is modified. --- ## Commit scheme Suggested atomic commits (one per logical unit): 1. `feat(nexus): add WORKSHOPS data + classifyIntent helper (phase 10)` — workshops.ts, classifyIntent.ts, their tests 2. `feat(nexus): add WorkshopCard and WorkshopGrid components (phase 10)` — cards + grid + tests 3. `feat(nexus): add StudioPromptBar with intent routing (phase 10)` — prompt bar + tests 4. `feat(nexus): add StudioWorkshopDetail page (phase 10)` — detail view + tests 5. `refactor(nexus): rewire ContentStudio to workshop grid (phase 10)` — ContentStudio.tsx rewrite Each commit must build and pass tests. Include `Co-Authored-By: Claude Opus 4.6 (1M context) ` on every commit. --- ## Before you begin Ask the controller if any of these are unclear: - The component names and import paths of the current ContentStudio's 7 tabs (read the file first and report what's there) - Whether `ConvertPage.tsx` is extractable or if you should wrap the whole thing - Whether the new sub-routes the controller needs to add warrant a pause and App.tsx edit, or if the hacky path-matching workaround is acceptable - Anything in §6 of the spec that reads as ambiguous If you find that the current ContentStudio has fewer than 7 tabs (e.g. only 5 generators are actually built), report it — the spec assumes 7 existing + 1 new (convert). The workshop grid can still show 8 cards with placeholder content for missing generators. --- ## When you're in over your head Escalate early if: - ConvertPage body extraction breaks tests in the legacy `/convert` route - The current ContentStudio has a global state or context that doesn't compose cleanly with the new grid → detail flow - Any generator component has hardcoded dependencies on the tab layout you're removing - classifyIntent is pulling in too many edge cases (stop at ~10 keyword patterns; more is Phase 14's classifier) --- ## Report format (final) - **Status** - **Commits produced** - **Files created / modified** - **Tests added and passing count** - **Typecheck result** for Phase 10 files - **Routing needs** for the controller to add to App.tsx - **ConvertPage fold-in notes** — did you extract a body component or wrap the whole page? - **Open concerns** - **Deviations from the plan** - **Self-review findings**