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:
21
globals.d.ts
vendored
21
globals.d.ts
vendored
@@ -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
|
||||
|
||||
@@ -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 |
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user