Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,12 +175,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).
16 changes: 9 additions & 7 deletions brand/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
4 changes: 2 additions & 2 deletions brand/visual-identity.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.

Expand Down
85 changes: 62 additions & 23 deletions packages/layout-engine/painters/dom/src/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -591,11 +591,43 @@ 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
type CommentHighlightToken = {
css: string;
fallback: string;
};

const COMMENT_HIGHLIGHT_EXTERNAL: CommentHighlightToken = {
css: 'var(--sd-comments-highlight-external, #B1124B40)',
fallback: '#B1124B40',
};
const COMMENT_HIGHLIGHT_EXTERNAL_ACTIVE: CommentHighlightToken = {
css: 'var(--sd-comments-highlight-external-active, #B1124B66)',
fallback: '#B1124B66',
};
const COMMENT_HIGHLIGHT_EXTERNAL_FADED: CommentHighlightToken = {
css: 'var(--sd-comments-highlight-external-faded, #B1124B20)',
fallback: '#B1124B20',
};
const COMMENT_HIGHLIGHT_INTERNAL: CommentHighlightToken = {
css: 'var(--sd-comments-highlight-internal, #07838340)',
fallback: '#07838340',
};
const COMMENT_HIGHLIGHT_INTERNAL_ACTIVE: CommentHighlightToken = {
css: 'var(--sd-comments-highlight-internal-active, #07838366)',
fallback: '#07838366',
};
const COMMENT_HIGHLIGHT_INTERNAL_FADED: CommentHighlightToken = {
css: 'var(--sd-comments-highlight-internal-faded, #07838320)',
fallback: '#07838320',
};
const COMMENT_HIGHLIGHT_EXTERNAL_NESTED_BORDER: CommentHighlightToken = {
css: 'var(--sd-comments-highlight-external-nested-border, #B1124B99)',
fallback: '#B1124B99',
};
const COMMENT_HIGHLIGHT_INTERNAL_NESTED_BORDER: CommentHighlightToken = {
css: 'var(--sd-comments-highlight-internal-nested-border, #07838399)',
fallback: '#07838399',
};

type LinkRenderData = {
href?: string;
Expand Down Expand Up @@ -2595,7 +2627,7 @@ export class DomPainter {
const base = this.options.pageStyles ?? {};
return {
...base,
background: base.background ?? '#fff',
background: base.background ?? 'var(--sd-layout-page-background, #fff)',
boxShadow: 'none',
border: 'none',
margin: '0',
Expand Down Expand Up @@ -4605,14 +4637,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
Expand Down Expand Up @@ -6958,8 +6999,8 @@ const applyRunStyles = (element: HTMLElement, run: Run, _isLink = false): void =
};

interface CommentHighlightResult {
color?: string;
baseColor?: string;
color?: CommentHighlightToken;
nestedBorderColor?: CommentHighlightToken;
hasNestedComments?: boolean;
}

Expand Down Expand Up @@ -7000,27 +7041,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 };
};

/**
Expand Down
46 changes: 24 additions & 22 deletions packages/layout-engine/painters/dom/src/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ export type PageStyles = {
};

export const DEFAULT_PAGE_STYLES: Required<PageStyles> = {
background: '#fff',
boxShadow: '0 4px 20px rgba(15, 23, 42, 0.08)',
background: 'var(--sd-layout-page-background, #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',
};
Expand Down Expand Up @@ -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);
}
`;

Expand Down Expand Up @@ -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-background, #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-background, #f2f2f2);
border-color: transparent;
}

.superdoc-structured-content-block.ProseMirror-selectednode {
border-color: #629be7;
border-color: var(--sd-content-controls-block-border, #629be7);
outline: none;
}

Expand All @@ -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-background, #629be7ee);
color: var(--sd-content-controls-label-text, #ffffff);
box-sizing: border-box;
z-index: 10;
display: none;
Expand Down Expand Up @@ -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-background, #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;
}
Expand All @@ -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-background, #629be7ee);
color: var(--sd-content-controls-label-text, #ffffff);
border-radius: 4px;
white-space: nowrap;
z-index: 100;
Expand All @@ -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-background, rgba(98, 155, 231, 0.08));
z-index: 9999999;
}

Expand Down
14 changes: 7 additions & 7 deletions packages/super-editor/src/assets/styles/elements/prosemirror.css
Original file line number Diff line number Diff line change
Expand Up @@ -241,21 +241,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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading
Loading