feat(01-10): wave-1 task-2 — welcome page bundle + Vite entries + web_accessible_resources

Plan 01-10 Wave 1: welcome page bundle staged with canonical Plan 01-12
tokens.css @import + chrome.i18n for D-08 tagline (Plan 01-12 path-B
contract).

Files created:
  - src/welcome/copy.ts (Russian non-tagline COPY map per D-03 Sober
    voice; WELCOME_HERO_RU_FALLBACK + WELCOME_HERO_EN_FALLBACK exported
    for the welcome.ts `|| <en-const>` fallback chain).
  - src/welcome/welcome.html (lang='ru'; data-mokosh-key + data-mokosh-
    i18n-key attribute conventions; SINGLE stylesheet link; D-02 Hero +
    body + footer structure; 10 keyed attrs total; <title> populated
    via populateCopy).
  - src/welcome/welcome.css (FIRST LINE `@import '../shared/tokens.css';`;
    ZERO hex literals in source; 65 var(--mks-*) refs; D-02 layout +
    --mks-welcome-max-w=720px; --mks-rec madder for recording-related
    accents per D-04 Loom palette).
  - src/welcome/welcome.ts (vanilla DOM; populateCopy + populateI18n
    filter-pipeline form per project rule "no `continue`"; no `as any`;
    Logger from src/shared; document.readyState guard; no event
    handlers per D-16-toolbar informational charter).

Files modified:
  - vite.config.ts: rollupOptions.input gains `welcome:
    'src/welcome/welcome.html'`; __VITE_DEV__ + __MOKOSH_UAT__ defines
    untouched (Plan 01-12 Wave 5 baseline preserved verbatim).
  - vite.test.config.ts: mirror entry in dist-test/; mergeConfig pattern
    untouched.
  - manifest.json: web_accessible_resources block added after
    host_permissions, before background; storage permission preserved
    in permissions array; default_locale='en' + __MSG_*__ placeholders
    from Plan 01-12 Wave 3 preserved verbatim.

