---
name: vitedocs:generate
description: Analyze the codebase and write VitePress documentation pages.
allowed-tools:
  - Read
  - Write
  - Edit
  - Bash
  - Glob
  - Grep
  - AskUserQuestion
---

## Manifest

All modes read and write a manifest at `.vitepress/docs-manifest.json`. This file is **local-only** — always add it to `.gitignore` (under the docs folder's nearest `.gitignore`) so it is never committed or distributed.

Add this line if not already present:
```
.vitepress/docs-manifest.json
```

### Manifest schema

```json
{
  "generated": "ISO-8601 timestamp",
  "project": {
    "type": "user-facing | developer | both",
    "baseUrl": "http://localhost:3000",
    "serverType": "node | wordpress | static | other",
    "startCommand": "npm run dev"
  },
  "pages": [
    {
      "file": "docs/guides/timer.md",
      "title": "Running a Timer",
      "docType": "user-facing",
      "sources": ["src/state/timer.js", "src/components/layouts/workouts.jsx"],
      "images": [
        {
          "path": "docs/public/screenshots/timer-main.png",
          "type": "screenshot",
          "placeholder": true,
          "caption": "Timer in active interval state",
          "captureUrl": "/workouts/123",
          "captureNote": "Timer should be running with intervals visible"
        },
        {
          "path": "docs/public/videos/timer-start.webm",
          "type": "video",
          "poster": "docs/public/screenshots/timer-start-poster.png",
          "vtt": "docs/public/videos/timer-start.vtt",
          "videoUrl": "",
          "caption": "Starting a timer interval",
          "captureUrl": "/workouts/123",
          "steps": [
            { "caption": "Click Start to begin the interval", "selector": ".start-btn", "action": "click" },
            { "caption": "Timer counts down in real time", "action": "wait", "ms": 1500 },
            { "caption": "Pause freezes the current interval", "selector": ".pause-btn", "action": "click" }
          ]
        }
      ],
      "lastSynced": "ISO-8601 timestamp",
      "syncHash": "sha of source file contents at last sync"
    }
  ]
}
```

---

## Mode: generate

### Step 0 — Detect tech stack

Before asking any questions, scan the codebase root for stack markers. Use Glob and Grep — do not ask the user what their stack is until you've made an attempt to detect it yourself.

**WordPress indicators** — check for any of these:
```
wp-config.php
wp-content/
wp-includes/
functions.php (in a theme directory)
style.css with "Theme Name:" header
composer.json with "johnpbloch/wordpress" or "roots/wordpress"
```

**Node/Next indicators:**
```
next.config.js / next.config.ts / next.config.mjs
package.json with "next" dependency
.next/ directory
```

**Other Node indicators:** `package.json`, `vite.config.*`, `astro.config.*`, `nuxt.config.*`

**Static:** no package.json, no wp-config.php, only HTML/CSS/JS files.

After scanning, present your finding and confirm with the user:

- header: "Tech stack detected"
- question: "I detected this as a [DETECTED_STACK] project — is that right?"
- options:
  - "Yes, that's correct"
  - "No — let me tell you what it actually is"

If the user corrects you, accept their answer and proceed with the corrected stack.

---

**If WordPress is detected**, ask the following WP-specific questions before running the git root check:

**WP-Q1 — WordPress project type:**
- header: "WordPress setup"
- question: "What kind of WordPress project is this?"
- options:
  - "Traditional theme (PHP templates, The Loop, etc.)"
  - "Block theme (Full Site Editing / Gutenberg blocks)"
  - "Headless WordPress (REST API or WPGraphQL feeding a front-end)"
  - "Plugin (documenting a plugin, not a theme)"
  - "Full site — theme + plugins together"

**WP-Q2 — Local dev environment** (plain text): Ask — "What URL is the WordPress site running on locally? (e.g. `http://localhost:8888`, `http://mysite.local`)"

**WP-Q3 — Page builder / block editor:**
- header: "Page builder"
- question: "Is this site built with a page builder or block toolkit?"
- options:
  - "Standard Gutenberg / block editor only"
  - "Elementor"
  - "Divi"
  - "ACF Blocks (Advanced Custom Fields)"
  - "Other — I'll tell you"
  - "No page builder — mostly PHP templates"

**WP-Q4 — Key plugins to document** (plain text): Ask — "Are there any plugins that are central to how the site works and should be included in the docs? (e.g. WooCommerce, ACF, Yoast — or 'none')"

**WP-Q5 — Headless front-end** (only if headless was selected in WP-Q1):
- header: "Front-end"
- question: "What is the headless front-end stack?"
- options:
  - "Next.js"
  - "Nuxt"
  - "SvelteKit"
  - "Astro"
  - "Other — I'll tell you"

**WP-Q6 — Theme / plugin folder(s)** (plain text): Scan `wp-content/themes/` and `wp-content/plugins/` for likely candidates first — present your best guess and let the user confirm or correct it. Ask — "Which theme or plugin should I focus on? You can provide multiple paths if they work together. (e.g. `wp-content/themes/my-theme, wp-content/plugins/my-plugin`)"

Once WP-Q6 is answered, the confirmed paths become the **focus paths**. All subsequent analysis, docs placement, and git root detection are scoped to these paths — the WP install root is off-limits.

**Each focus path must be its own git repository.** Git root detection (below) will validate this for every path provided. If any path is missing a `.git`, halt and ask the user to resolve it before continuing — do not proceed with a partial set.

Store all WP answers and carry them through the rest of generate mode. They affect codebase analysis (Step 2) and the doc structure proposal (Step 3).

---

#### Git root detection (all stacks)

**For WordPress:** run this check against every path confirmed in WP-Q6.
**For all other stacks:** run this check against the detected codebase root (current working directory unless corrected above).

For each focus path, check for a `.git` directory at **that exact path**:

```bash
ls FOCUS_PATH/.git
```

Do not check parent folders — the `.git` must be in the specific folder being documented. For WordPress this means checking inside `wp-content/themes/my-theme/` or `wp-content/plugins/my-plugin/`, not in `wp-content/themes/`, `wp-content/`, or the WP install root.

**If `.git` is found:** no question needed — the repo root and focus path are the same. Proceed.

**If `.git` is not found:** halt and tell the user:
> "I didn't find a `.git` folder at [FOCUS_PATH]. Each documented path must be its own git repository. Please initialise a repo there and come back, or remove this path from the list."

Do not continue until every focus path has a confirmed `.git`. Each path gets its own independent docs output, manifest, and GitHub Actions workflow.

This matters because the GitHub Actions workflow file must go in `.github/workflows/` at the **git root** of each respective path.

---

### Step 1 — Gather project details

**Q1 — Doc type:**
- header: "Documentation type"
- question: "What type of docs should I generate?"
- options:
  - "User-facing guides — how to use the app"
  - "Developer reference — architecture, API, data layer"
  - "Both"

**Q2 — App description** (plain text): Ask — "What is this app called, and what does it do in one sentence? (Used for headings and introductions.)"

**Q3 — Codebase root** (plain text): Ask — "Where is the codebase root? (Press enter to use the current directory.)" — **Skip for WordPress; already established by WP-Q6.**

**Q4 — Docs output:** Before asking, check whether `setup` has already been run by looking for `.vitepress/config.mjs` anywhere inside the focus path:

```bash
find FOCUS_PATH -name "config.mjs" -path "*/.vitepress/*" 2>/dev/null
```

**If found:** docs location is already established — skip this question, infer the docs folder from the found path, and tell the user: "I found an existing VitePress config at `[PATH]` — using that as the docs folder."

**If not found:** ask:

- header: "Docs location"
- question: "Where should the docs folder be created? I found your repo at `[GIT_ROOT_PATH]`."
- options:
  - "Here → `[GIT_ROOT_PATH]/docs/`"
  - "Somewhere else — I'll type the path"

If "Somewhere else": ask as plain text — "What path should I use? (Full path or relative to `[GIT_ROOT_PATH]`)"

Always show the full resolved path — never use abstract terms like "project root."

**Q1b — API base URL** (only if developer or both selected): Scan for API endpoints first — look for REST API routes, WP REST API extensions, Next.js API routes, Express routers, etc. If any are found, ask:

- header: "API base URL"
- question: "I found API endpoints in the codebase. What is the base URL for your API? This is used to generate live code samples in the docs."
- options:
  - "Same as the local dev URL already provided"
  - "Different URL — I'll type it"
  - "Skip — I don't want code samples"

If different: ask as plain text — "What is the API base URL? (e.g. `http://localhost:8888/wp-json` or `https://api.mysite.com`)"

Then ask:

- header: "Code sample tabs"
- question: "Which code sample types should the API endpoint component show?"
- options:
  - "Fetch + cURL (default)"
  - "Fetch only"
  - "cURL only"
  - "Fetch + cURL + PHP"
  - "Custom — I'll tell you which"

If custom: ask as plain text — "Which languages or methods? (e.g. Fetch, cURL, PHP, Python, Axios)"

Store the selected tabs — they determine which tab options are rendered in the `ApiEndpoint` component and which code samples are generated per endpoint.

Store `apiBase` and `tabs` — both used in VitePress config and the `ApiEndpoint` component.

**Q5 — Skip anything:**
- header: "Exclusions"
- question: "Anything to exclude from analysis? (node_modules, dist, .git, .next, coverage are always skipped.)"
- options:
  - "No, defaults are fine"
  - "Yes — I'll tell you what to skip"

If yes: ask as plain text — "Which folders or files should I skip?"

Wait for all answers.

### Step 2 — Analyze the codebase

#### Scope rule

**Single focus path:** analysis is strictly confined to that directory. Do not read, Glob, or Grep anywhere outside it — including the WP install root, other themes, other plugins, or WP core.

**Multiple focus paths:** analysis is confined to the declared paths combined. Cross-reading between them is allowed and expected — if the theme references a function from the plugin, follow that reference. Nothing outside the declared set is fair game.

In both cases: if a file inside a focus path imports or references something outside the declared paths, read only that specific referenced file — do not expand analysis to its parent directory.

Scan the codebase systematically. Build a mental map before writing anything. Look for:

**For user-facing docs:**
- App routes / pages (what screens exist)
- Key user flows (what can a user do)
- Forms and interactive features
- Settings and configuration surfaces

**For developer docs:**
- Entry points and routing structure
- Data layer (API routes, DB queries, service modules)
- State management
- Auth and middleware
- Reusable utilities and lib functions
- Environment variable requirements

Use Glob and Grep to find these. Do not read every file — read enough to understand what each area does.

### Step 3 — Plan the doc structure

Before writing any pages, output a proposed outline:

Present the proposed outline as plain text, then use AskUserQuestion to confirm:

- header: "Doc structure"
- question: "Does this outline look right?"
- options:
  - "Looks good — start writing"
  - "I want to make changes"

If changes: ask as plain text — "What would you like to add, remove, or rename?" Then re-present the updated outline and ask again.

### Step 3b — Identify home page features

Based on your codebase analysis, identify 3–6 meaningful capabilities or selling points of the project that would work well as `features` entries on the VitePress home page (`index.md`).

Good features are things a user or developer would care about — core functionality, key integrations, notable technical traits, or workflow benefits. Avoid generic filler like "Easy to use" unless it's specifically supported by something concrete in the code.

For each feature, derive:
- `icon` — a single emoji that represents it
- `title` — 2–4 words
- `details` — 1–2 sentences drawn from what you actually read in the codebase

Present the proposed features as plain text before writing anything:

```
Home page features I identified:

⚡️ Real-time Sync
   Changes pushed from the dashboard are reflected in the app within 500ms via WebSockets.

🔒 Role-Based Access
   Three permission tiers (admin, editor, viewer) enforced at the API and UI layer.

🧩 Block Builder
   Drag-and-drop layout editor built on ACF Blocks — no PHP required for content authors.
...
```

Then ask:

- header: "Home page features"
- question: "Do these features look right for the home page?"
- options:
  - "Yes — use these"
  - "I want to change some"
  - "Skip the home page features section"

If changes: ask as plain text — "What would you like to add, remove, or reword?"

Store the confirmed features — they are written into `index.md` in Step 4.

### Step 4 — Write pages

Write `index.md` first, then remaining pages one at a time. For `index.md`, use the VitePress home page layout with the confirmed features:

```md
---
layout: home

hero:
  name: "App Name"
  text: "One-line tagline"
  tagline: Slightly longer supporting line drawn from Q2 app description.
  actions:
    - theme: brand
      text: Get Started
      link: /FIRST_GUIDE_PAGE

features:
  - icon: ⚡️
    title: Feature Title
    details: Feature details sentence.
  # ... remaining features
---
```

Populate `name`, `text`, `tagline`, and the `link` in `actions` from what you know about the project. For each remaining page:

1. Read the relevant source files
2. Write the markdown based on what you can confidently determine
3. Where information is unclear or missing, insert a gap comment and continue:
   ```md
   <!-- GAP: What are the valid values for rotation frequency? -->
   ```
4. Where a screenshot would help understanding, insert a placeholder image and record it in the manifest:
   ```md
   ![Timer in active interval state](../public/screenshots/timer-main.png)
   ```
   Then generate the placeholder PNG (see **Placeholder generation** below).

5. For any page documenting API endpoints (REST routes, WP REST API extensions, etc.), use the `ApiEndpoint` component instead of plain code blocks — but only if `apiBase` was collected in Q1b. Use it like this:
   ```md
   <ApiEndpoint
     method="GET"
     path="/wp-json/alabama-news/v1/sources"
     auth="none"
     :response="{ sources: ['AP', 'Reuters'] }"
   />
   ```
   Populate `method`, `path`, `auth`, `body`, and `response` from what you read in the source files. If a request body or response shape is unclear, leave it as a gap comment.

6. For pages documenting interactions, flows, or button behaviors — anything where "what happens next" matters — suggest a video capture instead of a screenshot. Use `VideoDemo` like this:

   ```md
   <VideoDemo
     src="https://github.com/OWNER/REPO/releases/download/docs-media/timer-start.webm"
     poster="/screenshots/timer-start-poster.png"
     vtt="/videos/timer-start.vtt"
     caption="Starting a timer interval"
     :steps="[
       { caption: 'Click Start to begin the interval', time: 0 },
       { caption: 'Timer counts down in real time', time: 1.5 },
       { caption: 'Pause freezes the current interval', time: 4.2 }
     ]"
   />
   ```

   Leave `src` with a placeholder GitHub Releases URL — it will be filled in after `/vitedocs:capture` runs. Record the entry in the manifest with `"type": "video"` and `"videoUrl": ""`.

   Use a screenshot (not video) for: static UI states, settings screens, empty states, error messages.
   Use a video for: multi-step flows, button interactions, animations, form submissions, drag-and-drop.

**Color chips in tables:** Whenever a doc page contains a table with color values (hex codes, `rgb()`, `hsl()`, or CSS custom properties that resolve to a color), include an inline color chip in the same table cell. Use a `<span>` with an inline style — VitePress renders HTML inside markdown tables:

```md
| Token | | Value |
|---|---|---|
| `--brand-primary` | <span style="display:inline-block;width:14px;height:14px;background:#E63946;border-radius:2px;vertical-align:middle;border:1px solid rgba(0,0,0,.12)"></span> | `#E63946` |
```

Apply this to any color reference you encounter in the source — CSS variables, Tailwind config values, design token files, theme files, etc. If the value is a CSS variable (e.g. `var(--some-color)`) and you can resolve its hex from the source, use the resolved hex for the chip. If you can't resolve it, omit the chip for that row.

Do not stop to ask gap questions mid-page. Keep writing and accumulate all gaps.

### Step 5 — Gap review

After all pages are written, present all gaps in one batch:

```
I've written all X pages. I hit N gaps where I couldn't confidently determine
the correct information. Can you fill these in?

1. [guides/timer.md] What are the valid values for rotation frequency?
2. [developer/auth.md] Is the OAuth callback scoped to a single provider or does it support multiple?
...
```

Go back and fill in the answers the user provides.

### Step 6 — Install VitePress components

If `apiBase` was collected in Q1b, write the following files before updating the config.

**`.vitepress/components/ApiEndpoint.vue`** — write this file exactly, substituting `TABS` with the selected tab labels (e.g. `['Fetch', 'cURL']`) and adding sample generators for any additional tabs beyond Fetch and cURL:

```vue
<template>
  <div class="api-ep">
    <div class="api-ep__header">
      <div class="api-ep__badge">
        <span class="api-ep__method" :class="`api-ep__method--${method.toLowerCase()}`">{{ method }}</span>
        <code class="api-ep__path">{{ path }}</code>
      </div>
      <span class="api-ep__auth">{{ authLabel }}</span>
    </div>
    <div class="api-ep__body">
      <div class="api-ep__tabs">
        <button
          v-for="tab in tabs"
          :key="tab"
          class="api-ep__tab"
          :class="{ 'api-ep__tab--active': activeTab === tab }"
          @click="activeTab = tab"
        >{{ tab }}</button>
      </div>
      <pre class="api-ep__code"><code v-html="highlightedSample"></code></pre>
      <p v-if="activeTab === 'Fetch'" class="api-ep__note">
        Must be called from an authenticated browser session — cookies are sent automatically.
      </p>
      <template v-if="response">
        <div class="api-ep__section-label">Sample Response</div>
        <pre class="api-ep__code"><code v-html="highlightedResponse"></code></pre>
      </template>
    </div>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'
import { useData } from 'vitepress'
import hljs from 'highlight.js/lib/core'
import javascript from 'highlight.js/lib/languages/javascript'
import bash from 'highlight.js/lib/languages/bash'
import json from 'highlight.js/lib/languages/json'
// ADD THESE ONLY IF THE CORRESPONDING TAB WAS SELECTED IN Q1b:
// import php from 'highlight.js/lib/languages/php'
// import python from 'highlight.js/lib/languages/python'

hljs.registerLanguage('javascript', javascript) // Fetch + Axios
hljs.registerLanguage('bash', bash)             // cURL
hljs.registerLanguage('json', json)             // response bodies
// hljs.registerLanguage('php', php)
// hljs.registerLanguage('python', python)

const props = defineProps({
  method: { type: String, required: true },
  path: { type: String, required: true },
  auth: { type: String, default: 'authenticated' },
  body: { type: Object, default: null },
  response: { type: Object, default: null },
  formData: { type: Boolean, default: false },
})

const { theme } = useData()
const baseUrl = computed(() => theme.value.apiBase ?? '')

const authLabel = computed(() => ({
  'authenticated': 'Auth required',
  'admin:manage': 'Admin only',
  'self': 'Authenticated (self)',
  'none': 'Unauthenticated',
}[props.auth] ?? props.auth))

const tabs = TABS  // substituted from Q1b answer
const activeTab = ref(tabs[0])

const fetchSample = computed(() => {
  const url = `${baseUrl.value}${props.path}`
  if (props.method === 'GET') {
    return `const res = await fetch('${url}', {\n  credentials: 'include'\n})\nconst data = await res.json()`
  }
  if (props.formData) {
    return `const form = new FormData()\n// append fields to form...\n\nconst res = await fetch('${url}', {\n  method: 'POST',\n  credentials: 'include',\n  body: form\n})\nconst data = await res.json()`
  }
  const bodyStr = props.body ? JSON.stringify(props.body, null, 2) : '{}'
  return `const res = await fetch('${url}', {\n  method: '${props.method}',\n  headers: { 'Content-Type': 'application/json' },\n  credentials: 'include',\n  body: JSON.stringify(${bodyStr})\n})\nconst data = await res.json()`
})

const curlSample = computed(() => {
  const url = `${baseUrl.value}${props.path}`
  if (props.method === 'GET') {
    return `curl '${url}' \\\n  -H 'Cookie: <session-cookie>'`
  }
  if (props.formData) {
    return `curl -X POST '${url}' \\\n  -H 'Cookie: <session-cookie>' \\\n  -F 'field=value'`
  }
  const bodyStr = props.body ? JSON.stringify(props.body, null, 2) : '{}'
  return `curl -X ${props.method} '${url}' \\\n  -H 'Content-Type: application/json' \\\n  -H 'Cookie: <session-cookie>' \\\n  -d '${bodyStr}'`
})

// Add additional sample computed properties here for PHP, Python, Axios etc if selected in Q1b

const currentSample = computed(() => {
  if (activeTab.value === 'Fetch') return fetchSample.value
  if (activeTab.value === 'cURL') return curlSample.value
  return ''
})

const tabLang = {
  'Fetch': 'javascript',
  'Axios': 'javascript',
  'cURL': 'bash',
  'PHP': 'php',
  'Python': 'python',
}

const highlight = (code, lang) => {
  const registered = hljs.getLanguage(lang)
  return registered
    ? hljs.highlight(code, { language: lang }).value
    : hljs.highlightAuto(code).value
}

const highlightedSample = computed(() =>
  highlight(currentSample.value, tabLang[activeTab.value] ?? 'bash')
)

const formattedResponse = computed(() =>
  props.response ? JSON.stringify(props.response, null, 2) : ''
)

const highlightedResponse = computed(() =>
  formattedResponse.value ? highlight(formattedResponse.value, 'json') : ''
)
</script>

<style scoped>
.api-ep { border: 1px solid var(--vp-c-divider); border-radius: 8px; margin: 1.5rem 0 2.5rem; overflow: hidden; }
.api-ep__header { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 16px; background: var(--vp-c-bg-soft); border-bottom: 1px solid var(--vp-c-divider); }
.api-ep__badge { display: flex; align-items: center; gap: 10px; min-width: 0; }
.api-ep__method { display: inline-flex; align-items: center; font-size: 0.65rem; font-weight: 700; letter-spacing: 0.06em; padding: 2px 7px; border-radius: 4px; flex-shrink: 0; }
.api-ep__method--get { background: var(--vp-c-green-soft); color: var(--vp-c-green-1); }
.api-ep__method--post { background: var(--vp-c-brand-soft); color: var(--vp-c-brand-1); }
.api-ep__method--put { background: var(--vp-c-yellow-soft); color: var(--vp-c-yellow-1); }
.api-ep__method--delete { background: var(--vp-c-danger-soft); color: var(--vp-c-danger-1); }
.api-ep__path { font-family: var(--vp-font-family-mono); font-size: 0.85rem; color: var(--vp-c-text-1); background: none; padding: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.api-ep__auth { font-size: 0.7rem; font-weight: 500; color: var(--vp-c-text-3); flex-shrink: 0; white-space: nowrap; }
.api-ep__body { padding: 16px; background: var(--vp-c-bg-soft); display: flex; flex-direction: column; gap: 8px; }
.api-ep__tabs { display: flex; gap: 4px; margin-bottom: 2px; }
.api-ep__tab { font-size: 0.75rem; font-weight: 500; padding: 3px 10px; border-radius: 4px; border: 1px solid transparent; background: transparent; color: var(--vp-c-text-2); cursor: pointer; transition: all 0.15s; }
.api-ep__tab:hover { color: var(--vp-c-text-1); background: var(--vp-c-default-soft); }
.api-ep__tab--active { border-color: var(--vp-c-divider); background: var(--vp-c-bg); color: var(--vp-c-text-1); }
.api-ep__code { margin: 0; padding: 12px; background: var(--vp-c-bg); border-radius: 6px; overflow-x: auto; font-family: var(--vp-font-family-mono); font-size: 0.72rem; line-height: 1.65; color: var(--vp-c-text-1); }
.api-ep__code code { background: none; padding: 0; font-size: inherit; color: inherit; }
.api-ep__note { font-size: 0.7rem; color: var(--vp-c-text-3); margin: 0; line-height: 1.5; }
.api-ep__section-label { font-size: 0.68rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; color: var(--vp-c-text-3); margin-top: 4px; }
</style>
```

**`.vitepress/components/VideoDemo.vue`** — write this file exactly:

```vue
<template>
  <figure class="vd">
    <div class="vd__player">
      <video
        ref="videoEl"
        :src="src"
        :poster="poster"
        controls
        preload="metadata"
        @timeupdate="onTimeUpdate"
      >
        <track v-if="vtt" kind="captions" :src="vtt" default label="Steps" />
      </video>
    </div>
    <ol v-if="steps && steps.length" class="vd__steps">
      <li
        v-for="(step, i) in steps"
        :key="i"
        class="vd__step"
        :class="{ 'vd__step--active': activeStep === i }"
      >
        <span class="vd__num">{{ i + 1 }}</span>
        {{ step.caption }}
      </li>
    </ol>
    <figcaption v-if="caption" class="vd__caption">{{ caption }}</figcaption>
  </figure>
</template>

<script setup>
import { ref, onMounted } from 'vue'

const props = defineProps({
  src:     { type: String, required: true },
  poster:  { type: String, default: '' },
  vtt:     { type: String, default: '' },
  caption: { type: String, default: '' },
  steps:   { type: Array,  default: () => [] },
})

const videoEl    = ref(null)
const activeStep = ref(-1)

onMounted(() => {
  if (!videoEl.value || !props.vtt) return
  videoEl.value.addEventListener('loadedmetadata', () => {
    const tracks = videoEl.value.textTracks
    for (let i = 0; i < tracks.length; i++) {
      tracks[i].mode = 'showing'
    }
  })
})

const onTimeUpdate = () => {
  if (!props.steps.length || !videoEl.value) return
  const t = videoEl.value.currentTime
  let active = -1
  for (let i = 0; i < props.steps.length; i++) {
    if (t >= props.steps[i].time) active = i
  }
  activeStep.value = active
}
</script>

<style scoped>
.vd { margin: 1.5rem 0 2.5rem; }
.vd__player { border-radius: 8px; overflow: hidden; border: 1px solid var(--vp-c-divider); background: #000; }
.vd__player video { width: 100%; display: block; max-height: 500px; }
.vd__steps { margin: 12px 0 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 6px; }
.vd__step { display: flex; align-items: baseline; gap: 8px; font-size: 0.82rem; color: var(--vp-c-text-2); padding: 6px 10px; border-radius: 6px; border: 1px solid transparent; transition: all 0.2s; }
.vd__step--active { color: var(--vp-c-text-1); background: var(--vp-c-bg-soft); border-color: var(--vp-c-divider); }
.vd__num { font-size: 0.65rem; font-weight: 700; color: var(--vp-c-brand-1); background: var(--vp-c-brand-soft); width: 18px; height: 18px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; }
.vd__caption { font-size: 0.75rem; color: var(--vp-c-text-3); margin-top: 8px; text-align: center; }
</style>
```

**Install dependencies** in the docs folder:

```bash
cd DOCS_FOLDER && npm install medium-zoom highlight.js
```

**`.vitepress/theme/index.css`** — create if it doesn't exist, otherwise append:

```css
.medium-zoom-overlay {
  z-index: 100;
}

.medium-zoom-image--opened {
  z-index: 101;
}
```

**`.vitepress/theme/index.js`** — create if it doesn't exist, otherwise merge into existing. This template includes both `ApiEndpoint` registration and medium-zoom. If `apiBase` was not collected, omit the `ApiEndpoint` lines:

```js
import DefaultTheme from 'vitepress/theme'
import { onMounted, watch, nextTick } from 'vue'
import { useRoute } from 'vitepress'
import mediumZoom from 'medium-zoom'
import './index.css'
import ApiEndpoint from '../components/ApiEndpoint.vue'
import VideoDemo from '../components/VideoDemo.vue'

export default {
  extends: DefaultTheme,
  setup() {
    const route = useRoute()
    const initZoom = () => mediumZoom('.main img', { background: 'var(--vp-c-bg)' })
    onMounted(initZoom)
    watch(() => route.path, () => nextTick(initZoom))
  },
  enhanceApp({ app }) {
    app.component('ApiEndpoint', ApiEndpoint)
    app.component('VideoDemo', VideoDemo)
  }
}
```

If the file already exists, merge only the missing pieces — do not overwrite other registrations or setup logic.

### Step 6b — Update VitePress config sidebar

Read the existing `.vitepress/config.mjs` (or `.ts`) and update the `sidebar` and `nav` to include all newly generated pages. Edit the config in place — do not rewrite unrelated sections.

If `apiBase` was collected, also add it to `themeConfig`:

```js
themeConfig: {
  apiBase: 'API_BASE_URL',
  // ...existing themeConfig
}
```

### Step 7 — Write the manifest

Write `.vitepress/docs-manifest.json` capturing all pages, their source file mappings, image placeholder status, and a `syncHash` (SHA of source file contents at generation time — use a simple string hash if needed).

Add `docs-manifest.json` to `.vitepress/` entry in `.gitignore`.

### Step 8 — Summary

```
Generated X pages (Y user-facing, Z developer).
Created N placeholder screenshots — run /vitedocs:capture when ready to capture real images.
Filled M of P gaps — X remaining gap comments left in files for manual review.
```

---

## Placeholder generation

When a page needs a screenshot, generate a 1200×630 grey PNG at the specified path before moving on.

Try in order:

**Option A — ImageMagick:**
```bash
convert -size 1200x630 xc:'#888888' path/to/placeholder.png
```

**Option B — Node.js (no external deps):**

Write this to `/tmp/make-placeholder.mjs`, run it, then delete it:

```js
import zlib from 'zlib';
import { writeFileSync, mkdirSync } from 'fs';
import { dirname } from 'path';

const W = 1200, H = 630;
const OUTPUT = 'FILL_IN_OUTPUT_PATH';

const scanlines = [];
for (let y = 0; y < H; y++) {
  const row = Buffer.alloc(1 + W * 3);
  row[0] = 0;
  row.fill(0x88, 1);
  scanlines.push(row);
}
const raw = Buffer.concat(scanlines);
const compressed = zlib.deflateSync(raw, { level: 9 });

const crcTable = new Uint32Array(256);
for (let i = 0; i < 256; i++) {
  let c = i;
  for (let j = 0; j < 8; j++) c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);
  crcTable[i] = c;
}
function crc32(buf) {
  let c = 0xFFFFFFFF;
  for (const b of buf) c = crcTable[(c ^ b) & 0xFF] ^ (c >>> 8);
  return (c ^ 0xFFFFFFFF) >>> 0;
}
function makeChunk(type, data) {
  const t = Buffer.from(type, 'ascii');
  const len = Buffer.alloc(4); len.writeUInt32BE(data.length);
  const crcVal = Buffer.alloc(4); crcVal.writeUInt32BE(crc32(Buffer.concat([t, data])));
  return Buffer.concat([len, t, data, crcVal]);
}

const sig = Buffer.from([137,80,78,71,13,10,26,10]);
const ihdr = Buffer.alloc(13);
ihdr.writeUInt32BE(W, 0); ihdr.writeUInt32BE(H, 4);
ihdr[8] = 8; ihdr[9] = 2;

mkdirSync(dirname(OUTPUT), { recursive: true });
writeFileSync(OUTPUT, Buffer.concat([
  sig,
  makeChunk('IHDR', ihdr),
  makeChunk('IDAT', compressed),
  makeChunk('IEND', Buffer.alloc(0))
]));
console.log('Created', OUTPUT);
```

If both options fail, tell the user and skip the placeholder for that image — leave the `![...](path)` reference in the markdown so it renders as a broken image, making it obvious during preview.
