# Structured Editor

> Section-based document editor — documents composed of named, typed sections that render as collapsible React components with inline editing, status badges, and annotation support. Use this skill when the user says "add structured editor", "section editor", "spec editor", "document builder", or "structured document".

- Skill: `mattwoodco/structured-editor` (Agent Skill)
- Install (CLI): `npx skillmds@latest add mattwoodco/structured-editor`
- Raw SKILL.md: https://api.skillmd.com/api/skills/mattwoodco/structured-editor/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: mattwoodco (https://skillmd.com/u/mattwoodco)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/mattwoodco/structured-editor

---


# Structured Editor

Section-based document editor where documents are composed of named, typed sections (Summary, User Stories, Acceptance Criteria, Edge Cases, Error States, Dependencies, Open Questions) that render as collapsible React components with inline editing, status badges, and annotation support.

A document is a collection of typed sections. Each section has: title, type, content (JSON), status, and sort order. Annotations are comments attached to sections from users or AI agents.

## Prerequisites

- Next.js app with `src/` directory and App Router
- `add-shadcn` skill installed (shadcn/ui components available)
- `db` skill installed (Drizzle ORM at `@/lib/db`, Postgres running)
- `auth` skill installed (`withAuth` available at `@/lib/auth-guard`)

## Installation

```bash
bunx shadcn@latest add collapsible badge dropdown-menu textarea
```

## What Gets Created

```
src/
├── lib/
│   └── db/
│       └── schema/
│           └── document.ts                # specDocument, specSection, specAnnotation tables
├── app/
│   └── api/
│       └── documents/
│           ├── route.ts                   # GET (list), POST (create)
│           ├── [documentId]/
│           │   ├── route.ts               # GET, PATCH, DELETE
│           │   └── sections/
│           │       ├── route.ts           # GET (list sections), POST (add section)
│           │       └── [sectionId]/
│           │           ├── route.ts       # PATCH, DELETE section
│           │           └── annotations/
│           │               └── route.ts   # GET, POST annotations
└── components/
    └── editor/
        ├── document-editor.tsx            # Main editor — renders all sections
        ├── section-card.tsx               # Single section: collapse, edit, status, annotations
        ├── section-content.tsx            # Content renderer by section type
        ├── section-toolbar.tsx            # Status badge, annotation count, actions
        ├── annotation-list.tsx            # Inline annotation thread on a section
        └── add-section-button.tsx         # Button to add new section with type picker
```

## What Gets Modified

```
src/
└── lib/
    └── db/
        └── schema/
            └── index.ts                   # Add document schema export
```

## Database

After applying this skill, push the schema to create the `spec_document`, `spec_section`, and `spec_annotation` tables:

```bash
bunx drizzle-kit push
```

## Section Types and Content Shapes

Each section type has a specific content JSON shape:

| Type | Content Shape |
|------|--------------|
| `summary` | `{ text: string }` |
| `user_stories` | `{ items: Array<{ id: string; persona: string; action: string; benefit: string }> }` |
| `acceptance_criteria` | `{ items: Array<{ id: string; criterion: string; checked: boolean }> }` |
| `edge_cases` | `{ items: Array<{ id: string; scenario: string; expectedBehavior: string }> }` |
| `error_states` | `{ items: Array<{ id: string; trigger: string; userMessage: string; recovery: string }> }` |
| `dependencies` | `{ items: Array<{ id: string; name: string; type: "api" \| "service" \| "feature" \| "data"; status: "ready" \| "in_progress" \| "blocked" }> }` |
| `open_questions` | `{ items: Array<{ id: string; question: string; answer: string \| null; resolved: boolean }> }` |
| `custom` | `{ text: string }` |

## Setup Steps

### Step 1: Create `src/lib/db/schema/document.ts`

