diff --git a/src/lib/components/CLAUDE.md b/src/lib/components/CLAUDE.md index 3d0f662..dc6c15f 100644 --- a/src/lib/components/CLAUDE.md +++ b/src/lib/components/CLAUDE.md @@ -141,7 +141,7 @@ ceiling noted in the file. ## Debug tooling (dev only) -In dev builds a **Debug** button (`App.svelte`) downloads a JSON snapshot combining `getPageBreakDebug(view)` (leaves, placements, rendered spacers, table-break bands, live overlay geometry), `getTableCellDebug(view)` (per cell: the `verticalAlign` attr beside the computed value, cell/row height, the gap above and below the content, and every block's margins/`--space-before` — cell alignment reads as broken whenever the content fills the box, so the numbers that decide that travel with it), `getFrameDebug(view)` (`caption.ts` — per picture: its attrs, its rendered span in the text column and its caption's; every other section measures the flow downwards, and a misplaced caption is a sideways question), the style sheet + spacing model, and `getColorDebug(editor)` (selection marks, text runs, document colors, DOM spans). +In dev builds a **Debug** button (`App.svelte`) downloads a JSON snapshot combining `getPageBreakDebug(view)` (leaves, placements, rendered spacers, table-break bands, live overlay geometry), `getTableCellDebug(view)` (per cell: the `verticalAlign` attr beside the computed value, cell/row height, the gap above and below the content, and every block's margins/`--space-before` — cell alignment reads as broken whenever the content fills the box, so the numbers that decide that travel with it), `getFrameDebug(view)` (`caption.ts` — per picture: its attrs, its rendered span in the text column and its caption's; every other section measures the flow downwards, and a misplaced caption is a sideways question), the style sheet + spacing model, and `getColorDebug(editor)` (selection marks, text runs, document colors, DOM spans, and `painted`: the colour the browser actually computes per rendered run, the element that declares it, the page's colour variables and the document stylesheet's colour rules — a mark says what was asked for, and a decoration paints a colour no mark carries). ## Per-section header/footer diff --git a/src/lib/utils/colorDebug.ts b/src/lib/utils/colorDebug.ts index f2f5f43..9f91990 100644 --- a/src/lib/utils/colorDebug.ts +++ b/src/lib/utils/colorDebug.ts @@ -30,6 +30,23 @@ export type ColorDebugSnapshot = { total: number; sample: Array<{ dataColor: string | null; inlineStyle: string | null; text: string }>; }; + /** What the browser paints — the marks above only say what it was asked to. */ + painted: { + /** The custom properties a run of no colour of its own resolves through. */ + vars: Record; + /** One entry per colour actually rendered, with where that colour comes from. */ + colors: Record; + /** The colour-carrying rules of the document stylesheet (styles/styleSheet.ts). */ + styleRules: string[]; + }; +}; + +/** The nearest element declaring a colour, i.e. the channel a painted colour arrives on. */ +type PaintSource = { + tag: string; + className: string; + dataColor: string | null; + inlineColor: string | null; }; const TEXT_PREVIEW_LIMIT = 80; @@ -55,6 +72,67 @@ function getTextStyleAttr(marks: readonly Mark[], attr: string): string | null { return typeof v === 'string' && v ? v : null; } +const PAINT_VARS = ['--color-page-text', '--color-page-bg', '--color-text', '--font-color']; +const INLINE_COLOR = /(?:^|;)\s*color\s*:\s*([^;]+)/; +const STYLE_RULE_LIMIT = 40; + +// Walking up to the editor root: the first element declaring a colour is the channel the +// run is painted through. None of them = the page's own colour (editor.css). +function paintSource(from: HTMLElement, root: HTMLElement): PaintSource | null { + for (let e: HTMLElement | null = from; e; e = e.parentElement) { + const inline = INLINE_COLOR.exec(e.getAttribute('style') ?? ''); + if (inline || e.hasAttribute('data-color')) { + return { + tag: e.tagName.toLowerCase(), + className: e.getAttribute('class') ?? '', + dataColor: e.getAttribute('data-color'), + inlineColor: inline ? inline[1].trim() : null, + }; + } + if (e === root) break; + } + return null; +} + +// The block a run sits in, named as the CSS would address it: its tag and the named +// paragraph style whose rule reaches it. +function blockOf(from: HTMLElement, root: HTMLElement): string { + const block = from.closest('p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10, li, td, th') ?? from; + const styled = from.closest('[data-style]'); + const name = styled && root.contains(styled) ? styled.getAttribute('data-style') : null; + return name ? `${block.tagName.toLowerCase()}[${name}]` : block.tagName.toLowerCase(); +} + +function paintedColors(root: HTMLElement): ColorDebugSnapshot['painted']['colors'] { + const out: ColorDebugSnapshot['painted']['colors'] = {}; + if (typeof document === 'undefined' || typeof getComputedStyle !== 'function') return out; + const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT); + for (let n = walker.nextNode(); n; n = walker.nextNode()) { + const text = n.textContent ?? ''; + const el = n.parentElement; + if (!text.trim() || !el) continue; + const color = getComputedStyle(el).color; + if (!out[color]) { + out[color] = { chars: 0, sample: truncate(text.trim(), 40), block: blockOf(el, root), source: paintSource(el, root) }; + } + out[color].chars += text.length; + } + return out; +} + +function documentStyleColorRules(): string[] { + const sheet = (document.getElementById('document-styles') as HTMLStyleElement | null)?.sheet; + if (!sheet) return []; + try { + return Array.from(sheet.cssRules) + .map((r) => r.cssText) + .filter((t) => INLINE_COLOR.test(t) || t.includes('{color:') || t.includes(' color:')) + .slice(0, STYLE_RULE_LIMIT); + } catch { + return []; + } +} + export function getColorDebug(editor: Editor): ColorDebugSnapshot { const { state } = editor; const { doc, selection, storedMarks } = state; @@ -115,6 +193,15 @@ export function getColorDebug(editor: Editor): ColorDebugSnapshot { ? document.documentElement.getAttribute('data-theme') : null; + // Read on the editor's own element: a page decoration or a theme override lands + // between :root and the text, and only this end of the cascade shows it. + const paintVars: Record = {}; + if (root && typeof getComputedStyle === 'function') { + const cs = getComputedStyle(root as HTMLElement); + for (const v of PAINT_VARS) paintVars[v] = cs.getPropertyValue(v).trim(); + paintVars.color = cs.color; + } + return { timestamp: new Date().toISOString(), theme: { dataTheme }, @@ -133,5 +220,10 @@ export function getColorDebug(editor: Editor): ColorDebugSnapshot { total: domSpansTotal, sample: domSpansSample, }, + painted: { + vars: paintVars, + colors: root ? paintedColors(root as HTMLElement) : {}, + styleRules: typeof document !== 'undefined' ? documentStyleColorRules() : [], + }, }; }