diff --git a/CLAUDE.md b/CLAUDE.md
index ed981697a1..9ce8fb6110 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -67,6 +67,10 @@ tests/visual/ Visual regression tests (Playwright + R2 baselines)
| Visual regression tests | `tests/visual/` (see its CLAUDE.md) |
| Document API contract | `packages/document-api/src/contract/operation-definitions.ts` |
| Adding a doc-api operation | See `packages/document-api/README.md` § "Adding a new operation" |
+| Theming (`createTheme()`) | `packages/superdoc/src/core/theme/create-theme.js` |
+| CSS variable defaults | `packages/superdoc/src/assets/styles/helpers/variables.css` |
+| Preset themes | `packages/superdoc/src/assets/styles/helpers/themes.css` |
+| Consumer-facing agent guide | `packages/superdoc/AGENTS.md` (ships with npm package) |
## Style Resolution Boundary
@@ -193,12 +197,15 @@ Pixel-level before/after comparison for documents that failed layout comparison.
## Brand & Design System
-Brand guidelines, voice, and design tokens live in `brand/`. Token values are defined in `packages/superdoc/src/assets/styles/tokens.css`.
+Brand guidelines, voice, and design tokens live in `brand/`.
+Token contract source is `packages/superdoc/src/assets/styles/helpers/variables.css` (`:root` defaults).
+Preset theme overrides are defined in `packages/superdoc/src/assets/styles/helpers/themes.css`.
**When creating or modifying UI components:**
-- Use `--sd-*` CSS custom properties — never hardcode hex values. See `tokens.css` for all available variables.
-- Tokens follow three tiers: primitive (`--sd-color-blue-500`) → semantic (`--sd-action-primary`) → component (`--sd-comment-bg`). Components reference semantic or component-level variables.
-- Expose component-specific variables as `--sd-{component}-*` so consumers can customize via CSS.
-- Document component CSS variables in `apps/docs/ui-components/` (Mintlify docs).
+- Use `--sd-*` CSS custom properties — never hardcode hex values.
+- Treat `variables.css` as the canonical token contract; add new tokens there.
+- Keep preset themes in `themes.css` (`.sd-theme-*`) and override only the tokens that need theme-specific values.
+- Tokens are organized by layers: primitive (`--sd-color-blue-500`) → UI/document tokens (`--sd-ui-*`, `--sd-comments-*`, etc.) → component usage.
+- Expose UI component-specific variables as `--sd-ui-{component}-*` so consumers can customize via CSS.
**When writing copy or content:** see `brand/brand-guidelines.md` for voice, tone, and the dual-register pattern (developer vs. leader). Product name is always **SuperDoc** (capital S, capital D).
diff --git a/apps/docs/docs.json b/apps/docs/docs.json
index c9e5cebb58..9de76d9c90 100644
--- a/apps/docs/docs.json
+++ b/apps/docs/docs.json
@@ -65,7 +65,8 @@
]
},
"getting-started/import-export",
- "getting-started/fonts"
+ "getting-started/fonts",
+ "getting-started/theming"
]
},
{
@@ -238,6 +239,7 @@
"pages": [
"guides/general/storage",
"guides/general/stable-navigation",
+ "guides/general/custom-themes",
{
"group": "Collaboration",
"pages": [
@@ -251,7 +253,8 @@
"pages": [
"guides/migration/prosemirror",
"guides/migration/breaking-changes-v1",
- "guides/migration/typescript-migration"
+ "guides/migration/typescript-migration",
+ "guides/migration/css-variables"
]
}
]
diff --git a/apps/docs/getting-started/theming.mdx b/apps/docs/getting-started/theming.mdx
new file mode 100644
index 0000000000..98500f1949
--- /dev/null
+++ b/apps/docs/getting-started/theming.mdx
@@ -0,0 +1,130 @@
+---
+title: Theming
+sidebarTitle: Theming
+keywords: "theming, css variables, custom theme, dark mode, design tokens, branding, css customization, createTheme, styling"
+---
+
+Set your brand colors in JavaScript and every SuperDoc component updates — toolbar, comments, dropdowns, everything.
+
+```javascript
+import { createTheme } from 'superdoc';
+
+const theme = createTheme({
+ colors: { action: '#6366f1', bg: '#ffffff', text: '#1e293b', border: '#e2e8f0' },
+ font: 'Inter, sans-serif',
+});
+
+document.documentElement.classList.add(theme);
+```
+
+Five properties theme the entire UI.
+
+## `createTheme()`
+
+Pass a config object, get back a CSS class name. Apply it to ``.
+
+```javascript
+import { createTheme } from 'superdoc';
+
+const theme = createTheme({
+ name: 'my-brand', // optional — generates sd-theme-my-brand
+ font: 'Inter, sans-serif',
+ radius: '8px',
+ colors: {
+ action: '#6366f1', // buttons, links, active states
+ actionHover: '#4f46e5',
+ bg: '#ffffff', // panels, cards, dropdowns
+ text: '#1e293b', // primary text
+ textMuted: '#64748b', // secondary text
+ border: '#e2e8f0', // all borders
+ },
+ // Escape hatch — any CSS variable
+ vars: {
+ '--sd-ui-toolbar-bg': '#f8fafc',
+ '--sd-ui-comments-card-bg': '#f0f0ff',
+ },
+});
+
+document.documentElement.classList.add(theme);
+```
+
+`colors` controls the semantic tier — every component inherits from it. The `vars` escape hatch lets you set any `--sd-*` CSS variable directly for fine-grained control.
+
+## SSR support
+
+Use `buildTheme()` to get the raw CSS string for server-side rendering:
+
+```javascript
+import { buildTheme } from 'superdoc';
+
+const { className, css } = buildTheme({
+ colors: { action: '#6366f1', bg: '#ffffff', text: '#1e293b' },
+});
+
+const html = `
+
+
+
+
+`;
+```
+
+## Preset themes
+
+Three presets ship out of the box. Add the class to `` — some SuperDoc elements (popovers, dropdowns) are appended to ``, so they need to inherit from ``.
+
+
+
+ Google Docs aesthetic. Clean blues, compact toolbar, subtle shadows.
+ ```html
+
+ ```
+
+
+ Microsoft Word aesthetic. Fluent-style borders and surfaces.
+ ```html
+
+ ```
+
+
+ High-contrast technical preset. Teal accents, structured layout.
+ ```html
+
+ ```
+
+
+
+## CSS variables (advanced)
+
+`createTheme()` generates CSS variables under the hood. You can also set them directly:
+
+```css
+:root {
+ --sd-ui-action: #6366f1;
+ --sd-ui-bg: #ffffff;
+ --sd-ui-text: #1e293b;
+ --sd-ui-border: #e2e8f0;
+ --sd-ui-font-family: Inter, sans-serif;
+}
+```
+
+See the [full variable reference](/guides/general/custom-themes#variable-reference) for every token.
+
+## Next steps
+
+
+
+ Full API reference, dark theme starter, all CSS variables by component.
+
+
+ Old-to-new variable name mapping. Backward compatibility details.
+
+
diff --git a/apps/docs/guides/general/custom-themes.mdx b/apps/docs/guides/general/custom-themes.mdx
new file mode 100644
index 0000000000..ed091fad27
--- /dev/null
+++ b/apps/docs/guides/general/custom-themes.mdx
@@ -0,0 +1,331 @@
+---
+title: Custom themes
+sidebarTitle: Custom themes
+keywords: "custom theme, css variables reference, design tokens, theming guide, component styling, dark theme"
+---
+
+Full `createTheme()` API reference and CSS variable catalog. Start with the [theming overview](/getting-started/theming) if you're new to theming.
+
+## `createTheme()` options
+
+```javascript
+import { createTheme } from 'superdoc';
+
+const theme = createTheme({
+ name: 'my-brand',
+ font: 'Inter, sans-serif',
+ radius: '8px',
+ shadow: '0 2px 8px rgba(0,0,0,0.1)',
+ colors: {
+ action: '#6366f1', // buttons, links, active states
+ actionHover: '#4f46e5',
+ actionText: '#ffffff', // text on action buttons
+ bg: '#ffffff', // panels, cards, dropdowns
+ hoverBg: '#f1f5f9', // hover backgrounds
+ activeBg: '#e2e8f0', // active/pressed backgrounds
+ disabledBg: '#f5f5f5',
+ text: '#1e293b', // primary text
+ textMuted: '#64748b', // secondary text
+ textDisabled: '#94a3b8',
+ border: '#e2e8f0',
+ },
+ // Escape hatch — any --sd-* CSS variable
+ vars: {
+ '--sd-ui-toolbar-bg': '#f8fafc',
+ '--sd-ui-comments-card-bg': '#f0f0ff',
+ },
+});
+
+document.documentElement.classList.add(theme);
+```
+
+
+Start with `colors` only. Use `vars` when a specific component needs to look different.
+
+
+## Dark theme example
+
+```javascript
+import { createTheme } from 'superdoc';
+
+const dark = createTheme({
+ name: 'dark',
+ colors: {
+ bg: '#1a1a2e',
+ hoverBg: '#2a2a3e',
+ activeBg: '#3a3a4e',
+ text: '#e2e8f0',
+ textMuted: '#94a3b8',
+ border: '#334155',
+ action: '#60a5fa',
+ actionHover: '#93c5fd',
+ actionText: '#0f172a',
+ },
+ vars: {
+ '--sd-ui-toolbar-bg': '#0f172a',
+ '--sd-ui-toolbar-button-text': '#e2e8f0',
+ '--sd-ui-comments-card-bg': '#1e293b',
+ '--sd-ui-comments-body-text': '#cbd5e1',
+ },
+});
+
+document.documentElement.classList.add(dark);
+```
+
+## Raw CSS alternative
+
+You can also write CSS directly. Apply overrides to ``.
+
+```css
+.dark-theme {
+ --sd-ui-bg: #1a1a2e;
+ --sd-ui-hover-bg: #2a2a3e;
+ --sd-ui-active-bg: #3a3a4e;
+ --sd-ui-text: #e2e8f0;
+ --sd-ui-text-muted: #94a3b8;
+ --sd-ui-border: #334155;
+ --sd-ui-action: #60a5fa;
+ --sd-ui-action-hover: #93c5fd;
+ --sd-ui-action-text: #0f172a;
+
+ /* Toolbar */
+ --sd-ui-toolbar-bg: #0f172a;
+ --sd-ui-toolbar-button-text: #e2e8f0;
+ --sd-ui-toolbar-button-hover-bg: #1e293b;
+ --sd-ui-toolbar-button-active-bg: #334155;
+
+ /* Dropdowns */
+ --sd-ui-dropdown-bg: #1e293b;
+ --sd-ui-dropdown-border: #334155;
+ --sd-ui-dropdown-hover-bg: #2d3a4e;
+
+ /* Comments */
+ --sd-ui-comments-card-bg: #1e293b;
+ --sd-ui-comments-card-active-bg: #0f172a;
+ --sd-ui-comments-input-bg: #1e293b;
+ --sd-ui-comments-author-text: #e2e8f0;
+ --sd-ui-comments-body-text: #cbd5e1;
+ --sd-ui-comments-timestamp-text: #64748b;
+}
+```
+
+## Variable reference
+
+### Semantic (UI-wide)
+
+These cascade to every component. Changing one updates the entire UI.
+
+| Variable | Default | Controls |
+|----------|---------|----------|
+| `--sd-ui-font-family` | `Arial, Helvetica, sans-serif` | UI font |
+| `--sd-ui-text` | `#47484a` | Primary text |
+| `--sd-ui-text-muted` | `#666666` | Secondary text |
+| `--sd-ui-text-disabled` | `#ababab` | Disabled text |
+| `--sd-ui-bg` | `#ffffff` | Default background |
+| `--sd-ui-hover-bg` | `#dbdbdb` | Hover background |
+| `--sd-ui-active-bg` | `#c8d0d8` | Active/pressed background |
+| `--sd-ui-disabled-bg` | `#f5f5f5` | Disabled background |
+| `--sd-ui-border` | `#dbdbdb` | Default border |
+| `--sd-ui-action` | `#1355ff` | Action/accent color |
+| `--sd-ui-action-hover` | `#0f44cc` | Action hover |
+| `--sd-ui-action-text` | `#ffffff` | Text on action buttons |
+| `--sd-ui-shadow` | `0 4px 12px rgba(0,0,0,0.12)` | Default shadow |
+| `--sd-ui-radius` | `6px` | Default border radius |
+
+### Toolbar
+
+| Variable | Default | Controls |
+|----------|---------|----------|
+| `--sd-ui-toolbar-height` | `32px` | Row height |
+| `--sd-ui-toolbar-padding-x` | `16px` | Horizontal padding |
+| `--sd-ui-toolbar-padding-y` | `4px` | Vertical padding |
+| `--sd-ui-toolbar-item-gap` | `2px` | Gap between buttons |
+| `--sd-ui-toolbar-item-padding` | `5px` | Button padding |
+| `--sd-ui-toolbar-bg` | `transparent` | Background |
+| `--sd-ui-toolbar-button-text` | inherits `--sd-ui-text` | Button text/icon color |
+| `--sd-ui-toolbar-button-hover-bg` | inherits `--sd-ui-hover-bg` | Button hover |
+| `--sd-ui-toolbar-button-active-bg` | inherits `--sd-ui-active-bg` | Button active |
+
+### Dropdowns
+
+Toolbar dropdowns (font picker, style picker) and the overflow menu.
+
+| Variable | Default | Controls |
+|----------|---------|----------|
+| `--sd-ui-dropdown-bg` | inherits `--sd-ui-bg` | Panel background |
+| `--sd-ui-dropdown-border` | inherits `--sd-ui-border` | Panel border |
+| `--sd-ui-dropdown-text` | inherits `--sd-ui-text` | Option text |
+| `--sd-ui-dropdown-hover-text` | inherits dropdown text | Hovered option text |
+| `--sd-ui-dropdown-selected-text` | inherits dropdown text | Selected option text |
+| `--sd-ui-dropdown-hover-bg` | inherits `--sd-ui-hover-bg` | Option hover |
+| `--sd-ui-dropdown-active-bg` | inherits `--sd-ui-active-bg` | Option active |
+| `--sd-ui-dropdown-option-radius` | `3px` | Option border radius |
+| `--sd-ui-dropdown-shadow` | `0 8px 24px rgba(0,0,0,0.16)` | Panel shadow |
+
+### Context menu
+
+The right-click menu.
+
+| Variable | Default | Controls |
+|----------|---------|----------|
+| `--sd-ui-menu-bg` | inherits `--sd-ui-bg` | Background |
+| `--sd-ui-menu-text` | inherits `--sd-ui-text` | Text |
+| `--sd-ui-menu-font-size` | `12px` | Text size |
+| `--sd-ui-menu-radius` | `0` | Border radius |
+| `--sd-ui-menu-border` | inherits `--sd-ui-border` | Border |
+| `--sd-ui-menu-shadow` | `0 0 0 1px rgba(0,0,0,0.05), 0 10px 20px rgba(0,0,0,0.1)` | Shadow |
+| `--sd-ui-menu-input-border` | inherits `--sd-ui-border` | Search input border |
+| `--sd-ui-menu-input-focus-border` | inherits `--sd-ui-action` | Search input focus |
+| `--sd-ui-menu-item-hover-bg` | inherits `--sd-ui-hover-bg` | Item hover |
+| `--sd-ui-menu-item-active-bg` | inherits `--sd-ui-active-bg` | Active item |
+| `--sd-ui-menu-item-active-text` | inherits `--sd-ui-action` | Active item text |
+
+### Tooltip
+
+| Variable | Default | Controls |
+|----------|---------|----------|
+| `--sd-ui-tooltip-bg` | `#262626` | Background (intentionally dark) |
+| `--sd-ui-tooltip-text` | `#ffffff` | Text (intentionally light) |
+| `--sd-ui-tooltip-radius` | `6px` | Border radius |
+| `--sd-ui-tooltip-shadow` | `0 3px 12px rgba(0,0,0,0.28)` | Shadow |
+
+### Comments
+
+Card styling, typography, and input elements for the comments sidebar.
+
+**Card:**
+
+| Variable | Default | Controls |
+|----------|---------|----------|
+| `--sd-ui-comments-card-bg` | `#f3f6fd` | Default card |
+| `--sd-ui-comments-card-hover-bg` | inherits card bg | Hovered card |
+| `--sd-ui-comments-card-active-bg` | inherits `--sd-ui-bg` | Selected card |
+| `--sd-ui-comments-card-resolved-bg` | inherits `--sd-ui-disabled-bg` | Resolved card |
+| `--sd-ui-comments-card-active-border` | inherits `--sd-ui-border` | Selected card border |
+| `--sd-ui-comments-card-radius` | `12px` | Card radius |
+| `--sd-ui-comments-card-padding` | `16px` | Card padding |
+| `--sd-ui-comments-card-shadow` | `0 4px 12px rgba(50,50,50,0.15)` | Card shadow |
+| `--sd-ui-comments-transition` | `all 200ms ease` | Animation |
+| `--sd-ui-comments-separator` | inherits `--sd-ui-border` | Divider line |
+
+**Text:**
+
+| Variable | Default | Controls |
+|----------|---------|----------|
+| `--sd-ui-comments-author-text` | inherits `--sd-ui-text` | Author name |
+| `--sd-ui-comments-author-size` | `14px` | Author size |
+| `--sd-ui-comments-author-weight` | `600` | Author weight |
+| `--sd-ui-comments-timestamp-text` | inherits `--sd-ui-text-muted` | Timestamp |
+| `--sd-ui-comments-timestamp-size` | `12px` | Timestamp size |
+| `--sd-ui-comments-body-text` | inherits `--sd-ui-text` | Body text |
+| `--sd-ui-comments-body-size` | `14px` | Body size |
+
+**Input:**
+
+| Variable | Default | Controls |
+|----------|---------|----------|
+| `--sd-ui-comments-input-bg` | inherits `--sd-ui-bg` | Input background |
+| `--sd-ui-comments-input-border` | inherits `--sd-ui-border` | Input border |
+
+**Status:**
+
+| Variable | Default | Controls |
+|----------|---------|----------|
+| `--sd-ui-comments-resolved-text` | `#00853d` | "Resolved" badge |
+| `--sd-ui-comments-insert-text` | `#00853d` | Tracked change insert |
+| `--sd-ui-comments-delete-text` | `#cb0e47` | Tracked change delete |
+
+**Filter dropdown:**
+
+| Variable | Default | Controls |
+|----------|---------|----------|
+| `--sd-ui-comments-dropdown-bg` | inherits `--sd-ui-bg` | Dropdown background |
+| `--sd-ui-comments-dropdown-border` | inherits `--sd-ui-border` | Dropdown border |
+| `--sd-ui-comments-dropdown-shadow` | `0 8px 24px rgba(0,0,0,0.12)` | Dropdown shadow |
+| `--sd-ui-comments-option-text` | inherits body text | Option text |
+| `--sd-ui-comments-option-hover-text` | inherits option text | Hovered option |
+| `--sd-ui-comments-option-hover-bg` | inherits `--sd-ui-hover-bg` | Option hover |
+| `--sd-ui-comments-option-size` | `14px` | Option size |
+
+**Visibility badges:**
+
+| Variable | Default | Controls |
+|----------|---------|----------|
+| `--sd-ui-comments-internal-bg` | `#cde6e6` | Internal badge |
+| `--sd-ui-comments-external-bg` | `#f5cfda` | External badge |
+
+### Comment highlights (in-document)
+
+Colors applied to highlighted text when comments are present. These are document-level, not UI chrome.
+
+| Variable | Default | Controls |
+|----------|---------|----------|
+| `--sd-comments-highlight-external` | `#b1124b40` | External highlight |
+| `--sd-comments-highlight-external-active` | `#b1124b66` | Active external |
+| `--sd-comments-highlight-external-faded` | `#b1124b20` | Faded external |
+| `--sd-comments-highlight-external-nested-border` | `#b1124b99` | Nested left border |
+| `--sd-comments-highlight-internal` | `#07838340` | Internal highlight |
+| `--sd-comments-highlight-internal-active` | `#07838366` | Active internal |
+| `--sd-comments-highlight-internal-faded` | `#07838320` | Faded internal |
+| `--sd-comments-highlight-internal-nested-border` | `#07838399` | Nested left border |
+| `--sd-comments-highlight-hover` | `#1354ff55` | Hover highlight |
+| `--sd-comments-selection-background` | `#1354ff55` | Selection highlight |
+
+### Tracked changes
+
+Insert, delete, and format change decorations in the document.
+
+| Variable | Default | Controls |
+|----------|---------|----------|
+| `--sd-tracked-changes-insert-background` | `#399c7222` | Insert highlight |
+| `--sd-tracked-changes-insert-border` | `#00853d` | Insert left border |
+| `--sd-tracked-changes-delete-background` | `#cb0e4722` | Delete highlight |
+| `--sd-tracked-changes-delete-border` | `#cb0e47` | Delete left border |
+| `--sd-tracked-changes-format-border` | `gold` | Format change border |
+| `--sd-tracked-changes-insert-background-focused` | `#399c7244` | Focused insert |
+| `--sd-tracked-changes-delete-background-focused` | `#cb0e4744` | Focused delete |
+| `--sd-tracked-changes-format-background-focused` | `#ffd70033` | Focused format change |
+
+### Content controls
+
+DOCX content controls (SDTs) — form fields, dropdowns, date pickers.
+
+| Variable | Default | Controls |
+|----------|---------|----------|
+| `--sd-content-controls-block-border` | `#629be7` | Block control border |
+| `--sd-content-controls-block-hover-bg` | inherits `--sd-ui-hover-bg` | Block hover |
+| `--sd-content-controls-inline-border` | `#629be7` | Inline control border |
+| `--sd-content-controls-inline-hover-bg` | inherits `--sd-ui-hover-bg` | Inline hover |
+| `--sd-content-controls-label-border` | `#629be7` | Label border |
+| `--sd-content-controls-label-bg` | `#629be7ee` | Label background |
+| `--sd-content-controls-label-text` | `#ffffff` | Label text |
+| `--sd-content-controls-lock-hover-bg` | `rgba(98,155,231,0.08)` | Locked control hover |
+
+### Layout
+
+Page appearance in presentation mode.
+
+| Variable | Default | Controls |
+|----------|---------|----------|
+| `--sd-layout-page-bg` | inherits `--sd-ui-bg` | Page background |
+| `--sd-layout-page-shadow` | `0 4px 20px rgba(15,23,42,0.08)` | Page shadow |
+
+### Tools
+
+The floating action toolbar for AI and insert actions.
+
+| Variable | Default | Controls |
+|----------|---------|----------|
+| `--sd-ui-tools-gap` | `6px` | Gap between items |
+| `--sd-ui-tools-item-size` | `50px` | Button size |
+| `--sd-ui-tools-item-radius` | `12px` | Button radius |
+| `--sd-ui-tools-item-bg` | `rgba(219,219,219,0.6)` | Button background |
+| `--sd-ui-tools-icon-size` | `20px` | Icon size |
+
+## Source files
+
+Token definitions live in your `node_modules/superdoc/` package:
+
+- `src/assets/styles/helpers/variables.css` — defaults
+- `src/assets/styles/helpers/themes.css` — preset themes
+- `src/assets/styles/helpers/compat.css` — aliases for old names that map to a different concept
diff --git a/apps/docs/guides/migration/css-variables.mdx b/apps/docs/guides/migration/css-variables.mdx
new file mode 100644
index 0000000000..043bb52e7f
--- /dev/null
+++ b/apps/docs/guides/migration/css-variables.mdx
@@ -0,0 +1,163 @@
+---
+title: CSS variable migration
+sidebarTitle: CSS variables
+keywords: "migration, css variables, renamed variables, breaking changes, backward compatibility, tokens"
+---
+
+SuperDoc's CSS variables were restructured into a three-tier token system. This page maps every old name to its new equivalent.
+
+
+**Old names still work.** New tokens fall back to old names automatically. You don't need to change your code immediately.
+
+
+## What changed
+
+The old system used flat names (`--sd-comment-bg`, `--sd-track-insert-border`). The new system uses a structured pattern:
+
+```
+--sd-{scope}-{component}-{element?}-{state?}-{property}
+
+Old: --sd-comment-bg
+New: --sd-ui-comments-card-bg
+```
+
+The JavaScript config API is unchanged. Only CSS variable names moved.
+
+## Full mapping
+
+### Surfaces
+
+| Old | New |
+|-----|-----|
+| `--sd-surface-page` | `--sd-ui-bg` |
+| `--sd-surface-canvas` | `--sd-color-gray-50` |
+| `--sd-surface-card` | `--sd-ui-bg` |
+| `--sd-surface-muted` | `--sd-ui-disabled-bg` |
+| `--sd-surface-hover` | `--sd-ui-hover-bg` |
+| `--sd-surface-selected` | `--sd-color-blue-100` |
+
+### Text
+
+| Old | New |
+|-----|-----|
+| `--sd-text-primary` | `--sd-ui-text` |
+| `--sd-text-secondary` | `--sd-color-gray-700` |
+| `--sd-text-muted` | `--sd-ui-text-muted` |
+| `--sd-text-placeholder` | `--sd-ui-text-disabled` |
+
+### Borders
+
+| Old | New |
+|-----|-----|
+| `--sd-border-default` | `--sd-ui-border` |
+| `--sd-border-subtle` | `--sd-ui-comments-separator` |
+| `--sd-border-focus` | `--sd-ui-action` |
+
+### Actions
+
+| Old | New |
+|-----|-----|
+| `--sd-action-primary` | `--sd-ui-action` |
+| `--sd-action-primary-hover` | `--sd-ui-action-hover` |
+
+### Typography and radius
+
+| Old | New |
+|-----|-----|
+| `--sd-font-family` | `--sd-ui-font-family` |
+| `--sd-radius-sm` | `--sd-radius-50` |
+| `--sd-radius-md` | `--sd-radius-200` |
+| `--sd-radius-lg` | `--sd-radius-300` |
+
+### Comment card
+
+| Old | New |
+|-----|-----|
+| `--sd-comment-bg` | `--sd-ui-comments-card-bg` |
+| `--sd-comment-bg-hover` | `--sd-ui-comments-card-hover-bg` |
+| `--sd-comment-bg-active` | `--sd-ui-comments-card-active-bg` |
+| `--sd-comment-bg-resolved` | `--sd-ui-comments-card-resolved-bg` |
+| `--sd-comment-border-active` | `--sd-ui-comments-card-active-border` |
+| `--sd-comment-radius` | `--sd-ui-comments-card-radius` |
+| `--sd-comment-padding` | `--sd-ui-comments-card-padding` |
+| `--sd-comment-shadow` | `--sd-ui-comments-card-shadow` |
+| `--sd-comment-separator` | `--sd-ui-comments-separator` |
+| `--sd-comment-transition` | `--sd-ui-comments-transition` |
+
+### Comment text
+
+| Old | New |
+|-----|-----|
+| `--sd-comment-author-color` | `--sd-ui-comments-author-text` |
+| `--sd-comment-author-size` | `--sd-ui-comments-author-size` |
+| `--sd-comment-author-weight` | `--sd-ui-comments-author-weight` |
+| `--sd-comment-time-color` | `--sd-ui-comments-timestamp-text` |
+| `--sd-comment-time-size` | `--sd-ui-comments-timestamp-size` |
+| `--sd-comment-body-size` | `--sd-ui-comments-body-size` |
+| `--sd-comment-tc-insert-color` | `--sd-ui-comments-insert-text` |
+| `--sd-comment-tc-delete-color` | `--sd-ui-comments-delete-text` |
+
+### Comment highlights
+
+| Old | New |
+|-----|-----|
+| `--sd-comment-highlight-internal` | `--sd-comments-highlight-internal` |
+| `--sd-comment-highlight-external` | `--sd-comments-highlight-external` |
+| `--sd-comment-internal-bg` | `--sd-ui-comments-internal-bg` |
+| `--sd-comment-external-bg` | `--sd-ui-comments-external-bg` |
+
+
+`--sd-comment-highlight-opacity` and `--sd-comment-highlight-opacity-active` were removed. The new system uses pre-computed RGBA colors (`--sd-comments-highlight-external: #b1124b40`). Override the full value directly.
+
+
+### Tracked changes
+
+| Old | New |
+|-----|-----|
+| `--sd-track-insert-border` | `--sd-tracked-changes-insert-border` |
+| `--sd-track-insert-bg` | `--sd-tracked-changes-insert-background` |
+| `--sd-track-delete-border` | `--sd-tracked-changes-delete-border` |
+| `--sd-track-delete-bg` | `--sd-tracked-changes-delete-background` |
+| `--sd-track-format-border` | `--sd-tracked-changes-format-border` |
+
+### Removed
+
+These have no replacement:
+
+| Variable | Why |
+|----------|-----|
+| `--sd-comment-highlight-opacity` | Replaced by pre-computed RGBA values |
+| `--sd-comment-highlight-opacity-active` | Replaced by pre-computed RGBA values |
+| `--sd-comment-max-width` | Managed internally |
+| `--sd-comment-min-width` | Managed internally |
+| `--sd-comment-group-bg` | Group bubble styling removed |
+| `--sd-comment-group-color` | Group bubble styling removed |
+| `--sd-comment-group-size` | Group bubble styling removed |
+| `--sd-comment-group-expanded-size` | Group bubble styling removed |
+| `--sd-font-mono` | No longer exposed |
+
+## How backward compatibility works
+
+New tokens in `variables.css` fall back to old names via `var()`:
+
+```css
+/* Inside variables.css */
+:root {
+ --sd-ui-comments-card-bg: var(--sd-comment-bg, #f3f6fd);
+ --sd-tracked-changes-insert-border: var(--sd-track-insert-border, #00853d);
+}
+```
+
+CSS resolves `var()` at computed-value time. If you set `--sd-comment-bg` on any element, the new token picks it up in that scope:
+
+```css
+.my-overrides {
+ --sd-comment-bg: #f0f0f0; /* works — --sd-ui-comments-card-bg resolves to #f0f0f0 */
+}
+```
+
+Setting the new name directly works too — it overrides the whole fallback expression.
+
+
+Old-name fallbacks will be removed in the next major version. Migrate to the new names when convenient.
+
diff --git a/brand/README.md b/brand/README.md
index dae815d749..3a4ed2c80d 100644
--- a/brand/README.md
+++ b/brand/README.md
@@ -14,17 +14,19 @@ brand/
## Design tokens
-Token values live in `packages/superdoc/src/assets/styles/tokens.css` as CSS custom properties (`--sd-*`). That file is the single source of truth.
+Token defaults live in `packages/superdoc/src/assets/styles/helpers/variables.css` as CSS custom properties (`--sd-*`).
+Preset theme overrides live in `packages/superdoc/src/assets/styles/helpers/themes.css`.
+Together, these files are the design-token source of truth.
-Tokens follow three tiers:
-- **Primitive** (`--sd-color-blue-500`) — raw palette values
-- **Semantic** (`--sd-action-primary`, `--sd-surface-card`) — UI roles that reference primitives
-- **Component** (`--sd-comment-bg`) — component-specific overrides that reference semantic tokens
+Tokens are organized by layers:
+- **Primitive** (`--sd-color-blue-500`, `--sd-font-size-400`, `--sd-radius-100`) — raw design values
+- **UI/Document semantic** (`--sd-ui-*`, `--sd-comments-*`, `--sd-tracked-changes-*`, `--sd-layout-*`) — role-based tokens used by components and rendering layers
+- **Component-level (optional)** (`--sd-ui-{component}-*`) — local overrides for a specific UI component when cross-component tokens are not enough
-Consumers customize SuperDoc by overriding `--sd-*` variables in their own CSS. Component customization is documented at `apps/docs/ui-components/`.
+Consumers customize SuperDoc by overriding `--sd-*` variables in their own CSS.
## How to use
-**For development**: Use semantic or component tokens in CSS — never hardcode hex values. When adding a new UI component, expose its visual properties as `--sd-{component}-*` variables in `tokens.css`.
+**For development**: Use semantic or component tokens in CSS — never hardcode hex values. When adding a new UI component, expose its visual properties as `--sd-ui-{component}-*` variables in `variables.css`; add per-theme overrides in `themes.css` only when needed.
**For marketing/content**: See `brand-guidelines.md` for voice, tone, and the dual-register pattern (developer vs. leader).
diff --git a/brand/visual-identity.md b/brand/visual-identity.md
index 85acb3cd5e..20f2b37127 100644
--- a/brand/visual-identity.md
+++ b/brand/visual-identity.md
@@ -12,7 +12,7 @@
- **Don't place blue text on blue backgrounds** — maintain contrast
### Color scale
-See `packages/superdoc/src/assets/styles/tokens.css` for the full blue scale (`--sd-color-blue-50` through `--sd-color-blue-900`). The scale moves from near-white (#EBF0FF) to near-black (#041133), with 500 being the canonical brand color.
+See `packages/superdoc/src/assets/styles/helpers/variables.css` for the full blue scale (`--sd-color-blue-50` through `--sd-color-blue-900`). The scale moves from near-white (#EBF0FF) to near-black (#041133), with 500 being the canonical brand color.
## Logo
@@ -82,7 +82,7 @@ No heavy shadows, no gradients on UI elements (gradients are reserved for market
Dark mode is currently used on the homepage/marketing site. The product UI is light-only.
-Dark mode token overrides are documented in `packages/superdoc/src/assets/styles/tokens.css`.
+Theme overrides are defined in `packages/superdoc/src/assets/styles/helpers/themes.css`, while base token defaults are in `packages/superdoc/src/assets/styles/helpers/variables.css`.
Key principle: dark backgrounds (#0B0C10) with reduced-brightness text (#E8E8E8), not pure white on pure black.
diff --git a/examples/features/theming/index.html b/examples/features/theming/index.html
new file mode 100644
index 0000000000..8f513da037
--- /dev/null
+++ b/examples/features/theming/index.html
@@ -0,0 +1,17 @@
+
+
+
+
+
+ SuperDoc — Theming
+
+
+
+
+
+
+
diff --git a/examples/features/theming/package.json b/examples/features/theming/package.json
new file mode 100644
index 0000000000..ff5a364c98
--- /dev/null
+++ b/examples/features/theming/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "superdoc-theming-example",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build"
+ },
+ "dependencies": {
+ "react": "^19.2.1",
+ "react-dom": "^19.2.1",
+ "superdoc": "latest"
+ },
+ "devDependencies": {
+ "@types/react": "^19.2.7",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^5.1.2",
+ "typescript": "^5.9.3",
+ "vite": "^7.2.7"
+ }
+}
diff --git a/examples/features/theming/src/App.tsx b/examples/features/theming/src/App.tsx
new file mode 100644
index 0000000000..33e2b9e2d2
--- /dev/null
+++ b/examples/features/theming/src/App.tsx
@@ -0,0 +1,99 @@
+import { useEffect, useRef, useState } from 'react';
+import { SuperDoc } from 'superdoc';
+import 'superdoc/style.css';
+import { themes, presets, themeLabels, type ThemeKey } from './themes';
+
+const allThemeClasses = [
+ ...Object.values(themes).filter(Boolean),
+ ...Object.values(presets),
+] as string[];
+
+function applyTheme(key: ThemeKey) {
+ const html = document.documentElement;
+ allThemeClasses.forEach((cls) => html.classList.remove(cls));
+
+ if (key === 'default') return;
+ const cls = (presets as any)[key] ?? (themes as any)[key];
+ if (cls) html.classList.add(cls);
+}
+
+export default function App() {
+ const [file, setFile] = useState(null);
+ const [theme, setTheme] = useState('default');
+ const containerRef = useRef(null);
+ const superdocRef = useRef(null);
+
+ useEffect(() => {
+ applyTheme(theme);
+ }, [theme]);
+
+ useEffect(() => {
+ if (!containerRef.current) return;
+
+ superdocRef.current?.destroy();
+ superdocRef.current = new SuperDoc({
+ selector: containerRef.current,
+ document: file ?? undefined,
+ documentMode: 'editing',
+ user: { name: 'Jane Doe', email: 'jane@example.com' },
+ modules: { toolbar: true },
+ });
+
+ return () => {
+ superdocRef.current?.destroy();
+ superdocRef.current = null;
+ };
+ }, [file]);
+
+ return (
+
+ );
+}
diff --git a/examples/features/theming/src/main.tsx b/examples/features/theming/src/main.tsx
new file mode 100644
index 0000000000..c018515cd7
--- /dev/null
+++ b/examples/features/theming/src/main.tsx
@@ -0,0 +1,9 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import App from './App';
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+ ,
+);
diff --git a/examples/features/theming/src/themes.ts b/examples/features/theming/src/themes.ts
new file mode 100644
index 0000000000..c0a71c23bf
--- /dev/null
+++ b/examples/features/theming/src/themes.ts
@@ -0,0 +1,91 @@
+import { createTheme } from 'superdoc';
+
+export const themes = {
+ default: null,
+
+ indigo: createTheme({
+ name: 'indigo',
+ font: 'Inter, system-ui, sans-serif',
+ radius: '8px',
+ colors: {
+ action: '#6366f1',
+ actionHover: '#4f46e5',
+ bg: '#ffffff',
+ text: '#1e293b',
+ textMuted: '#64748b',
+ border: '#e2e8f0',
+ hoverBg: '#f1f5f9',
+ },
+ vars: {
+ '--sd-ui-toolbar-bg': '#f8fafc',
+ },
+ }),
+
+ dark: createTheme({
+ name: 'dark',
+ font: 'Inter, system-ui, sans-serif',
+ colors: {
+ bg: '#1a1a2e',
+ hoverBg: '#2a2a3e',
+ activeBg: '#3a3a4e',
+ text: '#e2e8f0',
+ textMuted: '#94a3b8',
+ textDisabled: '#64748b',
+ border: '#334155',
+ action: '#60a5fa',
+ actionHover: '#93c5fd',
+ },
+ vars: {
+ '--sd-ui-toolbar-bg': '#0f172a',
+ '--sd-ui-toolbar-button-text': '#e2e8f0',
+ '--sd-ui-dropdown-bg': '#1e293b',
+ '--sd-ui-dropdown-border': '#334155',
+ '--sd-ui-menu-bg': '#1e293b',
+ '--sd-ui-menu-border': '#334155',
+ '--sd-ui-comments-card-bg': '#1e293b',
+ '--sd-ui-comments-input-bg': '#1e293b',
+ '--sd-ui-comments-body-text': '#cbd5e1',
+ '--sd-ui-tooltip-bg': '#f1f5f9',
+ '--sd-ui-tooltip-text': '#1e293b',
+ '--sd-layout-page-shadow': '0 4px 20px rgba(0, 0, 0, 0.4)',
+ },
+ }),
+
+ warm: createTheme({
+ name: 'warm',
+ font: 'Georgia, serif',
+ radius: '4px',
+ colors: {
+ action: '#b45309',
+ actionHover: '#92400e',
+ bg: '#fffbeb',
+ hoverBg: '#fef3c7',
+ activeBg: '#fde68a',
+ text: '#451a03',
+ textMuted: '#78350f',
+ border: '#d97706',
+ },
+ vars: {
+ '--sd-ui-toolbar-bg': '#fef3c7',
+ '--sd-ui-comments-card-bg': '#fef9e7',
+ },
+ }),
+} as const;
+
+export const presets = {
+ 'preset-docs': 'sd-theme-docs',
+ 'preset-word': 'sd-theme-word',
+ 'preset-blueprint': 'sd-theme-blueprint',
+} as const;
+
+export type ThemeKey = keyof typeof themes | keyof typeof presets;
+
+export const themeLabels: Record = {
+ default: 'Default',
+ indigo: 'Indigo Brand',
+ dark: 'Dark',
+ warm: 'Warm Earth',
+ 'preset-docs': 'Preset: Docs',
+ 'preset-word': 'Preset: Word',
+ 'preset-blueprint': 'Preset: Blueprint',
+};
diff --git a/examples/features/theming/vite.config.ts b/examples/features/theming/vite.config.ts
new file mode 100644
index 0000000000..0466183af6
--- /dev/null
+++ b/examples/features/theming/vite.config.ts
@@ -0,0 +1,6 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+
+export default defineConfig({
+ plugins: [react()],
+});
diff --git a/examples/getting-started/laravel/package.json b/examples/getting-started/laravel/package.json
index 5ced1f78dc..d42caa20b7 100644
--- a/examples/getting-started/laravel/package.json
+++ b/examples/getting-started/laravel/package.json
@@ -4,7 +4,7 @@
"type": "module",
"scripts": {
"build": "vite build",
- "dev": "concurrently \"php artisan serve\" \"vite\""
+ "dev": "concurrently \"php artisan serve --host=0.0.0.0 --port=8000\" \"vite\""
},
"dependencies": {
"concurrently": "^9.0.0",
diff --git a/packages/layout-engine/painters/dom/src/css-token.test.ts b/packages/layout-engine/painters/dom/src/css-token.test.ts
new file mode 100644
index 0000000000..4107087363
--- /dev/null
+++ b/packages/layout-engine/painters/dom/src/css-token.test.ts
@@ -0,0 +1,25 @@
+import { describe, expect, it } from 'vitest';
+import { cssToken } from './css-token.js';
+
+describe('cssToken', () => {
+ it('builds a var() string with the fallback embedded', () => {
+ const token = cssToken('--sd-ui-bg', '#ffffff');
+ expect(token.css).toBe('var(--sd-ui-bg, #ffffff)');
+ });
+
+ it('stores the fallback separately for jsdom environments', () => {
+ const token = cssToken('--sd-ui-bg', '#ffffff');
+ expect(token.fallback).toBe('#ffffff');
+ });
+
+ it('keeps css and fallback in sync', () => {
+ const token = cssToken('--sd-comments-highlight-external', '#B1124B40');
+ expect(token.css).toContain(token.fallback);
+ });
+
+ it('works with rgba values', () => {
+ const token = cssToken('--sd-ui-tools-item-bg', 'rgba(219, 219, 219, 0.6)');
+ expect(token.css).toBe('var(--sd-ui-tools-item-bg, rgba(219, 219, 219, 0.6))');
+ expect(token.fallback).toBe('rgba(219, 219, 219, 0.6)');
+ });
+});
diff --git a/packages/layout-engine/painters/dom/src/css-token.ts b/packages/layout-engine/painters/dom/src/css-token.ts
new file mode 100644
index 0000000000..5d15e3ba4a
--- /dev/null
+++ b/packages/layout-engine/painters/dom/src/css-token.ts
@@ -0,0 +1,14 @@
+export type CssToken = {
+ css: string;
+ fallback: string;
+};
+
+/**
+ * Creates a CSS variable token with a fallback value.
+ * The `css` string is used in real browsers (supports var()).
+ * The `fallback` is used in environments that don't (e.g., jsdom in tests).
+ */
+export const cssToken = (varName: string, fallback: string): CssToken => ({
+ css: `var(${varName}, ${fallback})`,
+ fallback,
+});
diff --git a/packages/layout-engine/painters/dom/src/renderer.ts b/packages/layout-engine/painters/dom/src/renderer.ts
index 16ae2cea51..a589982cd7 100644
--- a/packages/layout-engine/painters/dom/src/renderer.ts
+++ b/packages/layout-engine/painters/dom/src/renderer.ts
@@ -591,11 +591,25 @@ const LIST_MARKER_GAP = 8;
const DEFAULT_PAGE_HEIGHT_PX = 1056;
/** Default gap used when virtualization is enabled (kept in sync with PresentationEditor layout defaults). */
const DEFAULT_VIRTUALIZED_PAGE_GAP = 72;
-const COMMENT_EXTERNAL_COLOR = '#B1124B';
-const COMMENT_INTERNAL_COLOR = '#078383';
-const COMMENT_INACTIVE_ALPHA = '40'; // ~25% for inactive
-const COMMENT_ACTIVE_ALPHA = '66'; // ~40% for active/selected
-const COMMENT_FADED_ALPHA = '20'; // ~12% for non-selected when another comment is active
+import { cssToken } from './css-token.js';
+import type { CssToken } from './css-token.js';
+
+type CommentHighlightToken = CssToken;
+
+const COMMENT_HIGHLIGHT_EXTERNAL = cssToken('--sd-comments-highlight-external', '#B1124B40');
+const COMMENT_HIGHLIGHT_EXTERNAL_ACTIVE = cssToken('--sd-comments-highlight-external-active', '#B1124B66');
+const COMMENT_HIGHLIGHT_EXTERNAL_FADED = cssToken('--sd-comments-highlight-external-faded', '#B1124B20');
+const COMMENT_HIGHLIGHT_INTERNAL = cssToken('--sd-comments-highlight-internal', '#07838340');
+const COMMENT_HIGHLIGHT_INTERNAL_ACTIVE = cssToken('--sd-comments-highlight-internal-active', '#07838366');
+const COMMENT_HIGHLIGHT_INTERNAL_FADED = cssToken('--sd-comments-highlight-internal-faded', '#07838320');
+const COMMENT_HIGHLIGHT_EXTERNAL_NESTED_BORDER = cssToken(
+ '--sd-comments-highlight-external-nested-border',
+ '#B1124B99',
+);
+const COMMENT_HIGHLIGHT_INTERNAL_NESTED_BORDER = cssToken(
+ '--sd-comments-highlight-internal-nested-border',
+ '#07838399',
+);
type LinkRenderData = {
href?: string;
@@ -2595,7 +2609,7 @@ export class DomPainter {
const base = this.options.pageStyles ?? {};
return {
...base,
- background: base.background ?? '#fff',
+ background: base.background ?? 'var(--sd-layout-page-bg, #fff)',
boxShadow: 'none',
border: 'none',
margin: '0',
@@ -4605,14 +4619,23 @@ export class DomPainter {
const commentHighlight = getCommentHighlight(textRun, this.activeCommentId);
if (commentHighlight.color && hasAnyComment) {
- (elem as HTMLElement).style.backgroundColor = commentHighlight.color;
- // Add thin visual indicator for nested comments when outer comment is selected
- // Use box-shadow instead of border to avoid affecting text layout
- if (commentHighlight.hasNestedComments && commentHighlight.baseColor) {
- const borderColor = `${commentHighlight.baseColor}99`; // Semi-transparent for subtlety
- (elem as HTMLElement).style.boxShadow = `inset 1px 0 0 ${borderColor}, inset -1px 0 0 ${borderColor}`;
+ const runElement = elem as HTMLElement;
+ const previousBackgroundColor = runElement.style.backgroundColor;
+ runElement.style.backgroundColor = commentHighlight.color.css;
+ // jsdom may drop var() values for inline style properties.
+ // Fall back to concrete color to keep rendering/tests stable.
+ if (!runElement.style.backgroundColor || runElement.style.backgroundColor === previousBackgroundColor) {
+ runElement.style.backgroundColor = commentHighlight.color.fallback;
+ }
+ // Add thin visual indicator for nested comments when outer comment is selected.
+ // Use box-shadow instead of border to avoid affecting text layout.
+ if (commentHighlight.hasNestedComments && commentHighlight.nestedBorderColor) {
+ runElement.style.boxShadow = `inset 1px 0 0 ${commentHighlight.nestedBorderColor.css}, inset -1px 0 0 ${commentHighlight.nestedBorderColor.css}`;
+ if (!runElement.style.boxShadow) {
+ runElement.style.boxShadow = `inset 1px 0 0 ${commentHighlight.nestedBorderColor.fallback}, inset -1px 0 0 ${commentHighlight.nestedBorderColor.fallback}`;
+ }
} else {
- (elem as HTMLElement).style.boxShadow = '';
+ runElement.style.boxShadow = '';
}
}
// We still need to preserve the comment ids
@@ -6958,8 +6981,8 @@ const applyRunStyles = (element: HTMLElement, run: Run, _isLink = false): void =
};
interface CommentHighlightResult {
- color?: string;
- baseColor?: string;
+ color?: CommentHighlightToken;
+ nestedBorderColor?: CommentHighlightToken;
hasNestedComments?: boolean;
}
@@ -7000,27 +7023,25 @@ const getCommentHighlight = (run: TextRun, activeCommentId: string | null): Comm
matchesId(c as { commentId: string; importedId?: string }, activeCommentId),
);
if (activeComment) {
- const base = activeComment.internal ? COMMENT_INTERNAL_COLOR : COMMENT_EXTERNAL_COLOR;
- // Check if there are OTHER comments besides the active one (nested comments)
const nestedComments = comments.filter(
(c) => !matchesId(c as { commentId: string; importedId?: string }, activeCommentId),
);
return {
- color: `${base}${COMMENT_ACTIVE_ALPHA}`,
- baseColor: base,
+ color: activeComment.internal ? COMMENT_HIGHLIGHT_INTERNAL_ACTIVE : COMMENT_HIGHLIGHT_EXTERNAL_ACTIVE,
+ nestedBorderColor: activeComment.internal
+ ? COMMENT_HIGHLIGHT_INTERNAL_NESTED_BORDER
+ : COMMENT_HIGHLIGHT_EXTERNAL_NESTED_BORDER,
hasNestedComments: nestedComments.length > 0,
};
}
// Active comment is set but this run does not belong to it - show faded highlight.
const fadedPrimary = comments[0];
- const fadedBase = fadedPrimary.internal ? COMMENT_INTERNAL_COLOR : COMMENT_EXTERNAL_COLOR;
- return { color: `${fadedBase}${COMMENT_FADED_ALPHA}` };
+ return { color: fadedPrimary.internal ? COMMENT_HIGHLIGHT_INTERNAL_FADED : COMMENT_HIGHLIGHT_EXTERNAL_FADED };
}
// No active comment - show uniform light highlight (like Word/Google Docs)
const primary = comments[0];
- const base = primary.internal ? COMMENT_INTERNAL_COLOR : COMMENT_EXTERNAL_COLOR;
- return { color: `${base}${COMMENT_INACTIVE_ALPHA}` };
+ return { color: primary.internal ? COMMENT_HIGHLIGHT_INTERNAL : COMMENT_HIGHLIGHT_EXTERNAL };
};
/**
diff --git a/packages/layout-engine/painters/dom/src/styles.ts b/packages/layout-engine/painters/dom/src/styles.ts
index 3f9e39ba0d..05ad697d52 100644
--- a/packages/layout-engine/painters/dom/src/styles.ts
+++ b/packages/layout-engine/painters/dom/src/styles.ts
@@ -18,8 +18,8 @@ export type PageStyles = {
};
export const DEFAULT_PAGE_STYLES: Required = {
- background: '#fff',
- boxShadow: '0 4px 20px rgba(15, 23, 42, 0.08)',
+ background: 'var(--sd-layout-page-bg, #fff)',
+ boxShadow: 'var(--sd-layout-page-shadow, 0 4px 20px rgba(15, 23, 42, 0.08))',
border: '1px solid rgba(15, 23, 42, 0.08)',
margin: '0 auto',
};
@@ -233,38 +233,38 @@ const TRACK_CHANGE_STYLES = `
}
.superdoc-layout .track-insert-dec.highlighted {
- border-top: 1px dashed #00853d;
- border-bottom: 1px dashed #00853d;
- background-color: #399c7222;
+ border-top: 1px dashed var(--sd-tracked-changes-insert-border, #00853d);
+ border-bottom: 1px dashed var(--sd-tracked-changes-insert-border, #00853d);
+ background-color: var(--sd-tracked-changes-insert-background, #399c7222);
}
.superdoc-layout .track-delete-dec.highlighted {
- border-top: 1px dashed #cb0e47;
- border-bottom: 1px dashed #cb0e47;
- background-color: #cb0e4722;
+ border-top: 1px dashed var(--sd-tracked-changes-delete-border, #cb0e47);
+ border-bottom: 1px dashed var(--sd-tracked-changes-delete-border, #cb0e47);
+ background-color: var(--sd-tracked-changes-delete-background, #cb0e4722);
text-decoration: line-through !important;
text-decoration-thickness: 2px !important;
}
.superdoc-layout .track-format-dec.highlighted {
- border-bottom: 2px solid gold;
+ border-bottom: 2px solid var(--sd-tracked-changes-format-border, gold);
}
.superdoc-layout .track-insert-dec.highlighted.track-change-focused {
border-style: solid;
border-width: 2px;
- background-color: #399c7244;
+ background-color: var(--sd-tracked-changes-insert-background-focused, #399c7244);
}
.superdoc-layout .track-delete-dec.highlighted.track-change-focused {
border-style: solid;
border-width: 2px;
- background-color: #cb0e4744;
+ background-color: var(--sd-tracked-changes-delete-background-focused, #cb0e4744);
}
.superdoc-layout .track-format-dec.highlighted.track-change-focused {
border-bottom-width: 3px;
- background-color: #ffd70033;
+ background-color: var(--sd-tracked-changes-format-background-focused, #ffd70033);
}
`;
@@ -380,19 +380,19 @@ const SDT_CONTAINER_STYLES = `
}
.superdoc-structured-content-block:not(.ProseMirror-selectednode):hover {
- background-color: #f2f2f2;
+ background-color: var(--sd-content-controls-block-hover-bg, #f2f2f2);
border-color: transparent;
}
/* Group hover (JavaScript-coordinated) */
.superdoc-structured-content-block.sdt-group-hover:not(.ProseMirror-selectednode),
.superdoc-structured-content-block.sdt-hover:not(.ProseMirror-selectednode) {
- background-color: #f2f2f2;
+ background-color: var(--sd-content-controls-block-hover-bg, #f2f2f2);
border-color: transparent;
}
.superdoc-structured-content-block.ProseMirror-selectednode {
- border-color: #629be7;
+ border-color: var(--sd-content-controls-block-border, #629be7);
outline: none;
}
@@ -409,10 +409,11 @@ const SDT_CONTAINER_STYLES = `
min-width: 0;
height: 18px;
padding: 0 4px;
- border: 1px solid #629be7;
+ border: 1px solid var(--sd-content-controls-label-border, #629be7);
border-bottom: none;
border-radius: 6px 6px 0 0;
- background-color: #629be7ee;
+ background-color: var(--sd-content-controls-label-bg, #629be7ee);
+ color: var(--sd-content-controls-label-text, #ffffff);
box-sizing: border-box;
z-index: 10;
display: none;
@@ -478,12 +479,12 @@ const SDT_CONTAINER_STYLES = `
/* Hover effect for inline structured content */
.superdoc-structured-content-inline:not(.ProseMirror-selectednode):hover {
- background-color: #f2f2f2;
+ background-color: var(--sd-content-controls-inline-hover-bg, #f2f2f2);
border-color: transparent;
}
.superdoc-structured-content-inline.ProseMirror-selectednode {
- border-color: #629be7;
+ border-color: var(--sd-content-controls-inline-border, #629be7);
outline: none;
background-color: transparent;
}
@@ -495,8 +496,9 @@ const SDT_CONTAINER_STYLES = `
transform: translateX(-50%);
font-size: 11px;
padding: 0 4px;
- background-color: #629be7ee;
- color: white;
+ border: 1px solid var(--sd-content-controls-label-border, #629be7);
+ background-color: var(--sd-content-controls-label-bg, #629be7ee);
+ color: var(--sd-content-controls-label-text, #ffffff);
border-radius: 4px;
white-space: nowrap;
z-index: 100;
@@ -521,7 +523,7 @@ const SDT_CONTAINER_STYLES = `
* Hover is suppressed when the node is selected (SD-1584). */
.superdoc-structured-content-block[data-lock-mode].sdt-hover:not(.ProseMirror-selectednode),
.superdoc-structured-content-inline[data-lock-mode]:hover:not(.ProseMirror-selectednode) {
- background-color: rgba(98, 155, 231, 0.08);
+ background-color: var(--sd-content-controls-lock-hover-bg, rgba(98, 155, 231, 0.08));
z-index: 9999999;
}
diff --git a/packages/super-editor/src/assets/styles/elements/prosemirror.css b/packages/super-editor/src/assets/styles/elements/prosemirror.css
index 9dfa3863d8..8adf349020 100644
--- a/packages/super-editor/src/assets/styles/elements/prosemirror.css
+++ b/packages/super-editor/src/assets/styles/elements/prosemirror.css
@@ -255,21 +255,21 @@ https://github.com/ProseMirror/prosemirror-tables/blob/master/demo/index.html
}
.sd-editor-scoped .ProseMirror .track-insert-dec.highlighted {
- border-top: 1px dashed var(--sd-track-insert-border, #00853d);
- border-bottom: 1px dashed var(--sd-track-insert-border, #00853d);
- background-color: var(--sd-track-insert-bg, #399c7222);
+ border-top: 1px dashed var(--sd-tracked-changes-insert-border, #00853d);
+ border-bottom: 1px dashed var(--sd-tracked-changes-insert-border, #00853d);
+ background-color: var(--sd-tracked-changes-insert-background, #399c7222);
}
.sd-editor-scoped .ProseMirror .track-delete-dec.highlighted {
- border-top: 1px dashed var(--sd-track-delete-border, #cb0e47);
- border-bottom: 1px dashed var(--sd-track-delete-border, #cb0e47);
- background-color: var(--sd-track-delete-bg, #cb0e4722);
+ border-top: 1px dashed var(--sd-tracked-changes-delete-border, #cb0e47);
+ border-bottom: 1px dashed var(--sd-tracked-changes-delete-border, #cb0e47);
+ background-color: var(--sd-tracked-changes-delete-background, #cb0e4722);
text-decoration: line-through !important;
text-decoration-thickness: 2px !important;
}
.sd-editor-scoped .ProseMirror .track-format-dec.highlighted {
- border-bottom: 2px solid var(--sd-track-format-border, gold);
+ border-bottom: 2px solid var(--sd-tracked-changes-format-border, gold);
}
.sd-editor-scoped .ProseMirror .track-delete-widget {
diff --git a/packages/super-editor/src/assets/styles/extensions/comments.css b/packages/super-editor/src/assets/styles/extensions/comments.css
index 9b4ab407e0..3c75c319a0 100644
--- a/packages/super-editor/src/assets/styles/extensions/comments.css
+++ b/packages/super-editor/src/assets/styles/extensions/comments.css
@@ -3,7 +3,7 @@
}
.sd-editor-comment-highlight:hover {
- background-color: var(--sd-comment-highlight-hover, #1354ff55);
+ background-color: var(--sd-comments-highlight-hover, #1354ff55);
}
.sd-editor-comment-highlight.sd-custom-selection {
diff --git a/packages/super-editor/src/assets/styles/extensions/structured-content.css b/packages/super-editor/src/assets/styles/extensions/structured-content.css
index 8d80d5d434..387e70a71b 100644
--- a/packages/super-editor/src/assets/styles/extensions/structured-content.css
+++ b/packages/super-editor/src/assets/styles/extensions/structured-content.css
@@ -13,15 +13,21 @@
}
/* Hover (not selected): light grey background, no handle */
-.super-editor .sd-structured-content:not(.ProseMirror-selectednode):hover,
+.super-editor .sd-structured-content:not(.ProseMirror-selectednode):hover {
+ background-color: var(--sd-content-controls-inline-hover-bg, #f2f2f2);
+}
+
.super-editor .sd-structured-content-block:not(.ProseMirror-selectednode):hover {
- background-color: #f2f2f2;
+ background-color: var(--sd-content-controls-block-hover-bg, #f2f2f2);
}
/* Selected: border + handle visible */
-.super-editor .sd-structured-content.ProseMirror-selectednode,
+.super-editor .sd-structured-content.ProseMirror-selectednode {
+ border-color: var(--sd-content-controls-inline-border, #629be7);
+}
+
.super-editor .sd-structured-content-block.ProseMirror-selectednode {
- border-color: #629be7;
+ border-color: var(--sd-content-controls-block-border, #629be7);
}
.super-editor .sd-structured-content-draggable {
@@ -36,10 +42,11 @@
min-width: 0;
height: 18px;
padding: 0 4px;
- border: 1px solid #629be7;
+ border: 1px solid var(--sd-content-controls-label-border, #629be7);
border-bottom: none;
border-radius: 6px 6px 0 0;
- background-color: #629be7dd;
+ background-color: var(--sd-content-controls-label-bg, #629be7ee);
+ color: var(--sd-content-controls-label-text, #ffffff);
box-sizing: border-box;
z-index: 10;
cursor: grab;
diff --git a/packages/super-editor/src/components/context-menu/ContextMenu.vue b/packages/super-editor/src/components/context-menu/ContextMenu.vue
index 4147d044b4..53dd19da12 100644
--- a/packages/super-editor/src/components/context-menu/ContextMenu.vue
+++ b/packages/super-editor/src/components/context-menu/ContextMenu.vue
@@ -527,13 +527,13 @@ onBeforeUnmount(() => {
position: fixed;
z-index: 50;
width: 180px;
- color: #47484a;
- background: white;
- box-shadow:
- 0 0 0 1px rgba(0, 0, 0, 0.05),
- 0px 10px 20px rgba(0, 0, 0, 0.1);
+ color: var(--sd-ui-menu-text, #47484a);
+ background: var(--sd-ui-menu-bg, #ffffff);
+ border-radius: var(--sd-ui-menu-radius, 0);
+ overflow: hidden;
+ box-shadow: var(--sd-ui-menu-shadow, 0 0 0 1px rgba(0, 0, 0, 0.05), 0px 10px 20px rgba(0, 0, 0, 0.1));
margin-top: 0.5rem;
- font-size: 12px;
+ font-size: var(--sd-ui-menu-font-size, 12px);
}
/* Hide the input but keep it functional */
@@ -555,18 +555,18 @@ onBeforeUnmount(() => {
.context-menu-search {
padding: 0.5rem;
- border-bottom: 1px solid #eee;
+ border-bottom: 1px solid var(--sd-ui-menu-border, #eee);
}
.context-menu-search input {
width: 100%;
padding: 0.25rem 0.5rem;
- border: 1px solid #ddd;
+ border: 1px solid var(--sd-ui-menu-input-border, #ddd);
outline: none;
}
.context-menu-search input:focus {
- border-color: #0096fd;
+ border-color: var(--sd-ui-menu-input-focus-border, #0096fd);
}
/* Remove unused group styles */
@@ -584,13 +584,13 @@ onBeforeUnmount(() => {
}
.context-menu-item:hover {
- background: #f5f5f5;
+ background: var(--sd-ui-menu-item-hover-bg, #f5f5f5);
}
.context-menu-item.is-selected {
- background: #edf6ff;
- color: #0096fd;
- fill: #0096fd;
+ background: var(--sd-ui-menu-item-active-bg, #edf6ff);
+ color: var(--sd-ui-menu-item-active-text, #0096fd);
+ fill: var(--sd-ui-menu-item-active-text, #0096fd);
}
.context-menu-item-icon {
@@ -617,17 +617,15 @@ onBeforeUnmount(() => {
}
.popover {
- background: white;
- border-radius: 6px;
- box-shadow:
- 0 0 0 1px rgba(0, 0, 0, 0.05),
- 0px 10px 20px rgba(0, 0, 0, 0.1);
+ background: var(--sd-ui-menu-bg, #ffffff);
+ border-radius: var(--sd-ui-menu-radius, 0);
+ box-shadow: var(--sd-ui-menu-shadow, 0 0 0 1px rgba(0, 0, 0, 0.05), 0px 10px 20px rgba(0, 0, 0, 0.1));
z-index: 100;
}
.context-menu-divider {
height: 1px;
- background: #eee;
+ background: var(--sd-ui-menu-border, #eee);
margin: 4px 0;
}
diff --git a/packages/super-editor/src/components/rulers/Ruler.vue b/packages/super-editor/src/components/rulers/Ruler.vue
index 517b312f5d..8d14098607 100644
--- a/packages/super-editor/src/components/rulers/Ruler.vue
+++ b/packages/super-editor/src/components/rulers/Ruler.vue
@@ -527,7 +527,7 @@ onUnmounted(() => {
position: absolute;
top: -16px;
left: -2px;
- font-size: 10px;
+ font-size: var(--sd-ui-font-size-50, 10px);
pointer-events: none;
user-select: none;
}
diff --git a/packages/super-editor/src/components/toolbar/AIWriter.vue b/packages/super-editor/src/components/toolbar/AIWriter.vue
index c75db9fbb4..74280a9a20 100644
--- a/packages/super-editor/src/components/toolbar/AIWriter.vue
+++ b/packages/super-editor/src/components/toolbar/AIWriter.vue
@@ -419,7 +419,7 @@ const handleInput = (event) => {
padding-left: 8px;
width: 100%;
color: #47484a;
- font-size: 12px;
+ font-size: var(--sd-ui-font-size-200, 12px);
border: none;
background: transparent;
outline: none;
@@ -449,7 +449,7 @@ const handleInput = (event) => {
.ai-textarea-icon {
display: block;
font-weight: 800;
- font-size: 14px;
+ font-size: var(--sd-ui-font-size-400, 14px);
width: 16px;
height: 16px;
}
diff --git a/packages/super-editor/src/components/toolbar/AlignmentButtons.vue b/packages/super-editor/src/components/toolbar/AlignmentButtons.vue
index a621308cc1..7f90757fa5 100644
--- a/packages/super-editor/src/components/toolbar/AlignmentButtons.vue
+++ b/packages/super-editor/src/components/toolbar/AlignmentButtons.vue
@@ -105,17 +105,19 @@ onMounted(() => {
.button-icon {
cursor: pointer;
padding: 5px;
- font-size: 16px;
+ font-size: var(--sd-ui-font-size-600, 16px);
+ color: var(--sd-ui-dropdown-text, #47484a);
width: 25px;
height: 25px;
- border-radius: 4px;
+ border-radius: var(--sd-ui-dropdown-option-radius, 3px);
display: flex;
justify-content: center;
align-items: center;
box-sizing: border-box;
&:hover {
- background-color: #d8dee5;
+ background-color: var(--sd-ui-dropdown-hover-bg, #d8dee5);
+ color: var(--sd-ui-dropdown-hover-text, #47484a);
}
:deep(svg) {
diff --git a/packages/super-editor/src/components/toolbar/DocumentMode.vue b/packages/super-editor/src/components/toolbar/DocumentMode.vue
index 021e83597c..aa2dbcaca6 100644
--- a/packages/super-editor/src/components/toolbar/DocumentMode.vue
+++ b/packages/super-editor/src/components/toolbar/DocumentMode.vue
@@ -103,14 +103,14 @@ onMounted(() => {
.option-item {
display: flex;
flex-direction: row;
- background-color: white;
+ background-color: var(--sd-ui-dropdown-bg, #ffffff);
padding: 10px;
- border-radius: 4px;
+ border-radius: var(--sd-ui-dropdown-option-radius, 3px);
cursor: pointer;
box-sizing: border-box;
&:hover {
- background-color: #c8d0d8;
+ background-color: var(--sd-ui-dropdown-hover-bg, #d8dee5);
}
}
@@ -147,8 +147,8 @@ onMounted(() => {
.document-mode-type {
font-weight: 400;
- font-size: 15px;
- color: #222;
+ font-size: var(--sd-ui-font-size-500, 15px);
+ color: var(--sd-ui-text, #47484a);
}
.icon-column {
@@ -156,7 +156,7 @@ onMounted(() => {
justify-content: flex-start;
align-items: center;
padding: 0 5px;
- color: black;
+ color: var(--sd-ui-text, #47484a);
height: 100%;
box-sizing: border-box;
@@ -166,7 +166,7 @@ onMounted(() => {
align-items: center;
flex-shrink: 0;
height: 18px;
- color: #47484a;
+ color: var(--sd-ui-text, #47484a);
}
}
@@ -177,7 +177,7 @@ onMounted(() => {
}
.document-mode-description {
- font-size: 12px;
- color: #666;
+ font-size: var(--sd-ui-font-size-200, 12px);
+ color: var(--sd-ui-text-muted, #666666);
}
diff --git a/packages/super-editor/src/components/toolbar/IconGrid.vue b/packages/super-editor/src/components/toolbar/IconGrid.vue
index 946a5f865b..82dcde6846 100644
--- a/packages/super-editor/src/components/toolbar/IconGrid.vue
+++ b/packages/super-editor/src/components/toolbar/IconGrid.vue
@@ -53,7 +53,7 @@ const handleSelect = (option) => {
diff --git a/packages/super-editor/src/components/toolbar/OverflowMenu.vue b/packages/super-editor/src/components/toolbar/OverflowMenu.vue
index 9f668d40d4..912871a6bc 100644
--- a/packages/super-editor/src/components/toolbar/OverflowMenu.vue
+++ b/packages/super-editor/src/components/toolbar/OverflowMenu.vue
@@ -72,24 +72,27 @@ onBeforeUnmount(() => {
diff --git a/packages/super-editor/src/components/toolbar/TableGrid.vue b/packages/super-editor/src/components/toolbar/TableGrid.vue
index 6c3c825cde..5aa1b591be 100644
--- a/packages/super-editor/src/components/toolbar/TableGrid.vue
+++ b/packages/super-editor/src/components/toolbar/TableGrid.vue
@@ -157,13 +157,13 @@ onMounted(() => {
.toolbar-table-grid__item {
width: 20px;
height: 20px;
- border: 1px solid #d3d3d3;
+ border: 1px solid var(--sd-ui-border, #dbdbdb);
cursor: pointer;
transition: all 0.15s;
}
.toolbar-table-grid__item.selected {
- background-color: #dbdbdb;
+ background-color: var(--sd-ui-dropdown-hover-bg, #d8dee5);
}
&.high-contrast {
@@ -177,7 +177,7 @@ onMounted(() => {
}
.toolbar-table-grid-value {
- font-size: 13px;
+ font-size: var(--sd-ui-font-size-300, 13px);
line-height: 1.1;
padding: 0px 8px 2px;
}
diff --git a/packages/super-editor/src/components/toolbar/Toolbar.vue b/packages/super-editor/src/components/toolbar/Toolbar.vue
index 7fccad53af..918ba93e9e 100644
--- a/packages/super-editor/src/components/toolbar/Toolbar.vue
+++ b/packages/super-editor/src/components/toolbar/Toolbar.vue
@@ -140,7 +140,8 @@ const handleToolbarMousedown = (e) => {
display: flex;
width: 100%;
justify-content: space-between;
- padding: 4px 16px;
+ background: var(--sd-ui-toolbar-bg, transparent);
+ padding: var(--sd-ui-toolbar-padding-y, 4px) var(--sd-ui-toolbar-padding-x, 16px);
box-sizing: border-box;
font-family: var(--sd-ui-font-family, Arial, Helvetica, sans-serif);
}
diff --git a/packages/super-editor/src/components/toolbar/ToolbarButton.vue b/packages/super-editor/src/components/toolbar/ToolbarButton.vue
index 26a6f4a27c..6747576dcb 100644
--- a/packages/super-editor/src/components/toolbar/ToolbarButton.vue
+++ b/packages/super-editor/src/components/toolbar/ToolbarButton.vue
@@ -158,7 +158,7 @@ const caretIcon = computed(() => {
position: relative;
z-index: 1;
min-width: 30px;
- margin: 0 1px;
+ margin: 0 calc(var(--sd-ui-toolbar-item-gap, 2px) / 2);
}
.visually-hidden {
@@ -170,16 +170,16 @@ const caretIcon = computed(() => {
}
.toolbar-button {
- padding: 5px;
- height: 32px;
- max-height: 32px;
- border-radius: 6px;
+ padding: var(--sd-ui-toolbar-item-padding, 5px);
+ height: var(--sd-ui-toolbar-height, 32px);
+ max-height: var(--sd-ui-toolbar-height, 32px);
+ border-radius: var(--sd-ui-radius, 6px);
overflow-y: visible;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
- color: #47484a;
+ color: var(--sd-ui-toolbar-button-text, #47484a);
transition: all 0.2s ease-out;
user-select: none;
position: relative;
@@ -187,7 +187,7 @@ const caretIcon = computed(() => {
}
.toolbar-button:hover {
- background-color: #dbdbdb;
+ background-color: var(--sd-ui-toolbar-button-hover-bg, var(--sd-ui-hover-bg, #dbdbdb));
.toolbar-icon {
&.high-contrast {
@@ -203,7 +203,7 @@ const caretIcon = computed(() => {
.toolbar-button:active,
.active {
- background-color: #c8d0d8;
+ background-color: var(--sd-ui-toolbar-button-active-bg, var(--sd-ui-active-bg, #c8d0d8));
}
.button-label {
@@ -213,7 +213,7 @@ const caretIcon = computed(() => {
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 400;
- font-size: 15px;
+ font-size: var(--sd-ui-font-size-500, 15px);
margin: 5px;
}
@@ -255,17 +255,17 @@ const caretIcon = computed(() => {
}
.button-text-input {
- color: #47484a;
+ color: var(--sd-ui-toolbar-button-text, #47484a);
border-radius: 4px;
text-align: center;
width: 30px;
- font-size: 14px;
+ font-size: var(--sd-ui-font-size-400, 14px);
margin-right: 5px;
font-weight: 400;
background-color: transparent;
padding: 2px 0;
outline: none;
- border: 1px solid #d8dee5;
+ border: 1px solid var(--sd-ui-border, #dbdbdb);
box-sizing: border-box;
&.high-contrast {
@@ -278,7 +278,7 @@ const caretIcon = computed(() => {
}
.button-text-input::placeholder {
- color: #47484a;
+ color: var(--sd-ui-toolbar-button-text, #47484a);
}
.dropdown-caret {
diff --git a/packages/super-editor/src/components/toolbar/ToolbarButtonIcon.vue b/packages/super-editor/src/components/toolbar/ToolbarButtonIcon.vue
index d91b989538..d0f6739150 100644
--- a/packages/super-editor/src/components/toolbar/ToolbarButtonIcon.vue
+++ b/packages/super-editor/src/components/toolbar/ToolbarButtonIcon.vue
@@ -64,15 +64,6 @@ const hasColorBar = computed(() => {
margin-top: -3px;
}
-.toolbar-button:hover {
- color: black;
- background-color: #d8dee5;
-}
-.toolbar-button:active,
-.active {
- background-color: #c8d0d8;
-}
-
.color-bar {
border-radius: 4px;
position: absolute;
diff --git a/packages/super-editor/src/components/toolbar/ToolbarDropdown.vue b/packages/super-editor/src/components/toolbar/ToolbarDropdown.vue
index afdc2ffb4c..65cb02b409 100644
--- a/packages/super-editor/src/components/toolbar/ToolbarDropdown.vue
+++ b/packages/super-editor/src/components/toolbar/ToolbarDropdown.vue
@@ -373,10 +373,10 @@ onBeforeUnmount(() => {
.toolbar-dropdown-menu {
min-width: 80px;
padding: 4px;
- border-radius: 8px;
- background: #fff;
- border: 1px solid #e4e6eb;
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.16);
+ border-radius: var(--sd-ui-radius, 6px);
+ background: var(--sd-ui-dropdown-bg, #fff);
+ border: 1px solid var(--sd-ui-dropdown-border, #e4e6eb);
+ box-shadow: var(--sd-ui-dropdown-shadow, 0 8px 24px rgba(0, 0, 0, 0.16));
box-sizing: border-box;
}
@@ -389,9 +389,10 @@ onBeforeUnmount(() => {
display: flex;
align-items: center;
padding: 0 10px;
- border-radius: 3px;
+ border-radius: var(--sd-ui-dropdown-option-radius, 3px);
cursor: pointer;
- font-size: 14px;
+ font-size: var(--sd-ui-font-size-400, 14px);
+ color: var(--sd-ui-dropdown-text, #47484a);
transition: background-color 0.2s ease-out;
box-sizing: border-box;
}
@@ -413,11 +414,13 @@ onBeforeUnmount(() => {
}
.toolbar-dropdown-option:hover {
- background: #d8dee5;
+ background: var(--sd-ui-dropdown-hover-bg, #d8dee5);
+ color: var(--sd-ui-dropdown-hover-text, #47484a);
}
.toolbar-dropdown-option.selected {
- background: #d8dee5;
+ background: var(--sd-ui-dropdown-active-bg, #d8dee5);
+ color: var(--sd-ui-dropdown-selected-text, #47484a);
}
.toolbar-dropdown-menu.high-contrast .toolbar-dropdown-option:not(.render):hover {
@@ -438,9 +441,12 @@ onBeforeUnmount(() => {
.toolbar-dropdown-option.render {
padding: 0;
cursor: default;
+ background: transparent;
+ color: inherit;
}
-.toolbar-dropdown-option.render:hover {
+.toolbar-dropdown-option.render:hover,
+.toolbar-dropdown-option.render.selected {
background: transparent;
color: inherit;
}
diff --git a/packages/super-editor/src/components/toolbar/ToolbarSeparator.vue b/packages/super-editor/src/components/toolbar/ToolbarSeparator.vue
index 059a16927a..3a5efee57d 100644
--- a/packages/super-editor/src/components/toolbar/ToolbarSeparator.vue
+++ b/packages/super-editor/src/components/toolbar/ToolbarSeparator.vue
@@ -14,7 +14,7 @@ const getSeparatorColor = () => {
if (isHighContrastMode.value) {
return '#000';
}
- return '#dbdbdb';
+ return 'var(--sd-ui-border, #dbdbdb)';
};
@@ -26,13 +26,13 @@ const getSeparatorColor = () => {
diff --git a/packages/superdoc/CLAUDE.md b/packages/superdoc/CLAUDE.md
index c0a84e3656..1c6b28c204 100644
--- a/packages/superdoc/CLAUDE.md
+++ b/packages/superdoc/CLAUDE.md
@@ -55,6 +55,28 @@ Uses `layout-engine` for virtualized rendering with pagination.
`PresentationEditor.ts` bridges state between modes.
See `super-editor/src/core/presentation-editor/` for implementation.
+## Theming
+
+SuperDoc UI is themed via `--sd-*` CSS variables. Use `createTheme()` for JS-based theming or set variables directly in CSS.
+
+```javascript
+import { createTheme } from 'superdoc';
+
+const theme = createTheme({
+ colors: { action: '#6366f1', bg: '#ffffff', text: '#1e293b', border: '#e2e8f0' },
+ font: 'Inter, sans-serif',
+ vars: { '--sd-ui-toolbar-bg': '#f8fafc' }, // escape hatch for any --sd-* variable
+});
+
+document.documentElement.classList.add(theme);
+```
+
+- `createTheme()` / `buildTheme()` — `src/core/theme/create-theme.js`
+- CSS variable defaults — `src/assets/styles/helpers/variables.css`
+- Preset themes — `src/assets/styles/helpers/themes.css`
+- Backward-compat aliases — `src/assets/styles/helpers/compat.css`
+- Consumer-facing agent guide — `AGENTS.md` (ships with npm package)
+
## Testing
- Unit tests: `src/SuperDoc.test.js`
diff --git a/packages/superdoc/package.json b/packages/superdoc/package.json
index a44b78c23a..5bedb8f6a4 100644
--- a/packages/superdoc/package.json
+++ b/packages/superdoc/package.json
@@ -14,7 +14,8 @@
},
"readme": "../../README.md",
"files": [
- "dist"
+ "dist",
+ "AGENTS.md"
],
"exports": {
".": {
diff --git a/packages/superdoc/src/SuperDoc.test.js b/packages/superdoc/src/SuperDoc.test.js
index d6297298ec..deaeb0ee68 100644
--- a/packages/superdoc/src/SuperDoc.test.js
+++ b/packages/superdoc/src/SuperDoc.test.js
@@ -1466,9 +1466,9 @@ describe('SuperDoc.vue', () => {
const styleVars = wrapper.vm.superdocStyleVars;
// Active insertBorder should be overridden
- expect(styleVars['--sd-track-insert-border']).toBe('#ff0000');
+ expect(styleVars['--sd-tracked-changes-insert-border']).toBe('#ff0000');
// deleteBackground should be inherited from base config
- expect(styleVars['--sd-track-delete-bg']).toBe('#0000ff');
+ expect(styleVars['--sd-tracked-changes-delete-background']).toBe('#0000ff');
});
it('sets track change CSS vars from base config when no active config provided', async () => {
@@ -1486,9 +1486,9 @@ describe('SuperDoc.vue', () => {
const styleVars = wrapper.vm.superdocStyleVars;
- expect(styleVars['--sd-track-insert-border']).toBe('#11ff11');
- expect(styleVars['--sd-track-delete-border']).toBe('#ff1111');
- expect(styleVars['--sd-track-format-border']).toBe('#1111ff');
+ expect(styleVars['--sd-tracked-changes-insert-border']).toBe('#11ff11');
+ expect(styleVars['--sd-tracked-changes-delete-border']).toBe('#ff1111');
+ expect(styleVars['--sd-tracked-changes-format-border']).toBe('#1111ff');
});
it('sets comment highlight hover color CSS var', async () => {
@@ -1501,6 +1501,6 @@ describe('SuperDoc.vue', () => {
await nextTick();
const styleVars = wrapper.vm.superdocStyleVars;
- expect(styleVars['--sd-comment-highlight-hover']).toBe('#abcdef88');
+ expect(styleVars['--sd-comments-highlight-hover']).toBe('#abcdef88');
});
});
diff --git a/packages/superdoc/src/SuperDoc.vue b/packages/superdoc/src/SuperDoc.vue
index 453dc4d773..a57fd88a9f 100644
--- a/packages/superdoc/src/SuperDoc.vue
+++ b/packages/superdoc/src/SuperDoc.vue
@@ -130,7 +130,7 @@ const superdocStyleVars = computed(() => {
if (!commentsConfig || commentsConfig === false) return vars;
if (commentsConfig.highlightHoverColor) {
- vars['--sd-comment-highlight-hover'] = commentsConfig.highlightHoverColor;
+ vars['--sd-comments-highlight-hover'] = commentsConfig.highlightHoverColor;
}
const trackChangeColors = commentsConfig.trackChangeHighlightColors || {};
@@ -138,11 +138,16 @@ const superdocStyleVars = computed(() => {
...trackChangeColors,
...(commentsConfig.trackChangeActiveHighlightColors || {}),
};
- if (activeTrackChangeColors.insertBorder) vars['--sd-track-insert-border'] = activeTrackChangeColors.insertBorder;
- if (activeTrackChangeColors.insertBackground) vars['--sd-track-insert-bg'] = activeTrackChangeColors.insertBackground;
- if (activeTrackChangeColors.deleteBorder) vars['--sd-track-delete-border'] = activeTrackChangeColors.deleteBorder;
- if (activeTrackChangeColors.deleteBackground) vars['--sd-track-delete-bg'] = activeTrackChangeColors.deleteBackground;
- if (activeTrackChangeColors.formatBorder) vars['--sd-track-format-border'] = activeTrackChangeColors.formatBorder;
+ if (activeTrackChangeColors.insertBorder)
+ vars['--sd-tracked-changes-insert-border'] = activeTrackChangeColors.insertBorder;
+ if (activeTrackChangeColors.insertBackground)
+ vars['--sd-tracked-changes-insert-background'] = activeTrackChangeColors.insertBackground;
+ if (activeTrackChangeColors.deleteBorder)
+ vars['--sd-tracked-changes-delete-border'] = activeTrackChangeColors.deleteBorder;
+ if (activeTrackChangeColors.deleteBackground)
+ vars['--sd-tracked-changes-delete-background'] = activeTrackChangeColors.deleteBackground;
+ if (activeTrackChangeColors.formatBorder)
+ vars['--sd-tracked-changes-format-border'] = activeTrackChangeColors.formatBorder;
return vars;
});
@@ -1536,26 +1541,17 @@ const getPDFViewer = () => {
z-index: 3;
display: flex;
flex-direction: column;
- gap: 6px;
-}
-
-.tools .tool-icon {
- font-size: 20px;
- border-radius: 12px;
- border: none;
- outline: none;
- background-color: #dbdbdb;
- cursor: pointer;
+ gap: var(--sd-ui-tools-gap, 6px);
}
.tools-item {
display: flex;
align-items: center;
justify-content: center;
- width: 50px;
- height: 50px;
- background-color: rgba(219, 219, 219, 0.6);
- border-radius: 12px;
+ width: var(--sd-ui-tools-item-size, 50px);
+ height: var(--sd-ui-tools-item-size, 50px);
+ background-color: var(--sd-ui-tools-item-bg, rgba(219, 219, 219, 0.6));
+ border-radius: var(--sd-ui-tools-item-radius, 12px);
cursor: pointer;
position: relative;
}
@@ -1565,8 +1561,8 @@ const getPDFViewer = () => {
}
.superdoc__tools-icon {
- width: 20px;
- height: 20px;
+ width: var(--sd-ui-tools-icon-size, 20px);
+ height: var(--sd-ui-tools-icon-size, 20px);
flex-shrink: 0;
}
@@ -1614,45 +1610,6 @@ const getPDFViewer = () => {
z-index: 50;
} */
-/* Tools styles */
-.tools {
- position: absolute;
- z-index: 3;
- display: flex;
- gap: 6px;
-}
-
-.tools .tool-icon {
- font-size: 20px;
- border-radius: 12px;
- border: none;
- outline: none;
- background-color: #dbdbdb;
- cursor: pointer;
-}
-
-.tools-item {
- display: flex;
- align-items: center;
- justify-content: center;
- position: relative;
- width: 50px;
- height: 50px;
- background-color: rgba(219, 219, 219, 0.6);
- border-radius: 12px;
- cursor: pointer;
-}
-
-.tools-item i {
- cursor: pointer;
-}
-
-.superdoc__tools-icon {
- width: 20px;
- height: 20px;
- flex-shrink: 0;
-}
-
.ai-tool > svg {
fill: transparent;
}
diff --git a/packages/superdoc/src/assets/styles/helpers/_all.css b/packages/superdoc/src/assets/styles/helpers/_all.css
index 470745bdc3..1f70dc6a9b 100644
--- a/packages/superdoc/src/assets/styles/helpers/_all.css
+++ b/packages/superdoc/src/assets/styles/helpers/_all.css
@@ -1 +1,3 @@
@import './variables.css';
+@import './compat.css';
+@import './themes.css';
diff --git a/packages/superdoc/src/assets/styles/helpers/compat.css b/packages/superdoc/src/assets/styles/helpers/compat.css
new file mode 100644
index 0000000000..c6107d1729
--- /dev/null
+++ b/packages/superdoc/src/assets/styles/helpers/compat.css
@@ -0,0 +1,22 @@
+/*
+ * Backward-compatible aliases for renamed CSS variables.
+ *
+ * Most old names are honored via var() fallbacks in variables.css — if a
+ * customer sets --sd-comment-bg, the new --sd-ui-comments-card-bg picks it up
+ * automatically. This file only covers aliases where the old name maps to a
+ * different concept (no direct 1:1 fallback possible).
+ *
+ * These aliases will be removed in the next major version.
+ */
+
+:root {
+ /* Old semantic names that map to primitives (no new-token equivalent) */
+ --sd-surface-canvas: var(--sd-color-gray-50);
+ --sd-surface-selected: var(--sd-color-blue-100);
+ --sd-text-secondary: var(--sd-color-gray-700);
+
+ /* Old names that map to a different concept in the new system */
+ --sd-surface-page: var(--sd-ui-bg);
+ --sd-border-subtle: var(--sd-ui-comments-separator);
+ --sd-border-focus: var(--sd-ui-action);
+}
diff --git a/packages/superdoc/src/assets/styles/helpers/compat.test.ts b/packages/superdoc/src/assets/styles/helpers/compat.test.ts
new file mode 100644
index 0000000000..ff8efe1144
--- /dev/null
+++ b/packages/superdoc/src/assets/styles/helpers/compat.test.ts
@@ -0,0 +1,169 @@
+import { describe, expect, it } from 'vitest';
+import { readFileSync } from 'node:fs';
+import { resolve, dirname } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+const variablesCss = readFileSync(resolve(__dirname, 'variables.css'), 'utf-8');
+const compatCss = readFileSync(resolve(__dirname, 'compat.css'), 'utf-8');
+const themesCss = readFileSync(resolve(__dirname, 'themes.css'), 'utf-8');
+
+/** Extract all --sd-* variable declarations from a CSS string. */
+const extractDeclaredVars = (css: string): Set => {
+ const vars = new Set();
+ for (const match of css.matchAll(/(--sd-[\w-]+)\s*:/g)) {
+ vars.add(match[1]);
+ }
+ return vars;
+};
+
+/** Extract all var(--sd-*) references from a CSS string. */
+const extractReferencedVars = (css: string): Set => {
+ const vars = new Set();
+ for (const match of css.matchAll(/var\((--sd-[\w-]+)/g)) {
+ vars.add(match[1]);
+ }
+ return vars;
+};
+
+describe('backward compatibility', () => {
+ const declaredInVariables = extractDeclaredVars(variablesCss);
+ const declaredInCompat = extractDeclaredVars(compatCss);
+ const referencedByCompat = extractReferencedVars(compatCss);
+ const referencedByVariables = extractReferencedVars(variablesCss);
+
+ describe('compat.css aliases', () => {
+ it('every alias points to a variable defined in variables.css', () => {
+ const broken: string[] = [];
+ for (const ref of referencedByCompat) {
+ if (!declaredInVariables.has(ref)) {
+ broken.push(ref);
+ }
+ }
+ expect(broken, `Compat aliases reference undefined variables: ${broken.join(', ')}`).toEqual([]);
+ });
+
+ it('does not re-declare any variable from variables.css', () => {
+ const collisions: string[] = [];
+ for (const name of declaredInCompat) {
+ if (declaredInVariables.has(name)) {
+ collisions.push(name);
+ }
+ }
+ expect(collisions, `Compat re-declares variables from variables.css: ${collisions.join(', ')}`).toEqual([]);
+ });
+
+ it('has no circular references with variables.css', () => {
+ // A circular reference: compat declares --old pointing to --new,
+ // AND variables.css declares --new pointing back to --old.
+ const circular: string[] = [];
+ for (const compatVar of declaredInCompat) {
+ // Find what this compat alias points to
+ const match = compatCss.match(
+ new RegExp(`${compatVar.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*:\\s*var\\((--sd-[\\w-]+)`),
+ );
+ if (!match) continue;
+ const target = match[1];
+ // Check if variables.css references the compat var name
+ const targetDecl = variablesCss.match(
+ new RegExp(
+ `${target.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*:[^;]*var\\(${compatVar.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`,
+ ),
+ );
+ if (targetDecl) {
+ circular.push(`${compatVar} ↔ ${target}`);
+ }
+ }
+ expect(circular, `Circular references found: ${circular.join(', ')}`).toEqual([]);
+ });
+ });
+
+ describe('old-name fallbacks in variables.css', () => {
+ it('honors old comment variable names', () => {
+ const expected: [string, string][] = [
+ ['--sd-ui-comments-card-bg', '--sd-comment-bg'],
+ ['--sd-ui-comments-card-hover-bg', '--sd-comment-bg-hover'],
+ ['--sd-ui-comments-card-active-bg', '--sd-comment-bg-active'],
+ ['--sd-ui-comments-separator', '--sd-comment-separator'],
+ ['--sd-ui-comments-author-text', '--sd-comment-author-color'],
+ ['--sd-ui-comments-timestamp-text', '--sd-comment-time-color'],
+ ['--sd-ui-comments-internal-bg', '--sd-comment-internal-bg'],
+ ['--sd-ui-comments-external-bg', '--sd-comment-external-bg'],
+ ];
+ for (const [newName, oldName] of expected) {
+ const pattern = new RegExp(
+ `${newName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*:\\s*var\\(${oldName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`,
+ );
+ expect(variablesCss, `Expected ${newName} to fall back to ${oldName}`).toMatch(pattern);
+ }
+ });
+
+ it('honors old tracked change variable names', () => {
+ const expected: [string, string][] = [
+ ['--sd-tracked-changes-insert-border', '--sd-track-insert-border'],
+ ['--sd-tracked-changes-insert-background', '--sd-track-insert-bg'],
+ ['--sd-tracked-changes-delete-border', '--sd-track-delete-border'],
+ ['--sd-tracked-changes-delete-background', '--sd-track-delete-bg'],
+ ['--sd-tracked-changes-format-border', '--sd-track-format-border'],
+ ];
+ for (const [newName, oldName] of expected) {
+ const pattern = new RegExp(
+ `${newName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*:\\s*var\\(${oldName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`,
+ );
+ expect(variablesCss, `Expected ${newName} to fall back to ${oldName}`).toMatch(pattern);
+ }
+ });
+
+ it('honors old semantic variable names', () => {
+ const expected: [string, string][] = [
+ ['--sd-ui-bg', '--sd-surface-card'],
+ ['--sd-ui-text', '--sd-text-primary'],
+ ['--sd-ui-border', '--sd-border-default'],
+ ['--sd-ui-action', '--sd-action-primary'],
+ ['--sd-ui-font-family', '--sd-font-family'],
+ ['--sd-ui-hover-bg', '--sd-surface-hover'],
+ ];
+ for (const [newName, oldName] of expected) {
+ const pattern = new RegExp(
+ `${newName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*:\\s*var\\(${oldName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`,
+ );
+ expect(variablesCss, `Expected ${newName} to fall back to ${oldName}`).toMatch(pattern);
+ }
+ });
+ });
+
+ describe('preset themes', () => {
+ /** Extract variables declared inside each .sd-theme-* block. */
+ const extractThemeBlocks = (css: string): Map> => {
+ const themes = new Map>();
+ const blockRegex = /\.(sd-theme-[\w-]+)\s*\{([^}]+)\}/g;
+ for (const match of css.matchAll(blockRegex)) {
+ const vars = new Set();
+ for (const decl of match[2].matchAll(/(--sd-[\w-]+)\s*:/g)) {
+ vars.add(decl[1]);
+ }
+ themes.set(match[1], vars);
+ }
+ return themes;
+ };
+
+ const themeBlocks = extractThemeBlocks(themesCss);
+
+ it('contains all expected preset themes', () => {
+ expect([...themeBlocks.keys()].sort()).toEqual(['sd-theme-blueprint', 'sd-theme-docs', 'sd-theme-word']);
+ });
+
+ it('every theme variable is declared in variables.css', () => {
+ const declaredInVariables = extractDeclaredVars(variablesCss);
+ const broken: string[] = [];
+ for (const [theme, vars] of themeBlocks) {
+ for (const v of vars) {
+ if (!declaredInVariables.has(v)) {
+ broken.push(`${theme}: ${v}`);
+ }
+ }
+ }
+ expect(broken, `Theme variables not in variables.css: ${broken.join(', ')}`).toEqual([]);
+ });
+ });
+});
diff --git a/packages/superdoc/src/assets/styles/helpers/themes.css b/packages/superdoc/src/assets/styles/helpers/themes.css
new file mode 100644
index 0000000000..4b3a29ba82
--- /dev/null
+++ b/packages/superdoc/src/assets/styles/helpers/themes.css
@@ -0,0 +1,166 @@
+/* Preset theme: Docs-like */
+.sd-theme-docs {
+ /* UI base */
+ --sd-ui-font-family: Arial, sans-serif;
+ --sd-ui-text: #202124;
+ --sd-ui-bg: #f1f3f4;
+ --sd-ui-border: #dadce0;
+ --sd-ui-action: #1a73e8;
+ --sd-ui-action-hover: #185abc;
+
+ /* UI: toolbar/dropdown/context */
+ --sd-ui-toolbar-bg: #f0f4f9;
+ --sd-ui-toolbar-button-text: #3c4043;
+ --sd-ui-toolbar-button-hover-bg: #e8eaed;
+ --sd-ui-toolbar-button-active-bg: #d2e3fc;
+ --sd-ui-toolbar-item-padding: 4px;
+
+ --sd-ui-dropdown-border: #dadce0;
+ --sd-ui-dropdown-bg: #ffffff;
+ --sd-ui-dropdown-hover-bg: #e8eaed;
+ --sd-ui-dropdown-active-bg: #d2e3fc;
+ --sd-ui-dropdown-shadow: 0 1px 2px rgba(60, 64, 67, 0.3), 0 1px 3px 1px rgba(60, 64, 67, 0.15);
+
+ --sd-ui-menu-bg: #ffffff;
+ --sd-ui-menu-border: #dadce0;
+ --sd-ui-menu-shadow: 0 1px 2px rgba(60, 64, 67, 0.25), 0 3px 8px rgba(60, 64, 67, 0.18);
+ --sd-ui-menu-item-hover-bg: #e8eaed;
+ --sd-ui-menu-item-active-bg: #d2e3fc;
+
+ /* Comments */
+ --sd-ui-comments-card-bg: #d7dde8;
+ --sd-ui-comments-card-active-bg: #ffffff;
+ --sd-ui-comments-card-active-border: #c7cdd6;
+ --sd-ui-comments-card-shadow: 0 1px 2px rgba(60, 64, 67, 0.2), 0 2px 6px rgba(60, 64, 67, 0.18);
+ --sd-ui-comments-separator: #c7cdd6;
+ --sd-ui-comments-timestamp-text: #5f6368;
+ --sd-ui-comments-option-hover-bg: #e8eaed;
+ --sd-ui-comments-input-border: #80868b;
+
+ --sd-comments-highlight-external-base: #f9ab00;
+ --sd-comments-highlight-external: #f9ab0033;
+ --sd-comments-highlight-external-active: #f9ab0044;
+ --sd-comments-highlight-external-faded: #f9ab001f;
+ --sd-comments-highlight-external-nested-border: #f9ab0088;
+ --sd-comments-highlight-internal-base: #188038;
+ --sd-comments-highlight-internal: #18803833;
+ --sd-comments-highlight-internal-active: #18803844;
+ --sd-comments-highlight-internal-faded: #1880381f;
+ --sd-comments-highlight-internal-nested-border: #18803888;
+
+ /* Layout */
+ --sd-layout-page-bg: #ffffff;
+ --sd-layout-page-shadow: 0 1px 2px rgba(60, 64, 67, 0.2), 0 2px 6px rgba(60, 64, 67, 0.12);
+}
+
+/* Preset theme: Microsoft Word-like */
+.sd-theme-word {
+ /* UI base */
+ --sd-ui-font-family: 'Segoe UI', Arial, sans-serif;
+ --sd-ui-text: #323130;
+ --sd-ui-bg: #f3f2f1;
+ --sd-ui-border: #d2d0ce;
+ --sd-ui-action: #185abd;
+ --sd-ui-action-hover: #134a9e;
+
+ /* UI: toolbar/dropdown/context */
+ --sd-ui-toolbar-bg: #f3f2f1;
+ --sd-ui-toolbar-button-text: #201f1e;
+ --sd-ui-toolbar-button-hover-bg: #e8e6e4;
+ --sd-ui-toolbar-button-active-bg: #d2d0ce;
+ --sd-ui-toolbar-item-padding: 4px;
+
+ --sd-ui-dropdown-border: #d2d0ce;
+ --sd-ui-dropdown-bg: #ffffff;
+ --sd-ui-dropdown-hover-bg: #e8e6e4;
+ --sd-ui-dropdown-active-bg: #d2d0ce;
+ --sd-ui-dropdown-shadow: 0 1px 2px rgba(0, 0, 0, 0.14), 0 4px 8px rgba(0, 0, 0, 0.08);
+
+ --sd-ui-menu-bg: #ffffff;
+ --sd-ui-menu-border: #d2d0ce;
+ --sd-ui-menu-shadow: 0 2px 6px rgba(0, 0, 0, 0.14);
+ --sd-ui-menu-item-hover-bg: #f3f2f1;
+ --sd-ui-menu-item-active-bg: #edebe9;
+
+ /* Comments */
+ --sd-ui-comments-card-bg: #ebebeb;
+ --sd-ui-comments-card-active-bg: #ffffff;
+ --sd-ui-comments-card-active-border: #d2d0ce;
+ --sd-ui-comments-card-shadow: 0 2px 4px rgba(0, 0, 0, 0.16), 0 10px 20px rgba(0, 0, 0, 0.1);
+ --sd-ui-comments-separator: #c8c6c4;
+ --sd-ui-comments-timestamp-text: #605e5c;
+ --sd-ui-comments-option-hover-bg: #f3f2f1;
+ --sd-ui-comments-input-border: #8a8886;
+
+ --sd-comments-highlight-external-base: #8a8886;
+ --sd-comments-highlight-external: #8a888633;
+ --sd-comments-highlight-external-active: #8a888655;
+ --sd-comments-highlight-external-faded: #8a88861f;
+ --sd-comments-highlight-external-nested-border: #8a888688;
+ --sd-comments-highlight-internal-base: #0f6cbd;
+ --sd-comments-highlight-internal: #0f6cbd33;
+ --sd-comments-highlight-internal-active: #0f6cbd55;
+ --sd-comments-highlight-internal-faded: #0f6cbd1f;
+ --sd-comments-highlight-internal-nested-border: #0f6cbd88;
+
+ /* Layout */
+ --sd-layout-page-bg: #ffffff;
+ --sd-layout-page-shadow: 0 1px 2px rgba(0, 0, 0, 0.16), 0 6px 12px rgba(0, 0, 0, 0.08);
+}
+
+/* Preset theme: Blueprint */
+.sd-theme-blueprint {
+ /* UI base */
+ --sd-ui-font-family: 'IBM Plex Sans', 'Segoe UI', Arial, sans-serif;
+ --sd-ui-text: #0f172a;
+ --sd-ui-text-muted: #334155;
+ --sd-ui-text-disabled: #64748b;
+ --sd-ui-bg: #e8eff5;
+ --sd-ui-hover-bg: #d3e2ef;
+ --sd-ui-active-bg: #bfd4e6;
+ --sd-ui-border: #9fb4c7;
+ --sd-ui-action: #0f766e;
+ --sd-ui-action-hover: #115e59;
+ --sd-ui-shadow: 0 2px 8px rgba(15, 23, 42, 0.18);
+
+ /* Toolbar / dropdown / context */
+ --sd-ui-toolbar-bg: #dbe6f0;
+ --sd-ui-toolbar-button-text: #0f172a;
+ --sd-ui-toolbar-button-hover-bg: #c9dced;
+ --sd-ui-toolbar-button-active-bg: #b7cee3;
+ --sd-ui-dropdown-bg: #ffffff;
+ --sd-ui-dropdown-border: #8ea5b8;
+ --sd-ui-dropdown-hover-bg: #e7f0f8;
+ --sd-ui-dropdown-active-bg: #cde2f3;
+ --sd-ui-dropdown-shadow: 0 8px 24px rgba(15, 23, 42, 0.22);
+ --sd-ui-menu-bg: #ffffff;
+ --sd-ui-menu-border: #8ea5b8;
+ --sd-ui-menu-item-hover-bg: #e2eef8;
+ --sd-ui-menu-item-active-bg: #cde2f3;
+ --sd-ui-menu-shadow: 0 8px 24px rgba(15, 23, 42, 0.22);
+
+ /* Layout */
+ --sd-layout-page-bg: #ffffff;
+ --sd-layout-page-shadow: 0 8px 24px rgba(15, 23, 42, 0.16);
+
+ /* Comments */
+ --sd-ui-comments-card-bg: #dbe7f2;
+ --sd-ui-comments-card-active-bg: #ffffff;
+ --sd-ui-comments-card-active-border: #8ea5b8;
+ --sd-ui-comments-card-shadow: 0 10px 24px rgba(15, 23, 42, 0.18);
+ --sd-ui-comments-input-border: #7f96aa;
+ --sd-ui-comments-separator: #9fb4c7;
+ --sd-ui-comments-timestamp-text: #475569;
+
+ /* Highlights */
+ --sd-comments-highlight-external-base: #b45309;
+ --sd-comments-highlight-external: #b4530933;
+ --sd-comments-highlight-external-active: #b4530944;
+ --sd-comments-highlight-external-faded: #b453091f;
+ --sd-comments-highlight-external-nested-border: #b4530988;
+ --sd-comments-highlight-internal-base: #0f766e;
+ --sd-comments-highlight-internal: #0f766e33;
+ --sd-comments-highlight-internal-active: #0f766e44;
+ --sd-comments-highlight-internal-faded: #0f766e1f;
+ --sd-comments-highlight-internal-nested-border: #0f766e88;
+}
diff --git a/packages/superdoc/src/assets/styles/helpers/variables.css b/packages/superdoc/src/assets/styles/helpers/variables.css
index d9bb9236df..df9bac3657 100644
--- a/packages/superdoc/src/assets/styles/helpers/variables.css
+++ b/packages/superdoc/src/assets/styles/helpers/variables.css
@@ -1,3 +1,200 @@
:root {
- /* CSS variables */
+ /* SuperDoc CSS Variables */
+
+ /* Primitive: colors */
+ --sd-color-blue-50: #ebf0ff;
+ --sd-color-blue-100: #d6e1ff;
+ --sd-color-blue-200: #adc3ff;
+ --sd-color-blue-300: #85a5ff;
+ --sd-color-blue-400: #5c87ff;
+ --sd-color-blue-500: #1355ff;
+ --sd-color-blue-600: #0f44cc;
+ --sd-color-blue-700: #0b3399;
+ --sd-color-blue-800: #082266;
+ --sd-color-blue-900: #041133;
+
+ --sd-color-gray-50: #fafafa;
+ --sd-color-gray-100: #f5f5f5;
+ --sd-color-gray-200: #f2f2f2;
+ --sd-color-gray-300: #e0e0e0;
+ --sd-color-gray-400: #dbdbdb;
+ --sd-color-gray-500: #ababab;
+ --sd-color-gray-600: #888888;
+ --sd-color-gray-700: #666666;
+ --sd-color-gray-800: #424242;
+ --sd-color-gray-900: #212121;
+
+ --sd-color-red-500: #ed4337;
+ --sd-color-green-500: #00853d;
+ --sd-color-rose-500: #cb0e47;
+
+ /* Primitive: font-size */
+ --sd-font-size-50: 10px;
+ --sd-font-size-100: 11px;
+ --sd-font-size-200: 12px;
+ --sd-font-size-300: 13px;
+ --sd-font-size-400: 14px;
+ --sd-font-size-500: 15px;
+ --sd-font-size-600: 16px;
+
+ /* Primitive: radius — old names (--sd-radius-sm/md/lg) honored via fallback */
+ --sd-radius-50: var(--sd-radius-sm, 4px);
+ --sd-radius-100: 6px;
+ --sd-radius-200: var(--sd-radius-md, 8px);
+ --sd-radius-300: var(--sd-radius-lg, 12px);
+
+ /*
+ * UI: general (semantic tier)
+ *
+ * These are the "5-variable theme" — change these and the entire UI updates.
+ * Component-specific variables below cascade from these defaults.
+ *
+ * Old-name fallbacks (var(--sd-old-name, ...)) honor pre-existing customer
+ * overrides. They resolve at computed-value time, so setting the old name on
+ * any element still works. These fallbacks will be removed in the next major.
+ */
+ --sd-ui-font-family: var(--sd-font-family, Arial, Helvetica, sans-serif);
+ --sd-ui-text: var(--sd-text-primary, #47484a);
+ --sd-ui-text-muted: var(--sd-text-muted, var(--sd-color-gray-700));
+ --sd-ui-text-disabled: var(--sd-text-placeholder, var(--sd-color-gray-500));
+ --sd-ui-bg: var(--sd-surface-card, #ffffff);
+ --sd-ui-hover-bg: var(--sd-surface-hover, var(--sd-color-gray-400));
+ --sd-ui-active-bg: #c8d0d8;
+ --sd-ui-disabled-bg: var(--sd-surface-muted, var(--sd-color-gray-100));
+ --sd-ui-border: var(--sd-border-default, var(--sd-color-gray-400));
+ --sd-ui-action: var(--sd-action-primary, var(--sd-color-blue-500));
+ --sd-ui-action-hover: var(--sd-action-primary-hover, var(--sd-color-blue-600));
+ --sd-ui-action-text: #ffffff;
+ --sd-ui-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
+ --sd-ui-radius: var(--sd-radius-100);
+
+ --sd-ui-font-size-50: var(--sd-font-size-50);
+ --sd-ui-font-size-100: var(--sd-font-size-100);
+ --sd-ui-font-size-200: var(--sd-font-size-200);
+ --sd-ui-font-size-300: var(--sd-font-size-300);
+ --sd-ui-font-size-400: var(--sd-font-size-400);
+ --sd-ui-font-size-500: var(--sd-font-size-500);
+ --sd-ui-font-size-600: var(--sd-font-size-600);
+
+ /* UI: dropdown — cascades from semantic tier */
+ --sd-ui-dropdown-border: var(--sd-ui-border);
+ --sd-ui-dropdown-bg: var(--sd-ui-bg);
+ --sd-ui-dropdown-option-radius: 3px;
+ --sd-ui-dropdown-text: var(--sd-ui-text);
+ --sd-ui-dropdown-hover-text: var(--sd-ui-dropdown-text);
+ --sd-ui-dropdown-selected-text: var(--sd-ui-dropdown-text);
+ --sd-ui-dropdown-hover-bg: var(--sd-ui-hover-bg);
+ --sd-ui-dropdown-active-bg: var(--sd-ui-active-bg);
+ --sd-ui-dropdown-shadow: 0 8px 24px rgba(0, 0, 0, 0.16);
+
+ /* UI: tooltip — intentionally inverted palette (dark bg, light text) */
+ --sd-ui-tooltip-bg: #262626;
+ --sd-ui-tooltip-text: #ffffff;
+ --sd-ui-tooltip-radius: var(--sd-radius-100);
+ --sd-ui-tooltip-shadow: 0 3px 12px rgba(0, 0, 0, 0.28);
+
+ /* UI: toolbar — cascades from semantic tier */
+ --sd-ui-toolbar-height: 32px;
+ --sd-ui-toolbar-padding-x: 16px;
+ --sd-ui-toolbar-padding-y: 4px;
+ --sd-ui-toolbar-item-gap: 2px;
+ --sd-ui-toolbar-item-padding: 5px;
+ --sd-ui-toolbar-bg: transparent;
+ --sd-ui-toolbar-button-text: var(--sd-ui-text);
+ --sd-ui-toolbar-button-hover-bg: var(--sd-ui-hover-bg);
+ --sd-ui-toolbar-button-active-bg: var(--sd-ui-active-bg);
+
+ /* UI: context menu — cascades from semantic tier */
+ --sd-ui-menu-bg: var(--sd-ui-bg);
+ --sd-ui-menu-text: var(--sd-ui-text);
+ --sd-ui-menu-font-size: var(--sd-font-size-200);
+ --sd-ui-menu-radius: 0;
+ --sd-ui-menu-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05), 0px 10px 20px rgba(0, 0, 0, 0.1);
+ --sd-ui-menu-border: var(--sd-ui-border);
+ --sd-ui-menu-input-border: var(--sd-ui-border);
+ --sd-ui-menu-input-focus-border: var(--sd-ui-action);
+ --sd-ui-menu-item-hover-bg: var(--sd-ui-hover-bg);
+ --sd-ui-menu-item-active-bg: var(--sd-ui-active-bg);
+ --sd-ui-menu-item-active-text: var(--sd-ui-action);
+
+ /* UI: tools */
+ --sd-ui-tools-gap: 6px;
+ --sd-ui-tools-item-size: 50px;
+ --sd-ui-tools-item-radius: var(--sd-radius-300);
+ --sd-ui-tools-item-bg: rgba(219, 219, 219, 0.6);
+ --sd-ui-tools-icon-size: 20px;
+
+ /* UI: comments — cascades from semantic tier, old names honored via fallback */
+ --sd-ui-comments-card-bg: var(--sd-comment-bg, #f3f6fd);
+ --sd-ui-comments-card-hover-bg: var(--sd-comment-bg-hover, var(--sd-ui-comments-card-bg));
+ --sd-ui-comments-card-active-bg: var(--sd-comment-bg-active, var(--sd-ui-bg));
+ --sd-ui-comments-card-resolved-bg: var(--sd-comment-bg-resolved, var(--sd-ui-disabled-bg));
+ --sd-ui-comments-card-active-border: var(--sd-comment-border-active, var(--sd-ui-border));
+ --sd-ui-comments-card-radius: var(--sd-comment-radius, var(--sd-radius-300));
+ --sd-ui-comments-card-padding: var(--sd-comment-padding, 16px);
+ --sd-ui-comments-card-shadow: var(--sd-comment-shadow, 0px 4px 12px 0px rgba(50, 50, 50, 0.15));
+ --sd-ui-comments-separator: var(--sd-comment-separator, var(--sd-ui-border));
+ --sd-ui-comments-transition: var(--sd-comment-transition, all 200ms ease);
+ --sd-ui-comments-author-text: var(--sd-comment-author-color, var(--sd-ui-text));
+ --sd-ui-comments-author-size: var(--sd-comment-author-size, var(--sd-font-size-400));
+ --sd-ui-comments-author-weight: var(--sd-comment-author-weight, 600);
+ --sd-ui-comments-tag-text: var(--sd-ui-text-muted);
+ --sd-ui-comments-tag-bg: var(--sd-color-gray-200);
+ --sd-ui-comments-timestamp-text: var(--sd-comment-time-color, var(--sd-ui-text-muted));
+ --sd-ui-comments-timestamp-size: var(--sd-comment-time-size, var(--sd-font-size-200));
+ --sd-ui-comments-body-text: var(--sd-ui-text);
+ --sd-ui-comments-body-size: var(--sd-comment-body-size, var(--sd-font-size-400));
+ --sd-ui-comments-resolved-text: var(--sd-color-green-500);
+ --sd-ui-comments-insert-text: var(--sd-comment-tc-insert-color, var(--sd-color-green-500));
+ --sd-ui-comments-delete-text: var(--sd-comment-tc-delete-color, var(--sd-color-rose-500));
+ --sd-ui-comments-dropdown-border: var(--sd-ui-border);
+ --sd-ui-comments-dropdown-bg: var(--sd-ui-bg);
+ --sd-ui-comments-dropdown-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
+ --sd-ui-comments-option-size: var(--sd-font-size-400);
+ --sd-ui-comments-option-text: var(--sd-ui-comments-body-text);
+ --sd-ui-comments-option-hover-text: var(--sd-ui-comments-option-text);
+ --sd-ui-comments-option-selected-text: var(--sd-ui-comments-option-text);
+ --sd-ui-comments-option-hover-bg: var(--sd-ui-hover-bg);
+ --sd-ui-comments-input-bg: var(--sd-ui-bg);
+ --sd-ui-comments-input-border: var(--sd-ui-border);
+ --sd-ui-comments-internal-bg: var(--sd-comment-internal-bg, #cde6e6);
+ --sd-ui-comments-external-bg: var(--sd-comment-external-bg, #f5cfda);
+
+ /* Styles: comments — old names honored via fallback */
+ --sd-comments-highlight-external-base: #b1124b;
+ --sd-comments-highlight-external: var(--sd-comment-highlight-external, #b1124b40);
+ --sd-comments-highlight-external-active: #b1124b66;
+ --sd-comments-highlight-external-faded: #b1124b20;
+ --sd-comments-highlight-internal-base: #078383;
+ --sd-comments-highlight-internal: var(--sd-comment-highlight-internal, #07838340);
+ --sd-comments-highlight-internal-active: #07838366;
+ --sd-comments-highlight-internal-faded: #07838320;
+ --sd-comments-highlight-external-nested-border: #b1124b99;
+ --sd-comments-highlight-internal-nested-border: #07838399;
+ --sd-comments-highlight-hover: #1354ff55;
+ --sd-comments-selection-background: #1354ff55;
+
+ /* Styles: tracked changes — old names honored via fallback */
+ --sd-tracked-changes-insert-background: var(--sd-track-insert-bg, #399c7222);
+ --sd-tracked-changes-insert-border: var(--sd-track-insert-border, #00853d);
+ --sd-tracked-changes-delete-background: var(--sd-track-delete-bg, #cb0e4722);
+ --sd-tracked-changes-delete-border: var(--sd-track-delete-border, #cb0e47);
+ --sd-tracked-changes-format-border: var(--sd-track-format-border, gold);
+ --sd-tracked-changes-insert-background-focused: #399c7244;
+ --sd-tracked-changes-delete-background-focused: #cb0e4744;
+ --sd-tracked-changes-format-background-focused: #ffd70033;
+
+ /* Styles: content controls (SDT) — blue accent, intentionally standalone */
+ --sd-content-controls-block-border: #629be7;
+ --sd-content-controls-block-hover-bg: var(--sd-ui-hover-bg);
+ --sd-content-controls-inline-border: #629be7;
+ --sd-content-controls-inline-hover-bg: var(--sd-ui-hover-bg);
+ --sd-content-controls-label-border: #629be7;
+ --sd-content-controls-label-bg: #629be7ee;
+ --sd-content-controls-label-text: #ffffff;
+ --sd-content-controls-lock-hover-bg: rgba(98, 155, 231, 0.08);
+
+ /* Styles: layout — cascades from semantic tier */
+ --sd-layout-page-bg: var(--sd-ui-bg);
+ --sd-layout-page-shadow: 0 4px 20px rgba(15, 23, 42, 0.08);
}
diff --git a/packages/superdoc/src/assets/styles/tokens.css b/packages/superdoc/src/assets/styles/tokens.css
deleted file mode 100644
index 9105f36522..0000000000
--- a/packages/superdoc/src/assets/styles/tokens.css
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- * SuperDoc Design Tokens as CSS Custom Properties
- *
- * Generated from brand/tokens/. In production, a build step (Style Dictionary)
- * would generate this file automatically. For now, it's maintained manually.
- *
- * Users of SuperDoc can override any of these variables to customize
- * the look and feel of every component.
- */
-
-:root {
- /* ─── Primitive: Colors ─── */
- --sd-color-blue-50: #ebf0ff;
- --sd-color-blue-100: #d6e1ff;
- --sd-color-blue-200: #adc3ff;
- --sd-color-blue-300: #85a5ff;
- --sd-color-blue-400: #5c87ff;
- --sd-color-blue-500: #1355ff;
- --sd-color-blue-600: #0f44cc;
- --sd-color-blue-700: #0b3399;
- --sd-color-blue-800: #082266;
- --sd-color-blue-900: #041133;
-
- --sd-color-gray-50: #fafafa;
- --sd-color-gray-100: #f5f5f5;
- --sd-color-gray-200: #f2f2f2;
- --sd-color-gray-300: #e0e0e0;
- --sd-color-gray-400: #dbdbdb;
- --sd-color-gray-500: #ababab;
- --sd-color-gray-600: #888888;
- --sd-color-gray-700: #666666;
- --sd-color-gray-800: #424242;
- --sd-color-gray-900: #212121;
-
- --sd-color-red-500: #ed4337;
- --sd-color-green-500: #00853d;
- --sd-color-rose-500: #cb0e47;
-
- /* ─── Primitive: Typography ─── */
- --sd-font-family: 'Inter', system-ui, sans-serif;
- --sd-font-mono: 'JetBrains Mono', monospace;
-
- /* ─── Primitive: Spacing ─── */
- --sd-radius-sm: 4px;
- --sd-radius-md: 8px;
- --sd-radius-lg: 12px;
-
- /* ─── Semantic: Surfaces ─── */
- --sd-surface-page: #ffffff;
- --sd-surface-canvas: var(--sd-color-gray-50);
- --sd-surface-card: #ffffff;
- --sd-surface-muted: var(--sd-color-gray-100);
- --sd-surface-hover: var(--sd-color-gray-200);
- --sd-surface-selected: var(--sd-color-blue-100);
-
- /* ─── Semantic: Text ─── */
- --sd-text-primary: var(--sd-color-gray-900);
- --sd-text-secondary: var(--sd-color-gray-700);
- --sd-text-muted: var(--sd-color-gray-600);
- --sd-text-placeholder: var(--sd-color-gray-500);
-
- /* ─── Semantic: Borders ─── */
- --sd-border-default: var(--sd-color-gray-400);
- --sd-border-subtle: var(--sd-color-gray-300);
- --sd-border-focus: var(--sd-color-blue-500);
-
- /* ─── Semantic: Actions ─── */
- --sd-action-primary: var(--sd-color-blue-500);
- --sd-action-primary-hover: var(--sd-color-blue-600);
-
- /* ─── Component: Comment Dialog ─── */
- --sd-comment-bg: #f3f6fd;
- --sd-comment-bg-hover: #f3f6fd;
- --sd-comment-bg-active: var(--sd-surface-card);
- --sd-comment-bg-resolved: #f0f0f0;
- --sd-comment-border-active: var(--sd-border-subtle);
- --sd-comment-radius: var(--sd-radius-lg);
- --sd-comment-padding: 16px;
- --sd-comment-shadow: 0px 4px 12px 0px rgba(50, 50, 50, 0.15);
- --sd-comment-max-width: 300px;
- --sd-comment-min-width: 200px;
- --sd-comment-separator: var(--sd-border-subtle);
- --sd-comment-transition: all 200ms ease;
-
- /* Comment: author & timestamp */
- --sd-comment-author-color: var(--sd-text-primary);
- --sd-comment-author-size: 14px;
- --sd-comment-author-weight: 600;
- --sd-comment-time-color: var(--sd-text-muted);
- --sd-comment-time-size: 12px;
- --sd-comment-body-size: 14px;
-
- /* Comment: tracked change text colors */
- --sd-comment-tc-insert-color: var(--sd-color-green-500);
- --sd-comment-tc-delete-color: var(--sd-color-rose-500);
-
- /* Comment: document highlights */
- --sd-comment-highlight-internal: #078383;
- --sd-comment-highlight-external: #b1124b;
- --sd-comment-highlight-opacity: 0.2;
- --sd-comment-highlight-opacity-active: 0.4;
-
- /* Comment: internal/external toggle */
- --sd-comment-internal-bg: #cde6e6;
- --sd-comment-external-bg: #f5cfda;
-
- /* Comment: group bubble (collapsed) */
- --sd-comment-group-bg: #e2e9fb;
- --sd-comment-group-color: var(--sd-action-primary);
- --sd-comment-group-size: 51px;
- --sd-comment-group-expanded-size: 300px;
-}
diff --git a/packages/superdoc/src/components/CommentsLayer/CommentDialog.vue b/packages/superdoc/src/components/CommentsLayer/CommentDialog.vue
index 1c8f4e11d3..b3e91645f5 100644
--- a/packages/superdoc/src/components/CommentsLayer/CommentDialog.vue
+++ b/packages/superdoc/src/components/CommentsLayer/CommentDialog.vue
@@ -754,18 +754,18 @@ watch(editingCommentId, (commentId) => {
.comments-dialog {
display: flex;
flex-direction: column;
- padding: var(--sd-comment-padding, 16px);
- border-radius: var(--sd-comment-radius, 12px);
- background-color: var(--sd-comment-bg, #f3f6fd);
+ padding: var(--sd-ui-comments-card-padding, 16px);
+ border-radius: var(--sd-ui-comments-card-radius, 12px);
+ background-color: var(--sd-ui-comments-card-bg, #f3f6fd);
border: 1px solid transparent;
font-family: var(--sd-ui-font-family, Arial, Helvetica, sans-serif);
- font-size: var(--sd-comment-body-size, 14px);
+ font-size: var(--sd-ui-comments-body-size, 14px);
line-height: 1.5;
- transition: var(--sd-comment-transition, all 200ms ease);
+ transition: var(--sd-ui-comments-transition, all 200ms ease);
box-shadow: none;
z-index: 5;
- max-width: var(--sd-comment-max-width, 300px);
- min-width: var(--sd-comment-min-width, 200px);
+ max-width: 300px;
+ min-width: 200px;
width: 100%;
overflow-wrap: break-word;
word-break: break-word;
@@ -774,33 +774,33 @@ watch(editingCommentId, (commentId) => {
cursor: pointer;
}
.comments-dialog:not(.is-active):not(.is-resolved):hover {
- background-color: var(--sd-comment-bg-hover, #f3f6fd);
+ background-color: var(--sd-ui-comments-card-hover-bg, #f3f6fd);
}
.comments-dialog:not(.is-resolved):hover :deep(.overflow-menu) {
opacity: 1;
pointer-events: auto;
}
.comments-dialog.is-active {
- background-color: var(--sd-comment-bg-active, #ffffff);
- border-color: var(--sd-comment-border-active, #e0e0e0);
- box-shadow: var(--sd-comment-shadow, 0px 4px 12px 0px rgba(50, 50, 50, 0.15));
+ background-color: var(--sd-ui-comments-card-active-bg, #ffffff);
+ border-color: var(--sd-ui-comments-card-active-border, #e0e0e0);
+ box-shadow: var(--sd-ui-comments-card-shadow, 0px 4px 12px 0px rgba(50, 50, 50, 0.15));
z-index: 10;
}
.comments-dialog.is-resolved {
- background-color: var(--sd-comment-bg-resolved, #f0f0f0);
+ background-color: var(--sd-ui-comments-card-resolved-bg, #f0f0f0);
}
.comment-separator {
- background-color: var(--sd-comment-separator, #e0e0e0);
+ background-color: var(--sd-ui-comments-separator, #e0e0e0);
height: 1px;
width: 100%;
margin: 10px 0;
}
.comment {
- font-size: var(--sd-comment-body-size, 14px);
+ font-size: var(--sd-ui-comments-body-size, 14px);
line-height: 1.5;
- color: var(--sd-comment-author-color, #212121);
+ color: var(--sd-ui-comments-body-text, #212121);
margin: 4px 0 0 0;
}
.comment :deep(p) {
@@ -808,22 +808,22 @@ watch(editingCommentId, (commentId) => {
}
.tracked-change {
- font-size: var(--sd-comment-body-size, 14px);
+ font-size: var(--sd-ui-comments-body-size, 14px);
line-height: 1.5;
- color: var(--sd-comment-author-color, #212121);
+ color: var(--sd-ui-comments-body-text, #212121);
margin: 4px 0 0 0;
}
.change-type {
- color: var(--sd-comment-author-color, #212121);
+ color: var(--sd-ui-comments-body-text, #212121);
}
.tracked-change-text {
- color: var(--sd-comment-author-color, #212121);
+ color: var(--sd-ui-comments-body-text, #212121);
}
.tracked-change-text.is-deleted {
- color: var(--sd-comment-tc-delete-color, #cb0e47);
+ color: var(--sd-ui-comments-delete-text, #cb0e47);
}
.tracked-change-text.is-inserted {
- color: var(--sd-comment-tc-insert-color, #00853d);
+ color: var(--sd-ui-comments-insert-text, #00853d);
font-weight: 500;
}
@@ -834,7 +834,7 @@ watch(editingCommentId, (commentId) => {
gap: 4px;
font-size: 11px;
font-weight: 500;
- color: var(--sd-color-green-500, #00853d);
+ color: var(--sd-ui-comments-resolved-text, #00853d);
margin-bottom: 4px;
}
.resolved-badge__icon {
@@ -858,7 +858,7 @@ watch(editingCommentId, (commentId) => {
}
.show-more-toggle {
font-size: 12px;
- color: var(--sd-action-primary, #1355ff);
+ color: var(--sd-ui-action, #1355ff);
cursor: pointer;
font-weight: 500;
margin-top: 4px;
@@ -875,7 +875,7 @@ watch(editingCommentId, (commentId) => {
gap: 6px;
padding: 8px 0;
font-size: 12px;
- color: var(--sd-action-primary, #1355ff);
+ color: var(--sd-ui-action, #1355ff);
font-weight: 500;
cursor: pointer;
user-select: none;
@@ -890,7 +890,7 @@ watch(editingCommentId, (commentId) => {
--sd-comment-avatar-size: 20px;
--sd-comment-avatar-font-size: 8px;
margin-left: -4px;
- border: 2px solid var(--sd-comment-bg-active, #ffffff);
+ border: 2px solid var(--sd-ui-comments-card-active-bg, #ffffff);
}
.collapsed-avatars .mini-avatar:first-child {
margin-left: 0;
@@ -898,10 +898,10 @@ watch(editingCommentId, (commentId) => {
/* ── New comment input ── */
.new-comment-input-wrapper {
- border: 1.5px solid var(--sd-border-default, #dbdbdb);
+ border: 1.5px solid var(--sd-ui-comments-input-border, #dbdbdb);
border-radius: 12px;
padding: 8.5px 10.5px;
- background: var(--sd-surface-card, #ffffff);
+ background: var(--sd-ui-comments-input-bg, #ffffff);
margin-top: 4px;
max-height: 150px;
overflow-y: auto;
@@ -945,10 +945,10 @@ watch(editingCommentId, (commentId) => {
margin-top: 10px;
}
.reply-input-wrapper {
- border: 1.5px solid var(--sd-border-default, #dbdbdb);
+ border: 1.5px solid var(--sd-ui-comments-input-border, #dbdbdb);
border-radius: 12px;
padding: 8.5px 10.5px;
- background: var(--sd-surface-card, #ffffff);
+ background: var(--sd-ui-comments-input-bg, #ffffff);
max-height: 150px;
overflow-y: auto;
}
@@ -983,21 +983,21 @@ watch(editingCommentId, (commentId) => {
border: none;
font-size: 13px;
font-weight: 500;
- color: var(--sd-color-gray-700, #666666);
+ color: var(--sd-ui-text-muted, #666666);
cursor: pointer;
padding: 0;
font-family: inherit;
transition: color 150ms;
}
.reply-btn-cancel:hover {
- color: var(--sd-color-gray-900, #212121);
+ color: var(--sd-ui-text, #212121);
}
.reply-btn-primary {
- background: var(--sd-action-primary, #1355ff);
+ background: var(--sd-ui-action, #1355ff);
border: none;
font-size: 13px;
font-weight: 600;
- color: #ffffff;
+ color: var(--sd-ui-action-text, #ffffff);
cursor: pointer;
padding: 6px 16px;
border-radius: 9999px;
@@ -1005,7 +1005,7 @@ watch(editingCommentId, (commentId) => {
transition: background 150ms;
}
.reply-btn-primary:hover {
- background: var(--sd-action-primary-hover, #0f44cc);
+ background: var(--sd-ui-action-hover, #0f44cc);
}
.reply-btn-primary.is-disabled {
background: var(--sd-color-gray-400, #dbdbdb);
diff --git a/packages/superdoc/src/components/CommentsLayer/CommentHeader.vue b/packages/superdoc/src/components/CommentsLayer/CommentHeader.vue
index f7f74a692b..77bd5084b3 100644
--- a/packages/superdoc/src/components/CommentsLayer/CommentHeader.vue
+++ b/packages/superdoc/src/components/CommentsLayer/CommentHeader.vue
@@ -209,9 +209,9 @@ const getCurrentUser = computed(() => {
flex-direction: column;
}
.user-name {
- font-size: var(--sd-comment-author-size, 14px);
- font-weight: var(--sd-comment-author-weight, 600);
- color: var(--sd-comment-author-color, #212121);
+ font-size: var(--sd-ui-comments-author-size, 14px);
+ font-weight: var(--sd-ui-comments-author-weight, 600);
+ color: var(--sd-ui-comments-author-text, #212121);
line-height: 1.2em;
}
.imported-tag {
@@ -220,8 +220,8 @@ const getCurrentUser = computed(() => {
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
- color: var(--sd-comment-imported-tag-color, var(--sd-color-gray-600, #888888));
- background: var(--sd-comment-imported-tag-bg, var(--sd-color-gray-200, #f2f2f2));
+ color: var(--sd-ui-comments-tag-text, #888888);
+ background: var(--sd-ui-comments-tag-bg, #f2f2f2);
border-radius: 3px;
padding: 1px 4px;
margin-left: 6px;
@@ -230,8 +230,8 @@ const getCurrentUser = computed(() => {
}
.user-timestamp {
line-height: 1.2em;
- font-size: var(--sd-comment-time-size, 12px);
- color: var(--sd-comment-time-color, #888888);
+ font-size: var(--sd-ui-comments-timestamp-size, 12px);
+ color: var(--sd-ui-comments-timestamp-text, #888888);
}
.overflow-menu {
flex-shrink: 1;
@@ -259,7 +259,7 @@ const getCurrentUser = computed(() => {
transition: all 250ms ease;
}
.overflow-menu__icon:hover {
- background-color: var(--sd-comment-separator, #dbdbdb);
+ background-color: var(--sd-ui-comments-separator, #dbdbdb);
}
.overflow-menu__icon :deep(svg) {
width: 100%;
diff --git a/packages/superdoc/src/components/CommentsLayer/CommentsDropdown.vue b/packages/superdoc/src/components/CommentsLayer/CommentsDropdown.vue
index d3aca11bb2..a6bbd3d911 100644
--- a/packages/superdoc/src/components/CommentsLayer/CommentsDropdown.vue
+++ b/packages/superdoc/src/components/CommentsLayer/CommentsDropdown.vue
@@ -176,15 +176,16 @@ onBeforeUnmount(() => {
.comments-dropdown__menu {
min-width: 120px;
border-radius: 8px;
- border: 1px solid var(--sd-border-default, #dbdbdb);
- background: #fff;
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
+ border: 1px solid var(--sd-ui-comments-dropdown-border, #dbdbdb);
+ background: var(--sd-ui-comments-dropdown-bg, #fff);
+ box-shadow: var(--sd-ui-comments-dropdown-shadow, 0 8px 24px rgba(0, 0, 0, 0.12));
padding: 4px;
box-sizing: border-box;
}
.comments-dropdown__option {
- font-size: 14px;
+ font-size: var(--sd-ui-comments-option-size, 14px);
+ color: var(--sd-ui-comments-option-text, #212121);
display: flex;
align-items: center;
gap: 8px;
@@ -196,7 +197,8 @@ onBeforeUnmount(() => {
}
.comments-dropdown__option:hover {
- background-color: #f3f3f5;
+ background-color: var(--sd-ui-comments-option-hover-bg, #f3f3f5);
+ color: var(--sd-ui-comments-option-hover-text, #212121);
}
.comments-dropdown__option.disabled {
diff --git a/packages/superdoc/src/components/CommentsLayer/InternalDropdown.vue b/packages/superdoc/src/components/CommentsLayer/InternalDropdown.vue
index 0af31611ed..38ecc5c3f3 100644
--- a/packages/superdoc/src/components/CommentsLayer/InternalDropdown.vue
+++ b/packages/superdoc/src/components/CommentsLayer/InternalDropdown.vue
@@ -23,13 +23,13 @@ const options = [
label: 'Internal',
key: 'internal',
iconString: superdocIcons.internal,
- backgroundColor: '#CDE6E6',
+ backgroundColor: 'var(--sd-ui-comments-internal-bg, #CDE6E6)',
},
{
label: 'External',
key: 'external',
iconString: superdocIcons.external,
- backgroundColor: '#F5CFDA',
+ backgroundColor: 'var(--sd-ui-comments-external-bg, #F5CFDA)',
},
];
diff --git a/packages/superdoc/src/core/theme/create-theme.test.js b/packages/superdoc/src/core/theme/create-theme.test.js
new file mode 100644
index 0000000000..73fe1d89a4
--- /dev/null
+++ b/packages/superdoc/src/core/theme/create-theme.test.js
@@ -0,0 +1,166 @@
+import { describe, expect, it, beforeEach } from 'vitest';
+import { createTheme, buildTheme } from './create-theme.ts';
+
+describe('createTheme', () => {
+ beforeEach(() => {
+ document.querySelectorAll('[data-sd-theme]').forEach((el) => el.remove());
+ });
+
+ it('returns a class name with sd-theme- prefix', () => {
+ const cls = createTheme({ colors: { action: '#ff0000' } });
+ expect(cls).toMatch(/^sd-theme-/);
+ });
+
+ it('uses the provided name', () => {
+ expect(createTheme({ name: 'dark', colors: { bg: '#000' } })).toBe('sd-theme-dark');
+ });
+
+ it('generates unique names when no name provided', () => {
+ const a = createTheme({ colors: { action: '#111' } });
+ const b = createTheme({ colors: { action: '#222' } });
+ expect(a).not.toBe(b);
+ });
+
+ describe('color mapping', () => {
+ it('maps all color properties to CSS variables', () => {
+ const { css } = buildTheme({
+ name: 'colors-test',
+ colors: {
+ action: '#6366f1',
+ actionHover: '#4f46e5',
+ bg: '#ffffff',
+ hoverBg: '#f1f5f9',
+ activeBg: '#e2e8f0',
+ disabledBg: '#f5f5f5',
+ text: '#1e293b',
+ textMuted: '#64748b',
+ textDisabled: '#94a3b8',
+ border: '#e2e8f0',
+ },
+ });
+ expect(css).toContain('--sd-ui-action: #6366f1');
+ expect(css).toContain('--sd-ui-action-hover: #4f46e5');
+ expect(css).toContain('--sd-ui-bg: #ffffff');
+ expect(css).toContain('--sd-ui-hover-bg: #f1f5f9');
+ expect(css).toContain('--sd-ui-active-bg: #e2e8f0');
+ expect(css).toContain('--sd-ui-disabled-bg: #f5f5f5');
+ expect(css).toContain('--sd-ui-text: #1e293b');
+ expect(css).toContain('--sd-ui-text-muted: #64748b');
+ expect(css).toContain('--sd-ui-text-disabled: #94a3b8');
+ expect(css).toContain('--sd-ui-border: #e2e8f0');
+ });
+
+ it('ignores null and undefined color values', () => {
+ const { css } = buildTheme({
+ name: 'null-test',
+ colors: { action: '#ff0000', bg: undefined, text: null },
+ });
+ expect(css).toContain('--sd-ui-action: #ff0000');
+ expect(css).not.toContain('--sd-ui-bg');
+ expect(css).not.toContain('--sd-ui-text');
+ });
+
+ it('ignores unknown color keys', () => {
+ const { css } = buildTheme({
+ name: 'unknown-test',
+ colors: { action: '#ff0000', notAColor: '#000' },
+ });
+ expect(css).toContain('--sd-ui-action');
+ expect(css).not.toContain('notAColor');
+ });
+ });
+
+ describe('top-level shortcuts', () => {
+ it('maps font to --sd-ui-font-family', () => {
+ const { css } = buildTheme({ name: 'font-test', font: 'Inter, sans-serif' });
+ expect(css).toContain('--sd-ui-font-family: Inter, sans-serif');
+ });
+
+ it('maps radius to --sd-ui-radius', () => {
+ const { css } = buildTheme({ name: 'radius-test', radius: '8px' });
+ expect(css).toContain('--sd-ui-radius: 8px');
+ });
+
+ it('maps shadow to --sd-ui-shadow', () => {
+ const { css } = buildTheme({ name: 'shadow-test', shadow: '0 2px 8px rgba(0,0,0,0.1)' });
+ expect(css).toContain('--sd-ui-shadow: 0 2px 8px rgba(0,0,0,0.1)');
+ });
+ });
+
+ describe('vars escape hatch', () => {
+ it('spreads raw CSS variable overrides', () => {
+ const { css } = buildTheme({
+ name: 'vars-test',
+ vars: {
+ '--sd-ui-toolbar-bg': '#f8fafc',
+ '--sd-ui-comments-card-bg': '#f0f0ff',
+ },
+ });
+ expect(css).toContain('--sd-ui-toolbar-bg: #f8fafc');
+ expect(css).toContain('--sd-ui-comments-card-bg: #f0f0ff');
+ });
+
+ it('ignores null vars', () => {
+ const { css } = buildTheme({
+ name: 'vars-null',
+ vars: { '--sd-ui-toolbar-bg': '#fff', '--sd-ui-menu-bg': null },
+ });
+ expect(css).toContain('--sd-ui-toolbar-bg');
+ expect(css).not.toContain('--sd-ui-menu-bg');
+ });
+
+ it('combines colors and vars', () => {
+ const { css } = buildTheme({
+ name: 'combined',
+ colors: { action: '#6366f1' },
+ vars: { '--sd-ui-toolbar-bg': '#f8fafc' },
+ });
+ expect(css).toContain('--sd-ui-action: #6366f1');
+ expect(css).toContain('--sd-ui-toolbar-bg: #f8fafc');
+ });
+ });
+
+ describe('style injection', () => {
+ it('injects a style element into the document', () => {
+ const cls = createTheme({ name: 'inject', colors: { action: '#abc' } });
+ const style = document.querySelector(`[data-sd-theme="${cls}"]`);
+ expect(style).not.toBeNull();
+ expect(style.textContent).toContain('--sd-ui-action: #abc');
+ });
+
+ it('updates existing style element on re-call with same name', () => {
+ createTheme({ name: 'reuse', colors: { action: '#111' } });
+ createTheme({ name: 'reuse', colors: { action: '#222' } });
+ const styles = document.querySelectorAll('[data-sd-theme="sd-theme-reuse"]');
+ expect(styles.length).toBe(1);
+ expect(styles[0].textContent).toContain('#222');
+ });
+
+ it('returns class name even with empty config', () => {
+ const cls = createTheme({ name: 'empty' });
+ expect(cls).toBe('sd-theme-empty');
+ // No style element injected for empty config
+ expect(document.querySelector('[data-sd-theme="sd-theme-empty"]')).toBeNull();
+ });
+ });
+
+ describe('buildTheme', () => {
+ it('returns className and css', () => {
+ const result = buildTheme({ name: 'build', colors: { action: '#f00' } });
+ expect(result.className).toBe('sd-theme-build');
+ expect(result.css).toContain('.sd-theme-build');
+ expect(result.css).toContain('--sd-ui-action: #f00');
+ });
+
+ it('does not inject styles into the DOM', () => {
+ buildTheme({ name: 'no-inject', colors: { action: '#abc' } });
+ expect(document.querySelector('[data-sd-theme="sd-theme-no-inject"]')).toBeNull();
+ });
+
+ it('wraps css in the class selector', () => {
+ const { css } = buildTheme({ name: 'selector', colors: { bg: '#fff' } });
+ expect(css).toMatch(/^\.sd-theme-selector \{/);
+ expect(css).toMatch(/\}$/);
+ });
+ });
+});
diff --git a/packages/superdoc/src/core/theme/create-theme.ts b/packages/superdoc/src/core/theme/create-theme.ts
new file mode 100644
index 0000000000..449b360b1a
--- /dev/null
+++ b/packages/superdoc/src/core/theme/create-theme.ts
@@ -0,0 +1,166 @@
+export interface ThemeColors {
+ /** Action/accent color (buttons, links, active states). Default: #1355ff */
+ action?: string;
+ /** Action hover state. Default: #0f44cc */
+ actionHover?: string;
+ /** Text color on action-colored buttons. Default: #ffffff */
+ actionText?: string;
+ /** Default background for panels, cards, dropdowns. Default: #ffffff */
+ bg?: string;
+ /** Hover background. Default: #dbdbdb */
+ hoverBg?: string;
+ /** Active/pressed background. Default: #c8d0d8 */
+ activeBg?: string;
+ /** Disabled background. Default: #f5f5f5 */
+ disabledBg?: string;
+ /** Primary text color. Default: #47484a */
+ text?: string;
+ /** Secondary/muted text. Default: #666666 */
+ textMuted?: string;
+ /** Disabled text. Default: #ababab */
+ textDisabled?: string;
+ /** Default border color. Default: #dbdbdb */
+ border?: string;
+}
+
+export interface ThemeConfig {
+ /** Theme name — used in the generated class name (e.g., "dark" → "sd-theme-dark") */
+ name?: string;
+ /** UI font family */
+ font?: string;
+ /** Default border radius (e.g., "8px") */
+ radius?: string;
+ /** Default box shadow */
+ shadow?: string;
+ /** Core color palette — cascades to every component */
+ colors?: ThemeColors;
+ /** Escape hatch — raw CSS variable overrides (e.g., { '--sd-ui-toolbar-bg': '#f8fafc' }) */
+ vars?: Record;
+}
+
+export interface ThemeResult {
+ className: string;
+ css: string;
+}
+
+/*
+ * These map to the --sd-ui-* variable names introduced in the SD-2083
+ * theming system. Components consume them once that PR lands. Until then,
+ * createTheme() generates the correct variables ahead of time.
+ */
+const COLORS_TO_VARS: Record = {
+ action: '--sd-ui-action',
+ actionHover: '--sd-ui-action-hover',
+ actionText: '--sd-ui-action-text',
+ bg: '--sd-ui-bg',
+ hoverBg: '--sd-ui-hover-bg',
+ activeBg: '--sd-ui-active-bg',
+ disabledBg: '--sd-ui-disabled-bg',
+ text: '--sd-ui-text',
+ textMuted: '--sd-ui-text-muted',
+ textDisabled: '--sd-ui-text-disabled',
+ border: '--sd-ui-border',
+};
+
+let themeCounter = 0;
+
+/**
+ * Generate the className and CSS string from a theme config.
+ * Shared core used by both createTheme and buildTheme.
+ */
+function generateTheme(config: ThemeConfig): ThemeResult {
+ const { name, font, radius, shadow, colors, vars } = config;
+ const safeName = name ? name.replace(/[^a-zA-Z0-9-_]/g, '-') : null;
+ const className = `sd-theme-${safeName || `custom-${++themeCounter}`}`;
+
+ const declarations: string[] = [];
+
+ if (colors) {
+ for (const [key, value] of Object.entries(colors)) {
+ if (value == null) continue;
+ const varName = COLORS_TO_VARS[key];
+ if (varName) {
+ declarations.push(` ${varName}: ${value};`);
+ }
+ }
+ }
+
+ if (font != null) declarations.push(` --sd-ui-font-family: ${font};`);
+ if (radius != null) declarations.push(` --sd-ui-radius: ${radius};`);
+ if (shadow != null) declarations.push(` --sd-ui-shadow: ${shadow};`);
+
+ if (vars) {
+ for (const [varName, value] of Object.entries(vars)) {
+ if (value == null) continue;
+ declarations.push(` ${varName}: ${value};`);
+ }
+ }
+
+ const css = declarations.length > 0 ? `.${className} {\n${declarations.join('\n')}\n}` : '';
+
+ return { className, css };
+}
+
+/**
+ * Inject a theme's CSS into the document as a `...`;
+ * ```
+ */
+export function buildTheme(config: ThemeConfig): ThemeResult {
+ return generateTheme(config);
+}
diff --git a/packages/superdoc/src/dev/components/SuperdocDev.vue b/packages/superdoc/src/dev/components/SuperdocDev.vue
index 467f7770fd..c2dc34e6ed 100644
--- a/packages/superdoc/src/dev/components/SuperdocDev.vue
+++ b/packages/superdoc/src/dev/components/SuperdocDev.vue
@@ -1,6 +1,7 @@