```typescript
import {
  pgTable,
  text,
  timestamp,
  uuid,
  json,
  integer,
  boolean,
} from "drizzle-orm/pg-core";

// --- Section content types ---

type SummaryContent = { text: string };

type UserStoryItem = {
  id: string;
  persona: string;
  action: string;
  benefit: string;
};
type UserStoriesContent = { items: UserStoryItem[] };

type AcceptanceCriterionItem = {
  id: string;
  criterion: string;
  checked: boolean;
};
type AcceptanceCriteriaContent = { items: AcceptanceCriterionItem[] };

type EdgeCaseItem = {
  id: string;
  scenario: string;
  expectedBehavior: string;
};
type EdgeCasesContent = { items: EdgeCaseItem[] };

type ErrorStateItem = {
  id: string;
  trigger: string;
  userMessage: string;
  recovery: string;
};
type ErrorStatesContent = { items: ErrorStateItem[] };

type DependencyItem = {
  id: string;
  name: string;
  type: "api" | "service" | "feature" | "data";
  status: "ready" | "in_progress" | "blocked";
};
type DependenciesContent = { items: DependencyItem[] };

type OpenQuestionItem = {
  id: string;
  question: string;
  answer: string | null;
  resolved: boolean;
};
type OpenQuestionsContent = { items: OpenQuestionItem[] };

type CustomContent = { text: string };

export type SectionContent =
  | SummaryContent
  | UserStoriesContent
  | AcceptanceCriteriaContent
  | EdgeCasesContent
  | ErrorStatesContent
  | DependenciesContent
  | OpenQuestionsContent
  | CustomContent;

export type SectionType =
  | "summary"
  | "user_stories"
  | "acceptance_criteria"
  | "edge_cases"
  | "error_states"
  | "dependencies"
  | "open_questions"
  | "custom";

export type SectionStatus = "draft" | "reviewed" | "approved";
export type DocumentStatus = "draft" | "review" | "approved" | "archived";

export {
  type SummaryContent,
  type UserStoriesContent,
  type UserStoryItem,
  type AcceptanceCriteriaContent,
  type AcceptanceCriterionItem,
  type EdgeCasesContent,
  type EdgeCaseItem,
  type ErrorStatesContent,
  type ErrorStateItem,
  type DependenciesContent,
  type DependencyItem,
  type OpenQuestionsContent,
  type OpenQuestionItem,
  type CustomContent,
};

// --- Tables ---

export const specDocument = pgTable("spec_document", {
  id: uuid("id").defaultRandom().primaryKey(),
  userId: text("user_id").notNull(),
  title: text("title").notNull(),
  description: text("description"),
  status: text("status", {
    enum: ["draft", "review", "approved", "archived"],
  })
    .notNull()
    .default("draft"),
  createdAt: timestamp("created_at", { withTimezone: true })
    .defaultNow()
    .notNull(),
  updatedAt: timestamp("updated_at", { withTimezone: true })
    .defaultNow()
    .notNull(),
});

export const specSection = pgTable("spec_section", {
  id: uuid("id").defaultRandom().primaryKey(),
  documentId: uuid("document_id")
    .notNull()
    .references(() => specDocument.id, { onDelete: "cascade" }),
  type: text("type", {
    enum: [
      "summary",
      "user_stories",
      "acceptance_criteria",
      "edge_cases",
      "error_states",
      "dependencies",
      "open_questions",
      "custom",
    ],
  }).notNull(),
  title: text("title").notNull(),
  content: json("content").$type<SectionContent>().notNull().default({ text: "" }),
  status: text("status", {
    enum: ["draft", "reviewed", "approved"],
  })
    .notNull()
    .default("draft"),
  sortOrder: integer("sort_order").notNull().default(0),
  collapsed: boolean("collapsed").notNull().default(false),
  createdAt: timestamp("created_at", { withTimezone: true })
    .defaultNow()
    .notNull(),
  updatedAt: timestamp("updated_at", { withTimezone: true })
    .defaultNow()
    .notNull(),
});

export const specAnnotation = pgTable("spec_annotation", {
  id: uuid("id").defaultRandom().primaryKey(),
  sectionId: uuid("section_id")
    .notNull()
    .references(() => specSection.id, { onDelete: "cascade" }),
  authorId: text("author_id"),
  authorName: text("author_name").notNull(),
  authorRole: text("author_role").default("user"),
  content: text("content").notNull(),
  color: text("color"),
  createdAt: timestamp("created_at", { withTimezone: true })
    .defaultNow()
    .notNull(),
});
```

### Step 2: Add document schema to barrel export

Add the document schema export to `src/lib/db/schema/index.ts`:

```typescript
export * from "./document";
```

### Step 3: Create `src/app/api/documents/route.ts`

```typescript
import { NextResponse } from "next/server";
import { withAuth } from "@/lib/auth-guard";
import { db } from "@/lib/db";
import { specDocument } from "@/lib/db/schema/document";
import { eq, desc } from "drizzle-orm";

type DocumentListItem = {
  id: string;
  title: string;
  description: string | null;
  status: string;
  createdAt: Date;
  updatedAt: Date;
};

/** GET /api/documents — list all documents for the current user */
export const GET = withAuth(async (_request, { user }) => {
  const documents = await db
    .select({
      id: specDocument.id,
      title: specDocument.title,
      description: specDocument.description,
      status: specDocument.status,
      createdAt: specDocument.createdAt,
      updatedAt: specDocument.updatedAt,
    })
    .from(specDocument)
    .where(eq(specDocument.userId, user.id))
    .orderBy(desc(specDocument.updatedAt));

  return NextResponse.json<DocumentListItem[]>(documents);
});

type CreateDocumentBody = {
  title: string;
  description?: string;
};

/** POST /api/documents — create a new document */
export const POST = withAuth(async (request, { user }) => {
  const body: CreateDocumentBody = await request.json();

  const [document] = await db
    .insert(specDocument)
    .values({
      userId: user.id,
      title: body.title,
      description: body.description ?? null,
    })
    .returning();

  return NextResponse.json(document, { status: 201 });
});
```

