@gotcos/glasses-server 6.99.29 → 6.99.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +58 -0
- package/package.json +1 -1
- package/server/web-control/app.css +40 -9
- package/server/web-control/app.js +245 -20
- package/server/web-control/control.css +12 -6
- package/server/web-control/index.html +1 -0
- package/server/web-control/panes/cos-chat.css +10 -4
- package/server/web-control/panes/cos-chat.js +1 -1
- package/server/web-control/panes/meetings.css +5 -2
- package/server/web-control/panes/meetings.js +1 -1
- package/server/web-control/panes/memories.css +1 -1
- package/server/web-control/panes/memories.js +3 -8
- package/server/web-control/panes/questions.css +20 -4
- package/server/web-control/panes/questions.js +102 -21
- package/server/web-control/panes/sessions.css +18 -7
- package/server/web-control/panes/speakers.css +3 -5
- package/server/web-control/panes/speakers.js +7 -4
- package/server/web-control/panes/threads.js +3 -2
- package/server/web-control/panes/today-actions.css +38 -18
- package/server/web-control/panes/today-actions.js +176 -42
- package/server/web-control/panes/work.css +1 -1
- package/server/web-control/shell.js +3 -0
|
@@ -128,7 +128,7 @@
|
|
|
128
128
|
.mm-try { margin: 8px 0 2px; }
|
|
129
129
|
.mm-tries { margin: 0; padding: 0; list-style: none; display: grid; gap: 4px; }
|
|
130
130
|
.mm-tries .mm-link { font-size: 13.5px; overflow-wrap: anywhere; }
|
|
131
|
-
.mm-answer { margin: 12px 0 8px; padding: 12px 14px; border-radius: 10px; background: var(--qopen); font-size: 14.5px; line-height: 1.55; overflow-wrap: anywhere; max-height: 60vh; overflow-y: auto; }
|
|
131
|
+
.mm-answer { margin: 12px 0 8px; padding: 12px 14px; border-radius: 10px; background: var(--qopen); font-size: 14.5px; line-height: 1.55; overflow-wrap: anywhere; max-height: 60vh; max-height: 60dvh; overflow-y: auto; }
|
|
132
132
|
.mm-answer h4 { margin: 10px 0 4px; font: 600 14.5px/1.3 var(--body); }
|
|
133
133
|
.mm-answer p { margin: 0 0 8px; }
|
|
134
134
|
.mm-answer ul { margin: 0 0 8px; padding-left: 1.2em; }
|
|
@@ -109,19 +109,14 @@
|
|
|
109
109
|
// Copy stays in this browser: the same copy the rest of Control Web uses (plain-http fallback included).
|
|
110
110
|
if (op === 'copy') return W.copyText(String(a.text || '')).then(function () { return {}; });
|
|
111
111
|
var limit = READ_LIMIT[op] != null ? READ_LIMIT[op] : (isRead(op, a) ? 45000 : 0);
|
|
112
|
-
return W.api('/api/web-bridge/memories', { method: 'POST', body: { id: ++seq, op: op, args: a }, timeoutMs: limit }).then(
|
|
112
|
+
return W.api('/api/web-bridge/memories', { method: 'POST', body: { id: ++seq, op: op, args: a }, timeoutMs: limit }).then(function (r) {
|
|
113
113
|
if (r && r.ok) return r.details || {};
|
|
114
114
|
var raw = (r && r.message) || 'Your Mac did not answer that.';
|
|
115
115
|
var err = new Error(PLAIN[raw] || raw); err.code = raw; throw err;
|
|
116
116
|
});
|
|
117
117
|
}
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
var UNREACHABLE = 'Could not reach COS on your Mac. Check that the Mac is awake and on Tailscale, then try again.';
|
|
121
|
-
function plainNetError(err) {
|
|
122
|
-
if (err && !err.status && err.code !== 'timeout') { var e = new Error(UNREACHABLE); e.code = 'unreachable'; throw e; }
|
|
123
|
-
throw err;
|
|
124
|
-
}
|
|
118
|
+
// A request that never reached COS is said in plain words by W.api itself (app.js, 6.99.30: code 'unreachable'),
|
|
119
|
+
// as a timeout is: nothing to translate here.
|
|
125
120
|
function codeOf(e) { return String((e && (e.code || e.message)) || ''); }
|
|
126
121
|
|
|
127
122
|
// ── state ────────────────────────────────────────────────
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* COS Control Web: session questions and approvals (panes/questions.js), server 6.99.12. */
|
|
1
|
+
/* COS Control Web: session questions and approvals (panes/questions.js), from server 6.99.12. */
|
|
2
2
|
.sq-host { display: grid; gap: 12px; margin: 0 0 16px; }
|
|
3
3
|
.sq-host[hidden] { display: none; }
|
|
4
4
|
.sq-card { background: var(--panel); border: 1px solid color-mix(in srgb, var(--hot) 45%, var(--panel-border)); border-radius: 16px; padding: 14px 16px 12px; min-width: 0; }
|
|
@@ -52,11 +52,27 @@
|
|
|
52
52
|
.sq-foot { position: sticky; bottom: var(--safe-bottom, 0px); z-index: 2; margin: 12px -16px -12px; padding: 10px 16px 12px; background: var(--panel);
|
|
53
53
|
border-top: 1px solid var(--row-border); border-radius: 0 0 16px 16px; }
|
|
54
54
|
.sq-foot .sq-acts { margin-top: 0; }
|
|
55
|
-
/* QA 6.99.27: on the page, the footer rides above the
|
|
56
|
-
|
|
57
|
-
bottom of the sheet with no gap. */
|
|
55
|
+
/* QA 6.99.27: on the page, the footer rides above whatever covers the bottom (questions.js sets --sq-kb from app.js
|
|
56
|
+
viewportFit: a keyboard, its strip, a browser's bottom bar). In Today's sheet the layer is itself the visible part of
|
|
57
|
+
the page (today-actions.js applyFit), so the footer sits at the bottom of the sheet with no gap. */
|
|
58
58
|
.sq-host .sq-foot { bottom: max(var(--safe-bottom, 0px), var(--sq-kb, 0px)); }
|
|
59
59
|
.ta-q .sq-foot { bottom: 0; }
|
|
60
|
+
/* 6.99.30 (canary): with a keyboard up on the page (questions.js sets sq-kb-up) the footer scrolls with the card, so
|
|
61
|
+
the options it would cover stay readable. */
|
|
62
|
+
.sq-host.sq-kb-up .sq-foot { position: static; }
|
|
63
|
+
/* The Mac not answering: Submit, Allow and Deny wait, and say why (questions.js offline). */
|
|
64
|
+
.sq-offline { margin: 8px 0 0; font-size: 13px; color: var(--sub); }
|
|
65
|
+
/* 6.99.30 (canary): folded, a waiting question is one line over the pane (it filled the first screen of every pane);
|
|
66
|
+
a tap opens the cards. From 600 px the cards show as before and the line does not. */
|
|
67
|
+
.sq-banner { display: none; }
|
|
68
|
+
@media (max-width: 599px) {
|
|
69
|
+
.sq-host .sq-banner { display: flex; align-items: center; gap: 10px; width: 100%; min-height: 44px; padding: 10px 14px; border-radius: 14px;
|
|
70
|
+
background: var(--panel); border: 1px solid color-mix(in srgb, var(--hot) 45%, var(--panel-border)); color: var(--ink); font: 600 15px/1.3 var(--body); text-align: left; }
|
|
71
|
+
.sq-host .sq-banner-text { flex: 1; min-width: 0; }
|
|
72
|
+
.sq-host .sq-banner-act { flex: none; color: var(--accent); }
|
|
73
|
+
.sq-host .sq-cards:not(.open) { display: none; }
|
|
74
|
+
}
|
|
75
|
+
.sq-cards { display: grid; gap: 12px; }
|
|
60
76
|
.sq-progress { margin: 0 0 8px; font: 500 13.5px/1.35 var(--body); color: var(--sub); }
|
|
61
77
|
.sq-progress[data-alert] { color: var(--hot); font-weight: 600; }
|
|
62
78
|
.sq-keys { display: none; margin-top: 10px; }
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
lastPollAt: 0 // when the last poll went out (Date.now)
|
|
51
51
|
};
|
|
52
52
|
var DEFAULT_POLL_MS = 10000;
|
|
53
|
+
var POLL_TIMEOUT_MS = 15000;
|
|
53
54
|
// 6.99.27 (Miles 11/07 09:29, "a big delay to see the questions on the homepage"): while this tab is visible on
|
|
54
55
|
// Today it reads every 3 s, whatever the server's idle pace (10 s). Before, a question parked just after an idle read
|
|
55
56
|
// waited up to 10 s plus the round trip for the next one. Hidden, the page reads nothing (visibilitychange below).
|
|
@@ -141,18 +142,37 @@
|
|
|
141
142
|
Q.inflight = true; Q.again = false; Q.lastPollAt = Date.now();
|
|
142
143
|
capForPoll().then(function (cap) {
|
|
143
144
|
if (!usable(cap)) { Q.again = false; landed(); clearCards(); schedule(60000); return; }
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
145
|
+
// 6.99.32 (QA 6.99.30 canary W1): a Mac that hangs (asleep, a stale Tailscale route) never answered and never
|
|
146
|
+
// refused, so this read stayed in flight forever: no more polls, and the Mac never marked down. It gives up after
|
|
147
|
+
// POLL_TIMEOUT_MS, says the Mac is not answering, and polls again. The limit covers the body too (QA 6.99.32
|
|
148
|
+
// Skeptic W-1): a Mac that sleeps between the headers and the body otherwise held the poll for good.
|
|
149
|
+
var ctrl = typeof AbortController === 'function' ? new AbortController() : null;
|
|
150
|
+
var limit = ctrl ? setTimeout(function () { ctrl.abort(); }, POLL_TIMEOUT_MS) : null;
|
|
151
|
+
return fetch(pollUrl(), { headers: { 'X-COS-Web': '1' }, credentials: 'same-origin', cache: 'no-store', signal: ctrl ? ctrl.signal : undefined }).then(function (res) {
|
|
152
|
+
// QA 6.99.30 (Skeptic): this 3 s read is what Sessions and Meetings pages keep running, so it tells app.js the Mac
|
|
153
|
+
// answered (any status) or did not (below); otherwise a "down" mark stranded Submit, Allow and Deny for minutes.
|
|
154
|
+
if (W.noteServer) W.noteServer(true);
|
|
155
|
+
if (res.status === 401) { clearTimeout(limit); Q.again = false; landed(); Q.stopped = true; clearCards(); return; }
|
|
156
|
+
if (!res.ok) { clearTimeout(limit); schedule(DEFAULT_POLL_MS); landed(); return; }
|
|
147
157
|
return res.json().then(function (d) {
|
|
158
|
+
clearTimeout(limit);
|
|
148
159
|
if (!d || d.enabled === false) { Q.again = false; landed(); clearCards(); schedule(60000); return; }
|
|
149
160
|
// Painted as soon as the read lands: take() draws the cards and repaints Today's Needs you and the pet.
|
|
150
161
|
take(Array.isArray(d.items) ? d.items : [], Array.isArray(d.settled) ? d.settled : []);
|
|
151
162
|
var every = Number(d.pollIntervalMs) > 0 ? Number(d.pollIntervalMs) : DEFAULT_POLL_MS;
|
|
152
163
|
schedule(nextWait(every));
|
|
153
164
|
landed();
|
|
165
|
+
}, function () {
|
|
166
|
+
// The body never finished (cut off, or the limit fired mid-body). The Mac did answer the headers, so it is not
|
|
167
|
+
// marked down; the poll is freed and goes again.
|
|
168
|
+
clearTimeout(limit); schedule(DEFAULT_POLL_MS); landed();
|
|
154
169
|
});
|
|
155
|
-
}, function () {
|
|
170
|
+
}, function () {
|
|
171
|
+
// No answer at all: refused, unreachable, or the limit fired before the headers (the only abort this read has).
|
|
172
|
+
clearTimeout(limit);
|
|
173
|
+
if (W.noteServer) W.noteServer(false);
|
|
174
|
+
schedule(DEFAULT_POLL_MS); landed();
|
|
175
|
+
});
|
|
156
176
|
}).catch(function () { schedule(DEFAULT_POLL_MS); landed(); });
|
|
157
177
|
}
|
|
158
178
|
// Read now, unless a read went out under NUDGE_GAP_MS ago (then at the end of that gap) or one is on the wire (then
|
|
@@ -190,8 +210,9 @@
|
|
|
190
210
|
var ids = {}; pending.forEach(function (i) { ids[i.id] = 1; });
|
|
191
211
|
Object.keys(Q.picks).forEach(function (id) { if (!ids[id]) { delete Q.picks[id]; delete Q.armed[id]; delete Q.notes[id]; delete Q.answerIds[id]; delete Q.missing[id]; } });
|
|
192
212
|
pending.forEach(function (i) {
|
|
193
|
-
if (!Q.picks[i.id]) Q.picks[i.id] = (i.questions || []).map(function () { return { labels: [], other: '' }; });
|
|
213
|
+
if (!Q.picks[i.id]) Q.picks[i.id] = keptPicks(i) || (i.questions || []).map(function () { return { labels: [], other: '' }; });
|
|
194
214
|
});
|
|
215
|
+
forgetPicks(ids);
|
|
195
216
|
Q.items = pending;
|
|
196
217
|
// A card that is already drawn is never redrawn by a poll: a half-typed Other answer stays as it is.
|
|
197
218
|
if (before !== after) { render(); changed(); }
|
|
@@ -295,8 +316,9 @@
|
|
|
295
316
|
// always answers: it never greys out for a missing answer, it goes to it.
|
|
296
317
|
'<div class="sq-foot" data-sq-foot="' + esc(item.id) + '">' +
|
|
297
318
|
'<p class="sq-progress" data-sq-progress="' + esc(item.id) + '" role="status"' + (Q.missing[item.id] != null && !complete(item) ? ' data-alert="1"' : '') + '>' + esc(progressText(item)) + '</p>' +
|
|
298
|
-
'<div class="sq-acts"><button type="button" class="cx-btn primary sq-send" data-sq="send" data-sq-id="' + esc(item.id) + '"' + (sending ? ' disabled' : '') + '>' + sendText(item.id) + '</button>' +
|
|
299
|
-
'<button type="button" class="link-btn sq-leave" data-sq="leave" data-sq-id="' + esc(item.id) + '"' + (sending ? ' disabled' : '') + '>Leave for the Mac</button></div>' +
|
|
319
|
+
'<div class="sq-acts"><button type="button" class="cx-btn primary sq-send" data-sq="send" data-sq-id="' + esc(item.id) + '"' + (sending || offline() ? ' disabled' : '') + '>' + sendText(item.id) + '</button>' +
|
|
320
|
+
'<button type="button" class="link-btn sq-leave" data-sq="leave" data-sq-id="' + esc(item.id) + '"' + (sending || offline() ? ' disabled' : '') + '>Leave for the Mac</button></div>' +
|
|
321
|
+
(offline() ? '<p class="sq-offline" role="status">' + esc(OFFLINE_LINE) + '</p>' : '') +
|
|
300
322
|
noteHtml(item) + '</div></article>';
|
|
301
323
|
}
|
|
302
324
|
function approvalCard(item) {
|
|
@@ -307,9 +329,10 @@
|
|
|
307
329
|
'<header class="sq-head"><h2 class="sq-title">' + esc(engine(item)) + ' wants to use ' + esc(a.tool || item.tool || 'a tool') + '</h2><span class="sq-secs" data-sq-secs="' + esc(item.id) + '">' + esc(timeLeft(secondsLeft(item))) + '</span></header>' +
|
|
308
330
|
'<pre class="sq-command">' + esc(a.summary || 'No details were sent with this request.') + '</pre>' +
|
|
309
331
|
(a.detail && a.detail !== a.summary ? '<details class="sq-details"><summary>Full command</summary><pre class="sq-command">' + esc(a.detail) + '</pre></details>' : '') +
|
|
310
|
-
'<div class="sq-acts"><button type="button" class="cx-btn primary" data-sq="allow" data-sq-id="' + esc(item.id) + '"' + (sending ? ' disabled' : '') + '>' + (armed === 'allow' ? 'Tap again to allow' : 'Allow once') + '</button>' +
|
|
311
|
-
'<button type="button" class="cx-btn sq-deny" data-sq="deny" data-sq-id="' + esc(item.id) + '"' + (sending ? ' disabled' : '') + '>' + (armed === 'deny' ? 'Tap again to deny' : 'Deny') + '</button>' +
|
|
312
|
-
'<button type="button" class="link-btn sq-leave" data-sq="leave" data-sq-id="' + esc(item.id) + '"' + (sending ? ' disabled' : '') + '>Leave for the Mac</button></div>' +
|
|
332
|
+
'<div class="sq-acts"><button type="button" class="cx-btn primary" data-sq="allow" data-sq-id="' + esc(item.id) + '"' + (sending || offline() ? ' disabled' : '') + '>' + (armed === 'allow' ? 'Tap again to allow' : 'Allow once') + '</button>' +
|
|
333
|
+
'<button type="button" class="cx-btn sq-deny" data-sq="deny" data-sq-id="' + esc(item.id) + '"' + (sending || offline() ? ' disabled' : '') + '>' + (armed === 'deny' ? 'Tap again to deny' : 'Deny') + '</button>' +
|
|
334
|
+
'<button type="button" class="link-btn sq-leave" data-sq="leave" data-sq-id="' + esc(item.id) + '"' + (sending || offline() ? ' disabled' : '') + '>Leave for the Mac</button></div>' +
|
|
335
|
+
(offline() ? '<p class="sq-offline" role="status">' + esc(OFFLINE_LINE) + '</p>' : '') +
|
|
313
336
|
noteHtml(item) + '</article>';
|
|
314
337
|
}
|
|
315
338
|
// A question or an approval the Mac's own prompt waits on: what it asks when this page saw it held, and where to answer.
|
|
@@ -356,7 +379,11 @@
|
|
|
356
379
|
var active = document.activeElement;
|
|
357
380
|
var focus = active && active.hasAttribute && active.hasAttribute('data-sq-other') && m.el.contains && m.el.contains(active) ? { id: active.getAttribute('data-sq-id'), q: active.getAttribute('data-q'), start: active.selectionStart, end: active.selectionEnd } : null;
|
|
358
381
|
var list = itemsFor(m.sessionId), mac = atMacFor(m.sessionId);
|
|
359
|
-
|
|
382
|
+
var cards = list.map(function (i) { return i.kind === 'approval' ? approvalCard(i) : questionCard(i, m.scope); }).join('') + mac.map(macCard).join('');
|
|
383
|
+
// The page's own host (over every pane): folded it is one line until tapped (questions.css .sq-banner, 6.99.30).
|
|
384
|
+
if (m.el === host && list.length) cards = bannerHtml(list) + '<div class="sq-cards' + (Q.bannerOpen ? ' open' : '') + '" id="sq-cards">' + cards + '</div>';
|
|
385
|
+
else if (m.el === host) Q.bannerOpen = false;
|
|
386
|
+
m.el.innerHTML = cards;
|
|
360
387
|
m.el.hidden = !list.length && !mac.length;
|
|
361
388
|
if (focus) {
|
|
362
389
|
var box = m.el.querySelector('[data-sq-other][data-sq-id="' + focus.id + '"][data-q="' + focus.q + '"]');
|
|
@@ -364,6 +391,17 @@
|
|
|
364
391
|
}
|
|
365
392
|
if (m.onChange) { try { m.onChange(list.length, mac.length); } catch (_e) {} }
|
|
366
393
|
}
|
|
394
|
+
// 6.99.30 (canary): "1 question for you. Answer", one line over the pane on a folded phone; a tap opens the cards.
|
|
395
|
+
function bannerHtml(list) {
|
|
396
|
+
var qs = list.filter(function (i) { return i.kind !== 'approval'; }).length, n = list.length;
|
|
397
|
+
var noun = qs === n ? (n === 1 ? 'question' : 'questions') : qs === 0 ? (n === 1 ? 'approval' : 'approvals') : (n === 1 ? 'request' : 'requests');
|
|
398
|
+
return '<button type="button" class="sq-banner" data-sq="banner" aria-expanded="' + (Q.bannerOpen ? 'true' : 'false') + '" aria-controls="sq-cards">' +
|
|
399
|
+
'<span class="sq-banner-text">' + esc(n + ' ' + noun + ' for you.') + '</span><span class="sq-banner-act">' + (Q.bannerOpen ? 'Hide' : 'Answer') + '</span></button>';
|
|
400
|
+
}
|
|
401
|
+
// 6.99.30 (canary): with the Mac not answering (app.js noteServer), Submit, Allow and Deny wait and say why, instead of
|
|
402
|
+
// offering a tap that cannot land.
|
|
403
|
+
function offline() { return !!(W.serverUp && W.serverUp() === false); }
|
|
404
|
+
var OFFLINE_LINE = 'Your Mac is not answering. You can answer here once it is back.';
|
|
367
405
|
function render() {
|
|
368
406
|
hostsOf().forEach(drawInto);
|
|
369
407
|
clearInterval(Q.tick);
|
|
@@ -384,7 +422,7 @@
|
|
|
384
422
|
}
|
|
385
423
|
function patchOne(card, id, item) {
|
|
386
424
|
var send = card.querySelector('[data-sq="send"]');
|
|
387
|
-
if (send) { send.disabled = !!Q.sending[id]; send.textContent = sendText(id); }
|
|
425
|
+
if (send) { send.disabled = !!Q.sending[id] || offline(); send.textContent = sendText(id); }
|
|
388
426
|
var progress = card.querySelector('[data-sq-progress]');
|
|
389
427
|
if (progress && item.kind === 'question') {
|
|
390
428
|
progress.textContent = progressText(item);
|
|
@@ -487,6 +525,7 @@
|
|
|
487
525
|
function onClick(e) {
|
|
488
526
|
var b = e.target.closest('[data-sq]');
|
|
489
527
|
if (!b || b.disabled) return;
|
|
528
|
+
if (b.getAttribute('data-sq') === 'banner') { Q.bannerOpen = !Q.bannerOpen; drawInto(hostsOf()[0]); return; }
|
|
490
529
|
var id = b.getAttribute('data-sq-id'), what = b.getAttribute('data-sq'), item = itemOf(id);
|
|
491
530
|
if (!item) return;
|
|
492
531
|
var card = b.closest ? b.closest('.sq-card') : null;
|
|
@@ -551,9 +590,36 @@
|
|
|
551
590
|
// A change after Submit was armed takes the arm back: review, then submit.
|
|
552
591
|
Q.armed[id] = '';
|
|
553
592
|
if (Q.missing[id] != null) delete Q.missing[id];
|
|
593
|
+
keepPicks(id);
|
|
554
594
|
patchCard(id);
|
|
555
595
|
return true;
|
|
556
596
|
}
|
|
597
|
+
// 6.99.30 (QA): a reload mid-answer keeps the picks and the typed Other words, per question id, for this tab only
|
|
598
|
+
// (sessionStorage). Restored only onto the same question (the same number of parts, and labels it still offers);
|
|
599
|
+
// dropped once the question is answered or gone.
|
|
600
|
+
var PICKS_KEY = 'cos.web.questions.picks';
|
|
601
|
+
function picksStore() { try { var v = JSON.parse(window.sessionStorage.getItem(PICKS_KEY) || '{}'); return v && typeof v === 'object' ? v : {}; } catch (_e) { return {}; } }
|
|
602
|
+
function writePicks(all) { try { window.sessionStorage.setItem(PICKS_KEY, JSON.stringify(all)); } catch (_e) {} }
|
|
603
|
+
function keepPicks(id) {
|
|
604
|
+
var all = picksStore(), p = Q.picks[id];
|
|
605
|
+
if (!p) return;
|
|
606
|
+
all[id] = p.map(function (x) { return { labels: x.labels.slice(0, 20), other: String(x.other || '').slice(0, 2000) }; });
|
|
607
|
+
writePicks(all);
|
|
608
|
+
}
|
|
609
|
+
function keptPicks(item) {
|
|
610
|
+
var kept = picksStore()[item.id], qs = item.questions || [];
|
|
611
|
+
if (!Array.isArray(kept) || kept.length !== qs.length) return null;
|
|
612
|
+
return qs.map(function (q, qi) {
|
|
613
|
+
var k = kept[qi] || {}, labels = (q.options || []).map(function (o) { return o.label; });
|
|
614
|
+
return { labels: (Array.isArray(k.labels) ? k.labels : []).filter(function (l) { return labels.indexOf(l) >= 0; }), other: typeof k.other === 'string' ? k.other : '' };
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
function forgetPicks(live) {
|
|
618
|
+
var all = picksStore(), gone = Object.keys(all).filter(function (id) { return !live[id]; });
|
|
619
|
+
if (!gone.length) return;
|
|
620
|
+
gone.forEach(function (id) { delete all[id]; });
|
|
621
|
+
writePicks(all);
|
|
622
|
+
}
|
|
557
623
|
// Keys 1 to 9 while a question has focus, as in the terminal: the option with that number, or the last number to type
|
|
558
624
|
// your own. Never while typing in Other, and never with a modifier held.
|
|
559
625
|
function onKey(e) {
|
|
@@ -584,6 +650,10 @@
|
|
|
584
650
|
var id = t.getAttribute('data-sq-id'), item = itemOf(id); if (!item) return;
|
|
585
651
|
var qi = Number(t.getAttribute('data-q')), q = item.questions[qi], p = Q.picks[id][qi];
|
|
586
652
|
p.other = t.value;
|
|
653
|
+
// 6.99.30 (QA): the same card drawn on Today and in the sheet keeps one answer: the words go into every copy.
|
|
654
|
+
hostsOf().forEach(function (m) {
|
|
655
|
+
m.el.querySelectorAll('[data-sq-other][data-sq-id="' + id + '"][data-q="' + qi + '"]').forEach(function (box) { if (box !== t) box.value = t.value; });
|
|
656
|
+
});
|
|
587
657
|
// Single choice: typing your own answer clears the picked option, as on the phone.
|
|
588
658
|
if (!q.multiSelect && t.value.trim() && p.labels.length) {
|
|
589
659
|
p.labels = [];
|
|
@@ -591,6 +661,7 @@
|
|
|
591
661
|
}
|
|
592
662
|
Q.armed[id] = '';
|
|
593
663
|
if (Q.missing[id] != null) delete Q.missing[id];
|
|
664
|
+
keepPicks(id);
|
|
594
665
|
patchCard(id);
|
|
595
666
|
}
|
|
596
667
|
function listen(el) { el.addEventListener('click', onClick); el.addEventListener('change', onChange); el.addEventListener('input', onInput); el.addEventListener('keydown', onKey); }
|
|
@@ -604,20 +675,30 @@
|
|
|
604
675
|
return { count: function () { return itemsFor(m.sessionId).length; }, unmount: function () { mounts = mounts.filter(function (x) { return x !== m; }); } };
|
|
605
676
|
}
|
|
606
677
|
|
|
607
|
-
// QA 6.99.27 (User 2): Today's own card (#sq-host) has a sticky footer, and
|
|
608
|
-
// viewport, so with Other focused the footer sat under it.
|
|
609
|
-
//
|
|
610
|
-
//
|
|
678
|
+
// QA 6.99.27 (User 2): Today's own card (#sq-host) has a sticky footer, and the keyboard covers the bottom of the
|
|
679
|
+
// layout viewport, so with Other focused the footer sat under it. What covers the bottom goes on the host as --sq-kb
|
|
680
|
+
// and questions.css lifts the footer by it. 6.99.30: read the one way Control Web reads the visible viewport (app.js
|
|
681
|
+
// COSWeb.viewportFit, which Today's sheet uses too): the layout viewport under the visible box, in layout px, so a
|
|
682
|
+
// pinch zoom alone lifts nothing and a page the browser panned to the field lifts by what is really under it.
|
|
683
|
+
// The card in Today's sheet (.ta-q) needs none of this: the sheet itself is the visible box (today-actions.js fit).
|
|
611
684
|
function keyboardInset() {
|
|
612
|
-
var
|
|
613
|
-
|
|
614
|
-
var inset = Math.round((window.innerHeight || 0) - (vv.height + vv.offsetTop));
|
|
615
|
-
return inset > 0 ? inset : 0;
|
|
685
|
+
var v = W.viewportFit ? W.viewportFit() : null;
|
|
686
|
+
return v && v.vv ? v.below : 0;
|
|
616
687
|
}
|
|
617
688
|
function fitKeyboard() {
|
|
618
|
-
|
|
689
|
+
var inset = keyboardInset();
|
|
690
|
+
if (host.style && host.style.setProperty) host.style.setProperty('--sq-kb', inset + 'px');
|
|
691
|
+
// A keyboard up (120 px or more, or any strip under a field of the card): the footer stops sticking (questions.css).
|
|
692
|
+
var v = W.viewportFit ? W.viewportFit() : null, a = document.activeElement;
|
|
693
|
+
var typing = !!(a && host.contains && host.contains(a) && a.hasAttribute && a.hasAttribute('data-sq-other'));
|
|
694
|
+
if (host.classList) host.classList.toggle('sq-kb-up', !!(v && v.vv && (v.inset >= 120 || (typing && v.inset > 0))));
|
|
619
695
|
}
|
|
620
696
|
if (window.visualViewport) { window.visualViewport.addEventListener('resize', fitKeyboard); window.visualViewport.addEventListener('scroll', fitKeyboard); }
|
|
697
|
+
window.addEventListener('resize', fitKeyboard); window.addEventListener('orientationchange', fitKeyboard);
|
|
698
|
+
window.addEventListener('pageshow', fitKeyboard);
|
|
699
|
+
host.addEventListener('focusin', fitKeyboard); host.addEventListener('focusout', function () { setTimeout(fitKeyboard, 0); });
|
|
700
|
+
// The Mac going away or coming back redraws the cards at once (their buttons wait while it is away).
|
|
701
|
+
if (W.onServer) W.onServer(function () { render(); });
|
|
621
702
|
|
|
622
703
|
// Hidden: no reads at all (a phone in a pocket spends nothing). Visible again: one read at once (within the 1 s gap).
|
|
623
704
|
document.addEventListener('visibilitychange', function () { if (document.hidden) stop(); else if (signedIn()) start(); });
|
|
@@ -7,11 +7,18 @@
|
|
|
7
7
|
--pv-claude: rgb(199, 115, 56);
|
|
8
8
|
--pv-codex: rgb(26, 140, 122);
|
|
9
9
|
--pv-cursor: rgb(107, 97, 219);
|
|
10
|
+
/* 6.99.30 (design QA, light mode): the provider badges' 9 px words, about 25% darker than the glyph colour, so they
|
|
11
|
+
read at 4.5:1 or better on their tint. Dark mode keeps the glyph colour (below). */
|
|
12
|
+
--pv-claude-ink: rgb(143, 83, 40);
|
|
13
|
+
--pv-codex-ink: rgb(20, 105, 92);
|
|
14
|
+
--pv-cursor-ink: rgb(80, 73, 164);
|
|
10
15
|
--run-green: rgb(56, 145, 99);
|
|
11
16
|
}
|
|
12
|
-
:root[data-theme='dark'] .sx { --pv-claude: rgb(226, 146, 92); --pv-codex: rgb(72, 190, 168); --pv-cursor: rgb(158, 150, 245); --run-green: rgb(108, 207, 143);
|
|
17
|
+
:root[data-theme='dark'] .sx { --pv-claude: rgb(226, 146, 92); --pv-codex: rgb(72, 190, 168); --pv-cursor: rgb(158, 150, 245); --run-green: rgb(108, 207, 143);
|
|
18
|
+
--pv-claude-ink: var(--pv-claude); --pv-codex-ink: var(--pv-codex); --pv-cursor-ink: var(--pv-cursor); }
|
|
13
19
|
@media (prefers-color-scheme: dark) {
|
|
14
|
-
:root:not([data-theme='light']) .sx { --pv-claude: rgb(226, 146, 92); --pv-codex: rgb(72, 190, 168); --pv-cursor: rgb(158, 150, 245); --run-green: rgb(108, 207, 143);
|
|
20
|
+
:root:not([data-theme='light']) .sx { --pv-claude: rgb(226, 146, 92); --pv-codex: rgb(72, 190, 168); --pv-cursor: rgb(158, 150, 245); --run-green: rgb(108, 207, 143);
|
|
21
|
+
--pv-claude-ink: var(--pv-claude); --pv-codex-ink: var(--pv-codex); --pv-cursor-ink: var(--pv-cursor); }
|
|
15
22
|
}
|
|
16
23
|
|
|
17
24
|
/* ── header, views, clock ─────────────────────────────── */
|
|
@@ -41,7 +48,7 @@
|
|
|
41
48
|
@media (max-width: 599px) {
|
|
42
49
|
.sx-search { order: 0; flex: 1 1 auto; }
|
|
43
50
|
.sx-search .cx-clear { min-height: 40px; padding: 0 2px; }
|
|
44
|
-
.sx-select {
|
|
51
|
+
.sx-select { background-position: calc(100% - 16px) 19px, calc(100% - 11px) 19px; }
|
|
45
52
|
#sessions-recency { max-width: 128px; }
|
|
46
53
|
.sx-ufilters .sx-select { flex: 1 1 150px; max-width: none; }
|
|
47
54
|
.sx-utable .hp { display: none; }
|
|
@@ -62,12 +69,14 @@
|
|
|
62
69
|
.sx-title { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; overflow-wrap: anywhere; font: 600 15px/1.3 var(--body); color: var(--ink); }
|
|
63
70
|
.sx-snippet { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; overflow-wrap: anywhere; margin-top: 3px; font-size: 12.5px; line-height: 1.4; color: var(--sub); }
|
|
64
71
|
.sx-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px; margin-top: 5px; min-width: 0; }
|
|
65
|
-
.sx-badge { display: inline-flex; align-items: center; height: 17px; padding: 0 6px; border-radius: 9px; font: 700 9px/1 var(--mono); letter-spacing: 0.06em; color: var(--pv-claude); background: color-mix(in srgb, var(--pv-claude) 14%, transparent); white-space: nowrap; }
|
|
66
|
-
.sx-badge.codex { color: var(--pv-codex); background: color-mix(in srgb, var(--pv-codex) 14%, transparent); }
|
|
67
|
-
.sx-badge.cursor { color: var(--pv-cursor); background: color-mix(in srgb, var(--pv-cursor) 14%, transparent); }
|
|
72
|
+
.sx-badge { display: inline-flex; align-items: center; height: 17px; padding: 0 6px; border-radius: 9px; font: 700 9px/1 var(--mono); letter-spacing: 0.06em; color: var(--pv-claude-ink, var(--pv-claude)); background: color-mix(in srgb, var(--pv-claude) 14%, transparent); white-space: nowrap; }
|
|
73
|
+
.sx-badge.codex { color: var(--pv-codex-ink, var(--pv-codex)); background: color-mix(in srgb, var(--pv-codex) 14%, transparent); }
|
|
74
|
+
.sx-badge.cursor { color: var(--pv-cursor-ink, var(--pv-cursor)); background: color-mix(in srgb, var(--pv-cursor) 14%, transparent); }
|
|
68
75
|
.sx-pinned { display: inline-flex; align-items: center; height: 17px; padding: 0 6px; border-radius: 9px; font: 700 8.5px/1 var(--mono); letter-spacing: 0.06em; color: var(--sub); background: color-mix(in srgb, var(--ink) 8%, transparent); }
|
|
69
76
|
.sx-chip { font: 600 11.5px/1.3 var(--body); color: var(--sub); }
|
|
70
77
|
.sx-chip.waiting { color: var(--amber); }
|
|
78
|
+
/* 6.99.30: a queued message the server refused or let expire (app.js notSentFor). */
|
|
79
|
+
.sx-chip.refused { color: var(--hot); }
|
|
71
80
|
.sx-chip.running { color: var(--run-green); }
|
|
72
81
|
.sx-when, .sx-ws { font-size: 11.5px; line-height: 1.3; color: var(--sub); }
|
|
73
82
|
.sx-ws { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }
|
|
@@ -116,7 +125,9 @@
|
|
|
116
125
|
.sx-act-tool { flex: none; font: 400 11px/16px var(--mono); color: var(--sub); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
117
126
|
.sx-act-prose { flex: none; margin-top: 2px; font-size: 13px; line-height: 16px; color: var(--ink); white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
118
127
|
.sx-act-fallback { height: calc(var(--act-state) + var(--act-prompt) + var(--act-tail) + 8px); margin: 0; padding-top: 6px; font-size: 12.5px; line-height: 1.4; color: var(--sub); overflow: hidden; box-sizing: border-box; }
|
|
119
|
-
|
|
128
|
+
/* 6.99.32 (QA Completionist W4): below 600 px every .cx-btn is at least 44 px (app.css), so the row that holds Stop is 46 px;
|
|
129
|
+
at 38 it clipped Stop's border top and bottom. The fallback's height follows the variable. */
|
|
130
|
+
@media (max-width: 599px) { .sx-activity { --act-state: 46px; } }
|
|
120
131
|
.sx-note { font-size: 12px; color: var(--sub); margin: 0 2px 12px; }
|
|
121
132
|
|
|
122
133
|
/* YOU and ASSISTANT cards (Control: card fill, a tinted hairline; sessions tint for you, green for the reply). */
|
|
@@ -221,8 +221,7 @@
|
|
|
221
221
|
#pane-speakers .sp-mrow { grid-template-columns: 32px minmax(0, 1fr) 12px; }
|
|
222
222
|
#pane-speakers .sp-tools { width: 100%; }
|
|
223
223
|
#pane-speakers .sp-search { max-width: none; flex-basis: 100%; }
|
|
224
|
-
|
|
225
|
-
.sp-select select.field { min-height: 38px; font-size: 14px; background-position: calc(100% - 16px) 16px, calc(100% - 11px) 16px; }
|
|
224
|
+
.sp-select select.field { background-position: calc(100% - 16px) 19px, calc(100% - 11px) 19px; }
|
|
226
225
|
#pane-speakers .sp-text { min-height: 36px; font-size: 14.5px; }
|
|
227
226
|
#pane-speakers .sp-field { min-height: 40px; font-size: 16px; width: 100%; }
|
|
228
227
|
#pane-speakers .cx-switch { gap: 12px; flex-wrap: wrap; }
|
|
@@ -242,11 +241,10 @@
|
|
|
242
241
|
.sp-pplay { width: 36px; height: 36px; }
|
|
243
242
|
.sp-pplay-space { width: 36px; }
|
|
244
243
|
.sp-opt { min-height: 40px; font-size: 15px; }
|
|
245
|
-
.sp-vacts .cx-btn {
|
|
244
|
+
.sp-vacts .cx-btn { font-size: 14px; }
|
|
246
245
|
.sp-leg { min-height: 30px; }
|
|
247
246
|
.sp-racts { justify-content: flex-start; }
|
|
248
|
-
.sp-
|
|
249
|
-
.sp-policy .field { min-height: 38px; font-size: 14px; background-position: calc(100% - 16px) 16px, calc(100% - 11px) 16px; }
|
|
247
|
+
.sp-policy .field { background-position: calc(100% - 16px) 19px, calc(100% - 11px) 19px; }
|
|
250
248
|
}
|
|
251
249
|
@media (min-width: 600px) {
|
|
252
250
|
.sp-pills-in { display: none !important; }
|
|
@@ -107,7 +107,8 @@
|
|
|
107
107
|
var off = new Error(OFFLINE); off.code = 'offline'; throw off;
|
|
108
108
|
});
|
|
109
109
|
}
|
|
110
|
-
|
|
110
|
+
// The one sentence Control Web says for a request that never reached the Mac (app.js COSWeb.unreachableCopy, 6.99.30).
|
|
111
|
+
var OFFLINE = W.unreachableCopy || 'Could not reach COS on your Mac. Check that it is awake and on Tailscale, then try again.';
|
|
111
112
|
// The server answered, but not with JSON (a proxy page, a crash): say that, never that it stopped.
|
|
112
113
|
function unreadable(r) { return new Error('The server answered ' + r.status + ' without a readable reply.'); }
|
|
113
114
|
// A 401 hands the page to the shell's sign-in and stops every timer here, so nothing polls a signed-out session.
|
|
@@ -264,7 +265,9 @@
|
|
|
264
265
|
namedVoices: vr ? int(vr.namedVoices) || 0 : null, humanTouched: vr ? vr.humanTouched === true : null
|
|
265
266
|
};
|
|
266
267
|
}
|
|
267
|
-
|
|
268
|
+
// A row's day in plain words ("Today", "Nov 7"), never the ISO date (6.99.30).
|
|
269
|
+
function day(v) { return W.plainDay ? W.plainDay(v) : v; }
|
|
270
|
+
function dateLine(m) { return [day(m.date), m.time ? clock12(m.time) : '', m.duration].filter(Boolean).join(' · '); }
|
|
268
271
|
function countsSummary(m) {
|
|
269
272
|
var parts = [];
|
|
270
273
|
if (m.topicCount > 0) parts.push(m.topicCount + ' topic' + s(m.topicCount));
|
|
@@ -579,7 +582,7 @@
|
|
|
579
582
|
var message = str(r.body.message) || str(r.body.error) || (r.status === 200 ? 'Naming receipt ready' : 'Naming refused (' + r.status + ')');
|
|
580
583
|
return { message: message, receipt: parseReceipt(r.body, r.status) };
|
|
581
584
|
}, function (err) {
|
|
582
|
-
if (err && err.code === 'offline') throw new Error(
|
|
585
|
+
if (err && err.code === 'offline') throw new Error('Could not reach COS on your Mac. Check that it is awake and on Tailscale, then refresh to review any interrupted naming before retrying.');
|
|
583
586
|
throw err;
|
|
584
587
|
});
|
|
585
588
|
}
|
|
@@ -2049,7 +2052,7 @@
|
|
|
2049
2052
|
out += '<div class="cx-list sp-list">' + p.appearances.map(function (a) {
|
|
2050
2053
|
return '<button type="button" class="cx-row sp-arow" data-sp="open-appearance" data-sid="' + esc(a.sessionId) + '">' +
|
|
2051
2054
|
'<span class="cx-row-glyph' + (a.needsReview ? ' sp-amber' : '') + '">' + icon('speakers') + '</span><span class="cx-row-body">' +
|
|
2052
|
-
'<span class="cx-row-title strong sp-one">' + esc(a.title) + '</span><span class="sp-mono">' + esc(a.date
|
|
2055
|
+
'<span class="cx-row-title strong sp-one">' + esc(a.title) + '</span><span class="sp-mono">' + esc([day(a.date), a.segments + ' segments', formatDuration(a.speakingMs)].filter(Boolean).join(' · ')) + '</span></span>' +
|
|
2053
2056
|
'<span class="sp-match"><b>' + esc(a.observedMatch == null ? '–' : pct(a.observedMatch)) + '</b><small>' + (a.confirmedByHuman ? 'human confirmed' : a.needsReview ? 'review' : 'observed match') + '</small></span>' +
|
|
2054
2057
|
'<span class="chev"></span></button>';
|
|
2055
2058
|
}).join('') + '</div>';
|
|
@@ -118,14 +118,15 @@
|
|
|
118
118
|
'<span class="cx-row-glyph"><svg class="icon"><use href="#i-threads"/></svg></span><span class="cx-row-body">' +
|
|
119
119
|
'<span class="cx-row-title strong">' + esc(t.name || '(untitled)') + '</span>' +
|
|
120
120
|
(subtitle(t) ? '<span class="cx-row-sub">' + esc(subtitle(t)) + '</span>' : '') +
|
|
121
|
-
|
|
121
|
+
// 6.99.30 (design QA): no raw id ("t1") on the row; the thread's own page still shows it, to copy.
|
|
122
|
+
'</span><span class="chev"></span></button>';
|
|
122
123
|
}
|
|
123
124
|
function hitRow(h) {
|
|
124
125
|
return '<button type="button" class="cx-row" data-th="open" data-id="' + esc(h.id) + '">' +
|
|
125
126
|
'<span class="cx-row-glyph"><svg class="icon"><use href="#i-threads"/></svg></span><span class="cx-row-body">' +
|
|
126
127
|
'<span class="cx-row-title strong">' + esc(h.title || h.name || '(untitled)') + '</span>' +
|
|
127
128
|
(h.snippet ? '<span class="cx-row-sub">' + esc(h.snippet) + '</span>' : '') +
|
|
128
|
-
'<span class="cx-meta" style="margin-top:4px"><span class="cx-
|
|
129
|
+
'<span class="cx-meta" style="margin-top:4px"><span class="cx-cap">' + esc(matchLabel(h.match)) + '</span></span>' +
|
|
129
130
|
'</span><span class="chev"></span></button>';
|
|
130
131
|
}
|
|
131
132
|
function list() {
|
|
@@ -12,28 +12,35 @@
|
|
|
12
12
|
|
|
13
13
|
/* The page behind does not scroll. Only the root: hidden on body too would make body a scroll container and unstick the rail. */
|
|
14
14
|
html.ta-open { overflow: hidden; }
|
|
15
|
+
/* 6.99.30 (design QA): Today's own question card is drawn in the sheet; the one on the page under it waits. */
|
|
16
|
+
html.ta-open #sq-host { display: none; }
|
|
15
17
|
|
|
16
18
|
.ta-layer {
|
|
17
|
-
/* 6.99.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
the
|
|
21
|
-
|
|
19
|
+
/* 6.99.30: the layer is the VISIBLE part of the page, keyboard up or down, in any browser: top and height from
|
|
20
|
+
visualViewport (today-actions.js applyFit, app.js viewportFit), the dynamic viewport where there is none. Whatever
|
|
21
|
+
covers the bottom (a keyboard, its strip, Comet's or any browser's bottom bar) stays below the reply box and Send.
|
|
22
|
+
Only the backdrop and the sheet's colour reach on down to the physical bottom (::before and .ta-sheet::after,
|
|
23
|
+
--ta-below plus the bleed), so no band of Today shows (IMG_0503). The home indicator is cleared once, by the padding
|
|
24
|
+
inside the sheet (.ta-foot, or the sheet when there is no reply box), never by the layer or the sheet. */
|
|
25
|
+
position: fixed; left: 0; right: 0; z-index: 45;
|
|
26
|
+
top: var(--ta-vt, 0px); height: var(--ta-vh, 100vh);
|
|
22
27
|
--ta-bleed: 120px;
|
|
23
28
|
--run-green: rgb(56, 145, 99);
|
|
24
29
|
}
|
|
25
|
-
/*
|
|
26
|
-
|
|
30
|
+
/* Before the first reading (or with script failing), the dynamic viewport where the browser has it. A var() fallback
|
|
31
|
+
the browser cannot parse would void the whole height, so the dvh one sits behind @supports. */
|
|
32
|
+
@supports (height: 100dvh) { .ta-layer { height: var(--ta-vh, 100dvh); } }
|
|
27
33
|
:root[data-theme='dark'] .ta-layer { --run-green: rgb(108, 207, 143); }
|
|
28
34
|
@media (prefers-color-scheme: dark) { :root:not([data-theme='light']) .ta-layer { --run-green: rgb(108, 207, 143); } }
|
|
29
35
|
.ta-layer[hidden] { display: none; }
|
|
30
36
|
.ta-scrim { position: absolute; inset: 0; background: rgba(23, 18, 14, 0.45); touch-action: none; animation: fade 160ms ease; }
|
|
31
|
-
/*
|
|
32
|
-
|
|
37
|
+
/* Below the visible part, down to the physical bottom and past it: whatever lies there (a browser's bar the page shows
|
|
38
|
+
through, a keyboard, a fixed viewport that stops short) shows the dim and the sheet's own colour, never Today.
|
|
39
|
+
--ta-below is the layout viewport under the layer; the bleed covers a bottom edge reported short.
|
|
33
40
|
QA 6.99.29: the dim's bleed is the LAYER's own pseudo-element, never the scrim, so it is visual only: a tap on that
|
|
34
41
|
band lands on the layer (no data-ta, so nothing happens) instead of closing the overlay or reaching Today below. */
|
|
35
|
-
.ta-layer
|
|
36
|
-
.ta-layer
|
|
42
|
+
.ta-layer::before { content: ''; position: absolute; left: 0; right: 0; top: 100%; height: calc(var(--ta-below, 0px) + var(--ta-bleed)); background: rgba(23, 18, 14, 0.45); touch-action: none; animation: fade 160ms ease; }
|
|
43
|
+
.ta-layer .ta-sheet::after { content: ''; position: absolute; left: 0; right: 0; top: 100%; height: calc(var(--ta-below, 0px) + var(--ta-bleed)); background: var(--page); pointer-events: none; }
|
|
37
44
|
|
|
38
45
|
.ta-sheet {
|
|
39
46
|
position: absolute; left: 0; right: 0; bottom: 0;
|
|
@@ -62,9 +69,9 @@ html.ta-open { overflow: hidden; }
|
|
|
62
69
|
.ta-foot .status-line:empty { display: none; }
|
|
63
70
|
.ta-foot .sx-box, .ta-foot .sx-park { margin: 0 0 10px; }
|
|
64
71
|
/* No reply box (the session list, a question answered in its card): the sheet itself keeps the home indicator's strip,
|
|
65
|
-
so the end of the list and the question card's footer stop above it and nothing scrolls under it. With
|
|
66
|
-
up
|
|
67
|
-
.ta-layer:not(.ta-kb) .ta-sheet:has(> .ta-foot:empty), .ta-layer:not(.ta-kb) .ta-sheet:has(> .ta-foot[hidden]) { padding-bottom: var(--safe-bottom); }
|
|
72
|
+
so the end of the list and the question card's footer stop above it and nothing scrolls under it. With a keyboard or
|
|
73
|
+
its strip up, that covers the strip. */
|
|
74
|
+
.ta-layer:not(.ta-kb):not(.ta-kb-strip) .ta-sheet:has(> .ta-foot:empty), .ta-layer:not(.ta-kb):not(.ta-kb-strip) .ta-sheet:has(> .ta-foot[hidden]) { padding-bottom: var(--safe-bottom); }
|
|
68
75
|
|
|
69
76
|
.ta-sub { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px; margin: 0 0 8px; color: var(--sub); font-size: 14.5px; line-height: 1.4; }
|
|
70
77
|
.ta-note { margin: 6px 0 0; color: var(--hot); font-size: 13.5px; line-height: 1.4; }
|
|
@@ -126,9 +133,9 @@ html.ta-open { overflow: hidden; }
|
|
|
126
133
|
.ta-foot { padding-bottom: calc(var(--safe-bottom) + 14px); }
|
|
127
134
|
}
|
|
128
135
|
/* 6.99.29 (Miles's unfolded Duo, 11/07): the on-screen keyboard is up, at any width (today-actions.js applyFit marks the
|
|
129
|
-
layer ta-kb when
|
|
130
|
-
|
|
131
|
-
|
|
136
|
+
layer ta-kb when 120 px or more of the page is covered at the bottom). The sheet fills what is visible, the header
|
|
137
|
+
folds to one compact row, the reply box stays pinned at the bottom just above the keyboard, and the conversation
|
|
138
|
+
takes the rest and scrolls. The keyboard covers the home indicator, so the foot drops that inset. */
|
|
132
139
|
.ta-layer.ta-kb .ta-sheet { top: 0; bottom: 0; max-height: none; overflow: hidden; }
|
|
133
140
|
.ta-layer.ta-kb .ta-grip { display: none; }
|
|
134
141
|
.ta-layer.ta-kb .ta-head { align-items: center; padding-top: calc(var(--safe-top) + 6px); padding-bottom: 4px; }
|
|
@@ -139,6 +146,9 @@ html.ta-open { overflow: hidden; }
|
|
|
139
146
|
.ta-layer.ta-kb .ta-body { padding-bottom: 8px; }
|
|
140
147
|
.ta-layer.ta-kb .ta-foot { padding-top: 8px; padding-bottom: 8px; }
|
|
141
148
|
.ta-layer.ta-kb .ta-foot .composer textarea { max-height: 96px; }
|
|
149
|
+
/* 6.99.30: a hardware keyboard's strip with a field focused (ta-kb-strip): the header stays, the strip covers the home
|
|
150
|
+
indicator, so the reply box drops that inset. The layer already ends above the strip. */
|
|
151
|
+
.ta-layer.ta-kb-strip .ta-foot { padding-bottom: 8px; }
|
|
142
152
|
/* Under about three lines left for the conversation: the "Working right now" note waits until the keyboard closes. */
|
|
143
153
|
.ta-layer.ta-kb-tight .ta-foot .sx-park { display: none; }
|
|
144
154
|
@keyframes ta-rise { from { transform: translateY(32px); opacity: 0.6; } }
|
|
@@ -160,8 +170,18 @@ html.ta-open { overflow: hidden; }
|
|
|
160
170
|
/* Files with a reply or a note (11/07): the COS chat's own chips and clip (cos-chat.css .cc-chip, .cc-attach), placed in
|
|
161
171
|
the reply box. The chips take their own row above the words; the clip sits beside Send. */
|
|
162
172
|
.ta-composer { flex-wrap: wrap; }
|
|
163
|
-
|
|
173
|
+
/* 6.99.30 (canary BLOCKER): five files pushed the reply box and Send off the sheet at every size (the foot never
|
|
174
|
+
shrinks and the keyboard's sheet clips). The chips scroll inside a capped box, so the words and Send always fit: about
|
|
175
|
+
two chip rows with room, one with the keyboard up. */
|
|
176
|
+
.ta-composer .ta-files { flex: 1 1 100%; padding: 2px 0 4px; max-height: min(30vh, 132px); overflow-y: auto; overscroll-behavior: contain; }
|
|
177
|
+
.ta-layer.ta-kb .ta-composer .ta-files { max-height: 60px; }
|
|
164
178
|
.ta-composer .ta-files.hidden { display: none; }
|
|
165
179
|
.ta-clip:empty { display: none; }
|
|
166
180
|
.ta-clip .cc-attach { height: 38px; width: 42px; border-radius: var(--radius-sm); }
|
|
181
|
+
/* 6.99.30 (canary): with the keyboard up a sticky question footer covered all but part of one option; it scrolls with
|
|
182
|
+
the card instead while typing. */
|
|
183
|
+
.ta-layer.ta-kb .ta-q .sq-foot, .ta-layer.ta-kb-strip .ta-q .sq-foot { position: static; }
|
|
184
|
+
/* The "Not sent" line for a refused queued message (today-actions.js loadNotSent). */
|
|
185
|
+
.ta-notsent:empty { display: none; }
|
|
186
|
+
.ta-notsent { margin: 4px 0 8px; }
|
|
167
187
|
.ta-composer.dragging { outline: 2px dashed var(--accent); outline-offset: 4px; }
|