From c4161431e7fba6103869005e5110402eca81cffa Mon Sep 17 00:00:00 2001 From: Mark Date: Tue, 26 May 2026 07:58:06 +0200 Subject: [PATCH] =?UTF-8?q?feat(04-06):=20Wave=201=20GREEN=20=E2=80=94=20d?= =?UTF-8?q?ark-logo=20currentColor=20strategy=20+=20inline-SVG=20injection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UI-SPEC Option A landed end-to-end at the source layer: - src/shared/brand/mokosh-mark.svg: single-attribute change on the root — stroke="#181b2a" → stroke="currentColor". The 12 + 1 children inherit stroke from the root and are UNCHANGED. This switches the mark from a hardcoded near-black ink to inheriting the parent CSS `color` cascade (W3C SVG2 §13.3). - src/welcome/welcome.ts: `import markUrl from '..mokosh-mark.svg?url'` → `import markSvg from '..mokosh-mark.svg?raw'`. populateMark() rewritten to inline-inject the SVG via DOMParser + replaceChildren (NOT , NEVER innerHTML — MV3 CSP discipline / T-04-06-01). The inline inherits `color: var(--mks-fg-inverse)` from the `.welcome-hero__mark` wrapper (welcome.css:67); on the dark surface the `.dark` token override (tokens.css 234-251) flips the resolved colour automatically — contrast-correct on both surfaces, no JS branching. The bare class selector `.welcome-hero__mark-img` (welcome.css:91-95) is tag-agnostic so width/height/display rules apply identically to the injected . role='img' + aria-label preserve the prior accessibility shape. - globals.d.ts: append the `declare module '*.svg?raw'` ambient block alongside the existing `*.svg?url` + `*.webm?url` blocks so tsc accepts the new import. Gates run: - npx tsc --noEmit exit 0. - npm test against tests/welcome/inline-svg.test.ts + tests/build/ cursor-visibility.test.ts: 4/4 GREEN (the 3 Wave-0 RED inline-svg tests flipped to GREEN; cursor-visibility stays GREEN). - Full vitest: 187 passed / 1 failed (188 total). The single RED is tests/background/webm-remux.test.ts > ffprobe -count_frames timeout — the documented 04-CONTEXT #9/#10 parallel-vitest / ffprobe flake family. Re-run in isolation: 5/5 GREEN. TOLERATED per the Task 2 VITEST GATE LOGIC (isolation-passing flake is NOT a regression). - npm run build exit 0; the welcome chunk JS bundles the raw SVG source (currentColor + viewBox="0 0 32 32" both present in dist/assets/welcome-Bkrf1_bZ.js). References: - 04-UI-SPEC.md §"Implementation amendment" (the 2-part technique). - Vite ?raw query: https://vite.dev/guide/assets.html#importing-asset-as-string - W3C SVG2 §13.3 (currentColor inheritance). - DOMParser is CSP-safe per MDN (no script execution). --- globals.d.ts | 21 ++++++ src/shared/brand/mokosh-mark.svg | 2 +- src/welcome/welcome.ts | 110 +++++++++++++++++++++---------- 3 files changed, 96 insertions(+), 37 deletions(-) diff --git a/globals.d.ts b/globals.d.ts index e6fb530..37d80ea 100644 --- a/globals.d.ts +++ b/globals.d.ts @@ -36,6 +36,27 @@ declare module '*.svg?url' { export default url; } +// Plan 04-06 — Vite `?raw` import for SVG-as-string. The `?raw` query +// suffix instructs Vite to bundle the resolved module's CONTENTS verbatim +// as a UTF-8 string at build time (not a hashed asset URL). Used by +// src/welcome/welcome.ts populateMark to inline-inject the canonical +// mokosh-mark.svg via DOMParser + replaceChildren — required by the +// UI-SPEC dark-logo `currentColor` strategy (Option A), because +// `currentColor` only inherits the parent CSS `color` cascade when the +// lives in the host document's DOM (an -rendered SVG runs in +// an isolated SVG document context where the parent cascade does not +// reach it; W3C SVG2 §13.3). Mirrors the existing `*.svg?url` ambient +// decl above; both forms can coexist for different consumers. +// +// References: +// - https://vite.dev/guide/assets.html#importing-asset-as-string +// - .planning/phases/04-harden-clean-up-optional/04-UI-SPEC.md +// §"Implementation amendment" +declare module '*.svg?raw' { + const raw: string; + export default raw; +} + // Plan 04-08 — Vite `?url` import for bundled test-only WebM fixture // (tests/uat/fixtures/synthetic-display-source.webm). Mirrors the // Plan 01-10 mokosh-mark.svg precedent; only gated test builds resolve diff --git a/src/shared/brand/mokosh-mark.svg b/src/shared/brand/mokosh-mark.svg index 6e9bd8f..2d2ef8f 100644 --- a/src/shared/brand/mokosh-mark.svg +++ b/src/shared/brand/mokosh-mark.svg @@ -1,5 +1,5 @@ - + diff --git a/src/welcome/welcome.ts b/src/welcome/welcome.ts index c256af1..64f955e 100644 --- a/src/welcome/welcome.ts +++ b/src/welcome/welcome.ts @@ -33,17 +33,26 @@ import { WELCOME_HERO_RU_FALLBACK, WELCOME_HERO_EN_FALLBACK, } from './copy'; -// Plan 01-10 must_have #9 path-A swap-in (landed 2026-05-20 per debug -// session 01-10-welcome-page-missing-mark): import the canonical mark -// SVG as a Vite-bundled asset URL. The `?url` suffix instructs Vite to -// emit the SVG verbatim to `dist/assets/.svg` and replace the -// import with the hashed asset URL at build time. The @crxjs/vite-plugin -// ^2.0.0-beta.25 in this project auto-generates web_accessible_resources -// entries for resources transitively reachable from extension pages -// (welcome.html → welcome.ts → markUrl) — confirmed by Plan 01-12 -// RESEARCH §155 and the existing dist/assets/*.woff2 precedent. -// Reference: https://vite.dev/guide/assets.html#explicit-url-imports -import markUrl from '../shared/brand/mokosh-mark.svg?url'; +// Plan 04-06 UI-SPEC dark-logo `currentColor` strategy (Option A) — swap +// the Plan 01-10 `?url` asset import for a `?raw` source-as-string import, +// so the SVG source can be inlined into the welcome page DOM via DOMParser +// + replaceChildren (see populateMark below). The CSS `color` property is +// inherited (CSS Color L3 §4) and the mokosh-mark.svg root carries +// `stroke="currentColor"` (Plan 04-06 Wave 1 SVG edit); on a LIGHT surface +// the `.welcome-hero__mark` wrapper sets `color: var(--mks-fg-inverse)` +// (welcome.css:67), and on a DARK surface the `.dark` token override +// (src/shared/tokens.css lines 234-251) flips `--mks-fg-inverse` so the +// inline `` inherits the contrast-correct stroke without any +// JS/event-handler/conditional code. An ``-injected SVG would render +// in an isolated SVG document context where `currentColor` resolves to +// the SVG's own root color (defaults to `canvastext`, near-black) and the +// cascade would not work — hence the inline-SVG technique. +// +// References: +// - 04-UI-SPEC.md §"Implementation amendment" (currentColor + inline) +// - W3C SVG2 §13.3 (Specifying colours / color inheritance / currentColor) +// - https://vite.dev/guide/assets.html#importing-asset-as-string (?raw) +import markSvg from '../shared/brand/mokosh-mark.svg?raw'; const logger = new Logger('Welcome'); @@ -133,43 +142,72 @@ function populateI18n(): void { /** * Walk every [data-mokosh-slot='mark'] wrapper and replace its inner - * placeholder content with an referencing the bundled canonical - * mark SVG. Plan 01-10 must_have #9 path-A swap-in (landed 2026-05-20). + * placeholder content with an INLINE parsed from the `?raw` + * source string of the canonical mark SVG. Plan 04-06 Wave 1 — UI-SPEC + * dark-logo `currentColor` strategy (Option A). * - * The wrapper itself (.welcome-hero__mark) keeps its CSS-driven circle - * background + sizing (welcome.css:64-73) — the SVG renders INSIDE the - * styled wrapper at the existing dimensions. We preserve the - * data-mokosh-slot='mark' attribute on the wrapper for forward-compat - * (future plans can locate the slot generically). + * Why inline (not ): the SVG root carries `stroke="currentColor"`; + * for the stroke to inherit from the parent CSS `color` cascade the + * must live in the welcome page's DOM. An ``-rendered SVG + * runs in an isolated SVG document context where `currentColor` + * resolves to the SVG's own root color (defaults to `canvastext`, + * near-black) and the parent cascade does NOT reach it — verified + * empirically + W3C SVG2 §13.3. By inlining via DOMParser + + * replaceChildren, the inline 's `stroke="currentColor"` resolves + * to the wrapper's `color: var(--mks-fg-inverse)` (welcome.css:67), + * and on the dark surface the `.dark` token override + * (src/shared/tokens.css lines 234-251) automatically flips the + * resolved colour — contrast-correct on both surfaces, no JS branching. + * + * Security / CSP discipline (T-04-06-01 mitigation): the inner-HTML + * string-assignment path is FORBIDDEN — DOMParser + replaceChildren + * only. DOMParser is the CSP-safe path (it does NOT execute scripts + * in the parsed document and runs in a sandboxed parser context per + * MDN); the inner-HTML string sink in an extension page is a + * script-execution surface MV3 disallows. The `markSvg` input is a + * Vite-bundled compile-time string literal (`?raw` returns module + * content verbatim) — no runtime untrusted input enters the parser; + * the trust boundary terminates at build. + * + * The wrapper itself (.welcome-hero__mark) keeps its CSS-driven + * circle background + sizing (welcome.css:64-73); the bare class + * selector `.welcome-hero__mark-img` (welcome.css:91-95) is tag- + * agnostic, so width/height/display rules apply identically to the + * injected as they did to the prior . + * + * Alt-text + a11y: `aria-hidden='true'` on the inline (matches + * the prior shape — the mark is decorative; the welcome-hero + * already carries its visible accessible name via the H1 + tagline). + * We additionally tag `role='img'` + `aria-label={altText}` for + * screen-reader landmark identification (parity with the prior + * {altText} contract). altText resolves from + * COPY['welcome.hero.mark.alt'] when present, else falls back to a + * literal Russian default ('Знак Mokosh'). * * Filter-pipeline form per project rule "no `continue` statements". * Missing-slot count is logged once via logger.warn for visibility. * - * Img dimensions: width/height attributes match the wrapper inner - * dimensions implicit from --mks-space-20 minus negligible padding; - * we use percentage sizing so the SVG fills the wrapper responsively - * without coupling to the exact px value of the space token. - * - * Alt text resolves from COPY['welcome.hero.mark.alt'] when present, - * else falls back to a literal Russian default ('Знак Mokosh'). The - * alt is presentational (the mark is a decorative brand element, not - * a structural content element), but a non-empty alt aids screen- - * reader landmark identification. + * References: + * - 04-UI-SPEC.md §"Implementation amendment" (the 2-part technique) + * - W3C SVG2 §13.3 (Specifying colours / currentColor inheritance) + * - MDN DOMParser.parseFromString (CSP-safe, no script execution): + * https://developer.mozilla.org/en-US/docs/Web/API/DOMParser/parseFromString + * - https://vite.dev/guide/assets.html#importing-asset-as-string (?raw) */ function populateMark(): void { const slots = Array.from( document.querySelectorAll('[data-mokosh-slot="mark"]'), ); const altText = COPY['welcome.hero.mark.alt'] ?? 'Знак Mokosh'; + const parser = new DOMParser(); for (const slot of slots) { - const img = document.createElement('img'); - img.src = markUrl; - img.alt = altText; - img.width = 64; - img.height = 64; - img.className = 'welcome-hero__mark-img'; - img.setAttribute('aria-hidden', 'true'); - slot.replaceChildren(img); + const doc = parser.parseFromString(markSvg, 'image/svg+xml'); + const svg = doc.documentElement; + svg.classList.add('welcome-hero__mark-img'); + svg.setAttribute('aria-hidden', 'true'); + svg.setAttribute('role', 'img'); + svg.setAttribute('aria-label', altText); + slot.replaceChildren(svg); } if (slots.length === 0) { logger.warn(