### Step 4: Create `src/app/api/documents/[documentId]/route.ts`

```typescript
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { withAuth } from "@/lib/auth-guard";
import { db } from "@/lib/db";
import {
  specDocument,
  specSection,
  specAnnotation,
  type DocumentStatus,
} from "@/lib/db/schema/document";
import { eq, and, asc } from "drizzle-orm";

type RouteContext = { params: Promise<{ documentId: string }> };

/** GET /api/documents/[documentId] — get document with all sections and annotations */
export const GET = withAuth(async (request: NextRequest, { user }) => {
  const pathParts = request.nextUrl.pathname.split("/");
  const documentId = pathParts[pathParts.length - 1];

  const documents = await db
    .select()
    .from(specDocument)
    .where(
      and(
        eq(specDocument.id, documentId),
        eq(specDocument.userId, user.id)
      )
    )
    .limit(1);

  if (documents.length === 0) {
    return NextResponse.json({ error: "Document not found" }, { status: 404 });
  }

  const sections = await db
    .select()
    .from(specSection)
    .where(eq(specSection.documentId, documentId))
    .orderBy(asc(specSection.sortOrder));

  const sectionIds = sections.map((s) => s.id);

  const annotations =
    sectionIds.length > 0
      ? await db
          .select()
          .from(specAnnotation)
          .where(
            eq(
              specAnnotation.sectionId,
              sectionIds[0] // initial fetch — client fetches per-section as needed
            )
          )
      : [];

  // Fetch all annotations for all sections
  const allAnnotations: Record<string, typeof annotations> = {};
  for (const section of sections) {
    const sectionAnnotations = await db
      .select()
      .from(specAnnotation)
      .where(eq(specAnnotation.sectionId, section.id))
      .orderBy(asc(specAnnotation.createdAt));
    allAnnotations[section.id] = sectionAnnotations;
  }

  return NextResponse.json({
    document: documents[0],
    sections,
    annotations: allAnnotations,
  });
}) as (request: NextRequest, context: RouteContext) => Promise<NextResponse>;

type UpdateDocumentBody = {
  title?: string;
  description?: string;
  status?: DocumentStatus;
};

/** PATCH /api/documents/[documentId] — update document */
export const PATCH = withAuth(async (request: NextRequest, { user }) => {
  const pathParts = request.nextUrl.pathname.split("/");
  const documentId = pathParts[pathParts.length - 1];

  const body: UpdateDocumentBody = await request.json();

  const result = await db
    .update(specDocument)
    .set({
      ...(body.title !== undefined && { title: body.title }),
      ...(body.description !== undefined && { description: body.description }),
      ...(body.status !== undefined && { status: body.status }),
      updatedAt: new Date(),
    })
    .where(
      and(
        eq(specDocument.id, documentId),
        eq(specDocument.userId, user.id)
      )
    )
    .returning();

  if (result.length === 0) {
    return NextResponse.json({ error: "Document not found" }, { status: 404 });
  }

  return NextResponse.json(result[0]);
}) as (request: NextRequest, context: RouteContext) => Promise<NextResponse>;

/** DELETE /api/documents/[documentId] — delete document (cascades to sections and annotations) */
export const DELETE = withAuth(async (request: NextRequest, { user }) => {
  const pathParts = request.nextUrl.pathname.split("/");
  const documentId = pathParts[pathParts.length - 1];

  const result = await db
    .delete(specDocument)
    .where(
      and(
        eq(specDocument.id, documentId),
        eq(specDocument.userId, user.id)
      )
    )
    .returning({ id: specDocument.id });

  if (result.length === 0) {
    return NextResponse.json({ error: "Document not found" }, { status: 404 });
  }

  return NextResponse.json({ success: true });
}) as (request: NextRequest, context: RouteContext) => Promise<NextResponse>;
```

### Step 5: Create `src/app/api/documents/[documentId]/sections/route.ts`

