From 72f1188c3a897b5d929b0053ef7a12724aa2f58e Mon Sep 17 00:00:00 2001 From: G Pardhiv Varma Date: Wed, 27 May 2026 13:30:34 +0530 Subject: [PATCH] fix(painters/dom, super-editor): set explicit document text color floor (SD-3456) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `.superdoc-page` (layout-engine page surface) and `.sd-editor-scoped .ProseMirror` (editor isolation surface) both relied on the browser default `color`. On dark-themed OSes that resolves to the `canvastext` system color (white), so any document text without an explicit `` — auto-numbered list markers in particular — rendered invisible on the white page. Set an explicit `color: var(--sd-layout-page-text, #000)` floor on both surfaces. OOXML-explicit colors still win (inline `style.color` on runs has higher specificity). Consumers customize via the `--sd-layout-page-text` token, documented alongside `--sd-layout-page-bg` in variables.css. Adds CSS-content regression tests guarding both surfaces against future removal of the floor. --- .../painters/dom/src/styles.test.ts | 29 +++++++- .../layout-engine/painters/dom/src/styles.ts | 8 +++ .../v1/assets/styles/elements/prosemirror.css | 8 +++ .../editor-scoped-color-floor.test.js | 72 +++++++++++++++++++ .../src/assets/styles/helpers/variables.css | 1 + 5 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 packages/superdoc/src/assets/styles/elements/editor-scoped-color-floor.test.js diff --git a/packages/layout-engine/painters/dom/src/styles.test.ts b/packages/layout-engine/painters/dom/src/styles.test.ts index fec200adcd..844fb260ea 100644 --- a/packages/layout-engine/painters/dom/src/styles.test.ts +++ b/packages/layout-engine/painters/dom/src/styles.test.ts @@ -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', () => { @@ -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 ``) 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); diff --git a/packages/layout-engine/painters/dom/src/styles.ts b/packages/layout-engine/painters/dom/src/styles.ts index e86fb474a0..1d0f653a75 100644 --- a/packages/layout-engine/painters/dom/src/styles.ts +++ b/packages/layout-engine/painters/dom/src/styles.ts @@ -22,6 +22,7 @@ export type PageStyles = { boxShadow?: string; border?: string; margin?: string; + color?: string; }; export const DEFAULT_PAGE_STYLES: Required = { @@ -29,6 +30,12 @@ export const DEFAULT_PAGE_STYLES: Required = { 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 + // `` 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 = { @@ -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, diff --git a/packages/super-editor/src/editors/v1/assets/styles/elements/prosemirror.css b/packages/super-editor/src/editors/v1/assets/styles/elements/prosemirror.css index 090dbbbff2..c9338cb9cb 100644 --- a/packages/super-editor/src/editors/v1/assets/styles/elements/prosemirror.css +++ b/packages/super-editor/src/editors/v1/assets/styles/elements/prosemirror.css @@ -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 (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 { diff --git a/packages/superdoc/src/assets/styles/elements/editor-scoped-color-floor.test.js b/packages/superdoc/src/assets/styles/elements/editor-scoped-color-floor.test.js new file mode 100644 index 0000000000..25b6e3fcbe --- /dev/null +++ b/packages/superdoc/src/assets/styles/elements/editor-scoped-color-floor.test.js @@ -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 +// (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); + }); +}); diff --git a/packages/superdoc/src/assets/styles/helpers/variables.css b/packages/superdoc/src/assets/styles/helpers/variables.css index d8ec54adb3..07683b6207 100644 --- a/packages/superdoc/src/assets/styles/helpers/variables.css +++ b/packages/superdoc/src/assets/styles/helpers/variables.css @@ -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;