Skip to content
Open
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
29 changes: 28 additions & 1 deletion packages/layout-engine/painters/dom/src/styles.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { describe, expect, it } from 'vitest';
import { ensureSdtContainerStyles, ensureTrackChangeStyles, lineStyles } from './styles.js';
import {
DEFAULT_PAGE_STYLES,
ensureSdtContainerStyles,
ensureTrackChangeStyles,
lineStyles,
pageStyles,
} from './styles.js';

describe('lineStyles', () => {
it('sets height and lineHeight from the argument', () => {
Expand All @@ -14,6 +20,27 @@ describe('lineStyles', () => {
});
});

describe('pageStyles', () => {
// SD-3456 / IT-1102: auto-numbered list markers (and any other text that
// inherits its color rather than carrying an explicit `<w:color>`) render
// invisible on dark-themed OSes because the browser default `canvastext`
// system color resolves to white on the white page. The page element must
// therefore set an explicit text color floor.
it('sets an explicit color on the page so document text does not inherit the OS canvastext system color', () => {
const styles = pageStyles(816, 1056);
expect(styles.color).toBe('var(--sd-layout-page-text, #000)');
});

it('lets consumers override the page color via the pageStyles option', () => {
const styles = pageStyles(816, 1056, { color: '#222' });
expect(styles.color).toBe('#222');
});

it('exposes the color default through DEFAULT_PAGE_STYLES so consumers and themes can read it', () => {
expect(DEFAULT_PAGE_STYLES.color).toBe('var(--sd-layout-page-text, #000)');
});
});

describe('ensureSdtContainerStyles', () => {
it('exposes hover border tokens for structured content overrides', () => {
ensureSdtContainerStyles(document);
Expand Down
8 changes: 8 additions & 0 deletions packages/layout-engine/painters/dom/src/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,20 @@ export type PageStyles = {
boxShadow?: string;
border?: string;
margin?: string;
color?: string;
};

export const DEFAULT_PAGE_STYLES: Required<PageStyles> = {
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',
// Without an explicit color, document text inherits the browser default
// `canvastext` system color, which resolves to white on dark-themed OSes —
// so auto-numbered list markers and any other run without an explicit
// `<w:color>` render invisible on the white page (SD-3456 / IT-1102).
// Default to black; consumers can override via --sd-layout-page-text.
color: 'var(--sd-layout-page-text, #000)',
};

export const containerStyles: Partial<CSSStyleDeclaration> = {
Expand Down Expand Up @@ -75,6 +82,7 @@ export const pageStyles = (width: number, height: number, overrides?: PageStyles
minHeight: `${height}px`,
flexShrink: '0',
background: merged.background,
color: merged.color,
boxShadow: merged.boxShadow,
border: merged.border,
margin: merged.margin,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@
font-variant-ligatures: none;
font-feature-settings: 'liga' 0; /* the above doesn't seem to work in Edge */
z-index: 0; /* Needed to place images behind text with lower z-index */
/* SD-3456: `.sd-editor-scoped` applies `all: revert` to its descendants
* (see isolation.css), which reverts text color to the browser default
* `canvastext` system color. On dark-themed OSes that resolves to white,
* making any document text without an explicit <w:color> (e.g. auto-numbered
* list markers, untyped runs) invisible on the white page. Set an explicit
* color floor that mirrors the layout-engine page; consumers can override
* via --sd-layout-page-text. */
color: var(--sd-layout-page-text, #000);
}

.sd-editor-scoped .ProseMirror pre {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { describe, it, expect } from 'vitest';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';

const __dirname = dirname(fileURLToPath(import.meta.url));

// SD-3456 (cross-package CSS invariant). `isolation.css` applies `all: revert`
// to descendants of `.sd-editor-scoped`, which reverts text color to the
// browser default `canvastext` system color. On dark-themed OSes that
// resolves to white, making any document text without an explicit <w:color>
// (e.g. auto-numbered list markers, runs with no rPr) invisible on the white
// editor surface. This is the editor-mode sibling of the layout-engine
// `.superdoc-page` fix in `painters/dom/src/styles.ts`. These tests guard
// the CSS rule that re-establishes the color floor inside the isolation
// wrapper so the dark-OS bug cannot resurface.

const repoRoot = join(__dirname, '..', '..', '..', '..', '..', '..');

const editorScopedCss = readFileSync(
join(repoRoot, 'packages', 'super-editor', 'src', 'editors', 'v1', 'assets', 'styles', 'elements', 'prosemirror.css'),
'utf8',
);

const isolationCss = readFileSync(
join(repoRoot, 'packages', 'super-editor', 'src', 'editors', 'v1', 'assets', 'styles', 'helpers', 'isolation.css'),
'utf8',
);

const extractRuleBodies = (css, selector) => {
const bodies = [];
let cursor = 0;
while (cursor < css.length) {
const idx = css.indexOf(selector, cursor);
if (idx === -1) break;
const open = css.indexOf('{', idx);
const close = css.indexOf('}', open);
if (open === -1 || close === -1) break;
bodies.push(css.slice(open + 1, close));
cursor = close + 1;
}
return bodies;
};

describe('editor-scoped color floor (SD-3456)', () => {
it('isolation.css still applies `all: revert` — confirms the canvastext exposure the floor compensates for', () => {
expect(isolationCss).toMatch(/all\s*:\s*revert/);
});

it('`.sd-editor-scoped .ProseMirror` declares an explicit `color` so revert cannot bleed canvastext through', () => {
const bodies = extractRuleBodies(editorScopedCss, '.sd-editor-scoped .ProseMirror {');
expect(bodies.length, 'at least one .sd-editor-scoped .ProseMirror block must exist').toBeGreaterThan(0);

// At least one of the .sd-editor-scoped .ProseMirror blocks must set color.
const hasColor = bodies.some((body) => /\bcolor\s*:/.test(body));
expect(hasColor, 'one of the .sd-editor-scoped .ProseMirror blocks must declare `color`').toBe(true);
});

it('the color floor uses the shared `--sd-layout-page-text` token so themes set it once for both surfaces', () => {
const bodies = extractRuleBodies(editorScopedCss, '.sd-editor-scoped .ProseMirror {');
const usesToken = bodies.some((body) => /color\s*:[^;]*--sd-layout-page-text/.test(body));
expect(usesToken, 'color declaration should reference --sd-layout-page-text').toBe(true);
});

it('the floor falls back to #000 when the token is unset so dark-OS users get a sensible default out of the box', () => {
const bodies = extractRuleBodies(editorScopedCss, '.sd-editor-scoped .ProseMirror {');
const hasBlackFallback = bodies.some((body) =>
/color\s*:[^;]*var\(\s*--sd-layout-page-text\s*,\s*#000\s*\)/.test(body),
);
expect(hasBlackFallback, 'fallback must be #000 to match the layout-engine page default').toBe(true);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@
/* 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);
--sd-layout-page-text: #000;
--sd-formatting-mark-color: var(--sd-color-blue-500);
--sd-formatting-paragraph-mark-gap: 0.2em;

Expand Down
Loading