```typescript
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { withAuth } from "@/lib/auth-guard";
import { db } from "@/lib/db";
import {
  specDocument,
  specSection,
  type SectionType,
  type SectionContent,
} from "@/lib/db/schema/document";
import { eq, and, asc } from "drizzle-orm";

type RouteContext = { params: Promise<{ documentId: string }> };

/** GET /api/documents/[documentId]/sections — list sections for a document */
export const GET = withAuth(async (request: NextRequest, { user }) => {
  const pathParts = request.nextUrl.pathname.split("/");
  // URL: /api/documents/[documentId]/sections
  const documentId = pathParts[pathParts.length - 2];

  // Verify document ownership
  const documents = await db
    .select({ id: specDocument.id })
    .from(specDocument)
    .where(
      and(
        eq(specDocument.id, documentId),
        eq(specDocument.userId, user.id)
      )
    )
    .limit(1);

  if (documents.length === 0) {
    return NextResponse.json({ error: "Document not found" }, { status: 404 });
  }

  const sections = await db
    .select()
    .from(specSection)
    .where(eq(specSection.documentId, documentId))
    .orderBy(asc(specSection.sortOrder));

  return NextResponse.json(sections);
}) as (request: NextRequest, context: RouteContext) => Promise<NextResponse>;

type CreateSectionBody = {
  type: SectionType;
  title: string;
  content?: SectionContent;
  sortOrder?: number;
};

/** POST /api/documents/[documentId]/sections — add a section to the document */
export const POST = withAuth(async (request: NextRequest, { user }) => {
  const pathParts = request.nextUrl.pathname.split("/");
  const documentId = pathParts[pathParts.length - 2];

  // Verify document ownership
  const documents = await db
    .select({ id: specDocument.id })
    .from(specDocument)
    .where(
      and(
        eq(specDocument.id, documentId),
        eq(specDocument.userId, user.id)
      )
    )
    .limit(1);

  if (documents.length === 0) {
    return NextResponse.json({ error: "Document not found" }, { status: 404 });
  }

  const body: CreateSectionBody = await request.json();

  const defaultContent = getDefaultContent(body.type);

  const [section] = await db
    .insert(specSection)
    .values({
      documentId,
      type: body.type,
      title: body.title,
      content: body.content ?? defaultContent,
      sortOrder: body.sortOrder ?? 0,
    })
    .returning();

  // Update document timestamp
  await db
    .update(specDocument)
    .set({ updatedAt: new Date() })
    .where(eq(specDocument.id, documentId));

  return NextResponse.json(section, { status: 201 });
}) as (request: NextRequest, context: RouteContext) => Promise<NextResponse>;

function getDefaultContent(type: SectionType): SectionContent {
  switch (type) {
    case "summary":
      return { text: "" };
    case "user_stories":
      return { items: [] };
    case "acceptance_criteria":
      return { items: [] };
    case "edge_cases":
      return { items: [] };
    case "error_states":
      return { items: [] };
    case "dependencies":
      return { items: [] };
    case "open_questions":
      return { items: [] };
    case "custom":
      return { text: "" };
  }
}
```

### Step 6: Create `src/app/api/documents/[documentId]/sections/[sectionId]/route.ts`

```typescript
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { withAuth } from "@/lib/auth-guard";
import { db } from "@/lib/db";
import {
  specDocument,
  specSection,
  type SectionContent,
  type SectionStatus,
} from "@/lib/db/schema/document";
import { eq, and } from "drizzle-orm";

type RouteContext = {
  params: Promise<{ documentId: string; sectionId: string }>;
};

/** Helper to parse documentId and sectionId from the URL path */
function parseIds(pathname: string): {
  documentId: string;
  sectionId: string;
} {
  // URL: /api/documents/[documentId]/sections/[sectionId]
  const parts = pathname.split("/");
  const sectionsIndex = parts.indexOf("sections");
  return {
    documentId: parts[sectionsIndex - 1],
    sectionId: parts[sectionsIndex + 1],
  };
}

type UpdateSectionBody = {
  title?: string;
  content?: SectionContent;
  status?: SectionStatus;
  sortOrder?: number;
  collapsed?: boolean;
};

/** PATCH /api/documents/[documentId]/sections/[sectionId] — update a section */
export const PATCH = withAuth(async (request: NextRequest, { user }) => {
  const { documentId, sectionId } = parseIds(request.nextUrl.pathname);

  // Verify document ownership
  const documents = await db
    .select({ id: specDocument.id })
    .from(specDocument)
    .where(
      and(
        eq(specDocument.id, documentId),
        eq(specDocument.userId, user.id)
      )
    )
    .limit(1);

  if (documents.length === 0) {
    return NextResponse.json({ error: "Document not found" }, { status: 404 });
  }

  const body: UpdateSectionBody = await request.json();

  const result = await db
    .update(specSection)
    .set({
      ...(body.title !== undefined && { title: body.title }),
      ...(body.content !== undefined && { content: body.content }),
      ...(body.status !== undefined && { status: body.status }),
      ...(body.sortOrder !== undefined && { sortOrder: body.sortOrder }),
      ...(body.collapsed !== undefined && { collapsed: body.collapsed }),
      updatedAt: new Date(),
    })
    .where(
      and(
        eq(specSection.id, sectionId),
        eq(specSection.documentId, documentId)
      )
    )
    .returning();

  if (result.length === 0) {
    return NextResponse.json({ error: "Section not found" }, { status: 404 });
  }

  // Update document timestamp
  await db
    .update(specDocument)
    .set({ updatedAt: new Date() })
    .where(eq(specDocument.id, documentId));

  return NextResponse.json(result[0]);
}) as (request: NextRequest, context: RouteContext) => Promise<NextResponse>;

/** DELETE /api/documents/[documentId]/sections/[sectionId] — delete a section */
export const DELETE = withAuth(async (request: NextRequest, { user }) => {
  const { documentId, sectionId } = parseIds(request.nextUrl.pathname);

  // Verify document ownership
  const documents = await db
    .select({ id: specDocument.id })
    .from(specDocument)
    .where(
      and(
        eq(specDocument.id, documentId),
        eq(specDocument.userId, user.id)
      )
    )
    .limit(1);

  if (documents.length === 0) {
    return NextResponse.json({ error: "Document not found" }, { status: 404 });
  }

  const result = await db
    .delete(specSection)
    .where(
      and(
        eq(specSection.id, sectionId),
        eq(specSection.documentId, documentId)
      )
    )
    .returning({ id: specSection.id });

  if (result.length === 0) {
    return NextResponse.json({ error: "Section not found" }, { status: 404 });
  }

  // Update document timestamp
  await db
    .update(specDocument)
    .set({ updatedAt: new Date() })
    .where(eq(specDocument.id, documentId));

  return NextResponse.json({ success: true });
}) as (request: NextRequest, context: RouteContext) => Promise<NextResponse>;
```