NO src/welcome/welcome-tokens.css file is created — Plan 01-12 must_have
#9 path-B contract: Plan 01-12 landed FIRST (b909c37 → 865d394; SUMMARY
2026-05-20); canonical src/shared/tokens.css is import-ready
(Lora @font-face + IBM Plex Sans + D-04 Loom palette + --mks-rec=
var(--mks-madder-600) = #b2543d); welcome.css @imports it directly. No
placeholder transition needed.

Verify (all GREEN):
  - grep -F "@import '../shared/tokens.css'" src/welcome/welcome.css: exit 0
  - grep -E '#[0-9a-fA-F]{3,8}' src/welcome/welcome.css: exit 1 (zero hex)
  - grep -c 'var(--mks-' src/welcome/welcome.css: 65 (>= 5 required)
  - grep -oE 'data-mokosh-(i18n-)?key=' welcome.html | wc -l: 10 (>= 7)
  - npm run build: clean; dist/src/welcome/welcome.html present;
    dist/assets/welcome-D9oNz95l.css carries inlined tokens.css content
    (--mks-rec: var(--mks-madder-600); --mks-madder-600: #b2543d).
  - npm run build:test: clean; dist-test/src/welcome/welcome.html present;
    dist-test/assets/welcome-wB0e_R_n.js bundled.
  - npx tsc --noEmit: clean.
  - dist/manifest.json preserves "default_locale": "en" + __MSG_extName__
    + web_accessible_resources block present (Vite/crxjs propagated).
  - Vitest baseline preserved: Task 1's 3-test file unchanged
    (1 RED + 2 vacuous-GREEN; Task 3 flips Test A to GREEN).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-20 09:09:22 +02:00
parent 89e1e09d60
commit 49f087fe40
7 changed files with 436 additions and 0 deletions

65
src/welcome/copy.ts Normal file
View File

@@ -0,0 +1,65 @@
// src/welcome/copy.ts — single source-of-truth for welcome-page
// NON-TAGLINE copy.
//
// Plan 01-12 (landed first; SUMMARY 2026-05-20) migrated the two D-08
// tagline strings (welcomeHeroRu + welcomeHeroEn) to
// _locales/{en,ru}/messages.json. Those keys are read via
// chrome.i18n.getMessage in welcome.ts and intentionally NOT included
// in this map.
//
// The remaining keys are engineering-grade placeholders authored in
// Russian per D-03 Sober voice register; a future copy-iteration plan
// may migrate them to _locales/ for full operator-locale awareness.
//
// D-08 tagline reference (lives in _locales/, not here):
// en/welcomeHeroEn → "Thirty seconds ago, always at hand."
// ru/welcomeHeroRu → "Тридцать секунд назад, всегда под рукой."
//
// References:
// - D-02 (welcome layout — Hero + Loom dial), brand-decisions-v1.md
// - D-03 (Sober voice register), brand-decisions-v1.md
// - D-08 (tagline), brand-decisions-v1.md
// - D-16-toolbar (toolbar owns start path), 01-09-SUMMARY.md
// - Plan 01-12 Wave 4 chrome.i18n fallback pattern (popup precedent)
/**
* Plan 01-12 fallback-pattern constants for the welcomeHero keys.
*
* welcome.ts uses these as the `|| <en-const>` fallback when
* chrome.i18n.getMessage returns empty string (Plan 01-10 RESEARCH
* Pitfall 4 mitigation: chrome.i18n.getMessage returns '' for unknown
* keys instead of throwing).
*
* Exported separately from COPY so the i18n populate path imports them
* by name (clean import surface — COPY map stays free of tagline keys).
*/
export const WELCOME_HERO_RU_FALLBACK =
'Тридцать секунд назад, всегда под рукой.';
export const WELCOME_HERO_EN_FALLBACK =
'Thirty seconds ago, always at hand.';
/**
* Non-tagline welcome-page copy. Keys are stable identifiers used by
* `[data-mokosh-key='<key>']` attribute selectors in welcome.html;
* populateCopy() in welcome.ts walks every element with the attribute
* and writes textContent (or document.title for the <title> element).
*
* Russian phrasing per D-03 Sober voice — restrained, declarative,
* no marketing inflection.
*/
export const COPY: Readonly<Record<string, string>> = Object.freeze({
'welcome.page.title': 'Добро пожаловать в Mokosh',
'welcome.hero.title': 'Mokosh',
'welcome.body.explainer.line1':
'Mokosh непрерывно записывает последние 30 секунд экрана и 10 минут '
+ 'логов вашего браузера.',
'welcome.body.explainer.line2':
'Когда возникает баг, вы одним кликом сохраняете архив для службы '
+ 'поддержки. Данные не отправляются никуда — только локально.',
'welcome.body.cta.toolbar':
'Чтобы начать запись, нажмите иконку AI Call Recorder на панели '
+ 'инструментов браузера (правый верхний угол).',
'welcome.footer.privacy':
'Mokosh не отправляет данные на серверы. Архив создаётся '
+ 'локально по вашему запросу и остаётся на вашем компьютере.',
});

164
src/welcome/welcome.css Normal file
View File

@@ -0,0 +1,164 @@
/* welcome.css — Plan 01-10 D-17-onboarding welcome page styling.
*
* Imports the canonical Plan 01-12 token system (src/shared/tokens.css)
* which carries the full --mks-* variable set + 8 local @font-face
* rules (Lora display + IBM Plex Sans UI + IBM Plex Mono diagnostic
* — all self-hosted per MV3 CSP) + the D-04 Loom palette.
*
* KEY INVARIANTS (Plan 01-10 A17 design-swap-readiness contract):
* 1. FIRST LINE is `@import '../shared/tokens.css';` (Plan 01-12
* must_have #9 path-B; verified by `grep -F` in <verify> block).
* 2. ZERO hex literals in this source file (verified by
* `grep -E '#[0-9a-fA-F]{3,8}'` exit 1 in <verify>). Every color
* reads from var(--mks-*); every font reads from var(--mks-font-*).
* The @imported tokens.css IS allowed hex literals — it IS the
* canonical source.
* 3. >= 5 var(--mks-*) references (verified by `grep -c`).
* 4. NO placeholder welcome-tokens.css file is created — Plan 01-12
* landed first and supplies the canonical tokens directly.
*
* Layout: max-width 720px (= --mks-welcome-max-w in tokens.css),
* centered, hero + body + footer vertical stack per D-02 Surface Kit
* option A. BEM-ish class names matching welcome.html. */
@import '../shared/tokens.css';
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: var(--mks-font-ui);
background: var(--mks-surface);
color: var(--mks-fg-1);
min-height: 100vh;
line-height: var(--mks-lh-base);
}
.welcome {
max-width: var(--mks-welcome-max-w);
margin: 0 auto;
padding: var(--mks-space-12) var(--mks-space-8);
display: flex;
flex-direction: column;
gap: var(--mks-space-10);
}
/* ── Hero ──────────────────────────────────────────────────────────── */
.welcome-hero {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: var(--mks-space-4);
padding: var(--mks-space-10) var(--mks-space-6);
background: var(--mks-surface-raised);
border: var(--mks-border-width) solid var(--mks-border);
border-radius: var(--mks-radius-xl);
box-shadow: var(--mks-shadow-2);
}
.welcome-hero__mark {
display: flex;
align-items: center;
justify-content: center;
width: var(--mks-space-20);
height: var(--mks-space-20);
border-radius: var(--mks-radius-full);
background: var(--mks-rec);
color: var(--mks-fg-inverse);
}
.welcome-hero__mark-placeholder {
font-family: var(--mks-font-display);
font-size: var(--mks-text-md);
font-weight: var(--mks-weight-semibold);
letter-spacing: var(--mks-tracking-tight);
}
.welcome-hero__title {
font-family: var(--mks-font-display);
font-size: var(--mks-text-3xl);
font-weight: var(--mks-weight-regular);
line-height: var(--mks-lh-tight);
letter-spacing: var(--mks-tracking-display);
color: var(--mks-fg-1);
}
.welcome-hero__tagline-ru {
font-family: var(--mks-font-display);
font-size: var(--mks-text-lg);
font-weight: var(--mks-weight-regular);
font-style: italic;
line-height: var(--mks-lh-snug);
color: var(--mks-fg-1);
margin-top: var(--mks-space-3);
}
.welcome-hero__tagline-en {
font-family: var(--mks-font-ui);
font-size: var(--mks-text-base);
font-weight: var(--mks-weight-regular);
line-height: var(--mks-lh-snug);
color: var(--mks-fg-2);
}
/* ── Body ──────────────────────────────────────────────────────────── */
.welcome-body {
display: flex;
flex-direction: column;
gap: var(--mks-space-5);
}
.welcome-body__line {
font-family: var(--mks-font-ui);
font-size: var(--mks-text-md);
line-height: var(--mks-lh-base);
color: var(--mks-fg-1);
}
.welcome-body__cta {
font-family: var(--mks-font-ui);
font-size: var(--mks-text-md);
font-weight: var(--mks-weight-medium);
line-height: var(--mks-lh-base);
color: var(--mks-fg-1);
padding: var(--mks-space-5) var(--mks-space-6);
background: var(--mks-madder-100);
border-left: 3px solid var(--mks-rec);
border-radius: var(--mks-radius-md);
}
/* ── Footer ────────────────────────────────────────────────────────── */
.welcome-footer {
padding-top: var(--mks-space-6);
border-top: var(--mks-border-width) solid var(--mks-border);
}
.welcome-footer__privacy {
font-family: var(--mks-font-ui);
font-size: var(--mks-text-sm);
line-height: var(--mks-lh-base);
color: var(--mks-fg-3);
text-align: center;
}
/* ── Reduced motion / small screen courtesies ─────────────────────── */
@media (max-width: 600px) {
.welcome {
padding: var(--mks-space-6) var(--mks-space-4);
gap: var(--mks-space-6);
}
.welcome-hero {
padding: var(--mks-space-6) var(--mks-space-4);
}
.welcome-hero__title {
font-size: var(--mks-text-2xl);
}
}

53
src/welcome/welcome.html Normal file
View File

@@ -0,0 +1,53 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!--
Plan 01-10 D-17-onboarding welcome page.
NB on <title>: like the popup precedent (src/popup/index.html), the
title element carries data-mokosh-key='welcome.page.title' so
welcome.ts can populate it from the COPY map at populateCopy()
time. The literal Russian default below stays as a graceful-
degradation value when the script is unavailable (mirrors the popup
pattern Plan 01-12 Wave 4 introduced).
The two D-08 tagline elements (welcomeHeroRu + welcomeHeroEn)
carry data-mokosh-i18n-key='welcomeHero<Lang>' so populateI18n()
in welcome.ts reads them via chrome.i18n.getMessage from
_locales/{en,ru}/messages.json (Plan 01-12 Wave 3 baseline; 16
keys total). The parallel-text layout (RU primary + EN subtitle)
renders identically regardless of operator locale per D-08.
SINGLE <link rel=stylesheet> — welcome.css. The `@import
'../shared/tokens.css'` at the top of welcome.css resolves the
canonical Plan 01-12 token system (Lora @font-face + D-04 Loom
palette + --mks-* full set) at Vite build time. NO placeholder
welcome-tokens.css link (Plan 01-12 must_have #9 path-B contract).
-->
<title data-mokosh-key="welcome.page.title">Добро пожаловать в Mokosh</title>
<link rel="stylesheet" href="welcome.css">
</head>
<body>
<main class="welcome">
<section class="welcome-hero" aria-labelledby="welcome-title">
<div class="welcome-hero__mark" role="presentation" data-mokosh-slot="mark">
<span class="welcome-hero__mark-placeholder" aria-hidden="true">Mokosh</span>
</div>
<h1 id="welcome-title" class="welcome-hero__title" data-mokosh-key="welcome.hero.title"></h1>
<p class="welcome-hero__tagline-ru" data-mokosh-i18n-key="welcomeHeroRu"></p>
<p class="welcome-hero__tagline-en" data-mokosh-i18n-key="welcomeHeroEn"></p>
</section>
<section class="welcome-body">
<p class="welcome-body__line" data-mokosh-key="welcome.body.explainer.line1"></p>
<p class="welcome-body__line" data-mokosh-key="welcome.body.explainer.line2"></p>
<p class="welcome-body__cta" data-mokosh-key="welcome.body.cta.toolbar"></p>
</section>
<footer class="welcome-footer">
<p class="welcome-footer__privacy" data-mokosh-key="welcome.footer.privacy"></p>
</footer>
</main>
<script type="module" src="welcome.ts"></script>
</body>
</html>

137
src/welcome/welcome.ts Normal file
View File

@@ -0,0 +1,137 @@
// src/welcome/welcome.ts — Plan 01-10 D-17-onboarding welcome page
// entrypoint.
//
// Vanilla DOM. Populates two attribute-selector flows:
// - [data-mokosh-key='<key>'] from the COPY map (non-tagline strings;
// engineering placeholders per D-03 Sober voice register).
// - [data-mokosh-i18n-key='<key>'] from chrome.i18n.getMessage with
// `|| <en-const>` fallback per the Plan 01-12 Wave 4 popup pattern.
// The two D-08 tagline keys (welcomeHeroRu + welcomeHeroEn) live in
// _locales/{en,ru}/messages.json (Plan 01-12 Wave 3; 16 keys total).
//
// NO event handlers — the welcome tab is informational + read-only per
// D-16-toolbar charter (toolbar onClicked owns the start path; the
// welcome page TELLS the operator where the icon is, does NOT host a
// "Start Mokosh" actionable button).
//
// Style/naming:
// - filter-pipeline form per project rule "no `continue` statements";
// - full-word variable names;
// - no `as any`;
// - centralized Logger from src/shared/logger.ts.
//
// References:
// - D-17-onboarding (CONTEXT.md line 537+)
// - D-16-toolbar (01-09-SUMMARY.md)
// - Plan 01-12 Wave 4 fallback pattern (src/popup/index.ts:41 i18n
// helper). Plan 01-10 inlines the pattern into the filter-pipeline
// so the per-element resolution stays branch-free.
import { Logger } from '../shared/logger';
import {
COPY,
WELCOME_HERO_RU_FALLBACK,
WELCOME_HERO_EN_FALLBACK,
} from './copy';
const logger = new Logger('Welcome');
/**
* Walk every [data-mokosh-key='<key>'] element and write its
* textContent (or document.title for the <title> element) from the
* COPY map. Filter-pipeline form: any element whose attribute is
* missing or whose key is absent from COPY is dropped from the
* applied set; we log the missing count once at the end so an unwired
* attribute is visible in the SW devtools console without spamming.
*/
function populateCopy(): void {
const els = Array.from(
document.querySelectorAll<HTMLElement>('[data-mokosh-key]'),
);
const pairs = els
.map((el) => ({ el, key: el.getAttribute('data-mokosh-key') }))
.filter(
(p): p is { el: HTMLElement; key: string } => typeof p.key === 'string',
)
.map((p) => ({ ...p, value: COPY[p.key] }))
.filter(
(p): p is { el: HTMLElement; key: string; value: string } =>
typeof p.value === 'string',
);
for (const { el, value } of pairs) {
if (el.tagName === 'TITLE') {
document.title = value;
} else {
el.textContent = value;
}
}
const missing = els.length - pairs.length;
if (missing > 0) {
logger.warn(
'populateCopy: '
+ String(missing)
+ ' [data-mokosh-key] elements had missing COPY entries',
);
}
}
/**
* Walk every [data-mokosh-i18n-key='<key>'] element and write its
* textContent from chrome.i18n.getMessage(key). Plan 01-12 Wave 4
* fallback pattern: when getMessage returns '' (unknown key OR
* chrome.i18n undefined in non-extension contexts), fall back to the
* en-const string exported from ./copy. Empty resolutions are dropped
* from the applied set; missing count surfaces via logger.warn for
* post-mortem visibility.
*
* The two Plan 01-10 tagline keys (welcomeHeroRu + welcomeHeroEn)
* resolve from _locales/{en,ru}/messages.json (Plan 01-12 Wave 3).
*/
function populateI18n(): void {
const fallbacks: Readonly<Record<string, string>> = Object.freeze({
welcomeHeroRu: WELCOME_HERO_RU_FALLBACK,
welcomeHeroEn: WELCOME_HERO_EN_FALLBACK,
});
const els = Array.from(
document.querySelectorAll<HTMLElement>('[data-mokosh-i18n-key]'),
);
const pairs = els
.map((el) => ({ el, key: el.getAttribute('data-mokosh-i18n-key') }))
.filter(
(p): p is { el: HTMLElement; key: string } => typeof p.key === 'string',
)
.map((p) => {
const fromI18n = chrome?.i18n?.getMessage?.(p.key) ?? '';
const fromFallback = fallbacks[p.key] ?? '';
const value = fromI18n.length > 0 ? fromI18n : fromFallback;
return { ...p, value };
})
.filter((p) => p.value.length > 0);
for (const { el, value } of pairs) {
el.textContent = value;
}
const missing = els.length - pairs.length;
if (missing > 0) {
logger.warn(
'populateI18n: '
+ String(missing)
+ ' [data-mokosh-i18n-key] elements had missing chrome.i18n + fallback values',
);
}
}
/**
* Initialize the welcome page. populateCopy first (non-tagline strings),
* then populateI18n (the two D-08 tagline strings via chrome.i18n).
*/
function init(): void {
populateCopy();
populateI18n();
logger.log('welcome page ready');
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}