sign-off-axi 0.4.1 → 0.4.2
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 -0
- package/package.json +1 -1
- package/web/index.html +151 -5
- package/web/point.js +4 -0
package/dist/cli.js
CHANGED
|
@@ -2400,6 +2400,8 @@ const server = createServer(async (req, res) => {
|
|
|
2400
2400
|
if (given !== null)
|
|
2401
2401
|
return res.writeHead(303, { location: '/', 'set-cookie': `${cookie}=${token}; Path=/; HttpOnly; SameSite=Lax; Max-Age=31536000` }).end();
|
|
2402
2402
|
}
|
|
2403
|
+
if (req.method === 'GET' && req.url === '/favicon.ico')
|
|
2404
|
+
return res.writeHead(204).end();
|
|
2403
2405
|
if (req.method === 'GET' && req.url === '/') {
|
|
2404
2406
|
return res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'content-security-policy': "default-src 'self' 'unsafe-inline'" }).end(page);
|
|
2405
2407
|
}
|
package/package.json
CHANGED
package/web/index.html
CHANGED
|
@@ -149,6 +149,58 @@
|
|
|
149
149
|
#reader .close { display: inline-flex; flex: none; align-items: center; min-height: 3rem; margin-left: -0.5rem; padding: 0 0.5rem; border-radius: 0.625rem; color: var(--accent); font-weight: 650; text-decoration: none; }
|
|
150
150
|
#reader h1 { margin: 0; min-width: 0; overflow: hidden; font-size: 1rem; font-weight: 650; text-overflow: ellipsis; white-space: nowrap; }
|
|
151
151
|
#reader iframe { flex: 1; width: 100%; border: 0; background: var(--bg); }
|
|
152
|
+
#reader h1:not(:focus-visible) { outline: none; }
|
|
153
|
+
#reader h1:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
154
|
+
.i-plan { --area: var(--c-waiting); }
|
|
155
|
+
#reader .plan-scroll { flex: 1; min-height: 0; overflow: auto; padding: 1.25rem 1.25rem 9rem; }
|
|
156
|
+
.plan-layout { max-width: 72rem; margin: auto; display: grid; gap: 2rem; }
|
|
157
|
+
.plan-main { min-width: 0; }
|
|
158
|
+
.plan-main > h2 { margin: 2rem 0 0.75rem; }
|
|
159
|
+
.plan-main > h2:first-of-type { font-size: 3.5rem; line-height: 1.15; margin: 0.5rem 0; }
|
|
160
|
+
.plan-main > p { margin: 0.5rem 0 1.5rem; }
|
|
161
|
+
.plan-summary { display: flex; gap: 0.75rem; color: var(--muted); font-size: 1.125rem; line-height: 1.5; margin: 1.5rem 0 2rem; }
|
|
162
|
+
#reader .plan-view { width: 100%; height: 22rem; flex: none; border-radius: 1rem; }
|
|
163
|
+
.plan-section, .plan-decision, .plan-explain { background: var(--surface); border-radius: 1.125rem; padding: 1rem; margin: 1rem 0; scroll-margin-top: 1rem; }
|
|
164
|
+
.plan-section h3, .plan-decision h3 { font-size: 1.375rem; line-height: 1.3; margin: 0 0 0.75rem; }
|
|
165
|
+
.plan-section p { white-space: pre-line; }
|
|
166
|
+
.plan-verdicts { display: flex; gap: 0.625rem; }
|
|
167
|
+
.plan-verdicts button { flex: 1; border: 0; border-radius: 980px; background: var(--sunk); font-weight: 600; }
|
|
168
|
+
.plan-verdicts button[aria-pressed="true"] { box-shadow: inset 0 0 0 1.5px currentColor; background: var(--surface); }
|
|
169
|
+
.plan-verdicts button[aria-pressed="true"]::before, .plan-decision button[aria-pressed="true"]::before { content: none; }
|
|
170
|
+
.plan-verdicts .approve[aria-pressed="true"] { color: var(--ok); }
|
|
171
|
+
.plan-verdicts .change[aria-pressed="true"] { color: var(--warn); }
|
|
172
|
+
.plan-section label { display: grid; gap: 0.5rem; margin-top: 1rem; color: var(--muted); }
|
|
173
|
+
.plan-section label[hidden] { display: none; }
|
|
174
|
+
.plan-section textarea { width: 100%; min-height: 7rem; padding: 0.75rem; background: var(--sunk); }
|
|
175
|
+
.plan-decision .choices { display: grid; grid-template-columns: minmax(0, 1fr); gap: 0.625rem; }
|
|
176
|
+
.plan-decision .choices button { text-align: left; border-radius: 0.875rem; padding: 0.875rem; gap: 0.25rem; }
|
|
177
|
+
.plan-decision .choices button .tag { margin: 0; text-transform: none; letter-spacing: normal; color: var(--accent); }
|
|
178
|
+
.plan-decision .choices button .tag::before { content: none; }
|
|
179
|
+
.plan-decision .choices button[aria-pressed="true"] { background: color-mix(in srgb, var(--accent) 12%, var(--surface)); color: var(--text); border: 2px solid var(--accent); }
|
|
180
|
+
.plan-decision .tag { display: block; color: var(--accent); font-size: 0.875rem; }
|
|
181
|
+
.plan-explain .foot { justify-content: space-between; }
|
|
182
|
+
.plan-send { position: absolute; bottom: 0; left: 0; right: 0; padding: 0.625rem 1rem calc(0.75rem + env(safe-area-inset-bottom)); background: var(--bar); border-top: 1px solid var(--line); }
|
|
183
|
+
.plan-send p { margin: 0 0 0.5rem; }
|
|
184
|
+
.plan-send button { width: 100%; }
|
|
185
|
+
.plan-next-row { display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin-bottom: 0.5rem; }
|
|
186
|
+
.plan-next { width: auto !important; padding: 0; border: 0; background: none; color: var(--accent); font-weight: 600; }
|
|
187
|
+
.plan-send .err:empty { display: none; }
|
|
188
|
+
.plan-progress button { width: 100%; padding: 0.5rem 0; display: flex; justify-content: space-between; gap: 1rem; background: none; text-align: left; border-radius: 0.5rem; font-size: 1rem; }
|
|
189
|
+
.plan-progress span { color: var(--muted); }
|
|
190
|
+
@media (max-width: 59.999rem) { #reader .plan-scroll { margin-bottom: calc(9rem + env(safe-area-inset-bottom)); } #reader .plan-scroll.sent { margin-bottom: 0; } .plan-send h2, .plan-progress { display: none; } .plan-send p { font-size: 1rem; } }
|
|
191
|
+
#reader .plan-sent .back { display: inline-flex; }
|
|
192
|
+
.plan-sent { text-align: center; padding-top: 2rem; }
|
|
193
|
+
.plan-sent h2 { color: var(--ok); font-size: 2.125rem; }
|
|
194
|
+
.plan-sent .icon { margin: auto; background: var(--ok); }
|
|
195
|
+
.plan-layout.sent { grid-template-columns: minmax(0, 42.5rem); justify-content: center; }
|
|
196
|
+
.plan-send[hidden] { display: none; }
|
|
197
|
+
@media (min-width: 60rem) {
|
|
198
|
+
#reader .plan-scroll { padding: 2rem 3rem; }
|
|
199
|
+
.plan-layout { grid-template-columns: minmax(0, 42.5rem) minmax(15rem, 1fr); }
|
|
200
|
+
.plan-send { position: sticky; align-self: start; top: 0; border: 0; border-radius: 1.125rem; padding: 1.25rem; }
|
|
201
|
+
.plan-next { display: none; }
|
|
202
|
+
.plan-section, .plan-decision { padding: 1.5rem; }
|
|
203
|
+
}
|
|
152
204
|
body.reading { overflow: hidden; }
|
|
153
205
|
.foot { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem 1rem; }
|
|
154
206
|
.foot .meta { margin: 0 auto 0 0; }
|
|
@@ -572,7 +624,7 @@
|
|
|
572
624
|
const update = d.type === 'evidence', card = d.evidenceCard, result = RESULT[card?.result];
|
|
573
625
|
const brow = [update && area(d) !== 'waiting' ? `Update ${d.number}` : kind(d), result ? el('span', { class: `st ${result[0]}` }, result[1]) : '',
|
|
574
626
|
update && d.filed ? (isToday(d.filed) ? el('span', { class: 'st new' }, 'New') : day(d.filed)) : '', seen].filter(Boolean);
|
|
575
|
-
const lead = el('div', { class: 'lead' }, x ? xBtn(d) : '', brow.length ? el('p', { class: 'eyebrow' }, ...brow) : '', el('h3', { dir: 'auto' }, name));
|
|
627
|
+
const lead = el('div', { class: 'lead' }, x ? xBtn(d) : '', brow.length ? el('p', { class: 'eyebrow' }, ...(d.type === 'plan' ? [icon('plan')] : []), ...brow) : '', el('h3', { dir: 'auto' }, name));
|
|
576
628
|
if (d.type === 'signin') return signin(d, lead, saved, err);
|
|
577
629
|
if (d.type === 'plan') return plan(d, lead);
|
|
578
630
|
const quiet = !ro && d.options.find((o) => o.value === d.default_if_silent);
|
|
@@ -869,6 +921,7 @@
|
|
|
869
921
|
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
922
|
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
923
|
answered: 'M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zM9 12l2 2 4-4',
|
|
924
|
+
plan: 'M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7zM14 2v6h6M8 13h8M8 17h8M8 9h2',
|
|
872
925
|
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' };
|
|
873
926
|
// The drawings are fixed strings from this page, never text from the board file.
|
|
874
927
|
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; };
|
|
@@ -932,7 +985,7 @@
|
|
|
932
985
|
/** A short row: a picture (or the area's mark) and the title; an answered item has its answer and time under the title. */
|
|
933
986
|
const compact = (d, a) => {
|
|
934
987
|
const s = state.answers[d.key];
|
|
935
|
-
return [thumb(a, pic(d)), el('span', { class: 't' }, el('strong', { dir: 'auto' }, title(d)), s
|
|
988
|
+
return [thumb(d.type === 'plan' ? 'plan' : a, pic(d)), el('span', { class: 't' }, el('strong', { dir: 'auto' }, title(d)), s
|
|
936
989
|
? el('span', { class: 'ans' }, el('b', { dir: 'auto' }, s.answer === 'text' ? 'Your reply' : s.label), el('time', {}, isToday(s.at) ? hhmm(s.at) : day(s.at)))
|
|
937
990
|
: el('span', { class: 'meta', dir: 'auto' }, sub(d)))];
|
|
938
991
|
};
|
|
@@ -1013,6 +1066,95 @@
|
|
|
1013
1066
|
return box;
|
|
1014
1067
|
}
|
|
1015
1068
|
|
|
1069
|
+
/** Plan answers stay on the trusted board, never in the agent's sandboxed page. Drafts survive refresh until the server saves them. */
|
|
1070
|
+
function planReview(d, url) {
|
|
1071
|
+
const saved = answerOf(d.key), ro = !!saved || state.readonly || !!FROZEN, draftKey = `plan:${d.key}`;
|
|
1072
|
+
let draft;
|
|
1073
|
+
try { draft = JSON.parse(drafts.get(draftKey) ?? '{}'); } catch { draft = {}; }
|
|
1074
|
+
const notes = new Map((saved?.notes ?? draft.notes ?? []).filter((n) => d.sections.some((s) => s.id === n.section)).map((n) => [n.section, n]));
|
|
1075
|
+
const picks = new Map(Object.entries(saved?.decisions ?? draft.decisions ?? {}).filter(([key, value]) => d.decisions.some((q) => q.key === key && q.options.some((o) => o.value === value))));
|
|
1076
|
+
const remember = () => { drafts.set(draftKey, JSON.stringify({ notes: [...notes.values()], decisions: Object.fromEntries(picks) })); keep(); };
|
|
1077
|
+
const main = el('div', { class: 'plan-main' }), panel = el('aside', { class: 'plan-send' });
|
|
1078
|
+
const status = el('p', { role: 'status' }), err = el('p', { class: 'err', role: 'alert' }), progress = el('div', { class: 'plan-progress' });
|
|
1079
|
+
const send = el('button', { class: 'open-link' }, 'Send back'), next = el('button', { class: 'plan-next' }, 'Go to next');
|
|
1080
|
+
const complete = (s) => { const n = notes.get(s.id); return n?.verdict === 'approve' || (n?.verdict === 'change' && n.note?.trim()); };
|
|
1081
|
+
next.onclick = () => {
|
|
1082
|
+
const section = d.sections.find((s) => !complete(s)), decision = d.decisions.find((q) => !q.options.some((o) => o.value === picks.get(q.key)));
|
|
1083
|
+
document.getElementById(section ? `plan-section-${section.id}` : `plan-decision-${decision?.key}`)?.scrollIntoView({ block: 'start' });
|
|
1084
|
+
};
|
|
1085
|
+
const update = () => {
|
|
1086
|
+
const left = d.sections.filter((s) => !complete(s)).length, decisions = d.decisions.filter((q) => !q.options.some((o) => o.value === picks.get(q.key))).length;
|
|
1087
|
+
status.textContent = saved ? 'Sent back. Your answers are saved.' : left || decisions ? `${left} section${left === 1 ? '' : 's'} and ${decisions} decision${decisions === 1 ? '' : 's'} left` : 'Ready. Every part has an answer.';
|
|
1088
|
+
send.disabled = ro || !!left || !!decisions;
|
|
1089
|
+
next.hidden = !left && !decisions;
|
|
1090
|
+
progress.replaceChildren(...d.sections.map((s) => {
|
|
1091
|
+
const verdict = notes.get(s.id)?.verdict, b = el('button', {}, s.heading, el('span', {}, verdict === 'approve' ? 'Approved' : verdict === 'change' ? 'Change' : 'To review'));
|
|
1092
|
+
b.onclick = () => document.getElementById(`plan-section-${s.id}`).scrollIntoView(); return b;
|
|
1093
|
+
}), d.decisions.length ? el('p', { class: 'meta' }, `${d.decisions.length - decisions} of ${d.decisions.length} decisions answered`) : '');
|
|
1094
|
+
};
|
|
1095
|
+
if (saved) {
|
|
1096
|
+
main.append(el('div', { class: 'plan-sent', role: 'status' }, icon('answered'), el('h2', {}, 'Sent back'), el('p', {}, 'Your section notes and decisions are saved for the agent.'),
|
|
1097
|
+
el('a', { class: 'back', href: '#answered' }, 'See it in Answered')));
|
|
1098
|
+
}
|
|
1099
|
+
main.append(el('p', { class: 'eyebrow' }, icon('plan'), 'Plan'), el('h2', { dir: 'auto' }, d.title), el('p', { dir: 'auto' }, d.about ?? ''));
|
|
1100
|
+
if (!saved) main.append(el('iframe', { class: 'plan-view', src: url, title: `Plan: ${d.title}`, sandbox: 'allow-scripts' }),
|
|
1101
|
+
el('p', { class: 'plan-summary' }, icon('plan'), `${d.sections.length} sections to approve or change, and ${d.decisions.length} decisions to make. Nothing is built until you send back.`));
|
|
1102
|
+
main.append(el('h2', {}, saved ? 'What you sent' : 'The plan'), saved ? '' : el('p', { class: 'meta' }, 'Approve or change each part. A change needs a note.'));
|
|
1103
|
+
d.sections.forEach((s, i) => {
|
|
1104
|
+
const card = el('section', { class: 'plan-section', id: `plan-section-${s.id}`, 'aria-label': s.heading });
|
|
1105
|
+
const approve = el('button', { class: 'approve', 'aria-label': `Approve, ${s.heading}` }, 'Approve');
|
|
1106
|
+
const change = el('button', { class: 'change', 'aria-label': `Change, ${s.heading}` }, 'Change');
|
|
1107
|
+
const field = el('textarea', { id: `plan-note-${s.id}`, maxlength: '500', placeholder: 'Write your note for the agent' });
|
|
1108
|
+
field.value = notes.get(s.id)?.note ?? '';
|
|
1109
|
+
field.disabled = ro;
|
|
1110
|
+
const note = el('label', { for: field.id }, `What should change in ${s.heading.toLowerCase()}?`, field);
|
|
1111
|
+
const draw = () => {
|
|
1112
|
+
const verdict = notes.get(s.id)?.verdict;
|
|
1113
|
+
card.dataset.verdict = verdict ?? '';
|
|
1114
|
+
approve.setAttribute('aria-pressed', verdict === 'approve');
|
|
1115
|
+
approve.textContent = verdict === 'approve' ? '✓ Approved' : 'Approve';
|
|
1116
|
+
change.setAttribute('aria-pressed', verdict === 'change');
|
|
1117
|
+
approve.disabled = change.disabled = ro;
|
|
1118
|
+
note.hidden = verdict !== 'change';
|
|
1119
|
+
update();
|
|
1120
|
+
};
|
|
1121
|
+
[approve, change].forEach((b, n) => b.onclick = () => {
|
|
1122
|
+
notes.set(s.id, { section: s.id, verdict: n ? 'change' : 'approve', ...(n && { note: field.value }) });
|
|
1123
|
+
remember(); draw(); if (n) field.focus();
|
|
1124
|
+
});
|
|
1125
|
+
field.oninput = () => { notes.set(s.id, { section: s.id, verdict: 'change', note: field.value }); remember(); update(); };
|
|
1126
|
+
card.append(el('h3', { dir: 'auto' }, `${i + 1}. ${s.heading}`), el('p', { dir: 'auto' }, s.body), el('div', { class: 'plan-verdicts', role: 'group', 'aria-label': s.heading }, approve, change), note);
|
|
1127
|
+
main.append(card); draw();
|
|
1128
|
+
});
|
|
1129
|
+
if (d.decisions.length) main.append(el('h2', {}, 'Decisions for you'));
|
|
1130
|
+
d.decisions.forEach((q) => {
|
|
1131
|
+
const choices = el('div', { class: 'choices', role: 'group', 'aria-label': q.question });
|
|
1132
|
+
q.options.forEach((o) => {
|
|
1133
|
+
const b = el('button', { 'aria-pressed': picks.get(q.key) === o.value, ...(ro && { disabled: '' }) }, el('strong', {}, o.label),
|
|
1134
|
+
o.value === q.recommend ? el('span', { class: 'tag' }, 'Recommended') : '', el('span', { class: 'out', dir: 'auto' }, o.outcome));
|
|
1135
|
+
b.onclick = () => { picks.set(q.key, o.value); remember(); [...choices.children].forEach((b, i) => b.setAttribute('aria-pressed', q.options[i].value === o.value)); update(); };
|
|
1136
|
+
choices.append(b);
|
|
1137
|
+
});
|
|
1138
|
+
main.append(el('section', { class: 'plan-decision', id: `plan-decision-${q.key}` }, el('h3', { dir: 'auto' }, q.question), el('p', { class: 'meta', dir: 'auto' }, q.why), choices));
|
|
1139
|
+
});
|
|
1140
|
+
if (d.steps?.length && !saved) {
|
|
1141
|
+
let step = 0;
|
|
1142
|
+
const text = el('p', { 'aria-live': 'polite' }), prev = el('button', { 'aria-label': 'Previous step' }, '‹ Previous'), next = el('button', { 'aria-label': 'Next step' }, 'Next ›');
|
|
1143
|
+
const draw = () => { text.textContent = `Step ${step + 1} of ${d.steps.length}: ${d.steps[step].title}`; prev.disabled = !step; next.disabled = step === d.steps.length - 1; };
|
|
1144
|
+
prev.onclick = () => { step--; draw(); }; next.onclick = () => { step++; draw(); }; draw();
|
|
1145
|
+
main.append(el('section', { class: 'plan-explain' }, el('h3', {}, 'How it works'), text, el('div', { class: 'foot' }, prev, next), el('p', { class: 'meta' }, 'The direction page above explains these steps.')));
|
|
1146
|
+
}
|
|
1147
|
+
send.onclick = () => {
|
|
1148
|
+
send.disabled = true; err.textContent = '';
|
|
1149
|
+
call('/api/answers', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ key: d.key,
|
|
1150
|
+
notes: d.sections.map((s) => notes.get(s.id)), decisions: Object.fromEntries(picks) }) }, () => {
|
|
1151
|
+
drafts.delete(draftKey); keep(); location.hash = `#answered/${d.key}`;
|
|
1152
|
+
}).catch((e) => { err.textContent = `Couldn't send back: ${e.message}`; update(); });
|
|
1153
|
+
};
|
|
1154
|
+
panel.append(el('h2', {}, 'Your answers'), progress, el('div', { class: 'plan-next-row' }, status, next), send, err); panel.hidden = !!saved; update();
|
|
1155
|
+
return el('div', { class: `plan-scroll${saved ? ' sent' : ''}` }, el('div', { class: `plan-layout${saved ? ' sent' : ''}` }, main, panel));
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1016
1158
|
/** The report a #reports/<key> link opens (or the plan a #waiting/<key> link opens), over the whole board: a bar with the way back and its title, then its page in a sandboxed frame.
|
|
1017
1159
|
* The frame is made again only for another page, so a redraw never resets a demo halfway through. */
|
|
1018
1160
|
const reader = document.getElementById('reader');
|
|
@@ -1021,10 +1163,13 @@
|
|
|
1021
1163
|
for (const n of [document.querySelector('main'), document.getElementById('nav'), document.getElementById('tabs')]) n.inert = !!d;
|
|
1022
1164
|
reader.hidden = !d;
|
|
1023
1165
|
const url = d ? src(mockUrl(d.key, '', d.view)) : '';
|
|
1024
|
-
|
|
1166
|
+
const answer = d?.type === 'plan' ? JSON.stringify(state.answers[d.key] ?? null) : '';
|
|
1167
|
+
const plan = d?.type === 'plan' ? JSON.stringify(d) : '';
|
|
1168
|
+
if (reader.dataset.url === url && reader.dataset.answer === answer && reader.dataset.plan === plan) return d && (reader.querySelector('h1').textContent = d.title);
|
|
1169
|
+
Object.assign(reader.dataset, { answer, plan });
|
|
1025
1170
|
Object.assign(reader.dataset, { url, key: d?.key ?? '', from });
|
|
1026
1171
|
reader.replaceChildren(...(d ? [el('header', {}, el('a', { class: 'close', href: `#${from}` }, `‹ ${from === 'reports' ? 'Reports' : AREAS[from]}`), el('h1', { id: 'reader-title', tabindex: '-1', dir: 'auto' }, d.title)),
|
|
1027
|
-
el('iframe', { src: url, title:
|
|
1172
|
+
d.type === 'plan' ? planReview(d, url) : el('iframe', { src: url, title: `Report: ${d.title}`, sandbox: 'allow-scripts' })] : []));
|
|
1028
1173
|
}
|
|
1029
1174
|
addEventListener('keydown', (e) => { if (e.key === 'Escape' && !reader.hidden) location.hash = `#${reader.dataset.from}`; });
|
|
1030
1175
|
|
|
@@ -1176,7 +1321,8 @@
|
|
|
1176
1321
|
// An interactive page may post its own height, `{ height: <px> }`, and its frame then fits it with no blank below.
|
|
1177
1322
|
// Only a frame's own message sizes that frame, and never past 10000px.
|
|
1178
1323
|
addEventListener('message', (e) => {
|
|
1179
|
-
const frame = [...document.querySelectorAll('.rung iframe')].find((f) => f.contentWindow === e.source), h = e.data?.height;
|
|
1324
|
+
const frame = [...document.querySelectorAll('.rung iframe, iframe.plan-view')].find((f) => f.contentWindow === e.source), h = e.data?.height;
|
|
1325
|
+
if (frame?.classList.contains('plan-view') && e.data?.['sign-off'] === 'ready') frame.contentWindow.postMessage({ 'sign-off': 'size' }, '*');
|
|
1180
1326
|
if (frame && Number.isFinite(h) && h > 0) frame.style.height = `${Math.ceil(Math.min(h, 10_000))}px`;
|
|
1181
1327
|
});
|
|
1182
1328
|
// A design mock page may point (web/point.js): the board keeps the last element named per mock, and turns pointing on for one mock frame.
|
package/web/point.js
CHANGED
|
@@ -31,6 +31,10 @@
|
|
|
31
31
|
};
|
|
32
32
|
addEventListener('message', (m) => {
|
|
33
33
|
if (m.source === parent && m.data?.['sign-off'] === 'point') on = m.data.on === true;
|
|
34
|
+
if (m.source === parent && m.data?.['sign-off'] === 'size') {
|
|
35
|
+
const size = () => parent.postMessage({ height: document.documentElement.scrollHeight }, '*');
|
|
36
|
+
new ResizeObserver(size).observe(document.body); size();
|
|
37
|
+
}
|
|
34
38
|
});
|
|
35
39
|
for (const type of ['pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click']) addEventListener(type, (m) => {
|
|
36
40
|
if (!on) return;
|