@deeeed/metamask-harness 0.34.2 → 0.34.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/dist/adapters/mobile/source-freshness.js +51 -14
- package/dist/adapters.js +5 -2
- package/dist/recipe-security.js +1 -0
- package/library/actions/core/perps/_controller.mjs +75 -23
- package/library/actions/core/perps/read_account.mjs +6 -12
- package/library/actions/core/perps/read_snapshot.mjs +66 -0
- package/library/actions/extension/performance/_navigation-memory.mjs +372 -0
- package/library/actions/extension/performance/compare_idle_navigation_memory.mjs +18 -0
- package/library/actions/extension/performance/measure_detached_dom.mjs +18 -0
- package/library/actions/extension/performance/measure_navigation_memory.mjs +18 -0
- package/library/actions/mobile/app/network-control.mjs +24 -12
- package/library/manifests/core.action-manifest.json +55 -0
- package/library/manifests/extension.action-manifest.json +100 -0
- package/library/recipes/core/perps/snapshot.recipe.json +25 -0
- package/library/recipes/extension/performance/navigation-memory.recipe.json +175 -0
- package/package.json +1 -1
- package/scripts/site-contrast.mjs +66 -10
- package/site/architecture.html +22 -16
- package/site/assets/progress.mjs +47 -2
- package/site/assets/style.css +181 -1
- package/site/cheatsheet.html +14 -12
- package/site/how-it-works.html +693 -0
- package/site/index.html +133 -643
- package/site/perps.html +7 -6
- package/site/recipes.html +23 -17
- package/site/reviewers.html +7 -6
- package/site/tutorials/index.html +7 -6
- package/site/tutorials/v1.html +12 -11
- package/site/tutorials/v2.html +7 -6
- package/site/tutorials/v3.html +15 -11
- package/site/tutorials/v4.html +6 -5
- package/site/tutorials/v5.html +6 -5
- package/site/tutorials/v6.html +6 -5
- package/site/tutorials/v7.html +6 -5
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://farmslot.io/schemas/recipe-v1.schema.json",
|
|
3
|
+
"title": "Extension navigation retained-memory audit",
|
|
4
|
+
"description": "Measures post-GC retained nodes and listeners across eight representative Extension screens, verifies detached DOM does not grow, and separates navigation-attributable listener growth from an equal-duration idle control.",
|
|
5
|
+
"proofTargets": [
|
|
6
|
+
{
|
|
7
|
+
"id": "screen-slopes",
|
|
8
|
+
"claim": "Eight representative Extension screens remain within separate post-GC retained-node and listener growth bounds during navigation churn."
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"id": "detached-dom",
|
|
12
|
+
"claim": "Detached DOM retained after forced garbage collection does not grow beyond the declared bound."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"id": "idle-control",
|
|
16
|
+
"claim": "Navigation-attributable listener growth remains within its declared bound after subtracting an equal-duration idle control."
|
|
17
|
+
}
|
|
18
|
+
],
|
|
19
|
+
"workflow": {
|
|
20
|
+
"entry": "require-cdp",
|
|
21
|
+
"nodes": {
|
|
22
|
+
"require-cdp": {
|
|
23
|
+
"action": "cdp.target",
|
|
24
|
+
"required": true,
|
|
25
|
+
"require_reachable": true,
|
|
26
|
+
"intent": "Confirm the live Extension CDP target is reachable before measuring retained memory.",
|
|
27
|
+
"next": "ensure-unlocked"
|
|
28
|
+
},
|
|
29
|
+
"ensure-unlocked": {
|
|
30
|
+
"action": "metamask.wallet.ensure_unlocked",
|
|
31
|
+
"intent": "Ensure the representative screens are reachable from an unlocked wallet.",
|
|
32
|
+
"next": "market-detail"
|
|
33
|
+
},
|
|
34
|
+
"market-detail": {
|
|
35
|
+
"action": "metamask.performance.measure_navigation_memory",
|
|
36
|
+
"screen_hash": "#/perps/market/ETH",
|
|
37
|
+
"screen_test_id": "perps-market-detail-page",
|
|
38
|
+
"return_hash": "#/",
|
|
39
|
+
"cycles": 8,
|
|
40
|
+
"timeout_ms": 400000,
|
|
41
|
+
"max_nodes_per_cycle": 2,
|
|
42
|
+
"max_listeners_per_cycle": 4,
|
|
43
|
+
"intent": "Measure the post-GC navigation slope for Perps market detail.",
|
|
44
|
+
"proves": ["screen-slopes"],
|
|
45
|
+
"next": "order-entry"
|
|
46
|
+
},
|
|
47
|
+
"order-entry": {
|
|
48
|
+
"action": "metamask.performance.measure_navigation_memory",
|
|
49
|
+
"screen_hash": "#/perps/trade/ETH?direction=long&mode=new",
|
|
50
|
+
"screen_test_id": "perps-order-entry-page",
|
|
51
|
+
"return_hash": "#/",
|
|
52
|
+
"cycles": 6,
|
|
53
|
+
"timeout_ms": 400000,
|
|
54
|
+
"max_nodes_per_cycle": 2,
|
|
55
|
+
"max_listeners_per_cycle": 4,
|
|
56
|
+
"intent": "Measure the post-GC navigation slope for Perps order entry.",
|
|
57
|
+
"proves": ["screen-slopes"],
|
|
58
|
+
"next": "perps-home"
|
|
59
|
+
},
|
|
60
|
+
"perps-home": {
|
|
61
|
+
"action": "metamask.performance.measure_navigation_memory",
|
|
62
|
+
"screen_hash": "#/perps-home",
|
|
63
|
+
"screen_test_id": "perps-home-page",
|
|
64
|
+
"return_hash": "#/",
|
|
65
|
+
"cycles": 6,
|
|
66
|
+
"timeout_ms": 400000,
|
|
67
|
+
"max_nodes_per_cycle": 2,
|
|
68
|
+
"max_listeners_per_cycle": 4,
|
|
69
|
+
"intent": "Measure the post-GC navigation slope for Perps home.",
|
|
70
|
+
"proves": ["screen-slopes"],
|
|
71
|
+
"next": "market-list"
|
|
72
|
+
},
|
|
73
|
+
"market-list": {
|
|
74
|
+
"action": "metamask.performance.measure_navigation_memory",
|
|
75
|
+
"screen_hash": "#/perps/market-list",
|
|
76
|
+
"screen_test_id": "market-list-view",
|
|
77
|
+
"return_hash": "#/",
|
|
78
|
+
"cycles": 6,
|
|
79
|
+
"timeout_ms": 400000,
|
|
80
|
+
"max_nodes_per_cycle": 2,
|
|
81
|
+
"max_listeners_per_cycle": 4,
|
|
82
|
+
"intent": "Measure the post-GC navigation slope for the Perps market list.",
|
|
83
|
+
"proves": ["screen-slopes"],
|
|
84
|
+
"next": "perps-activity"
|
|
85
|
+
},
|
|
86
|
+
"perps-activity": {
|
|
87
|
+
"action": "metamask.performance.measure_navigation_memory",
|
|
88
|
+
"screen_hash": "#/perps/activity",
|
|
89
|
+
"screen_test_id": "perps-activity-page",
|
|
90
|
+
"return_hash": "#/",
|
|
91
|
+
"cycles": 6,
|
|
92
|
+
"timeout_ms": 400000,
|
|
93
|
+
"max_nodes_per_cycle": 2,
|
|
94
|
+
"max_listeners_per_cycle": 4,
|
|
95
|
+
"intent": "Measure the post-GC navigation slope for Perps activity.",
|
|
96
|
+
"proves": ["screen-slopes"],
|
|
97
|
+
"next": "asset-details"
|
|
98
|
+
},
|
|
99
|
+
"asset-details": {
|
|
100
|
+
"action": "metamask.performance.measure_navigation_memory",
|
|
101
|
+
"screen_hash": "#/asset/0x1/",
|
|
102
|
+
"screen_test_id": "asset-price-chart",
|
|
103
|
+
"return_hash": "#/",
|
|
104
|
+
"cycles": 6,
|
|
105
|
+
"timeout_ms": 400000,
|
|
106
|
+
"max_nodes_per_cycle": 2,
|
|
107
|
+
"max_listeners_per_cycle": 4,
|
|
108
|
+
"intent": "Measure the post-GC navigation slope for asset details.",
|
|
109
|
+
"proves": ["screen-slopes"],
|
|
110
|
+
"next": "swaps-bridge"
|
|
111
|
+
},
|
|
112
|
+
"swaps-bridge": {
|
|
113
|
+
"action": "metamask.performance.measure_navigation_memory",
|
|
114
|
+
"screen_hash": "#/cross-chain/swaps/prepare-bridge-page",
|
|
115
|
+
"screen_test_id": "bridge-source-button",
|
|
116
|
+
"return_hash": "#/",
|
|
117
|
+
"cycles": 6,
|
|
118
|
+
"timeout_ms": 400000,
|
|
119
|
+
"max_nodes_per_cycle": 2,
|
|
120
|
+
"max_listeners_per_cycle": 4,
|
|
121
|
+
"intent": "Measure the post-GC navigation slope for Swaps and Bridge.",
|
|
122
|
+
"proves": ["screen-slopes"],
|
|
123
|
+
"next": "settings-control"
|
|
124
|
+
},
|
|
125
|
+
"settings-control": {
|
|
126
|
+
"action": "metamask.performance.measure_navigation_memory",
|
|
127
|
+
"screen_hash": "#/settings",
|
|
128
|
+
"screen_test_id": "settings-tab-bar-grouped",
|
|
129
|
+
"return_hash": "#/",
|
|
130
|
+
"cycles": 6,
|
|
131
|
+
"timeout_ms": 400000,
|
|
132
|
+
"max_nodes_per_cycle": 2,
|
|
133
|
+
"max_listeners_per_cycle": 4,
|
|
134
|
+
"intent": "Measure a non-Perps control screen with the same retained-memory gate.",
|
|
135
|
+
"proves": ["screen-slopes"],
|
|
136
|
+
"next": "detached-market-detail"
|
|
137
|
+
},
|
|
138
|
+
"detached-market-detail": {
|
|
139
|
+
"action": "metamask.performance.measure_detached_dom",
|
|
140
|
+
"screen_hash": "#/perps/market/ETH",
|
|
141
|
+
"screen_test_id": "perps-market-detail-page",
|
|
142
|
+
"return_hash": "#/",
|
|
143
|
+
"cycles": 5,
|
|
144
|
+
"timeout_ms": 400000,
|
|
145
|
+
"max_detached_growth": 50,
|
|
146
|
+
"intent": "Compare retained detached DOM before and after market-detail navigation churn.",
|
|
147
|
+
"proves": ["detached-dom"],
|
|
148
|
+
"next": "idle-control-market-detail"
|
|
149
|
+
},
|
|
150
|
+
"idle-control-market-detail": {
|
|
151
|
+
"action": "metamask.performance.compare_idle_navigation_memory",
|
|
152
|
+
"screen_hash": "#/perps/market/ETH",
|
|
153
|
+
"screen_test_id": "perps-market-detail-page",
|
|
154
|
+
"return_hash": "#/",
|
|
155
|
+
"rounds": 8,
|
|
156
|
+
"round_duration_ms": 10000,
|
|
157
|
+
"timeout_ms": 400000,
|
|
158
|
+
"max_attributable_listeners_per_cycle": 2,
|
|
159
|
+
"intent": "Subtract equal-duration idle growth from the market-detail navigation slope.",
|
|
160
|
+
"proves": ["idle-control"],
|
|
161
|
+
"next": "return-home"
|
|
162
|
+
},
|
|
163
|
+
"return-home": {
|
|
164
|
+
"action": "ui.navigate",
|
|
165
|
+
"page": "home",
|
|
166
|
+
"intent": "Leave the Extension on a known screen after the audit.",
|
|
167
|
+
"next": "done"
|
|
168
|
+
},
|
|
169
|
+
"done": {
|
|
170
|
+
"action": "end",
|
|
171
|
+
"status": "pass"
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
package/package.json
CHANGED
|
@@ -146,6 +146,7 @@ const REVEAL = String.raw`
|
|
|
146
146
|
const d = document.getElementById(l.getAttribute('aria-controls'));
|
|
147
147
|
if (d) d.hidden = false;
|
|
148
148
|
});
|
|
149
|
+
document.querySelectorAll('[role="tabpanel"][hidden]').forEach((p) => { p.hidden = false; });
|
|
149
150
|
return true;
|
|
150
151
|
})()
|
|
151
152
|
`;
|
|
@@ -331,6 +332,9 @@ async function auditPage(browserEndpoint, url) {
|
|
|
331
332
|
async function checkBehavior(browserEndpoint, port, pages) {
|
|
332
333
|
const base = `http://127.0.0.1:${port}/`;
|
|
333
334
|
await withPage(browserEndpoint, `${base}index.html`, async (page) => {
|
|
335
|
+
// Step 1 is the hero call to action; it was a collapsible before, so accept
|
|
336
|
+
// either shape. Step 2 is a tabbed set of prompts, one per kind of work —
|
|
337
|
+
// every one of them has to survive a copy, including the hidden panels.
|
|
334
338
|
const copied = await page.evaluate(String.raw`
|
|
335
339
|
(async () => {
|
|
336
340
|
Object.defineProperty(navigator, 'clipboard', {
|
|
@@ -342,20 +346,58 @@ async function checkBehavior(browserEndpoint, port, pages) {
|
|
|
342
346
|
},
|
|
343
347
|
},
|
|
344
348
|
});
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
349
|
+
async function grab(button) {
|
|
350
|
+
if (!button) throw new Error('copy button was not initialized');
|
|
351
|
+
window.__siteCopied = null;
|
|
352
|
+
button.click();
|
|
353
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
354
|
+
return window.__siteCopied;
|
|
355
|
+
}
|
|
356
|
+
const out = { setup: await grab(document.querySelector('.cmd-hero .copy, .prompt .copy')), work: [] };
|
|
357
|
+
for (const tab of document.querySelectorAll('[role="tab"]')) {
|
|
358
|
+
tab.click();
|
|
359
|
+
const panel = document.getElementById(tab.getAttribute('aria-controls'));
|
|
360
|
+
if (!panel || panel.hidden) throw new Error('tab ' + tab.id + ' did not reveal its panel');
|
|
361
|
+
out.work.push(await grab(panel.querySelector('.cmd .copy')));
|
|
362
|
+
}
|
|
363
|
+
return out;
|
|
352
364
|
})()
|
|
353
365
|
`);
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
366
|
+
// Setting up installs the harness and the skills; the work prompts invoke
|
|
367
|
+
// them by their exact installed names.
|
|
368
|
+
const expected = [
|
|
369
|
+
['npm i -g @deeeed/metamask-harness@latest', copied.setup],
|
|
370
|
+
['yarn skills', copied.setup],
|
|
371
|
+
['/mms-recipe-pr-qa-review', copied.work.join('\n')],
|
|
372
|
+
['/mms-recipe-cook', copied.work.join('\n')],
|
|
373
|
+
];
|
|
374
|
+
const missing = expected.filter(([line, text]) => !text?.includes(line)).map(([line]) => line);
|
|
375
|
+
if (missing.length) {
|
|
376
|
+
throw new Error(`copy prompt omitted ${missing.map((m) => `"${m}"`).join(', ')}`);
|
|
377
|
+
}
|
|
378
|
+
if (copied.work.length < 3) {
|
|
379
|
+
throw new Error(`expected three work prompts, copied ${copied.work.length}`);
|
|
357
380
|
}
|
|
358
381
|
|
|
382
|
+
// The landing page is the lobby: one prompt, one way onward, nothing else.
|
|
383
|
+
const lobby = await page.evaluate(String.raw`
|
|
384
|
+
(() => ({
|
|
385
|
+
nav: document.querySelectorAll('.nav a').length,
|
|
386
|
+
steps: document.querySelectorAll('.step[data-step]').length,
|
|
387
|
+
depth: document.querySelector('a[href="how-it-works.html"]') !== null,
|
|
388
|
+
anchor: document.getElementById('prompt') !== null,
|
|
389
|
+
}))()
|
|
390
|
+
`);
|
|
391
|
+
if (lobby.nav || lobby.steps || !lobby.depth || !lobby.anchor) {
|
|
392
|
+
throw new Error(
|
|
393
|
+
`landing page is not a lobby: ${lobby.nav} nav links, ${lobby.steps} steps, ` +
|
|
394
|
+
`depth link ${lobby.depth}, prompt anchor ${lobby.anchor}`,
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// The walkthrough and its saved progress live one click deeper.
|
|
400
|
+
await withPage(browserEndpoint, `${base}how-it-works.html`, async (page) => {
|
|
359
401
|
const progress = await page.evaluate(String.raw`
|
|
360
402
|
(() => {
|
|
361
403
|
const box = document.querySelector('.step-check');
|
|
@@ -426,6 +468,20 @@ async function checkBehavior(browserEndpoint, port, pages) {
|
|
|
426
468
|
if (broken.length) {
|
|
427
469
|
throw new Error(`${relative} has broken internal links: ${broken.join(', ')}`);
|
|
428
470
|
}
|
|
471
|
+
// Every page past the lobby owes the reader one click back to the prompt.
|
|
472
|
+
if (relative !== 'index.html') {
|
|
473
|
+
const back = await page.evaluate(String.raw`
|
|
474
|
+
(() => {
|
|
475
|
+
const link = document.querySelector('.nav a.nav-cta');
|
|
476
|
+
if (!link) return null;
|
|
477
|
+
const url = new URL(link.getAttribute('href'), location.href);
|
|
478
|
+
return url.pathname + url.hash;
|
|
479
|
+
})()
|
|
480
|
+
`);
|
|
481
|
+
if (back !== '/index.html#prompt') {
|
|
482
|
+
throw new Error(`${relative} has no nav link back to the setup prompt (found ${back})`);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
429
485
|
for (const width of [834, 390]) {
|
|
430
486
|
await page.send('Emulation.setDeviceMetricsOverride', {
|
|
431
487
|
width,
|
package/site/architecture.html
CHANGED
|
@@ -14,11 +14,12 @@
|
|
|
14
14
|
<div class="wrap topbar-inner">
|
|
15
15
|
<a class="brand" href="index.html">
|
|
16
16
|
<span class="brand-mark" aria-hidden="true"></span>
|
|
17
|
-
<span class="brand-name">
|
|
17
|
+
<span class="brand-name">recipes</span>
|
|
18
18
|
</a>
|
|
19
19
|
<nav class="nav" aria-label="Main">
|
|
20
|
-
<a href="index.html">
|
|
21
|
-
<a href="
|
|
20
|
+
<a class="nav-cta" href="index.html#prompt">Quick start</a>
|
|
21
|
+
<a href="how-it-works.html">How it works</a>
|
|
22
|
+
<a href="recipes.html">Recipe anatomy</a>
|
|
22
23
|
<a href="perps.html">Perps</a>
|
|
23
24
|
<a href="cheatsheet.html">Cheatsheet</a>
|
|
24
25
|
<a href="architecture.html" aria-current="page">Architecture</a>
|
|
@@ -30,11 +31,12 @@
|
|
|
30
31
|
|
|
31
32
|
<main>
|
|
32
33
|
<section class="wrap hero" style="padding-bottom:1rem">
|
|
33
|
-
<span class="eyebrow">The stack</span>
|
|
34
|
+
<span class="eyebrow">The stack behind a recipe</span>
|
|
34
35
|
<h1>How the pieces fit together</h1>
|
|
35
36
|
<p class="lede">
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
The recipe is the unit of trust; the rest of the stack runs it. Four moving parts, two of which
|
|
38
|
+
people routinely confuse. What each layer is, where it lives, which workflow you are in, and the
|
|
39
|
+
mistakes that cost newcomers their first afternoon.
|
|
38
40
|
</p>
|
|
39
41
|
</section>
|
|
40
42
|
|
|
@@ -45,8 +47,9 @@
|
|
|
45
47
|
<span class="k">A recipe proves a task with actions.</span> Recipes are JSON graphs of typed
|
|
46
48
|
actions; <code>mm-harness</code> executes them against a real app and produces an evidence bundle
|
|
47
49
|
a reviewer can trust. <strong>Skills</strong> teach an agent a workflow — author a recipe, review
|
|
48
|
-
a PR, validate a release
|
|
49
|
-
|
|
50
|
+
a PR, validate a release — and are the layer you work through; they call the harness, which runs
|
|
51
|
+
the recipe. <strong>Recipe libraries</strong> hold each team's recipes and domain actions.
|
|
52
|
+
Compose in that order, against the product repo under test.
|
|
50
53
|
</p>
|
|
51
54
|
</div>
|
|
52
55
|
</section>
|
|
@@ -67,9 +70,12 @@
|
|
|
67
70
|
release — turning "figure it out" into a procedure with gates you can steer.
|
|
68
71
|
</p>
|
|
69
72
|
<p>
|
|
70
|
-
Skills
|
|
71
|
-
|
|
72
|
-
|
|
73
|
+
Skills come from the public <code>MetaMask/skills</code> repo plus the internal
|
|
74
|
+
<code>Consensys/skills</code> overlay, which needs access to that org; private skills override
|
|
75
|
+
public ones on a name collision. Clone each, point <code>METAMASK_SKILLS_DIR</code> and
|
|
76
|
+
<code>CONSENSYS_SKILLS_DIR</code> at them, then install per checkout with
|
|
77
|
+
<code>yarn skills</code>. <strong>You do not need skills to run a recipe</strong>, only to have
|
|
78
|
+
an agent follow a proven workflow instead of improvising one.
|
|
73
79
|
<a href="tutorials/v3.html">V3 walks the install.</a>
|
|
74
80
|
</p>
|
|
75
81
|
</div>
|
|
@@ -129,8 +135,8 @@
|
|
|
129
135
|
</p>
|
|
130
136
|
<p style="margin-bottom:0">
|
|
131
137
|
They differ, and the harness says so rather than pretending otherwise: Core is headless with
|
|
132
|
-
nothing to launch and no UI actions;
|
|
133
|
-
|
|
138
|
+
nothing to launch and no UI actions; Extension and iOS can record full-run video; Android
|
|
139
|
+
replay video is not implemented yet, so use screenshot evidence there.
|
|
134
140
|
</p>
|
|
135
141
|
</div>
|
|
136
142
|
</div>
|
|
@@ -466,7 +472,7 @@
|
|
|
466
472
|
|
|
467
473
|
<hr class="sep">
|
|
468
474
|
<div class="btn-row">
|
|
469
|
-
<a class="btn btn-primary" href="
|
|
475
|
+
<a class="btn btn-primary" href="how-it-works.html#steps">Do the walkthrough →</a>
|
|
470
476
|
<a class="btn btn-ghost" href="reviewers.html">Read an evidence bundle</a>
|
|
471
477
|
</div>
|
|
472
478
|
</section>
|
|
@@ -474,8 +480,8 @@
|
|
|
474
480
|
|
|
475
481
|
<footer class="footer">
|
|
476
482
|
<div class="wrap">
|
|
477
|
-
<p>Internal getting-started guide for
|
|
478
|
-
<p>
|
|
483
|
+
<p>Internal getting-started guide for proving MetaMask changes with recipes. Not official MetaMask product documentation.</p>
|
|
484
|
+
<p>Sample output moves between releases; trust your terminal over this page.</p>
|
|
479
485
|
</div>
|
|
480
486
|
</footer>
|
|
481
487
|
|
package/site/assets/progress.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* Shared behaviour for the getting-started site: checklist progress, copy
|
|
3
|
-
* buttons, platform filters, and the layer diagram. No dependencies, no
|
|
3
|
+
* buttons, platform filters, tabs, and the layer diagram. No dependencies, no
|
|
4
4
|
* network, no tracking. Progress lives in localStorage under
|
|
5
5
|
* `mmh.progress.<page>`, where <page> comes from body[data-progress-page].
|
|
6
|
-
* Pages that share a namespace share their state (
|
|
6
|
+
* Pages that share a namespace share their state (the How it works walkthrough and the V1
|
|
7
7
|
* tutorial are deliberately the same checklist).
|
|
8
8
|
*/
|
|
9
9
|
(function () {
|
|
@@ -235,6 +235,50 @@
|
|
|
235
235
|
select('all');
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
+
/* ---------- tabs ---------- */
|
|
239
|
+
|
|
240
|
+
/*
|
|
241
|
+
* One tablist per group, roving tabindex: only the selected tab is in the
|
|
242
|
+
* tab order, arrows move between them. Panels stay in the DOM so their
|
|
243
|
+
* commands remain copyable the moment a tab is shown.
|
|
244
|
+
*/
|
|
245
|
+
function initTabs() {
|
|
246
|
+
[].slice.call(document.querySelectorAll('[role="tablist"]')).forEach(function (list) {
|
|
247
|
+
var tabs = [].slice.call(list.querySelectorAll('[role="tab"]'));
|
|
248
|
+
if (!tabs.length) return;
|
|
249
|
+
|
|
250
|
+
function select(tab, focus) {
|
|
251
|
+
tabs.forEach(function (other) {
|
|
252
|
+
var on = other === tab;
|
|
253
|
+
other.setAttribute('aria-selected', String(on));
|
|
254
|
+
other.tabIndex = on ? 0 : -1;
|
|
255
|
+
var panel = document.getElementById(other.getAttribute('aria-controls'));
|
|
256
|
+
if (panel) panel.hidden = !on;
|
|
257
|
+
});
|
|
258
|
+
if (focus) tab.focus();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
tabs.forEach(function (tab, i) {
|
|
262
|
+
tab.addEventListener('click', function () { select(tab, false); });
|
|
263
|
+
tab.addEventListener('keydown', function (e) {
|
|
264
|
+
var next = null;
|
|
265
|
+
if (e.key === 'ArrowRight') next = tabs[(i + 1) % tabs.length];
|
|
266
|
+
if (e.key === 'ArrowLeft') next = tabs[(i - 1 + tabs.length) % tabs.length];
|
|
267
|
+
if (e.key === 'Home') next = tabs[0];
|
|
268
|
+
if (e.key === 'End') next = tabs[tabs.length - 1];
|
|
269
|
+
if (!next) return;
|
|
270
|
+
e.preventDefault();
|
|
271
|
+
select(next, true);
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
var current = tabs.filter(function (t) {
|
|
276
|
+
return t.getAttribute('aria-selected') === 'true';
|
|
277
|
+
})[0];
|
|
278
|
+
select(current || tabs[0], false);
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
238
282
|
/* ---------- layer diagram ---------- */
|
|
239
283
|
|
|
240
284
|
function initLayers() {
|
|
@@ -261,6 +305,7 @@
|
|
|
261
305
|
initCopy();
|
|
262
306
|
initCellCopy();
|
|
263
307
|
initFilters();
|
|
308
|
+
initTabs();
|
|
264
309
|
initLayers();
|
|
265
310
|
}
|
|
266
311
|
|
package/site/assets/style.css
CHANGED
|
@@ -37,7 +37,9 @@
|
|
|
37
37
|
|
|
38
38
|
*, *::before, *::after { box-sizing: border-box; }
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
/* Clears the sticky bar plus the progress rail (114px together), so an anchor
|
|
41
|
+
never parks its heading underneath them. */
|
|
42
|
+
html { scroll-behavior: smooth; scroll-padding-top: 116px; }
|
|
41
43
|
|
|
42
44
|
body {
|
|
43
45
|
margin: 0;
|
|
@@ -158,6 +160,11 @@ main { padding-bottom: 6rem; }
|
|
|
158
160
|
.nav a:hover { color: var(--text); background: var(--surface); text-decoration: none; }
|
|
159
161
|
.nav a[aria-current="page"] { color: var(--accent); background: var(--accent-soft); }
|
|
160
162
|
|
|
163
|
+
/* The way back to the setup prompt. It leads the nav and stays visible when the
|
|
164
|
+
rest of the bar scrolls, because it is the one link every page owes a reader. */
|
|
165
|
+
.nav a.nav-cta { color: var(--accent); border: 1px solid var(--accent-line); }
|
|
166
|
+
.nav a.nav-cta:hover { background: var(--accent-soft); color: var(--accent); }
|
|
167
|
+
|
|
161
168
|
/* ---------- progress rail ---------- */
|
|
162
169
|
|
|
163
170
|
.progress-rail {
|
|
@@ -248,6 +255,169 @@ main { padding-bottom: 6rem; }
|
|
|
248
255
|
color: var(--text-faint);
|
|
249
256
|
}
|
|
250
257
|
|
|
258
|
+
/* ---------- lobby (front page) ----------
|
|
259
|
+
The entry page carries one idea and one call to action: no nav, no rail, no
|
|
260
|
+
checklist. Everything else in the site hangs off the single button below. */
|
|
261
|
+
|
|
262
|
+
.lobby {
|
|
263
|
+
display: flex;
|
|
264
|
+
align-items: center;
|
|
265
|
+
min-height: 100vh;
|
|
266
|
+
padding: 3rem 0 4rem;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.lobby-inner {
|
|
270
|
+
width: min(760px, calc(100% - 3rem));
|
|
271
|
+
margin: 0 auto;
|
|
272
|
+
text-align: center;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.lobby-mark {
|
|
276
|
+
display: block;
|
|
277
|
+
width: 26px;
|
|
278
|
+
height: 26px;
|
|
279
|
+
margin: 0 auto 2rem;
|
|
280
|
+
border-radius: 7px;
|
|
281
|
+
background: linear-gradient(145deg, var(--accent) 0%, var(--accent-deep) 100%);
|
|
282
|
+
transform: rotate(45deg);
|
|
283
|
+
box-shadow: 0 0 0 1px rgba(246, 133, 27, 0.3), 0 4px 18px rgba(246, 133, 27, 0.25);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.lobby h1 {
|
|
287
|
+
font-size: clamp(2rem, 4.6vw, 2.85rem);
|
|
288
|
+
max-width: 24ch;
|
|
289
|
+
margin: 0 auto 0.7rem;
|
|
290
|
+
/* Break at the comma rather than orphaning the last word. */
|
|
291
|
+
text-wrap: balance;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.lobby-lede {
|
|
295
|
+
font-size: 1.11rem;
|
|
296
|
+
line-height: 1.62;
|
|
297
|
+
color: var(--text-dim);
|
|
298
|
+
max-width: 60ch;
|
|
299
|
+
margin: 0 auto 1.1rem;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.lobby-sub {
|
|
303
|
+
font-size: 0.95rem;
|
|
304
|
+
color: var(--text-faint);
|
|
305
|
+
max-width: 54ch;
|
|
306
|
+
margin: 0 auto 1.5rem;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/* Code is never centred, even when its surroundings are. */
|
|
310
|
+
.lobby .cmd { text-align: left; }
|
|
311
|
+
|
|
312
|
+
/* The prompt is meant to be copied, not read: keep it short enough that the
|
|
313
|
+
button under it stays in view, and say so with a fade at the cut. */
|
|
314
|
+
/* Wider right gutter than the shared block: the copy control is bigger here. */
|
|
315
|
+
.lobby .cmd-hero pre { max-height: 15rem; font-size: 0.8rem; padding-right: 5rem; }
|
|
316
|
+
|
|
317
|
+
.lobby .cmd-hero::after {
|
|
318
|
+
content: "";
|
|
319
|
+
position: absolute;
|
|
320
|
+
left: 0;
|
|
321
|
+
right: 0;
|
|
322
|
+
bottom: 0;
|
|
323
|
+
height: 2.6rem;
|
|
324
|
+
pointer-events: none;
|
|
325
|
+
background: linear-gradient(to bottom, rgba(11, 12, 16, 0), #0b0c10);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/* Copying is the call to action here, so the control is a button, not a hint. */
|
|
329
|
+
.lobby .cmd-hero .copy {
|
|
330
|
+
font-size: 0.78rem;
|
|
331
|
+
font-weight: 600;
|
|
332
|
+
padding: 0.38rem 0.85rem;
|
|
333
|
+
color: #17120b;
|
|
334
|
+
background: var(--accent);
|
|
335
|
+
border-color: var(--accent);
|
|
336
|
+
}
|
|
337
|
+
.lobby .cmd-hero .copy:hover { color: #17120b; background: #ff9630; border-color: #ff9630; }
|
|
338
|
+
.lobby .cmd-hero .copy[data-copied="1"] {
|
|
339
|
+
color: #0a1a10;
|
|
340
|
+
background: var(--pass);
|
|
341
|
+
border-color: var(--pass);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/* ---------- quick start (lobby) ----------
|
|
345
|
+
Two steps: set up once, then pick the work. Left-aligned inside the centred
|
|
346
|
+
lobby, because everything in here is read line by line. */
|
|
347
|
+
|
|
348
|
+
.qs { text-align: left; margin-top: 1.8rem; }
|
|
349
|
+
|
|
350
|
+
.qs-step + .qs-step { margin-top: 2.6rem; }
|
|
351
|
+
|
|
352
|
+
.qs-title {
|
|
353
|
+
display: flex;
|
|
354
|
+
align-items: center;
|
|
355
|
+
gap: 0.65rem;
|
|
356
|
+
font-size: 1.16rem;
|
|
357
|
+
font-weight: 640;
|
|
358
|
+
margin: 0 0 0.35rem;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
.qs-num {
|
|
362
|
+
flex: none;
|
|
363
|
+
width: 1.6rem;
|
|
364
|
+
height: 1.6rem;
|
|
365
|
+
display: grid;
|
|
366
|
+
place-items: center;
|
|
367
|
+
border-radius: 50%;
|
|
368
|
+
background: var(--accent-soft);
|
|
369
|
+
border: 1px solid var(--accent-line);
|
|
370
|
+
color: var(--accent);
|
|
371
|
+
font-family: var(--mono);
|
|
372
|
+
font-size: 0.8rem;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
.qs-why { color: var(--text-dim); font-size: 0.95rem; margin-bottom: 0.9rem; }
|
|
376
|
+
|
|
377
|
+
/* ---------- tabs ---------- */
|
|
378
|
+
|
|
379
|
+
.tablist {
|
|
380
|
+
display: flex;
|
|
381
|
+
flex-wrap: wrap;
|
|
382
|
+
gap: 0.4rem;
|
|
383
|
+
margin-bottom: 0.9rem;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
.tab {
|
|
387
|
+
font: 560 0.9rem var(--sans);
|
|
388
|
+
color: var(--text-dim);
|
|
389
|
+
background: var(--surface);
|
|
390
|
+
border: 1px solid var(--line);
|
|
391
|
+
border-radius: 99px;
|
|
392
|
+
padding: 0.4rem 1rem;
|
|
393
|
+
cursor: pointer;
|
|
394
|
+
transition: all 0.16s var(--ease);
|
|
395
|
+
}
|
|
396
|
+
.tab:hover { color: var(--text); border-color: var(--accent-line); }
|
|
397
|
+
.tab[aria-selected="true"] {
|
|
398
|
+
color: var(--accent);
|
|
399
|
+
background: var(--accent-soft);
|
|
400
|
+
border-color: var(--accent-line);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.qs-panel[hidden] { display: none; }
|
|
404
|
+
.qs-panel:focus-visible { outline-offset: 4px; }
|
|
405
|
+
|
|
406
|
+
.qs-panel-why { color: var(--text-dim); font-size: 0.92rem; margin-bottom: 0.5rem; }
|
|
407
|
+
|
|
408
|
+
/* These prompts are the call to action too, so their copy control never hides. */
|
|
409
|
+
.qs-panel .cmd .copy { opacity: 1; }
|
|
410
|
+
.qs-panel .cmd pre { white-space: pre-wrap; word-break: break-word; font-size: 0.82rem; }
|
|
411
|
+
|
|
412
|
+
.lobby-cta { display: flex; justify-content: center; gap: 0.7rem; flex-wrap: wrap; margin-top: 1.6rem; }
|
|
413
|
+
|
|
414
|
+
.lobby-fine {
|
|
415
|
+
margin: 2.4rem auto 0;
|
|
416
|
+
max-width: 56ch;
|
|
417
|
+
font-size: 0.86rem;
|
|
418
|
+
color: var(--text-faint);
|
|
419
|
+
}
|
|
420
|
+
|
|
251
421
|
/* ---------- cards ---------- */
|
|
252
422
|
|
|
253
423
|
.card {
|
|
@@ -639,6 +809,16 @@ tr[hidden], .filter-hidden { display: none !important; }
|
|
|
639
809
|
|
|
640
810
|
.stack { display: flex; flex-direction: column; gap: 0.55rem; margin: 1.8rem 0; }
|
|
641
811
|
|
|
812
|
+
/* Names which end of the diagram is the one you touch. */
|
|
813
|
+
.stack-caption {
|
|
814
|
+
font-family: var(--mono);
|
|
815
|
+
font-size: 0.78rem;
|
|
816
|
+
letter-spacing: 0.04em;
|
|
817
|
+
color: var(--text-faint);
|
|
818
|
+
margin: 1.6rem 0 0.55rem;
|
|
819
|
+
}
|
|
820
|
+
.stack-caption + .stack { margin-top: 0; }
|
|
821
|
+
|
|
642
822
|
.layer {
|
|
643
823
|
width: 100%;
|
|
644
824
|
text-align: left;
|