@bongos/core 1.19.1064 → 1.19.1065
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/.bongos-core.json +85 -40
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +6 -0
- package/clients/bongos-client/index.cjs +6 -0
- package/clients/bongos-client/index.d.ts +11 -1
- package/clients/bongos-client/index.mjs +6 -0
- package/docs/api/openapi.json +237 -4
- package/docs/api-reference.md +5 -2
- package/docs/copy-inventory.md +60 -41
- package/docs/copy-registry.json +228 -47
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-inventory.json +11 -2
- package/docs/page-readings.json +45 -22
- package/modules/copy-desk/page-decisions.js +71 -0
- package/modules/copy-desk/page-status.js +96 -0
- package/modules/copy-desk/pages.js +30 -0
- package/modules/copy-desk/routes/copy-desk.js +153 -0
- package/modules/copy-desk/tests/copy_no_cms.mjs +40 -4
- package/modules/hall-ui/public/approval-queue-lib.js +156 -0
- package/modules/hall-ui/public/approval-queue.css +286 -0
- package/modules/hall-ui/public/approval-queue.js +338 -0
- package/modules/hall-ui/public/studio.css +33 -0
- package/modules/hall-ui/public/studio.html +18 -1
- package/modules/hall-ui/public/studio.js +30 -0
- package/modules/hall-ui/public/studio.states.json +198 -0
- package/modules/hall-ui/records/approval-queue.md +32 -0
- package/modules/lifecycle/db-grade.js +80 -58
- package/modules/lifecycle/github-pr-close.js +41 -0
- package/modules/lifecycle/github-push.js +5 -2
- package/modules/lifecycle/lifecycle.js +12 -0
- package/modules/lifecycle/page-tweak-approve.js +242 -0
- package/modules/lifecycle/routes/gate-approvals.js +17 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/scripts/hall-preview/server.js +18 -0
- package/src/module-api.js +1 -1
- package/tests/copy_desk_page_approve.mjs +307 -0
- package/tests/fixtures/page-ledger.mjs +41 -1
- package/tests/gate_approvals.mjs +93 -0
- package/tests/hall_approval_queue.mjs +327 -0
- package/tests/hall_audit.mjs +4 -0
|
@@ -21,9 +21,9 @@
|
|
|
21
21
|
// which no renderer reads — and the wording reaches a person only after somebody
|
|
22
22
|
// claims that task and ships the diff. So the four things pinned here are:
|
|
23
23
|
//
|
|
24
|
-
// * the write surface is EXACTLY the verbs the module defines (
|
|
25
|
-
//
|
|
26
|
-
// TASK, never a row),
|
|
24
|
+
// * the write surface is EXACTLY the verbs the module defines (ten since
|
|
25
|
+
// TW12: the page draft, its Word upload, the submit and the Approval
|
|
26
|
+
// queue's two decisions write the round TASK, never a row),
|
|
27
27
|
// * no column on copy_desk_flags could hold replacement text,
|
|
28
28
|
// * nothing in this module writes a file, and
|
|
29
29
|
// * the ONLY thing that writes a surface file is the applier, it lives outside
|
|
@@ -66,7 +66,8 @@ const stripComments = (src) => src
|
|
|
66
66
|
// status, drift and tally) and page-data.js (the read side of the page
|
|
67
67
|
// inventory and readings). They are held to the same two rules as the rest.
|
|
68
68
|
// TW09 (task 1004320) added docx.js, the Word round trip over Buffers.
|
|
69
|
-
|
|
69
|
+
// TW12 (task 1004323) added page-decisions.js, the Approval queue's route helpers.
|
|
70
|
+
const MODULE_FILES = ['registry.js', 'flags.js', 'proposals.js', 'pages.js', 'page-status.js', 'page-data.js', 'docx.js', 'page-decisions.js', 'routes/copy-desk.js'];
|
|
70
71
|
|
|
71
72
|
console.log('copy-desk: NO LIVE CMS');
|
|
72
73
|
|
|
@@ -78,6 +79,12 @@ await test('the write surface is exactly the verbs the module defines', () => {
|
|
|
78
79
|
assert.deepEqual(found, [
|
|
79
80
|
'POST /copy-desk/flags',
|
|
80
81
|
'POST /copy-desk/flags/:id/close',
|
|
82
|
+
// TW12 (task 1004323, ADR 0341 D7, D11): the Approval queue's two
|
|
83
|
+
// decisions. Both move the page round TASK through the lifecycle port
|
|
84
|
+
// (approveHeldTweak, sendBackHeldTweak); approve lands the applied branch the
|
|
85
|
+
// ordinary way, and send-back appends a note block to the task. Neither
|
|
86
|
+
// writes a row here, a file, or anything a renderer reads.
|
|
87
|
+
'POST /copy-desk/pages/:pageId/approve',
|
|
81
88
|
// TW06 (task 1004317, ADR 0341 D5, D6, D11): the page claim opens or takes a
|
|
82
89
|
// TASK and a web claim through the lifecycle port; the page ask writes one
|
|
83
90
|
// flag row whose target is a page id. Neither stores wording.
|
|
@@ -87,6 +94,7 @@ await test('the write surface is exactly the verbs the module defines', () => {
|
|
|
87
94
|
// round's DRAFT through the autosave's own port call and resolver, never a
|
|
88
95
|
// row here, a file, or the site.
|
|
89
96
|
'POST /copy-desk/pages/:pageId/draft.docx',
|
|
97
|
+
'POST /copy-desk/pages/:pageId/send-back',
|
|
90
98
|
// TW08 (task 1004319, ADR 0341 D4, D11): the draft autosave and the
|
|
91
99
|
// submit. Both write the page round's TASK DESCRIPTION through the
|
|
92
100
|
// lifecycle port, the place ADR 0233 lets wording rest; neither writes a
|
|
@@ -249,6 +257,34 @@ await test('the Word round trip is pure over Buffers, and its upload writes only
|
|
|
249
257
|
'the upload writes the draft and nothing else: no row, and never the frozen batch');
|
|
250
258
|
});
|
|
251
259
|
|
|
260
|
+
await test('the Approval queue decides only through the port, and a send-back writes a note, never wording', () => {
|
|
261
|
+
// TW12 (task 1004323, ADR 0341 D7): approve and send-back move the round
|
|
262
|
+
// TASK through the lifecycle port. Send-back's one piece of text is the
|
|
263
|
+
// artist's note to the next applier, written as a page-tweak-sent-back block
|
|
264
|
+
// by pages.appendSentBack; it must never touch the draft or the frozen batch.
|
|
265
|
+
const src = stripComments(read('routes/copy-desk.js'));
|
|
266
|
+
const handler = (p) => {
|
|
267
|
+
const start = src.indexOf(`router.post('${p}'`);
|
|
268
|
+
assert.ok(start >= 0, `${p} is mounted`);
|
|
269
|
+
const next = src.indexOf('router.', start + 1);
|
|
270
|
+
return src.slice(start, next === -1 ? undefined : next);
|
|
271
|
+
};
|
|
272
|
+
const approve = handler('/copy-desk/pages/:pageId/approve');
|
|
273
|
+
const sendBack = handler('/copy-desk/pages/:pageId/send-back');
|
|
274
|
+
assert.ok(/lifecycle\.approveHeldTweak\(/.test(approve), 'approve moves the task through the lifecycle port');
|
|
275
|
+
assert.ok(/lifecycle\.sendBackHeldTweak\(/.test(sendBack), 'send-back moves the task through the lifecycle port');
|
|
276
|
+
for (const [name, h] of [['approve', approve], ['send-back', sendBack]]) {
|
|
277
|
+
assert.equal(/insertFlag|closeFlag|INSERT INTO|UPDATE |createTask|composeBatchBlock|composeDraftBlock|saveDraftInto|replaceBlock/i.test(h), false,
|
|
278
|
+
`the ${name} route writes no row here and no draft or batch`);
|
|
279
|
+
assert.equal(/req\.body\.(before|after|section|placement|file|line|string_id|text|lines)\b/.test(h), false,
|
|
280
|
+
`the ${name} route takes no wording or target from the request body`);
|
|
281
|
+
}
|
|
282
|
+
assert.ok(/pages\.appendSentBack\(task\.description,/.test(sendBack), 'the note is appended by the one sent-back writer');
|
|
283
|
+
assert.ok(/pages\.normalizeSentBackNote\(req\.body\.note\)/.test(sendBack), 'the note floor is checked before anything moves');
|
|
284
|
+
const schemas = src.slice(src.indexOf('const APPROVE_BODY_SCHEMA'), src.indexOf('const PAGE_REFUSAL_STATUS'));
|
|
285
|
+
assert.equal(/\b(before|after|section|placement|file|string_id|lines)\s*:/.test(schemas), false, 'the decision schemas name no wording or target field');
|
|
286
|
+
});
|
|
287
|
+
|
|
252
288
|
await test('the module manifest declares no seam another module could write copy through', () => {
|
|
253
289
|
const manifest = JSON.parse(read('module.json'));
|
|
254
290
|
assert.deepEqual(manifest.provides, [], 'a provided port here would be a copy write path for every other module');
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// modules/hall-ui/public/approval-queue-lib.js — the Approval queue's pure rules
|
|
2
|
+
// (task 1004323 / BV2.TW12, goal 1000095; ADR 0341 D7). window.OTBApprovalQueueLib,
|
|
3
|
+
// node-require-safe (the tweak-editor-lib.js pattern), so tests/hall_approval_queue.mjs
|
|
4
|
+
// can pin every word the panel says without a browser.
|
|
5
|
+
//
|
|
6
|
+
// The words are the master design's (docs/design/mocks/tweak-mode/Main.dc.html,
|
|
7
|
+
// its Approval queue panel): "approval queue · 1 of 2", "Home · landing · 2 lines
|
|
8
|
+
// rewritten, applied", "highlighted: the words you changed", "Approved. It ships
|
|
9
|
+
// now, and Home reads text tweaked 3x. Next in the queue: Contact." and "Sent
|
|
10
|
+
// back with your note. It comes back to the queue once it is re-applied."
|
|
11
|
+
|
|
12
|
+
(() => {
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
// The renders /tweak attaches (modules/lifecycle/task-visuals.js
|
|
16
|
+
// TWEAK_RENDER_SLOTS): <phase>-<device>-<mode>, eight in all.
|
|
17
|
+
const PHASES = Object.freeze(['before', 'after']);
|
|
18
|
+
const DEVICES = Object.freeze(['desktop', 'phone']);
|
|
19
|
+
const MODES = Object.freeze(['light', 'dark']);
|
|
20
|
+
const MIN_NOTE = 10; // pages.MIN_NOTE_CHARS, the send-back floor
|
|
21
|
+
const MAX_NOTE = 500;
|
|
22
|
+
|
|
23
|
+
function slotName(phase, device, mode) {
|
|
24
|
+
return `${phase}-${device}-${mode}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// The same server-minted shape hall-visual.js guards (task-visuals.js mints
|
|
28
|
+
// it); anything else is never put in a src.
|
|
29
|
+
const VISUAL_URL_RE = /^\/api\/(gds|bongos)\/task-visuals\/task-\d+-[0-9a-f]{16}\.(png|jpg|gif|webp)$/;
|
|
30
|
+
|
|
31
|
+
// slots: GET /tasks/:id/visuals's `slots` -> { '<slot>': { url, alt } }, only
|
|
32
|
+
// the eight render names and only server-minted URLs.
|
|
33
|
+
function renderIndex(slots) {
|
|
34
|
+
const known = new Set(PHASES.flatMap((p) => DEVICES.flatMap((d) => MODES.map((m) => slotName(p, d, m)))));
|
|
35
|
+
const out = {};
|
|
36
|
+
for (const s of slots || []) {
|
|
37
|
+
if (!s || !known.has(s.slot) || !VISUAL_URL_RE.test(String(s.visual_url || ''))) continue;
|
|
38
|
+
out[s.slot] = { url: s.visual_url, alt: s.visual_alt || '' };
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// The render for a view, or the nearest one there is: the other colour mode
|
|
44
|
+
// first (a page that pins one mode is shot once per mode anyway), then the
|
|
45
|
+
// other device. `exact` says whether it is the view asked for.
|
|
46
|
+
function pickRender(index, { phase, device, mode }) {
|
|
47
|
+
const tries = [
|
|
48
|
+
[device, mode],
|
|
49
|
+
[device, mode === 'light' ? 'dark' : 'light'],
|
|
50
|
+
[device === 'desktop' ? 'phone' : 'desktop', mode],
|
|
51
|
+
[device === 'desktop' ? 'phone' : 'desktop', mode === 'light' ? 'dark' : 'light'],
|
|
52
|
+
];
|
|
53
|
+
for (const [i, [d, m]] of tries.entries()) {
|
|
54
|
+
const hit = index && index[slotName(phase, d, m)];
|
|
55
|
+
if (hit) return { ...hit, slot: slotName(phase, d, m), device: d, mode: m, exact: i === 0 };
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function hasAnyRender(index) {
|
|
61
|
+
return !!index && Object.keys(index).length > 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// An apex page's inventory title is its branding template ({{productName}}:
|
|
65
|
+
// {{tagline}}); the editor's own rule names it (tweak-editor-lib displayTitle),
|
|
66
|
+
// restated so this panel loads without the editor's lib.
|
|
67
|
+
function displayTitle(item) {
|
|
68
|
+
const t = String((item && item.title) || (item && item.page_id) || '');
|
|
69
|
+
if (!/\{\{/.test(t)) return t;
|
|
70
|
+
if (item.path === '/') return 'Home';
|
|
71
|
+
const rest = t.replace(/^\{\{[^}]*\}\}\s*(?:—|-|:)\s*/, '').replace(/\{\{[^}]*\}\}/g, '').trim();
|
|
72
|
+
if (!rest) return 'Home';
|
|
73
|
+
return rest.charAt(0).toUpperCase() + rest.slice(1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
|
|
77
|
+
|
|
78
|
+
function eyebrow(item) {
|
|
79
|
+
return item ? `approval queue · ${item.position} of ${item.of}` : 'approval queue';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// "· landing · 2 lines rewritten, applied", and the refused lines named when
|
|
83
|
+
// there are any ("· 1 not applied").
|
|
84
|
+
function subLine(item) {
|
|
85
|
+
if (!item) return '';
|
|
86
|
+
const parts = [item.surface_word || item.surface || '', `${plural(item.applied_count, 'line', 'lines')} rewritten, applied`];
|
|
87
|
+
if (item.refused_count > 0) parts.push(`${item.refused_count} not applied`);
|
|
88
|
+
return `· ${parts.filter(Boolean).join(' · ')}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// What the studio's quick action says under "Approval queue".
|
|
92
|
+
function doorDetail(count) {
|
|
93
|
+
if (count == null) return 'the pages /tweak has applied';
|
|
94
|
+
if (count === 0) return 'nothing waiting for you';
|
|
95
|
+
return `${plural(count, 'page', 'pages')} applied, waiting for you`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Why a line was not applied, in words (copy-apply.js's refusals).
|
|
99
|
+
const REFUSED = Object.freeze({
|
|
100
|
+
target_gone: 'not applied: the page no longer shows it',
|
|
101
|
+
ambiguous_target: 'not applied: the page shows these words in more than one place',
|
|
102
|
+
no_exact_span: 'not applied: this text cannot be changed mechanically',
|
|
103
|
+
placeholder_mismatch: 'not applied: a {…} value was added or dropped',
|
|
104
|
+
path_outside_surface: 'not applied: it lives outside this page\'s files',
|
|
105
|
+
});
|
|
106
|
+
function refusedWords(code) {
|
|
107
|
+
return REFUSED[code] || 'not applied';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// One row of "the words you changed", for a flip. In AFTER the new words are
|
|
111
|
+
// the line and carry the mark; in BEFORE the old words are the line, unmarked,
|
|
112
|
+
// the mock's own two states.
|
|
113
|
+
function lineRow(line, phase) {
|
|
114
|
+
const after = phase !== 'before';
|
|
115
|
+
return {
|
|
116
|
+
key: line.key,
|
|
117
|
+
section: line.section || '',
|
|
118
|
+
text: after && !line.refused ? line.after : line.before,
|
|
119
|
+
marked: after && !line.refused,
|
|
120
|
+
aside: line.refused ? refusedWords(line.refused) : (after ? `was: ${line.before}` : `becomes: ${line.after}`),
|
|
121
|
+
refused: !!line.refused,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function noteProblem(note) {
|
|
126
|
+
const n = String(note == null ? '' : note).replace(/\s+/g, ' ').trim();
|
|
127
|
+
if (n.length < MIN_NOTE) return `Say what needs changing in at least ${MIN_NOTE} characters.`;
|
|
128
|
+
if (n.length > MAX_NOTE) return `Keep it to one line, at most ${MAX_NOTE} characters.`;
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// "Approved. It ships now, and Home reads text tweaked 3x. Next in the queue:
|
|
133
|
+
// Contact." The status is the server's (status_after); the next page is the
|
|
134
|
+
// server's re-read of the queue.
|
|
135
|
+
function approvedLine(item, resp) {
|
|
136
|
+
const name = displayTitle(item);
|
|
137
|
+
const status = resp && resp.status_after ? resp.status_after.charAt(0).toLowerCase() + resp.status_after.slice(1) : null;
|
|
138
|
+
const next = resp && resp.next && resp.next.page ? displayTitle(resp.next.page) : null;
|
|
139
|
+
return [
|
|
140
|
+
status ? `Approved. It ships now, and ${name} reads ${status}.` : 'Approved. It ships now.',
|
|
141
|
+
next ? `Next in the queue: ${next}.` : 'Nothing else is waiting.',
|
|
142
|
+
].join(' ');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function sentBackLine() {
|
|
146
|
+
return 'Sent back with your note. It comes back to the queue once it is re-applied.';
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const api = {
|
|
150
|
+
PHASES, DEVICES, MODES, MIN_NOTE, MAX_NOTE,
|
|
151
|
+
slotName, renderIndex, pickRender, hasAnyRender, displayTitle,
|
|
152
|
+
eyebrow, subLine, doorDetail, refusedWords, lineRow, noteProblem, approvedLine, sentBackLine,
|
|
153
|
+
};
|
|
154
|
+
if (typeof window !== 'undefined') window.OTBApprovalQueueLib = api;
|
|
155
|
+
if (typeof module !== 'undefined' && module.exports) module.exports = api;
|
|
156
|
+
})();
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/* modules/hall-ui/public/approval-queue.css — the Approval queue panel
|
|
2
|
+
(task 1004323 / BV2.TW12, goal 1000095; ADR 0341 D7).
|
|
3
|
+
|
|
4
|
+
THE PIXEL CONTRACT. docs/design/mocks/tweak-mode/Main.dc.html's wide glass
|
|
5
|
+
dialog is the master design the owner approved, and this sheet is its inline
|
|
6
|
+
styles, one rule per element: the scrim, the 1180 by 820 glass, the header
|
|
7
|
+
row (the mono eyebrow, the page name, the before/after pills, close), the
|
|
8
|
+
rendered page on its light frame, the accent "approve and ship", the ghost
|
|
9
|
+
"send it back" and its one-line note, the hint, and the two outcome lines.
|
|
10
|
+
The values are the artboard's own, so the sheet is its own token tier (the
|
|
11
|
+
tweak editor's precedent, tweak-editor.css; tests/hall_audit.mjs
|
|
12
|
+
LITERAL_EXEMPT names it). What a gate or a route forced to differ is in the
|
|
13
|
+
task's ship notes and modules/hall-ui/records/approval-queue.md.
|
|
14
|
+
|
|
15
|
+
ONE PANEL, BOTH MODES. The dialog is glass over the night studio whatever
|
|
16
|
+
the hall's theme, so its palette sits on the panel's own root (.aq-overlay),
|
|
17
|
+
never :root or body. The one thing that follows the theme is the scrim: the
|
|
18
|
+
artboard's .42 over the dark room, and .94 over the light hall, where .42
|
|
19
|
+
would leave the glass's grey words below the AA floor. That scrim is the
|
|
20
|
+
artboard's own tint (--aq-scrim), not the hall's --scrim: the glass is the
|
|
21
|
+
design of record, the tweak editor's --tw-scrim precedent. */
|
|
22
|
+
|
|
23
|
+
.aq-overlay {
|
|
24
|
+
color-scheme: dark;
|
|
25
|
+
--aq-scrim: rgba(4, 5, 9, .94);
|
|
26
|
+
--aq-glass: rgba(255, 255, 255, .075);
|
|
27
|
+
--aq-glass-edge: rgba(255, 255, 255, .18);
|
|
28
|
+
--aq-glass-rim: rgba(255, 255, 255, .22);
|
|
29
|
+
--aq-drop: rgba(0, 0, 0, .55);
|
|
30
|
+
--aq-ink: #ffffff;
|
|
31
|
+
--aq-ink-soft: #c0c0c0;
|
|
32
|
+
--aq-ink-faint: #a8a8a8;
|
|
33
|
+
--aq-ink-dim: #8a8a90;
|
|
34
|
+
--aq-well: rgba(0, 0, 0, .25);
|
|
35
|
+
--aq-pill-on: rgba(255, 255, 255, .16);
|
|
36
|
+
--aq-close: rgba(255, 255, 255, .08);
|
|
37
|
+
--aq-ghost-edge: rgba(255, 255, 255, .24);
|
|
38
|
+
--aq-accent: #fb9569;
|
|
39
|
+
--aq-on-accent: #000000;
|
|
40
|
+
--aq-link: #fcbfa4;
|
|
41
|
+
--aq-mark: rgba(251, 149, 105, .35);
|
|
42
|
+
--aq-paper: #edecee;
|
|
43
|
+
--aq-paper-ink: #17181a;
|
|
44
|
+
--aq-rule: rgba(255, 255, 255, .12);
|
|
45
|
+
--aq-face: var(--font-body, Manrope, "Segoe UI", system-ui, sans-serif);
|
|
46
|
+
--aq-mono: var(--font-mono, "JetBrains Mono", ui-monospace, Consolas, monospace);
|
|
47
|
+
position: fixed;
|
|
48
|
+
inset: 0;
|
|
49
|
+
z-index: var(--z-modal, 160);
|
|
50
|
+
display: flex;
|
|
51
|
+
align-items: center;
|
|
52
|
+
justify-content: center;
|
|
53
|
+
padding: 24px;
|
|
54
|
+
box-sizing: border-box;
|
|
55
|
+
background: var(--aq-scrim);
|
|
56
|
+
color: var(--aq-ink);
|
|
57
|
+
font-family: var(--aq-face);
|
|
58
|
+
}
|
|
59
|
+
html[data-theme="dark"] .aq-overlay { --aq-scrim: rgba(4, 5, 9, .42); }
|
|
60
|
+
@media (prefers-color-scheme: dark) {
|
|
61
|
+
html:not([data-theme]) .aq-overlay { --aq-scrim: rgba(4, 5, 9, .42); }
|
|
62
|
+
}
|
|
63
|
+
.aq-overlay[hidden] { display: none; }
|
|
64
|
+
html.aq-open { overflow: hidden; }
|
|
65
|
+
|
|
66
|
+
.aq-dialog {
|
|
67
|
+
width: min(1180px, 100%);
|
|
68
|
+
height: min(820px, 100%);
|
|
69
|
+
box-sizing: border-box;
|
|
70
|
+
padding: 26px 30px;
|
|
71
|
+
border-radius: 30px;
|
|
72
|
+
background: var(--aq-glass);
|
|
73
|
+
border: 1px solid var(--aq-glass-edge);
|
|
74
|
+
backdrop-filter: blur(30px) saturate(160%);
|
|
75
|
+
-webkit-backdrop-filter: blur(30px) saturate(160%);
|
|
76
|
+
box-shadow: inset 0 1px 0 var(--aq-glass-rim), 0 40px 90px var(--aq-drop);
|
|
77
|
+
display: flex;
|
|
78
|
+
flex-direction: column;
|
|
79
|
+
gap: 16px;
|
|
80
|
+
|
|
81
|
+
}
|
|
82
|
+
.aq-overlay button,
|
|
83
|
+
.aq-overlay input { font-family: inherit; }
|
|
84
|
+
.aq-overlay :focus-visible { outline: 2px solid var(--aq-accent); outline-offset: 2px; }
|
|
85
|
+
|
|
86
|
+
/* The header row. */
|
|
87
|
+
.aq-head {
|
|
88
|
+
display: flex;
|
|
89
|
+
align-items: center;
|
|
90
|
+
gap: 14px;
|
|
91
|
+
min-width: 0;
|
|
92
|
+
}
|
|
93
|
+
.aq-eyebrow {
|
|
94
|
+
font-family: var(--aq-mono);
|
|
95
|
+
font-size: 11px;
|
|
96
|
+
letter-spacing: .14em;
|
|
97
|
+
text-transform: uppercase;
|
|
98
|
+
color: var(--aq-ink-soft);
|
|
99
|
+
white-space: nowrap;
|
|
100
|
+
}
|
|
101
|
+
.aq-title {
|
|
102
|
+
margin: 0;
|
|
103
|
+
font-size: 20px;
|
|
104
|
+
font-weight: 300;
|
|
105
|
+
line-height: 1.3;
|
|
106
|
+
color: var(--aq-ink);
|
|
107
|
+
min-width: 0;
|
|
108
|
+
}
|
|
109
|
+
.aq-title__sub { font-size: 14px; color: var(--aq-ink-dim); }
|
|
110
|
+
.aq-head__gap { margin-left: auto; }
|
|
111
|
+
.aq-flips {
|
|
112
|
+
display: flex;
|
|
113
|
+
gap: 4px;
|
|
114
|
+
padding: 4px;
|
|
115
|
+
border-radius: 999px;
|
|
116
|
+
background: var(--aq-well);
|
|
117
|
+
flex: none;
|
|
118
|
+
}
|
|
119
|
+
.aq-flip {
|
|
120
|
+
height: 40px;
|
|
121
|
+
padding: 0 18px;
|
|
122
|
+
border-radius: 999px;
|
|
123
|
+
cursor: pointer;
|
|
124
|
+
border: 0;
|
|
125
|
+
background: transparent;
|
|
126
|
+
color: var(--aq-ink-faint);
|
|
127
|
+
font-size: 13.5px;
|
|
128
|
+
}
|
|
129
|
+
.aq-flip[aria-pressed="true"] { background: var(--aq-pill-on); color: var(--aq-ink); }
|
|
130
|
+
.aq-close {
|
|
131
|
+
height: 44px;
|
|
132
|
+
padding: 0 16px;
|
|
133
|
+
border-radius: 999px;
|
|
134
|
+
cursor: pointer;
|
|
135
|
+
border: 0;
|
|
136
|
+
background: var(--aq-close);
|
|
137
|
+
color: var(--aq-ink);
|
|
138
|
+
font-size: 13px;
|
|
139
|
+
flex: none;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/* The page, and the words beside it. */
|
|
143
|
+
.aq-body {
|
|
144
|
+
flex-grow: 1;
|
|
145
|
+
min-height: 0;
|
|
146
|
+
display: flex;
|
|
147
|
+
gap: 16px;
|
|
148
|
+
}
|
|
149
|
+
.aq-frame {
|
|
150
|
+
flex-grow: 1;
|
|
151
|
+
min-width: 0;
|
|
152
|
+
min-height: 0;
|
|
153
|
+
overflow-y: auto;
|
|
154
|
+
border-radius: 14px;
|
|
155
|
+
background: var(--aq-paper);
|
|
156
|
+
color: var(--aq-paper-ink);
|
|
157
|
+
}
|
|
158
|
+
.aq-render { display: block; width: 100%; height: auto; }
|
|
159
|
+
.aq-render--phone { width: min(390px, 100%); margin: 24px auto; border-radius: 10px; }
|
|
160
|
+
.aq-frame__note {
|
|
161
|
+
margin: 0;
|
|
162
|
+
padding: 70px 40px;
|
|
163
|
+
font-size: 15px;
|
|
164
|
+
line-height: 1.5;
|
|
165
|
+
color: var(--aq-paper-ink);
|
|
166
|
+
max-width: 620px;
|
|
167
|
+
}
|
|
168
|
+
.aq-frame__swap {
|
|
169
|
+
margin: 0;
|
|
170
|
+
padding: 10px 16px;
|
|
171
|
+
font-size: 13px;
|
|
172
|
+
color: var(--aq-paper-ink);
|
|
173
|
+
}
|
|
174
|
+
.aq-lines {
|
|
175
|
+
width: 300px;
|
|
176
|
+
flex: none;
|
|
177
|
+
min-height: 0;
|
|
178
|
+
overflow-y: auto;
|
|
179
|
+
display: flex;
|
|
180
|
+
flex-direction: column;
|
|
181
|
+
gap: 16px;
|
|
182
|
+
}
|
|
183
|
+
.aq-views { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
184
|
+
.aq-lines__list {
|
|
185
|
+
list-style: none;
|
|
186
|
+
margin: 0;
|
|
187
|
+
padding: 0;
|
|
188
|
+
display: flex;
|
|
189
|
+
flex-direction: column;
|
|
190
|
+
gap: 14px;
|
|
191
|
+
}
|
|
192
|
+
.aq-line { display: flex; flex-direction: column; gap: 4px; padding-top: 14px; border-top: 1px solid var(--aq-rule); }
|
|
193
|
+
.aq-line__section {
|
|
194
|
+
font-family: var(--aq-mono);
|
|
195
|
+
font-size: 11px;
|
|
196
|
+
letter-spacing: .14em;
|
|
197
|
+
text-transform: uppercase;
|
|
198
|
+
color: var(--aq-ink-faint);
|
|
199
|
+
}
|
|
200
|
+
.aq-line__text { font-size: 15px; line-height: 1.5; color: var(--aq-ink); }
|
|
201
|
+
.aq-mark { background: var(--aq-mark); color: var(--aq-ink); border-radius: 4px; padding: 0 3px; }
|
|
202
|
+
.aq-line__aside { font-size: 13px; line-height: 1.45; color: var(--aq-ink-faint); }
|
|
203
|
+
.aq-line--refused .aq-line__aside { color: var(--aq-link); }
|
|
204
|
+
.aq-lines__none { margin: 0; font-size: 14px; color: var(--aq-ink-faint); }
|
|
205
|
+
|
|
206
|
+
/* The decision row. */
|
|
207
|
+
.aq-foot {
|
|
208
|
+
display: flex;
|
|
209
|
+
align-items: center;
|
|
210
|
+
gap: 12px;
|
|
211
|
+
min-height: 48px;
|
|
212
|
+
}
|
|
213
|
+
.aq-accent {
|
|
214
|
+
height: 48px;
|
|
215
|
+
padding: 0 26px;
|
|
216
|
+
border-radius: 999px;
|
|
217
|
+
cursor: pointer;
|
|
218
|
+
border: 0;
|
|
219
|
+
background: var(--aq-accent);
|
|
220
|
+
color: var(--aq-on-accent);
|
|
221
|
+
font-size: 15px;
|
|
222
|
+
flex: none;
|
|
223
|
+
}
|
|
224
|
+
.aq-ghost {
|
|
225
|
+
height: 48px;
|
|
226
|
+
padding: 0 24px;
|
|
227
|
+
border-radius: 999px;
|
|
228
|
+
cursor: pointer;
|
|
229
|
+
background: transparent;
|
|
230
|
+
border: 1px solid var(--aq-ghost-edge);
|
|
231
|
+
color: var(--aq-ink);
|
|
232
|
+
font-size: 15px;
|
|
233
|
+
flex: none;
|
|
234
|
+
}
|
|
235
|
+
.aq-ghost--send { padding: 0 20px; font-size: 14px; }
|
|
236
|
+
.aq-ghost--small { height: 44px; padding: 0 18px; font-size: 13.5px; }
|
|
237
|
+
.aq-accent:disabled,
|
|
238
|
+
.aq-ghost:disabled { opacity: .5; cursor: default; }
|
|
239
|
+
.aq-note { flex-grow: 1; display: flex; gap: 10px; align-items: center; min-width: 0; }
|
|
240
|
+
.aq-note__input {
|
|
241
|
+
flex-grow: 1;
|
|
242
|
+
min-width: 0;
|
|
243
|
+
box-sizing: border-box;
|
|
244
|
+
height: 48px;
|
|
245
|
+
padding: 0 16px;
|
|
246
|
+
border-radius: 999px;
|
|
247
|
+
font-size: 14.5px;
|
|
248
|
+
color: var(--aq-ink);
|
|
249
|
+
background: var(--aq-well);
|
|
250
|
+
border: 1px solid var(--aq-glass-edge);
|
|
251
|
+
}
|
|
252
|
+
.aq-note__input::placeholder { color: var(--aq-ink-faint); }
|
|
253
|
+
.aq-hint { margin-left: auto; font-size: 13px; color: var(--aq-ink-faint); white-space: nowrap; }
|
|
254
|
+
.aq-sealed { margin: 0; font-size: 14px; color: var(--aq-ink-soft); }
|
|
255
|
+
.aq-outcome { margin: 0; font-size: 15px; line-height: 1.5; }
|
|
256
|
+
.aq-outcome--approved { color: var(--aq-link); }
|
|
257
|
+
.aq-outcome--sent { color: var(--aq-ink-soft); }
|
|
258
|
+
.aq-foot--done { justify-content: space-between; }
|
|
259
|
+
.aq-error { margin: -6px 0 0; font-size: 13.5px; color: var(--aq-link); }
|
|
260
|
+
.aq-sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
|
|
261
|
+
|
|
262
|
+
/* A phone: the dialog is the screen, and it scrolls as one; the words go under
|
|
263
|
+
the page, and the header and the decision row wrap. */
|
|
264
|
+
@media (max-width: 760px) {
|
|
265
|
+
/* The dialog is the whole screen here, so the room behind would read through
|
|
266
|
+
the glass under its words; the scrim darkens to .88 in both modes. */
|
|
267
|
+
.aq-overlay { padding: 8px; align-items: stretch; --aq-scrim: rgba(4, 5, 9, .88); }
|
|
268
|
+
html[data-theme] .aq-overlay,
|
|
269
|
+
html:not([data-theme]) .aq-overlay { --aq-scrim: rgba(4, 5, 9, .88); }
|
|
270
|
+
.aq-dialog { height: 100%; padding: 18px 16px; border-radius: 22px; overflow-y: auto; gap: 14px; }
|
|
271
|
+
.aq-dialog > * { flex-shrink: 0; }
|
|
272
|
+
.aq-head { flex-wrap: wrap; row-gap: 10px; }
|
|
273
|
+
.aq-eyebrow { order: 1; flex: 1 1 auto; }
|
|
274
|
+
.aq-close { order: 2; }
|
|
275
|
+
.aq-title { order: 3; flex: 1 1 100%; }
|
|
276
|
+
.aq-head__gap { display: none; }
|
|
277
|
+
.aq-head .aq-flips { order: 4; }
|
|
278
|
+
.aq-body { flex-direction: column; flex-grow: 0; }
|
|
279
|
+
.aq-frame { flex: none; max-height: 70vh; }
|
|
280
|
+
.aq-frame__note { padding: 40px 20px; }
|
|
281
|
+
.aq-lines { width: auto; overflow: visible; }
|
|
282
|
+
.aq-foot { flex-wrap: wrap; }
|
|
283
|
+
.aq-note { flex: 1 1 100%; }
|
|
284
|
+
.aq-hint { margin-left: 0; flex: 1 1 100%; white-space: normal; }
|
|
285
|
+
.aq-foot--done { flex-direction: column; align-items: flex-start; }
|
|
286
|
+
}
|