diff --git a/manifest.json b/manifest.json index 9475042..182293d 100644 --- a/manifest.json +++ b/manifest.json @@ -16,6 +16,12 @@ "host_permissions": [ "" ], + "web_accessible_resources": [ + { + "resources": ["src/welcome/welcome.html"], + "matches": [""] + } + ], "background": { "service_worker": "src/background/index.ts", "type": "module" diff --git a/src/welcome/copy.ts b/src/welcome/copy.ts new file mode 100644 index 0000000..5b3a720 --- /dev/null +++ b/src/welcome/copy.ts @@ -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 `|| ` 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='']` attribute selectors in welcome.html; + * populateCopy() in welcome.ts walks every element with the attribute + * and writes textContent (or document.title for the 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 не отправляет данные на серверы. Архив создаётся ' + + 'локально по вашему запросу и остаётся на вашем компьютере.', +}); diff --git a/src/welcome/welcome.css b/src/welcome/welcome.css new file mode 100644 index 0000000..6fd9921 --- /dev/null +++ b/src/welcome/welcome.css @@ -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); + } +} diff --git a/src/welcome/welcome.html b/src/welcome/welcome.html new file mode 100644 index 0000000..3cc00eb --- /dev/null +++ b/src/welcome/welcome.html @@ -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 + + + +
+
+ +

+

+

+
+
+

+

+

+
+
+ +
+
+ + + diff --git a/src/welcome/welcome.ts b/src/welcome/welcome.ts new file mode 100644 index 0000000..5aaaa20 --- /dev/null +++ b/src/welcome/welcome.ts @@ -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=''] from the COPY map (non-tagline strings; +// engineering placeholders per D-03 Sober voice register). +// - [data-mokosh-i18n-key=''] from chrome.i18n.getMessage with +// `|| ` 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=''] element and write its + * textContent (or document.title for the 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(); +} diff --git a/vite.config.ts b/vite.config.ts index 76c869d..e8cd668 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -62,6 +62,12 @@ export default defineConfig({ rollupOptions: { input: { offscreen: 'src/offscreen/index.html', + // Plan 01-10 D-17-onboarding: welcome page bundled into both + // production and test builds. The page is reached via + // chrome.runtime.getURL('src/welcome/welcome.html') from the + // SW's openWelcomeIfFirstInstall helper; it requires the + // manifest's web_accessible_resources block to be navigable. + welcome: 'src/welcome/welcome.html', }, }, }, diff --git a/vite.test.config.ts b/vite.test.config.ts index 3216ca6..c219ac2 100644 --- a/vite.test.config.ts +++ b/vite.test.config.ts @@ -88,6 +88,11 @@ export default defineConfig(({ command, mode }) => // and rewrite the <script type="module" src> reference to // the bundled chunk's hashed filename. extension_page_harness: 'tests/uat/extension-page-harness.html', + // Plan 01-10 D-17-onboarding: mirror of vite.config.ts — + // the harness A15/A16/A17 assertions navigate against the + // test bundle and need welcome.html reachable under + // dist-test/src/welcome/welcome.html. + welcome: 'src/welcome/welcome.html', }, }, },