# 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 `<DiagramGeneratorTab />`, `<IconGeneratorTab />`, etc. Build a map:

```ts
// In StudioWorkshopDetail.tsx (or a small helper module within studio/)
const WORKSHOP_COMPONENTS: Record<string, ComponentType> = {
  "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 `<WorkshopPlaceholder />` 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 `<ConvertPage />` 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 `/<companyPrefix>/assistant?prompt=<urlencoded 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 (
    <div className="mx-auto w-full max-w-[1200px] px-6 py-8">
      <header className="mb-8">
        <h1 className="text-[14px] font-semibold uppercase tracking-[0.1em] text-primary">STUDIO</h1>
        <p className="mt-2 text-[14px] text-muted-foreground">
          Eight workshops. Pick one or describe what you need.
        </p>
      </header>

      <WorkshopGrid workshops={WORKSHOPS} onSelect={handleSelectWorkshop} />

      <div className="mt-12">
        <StudioPromptBar
          onClassified={(slug, prefilled) => navigate(`/${prefix}/content-studio/${slug}?prompt=${encodeURIComponent(prefilled)}`)}
          onFallbackToAssistant={handleFallbackToAssistant}
        />
      </div>
    </div>
  );
}
```

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. `/<prefix>/content-studio/:workshopSlug`. The existing App.tsx currently only has `/<prefix>/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 <StudioWorkshopDetail slug={workshopSlug} />;
  }
  return <StudioHomeGrid ... />;
}
```

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) <noreply@anthropic.com>` 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**
