Claude
Skills
Sign in
Back

structured-editor

Included with Lifetime
$97 forever

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".

Image & Video

What this skill does


# 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"
Files: 1
Size: 79.3 KB
Complexity: 40/100
Category: Image & Video

Related in Image & Video