sign-off-axi 0.4.0 → 0.4.1
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/dist/cli.js +2 -1
- package/dist/contract.js +9 -9
- package/package.json +1 -1
- package/web/index.html +96 -62
- package/web/report.html +8 -7
- package/fixtures/contract/ask.v1/invalid-default_if_silent-missing.json +0 -1
- package/fixtures/contract/ask.v1/invalid-options-missing-letter.json +0 -1
- package/fixtures/contract/ask.v1/invalid-options-not-an-answer.json +0 -1
- package/fixtures/contract/ask.v1/invalid-options-outcome-missing.json +0 -1
- package/fixtures/contract/ask.v1/invalid-options-yes-label.json +0 -1
- package/fixtures/contract/ask.v1/invalid-options-yes-no-for-letters.json +0 -1
- package/fixtures/contract/ask.v1/invalid-recommend_reason-missing.json +0 -1
- package/fixtures/contract/ask.v1/invalid-silent_by-not-utc.json +0 -1
- package/fixtures/contract/ask.v1/invalid-title-id-word.json +0 -1
- package/fixtures/contract/ask.v1/invalid-title-not-a-question.json +0 -1
- package/fixtures/contract/ask.v1/invalid-title-robot-prefix.json +0 -1
- package/fixtures/contract/ask.v1/invalid-title-too-long.json +0 -1
- package/fixtures/contract/ask.v1/invalid-title-too-many-words.json +0 -1
- package/fixtures/contract/ask.v1/invalid-why-missing.json +0 -1
package/dist/cli.js
CHANGED
|
@@ -1129,7 +1129,8 @@ async function postKind(file, on, values, built) {
|
|
|
1129
1129
|
let sources = [];
|
|
1130
1130
|
try {
|
|
1131
1131
|
m ??= parseMessage(bytes);
|
|
1132
|
-
|
|
1132
|
+
const existingAsk = !built && kind === 'ask' && existsSync(file) && readItems(file).items.some((i) => i?.key === m.key);
|
|
1133
|
+
checkMessage(m, built ? 'report' : kind, !built && kind === 'ask' && !existingAsk);
|
|
1133
1134
|
// Media paths read from the message file's folder, or from the current folder for stdin.
|
|
1134
1135
|
if (m.kind === 'evidence')
|
|
1135
1136
|
sources = mediaFiles(m, from === '-' ? process.cwd() : dirname(resolve(from)));
|
package/dist/contract.js
CHANGED
|
@@ -160,7 +160,7 @@ export function checkMessage(m, want, flags = false) {
|
|
|
160
160
|
throw bad(`${at}.outcome`, 'is required', `Add ${flags ? `--outcome ${o.value}=<text>` : `"outcome" to option ${o.value}`}: ${OUTCOME}`);
|
|
161
161
|
text(`${at}.outcome`, o.outcome, 120, OUTCOME);
|
|
162
162
|
}
|
|
163
|
-
else if (outcome)
|
|
163
|
+
else if (outcome === 'plan' || (outcome === 'legacy-ask' && Object.hasOwn(o, 'outcome')))
|
|
164
164
|
text(`${at}.outcome`, o.outcome, 200, 'what picking it leads to, in 200 characters or fewer');
|
|
165
165
|
});
|
|
166
166
|
/** The option the poster would pick, `field` on `o`: required, and one of the option values, which it returns. */
|
|
@@ -298,9 +298,9 @@ export function checkMessage(m, want, flags = false) {
|
|
|
298
298
|
text('detail', m.detail, 1000, 'the longer note, or leave it out');
|
|
299
299
|
// An ask is read on a phone with no other context: it says why it is asked, what each answer does, why one is recommended and what silence does.
|
|
300
300
|
const ask = m.kind === 'ask', WHY = 'what it is about and why it needs an answer now, in 1 or 2 short lines (200 characters or fewer)';
|
|
301
|
-
if (ask && !Object.hasOwn(m, 'why'))
|
|
301
|
+
if (ask && flags && !Object.hasOwn(m, 'why'))
|
|
302
302
|
throw bad('why', 'is required', `Add ${named('why', flags)}: ${WHY}`);
|
|
303
|
-
if (ask)
|
|
303
|
+
if (ask && flags)
|
|
304
304
|
text('why', m.why, 200, WHY);
|
|
305
305
|
// A gate offers 2 or 3 of done-done's actions and a finding both of apply and skip, in any order, each with its own label.
|
|
306
306
|
const options = m.options, actions = ACTIONS[m.kind], max = actions ? Object.keys(actions).length : 6;
|
|
@@ -308,24 +308,24 @@ export function checkMessage(m, want, flags = false) {
|
|
|
308
308
|
const listed = !actions ? (flags ? 'Give 2 to 6 --option flags, such as --option yes="Yes" --option no="No"' : 'Set "options" to 2-6 objects such as {"value":"yes","label":"Yes"}') : max === 3 ? `Set "options" to 2 or 3 of ${actionList}` : `Set "options" to ${actionList}`;
|
|
309
309
|
if (!Array.isArray(options) || options.length < 2 || options.length > max)
|
|
310
310
|
throw bad('options', Object.hasOwn(m, 'options') ? `must list ${{ 2: '2', 3: '2 or 3' }[max] ?? '2 to 6'} options` : 'is required', listed);
|
|
311
|
-
checkOptions('options', options, ask ? 'ask' : false);
|
|
311
|
+
checkOptions('options', options, ask ? (flags ? 'ask' : 'legacy-ask') : false);
|
|
312
312
|
const odd = actions && options.find((o) => !Object.hasOwn(actions, o.value) || actions[o.value] !== o.label);
|
|
313
313
|
if (odd)
|
|
314
314
|
throw bad('options', `${show(odd)} is not a ${m.kind} action: ${Object.keys(actions).join(', ')}, each with its label`, listed);
|
|
315
315
|
const values = recommend('recommend_value', m, options);
|
|
316
316
|
const REASON = `why you recommend ${m.recommend_value}, in one line (160 characters or fewer)`;
|
|
317
|
-
if (ask && !Object.hasOwn(m, 'recommend_reason'))
|
|
317
|
+
if (ask && flags && !Object.hasOwn(m, 'recommend_reason'))
|
|
318
318
|
throw bad('recommend_reason', 'is required', `Add ${named('recommend_reason', flags)}: ${REASON}`);
|
|
319
|
-
if (ask)
|
|
319
|
+
if (ask && flags)
|
|
320
320
|
text('recommend_reason', m.recommend_reason, 160, REASON);
|
|
321
|
-
if (ask && !Object.hasOwn(m, 'default_if_silent'))
|
|
321
|
+
if (ask && flags && !Object.hasOwn(m, 'default_if_silent'))
|
|
322
322
|
throw bad('default_if_silent', 'is required', `Add ${named('default_if_silent', flags)}: the option that happens if the person stays quiet, one of ${values.join(', ')}`);
|
|
323
323
|
if (Object.hasOwn(m, 'default_if_silent') && !values.includes(m.default_if_silent)) {
|
|
324
324
|
throw bad('default_if_silent', `${show(m.default_if_silent)} is not an option value`, `Set ${named('default_if_silent', flags)} to one of ${values.join(', ')}${ask ? '' : ', or leave it out'}`);
|
|
325
325
|
}
|
|
326
326
|
// A real UTC time to the minute, seconds optional: Date.parse would roll 2026-02-30 over to March.
|
|
327
327
|
const utc = (v) => typeof v === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2})?Z$/.test(v) && !Number.isNaN(Date.parse(v)) && new Date(v).toISOString().startsWith(v.slice(0, -1));
|
|
328
|
-
if (Object.hasOwn(m, 'silent_by') && !utc(m.silent_by))
|
|
328
|
+
if (Object.hasOwn(m, 'silent_by') && (!ask || flags) && !utc(m.silent_by))
|
|
329
329
|
throw bad('silent_by', `${show(m.silent_by)} is not a UTC time such as 2026-10-17T18:00:00Z`, `Set ${named('silent_by', flags)} to when the silent default happens, as YYYY-MM-DDTHH:MM:SSZ in UTC, or leave it out`);
|
|
330
330
|
if (Object.hasOwn(m, 'close') && m.close !== 'done' && m.close !== 'release')
|
|
331
331
|
throw bad('close', 'must be done or release', 'Set "close" to "done" or "release", or leave it out');
|
|
@@ -342,7 +342,7 @@ export function checkMessage(m, want, flags = false) {
|
|
|
342
342
|
throw bad(`merges[${n}]`, `repeats ${k}`, 'List each key once');
|
|
343
343
|
});
|
|
344
344
|
}
|
|
345
|
-
if (ask)
|
|
345
|
+
if (ask && flags)
|
|
346
346
|
question(m, bad, flags);
|
|
347
347
|
// A gate's run is its own rule (section 5), so it comes after the field rules of section 4.
|
|
348
348
|
if (m.kind === 'gate') {
|
package/package.json
CHANGED
package/web/index.html
CHANGED
|
@@ -2,33 +2,46 @@
|
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
6
6
|
<title>Dashboard · sign-off</title>
|
|
7
7
|
<style>
|
|
8
|
+
/* Spacious, Apple grade. A phone reads white tiles on a grey grouped page; a desk reads grey tiles on a white page (the 60rem block swaps them).
|
|
9
|
+
Dark is its own scale: a black page with #161617 tiles. --accent is the blue for text, links and focus; --action is the blue a button is filled with. */
|
|
8
10
|
:root { color-scheme: light dark;
|
|
9
|
-
--
|
|
10
|
-
--border: #
|
|
11
|
-
--c-waiting: #
|
|
12
|
-
--
|
|
11
|
+
--page: #F5F5F7; --tile: #FFFFFF; --desk: #FFFFFF; --desk-tile: #F5F5F7; --bar: #FAFAFC; --sunk: #E8E8ED; --line: #D2D2D7;
|
|
12
|
+
--text: #1D1D1F; --muted: #6E6E73; --border: #8E8E93; --accent: #0066CC; --action: #0071E3; --on-accent: #FFFFFF; --danger: #C00011; --bezel: #1D1D1F;
|
|
13
|
+
--c-waiting: #0071E3; --c-evidence: #17702F; --c-reports: #A34700; --c-answered: #6E6E73; --c-gate: #8A6100; --ok: #17702F; --warn: #A34700;
|
|
14
|
+
--bg: var(--page); --surface: var(--tile); }
|
|
13
15
|
@media (prefers-color-scheme: dark) { :root {
|
|
14
|
-
--
|
|
15
|
-
--
|
|
16
|
-
--c-waiting: #
|
|
17
|
-
--shadow: 0 1px 2px rgb(0 0 0 / 0.4), 0 8px 24px rgb(0 0 0 / 0.25); } }
|
|
16
|
+
--page: #000000; --tile: #161617; --desk: #000000; --desk-tile: #161617; --bar: #161617; --sunk: #2C2C2E; --line: #424245;
|
|
17
|
+
--text: #F5F5F7; --muted: #86868B; --border: #636366; --accent: #2997FF; --danger: #FF6961; --bezel: #424245;
|
|
18
|
+
--c-waiting: #0A84FF; --c-evidence: #30D158; --c-reports: #FF9F0A; --c-answered: #98989D; --c-gate: #FFD60A; --ok: #30D158; --warn: #FF9F0A; } }
|
|
18
19
|
* { box-sizing: border-box; }
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
/* A phone keeps room at the bottom for the tab bar. */
|
|
21
|
+
body { margin: 0; padding-bottom: calc(5.125rem + env(safe-area-inset-bottom)); background: var(--bg); color: var(--text); overflow-wrap: anywhere;
|
|
22
|
+
-webkit-font-smoothing: antialiased; font: 1.0625rem/1.4706 -apple-system, BlinkMacSystemFont, "SF Pro Text", system-ui, "Segoe UI", Roboto, sans-serif; letter-spacing: -0.022em; }
|
|
21
23
|
/* The four areas each have a colour, used on icons, dots and marks. */
|
|
22
24
|
.i-waiting { --area: var(--c-waiting); } .i-evidence { --area: var(--c-evidence); } .i-reports { --area: var(--c-reports); } .i-answered { --area: var(--c-answered); }
|
|
23
|
-
main { max-width: 72rem; margin: 0 auto; padding: 0
|
|
25
|
+
main { max-width: 72rem; margin: 0 auto; padding: 0 max(1.25rem, env(safe-area-inset-right)) 0 max(1.25rem, env(safe-area-inset-left)); }
|
|
24
26
|
.side { display: none; }
|
|
27
|
+
/* A phone moves between the areas with a tab bar, solid so no page text shows through it. */
|
|
28
|
+
.tabs { position: fixed; z-index: 2; left: 0; right: 0; bottom: 0; display: flex; padding: 0 env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
|
|
29
|
+
background: var(--bar); border-top: 0.5px solid var(--line); }
|
|
30
|
+
.tabs a { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 0.125rem; min-height: 4.125rem; color: var(--muted);
|
|
31
|
+
font-size: 1rem; line-height: 1.125; letter-spacing: -0.01em; text-decoration: none; }
|
|
32
|
+
.tabs a[aria-current] { color: var(--accent); }
|
|
33
|
+
.tabs a:focus-visible { outline-offset: -2px; border-radius: 0.75rem; }
|
|
34
|
+
/* The areas' icons in the bar and the sidebar are bare outlines. */
|
|
35
|
+
.tabs .icon, .side .icon { width: 1.5rem; height: 1.5rem; background: none; }
|
|
36
|
+
.tabs .icon svg, .side .icon svg { width: 1.5rem; height: 1.5rem; stroke: currentColor; stroke-width: 1.75; }
|
|
37
|
+
.side .icon { color: var(--accent); }
|
|
25
38
|
.banner { margin: 1rem 0 0; padding: 0.75rem 1rem; border: 1px solid var(--danger); border-radius: 0.5rem; color: var(--danger); }
|
|
26
|
-
.top { position: relative; padding:
|
|
39
|
+
.top { position: relative; padding: 1.5rem 0 1.25rem; }
|
|
27
40
|
/* The person who answers: a round mark beside the Summary title. The board has no name to show, so it is a plain figure. */
|
|
28
|
-
.top > .i-you { position: absolute; top: 2.4rem; right: 0; width: 2.5rem; height: 2.5rem; border-radius: 50%; --area: var(--
|
|
41
|
+
.top > .i-you { position: absolute; top: 2.4rem; right: 0; width: 2.5rem; height: 2.5rem; border-radius: 50%; --area: var(--action); }
|
|
29
42
|
.top > .i-you svg { width: 1.375rem; height: 1.375rem; }
|
|
30
43
|
.date { margin: 0; color: var(--muted); font-size: 0.9375rem; font-weight: 600; }
|
|
31
|
-
.top h1 { margin: 0; font-size: 2.125rem; line-height: 1.
|
|
44
|
+
.top h1 { margin: 0; font-size: 2.125rem; line-height: 1.2059; font-weight: 700; letter-spacing: 0.011em; outline: none; }
|
|
32
45
|
.lede { margin: 0.375rem 0 0; color: var(--muted); }
|
|
33
46
|
.back, .sec-head a, .more { display: inline-flex; align-items: center; min-height: 2.75rem; color: var(--accent); font-weight: 600; text-decoration: none; }
|
|
34
47
|
h2 { font-size: 1.375rem; line-height: 1.25; margin: 0 0 0.75rem; }
|
|
@@ -36,14 +49,14 @@
|
|
|
36
49
|
.icon svg { width: 1.125rem; height: 1.125rem; fill: none; stroke: var(--surface); stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
|
|
37
50
|
/* Summary: a tile per area (each opens its screen), what needs you first, the day so far, then a short list per area. */
|
|
38
51
|
.tiles { display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; }
|
|
39
|
-
.card, .tile { background: var(--surface); border
|
|
52
|
+
.card, .tile { background: var(--surface); border-radius: 1rem; }
|
|
40
53
|
.card { padding: 1rem; }
|
|
41
54
|
.tile { position: relative; display: grid; grid-template-columns: auto auto minmax(0, 1fr); align-content: start; gap: 0.25rem 0.5rem; padding: 0.625rem 0.875rem; color: var(--text); text-decoration: none; }
|
|
42
55
|
.tile::after { content: "›" / ""; position: absolute; top: 0.5rem; right: 0.875rem; color: var(--muted); font-size: 1.5rem; line-height: 1.2; }
|
|
43
56
|
.tile .name { grid-column: 1 / -1; font-size: 0.875rem; font-weight: 600; color: var(--muted); line-height: 1.3; }
|
|
44
57
|
.tile .detail { grid-column: 1 / -1; }
|
|
45
58
|
.tile .n { grid-column: 1 / 3; font-size: 2.125rem; font-weight: 700; line-height: 1.15; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
46
|
-
.tile .detail { font-size: 0.
|
|
59
|
+
.tile .detail { font-size: 0.875rem; color: var(--muted); display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; overflow: hidden; }
|
|
47
60
|
/* The marks give way to the count: one line at the right, and a mark with no room left wraps out of sight. */
|
|
48
61
|
.vis { align-self: center; display: flex; flex-wrap: wrap; justify-content: flex-end; align-items: center; gap: 0.25rem; height: 2.25rem; overflow: hidden; }
|
|
49
62
|
.vis::before { content: ""; height: 100%; }
|
|
@@ -71,6 +84,7 @@
|
|
|
71
84
|
.focus .since { margin: -0.375rem 0 1rem; font-weight: 600; } .needs > .since { margin: -0.25rem 0 0.5rem; font-weight: 600; }
|
|
72
85
|
.focus .row h3 { font-size: 1.75rem; line-height: 1.2; letter-spacing: -0.01em; }
|
|
73
86
|
.focus .choices button, .needs .choices .rec { min-height: 4rem; border-radius: 1rem; font-size: 1.125rem; font-weight: 600; }
|
|
87
|
+
@media (min-width: 60rem) { .focus .choices button, .needs .choices .rec { min-height: 2.75rem; } }
|
|
74
88
|
/* On Summary the recommended answer is the large button, and the others are pills under it. */
|
|
75
89
|
.needs .choices button { font-weight: 600; } .needs .choices button:not(.rec) { justify-self: start; border-radius: 999px; }
|
|
76
90
|
.needs .choices button[aria-pressed="true"] { padding-right: 2.75rem; }
|
|
@@ -86,10 +100,10 @@
|
|
|
86
100
|
.report { display: grid; gap: 0.75rem; scroll-margin-top: 4.5rem; }
|
|
87
101
|
.report .about, .report .decided { margin: 0; }
|
|
88
102
|
.st { display: inline-flex; flex: none; align-items: center; gap: 0.375rem; padding: 0.125rem 0.625rem; border-radius: 999px; background: var(--sunk);
|
|
89
|
-
font-size: 0.
|
|
103
|
+
font-size: 0.875rem; font-weight: 650; white-space: nowrap; }
|
|
90
104
|
.st::before { content: ""; width: 0.4375rem; height: 0.4375rem; border-radius: 50%; background: currentColor; }
|
|
91
105
|
.st.ok { color: var(--ok); } .st.new { color: var(--c-reports); } .st.bad { color: var(--danger); } .st.warn { color: var(--warn); }
|
|
92
|
-
.decided .lbl { display: block; color: var(--muted); font-size: 0.
|
|
106
|
+
.decided .lbl { display: block; color: var(--muted); font-size: 0.875rem; font-weight: 650; }
|
|
93
107
|
.sec-h { display: flex; align-items: baseline; justify-content: space-between; gap: 0.75rem; margin: 1.5rem 0 0.75rem; }
|
|
94
108
|
.sec-h h2, .sec-h .meta { margin: 0; }
|
|
95
109
|
.legend { display: flex; flex-wrap: wrap; gap: 0.25rem 1rem; margin: 0 0 0.75rem; padding: 0.625rem 1rem; color: var(--muted); font-size: 0.875rem; }
|
|
@@ -110,7 +124,7 @@
|
|
|
110
124
|
.flow li { position: relative; display: grid; justify-items: center; align-content: start; gap: 0.375rem; }
|
|
111
125
|
.flow li + li::before { content: ""; position: absolute; top: 0.8125rem; right: 50%; width: 100%; border-top: 2px solid var(--line); }
|
|
112
126
|
.num { position: relative; z-index: 1; display: grid; place-items: center; width: 1.75rem; height: 1.75rem; border: 2px solid var(--muted); border-radius: 50%;
|
|
113
|
-
background: var(--surface); font-size: 0.
|
|
127
|
+
background: var(--surface); font-size: 0.875rem; font-weight: 700; }
|
|
114
128
|
.steps { gap: 0.75rem; }
|
|
115
129
|
.steps .mk { width: 1.75rem; height: 1.75rem; border-radius: 0.5rem; }
|
|
116
130
|
.steps .mk::before { width: 0.75rem; height: 0.75rem; }
|
|
@@ -143,15 +157,17 @@
|
|
|
143
157
|
/* Tall enough for the tallest column, six dots under its count, so no count reaches the caption above. */
|
|
144
158
|
.day { position: relative; height: 6.5rem; border-bottom: 1px solid var(--border); }
|
|
145
159
|
.col { position: absolute; bottom: 0.25rem; display: flex; flex-direction: column-reverse; align-items: center; gap: 0.1875rem; transform: translateX(-50%);
|
|
146
|
-
font-size: 0.
|
|
160
|
+
font-size: 0.875rem; font-weight: 700; line-height: 1.2; overflow-wrap: normal; }
|
|
147
161
|
.dot { width: 0.625rem; height: 0.625rem; border-radius: 50%; background: var(--c-answered); }
|
|
148
|
-
.hours { display: flex; justify-content: space-between; color: var(--muted); font-size: 0.
|
|
162
|
+
.hours { display: flex; justify-content: space-between; color: var(--muted); font-size: 0.875rem; font-variant-numeric: tabular-nums; }
|
|
149
163
|
.events { list-style: none; margin: 0.75rem 0 0; padding: 0; display: grid; gap: 0.25rem; font-size: 0.9375rem; }
|
|
150
164
|
.events time, .sec-head .meta { font-weight: 700; font-variant-numeric: tabular-nums; margin-right: 0.5rem; }
|
|
151
165
|
.sec-head { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; }
|
|
152
166
|
.sec-head h2 { margin: 0; overflow-wrap: normal; } /* A long count beside a section name wraps itself, never the name mid-word. */ .sec-head:has(+ .in-evidence) { margin: 1rem 0 0.375rem; }
|
|
153
|
-
|
|
154
|
-
.rows
|
|
167
|
+
/* A grouped list: hairlines between rows, inset from the left as on iOS. */
|
|
168
|
+
.rows { list-style: none; margin: 0; padding: 0 0 0 1rem; }
|
|
169
|
+
.rows > li { padding-right: 0.5rem; }
|
|
170
|
+
.rows > li + li { border-top: 0.5px solid var(--line); }
|
|
155
171
|
.compact { display: flex; align-items: center; gap: 0.75rem; padding: 0.75rem 0; color: var(--text); text-decoration: none; }
|
|
156
172
|
.compact .t { display: grid; flex: 1; min-width: 0; }
|
|
157
173
|
/* The answer sits under the title, never beside it: a long answer beside the title squeezed it to a letter a line. */
|
|
@@ -169,13 +185,13 @@
|
|
|
169
185
|
/* A read-only board is for reading: its pages get most of the screen. */
|
|
170
186
|
.readonly .rung iframe { height: 80vh; }
|
|
171
187
|
.row { display: grid; grid-template-columns: minmax(0, 1fr); gap: 0.875rem; padding: 1rem; scroll-margin-top: 4.5rem;
|
|
172
|
-
background: var(--surface); border
|
|
173
|
-
/* Waiting cards carry the accent stripe
|
|
174
|
-
.in-waiting .row {
|
|
188
|
+
background: var(--surface); border-radius: 0.875rem; }
|
|
189
|
+
/* Waiting cards carry the accent stripe; answered ones sit flat and quiet. */
|
|
190
|
+
.in-waiting .row { border-left: 3px solid var(--accent); padding-left: calc(1rem - 3px); }
|
|
175
191
|
.in-answered .row { background: transparent; box-shadow: none; }
|
|
176
192
|
.in-answered .row h3 { color: var(--muted); font-weight: 500; }
|
|
177
193
|
/* A sign-in: one big Open for its link, off once the link expired. */
|
|
178
|
-
.open-link { display: grid; place-items: center; min-height: 3.5rem; border-radius:
|
|
194
|
+
.open-link { display: grid; place-items: center; min-height: 3.5rem; border-radius: 980px; background: var(--action); color: var(--on-accent);
|
|
179
195
|
font-size: 1.125rem; font-weight: 700; text-decoration: none; }
|
|
180
196
|
button.open-link:disabled { border: 1px solid var(--line); background: var(--sunk); color: var(--muted); cursor: not-allowed; }
|
|
181
197
|
.signin .line { margin: 0.375rem 0 0; } .signin .expiry { margin: 0.5rem 0 0; font-weight: 700; } .signin .expiry.gone { color: var(--danger); }
|
|
@@ -185,15 +201,15 @@
|
|
|
185
201
|
.about { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
|
|
186
202
|
.row:has(details:not(.note, .other, .tech)[open]) .about { display: none; }
|
|
187
203
|
.about.full { display: block; }
|
|
188
|
-
summary { cursor: pointer; color: var(--accent); font-size: 0.875rem; margin-top: 0.25rem; width: fit-content; }
|
|
189
|
-
.meta { color: var(--muted); font-size: 0.
|
|
204
|
+
summary { cursor: pointer; color: var(--accent); font-size: 0.875rem; margin-top: 0.25rem; width: fit-content; min-height: 2.75rem; }
|
|
205
|
+
.meta { color: var(--muted); font-size: 0.875rem; margin: 0.5rem 0 0; }
|
|
190
206
|
/* Every card reads top down in one order: a short status line, the result as its headline, what changed, the proof, then the one decision.
|
|
191
207
|
The headline keeps the card's whole width: the dismiss x floats at the corner, never in a row that squeezes the title. */
|
|
192
208
|
.lead > .x { float: right; margin: -0.625rem -0.625rem -0.75rem 0.5rem; }
|
|
193
|
-
.eyebrow { display: flex; flex-wrap: wrap; align-items: center; gap: 0.25rem 0.625rem; margin: 0 0 0.375rem; color: var(--muted); font-size: 0.
|
|
209
|
+
.eyebrow { display: flex; flex-wrap: wrap; align-items: center; gap: 0.25rem 0.625rem; margin: 0 0 0.375rem; color: var(--muted); font-size: 0.875rem; font-weight: 600; }
|
|
194
210
|
.ask-line { grid-column: 1 / -1; margin: 0 0 -0.375rem; font-weight: 650; }
|
|
195
211
|
.why { grid-column: 1 / -1; margin: 0; color: var(--warn); font-size: 0.875rem; font-weight: 600; }
|
|
196
|
-
.tech { grid-column: 1 / -1; font-size: 0.
|
|
212
|
+
.tech { grid-column: 1 / -1; font-size: 0.875rem; } .tech summary { margin: 0; color: var(--muted); font-size: inherit; } .tech p { margin: 0.25rem 0 0; }
|
|
197
213
|
/* Each capture keeps its own shape: the frame is the shot, bounded in height, never letterboxed; two or more share the row. */
|
|
198
214
|
.media { display: flex; flex-wrap: wrap; align-items: flex-start; gap: .75rem; }
|
|
199
215
|
.media a { display: block; max-width: 100%; color: var(--accent); cursor: zoom-in; }
|
|
@@ -203,7 +219,7 @@
|
|
|
203
219
|
.proof { grid-column: 1 / -1; } .proof:has(> :nth-child(2)) { display: grid; grid-template-columns: repeat(auto-fill, minmax(8rem, 1fr)); max-width: 26rem; }
|
|
204
220
|
/* An explainer's chapters: one tap jumps the video to that part and plays it. */
|
|
205
221
|
.chapters { grid-column: 1 / -1; display: flex; flex-wrap: wrap; gap: 0.375rem; margin: -0.25rem 0 0; padding: 0; list-style: none; }
|
|
206
|
-
.chapters button { min-height: 2.
|
|
222
|
+
.chapters button { min-height: 2.75rem; padding: 0 0.75rem; border-radius: 999px; border-color: var(--line); font-size: 0.875rem; }
|
|
207
223
|
.chapters .at { margin-right: 0.375rem; color: var(--muted); font-variant-numeric: tabular-nums; }
|
|
208
224
|
.meta a { color: var(--accent); }
|
|
209
225
|
.err:empty { display: none; }
|
|
@@ -211,7 +227,7 @@
|
|
|
211
227
|
.choices { display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; }
|
|
212
228
|
/* The quick answers come first and are big enough for a thumb; typing is last, behind Other answer. */
|
|
213
229
|
/* Every card's answers read from the left, with the Recommended label above the answer and a picked answer's check on the right. */
|
|
214
|
-
.choices button { position: relative; display: grid; justify-items: start; align-content: center; min-height:
|
|
230
|
+
.choices button { position: relative; display: grid; justify-items: start; align-content: center; min-height: 3.125rem; padding: 0.625rem 1.25rem; text-align: left; }
|
|
215
231
|
.choices .tag { order: -1; margin: 0 0 0.125rem; font-size: 0.875rem; letter-spacing: 0; text-transform: none; }
|
|
216
232
|
.choices button[aria-pressed="true"]::before { position: absolute; right: 1rem; top: 50%; translate: 0 -50%; margin: 0; }
|
|
217
233
|
.row > .note, .row > .err { grid-column: 1 / -1; }
|
|
@@ -223,23 +239,24 @@
|
|
|
223
239
|
.other[open] > summary::after { margin-top: 0.25rem; transform: rotate(-135deg); }
|
|
224
240
|
.actions .quick { border-radius: 999px; }
|
|
225
241
|
/* After a tap: what was saved, and an Undo for a few seconds. */
|
|
226
|
-
#toast { position: fixed; z-index: 3; left: 1rem; right: 1rem; bottom:
|
|
227
|
-
display: flex; align-items: center; gap: 0.
|
|
228
|
-
background: var(--text); color: var(--bg);
|
|
242
|
+
#toast { position: fixed; z-index: 3; left: 1rem; right: 1rem; bottom: calc(5.125rem + env(safe-area-inset-bottom)); max-width: 30rem; margin: 0 auto;
|
|
243
|
+
display: flex; align-items: center; gap: 0.875rem; padding: 0.5rem 0.5rem 0.5rem 1.125rem; border-radius: 0.875rem;
|
|
244
|
+
background: var(--text); color: var(--bg); font-size: 1rem; }
|
|
229
245
|
#toast[hidden] { display: none; }
|
|
230
246
|
#toast span { flex: 1; min-width: 0; }
|
|
231
|
-
#toast button { flex: none;
|
|
247
|
+
#toast button { flex: none; min-height: 3.125rem; padding: 0 0.875rem; background: color-mix(in srgb, currentColor 16%, transparent); color: inherit; font-weight: 600; }
|
|
232
248
|
#toast button:focus-visible { outline-color: var(--bg); }
|
|
233
|
-
|
|
234
|
-
|
|
249
|
+
/* Buttons are pills: grey for the plain ones, filled blue once picked, solid grey when off. 50px on a phone, 44px on a desk. */
|
|
250
|
+
button { min-height: 3.125rem; padding: 0 1.25rem; border: 0; border-radius: 980px; cursor: pointer;
|
|
251
|
+
background: var(--sunk); color: var(--text); font: inherit; font-weight: 500; }
|
|
235
252
|
/* Recommended is a dashed accent outline and a tag; picked is filled with a check. They never look alike. */
|
|
236
253
|
button.rec { border: 1.5px solid var(--accent); background: color-mix(in srgb, var(--accent) 10%, var(--surface)); }
|
|
237
254
|
.tag::before { content: "★ " / ""; }
|
|
238
|
-
.tag { display: inline-block; white-space: nowrap; margin-left: 0.375rem; color: var(--accent); font-size: 0.
|
|
239
|
-
button[aria-pressed="true"] { background: var(--
|
|
255
|
+
.tag { display: inline-block; white-space: nowrap; margin-left: 0.375rem; color: var(--accent); font-size: 0.875rem; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; }
|
|
256
|
+
button[aria-pressed="true"] { background: var(--action); color: var(--on-accent); font-weight: 600; }
|
|
240
257
|
button[aria-pressed="true"]::before { content: "✓" / ""; margin-right: 0.375rem; font-weight: 700; }
|
|
241
258
|
button[aria-pressed="true"] .tag { color: inherit; }
|
|
242
|
-
button:disabled {
|
|
259
|
+
button:disabled { cursor: progress; } button:disabled:not([aria-pressed="true"]) { background: var(--sunk); color: var(--muted); }
|
|
243
260
|
.choices.many { display: flex; flex-wrap: wrap; }
|
|
244
261
|
/* An ask that says what each answer does: one answer per line on a phone, its outcome and the reason for the recommended one under its label. */
|
|
245
262
|
.choices.told { grid-template-columns: minmax(0, 1fr); }
|
|
@@ -250,10 +267,10 @@
|
|
|
250
267
|
.ask-why, .silent { margin: 0.375rem 0 0; max-width: 70ch; font-size: 1rem; }
|
|
251
268
|
.silent { color: var(--muted); }
|
|
252
269
|
.choices.many button { flex: 1 1 auto; max-width: 100%; padding: 0.5rem 1rem; }
|
|
253
|
-
.note label { display: block; color: var(--muted); font-size: 0.
|
|
270
|
+
.note label { display: block; color: var(--muted); font-size: 0.875rem; margin-top: 0.5rem; }
|
|
254
271
|
.tradeoff { margin: 0; max-width: 70ch; }
|
|
255
272
|
/* Dismiss: a quiet round x at the corner of a card or the end of a row. */
|
|
256
|
-
.x { flex: none; display: grid; place-items: center; width: 2.75rem; padding: 0; margin: -0.5rem 0 -0.5rem auto; border: 0; border-radius: 50%;
|
|
273
|
+
.x { flex: none; display: grid; place-items: center; width: 2.75rem; min-height: 2.75rem; padding: 0; margin: -0.5rem 0 -0.5rem auto; border: 0; border-radius: 50%;
|
|
257
274
|
background: none; color: var(--muted); font-size: 1.5rem; line-height: 1.25; }
|
|
258
275
|
.x:hover { background: var(--sunk); color: var(--text); }
|
|
259
276
|
.rows > li { display: flex; align-items: center; }
|
|
@@ -277,11 +294,11 @@
|
|
|
277
294
|
/* Waiting on you: one row of type chips, and the sort beside them (or below them on a phone). */
|
|
278
295
|
.filters { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 0.5rem 0.75rem; margin: 0 0 0.75rem; }
|
|
279
296
|
.chips { display: flex; flex-wrap: wrap; gap: 0.375rem; }
|
|
280
|
-
.chips button { min-height: 2.
|
|
297
|
+
.chips button { min-height: 2.75rem; padding: 0 0.75rem; border-radius: 999px; font-size: 0.875rem; font-weight: 600; }
|
|
281
298
|
.sort { display: flex; align-items: center; gap: 0.5rem; color: var(--muted); font-size: 0.875rem; font-weight: 600; }
|
|
282
|
-
.sort select { min-height: 2.
|
|
299
|
+
.sort select { min-height: 2.75rem; padding: 0 0.5rem; border: 1px solid var(--border); border-radius: 0.5rem; background: var(--surface); color: var(--text); font: inherit; }
|
|
283
300
|
.chip { padding: 0 0.5rem; border: 1px solid currentColor; border-radius: 999px; color: var(--accent);
|
|
284
|
-
font-size: 0.
|
|
301
|
+
font-size: 0.875rem; font-weight: 600; line-height: 1.25rem; letter-spacing: 0.04em; text-transform: uppercase; }
|
|
285
302
|
/* Each option is a tall phone frame, side by side; on a phone the row swipes, one frame at a time, and the next one peeks in.
|
|
286
303
|
Every option shares the label, frame and button rows, so a label that wraps never pushes one frame below the rest.
|
|
287
304
|
The row keeps 0.25rem of room on each side for focus rings, given back by the margin. */
|
|
@@ -314,7 +331,7 @@
|
|
|
314
331
|
.rung iframe { display: block; width: 100%; height: min(18rem, 50vh); border: 0; }
|
|
315
332
|
.rung .gone { color: var(--danger); margin: 0; }
|
|
316
333
|
.rung > .meta { color: var(--accent); margin: 0; }
|
|
317
|
-
.badge { padding: 0.1rem 0.5rem; border-radius: 999px; font-size: 0.
|
|
334
|
+
.badge { padding: 0.1rem 0.5rem; border-radius: 999px; font-size: 0.875rem; font-weight: 600; color: var(--accent); border: 1.5px solid var(--accent);
|
|
318
335
|
background: color-mix(in srgb, var(--accent) 10%, var(--surface)); }
|
|
319
336
|
.round { margin-top: 0.5rem; }
|
|
320
337
|
.actions { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 0.5rem; }
|
|
@@ -328,6 +345,7 @@
|
|
|
328
345
|
@media (min-width: 45rem) {
|
|
329
346
|
main { padding: 0 2rem; }
|
|
330
347
|
.row { grid-template-columns: minmax(0, 1fr) auto; align-items: start; gap: 0.875rem 2rem; padding: 1.125rem 1.25rem; }
|
|
348
|
+
.in-waiting .row { padding-left: calc(1.25rem - 3px); }
|
|
331
349
|
.row.design, .row.update { grid-template-columns: minmax(0, 1fr); gap: 0.875rem; }
|
|
332
350
|
/* The frames share the card width down to 11rem each; only a list longer than that scrolls. */
|
|
333
351
|
.mocks { grid-auto-columns: minmax(11rem, calc(var(--tall) * 9 / 19.5)); }
|
|
@@ -344,13 +362,21 @@
|
|
|
344
362
|
}
|
|
345
363
|
/* A wide screen keeps the areas in a sidebar; the Summary puts its tiles in one row and its lists in two columns. */
|
|
346
364
|
@media (min-width: 60rem) {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
.side
|
|
351
|
-
|
|
352
|
-
.side
|
|
353
|
-
.
|
|
365
|
+
:root { --bg: var(--desk); --surface: var(--desk-tile); }
|
|
366
|
+
body { display: grid; grid-template-columns: 16.25rem minmax(0, 1fr); padding-bottom: 3rem; }
|
|
367
|
+
.tabs { display: none; }
|
|
368
|
+
.side { display: flex; flex-direction: column; gap: 0.125rem; position: sticky; top: 0; height: 100vh; padding: 1.25rem 0.75rem;
|
|
369
|
+
border-right: 1px solid var(--line); background: var(--surface); }
|
|
370
|
+
.side a { display: flex; align-items: center; gap: 0.75rem; min-height: 2.75rem; padding: 0 0.75rem; border-radius: 0.625rem; color: var(--text); text-decoration: none; }
|
|
371
|
+
.side a:hover { background: color-mix(in srgb, var(--sunk) 50%, transparent); }
|
|
372
|
+
.side a[aria-current] { background: var(--sunk); }
|
|
373
|
+
.side .n { margin-left: auto; color: var(--muted); font-size: 1rem; font-variant-numeric: tabular-nums; }
|
|
374
|
+
.brand { margin: 0.25rem 0.75rem 1.25rem; font-weight: 600; }
|
|
375
|
+
.top { padding: 3.5rem 0 1.5rem; }
|
|
376
|
+
.top h1 { font-size: 3rem; line-height: 1.0833; letter-spacing: -0.003em; }
|
|
377
|
+
button { min-height: 2.75rem; }
|
|
378
|
+
#toast button { min-height: 2.75rem; }
|
|
379
|
+
#toast { bottom: 1.75rem; }
|
|
354
380
|
main { width: 100%; padding: 0 3rem; }
|
|
355
381
|
.back { display: none; }
|
|
356
382
|
.tiles { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
|
@@ -359,13 +385,13 @@
|
|
|
359
385
|
.tri { display: grid; grid-template-columns: minmax(16rem, 2fr) minmax(0, 3fr); gap: 2rem; align-items: start; }
|
|
360
386
|
.queue { display: grid; gap: 0.25rem; margin: 0; padding: 0; list-style: none; }
|
|
361
387
|
.queue button { display: grid; width: 100%; padding: 0.75rem 1rem; border-color: transparent; background: none; text-align: left; }
|
|
362
|
-
.queue button[aria-current] { border-color: var(--line); background: var(--surface);
|
|
388
|
+
.queue button[aria-current] { border-color: var(--line); background: var(--surface); }
|
|
363
389
|
.queue .meta { margin: 0.125rem 0 0; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
|
364
390
|
.hint, .upnext { display: none; }
|
|
365
391
|
.reports { grid-template-columns: 1fr 1fr; align-items: start; }
|
|
366
392
|
/* Answered sets each row as its own card, two across; an opened row takes the whole width. */
|
|
367
393
|
.rows.i-answered { display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; align-items: start; padding: 0; border: 0; background: none; box-shadow: none; }
|
|
368
|
-
.rows.i-answered > li { padding: 0 0.5rem 0 1rem; border: 1px solid var(--line); border-radius: 1rem; background: var(--surface);
|
|
394
|
+
.rows.i-answered > li { padding: 0 0.5rem 0 1rem; border: 1px solid var(--line); border-radius: 1rem; background: var(--surface); }
|
|
369
395
|
.rows.i-answered > li:has(.done[open]) { grid-column: 1 / -1; }
|
|
370
396
|
/* Evidence sets its updates two across; a page that is not an update keeps the whole width. */
|
|
371
397
|
.list.in-evidence { grid-template-columns: 1fr 1fr; align-items: start; } .in-evidence > .row:not(.update) { grid-column: 1 / -1; }
|
|
@@ -376,6 +402,7 @@
|
|
|
376
402
|
<body>
|
|
377
403
|
<nav class="side" id="nav" aria-label="Board areas"></nav>
|
|
378
404
|
<main><p class="banner" id="banner" role="alert" hidden></p><div id="board"></div></main>
|
|
405
|
+
<nav class="tabs" id="tabs" aria-label="Board areas"></nav>
|
|
379
406
|
<div id="reader" role="dialog" aria-modal="true" aria-labelledby="reader-title" hidden></div>
|
|
380
407
|
<div id="toast" role="status" hidden><span id="toast-text"></span><button id="undo" hidden>Undo</button></div>
|
|
381
408
|
<script>
|
|
@@ -834,9 +861,14 @@
|
|
|
834
861
|
}
|
|
835
862
|
|
|
836
863
|
const AREAS = { waiting: 'Waiting on you', evidence: 'Evidence', reports: 'Reports and explainers', answered: 'Answered' };
|
|
864
|
+
// The sidebar and the tab bar name the areas short enough for one line.
|
|
865
|
+
const NAV = { ...AREAS, reports: 'Reports' }, TAB = { ...NAV, waiting: 'Waiting' };
|
|
837
866
|
const EMPTY = { waiting: 'Nothing is waiting on you.', evidence: 'No evidence updates.', reports: 'No reports or explainers.', answered: 'Nothing answered yet.' };
|
|
838
|
-
const ICONS = { summary: '
|
|
839
|
-
|
|
867
|
+
const ICONS = { summary: 'M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z',
|
|
868
|
+
waiting: 'M22 12h-6l-2 3h-4l-2-3H2M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z',
|
|
869
|
+
evidence: 'M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1zM9 12l2 2 4-4',
|
|
870
|
+
reports: 'M12 7v14M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z',
|
|
871
|
+
answered: 'M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zM9 12l2 2 4-4',
|
|
840
872
|
you: 'M12 4a4 4 0 1 0 0 8 4 4 0 0 0 0-8zM4.5 20.5c1.2-3.6 4-5.5 7.5-5.5s6.3 1.9 7.5 5.5' };
|
|
841
873
|
// The drawings are fixed strings from this page, never text from the board file.
|
|
842
874
|
const icon = (a) => { const s = el('span', { class: `icon i-${a}`, 'aria-hidden': 'true' }); s.innerHTML = `<svg viewBox="0 0 24 24"><path d="${ICONS[a]}"/></svg>`; return s; };
|
|
@@ -986,7 +1018,7 @@
|
|
|
986
1018
|
const reader = document.getElementById('reader');
|
|
987
1019
|
function readPage(d, from = 'reports') {
|
|
988
1020
|
document.body.classList.toggle('reading', !!d);
|
|
989
|
-
for (const n of [document.querySelector('main'), document.getElementById('nav')]) n.inert = !!d;
|
|
1021
|
+
for (const n of [document.querySelector('main'), document.getElementById('nav'), document.getElementById('tabs')]) n.inert = !!d;
|
|
990
1022
|
reader.hidden = !d;
|
|
991
1023
|
const url = d ? src(mockUrl(d.key, '', d.view)) : '';
|
|
992
1024
|
if (reader.dataset.url === url) return d && (reader.querySelector('h1').textContent = d.title);
|
|
@@ -1099,7 +1131,9 @@
|
|
|
1099
1131
|
by.answered.sort((x, y) => state.answers[y.key].at.localeCompare(state.answers[x.key].at));
|
|
1100
1132
|
const [, at, key] = /^#(waiting|evidence|reports|answered)(?:\/([\w.-]+))?$/.exec(location.hash) ?? [];
|
|
1101
1133
|
const link = (a, name, n) => el('a', { href: `#${a}`, ...(a === (at ?? '') && { 'aria-current': 'page' }) }, icon(a || 'summary'), name, n === undefined ? '' : el('span', { class: 'n' }, String(n)));
|
|
1102
|
-
document.getElementById('nav').replaceChildren(el('p', { class: 'brand' }, 'sign-off'), link('', 'Summary'), ...Object.keys(AREAS).map((a) => link(a,
|
|
1134
|
+
document.getElementById('nav').replaceChildren(el('p', { class: 'brand' }, 'sign-off'), link('', 'Summary'), ...Object.keys(AREAS).map((a) => link(a, NAV[a], by[a].length)));
|
|
1135
|
+
document.getElementById('tabs').replaceChildren(...Object.keys(AREAS).map((a) => el('a', { href: `#${a}`, 'aria-label': `${AREAS[a]}, ${by[a].length}`,
|
|
1136
|
+
...(a === at && { 'aria-current': 'page' }) }, icon(a), TAB[a])));
|
|
1103
1137
|
const ro = state.readonly ? 'Read only' : FROZEN ? `Read-only copy, ${FROZEN.exported}` : '';
|
|
1104
1138
|
const top = el('header', { class: 'top' }, at ? el('a', { class: 'back', href: '#' }, '‹ Summary')
|
|
1105
1139
|
: el('p', { class: 'date' }, new Date().toLocaleDateString('en-GB', { weekday: 'long', day: 'numeric', month: 'long' })),
|
package/web/report.html
CHANGED
|
@@ -10,18 +10,19 @@
|
|
|
10
10
|
<style>
|
|
11
11
|
:root {
|
|
12
12
|
color-scheme: light dark;
|
|
13
|
-
|
|
14
|
-
--
|
|
13
|
+
/* The board's Spacious tokens: --accent is the blue for text and lines, --action the blue a button is filled with. */
|
|
14
|
+
--bg: #F5F5F7; --surface: #FFFFFF; --line: #D2D2D7; --text: #1D1D1F; --muted: #6E6E73;
|
|
15
|
+
--accent: #0066CC; --action: #0071E3; --accent-soft: #E6F1FC; --on-accent: #FFFFFF; --ok: #17702F;
|
|
15
16
|
}
|
|
16
17
|
@media (prefers-color-scheme: dark) {
|
|
17
|
-
:root { --bg: #
|
|
18
|
+
:root { --bg: #000000; --surface: #161617; --line: #424245; --text: #F5F5F7; --muted: #86868B; --accent: #2997FF; --accent-soft: #19304A; --ok: #30D158; }
|
|
18
19
|
}
|
|
19
20
|
* { box-sizing: border-box; }
|
|
20
|
-
body { margin: 0; background: var(--bg); color: var(--text); font: 17px/1.
|
|
21
|
+
body { margin: 0; background: var(--bg); color: var(--text); font: 17px/1.4706 -apple-system, BlinkMacSystemFont, "SF Pro Text", system-ui, "Segoe UI", Roboto, sans-serif; letter-spacing: -0.022em; }
|
|
21
22
|
main { max-width: 34rem; margin: 0 auto; padding: 1rem 1rem 3rem; display: grid; gap: 1rem; }
|
|
22
23
|
section { background: var(--surface); border: 1px solid var(--line); border-radius: 1rem; padding: 1.25rem 1rem; display: grid; gap: 0.75rem; }
|
|
23
24
|
h1 { font-size: 1.625rem; line-height: 1.2; margin: 0; letter-spacing: -0.01em; }
|
|
24
|
-
h2 { font-size: 0.
|
|
25
|
+
h2 { font-size: 0.875rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); margin: 0; }
|
|
25
26
|
p { margin: 0; }
|
|
26
27
|
.lead { font-size: 1.125rem; }
|
|
27
28
|
.asof { font-size: 0.9375rem; color: var(--muted); }
|
|
@@ -35,14 +36,14 @@
|
|
|
35
36
|
.track::before { content: ""; position: absolute; left: 1.5rem; right: 1.5rem; top: 50%; border-top: 2px solid var(--line); }
|
|
36
37
|
.track button { position: relative; width: 3rem; height: 3rem; border-radius: 50%; border: 2px solid var(--line); background: var(--surface); color: var(--muted); font: 700 1.0625rem system-ui, sans-serif; cursor: pointer; }
|
|
37
38
|
.track .done { border-color: var(--accent); color: var(--accent); }
|
|
38
|
-
.track .on { background: var(--
|
|
39
|
+
.track .on { background: var(--action); border-color: var(--action); color: var(--on-accent); }
|
|
39
40
|
.stage { min-height: 7.5rem; border-radius: 0.75rem; background: var(--accent-soft); padding: 1rem; display: grid; gap: 0.375rem; align-content: start; }
|
|
40
41
|
.stage .count { font-size: 0.875rem; color: var(--muted); }
|
|
41
42
|
.stage h3 { margin: 0; font-size: 1.1875rem; }
|
|
42
43
|
.nav { display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; }
|
|
43
44
|
.nav button { min-height: 3rem; border-radius: 0.75rem; font: 650 1.0625rem system-ui, sans-serif; cursor: pointer; border: 2px solid var(--accent); }
|
|
44
45
|
.back { background: transparent; color: var(--accent); }
|
|
45
|
-
.next { background: var(--
|
|
46
|
+
.next { background: var(--action); border-color: var(--action); color: var(--on-accent); }
|
|
46
47
|
button:disabled { opacity: 0.4; cursor: default; }
|
|
47
48
|
button:focus-visible, summary:focus-visible { outline: 3px solid var(--accent); outline-offset: 3px; }
|
|
48
49
|
summary { min-height: 3rem; display: flex; align-items: center; justify-content: space-between; font-weight: 650; cursor: pointer; list-style: none; }
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"contract":1,"kind":"ask","schema":1,"key":"ship-signin-2","title":"Ship the new sign-in screen this week?","why":"The screen is ready and the release goes out Friday.","options":[{"value":"yes","label":"Yes","outcome":"It ships to everyone on Friday."},{"value":"no","label":"No","outcome":"It waits for next week's release."}],"recommend_value":"yes","recommend_reason":"Every check passed on 3 phones."}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"contract":1,"kind":"ask","schema":1,"key":"release-day","title":"Which day: A) today, B) Friday or C) later?","why":"The screen is ready and the release goes out Friday.","options":[{"value":"a","label":"A: One column","outcome":"The form stays in one column."},{"value":"b","label":"B: Split","outcome":"The form sits beside a picture."}],"recommend_value":"a","recommend_reason":"Every check passed on 3 phones.","default_if_silent":"a"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"contract":1,"kind":"ask","schema":1,"key":"ship-signin-2","title":"Ship the new sign-in screen this week?","why":"The screen is ready and the release goes out Friday.","options":[{"value":"done","label":"Done","outcome":"The card closes."},{"value":"later","label":"Not now","outcome":"The card waits."}],"recommend_value":"done","recommend_reason":"Every check passed on 3 phones.","default_if_silent":"later"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"contract":1,"kind":"ask","schema":1,"key":"ship-signin-2","title":"Ship the new sign-in screen this week?","why":"The screen is ready and the release goes out Friday.","options":[{"value":"yes","label":"Yes","outcome":"It ships to everyone on Friday."},{"value":"no","label":"No"}],"recommend_value":"yes","recommend_reason":"Every check passed on 3 phones.","default_if_silent":"no"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"contract":1,"kind":"ask","schema":1,"key":"ship-signin-2","title":"Ship the new sign-in screen this week?","why":"The screen is ready and the release goes out Friday.","options":[{"value":"yes","label":"Sure","outcome":"It ships to everyone on Friday."},{"value":"no","label":"No","outcome":"It waits for next week's release."}],"recommend_value":"yes","recommend_reason":"Every check passed on 3 phones.","default_if_silent":"no"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"contract":1,"kind":"ask","schema":1,"key":"release-day","title":"Which day: A) today, B) Friday or C) later?","why":"The screen is ready and the release goes out Friday.","options":[{"value":"yes","label":"Yes","outcome":"It ships to everyone on Friday."},{"value":"no","label":"No","outcome":"It waits for next week's release."}],"recommend_value":"yes","recommend_reason":"Every check passed on 3 phones.","default_if_silent":"no"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"contract":1,"kind":"ask","schema":1,"key":"ship-signin-2","title":"Ship the new sign-in screen this week?","why":"The screen is ready and the release goes out Friday.","options":[{"value":"yes","label":"Yes","outcome":"It ships to everyone on Friday."},{"value":"no","label":"No","outcome":"It waits for next week's release."}],"recommend_value":"yes","default_if_silent":"no"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"contract":1,"kind":"ask","schema":1,"key":"ship-signin-2","title":"Ship the new sign-in screen this week?","why":"The screen is ready and the release goes out Friday.","options":[{"value":"yes","label":"Yes","outcome":"It ships to everyone on Friday."},{"value":"no","label":"No","outcome":"It waits for next week's release."}],"recommend_value":"yes","recommend_reason":"Every check passed on 3 phones.","default_if_silent":"no","silent_by":"2026-10-17 16:00"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"contract":1,"kind":"ask","schema":1,"key":"ship-signin-2","title":"Ship ship-signin-2 this week?","why":"The screen is ready and the release goes out Friday.","options":[{"value":"yes","label":"Yes","outcome":"It ships to everyone on Friday."},{"value":"no","label":"No","outcome":"It waits for next week's release."}],"recommend_value":"yes","recommend_reason":"Every check passed on 3 phones.","default_if_silent":"no"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"contract":1,"kind":"ask","schema":1,"key":"ship-signin-2","title":"Ship the new sign-in screen this week","why":"The screen is ready and the release goes out Friday.","options":[{"value":"yes","label":"Yes","outcome":"It ships to everyone on Friday."},{"value":"no","label":"No","outcome":"It waits for next week's release."}],"recommend_value":"yes","recommend_reason":"Every check passed on 3 phones.","default_if_silent":"no"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"contract":1,"kind":"ask","schema":1,"key":"ship-signin-2","title":"Say yes or no: ship the new sign-in screen?","why":"The screen is ready and the release goes out Friday.","options":[{"value":"yes","label":"Yes","outcome":"It ships to everyone on Friday."},{"value":"no","label":"No","outcome":"It waits for next week's release."}],"recommend_value":"yes","recommend_reason":"Every check passed on 3 phones.","default_if_silent":"no"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"contract":1,"kind":"ask","schema":1,"key":"long-title","title":"Ship it Ship it Ship it Ship it Ship it Ship it Ship it Ship it Ship it Ship it Ship it Ship it Ship it Ship it Ship it Ship it Ship it Ship it Ship it Ship it Ship it ","why":"The screen is ready and the release goes out Friday.","options":[{"value":"yes","label":"Yes","outcome":"It ships to everyone on Friday."},{"value":"no","label":"No","outcome":"It waits for next week's release."}],"recommend_value":"yes","recommend_reason":"Every check passed on 3 phones.","default_if_silent":"no"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"contract":1,"kind":"ask","schema":1,"key":"ship-signin-2","title":"Should we ship the new sign-in screen to every single user this week?","why":"The screen is ready and the release goes out Friday.","options":[{"value":"yes","label":"Yes","outcome":"It ships to everyone on Friday."},{"value":"no","label":"No","outcome":"It waits for next week's release."}],"recommend_value":"yes","recommend_reason":"Every check passed on 3 phones.","default_if_silent":"no"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"contract":1,"kind":"ask","schema":1,"key":"ship-signin-2","title":"Ship the new sign-in screen this week?","options":[{"value":"yes","label":"Yes","outcome":"It ships to everyone on Friday."},{"value":"no","label":"No","outcome":"It waits for next week's release."}],"recommend_value":"yes","recommend_reason":"Every check passed on 3 phones.","default_if_silent":"no"}
|