Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
8e768fc
feat(sdk): add LLM tools preset registry (SD-3128)
andrii-harbour May 27, 2026
595564b
Merge remote-tracking branch 'origin/main' into andrii/llm-tools-bundles
andrii-harbour May 27, 2026
b4cb0d9
Merge branch 'main' into andrii/llm-tools-bundles
andrii-harbour May 29, 2026
3385cad
fix: make toc toolbar icon configurable
VladaHarbour May 28, 2026
2a893ba
feat: add layered style export
May 26, 2026
af05f1c
feat(demo): smart-tags palette that inserts custom-styled SDT fields
caio-pizzol May 29, 2026
05f6cbb
feat(demo): amber token palette, document->palette sync, README
caio-pizzol May 29, 2026
de7cac4
fix(demo): unify SDT field look + kill hover/select jitter; drop the …
caio-pizzol May 29, 2026
195c670
feat: namespace internal ui classes (#3544)
artem-harbour May 29, 2026
63c71be
Merge pull request #3563 from superdoc-dev/artem/layer-css-v2
harbournick May 29, 2026
37a0e04
Merge remote-tracking branch 'origin/stable' into sync/stable-to-main…
github-actions[bot] May 29, 2026
989f060
feat(demo): contract-templates as a clause + field library on locked …
caio-pizzol May 29, 2026
29a5d2f
Merge remote-tracking branch 'origin/main' into andrii/llm-tools-bundles
andrii-harbour May 29, 2026
bfd0525
fix(sdk): address PR review for preset registry (SD-3128)
andrii-harbour May 29, 2026
955b2a3
refactor(demo): single-use clause library + brand-blue fields
caio-pizzol May 29, 2026
6c9ae00
Merge pull request #3574 from superdoc-dev/caio/contract-templates-sm…
caio-pizzol May 29, 2026
09d85ec
Merge branch 'main' into andrii/llm-tools-bundles
andrii-harbour May 29, 2026
fa9d733
Merge pull request #3582 from superdoc-dev/sync/stable-to-main-202605…
caio-pizzol May 29, 2026
64d9236
Merge pull request #3547 from superdoc-dev/sd-2751_configurable-toc-icon
harbournick May 29, 2026
4c51e5c
Merge pull request #3541 from superdoc-dev/andrii/llm-tools-bundles
harbournick May 29, 2026
1371d49
fix: nested replacement tracked-change decisions (#3557)
harbournick May 30, 2026
42c6d5f
Merge remote-tracking branch 'origin/main' into merge/main-into-stabl…
github-actions[bot] May 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,14 @@ const superdoc = new SuperDoc({
});
```

Optional layered CSS mode:

```css
@layer reset, superdoc, app;
@import 'superdoc/style.layered.css';
@import 'your-app.css' layer(app);
```

Or use the CDN:

```html
Expand Down
16 changes: 16 additions & 0 deletions apps/docs/editor/theming/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,22 @@ document.documentElement.classList.add(theme);

Five properties theme the entire UI.

## Optional layered stylesheet

Default usage remains:

```javascript
import 'superdoc/style.css';
```

If your application uses CSS cascade layers and you want explicit layer ordering, use:

```css
@layer reset, superdoc, app;
@import 'superdoc/style.layered.css';
@import 'your-app.css' layer(app);
```

## `createTheme()`

Pass a config object, get back a CSS class name. Apply it to `<html>`.
Expand Down
16 changes: 16 additions & 0 deletions apps/docs/getting-started/theming.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,22 @@ new SuperDoc({ selector: '#editor', document: 'contract.docx' });

Toolbar buttons, comments sidebar, dropdowns, context menu, search bar, dialog surfaces. Any chrome SuperDoc renders. The document content itself (paragraphs, headings, tables) renders with its own styles from the DOCX.

## Optional layered stylesheet

By default, you can keep using:

```javascript
import 'superdoc/style.css';
```

If your app uses cascade layers and you want SuperDoc styles in a named layer, use the optional layered entrypoint:

```css
@layer reset, superdoc, app;
@import 'superdoc/style.layered.css';
@import 'your-app.css' layer(app);
```

## When to drop to CSS variables

`createTheme()` covers the common case. For component-level overrides, use the `vars` option or set raw `--sd-*` variables in your stylesheet:
Expand Down
59 changes: 59 additions & 0 deletions apps/mcp/src/__tests__/server-preset-env.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* MCP_PRESET env var selects which LLM-tools preset the server registers.
* Currently only 'legacy' is supported. Unknown preset ids must fail fast at
* startup so misconfiguration is visible instead of silently falling back to
* the default.
*/

import { describe, expect, test } from 'bun:test';
import { spawn } from 'node:child_process';
import path from 'node:path';

const REPO_ROOT = path.resolve(import.meta.dir, '../../../..');
const MCP_ENTRY = path.join(REPO_ROOT, 'apps/mcp/src/server.ts');

type RunResult = { code: number | null; stderr: string };

function runServer(env: NodeJS.ProcessEnv, timeoutMs = 2000): Promise<RunResult> {
return new Promise((resolve) => {
const proc = spawn('bun', ['run', MCP_ENTRY], {
cwd: REPO_ROOT,
env: { ...process.env, ...env },
stdio: ['pipe', 'pipe', 'pipe'],
});

let stderr = '';
proc.stderr.on('data', (chunk) => {
stderr += chunk;
});

// The MCP server runs forever waiting on stdio. We only care about whether
// it exits fast (rejecting bad preset id) or stays alive (accepting preset).
// For the success case we kill after a short window.
const timer = setTimeout(() => {
proc.kill('SIGTERM');
}, timeoutMs);

proc.on('close', (code) => {
clearTimeout(timer);
resolve({ code, stderr });
});
});
}

describe('MCP_PRESET env var', () => {
test('unknown preset id fails fast with exit code 2', async () => {
const result = await runServer({ MCP_PRESET: 'definitely-not-a-preset' });
expect(result.code).toBe(2);
expect(result.stderr).toContain('unknown preset');
expect(result.stderr).toContain('definitely-not-a-preset');
expect(result.stderr).toContain('legacy');
});

test('explicit MCP_PRESET=legacy is accepted (server stays alive)', async () => {
const result = await runServer({ MCP_PRESET: 'legacy' });
// Server should still be running when we kill it (SIGTERM → code is null
// or signal-derived non-2). Either way, it must NOT exit with 2.
expect(result.code).not.toBe(2);
});
});
12 changes: 12 additions & 0 deletions apps/mcp/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,18 @@ import { registerAllTools } from './tools/index.js';
const require = createRequire(import.meta.url);
const { version } = require('../package.json');

// Validate MCP_PRESET at startup so misconfiguration fails fast instead of
// silently falling back to 'legacy'. Tool registration is wired to legacy via
// the static MCP_TOOL_CATALOG + dispatchIntentTool imports in tools/intent.ts;
// the resolved id is not plumbed further yet. When a non-legacy preset lands,
// pass the id into registerAllTools() so it can route through the registry.
const PRESETS_SUPPORTED = new Set(['legacy']);
const requestedPreset = process.env.MCP_PRESET ?? 'legacy';
if (!PRESETS_SUPPORTED.has(requestedPreset)) {
console.error(`SuperDoc MCP: unknown preset "${requestedPreset}". Supported: ${[...PRESETS_SUPPORTED].join(', ')}.`);
process.exit(2);
}

const server = new McpServer(
{
name: 'superdoc',
Expand Down
73 changes: 0 additions & 73 deletions demos/__tests__/contract-templates-chip-anchor.spec.ts

This file was deleted.

80 changes: 2 additions & 78 deletions demos/__tests__/contract-templates-focus.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ test('clicking a field Focus places the caret inside that control', async ({ pag
{ timeout: 30_000 },
);

// Fields tab is the default; the Focus buttons live on field rows.
// Field value rows (with the Focus buttons) live on the Values tab.
await page.click('.tab[data-tab="values"]');
await page.waitForSelector('[data-focus-field]');
const key = await page.getAttribute('[data-focus-field]', 'data-focus-field');
expect(key).toBeTruthy();
Expand Down Expand Up @@ -62,80 +63,3 @@ test('clicking a field Focus places the caret inside that control', async ({ pag
// After focus, the caret lands inside a control whose tag carries this key.
await expect.poll(controlKeyAtSelection, { timeout: 5_000 }).toBe(key);
});

test('focusing an off-screen clause scrolls it in AND lands the caret inside it', async ({ page }) => {
test.skip(process.env.DEMO !== 'contract-templates', 'contract-templates demo only');

await page.route('**/ingest.superdoc.dev/**', (r) =>
r.fulfill({ status: 204, contentType: 'application/json', body: '{}' }),
);
await page.goto('/');
await page.waitForFunction(
() => (window as any).__demo?.state?.ui?.contentControls?.getSnapshot()?.items?.length > 0,
null,
{ timeout: 30_000 },
);

// Clause Focus buttons live in the (initially hidden) clauses panel.
await page.click('.tab[data-tab="clauses"]');
await page.waitForSelector('[data-focus-clause]');

// Bottom-most block clause: its painted id + sectionId (= the button's data attr).
const target = await page.evaluate(() => {
const ui = (window as any).__demo.state.ui;
const blocks = ui.contentControls.getSnapshot().items.filter((i: any) => i.kind === 'block');
const last = blocks[blocks.length - 1];
let sectionId: string | null = null;
try {
sectionId = JSON.parse(last?.properties?.tag ?? '{}').sectionId ?? null;
} catch {
sectionId = null;
}
return { id: last?.id ?? null, sectionId };
});
expect(target.id).toBeTruthy();
expect(target.sectionId).toBeTruthy();

// Scroll to the top so the bottom clause starts off-screen.
await page.evaluate(() => {
let node: HTMLElement | null = document.querySelector('.presentation-editor__pages');
while (node && !(node.scrollHeight > node.clientHeight + 4)) node = node.parentElement;
if (node) node.scrollTop = 0;
else window.scrollTo(0, 0);
});

const state = () =>
page.evaluate((id) => {
// caret's containing control id
const ed = (window as any).__demo.superdoc.activeEditor;
const from = ed?.state?.selection?.from;
let caretIn: string | null = null;
if (typeof from === 'number') {
ed.state.doc.descendants((node: any, pos: number) => {
if (
(node.type.name === 'structuredContent' || node.type.name === 'structuredContentBlock') &&
from > pos &&
from < pos + node.nodeSize
) {
caretIn = String(node.attrs.id);
}
return true;
});
}
// is the control's painted element in the viewport?
const el = document.querySelector<HTMLElement>(`[data-sdt-id="${id}"]`);
const r = el?.getBoundingClientRect();
const inViewport = r ? r.top >= 0 && r.top <= window.innerHeight : false;
return { caretIn, inViewport };
}, target.id);

// Before focus: caret is not in the bottom clause and it's off-screen.
const before = await state();
expect(before.caretIn).not.toBe(target.id);
expect(before.inViewport).toBe(false);

await page.click(`[data-focus-clause="${target.sectionId}"]`);

// After focus: the control is scrolled into view AND the caret is inside it.
await expect.poll(state, { timeout: 6_000 }).toEqual({ caretIn: target.id, inViewport: true });
});
79 changes: 0 additions & 79 deletions demos/__tests__/contract-templates-locate.spec.ts

This file was deleted.

Loading
Loading