Milestone v1 (v2.0.0): Mokosh — Session Capture #1
@@ -16,6 +16,12 @@
|
||||
"host_permissions": [
|
||||
"<all_urls>"
|
||||
],
|
||||
"web_accessible_resources": [
|
||||
{
|
||||
"resources": ["src/welcome/welcome.html"],
|
||||
"matches": ["<all_urls>"]
|
||||
}
|
||||
],
|
||||
"background": {
|
||||
"service_worker": "src/background/index.ts",
|
||||
"type": "module"
|
||||
|
||||
65
src/welcome/copy.ts
Normal file
65
src/welcome/copy.ts
Normal 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
164
src/welcome/welcome.css
Normal 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
53
src/welcome/welcome.html
Normal 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
137
src/welcome/welcome.ts
Normal 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();
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user