feat(04-06): Wave 1 GREEN — dark-logo currentColor strategy + inline-SVG injection

UI-SPEC Option A landed end-to-end at the source layer:

- src/shared/brand/mokosh-mark.svg: single-attribute change on the root
  <svg> — stroke="#181b2a" → stroke="currentColor". The 12 <line> + 1
  <rect> 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 <img>, NEVER innerHTML — MV3 CSP discipline / T-04-06-01).
  The inline <svg> 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 <svg>. 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).
This commit is contained in:
2026-05-26 07:58:06 +02:00
parent f0b88d4d17
commit c4161431e7
3 changed files with 96 additions and 37 deletions

21
globals.d.ts vendored
View File

@@ -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
// <svg> lives in the host document's DOM (an <img>-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

View File

@@ -1,5 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none" stroke="#181b2a" stroke-width="2.25" stroke-linecap="square" stroke-linejoin="miter">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none" stroke="currentColor" stroke-width="2.25" stroke-linecap="square" stroke-linejoin="miter">
<rect x="2.5" y="2.5" width="27" height="27" rx="3.5" ry="3.5"></rect>

Before

Width:  |  Height:  |  Size: 877 B

After

Width:  |  Height:  |  Size: 882 B

View File

@@ -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/<hash>.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 <svg> 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 `<svg>` inherits the contrast-correct stroke without any
// JS/event-handler/conditional code. An `<img>`-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 <img> 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 <svg> 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 <img>): the SVG root carries `stroke="currentColor"`;
* for the stroke to inherit from the parent CSS `color` cascade the
* <svg> must live in the welcome page's DOM. An `<img>`-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 <svg>'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 <svg> as they did to the prior <img>.
*
* Alt-text + a11y: `aria-hidden='true'` on the inline <svg> (matches
* the prior <img> 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
* <img alt={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<HTMLElement>('[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(