Milestone v1 (v2.0.0): Mokosh — Session Capture #1

Merged
strategy155 merged 297 commits from gsd/phase-04-harden-clean-up-optional into main 2026-05-31 15:34:17 +00:00
7 changed files with 436 additions and 0 deletions
Showing only changes of commit 49f087fe40 - Show all commits

View File

@@ -16,6 +16,12 @@
"host_permissions": [ "host_permissions": [
"<all_urls>" "<all_urls>"
], ],
"web_accessible_resources": [
{
"resources": ["src/welcome/welcome.html"],
"matches": ["<all_urls>"]
}
],
"background": { "background": {
"service_worker": "src/background/index.ts", "service_worker": "src/background/index.ts",
"type": "module" "type": "module"

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();
}

View File

@@ -62,6 +62,12 @@ export default defineConfig({
rollupOptions: { rollupOptions: {
input: { input: {
offscreen: 'src/offscreen/index.html', 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',
}, },
}, },
}, },

View File

@@ -88,6 +88,11 @@ export default defineConfig(({ command, mode }) =>
// and rewrite the <script type="module" src> reference to // and rewrite the <script type="module" src> reference to
// the bundled chunk's hashed filename. // the bundled chunk's hashed filename.
extension_page_harness: 'tests/uat/extension-page-harness.html', 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',
}, },
}, },
}, },