### Step 7: Create `src/app/api/documents/[documentId]/sections/[sectionId]/annotations/route.ts`

```typescript
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { withAuth } from "@/lib/auth-guard";
import { db } from "@/lib/db";
import {
  specDocument,
  specSection,
  specAnnotation,
} from "@/lib/db/schema/document";
import { eq, and, asc } from "drizzle-orm";

type RouteContext = {
  params: Promise<{ documentId: string; sectionId: string }>;
};

/** Helper to parse documentId and sectionId from the URL path */
function parseIds(pathname: string): {
  documentId: string;
  sectionId: string;
} {
  // URL: /api/documents/[documentId]/sections/[sectionId]/annotations
  const parts = pathname.split("/");
  const sectionsIndex = parts.indexOf("sections");
  return {
    documentId: parts[sectionsIndex - 1],
    sectionId: parts[sectionsIndex + 1],
  };
}

/** GET /api/documents/[documentId]/sections/[sectionId]/annotations — list annotations */
export const GET = withAuth(async (request: NextRequest, { user }) => {
  const { documentId, sectionId } = parseIds(request.nextUrl.pathname);

  // Verify document ownership
  const documents = await db
    .select({ id: specDocument.id })
    .from(specDocument)
    .where(
      and(
        eq(specDocument.id, documentId),
        eq(specDocument.userId, user.id)
      )
    )
    .limit(1);

  if (documents.length === 0) {
    return NextResponse.json({ error: "Document not found" }, { status: 404 });
  }

  // Verify section belongs to document
  const sections = await db
    .select({ id: specSection.id })
    .from(specSection)
    .where(
      and(
        eq(specSection.id, sectionId),
        eq(specSection.documentId, documentId)
      )
    )
    .limit(1);

  if (sections.length === 0) {
    return NextResponse.json({ error: "Section not found" }, { status: 404 });
  }

  const annotations = await db
    .select()
    .from(specAnnotation)
    .where(eq(specAnnotation.sectionId, sectionId))
    .orderBy(asc(specAnnotation.createdAt));

  return NextResponse.json(annotations);
}) as (request: NextRequest, context: RouteContext) => Promise<NextResponse>;

type CreateAnnotationBody = {
  content: string;
  authorName?: string;
  authorRole?: string;
  color?: string;
};

/** POST /api/documents/[documentId]/sections/[sectionId]/annotations — add annotation */
export const POST = withAuth(async (request: NextRequest, { user }) => {
  const { documentId, sectionId } = parseIds(request.nextUrl.pathname);

  // Verify document ownership
  const documents = await db
    .select({ id: specDocument.id })
    .from(specDocument)
    .where(
      and(
        eq(specDocument.id, documentId),
        eq(specDocument.userId, user.id)
      )
    )
    .limit(1);

  if (documents.length === 0) {
    return NextResponse.json({ error: "Document not found" }, { status: 404 });
  }

  // Verify section belongs to document
  const sections = await db
    .select({ id: specSection.id })
    .from(specSection)
    .where(
      and(
        eq(specSection.id, sectionId),
        eq(specSection.documentId, documentId)
      )
    )
    .limit(1);

  if (sections.length === 0) {
    return NextResponse.json({ error: "Section not found" }, { status: 404 });
  }

  const body: CreateAnnotationBody = await request.json();

  const [annotation] = await db
    .insert(specAnnotation)
    .values({
      sectionId,
      authorId: user.id,
      authorName: body.authorName ?? user.name ?? "Unknown",
      authorRole: body.authorRole ?? "user",
      content: body.content,
      color: body.color ?? null,
    })
    .returning();

  return NextResponse.json(annotation, { status: 201 });
}) as (request: NextRequest, context: RouteContext) => Promise<NextResponse>;
```

### Step 8: Create `src/components/editor/section-toolbar.tsx`

