@gotcos/glasses-server 6.99.20 → 6.99.25
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 +91 -0
- package/docs/work-orchestrator.md +20 -0
- package/package.json +1 -1
- package/server/index.ts +2 -1
- package/server/lib/web-control-static.ts +226 -0
- package/server/lib/web-memories-bridge.ts +9 -8
- package/server/lib/web-pet-choice.ts +225 -0
- package/server/lib/web-pet.ts +226 -0
- package/server/lib/work-lens-rounds.ts +29 -4
- package/server/routes/web-control.ts +110 -7
- package/server/routes/work-lens-rounds.ts +19 -5
- package/server/web-control/app.css +24 -5
- package/server/web-control/app.js +646 -108
- package/server/web-control/control.css +1 -1
- package/server/web-control/index.html +27 -13
- package/server/web-control/memories/memory-workspace.js +1 -1
- package/server/web-control/panes/cos-chat.css +4 -0
- package/server/web-control/panes/cos-chat.js +26 -4
- package/server/web-control/panes/meetings.css +15 -2
- package/server/web-control/panes/memories.css +291 -0
- package/server/web-control/panes/memories.js +2208 -0
- package/server/web-control/panes/questions.js +59 -21
- package/server/web-control/panes/settings.js +102 -4
- package/server/web-control/panes/today-actions.css +109 -0
- package/server/web-control/panes/today-actions.js +972 -0
- package/server/web-control/panes/work.css +54 -0
- package/server/web-control/panes/work.js +1037 -18
- package/server/web-control/shell.js +6 -2
- package/server/web-control/theme.js +0 -2
- package/server/web-control/memories/memories-app.js +0 -1561
- package/server/web-control/memories/memories-theme.css +0 -73
- package/server/web-control/memories/memories.html +0 -72
- package/server/web-control/memories/memory-stewardship.js +0 -63
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
items: [], // pending items, oldest first
|
|
29
29
|
picks: {}, // id -> [{ labels: [], other: '' }] per question
|
|
30
30
|
armed: {}, // id -> 'send' | 'allow' | 'deny'
|
|
31
|
+
armedAt: {}, // id -> when it was armed (COSWeb.confirmReady)
|
|
31
32
|
sending: {}, // id -> clientAnswerId while a POST is out
|
|
32
33
|
notes: {}, // id -> one line under the card
|
|
33
34
|
answerIds: {}, // id -> the clientAnswerId this page answers under (a retry replays it)
|
|
@@ -115,13 +116,13 @@
|
|
|
115
116
|
var text = Q.notes[item.id] || '';
|
|
116
117
|
return '<p class="sq-note' + (text ? '' : ' hidden') + '" data-sq-note="' + esc(item.id) + '" role="status">' + esc(text) + '</p>';
|
|
117
118
|
}
|
|
118
|
-
function questionCard(item) {
|
|
119
|
+
function questionCard(item, scope) {
|
|
119
120
|
var picks = Q.picks[item.id] || [];
|
|
120
121
|
var sending = !!Q.sending[item.id];
|
|
121
122
|
var blocks = (item.questions || []).map(function (q, qi) {
|
|
122
123
|
var p = picks[qi] || { labels: [], other: '' };
|
|
123
124
|
var type = q.multiSelect ? 'checkbox' : 'radio';
|
|
124
|
-
var name = 'sq-' + item.id + '-' + qi;
|
|
125
|
+
var name = (scope || 'sq') + '-' + item.id + '-' + qi;
|
|
125
126
|
return '<fieldset class="sq-q"><legend class="sq-q-text">' + esc(q.question) + (q.header ? ' <span class="sq-q-tag">' + esc(q.header) + '</span>' : '') + '</legend>' +
|
|
126
127
|
(q.multiSelect ? '<p class="sq-fine">Pick any that apply.</p>' : '') +
|
|
127
128
|
'<div class="sq-options">' + q.options.map(function (o, oi) {
|
|
@@ -153,26 +154,48 @@
|
|
|
153
154
|
'<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>' +
|
|
154
155
|
noteHtml(item) + '</article>';
|
|
155
156
|
}
|
|
156
|
-
|
|
157
|
+
// Other places can show the cards for one session (Today's overlay, panes/today-actions.js): the same items, picks,
|
|
158
|
+
// answer ids and posts, drawn into their own element. A mount is dropped when its element leaves the page.
|
|
159
|
+
var mounts = [];
|
|
160
|
+
function hostsOf() {
|
|
161
|
+
mounts = mounts.filter(function (m) { return m.el.isConnected !== false; });
|
|
162
|
+
return [{ el: host, sessionId: null, scope: 'sq' }].concat(mounts);
|
|
163
|
+
}
|
|
164
|
+
function itemsFor(sessionId) {
|
|
165
|
+
var want = String(sessionId || '').toLowerCase();
|
|
166
|
+
return Q.items.filter(function (i) { return !want || String(i.sessionId || '').toLowerCase() === want; });
|
|
167
|
+
}
|
|
168
|
+
function drawInto(m) {
|
|
157
169
|
var active = document.activeElement;
|
|
158
|
-
var focus = active && active.hasAttribute && active.hasAttribute('data-sq-other') ? { id: active.getAttribute('data-sq-id'), q: active.getAttribute('data-q'), start: active.selectionStart, end: active.selectionEnd } : null;
|
|
159
|
-
|
|
160
|
-
|
|
170
|
+
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;
|
|
171
|
+
var list = itemsFor(m.sessionId);
|
|
172
|
+
m.el.innerHTML = list.map(function (i) { return i.kind === 'approval' ? approvalCard(i) : questionCard(i, m.scope); }).join('');
|
|
173
|
+
m.el.hidden = !list.length;
|
|
161
174
|
if (focus) {
|
|
162
|
-
var box =
|
|
175
|
+
var box = m.el.querySelector('[data-sq-other][data-sq-id="' + focus.id + '"][data-q="' + focus.q + '"]');
|
|
163
176
|
if (box) { box.focus({ preventScroll: true }); try { box.setSelectionRange(focus.start, focus.end); } catch (_e) {} }
|
|
164
177
|
}
|
|
178
|
+
if (m.onChange) { try { m.onChange(list.length); } catch (_e) {} }
|
|
179
|
+
}
|
|
180
|
+
function render() {
|
|
181
|
+
hostsOf().forEach(drawInto);
|
|
165
182
|
clearInterval(Q.tick);
|
|
166
183
|
if (Q.items.length) Q.tick = setInterval(tickSeconds, 1000);
|
|
167
184
|
}
|
|
168
185
|
function itemOf(id) { for (var i = 0; i < Q.items.length; i++) if (Q.items[i].id === id) return Q.items[i]; return null; }
|
|
169
186
|
// The seconds move without a redraw (a redraw would drop a half-typed answer).
|
|
170
187
|
function tickSeconds() {
|
|
171
|
-
|
|
188
|
+
hostsOf().forEach(function (m) {
|
|
189
|
+
m.el.querySelectorAll('[data-sq-secs]').forEach(function (el) { var it = itemOf(el.getAttribute('data-sq-secs')); if (it) el.textContent = timeLeft(secondsLeft(it)); });
|
|
190
|
+
});
|
|
172
191
|
}
|
|
173
192
|
function patchCard(id) {
|
|
174
|
-
var item = itemOf(id),
|
|
175
|
-
|
|
193
|
+
var item = itemOf(id), cards = [];
|
|
194
|
+
hostsOf().forEach(function (m) { var c = m.el.querySelector('[data-sq-card="' + id + '"]'); if (c) cards.push(c); });
|
|
195
|
+
if (!item || !cards.length) return render();
|
|
196
|
+
cards.forEach(function (card) { patchOne(card, id, item); });
|
|
197
|
+
}
|
|
198
|
+
function patchOne(card, id, item) {
|
|
176
199
|
var send = card.querySelector('[data-sq="send"]');
|
|
177
200
|
if (send) { send.disabled = !!Q.sending[id] || !complete(item); send.textContent = Q.sending[id] ? 'Sending…' : Q.armed[id] === 'send' ? 'Tap again to send' : 'Send'; }
|
|
178
201
|
var allow = card.querySelector('[data-sq="allow"]'); if (allow) allow.textContent = Q.armed[id] === 'allow' ? 'Tap again to allow' : 'Allow once';
|
|
@@ -219,7 +242,7 @@
|
|
|
219
242
|
});
|
|
220
243
|
post(item.id, { answers: answers }, 'Sent to ' + engine(item) + '.');
|
|
221
244
|
}
|
|
222
|
-
|
|
245
|
+
function onClick(e) {
|
|
223
246
|
var b = e.target.closest('[data-sq]');
|
|
224
247
|
if (!b || b.disabled) return;
|
|
225
248
|
var id = b.getAttribute('data-sq-id'), what = b.getAttribute('data-sq'), item = itemOf(id);
|
|
@@ -237,23 +260,28 @@
|
|
|
237
260
|
});
|
|
238
261
|
return;
|
|
239
262
|
}
|
|
240
|
-
// Tap, then tap again: the first tap only arms the button
|
|
241
|
-
|
|
263
|
+
// Tap, then tap again: the first tap only arms the button, and the second counts only once the first has been on
|
|
264
|
+
// screen a moment (COSWeb.confirmReady, the rule Today's overlay uses), so a double tap never allows or sends.
|
|
265
|
+
if (Q.armed[id] !== what) { Q.armed[id] = what; Q.armedAt[id] = Date.now(); patchCard(id); return; }
|
|
266
|
+
if (W.confirmReady && !W.confirmReady(Q.armedAt[id])) return;
|
|
242
267
|
if (what === 'send') return sendAnswers(item);
|
|
243
268
|
if (what === 'allow' || what === 'deny') return post(id, { decision: what }, what === 'allow' ? 'Allowed once.' : 'Denied.');
|
|
244
|
-
}
|
|
245
|
-
|
|
269
|
+
}
|
|
270
|
+
function onChange(e) {
|
|
246
271
|
var t = e.target;
|
|
247
272
|
if (!t.hasAttribute('data-sq-pick')) return;
|
|
248
273
|
var id = t.getAttribute('data-sq-id'), item = itemOf(id); if (!item) return;
|
|
249
274
|
var qi = Number(t.getAttribute('data-q')), q = item.questions[qi], label = q.options[Number(t.getAttribute('data-o'))].label;
|
|
250
275
|
var p = Q.picks[id][qi];
|
|
251
276
|
if (q.multiSelect) p.labels = t.checked ? p.labels.concat([label]) : p.labels.filter(function (l) { return l !== label; });
|
|
252
|
-
else {
|
|
277
|
+
else {
|
|
278
|
+
p.labels = [label]; p.other = '';
|
|
279
|
+
hostsOf().forEach(function (m) { var box = m.el.querySelector('[data-sq-other][data-sq-id="' + id + '"][data-q="' + qi + '"]'); if (box) box.value = ''; });
|
|
280
|
+
}
|
|
253
281
|
Q.armed[id] = '';
|
|
254
282
|
patchCard(id);
|
|
255
|
-
}
|
|
256
|
-
|
|
283
|
+
}
|
|
284
|
+
function onInput(e) {
|
|
257
285
|
var t = e.target;
|
|
258
286
|
if (!t.hasAttribute('data-sq-other')) return;
|
|
259
287
|
var id = t.getAttribute('data-sq-id'), item = itemOf(id); if (!item) return;
|
|
@@ -262,14 +290,24 @@
|
|
|
262
290
|
// Single choice: typing your own answer clears the picked option, as on the phone.
|
|
263
291
|
if (!q.multiSelect && t.value.trim() && p.labels.length) {
|
|
264
292
|
p.labels = [];
|
|
265
|
-
|
|
293
|
+
hostsOf().forEach(function (m) { m.el.querySelectorAll('[data-sq-pick][data-sq-id="' + id + '"][data-q="' + qi + '"]').forEach(function (r) { r.checked = false; }); });
|
|
266
294
|
}
|
|
267
295
|
Q.armed[id] = '';
|
|
268
296
|
patchCard(id);
|
|
269
|
-
}
|
|
297
|
+
}
|
|
298
|
+
function listen(el) { el.addEventListener('click', onClick); el.addEventListener('change', onChange); el.addEventListener('input', onInput); }
|
|
299
|
+
listen(host);
|
|
300
|
+
// Draw this session's cards into `el` and keep them in step; `onChange(count)` hears each redraw.
|
|
301
|
+
function mount(el, sessionId, onChange) {
|
|
302
|
+
if (!el) return null;
|
|
303
|
+
var m = { el: el, sessionId: String(sessionId || '').toLowerCase(), scope: 'sqm' + (mounts.length + 1), onChange: onChange || null };
|
|
304
|
+
mounts.push(m); listen(el); drawInto(m);
|
|
305
|
+
if (!Q.stopped) { clearTimeout(Q.timer); Q.timer = setTimeout(poll, 0); }
|
|
306
|
+
return { count: function () { return itemsFor(m.sessionId).length; }, unmount: function () { mounts = mounts.filter(function (x) { return x !== m; }); } };
|
|
307
|
+
}
|
|
270
308
|
|
|
271
309
|
document.addEventListener('visibilitychange', function () { if (document.hidden) stop(); else if (signedIn()) start(); });
|
|
272
310
|
W.onShell(function () { start(); });
|
|
273
311
|
|
|
274
|
-
W.sessionQuestions = { _state: Q, _take: take, _render: render, poll: poll };
|
|
312
|
+
W.sessionQuestions = { _state: Q, _take: take, _render: render, poll: poll, mount: mount, itemsFor: itemsFor };
|
|
275
313
|
})();
|
|
@@ -24,12 +24,67 @@
|
|
|
24
24
|
// 6.99.11 (Drop it P2): shown only when the server has the P2 settings and Drop it is on.
|
|
25
25
|
{ id: 'reach', title: 'COS reaching out', summary: 'What COS tells you about on its own, when it may interrupt, and your reminders.', mac: [], p2: true },
|
|
26
26
|
{ id: 'privacy', title: 'Privacy and permissions', summary: 'What COS can use on this Mac. Each permission turns on one feature.', mac: ['Permissions'] },
|
|
27
|
-
{ id: 'pet', title: 'Pet', summary: '
|
|
27
|
+
{ id: 'pet', title: 'Pet', summary: 'Which pet this browser shows on Today, and whether it shows. Size, motion and the Mac\u2019s own pet are set in COS Control.', mac: ['Session pet size and motion', 'Characters on the Mac'] },
|
|
28
28
|
{ id: 'system', title: 'System and updates', summary: 'The COS server on this Mac, app updates, and checks when something is off.', mac: ['App updates', 'Troubleshooting'] }
|
|
29
29
|
];
|
|
30
30
|
var DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
31
31
|
|
|
32
32
|
function soft(p) { return p.then(function (d) { return d; }, function () { return null; }); }
|
|
33
|
+
// ── Pet (this browser) ──────────────────────────────────
|
|
34
|
+
// Which pet Today shows here and whether it shows at all. Kept per browser (COSWeb.petPrefs), like the theme.
|
|
35
|
+
// The Mac's own pet is changed in COS Control: the app does not watch its pet folder, so a change from here
|
|
36
|
+
// would leave the Mac showing the old pet. Catalog: GET /api/web-pet/catalog (lib/web-pet-choice.ts).
|
|
37
|
+
var PET = { catalog: null, state: 'idle', q: '' };
|
|
38
|
+
function loadPetCatalog() {
|
|
39
|
+
if (PET.state === 'loading' || PET.state === 'ready') return;
|
|
40
|
+
PET.state = 'loading';
|
|
41
|
+
api('/api/web-pet/catalog').then(function (c) { PET.catalog = c; PET.state = 'ready'; render(); },
|
|
42
|
+
function () { PET.state = 'error'; render(); });
|
|
43
|
+
}
|
|
44
|
+
function petChoiceRow(pet, name, desc, preview, current) {
|
|
45
|
+
var on = current === pet;
|
|
46
|
+
return '<button type="button" class="cx-set-row cx-set-choice pet-choice" data-pet="' + esc(pet) + '" aria-pressed="' + on + '">' +
|
|
47
|
+
(preview ? '<img class="pet-thumb" alt="" loading="lazy" src="' + esc(preview) + '">' : '<span class="pet-thumb"></span>') +
|
|
48
|
+
'<span class="cx-set-text"><span class="cx-set-name">' + esc(name) + '</span>' + (desc ? '<span class="cx-set-desc">' + esc(desc) + '</span>' : '') + '</span>' +
|
|
49
|
+
'<span class="value">' + (on ? '<span class="check">✓</span>' : '') + '</span></button>';
|
|
50
|
+
}
|
|
51
|
+
function petBody() {
|
|
52
|
+
var prefs = W.petPrefs ? W.petPrefs() : { pet: 'mac', show: true };
|
|
53
|
+
var out = group('On this browser', '',
|
|
54
|
+
row('Show the pet on Today', 'Only here. The pet on your Mac stays as it is.', toggle(prefs.show, 'data-set="pet-show"', 'Show the pet on Today')));
|
|
55
|
+
if (PET.state === 'idle') loadPetCatalog();
|
|
56
|
+
if (PET.state !== 'ready') {
|
|
57
|
+
return out + group('Character', '', PET.state === 'error'
|
|
58
|
+
? '<p class="cx-set-row">Could not load the pets. <button type="button" class="cx-btn quiet" data-set="pet-retry">Try again</button></p>'
|
|
59
|
+
: W.skeletonRows(3));
|
|
60
|
+
}
|
|
61
|
+
var c = PET.catalog || {}, shipped = c.shipped || [], gallery = c.gallery || [];
|
|
62
|
+
out += group('Character', 'The pet this browser shows on Today. It does not change the pet on your Mac.',
|
|
63
|
+
petChoiceRow('mac', 'Same as my Mac', c.mac && c.mac.available ? 'Follows the pet COS Control shows.' : 'Your Mac has no pet set.', c.mac && c.mac.preview, prefs.pet) +
|
|
64
|
+
shipped.map(function (p) { return petChoiceRow(p.pet, p.name, p.summary || (p.animated ? 'Animated.' : ''), p.preview, prefs.pet); }).join(''));
|
|
65
|
+
if (gallery.length) {
|
|
66
|
+
var picked = gallery.filter(function (p) { return p.pet === prefs.pet; })[0];
|
|
67
|
+
out += '<div class="cx-set-group"><h3 class="cx-set-title">Gallery</h3><p class="cx-set-blurb">' + gallery.length +
|
|
68
|
+
' pets from the OpenPets gallery COS Control uses. Each shows as one still picture, as it does on the Mac.' + (picked ? ' In use: ' + esc(picked.name) + '.' : '') + '</p>' +
|
|
69
|
+
'<input type="search" class="pet-search" id="pet-search" placeholder="Search the gallery" aria-label="Search the gallery" value="' + esc(PET.q) + '">' +
|
|
70
|
+
'<div class="pet-grid" id="pet-grid">' + gallery.map(function (p) {
|
|
71
|
+
var on = p.pet === prefs.pet, words = (p.name + ' ' + p.category + ' ' + p.description).toLowerCase();
|
|
72
|
+
return '<button type="button" class="pet-tile" data-pet="' + esc(p.pet) + '" data-words="' + esc(words) + '" aria-pressed="' + on + '" title="' + esc(p.description || p.name) + '">' +
|
|
73
|
+
'<img alt="" loading="lazy" src="' + esc(p.preview) + '"><span>' + esc(p.name) + '</span></button>';
|
|
74
|
+
}).join('') + '</div><p class="cx-set-blurb hidden" id="pet-none">No pets match that search.</p></div>';
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
function filterPets() {
|
|
79
|
+
var q = PET.q.trim().toLowerCase(), grid = $('pet-grid'), shown = 0;
|
|
80
|
+
if (!grid) return;
|
|
81
|
+
grid.querySelectorAll('.pet-tile').forEach(function (t) {
|
|
82
|
+
var hit = !q || (t.getAttribute('data-words') || '').indexOf(q) >= 0;
|
|
83
|
+
t.classList.toggle('hidden', !hit); if (hit) shown++;
|
|
84
|
+
});
|
|
85
|
+
var none = $('pet-none'); if (none) none.classList.toggle('hidden', shown > 0);
|
|
86
|
+
}
|
|
87
|
+
|
|
33
88
|
function load(force) {
|
|
34
89
|
if (S.state === 'loading' || (S.data && !force)) return;
|
|
35
90
|
S.state = 'loading';
|
|
@@ -67,7 +122,7 @@
|
|
|
67
122
|
return '<section class="cx-set-cat"><h2 class="cx-col-title">This browser</h2>' +
|
|
68
123
|
group('Appearance', '', themeBtn('system', 'Match this device') + themeBtn('light', 'Light') + themeBtn('dark', 'Dark')) +
|
|
69
124
|
group('Sign-in', '',
|
|
70
|
-
'<button type="button" class="cx-set-row" id="device-code-btn"><span class="cx-set-text"><span class="cx-set-name">
|
|
125
|
+
'<button type="button" class="cx-set-row" id="device-code-btn"><span class="cx-set-text"><span class="cx-set-name">Use COS on another phone or browser</span><span class="cx-set-desc">A QR for your iPhone Camera, and a code to type. This is not glasses pairing; pair glasses from COS Control on your Mac.</span></span><span class="chev"></span></button>' +
|
|
71
126
|
'<div id="device-code-panel" class="device-code hidden" aria-live="polite"></div>' +
|
|
72
127
|
'<button type="button" class="cx-set-row" id="copy-link"><span class="cx-set-text"><span class="cx-set-name">Copy link for your phone</span><span class="cx-set-desc" id="tailnet-url">' + esc(W.state.tailnetUrl || 'Checking Tailscale') + '</span></span></button>' +
|
|
73
128
|
'<button type="button" class="cx-set-row" id="sign-out"><span class="cx-set-text"><span class="cx-set-name">Sign out of this browser</span></span></button>' +
|
|
@@ -154,6 +209,7 @@
|
|
|
154
209
|
var d = S.data || {};
|
|
155
210
|
var out = '';
|
|
156
211
|
if (cat.id === 'reach') return reachBody();
|
|
212
|
+
if (cat.id === 'pet') return petBody() + onMac(cat.mac);
|
|
157
213
|
if (cat.id === 'ai' && d.dp) {
|
|
158
214
|
var p = d.dp.provider || '';
|
|
159
215
|
var name = function (x) { return x === 'subq' ? 'SubQ' : x === 'jev' ? 'Jev' : x || 'Not set'; };
|
|
@@ -226,9 +282,10 @@
|
|
|
226
282
|
browserGroup() +
|
|
227
283
|
(S.state !== 'ready' ? '<div class="cx-list" style="margin-top:18px">' + W.skeletonRows(4) + '</div>' :
|
|
228
284
|
CATEGORIES.filter(function (cat) { return !cat.p2 || p2On(); }).map(function (cat) {
|
|
229
|
-
return '<section class="cx-set-cat"><h2 class="cx-col-title">' + esc(cat.title) + '</h2><p class="cx-set-blurb">' + esc(cat.summary) + '</p>' + categoryBody(cat) + '</section>';
|
|
285
|
+
return '<section class="cx-set-cat" id="set-cat-' + cat.id + '"><h2 class="cx-col-title">' + esc(cat.title) + '</h2><p class="cx-set-blurb">' + esc(cat.summary) + '</p>' + categoryBody(cat) + '</section>';
|
|
230
286
|
}).join('')) +
|
|
231
287
|
'<p class="status-line" id="settings-status"></p></div>';
|
|
288
|
+
if (PET.q) filterPets();
|
|
232
289
|
}
|
|
233
290
|
|
|
234
291
|
function say(text, isError) { var el = $('settings-status'); if (el) { el.textContent = text; el.classList.toggle('error', !!isError); } }
|
|
@@ -236,8 +293,26 @@
|
|
|
236
293
|
pane.addEventListener('click', function (e) {
|
|
237
294
|
var go = e.target.closest('[data-go]');
|
|
238
295
|
if (go) { var parts = go.getAttribute('data-go').split('/'); return W.go(parts[0], parts.slice(1)); }
|
|
296
|
+
var pick = e.target.closest('[data-pet]');
|
|
297
|
+
if (pick && W.setPetPrefs) {
|
|
298
|
+
W.setPetPrefs({ pet: pick.getAttribute('data-pet') });
|
|
299
|
+
pane.querySelectorAll('[data-pet]').forEach(function (b) {
|
|
300
|
+
var on = b === pick; b.setAttribute('aria-pressed', String(on));
|
|
301
|
+
var v = b.querySelector('.value'); if (v) v.innerHTML = on ? '<span class="check">✓</span>' : '';
|
|
302
|
+
});
|
|
303
|
+
W.toast('Pet changed. Today shows it here.');
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
239
306
|
var t = e.target.closest('[data-set]');
|
|
240
307
|
if (!t || t.disabled) return;
|
|
308
|
+
if (t.getAttribute('data-set') === 'pet-show') {
|
|
309
|
+
var shown = t.getAttribute('aria-checked') === 'true';
|
|
310
|
+
t.setAttribute('aria-checked', String(!shown));
|
|
311
|
+
if (W.setPetPrefs) W.setPetPrefs({ show: !shown });
|
|
312
|
+
W.toast(!shown ? 'Pet on' : 'Pet off');
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
if (t.getAttribute('data-set') === 'pet-retry') { PET.state = 'idle'; return render(); }
|
|
241
316
|
var act = t.getAttribute('data-set');
|
|
242
317
|
if (act === 'refresh') { S.data = null; S.state = 'idle'; load(true); return render(); }
|
|
243
318
|
if (act === 'provider') {
|
|
@@ -273,6 +348,15 @@
|
|
|
273
348
|
}
|
|
274
349
|
});
|
|
275
350
|
|
|
351
|
+
// A gallery picture that will not load shows the pet's name alone, never a broken image.
|
|
352
|
+
pane.addEventListener('error', function (e) {
|
|
353
|
+
var img = e.target;
|
|
354
|
+
if (img && img.tagName === 'IMG' && img.closest && img.closest('.pet-tile, .pet-choice')) img.classList.add('hidden');
|
|
355
|
+
}, true);
|
|
356
|
+
pane.addEventListener('input', function (e) {
|
|
357
|
+
if (e.target && e.target.id === 'pet-search') { PET.q = e.target.value; filterPets(); }
|
|
358
|
+
});
|
|
359
|
+
|
|
276
360
|
// Number, time, day and steer fields save when they change (a number is clamped by the server, then shown as saved).
|
|
277
361
|
pane.addEventListener('change', function (e) {
|
|
278
362
|
var f = e.target && e.target.closest ? e.target.closest('[data-p2]') : null;
|
|
@@ -287,6 +371,20 @@
|
|
|
287
371
|
});
|
|
288
372
|
|
|
289
373
|
W.registerPane('settings', {
|
|
290
|
-
load: function () {
|
|
374
|
+
load: function (args) {
|
|
375
|
+
load(false); render();
|
|
376
|
+
// "Change pet" on Today opens Settings at the Pet section.
|
|
377
|
+
if (args && args[0] === 'pet') {
|
|
378
|
+
var tries = 0;
|
|
379
|
+
(function scroll() {
|
|
380
|
+
var el = document.getElementById('set-cat-pet');
|
|
381
|
+
// Land on the pet in use once the catalog has drawn; until then, the Pet section's top.
|
|
382
|
+
var chosen = el && el.querySelector('[data-pet][aria-pressed="true"]');
|
|
383
|
+
if (chosen) { chosen.scrollIntoView({ block: 'center' }); return; }
|
|
384
|
+
if (el && tries === 0) el.scrollIntoView({ block: 'start' });
|
|
385
|
+
if (++tries < 40) setTimeout(scroll, 150);
|
|
386
|
+
})();
|
|
387
|
+
}
|
|
388
|
+
}
|
|
291
389
|
});
|
|
292
390
|
})();
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/* COS Control Web: Today as a place to act (panes/today-actions.js). A bottom sheet on the folded phone, a side sheet
|
|
2
|
+
from 600 px. Built from the page's own tokens and pieces: the composer, .btn, .link-btn, .cx-btn, the reader's
|
|
3
|
+
activity rows (.sx-act-*) and boxes (.sx-box), and the question cards (questions.css). */
|
|
4
|
+
|
|
5
|
+
/* The Today rows that open it: whole-row buttons. */
|
|
6
|
+
.need-open { display: block; width: 100%; text-align: left; color: inherit; }
|
|
7
|
+
.need-open .need-top { display: flex; }
|
|
8
|
+
.need-open .need-foot { display: flex; }
|
|
9
|
+
.intake-row { width: 100%; text-align: left; color: inherit; align-items: center; min-height: 40px; }
|
|
10
|
+
.intake-row::after { content: ''; width: 7px; height: 7px; border-right: 2px solid var(--grey); border-bottom: 2px solid var(--grey); transform: rotate(-45deg); flex: none; opacity: 0.6; margin-left: 2px; }
|
|
11
|
+
.intake-row .intake-meeting { flex: 1; }
|
|
12
|
+
|
|
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
|
+
html.ta-open { overflow: hidden; }
|
|
15
|
+
|
|
16
|
+
.ta-layer {
|
|
17
|
+
position: fixed; left: 0; right: 0; z-index: 45;
|
|
18
|
+
/* The visible viewport (app sets these from visualViewport): the keyboard never covers the reply box. */
|
|
19
|
+
top: var(--ta-vt, 0px); height: var(--ta-vh, 100dvh);
|
|
20
|
+
--run-green: rgb(56, 145, 99);
|
|
21
|
+
}
|
|
22
|
+
:root[data-theme='dark'] .ta-layer { --run-green: rgb(108, 207, 143); }
|
|
23
|
+
@media (prefers-color-scheme: dark) { :root:not([data-theme='light']) .ta-layer { --run-green: rgb(108, 207, 143); } }
|
|
24
|
+
.ta-layer[hidden] { display: none; }
|
|
25
|
+
.ta-scrim { position: absolute; inset: 0; background: rgba(23, 18, 14, 0.45); touch-action: none; animation: fade 160ms ease; }
|
|
26
|
+
|
|
27
|
+
.ta-sheet {
|
|
28
|
+
position: absolute; left: 0; right: 0; bottom: 0;
|
|
29
|
+
/* A strip of Today stays in view above it: context, and somewhere to tap outside. */
|
|
30
|
+
max-height: calc(100% - var(--safe-top) - 56px);
|
|
31
|
+
display: flex; flex-direction: column;
|
|
32
|
+
background: var(--page); color: var(--ink);
|
|
33
|
+
border-radius: 20px 20px 0 0; box-shadow: 0 -12px 40px rgba(0, 0, 0, 0.25);
|
|
34
|
+
padding-left: var(--safe-left); padding-right: var(--safe-right);
|
|
35
|
+
outline: none;
|
|
36
|
+
animation: ta-rise 220ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
|
37
|
+
}
|
|
38
|
+
.ta-grip { flex: none; width: 36px; height: 5px; border-radius: 3px; background: var(--panel-border); margin: 8px auto 0; }
|
|
39
|
+
.ta-head { flex: none; display: flex; align-items: flex-start; gap: 10px; padding: 6px var(--gutter) 4px; }
|
|
40
|
+
.ta-title { flex: 1; min-width: 0; margin: 8px 0 0; font: 500 22px/1.2 var(--display); letter-spacing: -0.005em; overflow-wrap: anywhere; }
|
|
41
|
+
.ta-x { flex: none; display: grid; place-items: center; width: 44px; height: 44px; margin-right: -8px; border-radius: 12px; color: var(--ink); }
|
|
42
|
+
.ta-x .icon { width: 20px; height: 20px; stroke-width: 1.8; }
|
|
43
|
+
.ta-body { flex: 1 1 auto; min-height: 0; overflow: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch; padding: 2px var(--gutter) 16px; }
|
|
44
|
+
.ta-foot { flex: none; padding: 10px var(--gutter) calc(var(--safe-bottom) + 10px); border-top: 1px solid var(--row-border); background: var(--page); }
|
|
45
|
+
.ta-foot:empty { display: none; }
|
|
46
|
+
.ta-foot .composer { margin-top: 0; position: static; }
|
|
47
|
+
.ta-foot .status-line { margin: 6px 4px 0; min-height: 0; }
|
|
48
|
+
.ta-foot .status-line:empty { display: none; }
|
|
49
|
+
.ta-foot .sx-box, .ta-foot .sx-park { margin: 0 0 10px; }
|
|
50
|
+
|
|
51
|
+
.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; }
|
|
52
|
+
.ta-note { margin: 6px 0 0; color: var(--hot); font-size: 13.5px; line-height: 1.4; }
|
|
53
|
+
.ta-card-line { margin: 0 0 8px; font-size: 14px; color: var(--ink); overflow-wrap: anywhere; }
|
|
54
|
+
.ta-quote { margin: 8px 0 10px; padding-left: 12px; border-left: 2px solid var(--btn-border); font: italic 400 16px/1.42 var(--display); color: var(--ink); overflow-wrap: anywhere; }
|
|
55
|
+
.ta-ask-line { margin: 8px 0 4px; font-size: 14.5px; font-weight: 600; color: var(--hot); }
|
|
56
|
+
.ta-links { display: flex; flex-wrap: wrap; gap: 0 18px; }
|
|
57
|
+
.ta-q { display: grid; gap: 12px; margin: 8px 0 12px; }
|
|
58
|
+
.ta-q[hidden] { display: none; }
|
|
59
|
+
.ta-activity { margin: 8px 0 4px; }
|
|
60
|
+
.ta-activity .sx-act-state { margin-top: 0; }
|
|
61
|
+
.ta-act-prompt { margin: 2px 0 0; font-size: 13.5px; line-height: 1.4; color: var(--ink); overflow-wrap: anywhere; }
|
|
62
|
+
.ta-act-tail { margin-top: 6px; display: flex; flex-direction: column; }
|
|
63
|
+
.ta-last { margin-top: 14px; padding: 12px 14px; border-radius: 14px; background: var(--panel); border: 1px solid var(--panel-border); font-size: 15px; line-height: 1.5; }
|
|
64
|
+
.ta-last-head { margin: 0 0 6px; font-size: 13px; font-weight: 600; color: var(--sub); }
|
|
65
|
+
.ta-last-text.clamped { max-height: 18em; overflow: hidden; -webkit-mask-image: linear-gradient(#000 72%, transparent); mask-image: linear-gradient(#000 72%, transparent); }
|
|
66
|
+
|
|
67
|
+
/* From today's meetings */
|
|
68
|
+
.ta-list { margin-top: 2px; }
|
|
69
|
+
.ta-group { margin: 18px 0 0; font: 600 15px/1.3 var(--body); color: var(--ink); overflow-wrap: anywhere; }
|
|
70
|
+
.ta-sugg { padding: 12px 0 10px; border-top: 1px solid var(--row-border); }
|
|
71
|
+
.ta-group + .ta-sugg { border-top: 0; padding-top: 8px; }
|
|
72
|
+
.ta-sugg-text { margin: 0; font-size: 15px; font-weight: 600; line-height: 1.35; overflow-wrap: anywhere; }
|
|
73
|
+
.ta-sugg-src { margin: 4px 0 0; font-size: 13.5px; line-height: 1.4; color: var(--sub); overflow-wrap: anywhere; }
|
|
74
|
+
.ta-acts { display: flex; flex-wrap: wrap; align-items: center; gap: 2px 18px; margin-top: 6px; }
|
|
75
|
+
.ta-acts .btn.small { min-height: 44px; padding: 0 16px; }
|
|
76
|
+
.ta-acts .link-btn { font-size: 14.5px; }
|
|
77
|
+
.link-btn.quiet { color: var(--sub); font-weight: 500; }
|
|
78
|
+
.ta-wait { display: inline-flex; align-items: center; min-height: 44px; font-size: 14px; color: var(--sub); }
|
|
79
|
+
.ta-done { font-size: 14.5px; font-weight: 600; color: var(--ink); }
|
|
80
|
+
.ta-sugg.settled { display: flex; flex-wrap: wrap; align-items: center; gap: 0 16px; }
|
|
81
|
+
.ta-sugg.settled > p { flex-basis: 100%; }
|
|
82
|
+
.ta-sugg.settled .ta-sugg-text { color: var(--sub); font-weight: 500; }
|
|
83
|
+
|
|
84
|
+
/* Unfolded Duo, tablets, desktop: a sheet at the right edge, Today still in view beside it. */
|
|
85
|
+
@media (min-width: 600px) {
|
|
86
|
+
.ta-sheet {
|
|
87
|
+
left: auto; top: 0; width: clamp(360px, 60vw, 480px); max-height: none;
|
|
88
|
+
border-radius: 20px 0 0 20px; box-shadow: -16px 0 48px rgba(0, 0, 0, 0.22);
|
|
89
|
+
animation-name: ta-slide;
|
|
90
|
+
}
|
|
91
|
+
.ta-grip { display: none; }
|
|
92
|
+
.ta-head { padding-top: calc(var(--safe-top) + 14px); }
|
|
93
|
+
.ta-foot { padding-bottom: calc(var(--safe-bottom) + 14px); }
|
|
94
|
+
}
|
|
95
|
+
@keyframes ta-rise { from { transform: translateY(32px); opacity: 0.6; } }
|
|
96
|
+
@keyframes ta-slide { from { transform: translateX(32px); opacity: 0.6; } }
|
|
97
|
+
/* Dismiss armed: the second tap removes it (no Undo on this server). */
|
|
98
|
+
.link-btn.quiet.armed { color: var(--hot); font-weight: 600; }
|
|
99
|
+
/* Needs you: "Show N more", "can wait until tomorrow" and a source that could not be read (app.js renderNeeds). */
|
|
100
|
+
.need-more { display: block; width: 100%; text-align: left; padding: 12px 16px; min-height: 44px; border-top: 1px solid var(--row-border); color: var(--accent); font-weight: 600; font-size: 15px; }
|
|
101
|
+
.need-more.need-failed { color: var(--hot); font-weight: 500; }
|
|
102
|
+
/* Next up: Prep me and Open meeting (app.js renderNext). */
|
|
103
|
+
.next-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 18px; padding: 0 16px 14px; }
|
|
104
|
+
.next-actions:empty { display: none; }
|
|
105
|
+
.next-actions .btn.small { min-height: 44px; }
|
|
106
|
+
/* The Ask sheet's File to Work inbox result, on its own row (app.js fileNoteHtml). */
|
|
107
|
+
.nav-row .note.file-error { color: var(--hot); white-space: normal; }
|
|
108
|
+
/* Running card when the session list could not be read (app.js renderRunning). */
|
|
109
|
+
.run-more.run-failed { color: var(--run-open); }
|