# Build your own review queue

Keep the editor toolbar and render your own tracked change queue. Each row can reveal, accept or reject its exact change, and reports failed decisions.

## Run

Requires Node.js 22 or newer. `npm install` then `npm run dev`. Open the localhost URL Vite prints. To check and build the project, run `npm run check` and `npm run build`.

Browser editor: 2.30.0. Direct dependencies are pinned in package.json.

## Try it

Accepting or rejecting a change removes its row and updates the open count. Export the DOCX to inspect the result.

The included public DOCX is the original homepage sample. It is not your uploaded document or the current edited demo state. Export a separate DOCX, then reopen it in Word or SuperDoc. Built-in and custom review examples use Suggesting mode. The application form fills text fields directly.

## Files

- package.json
- examples/review-custom/main.ts
- examples/review-custom/index.html
- examples/shared/browser.ts
- examples/shared/style.css
- vite.config.ts
- tsconfig.json
- public/SuperDoc.docx

## Continue

[Current documentation](https://docs.superdoc.dev/editor/custom-ui/tracked-changes/)
[Try the homepage demo](https://www.superdoc.dev/?doc=ai#editor)
[SuperDoc licensing](https://docs.superdoc.dev/editor/license/)

The shared browser.ts file contains the export button, status messages and CSS imports. Document setup and task logic are in main.ts.

## Complete source

### package.json

```json
{
  "name": "superdoc-review-custom",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "engines": {
    "node": ">=22"
  },
  "scripts": {
    "dev": "vite --open /examples/review-custom/",
    "build": "vite build",
    "check": "tsc --noEmit"
  },
  "dependencies": {
    "superdoc": "2.30.0"
  },
  "devDependencies": {
    "vite": "6.4.1",
    "typescript": "5.8.3"
  }
}
```

### examples/review-custom/main.ts

```javascript
import { SuperDoc } from 'superdoc';
import type { TrackChangesItem } from 'superdoc/ui';
import { element, enableExport, report, showError } from '../shared/browser';

const list = element<HTMLUListElement>('changes');
const count = element('count');
let pending = false;

const superdoc = new SuperDoc({
  selector: '#editor',
  document: '/SuperDoc.docx',
  documentMode: 'suggesting',
  user: { name: 'Example reviewer', email: 'reviewer@example.com' },
  ui: { toolbar: { container: '#toolbar', responsiveTo: 'container' }, comments: false },
  onReady: ({ superdoc: ready }) => {
    enableExport(ready);
    const review = ready.ui.trackChanges;
    const stop = review.observe(({ items, total, listStatus }) => {
      count.textContent = listStatus === 'ready' ? `${total} open changes` : 'Reading changes…';
      list.replaceChildren(...items.map(row));
    });
    window.addEventListener('pagehide', () => { stop(); ready.destroy(); }, { once: true });
  },
  onContentError: ({ error }) => showError(error),
  onException: ({ error }) => showError(error),
});

function row(change: TrackChangesItem) {
  const item = document.createElement('li');
  const label = document.createElement('p');
  label.textContent = [
    change.deletedText ? `Remove “${change.deletedText}”` : '',
    change.insertedText ? `Add “${change.insertedText}”` : '',
  ].filter(Boolean).join(' · ') || change.formattingDeltaSummary || `${change.type} change`;
  const author = document.createElement('small');
  author.textContent = change.author ? `By ${change.author}` : 'Author not provided';
  item.append(label);
  item.append(author);
  for (const action of ['Show', 'Accept', 'Reject'] as const) {
    const button = document.createElement('button');
    button.textContent = action;
    button.type = 'button';
    button.addEventListener('click', async () => {
      if (pending) return;
      pending = true;
      list.querySelectorAll('button').forEach(node => { node.disabled = true; });
      try {
        const review = superdoc.ui.trackChanges;
        const target = { id: change.id, story: change.address?.story };
        if (action === 'Show') {
          const result = await review.scrollTo(target);
          if (!result.success) throw new Error('Could not reveal this change.');
        } else {
          const result = await (action === 'Accept' ? review.acceptAsync(target) : review.rejectAsync(target));
          if (result === false || (result !== true && !result.success)) {
            throw new Error('Review decision failed. Check the document before trying again.');
          }
          report(action === 'Accept' ? 'Change accepted. Export to check the saved DOCX.' : 'Change rejected. Export to check the saved DOCX.');
        }
      } catch (error) { showError(error); }
      finally {
        pending = false;
        list.querySelectorAll('button').forEach(node => { node.disabled = false; });
      }
    });
    item.append(button);
  }
  return item;
}
```

### examples/review-custom/index.html

```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Build your own review queue | SuperDoc example</title>
<meta name="description" content="Build your own review queue using SuperDoc and the homepage sample.">
<link rel="canonical" href="https://www.superdoc.dev/examples/review-custom/index.html">
<link rel="alternate" type="text/markdown" href="https://www.superdoc.dev/examples/review-custom.md">
</head>
<body>
<header>
<h1>Build your own review queue</h1>
<p>Keep the editor toolbar and render your own tracked change queue. Each row can reveal, accept or reject its exact change, and reports failed decisions.</p>
<div class="actions">
<button id="export" type="button" disabled>Export DOCX</button>
<output id="status" role="status">Opening document…</output>
</div>
<p>
<a href="https://docs.superdoc.dev/editor/custom-ui/tracked-changes/">Current documentation</a> · <a href="https://www.superdoc.dev/examples/review-custom.zip">Download this project</a> · <a href="https://www.superdoc.dev/examples/review-custom.md">Read the complete source</a>
</p>
</header>
<div id="toolbar">
</div>
<main>
<div id="editor">
</div>
<aside aria-labelledby="review-heading">
<h2 id="review-heading">Your review queue</h2>
<p id="count">Reading changes…</p>
<ul id="changes">
</ul>
</aside>
</main>
<script type="module" src="./main.ts">
</script>
</body>
</html>
```

### examples/shared/browser.ts

```javascript
import type { SuperDoc } from 'superdoc';
import 'superdoc/style.css';
import './style.css';

export function element<T extends HTMLElement>(id: string): T {
  const node = document.getElementById(id);
  if (!node) throw new Error(`Missing element: ${id}`);
  return node as T;
}

const status = element<HTMLOutputElement>('status');
const exportButton = element<HTMLButtonElement>('export');
export function report(message: string) { status.value = message; }
export function showError(error: unknown) {
  report(error instanceof Error ? error.message : String(error));
}

export function enableExport(superdoc: SuperDoc) {
  report('Ready. Edit the document, then export a copy.');
  exportButton.disabled = false;
  exportButton.addEventListener('click', async () => {
    exportButton.disabled = true;
    try {
      await superdoc.export({ exportType: ['docx'], exportedName: 'edited-example', triggerDownload: true });
      report('DOCX exported. Open the saved file to check your changes.');
    } catch (error) { showError(error); }
    finally { exportButton.disabled = false; }
  });
}
```

### examples/shared/style.css

```css
:root { font-family: system-ui, sans-serif; color: #242424; background: #f5f5f5; }
#editor { --sd-caret-color: #242424; }
input, textarea, [contenteditable="true"] { caret-color: #242424; }
* { box-sizing: border-box; }
body { margin: 0; }
header { padding: 1rem; border-bottom: 1px solid #ddd; background: white; }
h1 { font-size: 1.2rem; margin: 0 0 .5rem; }
p { line-height: 1.5; }
button, input { font: inherit; padding: .5rem .7rem; min-height: 44px; }
button { cursor: pointer; }
button:disabled { cursor: wait; }
label { display: grid; gap: .3rem; margin-bottom: .7rem; }
.actions { display: flex; flex-wrap: wrap; align-items: center; gap: .7rem; }
main { display: grid; grid-template-columns: minmax(0, 1fr) 300px; }
main.solo { display: block; }
#editor { height: 76vh; overflow: auto; min-width: 0; }
aside { padding: 1rem; background: white; overflow: auto; max-height: 76vh; }
ul { list-style: none; padding: 0; }
li { border-bottom: 1px solid #ddd; padding: .7rem 0; }
li button { margin: .2rem; }
input { width: 100%; }
@media (max-width: 760px) { main { grid-template-columns: minmax(0, 1fr); } aside { max-height: none; } }
```

### vite.config.ts

```javascript
import { defineConfig } from 'vite';
import { resolve } from 'node:path';
export default defineConfig({
  optimizeDeps: { exclude: ['@superdoc/docx-engine'], include: ['vue'] },
  resolve: { dedupe: ['vue'] },
  build: { rollupOptions: { input: resolve('examples/review-custom/index.html') } },
});
```

### tsconfig.json

```json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "lib": [
      "ES2022",
      "DOM",
      "DOM.Iterable"
    ],
    "strict": true,
    "skipLibCheck": true,
    "noEmit": true,
    "types": []
  },
  "include": [
    "examples/**/*.ts"
  ]
}
```

[Download the project](https://www.superdoc.dev/examples/review-custom.zip)
[Download the public sample](https://www.superdoc.dev/SuperDoc.docx)