```tsx
"use client";

import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
  PencilSimple,
  Trash,
  ArrowUp,
  ArrowDown,
  ChatCircle,
} from "@phosphor-icons/react";
import type { SectionStatus } from "@/lib/db/schema/document";

type SectionToolbarProps = {
  status: SectionStatus;
  annotationCount: number;
  onStatusCycle: () => void;
  onEdit: () => void;
  onDelete: () => void;
  onMoveUp: () => void;
  onMoveDown: () => void;
  isFirst: boolean;
  isLast: boolean;
};

const statusConfig: Record<
  SectionStatus,
  { label: string; variant: "secondary" | "outline" | "default" }
> = {
  draft: { label: "Draft", variant: "secondary" },
  reviewed: { label: "Reviewed", variant: "outline" },
  approved: { label: "Approved", variant: "default" },
};

const statusOrder: SectionStatus[] = ["draft", "reviewed", "approved"];

export function SectionToolbar({
  status,
  annotationCount,
  onStatusCycle,
  onEdit,
  onDelete,
  onMoveUp,
  onMoveDown,
  isFirst,
  isLast,
}: SectionToolbarProps) {
  const config = statusConfig[status];

  return (
    <div className="flex items-center gap-1">
      <button type="button" onClick={onStatusCycle} className="cursor-pointer">
        <Badge variant={config.variant}>{config.label}</Badge>
      </button>

      {annotationCount > 0 && (
        <Badge variant="outline" className="gap-1">
          <ChatCircle className="h-3 w-3" />
          {annotationCount}
        </Badge>
      )}

      <Button
        variant="ghost"
        size="icon"
        className="h-7 w-7"
        onClick={onMoveUp}
        disabled={isFirst}
        title="Move up"
      >
        <ArrowUp className="h-3.5 w-3.5" />
      </Button>

      <Button
        variant="ghost"
        size="icon"
        className="h-7 w-7"
        onClick={onMoveDown}
        disabled={isLast}
        title="Move down"
      >
        <ArrowDown className="h-3.5 w-3.5" />
      </Button>

      <Button
        variant="ghost"
        size="icon"
        className="h-7 w-7"
        onClick={onEdit}
        title="Edit section"
      >
        <PencilSimple className="h-3.5 w-3.5" />
      </Button>

      <Button
        variant="ghost"
        size="icon"
        className="h-7 w-7 text-destructive"
        onClick={onDelete}
        title="Delete section"
      >
        <Trash className="h-3.5 w-3.5" />
      </Button>
    </div>
  );
}

export function getNextStatus(current: SectionStatus): SectionStatus {
  const currentIndex = statusOrder.indexOf(current);
  return statusOrder[(currentIndex + 1) % statusOrder.length];
}
```

### Step 9: Create `src/components/editor/section-content.tsx`

```tsx
"use client";

import { useId, useCallback } from "react";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Badge } from "@/components/ui/badge";
import { Plus, Trash } from "@phosphor-icons/react";
import type {
  SectionType,
  SectionContent as SectionContentType,
  SummaryContent,
  UserStoriesContent,
  UserStoryItem,
  AcceptanceCriteriaContent,
  AcceptanceCriterionItem,
  EdgeCasesContent,
  EdgeCaseItem,
  ErrorStatesContent,
  ErrorStateItem,
  DependenciesContent,
  DependencyItem,
  OpenQuestionsContent,
  OpenQuestionItem,
  CustomContent,
} from "@/lib/db/schema/document";

type SectionContentProps = {
  type: SectionType;
  content: SectionContentType;
  isEditing: boolean;
  onContentChange: (content: SectionContentType) => void;
};

export function SectionContent({
  type,
  content,
  isEditing,
  onContentChange,
}: SectionContentProps) {
  switch (type) {
    case "summary":
      return (
        <SummaryRenderer
          content={content as SummaryContent}
          isEditing={isEditing}
          onChange={onContentChange}
        />
      );
    case "user_stories":
      return (
        <UserStoriesRenderer
          content={content as UserStoriesContent}
          isEditing={isEditing}
          onChange={onContentChange}
        />
      );
    case "acceptance_criteria":
      return (
        <AcceptanceCriteriaRenderer
          content={content as AcceptanceCriteriaContent}
          isEditing={isEditing}
          onChange={onContentChange}
        />
      );
    case "edge_cases":
      return (
        <EdgeCasesRenderer
          content={content as EdgeCasesContent}
          isEditing={isEditing}
          onChange={onContentChange}
        />
      );
    case "error_states":
      return (
        <ErrorStatesRenderer
          content={content as ErrorStatesContent}
          isEditing={isEditing}
          onChange={onContentChange}
        />
      );
    case "dependencies":
      return (
        <DependenciesRenderer
          content={content as DependenciesContent}
          isEditing={isEditing}
          onChange={onContentChange}
        />
      );
    case "open_questions":
      return (
        <OpenQuestionsRenderer
          content={content as OpenQuestionsContent}
          isEditing={isEditing}
          onChange={onContentChange}
        />
      );
    case "custom":
      return (
        <CustomRenderer
          content={content as CustomContent}
          isEditing={isEditing}
          onChange={onContentChange}
        />
      );
  }
}

// --- Summary ---

type SummaryRendererProps = {
  content: SummaryContent;
  isEditing: boolean;
  onChange: (content: SummaryContent) => void;
};

function SummaryRenderer({ content, isEditing, onChange }: SummaryRendererProps) {
  if (isEditing) {
    return (
      <Textarea
        value={content.text}
        onChange={(e) => onChange({ text: e.target.value })}
        placeholder="Enter summary..."
        rows={4}
      />
    );
  }

  return (
    <div className="prose dark:prose-invert max-w-none whitespace-pre-wrap">
      {content.text || <span className="text-muted-foreground">No summary yet.</span>}
    </div>
  );
}

// --- User Stories ---

type UserStoriesRendererProps = {
  content: UserStoriesContent;
  isEditing: boolean;
  onChange: (content: UserStoriesContent) => void;
};

function UserStoriesRenderer({
  content,
  isEditing,
  onChange,
}: UserStoriesRendererProps) {
  const rowId = useId();

  const addItem = useCallback(() => {
    onChange({
      items: [
        ...content.items,
        {
          id: crypto.randomUUID(),
          persona: "",
          action: "",
          benefit: "",
        },
      ],
    });
  }, [content, onChange]);

  const updateItem = useCallback(
    (itemId: string, field: keyof UserStoryItem, value: string) => {
      onChange({
        items: content.items.map((item) =>
          item.id === itemId ? { ...item, [field]: value } : item
        ),
      });
    },
    [content, onChange]
  );

  const removeItem = useCallback(
    (itemId: string) => {
      onChange({
        items: content.items.filter((item) => item.id !== itemId),
      });
    },
    [content, onChange]
  );

  if (!isEditing) {
    if (content.items.length === 0) {
      return <p className="text-muted-foreground">No user stories yet.</p>;
    }

    return (
      <div className="space-y-2">
        {content.items.map((item) => (
          <p key={`${rowId}-${item.id}`} className="text-sm">
            As a <strong>{item.persona || "..."}</strong>, I want{" "}
            <strong>{item.action || "..."}</strong> so that{" "}
            <strong>{item.benefit || "..."}</strong>
          </p>
        ))}
      </div>
    );
  }

  return (
    <div className="space-y-3">
      <table className="w-full text-sm">
        <thead>
          <tr className="border-b">
            <th className="p-2 text-left font-medium">Persona</th>
            <th className="p-2 text-left font-medium">Action</th>
            <th className="p-2 text-left font-medium">Benefit</th>
            <th className="w-10 p-2" />
          </tr>
        </thead>
        <tbody>
          {content.items.map((item) => (
            <tr key={`${rowId}-edit-${item.id}`} className="border-b">
              <td className="p-1">
                <input
                  type="text"
                  value={item.persona}
                  onChange={(e) =>
                    updateItem(item.id, "persona", e.target.value)
                  }
                  placeholder="user"
                  className="w-full rounded border bg-background px-2 py-1 text-sm"
                />
              </td>
              <td className="p-1">
                <input
                  type="text"
                  value={item.action}
                  onChange={(e) =>
                    updateItem(item.id, "action", e.target.value)
                  }
                  placeholder="to do something"
                  className="w-full rounded border bg-background px-2 py-1 text-sm"
                />
              </td>
              <td className="p-1">
                <input
                  type="text"
                  value={item.benefit}
                  onChange={(e) =>
                    updateItem(item.id, "benefit", e.target.value)
                  }
                  placeholder="I can achieve a goal"
                  className="w-full rounded border bg-background px-2 py-1 text-sm"
                />
              </td>
              <td className="p-1">
                <Button
                  variant="ghost"
                  size="icon"
                  className="h-7 w-7 text-destructive"
                  onClick={() => removeItem(item.id)}
                >
                  <Trash className="h-3.5 w-3.5" />
                </Button>
              </td>
            </tr>
          ))}
        </tbody>
      </table>
      <Button variant="outline" size="sm" onClick={addItem}>
        <Plus className="mr-1 h-3.5 w-3.5" />
        Add story
      </Button>
    </div>
  );
}

// --- Acceptance Criteria ---

type AcceptanceCriteriaRendererProps = {
  content: AcceptanceCriteriaContent;
  isEditing: boolean;
  onChange: (content: AcceptanceCriteriaContent) => void;
};

function AcceptanceCriteriaRenderer({
  content,
  isEditing,
  onChange,
}: AcceptanceCriteriaRendererProps) {
  const itemId = useId();

  const addItem = useCallback(() => {
    onChange({
      items: [
        ...content.items,
        { id: crypto.randomUUID(), criterion: "", checked: false },
      ],
    });
  }, [content, onChange]);

  const toggleItem = useCallback(
    (criterionId: string) => {
      onChange({
        items: content.items.map((item) =>
          item.id === criterionId
            ? { ...item, checked: !item.checked }
            : item
        ),
      });
    },
    [content, onChange]
  );

  const updateItem = useCallback(
    (criterionId: string, value: string) => {
      onChange({
        items: content.items.map((item) =>
          item.id === criterionId ? { ...item, criterion: value } : item
        ),
      });
    },
    [content, onChange]
  );

  const removeItem = useCallback(
    (criterionId: string) => {
      onChange({
        items: content.items.filter((item) => item.id !== criterionId),
      });
    },
    [content, onChange]
  );

  if (!isEditing) {
    if (content.items.length === 0) {
      return (
        <p className="text-muted-foreground">No acceptance criteria yet.</p>
      );
    }

    return (
      <ul className="space-y-1">
        {content.items.map((item) => (
          <li
            key={`${itemId}-${item.id}`}
            className="flex items-center gap-2 text-sm"
          >
            <input
              type="checkbox"
              checked={item.checked}
              onChange={() => toggleItem(item.id)}
              className="h-4 w-4 rounded border"
            />
            <span className={item.checked ? "line-through opacity-60" : ""}>
              {item.criterion || "..."}
            </span>
          </li>
        ))}
      </ul>
    );
  }

  return (
    <div className="space-y-2">
      {content.items.map((item) => (
        <div
          key={`${itemId}-edit-${item.id}`}
          className="flex items-center gap-2"
        >
          <input
            type="checkbox"
            checked={item.checked}
            onChange={() => toggleItem(item.id)}
            className="h-4 w-4 rounded border"
          />
          <input
            type="text"
            value={item.criterion}
            onChange={(e) => updateItem(item.id, e.target.value)}
            placeholder="Criterion..."
            className="flex-1 rounded border bg-background px-2 py-1 text-sm"
          />
          <Button
            variant="ghost"
            size="icon"
            className="h-7 w-7 text-destructive"
            onClick={() => removeItem(item.id)}
          >
            <Trash className="h-3.5 w-3.5" />
          </Button>
        </div>
      ))}
      <Button variant="outline" size="sm" onClick={addItem}>
        <Plus className="mr-1 h-3.5 w-3.5" />
        Add criterion
      </Button>
    </div>
  );
}

// --- Edge Cases ---

type EdgeCasesRendererProps = {
  content: EdgeCasesContent;
  isEditing: boolean;
  onChange: (content: EdgeCasesContent) => void;
};

function EdgeCasesRenderer({
  content,
  isEditing,
  onChange,
}: EdgeCasesRendererProps) {
  const rowId = useId();

  const addItem = useCallback(() => {
    onChange({
      items: [
        ...content.items,
        { id: crypto.randomUUID(), scenario: "", expectedBehavior: "" },
      ],
    });
  }, [content, onChange]);

  const updateItem = useCallback(
    (itemId: string, field: keyof EdgeCaseItem, value: string) => {
      onChange({
        items: content.items.map((item) =>
          item.id === itemId ? { ...item, [field]: value } : item
        ),
      });
    },
    [content, onChange]
  );

  const removeItem = useCallback(
    (itemId: string) => {
      onChange({
        items: content.items.filter((item) => item.id !== itemId),
      });
    },
    [content, onChange]
  );

  if (!isEditing) {
    if (content.items.length === 0) {
      return <p className="text-muted-foreground">No edge cases yet.</p>;
    }

    return (
      <div className="space-y-2">
        {content.items.map((item) => (
          <div
            key={`${rowId}-${item.id}`}
            className="rounded border p-2 text-sm"
          >
            <p>
              <strong>Scenario:</strong> {item.scenario || "..."}
            </p>
            <p>
              <strong>Expected:</strong> {item.expectedBehavior || "..."}
            </p>
          </div>
        ))}
      </div>
    );
  }

  return (
    <div className="space-y-3">
      {content.items.map((item) => (
        <div
          key={`${rowId}-edit-${item.id}`}
          className="flex gap-2 rounded border p-2"
        >
          <div className="flex-1 space-y-1">
            <input
              type="text"
              value={item.scenario}
              onChange={(e) =>
                updateItem(item.id, "scenario", e.target.value)
              }
              placeholder="Scenario..."
              className="w-full rounded border bg-background px-2 py-1 text-sm"
            />
            <input
              type="text"
              value={item.expectedBehavior}
              onChange={(e) =>
                updateItem(item.id, "expectedBehavior", e.target.value)
              }
              placeholder="Expected behavior..."
              className="w-full rounded border bg-background px-2 py-1 text-sm"
            />
          </div>
          <Button
            variant="ghost"
            size="icon"
  

…(truncated)
