@gotcos/glasses-server 7.0.29 → 7.0.31
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 +76 -0
- package/docs/web-key-verifier-contract.md +33 -7
- package/package.json +1 -1
- package/server/lib/provider-failure-text.ts +4 -1
- package/server/lib/web-key-authority.ts +1 -1
- package/server/lib/web-key-registry.ts +1 -1
- package/server/lib/web-key-signer-tls.ts +17 -7
- package/server/web-control/.manifest.json +15 -15
- package/server/web-control/app.css +6 -4
- package/server/web-control/app.js +309 -17
- package/server/web-control/control.css +32 -2
- package/server/web-control/index.html +2 -0
- package/server/web-control/panes/cos-chat.css +26 -1
- package/server/web-control/panes/cos-chat.js +9 -3
- package/server/web-control/panes/meetings.css +4 -0
- package/server/web-control/panes/meetings.js +25 -6
- package/server/web-control/panes/memories.js +2 -2
- package/server/web-control/panes/messages.js +7 -2
- package/server/web-control/panes/settings.js +1 -1
- package/server/web-control/panes/speakers.js +28 -3
- package/server/web-control/panes/today-actions.js +2 -2
- package/server/web-control/panes/work.js +1 -1
- package/server/web-control/shell.js +2 -1
- package/server/web-signer/.manifest.json +2 -2
- package/server/web-signer/signer-proof.js +18 -3
- package/server/web-signer/signer.js +4 -2
- package/shared/web-turn-envelope.ts +33 -8
|
@@ -573,6 +573,133 @@
|
|
|
573
573
|
var hash = '#/' + [pane].concat(args || []).map(encodeURIComponent).join('/');
|
|
574
574
|
if (location.hash === hash) route(); else location.hash = hash;
|
|
575
575
|
}
|
|
576
|
+
// ── Back by path (7.0.30, item 1: Anand on the 11-12 HC call, Miles's pick) ──
|
|
577
|
+
// Back goes where the person came from. Each address this page makes is marked in its own history entry
|
|
578
|
+
// (history.state.cosNav): this page load's id, the entry's position, and the address it came from. Back is then the
|
|
579
|
+
// browser's own Back (history.back), so the system Back and ours agree and Back adds no entry. With no mark (a deep
|
|
580
|
+
// link, a reload, the first page, or an entry an overlay took over), Back keeps the parent chain it always had, and it
|
|
581
|
+
// REPLACES the entry rather than adding one, so the two can never bounce between each other. The image viewer and
|
|
582
|
+
// Today's sheet push entries of their own (cosImageViewer, cosTodayOverlay): those are never marked, never stepped into,
|
|
583
|
+
// and an address made while one of them still sat behind gets no mark, because the entry behind it is not an address
|
|
584
|
+
// (its position is not the last address's position plus one).
|
|
585
|
+
var NAV = { doc: Math.random().toString(36).slice(2, 10), last: null, replace: null };
|
|
586
|
+
function navState() { try { return history.state; } catch (_e) { return null; } }
|
|
587
|
+
function navOverlay(st) { return !!(st && (st.cosImageViewer || st.cosTodayOverlay)); }
|
|
588
|
+
function navStamp() {
|
|
589
|
+
if (typeof history === 'undefined' || !history || typeof location === 'undefined') return; // a page without them: the parent chain
|
|
590
|
+
var hash = location.hash || '', st = navState(), pos;
|
|
591
|
+
if (navOverlay(st)) { NAV.last = null; NAV.replace = null; return; }
|
|
592
|
+
if (st && st.cosNav && st.cosNav.doc === NAV.doc) { NAV.last = { hash: hash, pos: st.cosNav.pos, back: st.cosNav.back, trail: st.cosNav.trail || null, label: st.cosNav.label || '' }; NAV.replace = null; return; }
|
|
593
|
+
if (st && st.cosNav) {
|
|
594
|
+
// Marked by an earlier load of this page (a reload): what lies behind it belongs to that load, so no path.
|
|
595
|
+
pos = st.cosNav.pos;
|
|
596
|
+
} else if (st) { NAV.last = null; NAV.replace = null; return; } // someone else's state: leave it alone
|
|
597
|
+
else pos = NAV.replace ? NAV.replace.pos : history.length - 1;
|
|
598
|
+
var back = null;
|
|
599
|
+
if (!st && !NAV.replace && NAV.last && typeof NAV.last.pos === 'number' && pos === NAV.last.pos + 1) back = NAV.last.hash;
|
|
600
|
+
NAV.replace = null;
|
|
601
|
+
// 7.0.30, Miles's pick B: the steps before this one ride in the same mark, the last of them being where Back goes.
|
|
602
|
+
var trail = back ? (NAV.last.trail || []).concat([{ h: NAV.last.hash, t: NAV.last.label || '' }]).slice(-NAV_TRAIL_MAX) : null;
|
|
603
|
+
var mark = { doc: NAV.doc, pos: pos, back: back };
|
|
604
|
+
if (trail) mark.trail = trail;
|
|
605
|
+
try { history.replaceState(Object.assign({}, st || {}, { cosNav: mark }), ''); } catch (_e) { /* no history API: the parent chain */ }
|
|
606
|
+
NAV.last = { hash: hash, pos: pos, back: back, trail: trail, label: '' };
|
|
607
|
+
}
|
|
608
|
+
var NAV_TRAIL_MAX = 12;
|
|
609
|
+
// The page's own name, kept in its mark, so the next page's trail can name it. setCrumb calls this.
|
|
610
|
+
function navLabel(text) {
|
|
611
|
+
var st = navState();
|
|
612
|
+
if (navOverlay(st) || !st || !st.cosNav || st.cosNav.doc !== NAV.doc || (st.cosNav.label || '') === text) return;
|
|
613
|
+
try { history.replaceState(Object.assign({}, st, { cosNav: Object.assign({}, st.cosNav, { label: text }) }), ''); } catch (_e) { return; }
|
|
614
|
+
if (NAV.last && NAV.last.pos === st.cosNav.pos) NAV.last.label = text;
|
|
615
|
+
}
|
|
616
|
+
// The way he came, oldest first, from this entry's mark: [{ label, hash, back }], back being how many steps back that
|
|
617
|
+
// one is. Null when Back has no path (a deep link, a reload, the first page, an overlay).
|
|
618
|
+
function navTrail() {
|
|
619
|
+
var st = navState();
|
|
620
|
+
if (navOverlay(st) || !st || !st.cosNav || !st.cosNav.back || !st.cosNav.trail || !st.cosNav.trail.length) return null;
|
|
621
|
+
var t = st.cosNav.trail;
|
|
622
|
+
return t.map(function (s, i) { return { label: trailName(s.h, s.t), hash: s.h, back: t.length - i }; });
|
|
623
|
+
}
|
|
624
|
+
// A step is named by what kind of page it is, not its title (the title is the heading under the row): "Meeting",
|
|
625
|
+
// "Session", "Naming voices". A list is its pane. A message, a day and a chat keep their own short name.
|
|
626
|
+
function trailName(hash, crumb) {
|
|
627
|
+
var a = String(hash || '').replace(/^#\/?/, '').split('/').filter(function (x) { return x !== ''; }), p = ROOT_PANES.indexOf(a[0]) >= 0 ? a[0] : 'today';
|
|
628
|
+
if (a.length < 2 || isTabRoute(a.slice(1))) return TITLES[p] || 'Today';
|
|
629
|
+
if (p === 'meetings') return 'Meeting';
|
|
630
|
+
if (p === 'sessions') return 'Session';
|
|
631
|
+
if (p === 'speakers' && a[1] === 'review') return 'Naming voices';
|
|
632
|
+
return crumb || TITLES[p] || 'Today';
|
|
633
|
+
}
|
|
634
|
+
// One line that never wraps. When it is too wide, steps leave from the middle (the one after the first, then the next),
|
|
635
|
+
// never the first, the one before this page, or this page; "…" stands where they were. widths: each step's, then this
|
|
636
|
+
// page's; more: the width "…" adds. Returns the indexes to leave out.
|
|
637
|
+
function trailCut(widths, more, avail) {
|
|
638
|
+
var n = widths.length - 1, total = widths.reduce(function (a, b) { return a + b; }, 0), cut = [];
|
|
639
|
+
if (total <= avail) return cut;
|
|
640
|
+
total += more;
|
|
641
|
+
for (var i = 1; i < n - 1 && total > avail; i++) { cut.push(i); total -= widths[i]; }
|
|
642
|
+
return cut;
|
|
643
|
+
}
|
|
644
|
+
// The row under the top bar, on a phone (below 600 px; wider screens have the navigation bar's crumbs). Only on a page
|
|
645
|
+
// you opened (a reader, a meeting, a session), and only when there is a path.
|
|
646
|
+
function paintTrail(r) {
|
|
647
|
+
var el = typeof document !== 'undefined' && document.getElementById('nav-trail');
|
|
648
|
+
if (!el) return;
|
|
649
|
+
r = r || parseRoute();
|
|
650
|
+
var steps = r.args.length && !isTabRoute(r.args) ? navTrail() : null;
|
|
651
|
+
if (!steps) { el.hidden = true; el.innerHTML = ''; return; }
|
|
652
|
+
var here = trailName(location.hash, crumbDetail);
|
|
653
|
+
el.innerHTML = '<ol>' + steps.map(function (s, i) {
|
|
654
|
+
return '<li class="trail-step"><a href="' + esc(s.hash) + '" data-trail-back="' + s.back + '">' + esc(s.label) + '</a></li>' +
|
|
655
|
+
(i === 0 ? '<li class="trail-more" aria-hidden="true" hidden>…</li>' : '');
|
|
656
|
+
}).join('') + '<li class="trail-here" aria-current="page"><span>' + esc(here) + '</span></li></ol>';
|
|
657
|
+
el.hidden = false;
|
|
658
|
+
var items = Array.prototype.slice.call(el.querySelectorAll('.trail-step,.trail-here')), more = el.querySelector('.trail-more');
|
|
659
|
+
if (!more || !el.clientWidth) return;
|
|
660
|
+
el.classList.add('trail-fit'); // each step at its full width, to measure
|
|
661
|
+
more.hidden = false;
|
|
662
|
+
var moreW = more.offsetWidth, widths = items.map(function (li) { return li.offsetWidth; });
|
|
663
|
+
more.hidden = true;
|
|
664
|
+
el.classList.remove('trail-fit');
|
|
665
|
+
var cut = trailCut(widths, moreW, el.clientWidth);
|
|
666
|
+
// A left-out step stays for a screen reader but takes no keyboard focus (Q1 L1): it is not on screen.
|
|
667
|
+
cut.forEach(function (i) { items[i].classList.add('trail-cut'); var a = items[i].querySelector('a'); if (a) a.setAttribute('tabindex', '-1'); });
|
|
668
|
+
more.hidden = !cut.length;
|
|
669
|
+
}
|
|
670
|
+
// Where Back goes from here: { pane, path } (path true: the way he came; false: the parent chain).
|
|
671
|
+
function navBackTo() {
|
|
672
|
+
var st = navState();
|
|
673
|
+
// navStamp has run on this entry (every landing routes), so its mark is this load's: an earlier load's was re-marked
|
|
674
|
+
// with no path there.
|
|
675
|
+
if (!navOverlay(st) && st && st.cosNav && st.cosNav.back) {
|
|
676
|
+
var p = String(st.cosNav.back).replace(/^#\/?/, '').split('/')[0];
|
|
677
|
+
return { pane: ROOT_PANES.indexOf(p) >= 0 ? p : 'today', path: true };
|
|
678
|
+
}
|
|
679
|
+
var r = parseRoute(), up = PANES[r.pane] && PANES[r.pane].up ? PANES[r.pane].up(r.args) : null;
|
|
680
|
+
return { pane: up || (r.args.length && !isTabRoute(r.args)) ? r.pane : 'today', path: false };
|
|
681
|
+
}
|
|
682
|
+
// The one way this page replaces an address (W.replace for the panes; Q1 7.0.30 H1). It tells navStamp the entry was
|
|
683
|
+
// replaced, never pushed, so a replaced entry never gets a path: a pane switching a tab with location.replace after a
|
|
684
|
+
// Back (a forward entry left) was stamped as a push at history.length - 1, and Back then called history.back() from
|
|
685
|
+
// the first entry, out of the app. Its position is this entry's mark, or one above the last mark when an overlay's
|
|
686
|
+
// entry (Today's sheet, the image viewer) is the one replaced. Returns false, doing nothing, when the address is the
|
|
687
|
+
// same (location.replace would not route, and the flag would wrongly mark the next address).
|
|
688
|
+
function navReplace(hash) {
|
|
689
|
+
if (location.hash === hash) return false;
|
|
690
|
+
var st = navState(), pos;
|
|
691
|
+
if (navOverlay(st)) pos = NAV.last && typeof NAV.last.pos === 'number' ? NAV.last.pos + 1 : history.length - 1;
|
|
692
|
+
else pos = st && st.cosNav && typeof st.cosNav.pos === 'number' ? st.cosNav.pos : history.length - 1;
|
|
693
|
+
NAV.replace = { pos: pos };
|
|
694
|
+
try { location.replace(location.pathname + location.search + hash); } catch (_e) { NAV.replace = null; location.hash = hash; }
|
|
695
|
+
return true;
|
|
696
|
+
}
|
|
697
|
+
// The parent chain, replacing this entry (never adding one).
|
|
698
|
+
function goReplace(pane, args) {
|
|
699
|
+
var hash = '#/' + [pane].concat(args || []).map(encodeURIComponent).join('/');
|
|
700
|
+
if (!navReplace(hash)) route();
|
|
701
|
+
}
|
|
702
|
+
// ── end back by path
|
|
576
703
|
// 7.0.2 (A8): a phone is under 600 px wide, or a short touch screen at any width (the unfolded Duo is 951x539 in
|
|
577
704
|
// Comet): height is its scarce axis, so it gets the phone Ask sheet and the 44 px tap floor (app.css), as folded does.
|
|
578
705
|
var SHORT_TOUCH_MQ = '(pointer: coarse) and (max-height: 720px)';
|
|
@@ -615,7 +742,7 @@
|
|
|
615
742
|
// The rail changes the reply box's width: an empty box re-fits its placeholder (autoGrow, F2).
|
|
616
743
|
var si = typeof $ === 'function' ? $('session-input') : null; if (si && typeof sxPlaceholderFit === 'function' && si.phLong) sxPlaceholderFit(si); else if (si && !si.value && typeof autoGrow === 'function') autoGrow(si);
|
|
617
744
|
// A change after boot (a fold, a rotation, a new screen) is reported once, with the screen's own numbers.
|
|
618
|
-
if (RAIL.on !== null && RAIL.on !== on && typeof layoutCrumb === 'function') { var sc = window.screen || {}; layoutCrumb('rail', { sw: sc.width, sh: sc.height, rail: on, railHit: RAIL.hit }); }
|
|
745
|
+
if (RAIL.on !== null && RAIL.on !== on && typeof layoutCrumb === 'function') { var sc = window.screen || {}; layoutCrumb('rail', { sw: sc.width, sh: sc.height, rail: on, railHit: RAIL.hit, dvh: typeof lrProbe === 'function' ? lrProbe('height:100dvh', 'h') : null }); }
|
|
619
746
|
RAIL.on = on;
|
|
620
747
|
if (!RAIL.hooked && typeof window.matchMedia === 'function' && typeof window.addEventListener === 'function') {
|
|
621
748
|
RAIL.hooked = true;
|
|
@@ -639,6 +766,7 @@
|
|
|
639
766
|
return !!(e.metaKey || e.ctrlKey || keyboardFirst());
|
|
640
767
|
}
|
|
641
768
|
function route(forceChanged) {
|
|
769
|
+
if (typeof navStamp === 'function') navStamp();
|
|
642
770
|
var r = parseRoute();
|
|
643
771
|
var changedPane = forceChanged === true || r.pane !== state.pane || !document.querySelector('#pane-' + r.pane + ':not(.hidden)');
|
|
644
772
|
if (state.pane !== r.pane && PANES[state.pane] && PANES[state.pane].leave) { try { PANES[state.pane].leave(); } catch (err) { console.error(err); } }
|
|
@@ -654,6 +782,7 @@
|
|
|
654
782
|
|
|
655
783
|
var reading = r.args.length > 0 && !isTabRoute(r.args);
|
|
656
784
|
if (typeof sxFrameSync === 'function') sxFrameSync();
|
|
785
|
+
if (typeof rdFrameSync === 'function') rdFrameSync(r);
|
|
657
786
|
var paneEl = $('pane-' + r.pane);
|
|
658
787
|
if (paneEl) paneEl.classList.toggle('reading', reading);
|
|
659
788
|
var split = paneEl && paneEl.querySelector('.split');
|
|
@@ -664,8 +793,18 @@
|
|
|
664
793
|
var railBack = $('rail-back');
|
|
665
794
|
if (railBack) railBack.hidden = !reading;
|
|
666
795
|
$('section-btn').classList.toggle('hidden', reading);
|
|
667
|
-
|
|
796
|
+
// 7.0.30: Back names where it goes, the way he came or the parent (navBackTo).
|
|
797
|
+
var backTo = typeof navBackTo === 'function' ? navBackTo() : { pane: r.pane };
|
|
798
|
+
$('topbar-back-label').textContent = TITLES[backTo.pane] || 'Back';
|
|
799
|
+
var topBack = $('topbar-back');
|
|
800
|
+
if (topBack && topBack.setAttribute) topBack.setAttribute('aria-label', 'Back to ' + (TITLES[backTo.pane] || 'the list'));
|
|
801
|
+
if (railBack && railBack.setAttribute) railBack.setAttribute('aria-label', 'Back to ' + (TITLES[backTo.pane] || 'the list'));
|
|
802
|
+
// From 600 px Back is the navigation bar's (shell.js): its word stays "Back", as on the Mac's bar, and its name and
|
|
803
|
+
// tooltip say where it goes, as the rail's do (7.0.30 counted run, items_check B1 and B2 at the open Duo).
|
|
804
|
+
var navBack = $('nav-back');
|
|
805
|
+
if (navBack && navBack.setAttribute) { navBack.setAttribute('aria-label', 'Back to ' + (TITLES[backTo.pane] || 'the list')); navBack.setAttribute('title', 'Back to ' + (TITLES[backTo.pane] || 'the list')); }
|
|
668
806
|
$('section-name').textContent = TITLES[r.pane] || 'Today';
|
|
807
|
+
if (typeof paintTrail === 'function') paintTrail(r);
|
|
669
808
|
closeMenu();
|
|
670
809
|
if (changedPane) window.scrollTo(0, 0);
|
|
671
810
|
$('topbar').classList.remove('tucked');
|
|
@@ -701,26 +840,32 @@
|
|
|
701
840
|
runHooks(routeHooks, r);
|
|
702
841
|
}
|
|
703
842
|
function runHooks(list, arg) { list.forEach(function (fn) { try { fn(arg); } catch (err) { console.error(err); } }); }
|
|
704
|
-
// Back
|
|
705
|
-
// nested reader (an archive chat goes back to its day,
|
|
843
|
+
// Back (7.0.30): the way he came, when this page made the entry behind (navBackTo). Otherwise one step up, the way
|
|
844
|
+
// Control's Back does: a pane module may name the step above a nested reader (an archive chat goes back to its day,
|
|
845
|
+
// not to the list), replacing this entry.
|
|
706
846
|
function goBack() {
|
|
847
|
+
if (typeof navBackTo === 'function' && navBackTo().path) { history.back(); return; }
|
|
707
848
|
var r = parseRoute();
|
|
708
849
|
var up = PANES[r.pane] && PANES[r.pane].up ? PANES[r.pane].up(r.args) : null;
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
else
|
|
850
|
+
var step = typeof goReplace === 'function' ? goReplace : go;
|
|
851
|
+
if (up) step(r.pane, up);
|
|
852
|
+
else if (r.args.length && !isTabRoute(r.args)) step(r.pane);
|
|
853
|
+
else step('today');
|
|
712
854
|
}
|
|
713
855
|
// '#/<pane>/t/<tab>' is the pane's list on one of its tabs, not a reader: the phone keeps the section switcher and Back
|
|
714
856
|
// leaves the pane. Memories keeps its tab in the address this way, so a reload or a new browser lands on the same tab.
|
|
715
857
|
function isTabRoute(args) { return args.length === 2 && args[0] === 't'; }
|
|
716
|
-
// On
|
|
717
|
-
//
|
|
858
|
+
// On a phone every vertical pixel counts: the top bar slides away while you scroll down a list and comes back the
|
|
859
|
+
// moment you scroll up. 7.0.30 (item 1): never on a reader, where the top bar holds Back, so Back stays in reach on a
|
|
860
|
+
// tall phone however far down he reads.
|
|
718
861
|
var lastY = 0;
|
|
719
862
|
function onScroll() {
|
|
720
863
|
var y = Math.max(0, window.scrollY || 0);
|
|
721
|
-
|
|
864
|
+
// On a reader the bar stays (its Back is there). The folded Duo's Back is on the side rail (the bar's is hidden by CSS,
|
|
865
|
+
// not the class), so there the bar tucks as before (Q1 7.0.30 M1).
|
|
866
|
+
var bar = $('topbar'), back = $('topbar-back'), onReader = !!(back && !back.classList.contains('hidden')) && !(typeof sideRail === 'function' && sideRail());
|
|
722
867
|
bar.classList.toggle('scrolled', y > 8);
|
|
723
|
-
if (y < 60 || y < lastY - 4) bar.classList.remove('tucked');
|
|
868
|
+
if (y < 60 || y < lastY - 4 || onReader) bar.classList.remove('tucked');
|
|
724
869
|
else if (y > lastY + 4 && isPhone()) bar.classList.add('tucked');
|
|
725
870
|
lastY = y;
|
|
726
871
|
}
|
|
@@ -760,7 +905,8 @@
|
|
|
760
905
|
// 7.0.23 regression (lapse_check, 08:2x): a frame class left on keeps html and body at one screen with overflow
|
|
761
906
|
// hidden, so the gate could not scroll and Connect sat below a keyboard-height screen. The gate is a normal page;
|
|
762
907
|
// showShell's route() puts the frame back (sxFrameSync) when the shell returns.
|
|
763
|
-
document.documentElement.classList.remove('sx-frame'
|
|
908
|
+
document.documentElement.classList.remove('sx-frame');
|
|
909
|
+
document.documentElement.classList.remove('rd-frame');
|
|
764
910
|
// A later 401 from a pane still polling (W.api passes no message) must not blank what the gate already says:
|
|
765
911
|
// the reason it opened, or "That code did not match" while the person retypes.
|
|
766
912
|
if (!message && alreadyOpen) return;
|
|
@@ -2588,7 +2734,7 @@
|
|
|
2588
2734
|
var dest = session ? ['sessions', s[0], s.slice(1).join('|')] : kind === 'pet' ? ['sessions'] : ['work'].concat(kind === 'need' ? [] : ['sort']);
|
|
2589
2735
|
console.error('[today] the overlay did not open (' + kind + '); opening ' + dest.join('/') + ' instead.', err);
|
|
2590
2736
|
toast(session ? 'Could not open that here. Opening the session.' : kind === 'pet' ? 'Could not open that here. Opening Sessions.' : 'Could not open that here. Opening it in Work.');
|
|
2591
|
-
if (history.state && history.state.cosTodayOverlay)
|
|
2737
|
+
if (history.state && history.state.cosTodayOverlay) navReplace('#/' + dest.map(encodeURIComponent).join('/'));
|
|
2592
2738
|
else go(dest[0], dest.slice(1));
|
|
2593
2739
|
});
|
|
2594
2740
|
}
|
|
@@ -2791,6 +2937,150 @@
|
|
|
2791
2937
|
if (on) sxFrameHook();
|
|
2792
2938
|
return on;
|
|
2793
2939
|
}
|
|
2940
|
+
// 7.0.31 hotfix (Miles 17:25, the open Duo): the message reader in its own frame from 600 px. html.rd-frame makes the
|
|
2941
|
+
// page one screen, as Sessions' frame does (panes/cos-chat.css): the app shell is the visible box (--vv-top, --vv-h
|
|
2942
|
+
// from viewportFit, which falls back to innerHeight when the visual viewport reads 0 or less), and the reader scrolls
|
|
2943
|
+
// in the workspace under the navigation bar, with Reply pinned to its bottom. Before, the reader was the document: its Reply stuck to the
|
|
2944
|
+
// document's scrollport and the rail was 100dvh, so when Safari's viewport went stale after folds (his Duo reported
|
|
2945
|
+
// visual viewport heights of 0 and -33554432 from 17:17 to 17:24) both ended mid-screen and the message drew below
|
|
2946
|
+
// them. Below 600 px (phones, the folded Duo's side rail) the reader is unchanged; the CSS is gated the same way, so a
|
|
2947
|
+
// class left on across a fold does nothing there. A message's image (#/messages/m/<no>/a/<id>) has no Reply: no frame.
|
|
2948
|
+
function rdFrameOn(r) {
|
|
2949
|
+
var shell = $('app-shell'), wide = !!(window.matchMedia && window.matchMedia('(min-width: 600px)').matches);
|
|
2950
|
+
return !!(r && r.pane === 'messages' && r.args && r.args[0] === 'm' && r.args.length === 2 && wide && !(shell && shell.classList.contains('hidden')));
|
|
2951
|
+
}
|
|
2952
|
+
var RD_FRAME = { hooked: false, n: 0 };
|
|
2953
|
+
function rdFrameSync(r) {
|
|
2954
|
+
var de = document.documentElement, on = rdFrameOn(r || parseRoute());
|
|
2955
|
+
if (de && de.classList) de.classList.toggle('rd-frame', on);
|
|
2956
|
+
if (!RD_FRAME.hooked && window.matchMedia) {
|
|
2957
|
+
RD_FRAME.hooked = true;
|
|
2958
|
+
var mq = window.matchMedia('(min-width: 600px)');
|
|
2959
|
+
if (mq && mq.addEventListener) mq.addEventListener('change', function () { rdFrameSync(); }); else if (mq && mq.addListener) mq.addListener(function () { rdFrameSync(); });
|
|
2960
|
+
}
|
|
2961
|
+
var r2 = r || parseRoute();
|
|
2962
|
+
rdPosHook();
|
|
2963
|
+
// A different message just opened: its place is its top until he scrolls. (This also runs when the width crosses
|
|
2964
|
+
// 600 px; the same message then keeps its anchor and its pending restore.)
|
|
2965
|
+
if (window.requestAnimationFrame) window.requestAnimationFrame(function () {
|
|
2966
|
+
var cx = rdPosReader(), key = cx ? cx.getAttribute('data-reader-key') : '';
|
|
2967
|
+
if (!RD_POS.a || RD_POS.a.key !== key) { RD_POS.pending = false; rdPosCapture(); }
|
|
2968
|
+
});
|
|
2969
|
+
if (r2 && r2.pane === 'messages' && r2.args && r2.args[0] === 'm' && r2.args.length === 2 && window.requestAnimationFrame) window.requestAnimationFrame(function () { rdFrameCrumb('open'); });
|
|
2970
|
+
return on;
|
|
2971
|
+
}
|
|
2972
|
+
// One report per message reader opened, at any width, at most 8 a page load (numbers only; lead 17:48): whether the frame
|
|
2973
|
+
// is on, the visible box viewportFit uses, the reader's box (top, bottom, height), the workspace's height and content height (the frame's scroller), Reply's top
|
|
2974
|
+
// and bottom, the document's height and scroll, and 100dvh. layoutCrumb adds innerWidth and innerHeight and the visual
|
|
2975
|
+
// viewport's height, top and scale, so a pan like 17:25's (top about 409) shows in the same row. The 17:25 frame could
|
|
2976
|
+
// not be measured; the next one will be.
|
|
2977
|
+
function rdFrameCrumb(why) {
|
|
2978
|
+
if (RD_FRAME.n >= 8 || typeof layoutCrumb !== 'function') return false;
|
|
2979
|
+
try {
|
|
2980
|
+
var v = viewportFit(), f = document.querySelector('#pane-messages .cx-reply-form'), cx = document.querySelector('#pane-messages > .cx'), ws = $('workspace');
|
|
2981
|
+
var fr = f && f.getBoundingClientRect ? f.getBoundingClientRect() : null, cr = cx && cx.getBoundingClientRect ? cx.getBoundingClientRect() : null;
|
|
2982
|
+
var se = document.scrollingElement || document.documentElement;
|
|
2983
|
+
var out = { why: String(why || '').slice(0, 24), frame: !!(document.documentElement.classList && document.documentElement.classList.contains('rd-frame')), fb: !!v.bad,
|
|
2984
|
+
boxT: v.top, boxB: v.top + v.height, rdT: cr ? cr.top : -1, rdB: cr ? cr.bottom : -1, rdH: cx ? cx.clientHeight : -1, wsH: ws ? ws.clientHeight : -1, wsSH: ws ? ws.scrollHeight : -1,
|
|
2985
|
+
replyT: fr ? fr.top : -1, replyB: fr ? fr.bottom : -1, docH: se ? se.scrollHeight : -1, sy: window.scrollY || 0, dvh: typeof lrProbe === 'function' ? lrProbe('height:100dvh', 'h') : null };
|
|
2986
|
+
if (layoutCrumb('reader', out)) { RD_FRAME.n++; return true; }
|
|
2987
|
+
} catch (_e) { /* a report never stops the page */ }
|
|
2988
|
+
return false;
|
|
2989
|
+
}
|
|
2990
|
+
// 7.0.31 (Q1 FIX FIRST, lead 20:41): ONE job. A fold or an unfold that crosses 600 px switches the message between the
|
|
2991
|
+
// page's scroll (under 600) and the frame's workspace (600 and up), and the new scroller would open at the top. So the
|
|
2992
|
+
// page keeps an anchor, the character at the top of the reading area, read as he scrolls, and applies it ONCE after a
|
|
2993
|
+
// switch: when no finger is down and the scroll has been still for RD_POS_STILL_MS. If he scrolls on purpose first (a
|
|
2994
|
+
// touch that moves, the wheel, a key), his scroll wins and the anchor is dropped. Nothing else moves the text: no
|
|
2995
|
+
// other resize, no keyboard, no card or notice. Under 600 px with no switch, the reader is 7.0.30's.
|
|
2996
|
+
var RD_POS_STILL_MS = 300, RD_POS_MOVE_PX = 10;
|
|
2997
|
+
var RD_POS = { a: null, mode: '', pending: false, timer: 0, touching: false, ty: 0, moved: false, scrollAt: 0, queued: false, hooked: false };
|
|
2998
|
+
function rdPosReader() {
|
|
2999
|
+
var r = parseRoute();
|
|
3000
|
+
return r && r.pane === 'messages' && r.args && r.args[0] === 'm' && r.args.length === 2 ? document.querySelector('#pane-messages > .cx[data-reader-key]') : null;
|
|
3001
|
+
}
|
|
3002
|
+
function rdPosMode() { var ws = $('workspace'); return ws && getComputedStyle(ws).overflowY === 'auto' ? 'frame' : 'page'; }
|
|
3003
|
+
// The top of the reading area: the workspace's top in the frame; on the page, below the pinned top bar.
|
|
3004
|
+
function rdPosTop(mode) {
|
|
3005
|
+
if (mode === 'frame') return $('workspace').getBoundingClientRect().top;
|
|
3006
|
+
var t = 0;
|
|
3007
|
+
['.topbar', '.navbar'].forEach(function (sel) {
|
|
3008
|
+
var e = document.querySelector(sel), st = e && getComputedStyle(e), r = e && e.getBoundingClientRect();
|
|
3009
|
+
if (e && st.display !== 'none' && r.height > 0 && r.bottom > 0 && (st.position === 'sticky' || st.position === 'fixed')) t = Math.max(t, r.bottom);
|
|
3010
|
+
});
|
|
3011
|
+
return t;
|
|
3012
|
+
}
|
|
3013
|
+
// One character's line box (a collapsed range has none in every engine).
|
|
3014
|
+
function rdPosRect(node, offset) {
|
|
3015
|
+
var len = node && node.nodeValue ? node.nodeValue.length : 0;
|
|
3016
|
+
if (!len) return null;
|
|
3017
|
+
var rg = document.createRange(), at = Math.min(offset, len - 1);
|
|
3018
|
+
rg.setStart(node, at); rg.setEnd(node, at + 1);
|
|
3019
|
+
var r = rg.getBoundingClientRect();
|
|
3020
|
+
return r.height || r.width ? r : null;
|
|
3021
|
+
}
|
|
3022
|
+
function rdPosCapture() {
|
|
3023
|
+
var cx = rdPosReader();
|
|
3024
|
+
if (!cx) { RD_POS.a = null; RD_POS.mode = ''; return null; }
|
|
3025
|
+
var mode = rdPosMode(), top = rdPosTop(mode), key = cx.getAttribute('data-reader-key');
|
|
3026
|
+
RD_POS.mode = mode;
|
|
3027
|
+
if ((mode === 'frame' ? $('workspace').scrollTop : window.scrollY || 0) <= 2) return (RD_POS.a = { key: key, atTop: true });
|
|
3028
|
+
var c = cx.getBoundingClientRect(), body = cx.querySelector('.cx-said.cos .cx-card-body'), xs = [c.left + 8, c.left + c.width / 2];
|
|
3029
|
+
if (body) xs.unshift(body.getBoundingClientRect().left + 6);
|
|
3030
|
+
for (var dy = 2; dy <= 62; dy += 12) {
|
|
3031
|
+
for (var i = 0; i < xs.length; i++) {
|
|
3032
|
+
var hit = document.caretRangeFromPoint ? document.caretRangeFromPoint(xs[i], top + dy) : null, n = hit && hit.startContainer;
|
|
3033
|
+
if (!n || n.nodeType !== 3 || !cx.contains(n) || n.parentElement.closest('.cx-reply-form')) continue;
|
|
3034
|
+
var r = rdPosRect(n, hit.startOffset);
|
|
3035
|
+
if (r) return (RD_POS.a = { key: key, node: n, offset: hit.startOffset, dy: r.top - top });
|
|
3036
|
+
}
|
|
3037
|
+
}
|
|
3038
|
+
return null;
|
|
3039
|
+
}
|
|
3040
|
+
function rdPosApply() {
|
|
3041
|
+
var cx = rdPosReader(), a = RD_POS.a, mode = rdPosMode(), ws = $('workspace');
|
|
3042
|
+
RD_POS.mode = mode;
|
|
3043
|
+
if (!cx || !a || a.key !== cx.getAttribute('data-reader-key')) return false;
|
|
3044
|
+
if (a.atTop) { if (mode === 'frame') ws.scrollTop = 0; else window.scrollTo(0, 0); return true; }
|
|
3045
|
+
var r = a.node.isConnected ? rdPosRect(a.node, a.offset) : null;
|
|
3046
|
+
if (!r) return false;
|
|
3047
|
+
var want = Math.round(r.top - rdPosTop(mode) - a.dy);
|
|
3048
|
+
if (mode === 'frame') ws.scrollTop += want; else window.scrollBy(0, want);
|
|
3049
|
+
return true;
|
|
3050
|
+
}
|
|
3051
|
+
// The scroller changed under him: wait for no finger and a still scroll, then apply once; his own scroll drops it.
|
|
3052
|
+
function rdPosTick() {
|
|
3053
|
+
RD_POS.timer = 0;
|
|
3054
|
+
if (!RD_POS.pending) return;
|
|
3055
|
+
if (RD_POS.moved) { RD_POS.pending = false; rdPosCapture(); return; }
|
|
3056
|
+
if (RD_POS.touching || Date.now() - RD_POS.scrollAt < RD_POS_STILL_MS) { RD_POS.timer = setTimeout(rdPosTick, 80); return; }
|
|
3057
|
+
RD_POS.pending = false;
|
|
3058
|
+
rdPosApply();
|
|
3059
|
+
}
|
|
3060
|
+
function rdPosCheck() {
|
|
3061
|
+
var mode = rdPosReader() ? rdPosMode() : '';
|
|
3062
|
+
if (!mode || !RD_POS.mode || mode === RD_POS.mode || RD_POS.pending) return;
|
|
3063
|
+
RD_POS.pending = true; RD_POS.moved = false;
|
|
3064
|
+
if (!RD_POS.timer) RD_POS.timer = setTimeout(rdPosTick, 80);
|
|
3065
|
+
}
|
|
3066
|
+
function rdPosHook() {
|
|
3067
|
+
if (RD_POS.hooked || !document.addEventListener) return;
|
|
3068
|
+
RD_POS.hooked = true;
|
|
3069
|
+
var o = { capture: true, passive: true };
|
|
3070
|
+
document.addEventListener('scroll', function () {
|
|
3071
|
+
RD_POS.scrollAt = Date.now();
|
|
3072
|
+
if (!rdPosReader()) return;
|
|
3073
|
+
rdPosCheck();
|
|
3074
|
+
if (RD_POS.pending || RD_POS.queued) return;
|
|
3075
|
+
RD_POS.queued = true;
|
|
3076
|
+
requestAnimationFrame(function () { RD_POS.queued = false; rdPosCheck(); if (!RD_POS.pending) rdPosCapture(); });
|
|
3077
|
+
}, o);
|
|
3078
|
+
window.addEventListener('resize', rdPosCheck);
|
|
3079
|
+
document.addEventListener('touchstart', function (e) { RD_POS.touching = true; RD_POS.ty = e.touches && e.touches[0] ? e.touches[0].clientY : 0; }, o);
|
|
3080
|
+
document.addEventListener('touchmove', function (e) { var y = e.touches && e.touches[0] ? e.touches[0].clientY : RD_POS.ty; if (RD_POS.pending && Math.abs(y - RD_POS.ty) > RD_POS_MOVE_PX) RD_POS.moved = true; }, o);
|
|
3081
|
+
['touchend', 'touchcancel'].forEach(function (t) { document.addEventListener(t, function () { RD_POS.touching = false; }, o); });
|
|
3082
|
+
['wheel', 'keydown'].forEach(function (t) { document.addEventListener(t, function () { if (RD_POS.pending) RD_POS.moved = true; }, o); });
|
|
3083
|
+
}
|
|
2794
3084
|
// Where the frame's parts sit against the VISIBLE area (step 1's position report, lead 23:00): on a reader's open, and
|
|
2795
3085
|
// KB_AFTER_MS after an on-screen keyboard closes while the frame is on. The visible area is viewportFit's box (a
|
|
2796
3086
|
// visible height under 80 px is no reading, so it is innerHeight from 0, and fb says so) and, across, visualViewport's
|
|
@@ -5591,7 +5881,7 @@
|
|
|
5591
5881
|
// estimates into them, and its rows keep Control's category titles.
|
|
5592
5882
|
function loadUsage() {
|
|
5593
5883
|
SX.view = 'usage'; sxSave();
|
|
5594
|
-
|
|
5884
|
+
navReplace('#/sessions');
|
|
5595
5885
|
}
|
|
5596
5886
|
function renderUsageView(force) {
|
|
5597
5887
|
var box = $('sessions-usage'); if (!box) return;
|
|
@@ -7366,11 +7656,13 @@
|
|
|
7366
7656
|
|
|
7367
7657
|
// ── events (one delegated listener) ─────────────────────
|
|
7368
7658
|
document.addEventListener('click', function (e) {
|
|
7369
|
-
var t = e.target.closest('[data-today-next],[data-next-prep],[data-next-prep-cancel],[data-next-prep-remove],[data-today-needs],[data-today],[data-goto],[data-pane],[data-session],[data-meeting],[data-speakers],[data-action],[data-open-ask],[data-theme-choice],#topbar-back,#rail-back,#section-btn,#sign-out,#copy-link,#ask-close,#auth-switch,#auth-token-show,#auth-retry,#device-code-btn');
|
|
7659
|
+
var t = e.target.closest('[data-today-next],[data-next-prep],[data-next-prep-cancel],[data-next-prep-remove],[data-today-needs],[data-today],[data-goto],[data-pane],[data-session],[data-meeting],[data-speakers],[data-action],[data-open-ask],[data-theme-choice],[data-trail-back],#topbar-back,#rail-back,#section-btn,#sign-out,#copy-link,#ask-close,#auth-switch,#auth-token-show,#auth-retry,#device-code-btn');
|
|
7370
7660
|
if (!t) { if (e.target.id === 'ask-sheet') closeAsk(); if (e.target.id === 'section-menu') closeMenu(); return; }
|
|
7371
7661
|
var inSheet = !!t.closest('#ask-sheet');
|
|
7372
7662
|
if (t.id === 'topbar-back') return goBack();
|
|
7373
7663
|
if (t.id === 'rail-back') return goBack();
|
|
7664
|
+
// 7.0.30 (pick B): a trail step is the browser's own Back, that many steps.
|
|
7665
|
+
if (t.hasAttribute('data-trail-back')) { e.preventDefault(); history.go(-Number(t.getAttribute('data-trail-back'))); return; }
|
|
7374
7666
|
if (t.id === 'section-btn') return openMenu();
|
|
7375
7667
|
if (t.id === 'auth-retry') { t.disabled = true; t.textContent = 'Checking'; bootStatus().then(function () { t.disabled = false; t.textContent = 'Try again'; }); return; }
|
|
7376
7668
|
if (t.id === 'auth-switch') {
|
|
@@ -7786,7 +8078,7 @@
|
|
|
7786
8078
|
plural: plural, countWord: countWord, dayWord: dayWord, plainDay: plainDay, providerLabel: providerLabel, domainLabel: domainLabel,
|
|
7787
8079
|
skeletonRows: skeletonRows, emptyBox: emptyBox, setStatus: setStatus, autoGrow: autoGrow,
|
|
7788
8080
|
readStore: readStore, writeStore: writeStore, copyText: copyText, isPhone: isPhone,
|
|
7789
|
-
state: state, titles: TITLES, go: go, parseRoute: parseRoute, goBack: goBack,
|
|
8081
|
+
state: state, titles: TITLES, go: go, replace: navReplace, parseRoute: parseRoute, goBack: goBack,
|
|
7790
8082
|
registerPane: function (name, def) { PANES[name] = def; },
|
|
7791
8083
|
sessionList: sessionList,
|
|
7792
8084
|
petPrefs: petPrefs,
|
|
@@ -7796,7 +8088,7 @@
|
|
|
7796
8088
|
onShell: function (fn) { shellHooks.push(fn); },
|
|
7797
8089
|
// The detail title after "COS Control > Section" (Control's breadcrumb). Pane modules set it
|
|
7798
8090
|
// once a detail has loaded; route() clears it on every navigation.
|
|
7799
|
-
setCrumb: function (text) { crumbDetail = String(text || ''); runHooks(routeHooks, parseRoute()); },
|
|
8091
|
+
setCrumb: function (text) { crumbDetail = String(text || ''); navLabel(crumbDetail); paintTrail(); runHooks(routeHooks, parseRoute()); },
|
|
7800
8092
|
crumb: function () { return crumbDetail; },
|
|
7801
8093
|
// Panes that read with their own fetch call this on a 401 so the sign-in screen shows.
|
|
7802
8094
|
showAuth: function (message) { showAuth(message); },
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* COS Control Web: Control's own chrome and pane kit, loaded after app.css.
|
|
2
2
|
Measurements follow Control 0.5.366 (ActivityWindow.swift, COSBrand.swift): the 248pt rail with a
|
|
3
3
|
story under each name from 1100px, the icon rail below that, the navigation bar off Today, and
|
|
4
|
-
the shared pane pieces (tinted hero, text switch with a gold rule, rows, tags,
|
|
4
|
+
the shared pane pieces (tinted hero, text switch with a gold rule, rows, tags, and the reader's sides for who said what).
|
|
5
5
|
Phone sizes are set for reading on the iPhone Duo; the desktop steps down toward Control's. */
|
|
6
6
|
|
|
7
7
|
:root {
|
|
@@ -236,6 +236,18 @@
|
|
|
236
236
|
.cx-card { background: var(--panel); border: 1px solid var(--panel-border); border-radius: 14px; padding: 12px 14px 14px; margin-bottom: 12px; }
|
|
237
237
|
.cx-card.you { background: var(--qopen); }
|
|
238
238
|
.cx-card-label { font: 600 9.5px/1 var(--mono); letter-spacing: 0.08em; color: var(--tertiary); margin-bottom: 8px; display: flex; justify-content: space-between; align-items: center; gap: 10px; }
|
|
239
|
+
/* 7.0.30, Miles's pick A (who said what): sides, in the COS chat's shapes (.cc-bubble, .cc-line.cos). His bubble's edge
|
|
240
|
+
is opaque in light, gold 85% with ink, 3.48:1 against the page; the chat's 45% gold edge composites to 1.52:1 there.
|
|
241
|
+
Dark keeps the chat's edge as it is (2.79:1 composited). */
|
|
242
|
+
:root { --said-edge: color-mix(in srgb, var(--gold) 85%, var(--ink)); }
|
|
243
|
+
:root[data-theme='dark'] { --said-edge: color-mix(in srgb, var(--gold) 45%, transparent); }
|
|
244
|
+
@media (prefers-color-scheme: dark) { :root:not([data-theme='light']) { --said-edge: color-mix(in srgb, var(--gold) 45%, transparent); } }
|
|
245
|
+
.cx-said.you { display: flex; flex-direction: column; align-items: flex-end; margin: 4px 0 16px; }
|
|
246
|
+
.cx-said-bubble { max-width: min(80%, 560px); background: color-mix(in srgb, var(--gold) 22%, var(--panel)); border: 1px solid var(--said-edge);
|
|
247
|
+
border-radius: 18px 18px 6px 18px; padding: 10px 14px; line-height: 1.45; }
|
|
248
|
+
.cx-said-who { font-size: 12px; color: var(--sub); margin: 4px 4px 0; }
|
|
249
|
+
.cx-said.cos { display: grid; grid-template-columns: 30px minmax(0, 1fr); gap: 10px; align-items: start; margin: 0 0 14px; }
|
|
250
|
+
.cx-said-text { min-width: 0; }
|
|
239
251
|
.cx-card-body { font-size: 15px; line-height: 1.5; color: var(--ink); overflow-wrap: anywhere; }
|
|
240
252
|
/* md() keeps line breaks as text (shared with the session reader), so the body keeps them too. */
|
|
241
253
|
.cx-card-body.md { white-space: pre-wrap; }
|
|
@@ -262,9 +274,27 @@
|
|
|
262
274
|
.cx-find .cx-find-count { font: 400 11.5px/1 var(--mono); color: var(--sub); }
|
|
263
275
|
.cx-find button { width: 30px; height: 30px; border-radius: 8px; border: 1px solid var(--panel-border); display: grid; place-items: center; color: var(--ink); }
|
|
264
276
|
.cx-find button .icon { width: 14px; height: 14px; }
|
|
265
|
-
.cx-turn.match .cx-
|
|
277
|
+
.cx-turn.match .cx-said-bubble, .cx-turn.match .cx-said-text { background: color-mix(in srgb, var(--green) 7%, var(--panel)); }
|
|
278
|
+
.cx-turn.match .cx-said-text { border-radius: 10px; }
|
|
266
279
|
@media (max-width: 599px) { .cx-find { top: calc(var(--topbar-h) + var(--safe-top)); } .cx-thumb { width: 132px; height: 92px; } }
|
|
267
280
|
|
|
281
|
+
/* 7.0.30, Miles's pick B (how you got here): one plain row of text links under the top bar, on a phone only (from 600 px
|
|
282
|
+
the navigation bar has its crumbs). No box, border or label. It never wraps: steps leave from the middle (app.js
|
|
283
|
+
trailCut) and a long name ends in an ellipsis. A left-out step stays for a screen reader. */
|
|
284
|
+
.trail { display: none; }
|
|
285
|
+
@media (max-width: 599px) {
|
|
286
|
+
.trail:not([hidden]) { display: block; margin: 0 0 2px; }
|
|
287
|
+
.trail ol { display: flex; align-items: center; list-style: none; margin: 0; padding: 0; overflow: hidden; white-space: nowrap; font-size: 13.5px; color: var(--sub); }
|
|
288
|
+
.trail li { display: flex; align-items: center; min-width: 0; flex: 0 1 auto; }
|
|
289
|
+
.trail li + li::before { content: '/'; content: '/' / ''; flex: none; padding: 0 6px; color: var(--tertiary); }
|
|
290
|
+
.trail a, .trail-here span { display: block; min-width: 0; overflow: hidden; text-overflow: ellipsis; line-height: 32px; }
|
|
291
|
+
.trail a { color: var(--accent); }
|
|
292
|
+
.trail-here span { color: var(--ink); font-weight: 600; }
|
|
293
|
+
.trail .trail-more[hidden] { display: none; }
|
|
294
|
+
.trail.trail-fit li { flex-shrink: 0; }
|
|
295
|
+
.trail .trail-cut { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
|
|
296
|
+
}
|
|
297
|
+
|
|
268
298
|
/* ── Dictation and Dictionary ─────────────────────────── */
|
|
269
299
|
.cx-pair { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 6px; }
|
|
270
300
|
.cx-pair .heard { color: var(--sub); text-decoration: line-through; text-decoration-color: color-mix(in srgb, var(--sub) 60%, transparent); }
|
|
@@ -173,6 +173,8 @@
|
|
|
173
173
|
</header>
|
|
174
174
|
|
|
175
175
|
<main class="workspace" id="workspace">
|
|
176
|
+
<!-- 7.0.30, Miles's pick B: how you got here, on a phone (app.js paintTrail), from Back's own marks. -->
|
|
177
|
+
<nav class="trail" id="nav-trail" aria-label="How you got here" hidden></nav>
|
|
176
178
|
<!-- W1: the Mac not answering, and the sign-in ending soon (app.js "Mac reachability and sign-in lifetime") -->
|
|
177
179
|
<div id="conn-notice" class="conn-notice" hidden></div>
|
|
178
180
|
<!-- SESSION QUESTIONS (panes/questions.js, 6.99.12): over every pane while one waits -->
|
|
@@ -215,7 +215,32 @@ html.kb-up .cc-kb .cc-model .field { width: 100%; }
|
|
|
215
215
|
.cx-reply-sub { margin: 2px 0 0; font-size: 13px; line-height: 1.4; color: var(--sub); }
|
|
216
216
|
.cx-reply-feed { min-height: 0; padding: 10px 0 0; }
|
|
217
217
|
.cx-reply-feed:empty { display: none; }
|
|
218
|
-
|
|
218
|
+
/* 7.0.30 item 2: the Reply box is the reader's last child, outside .cx-reply, so .cc-composer's own rule pins it to the
|
|
219
|
+
bottom of the screen (sticky, the page's colour, a hairline above). With the keyboard up for it (cos-chat.js sets
|
|
220
|
+
html.kb-reply; app.js publishes --vv-below, the layout viewport under the visible box) it sits on the keyboard, the
|
|
221
|
+
same way the chat's box does; the home indicator is under the keyboard, so its inset goes. */
|
|
222
|
+
html.kb-reply .cx-reply-form { bottom: var(--vv-below, 0px); padding-bottom: 8px; }
|
|
223
|
+
/* 7.0.31 hotfix (Miles 17:25, the open Duo): from 600 px the message reader is its own frame (app.js rdFrameSync sets
|
|
224
|
+
html.rd-frame on #/messages/m/<no>). The page is one screen, never taller, as in Sessions (panes/sessions.css): the
|
|
225
|
+
app shell is the visible box and the rail a column of it that scrolls by itself. The workspace under the navigation
|
|
226
|
+
bar is the one thing that scrolls: the sign-in notice, a question card and the message scroll together as they did
|
|
227
|
+
in 7.0.30, and Reply sticks to the workspace's bottom. Before, the reader was the document, so Reply stuck to the
|
|
228
|
+
document's viewport and the rail was 100dvh: with Safari's view left panned or short after folds, both ended
|
|
229
|
+
mid-screen and the message drew below them. Nothing here reaches under 600 px, where the phone and the folded Duo
|
|
230
|
+
keep the reader as shipped. */
|
|
231
|
+
@media (min-width: 600px) {
|
|
232
|
+
html.rd-frame, html.rd-frame body { height: 100%; overflow: hidden; overscroll-behavior: none; }
|
|
233
|
+
html.rd-frame body { min-height: 0; }
|
|
234
|
+
html.rd-frame .app-shell { position: fixed; left: 0; right: 0; top: var(--vv-top, 0px); height: var(--vv-h, 100dvh); min-height: 0;
|
|
235
|
+
grid-template-rows: minmax(0, 1fr); overflow: hidden; }
|
|
236
|
+
html.rd-frame .rail { position: static; top: auto; height: auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
|
|
237
|
+
html.rd-frame .main-col { min-height: 0; min-width: 0; display: flex; flex-direction: column; overflow: hidden; }
|
|
238
|
+
html.rd-frame .topbar, html.rd-frame .navbar { flex: none; position: relative; }
|
|
239
|
+
/* The scroller. No bottom padding: at the end of the message, Reply is the last thing and sits on the frame's bottom. */
|
|
240
|
+
html.rd-frame .workspace { flex: 1 1 auto; min-height: 0; min-width: 0; overflow-y: auto; overscroll-behavior: contain; padding-bottom: 0; }
|
|
241
|
+
/* With the keyboard up the frame is already the visible box (--vv-top, --vv-h), so the box stays at the frame's bottom. */
|
|
242
|
+
html.rd-frame.kb-reply .cx-reply-form { bottom: 0; }
|
|
243
|
+
}
|
|
219
244
|
|
|
220
245
|
/* ── the Messages badge in the rail and the section menu ─ */
|
|
221
246
|
.inbox-count { min-width: 18px; height: 18px; padding: 0 5px; border-radius: 9px; background: var(--hot); color: var(--panel); font: 600 11px/18px var(--body); text-align: center; }
|
|
@@ -1549,8 +1549,11 @@
|
|
|
1549
1549
|
return '<section class="cc cx-reply" data-reply-host="' + esc(key) + '" aria-label="' + esc(label) + '">' +
|
|
1550
1550
|
'<h3 class="cx-reply-title">Reply</h3>' +
|
|
1551
1551
|
'<p class="cx-reply-sub">' + esc(k.sessionId ? 'Continues this conversation. The answer lands in Messages with its own number.' : 'Starts a conversation. The answer lands in Messages with its own number.') + '</p>' +
|
|
1552
|
-
'<div class="cc-feed cx-reply-feed" data-reply-feed="' + esc(key) + '" aria-live="polite">' + replyFeedHtml(key) + '</div>' +
|
|
1553
|
-
|
|
1552
|
+
'<div class="cc-feed cx-reply-feed" data-reply-feed="' + esc(key) + '" aria-live="polite">' + replyFeedHtml(key) + '</div>' + '</section>' +
|
|
1553
|
+
// 7.0.30 item 2 (Anand: "the reply box stays at the bottom"): the box sits OUTSIDE the section, as a child of the
|
|
1554
|
+
// reader's column, so the composer's own sticky rule keeps it at the bottom of the screen while he reads the
|
|
1555
|
+
// message above (a sticky box cannot leave its parent, and the section is only at the end of the page).
|
|
1556
|
+
composerHtml(k, { drop: false, placeholder: label, label: label }).replace('class="cc-composer"', 'class="cc-composer cx-reply-form"');
|
|
1554
1557
|
}
|
|
1555
1558
|
function replyFeedHtml(key) {
|
|
1556
1559
|
return C.asks.filter(function (a) { return a.replyKey === replyName(key); }).map(function (a) { return askHtml(a, false); }).join('');
|
|
@@ -2396,6 +2399,9 @@
|
|
|
2396
2399
|
if (form && form.classList) form.classList.add('cc-kb');
|
|
2397
2400
|
var inChat = !!(a && C.host && C.host.contains && C.host.contains(a));
|
|
2398
2401
|
if (de && de.classList) de.classList.toggle('kb-chat', inChat);
|
|
2402
|
+
// 7.0.30 item 2: the message reader's Reply box, the same kit (cos-chat.css lifts it onto the keyboard).
|
|
2403
|
+
var inReply = !!(form && form.classList && typeof form.classList.contains === 'function' && form.classList.contains('cx-reply-form'));
|
|
2404
|
+
if (de && de.classList) de.classList.toggle('kb-reply', inReply);
|
|
2399
2405
|
return inChat;
|
|
2400
2406
|
}
|
|
2401
2407
|
// The composer that has the keyboard, brought into view on open and on every later change of the visible box (a fold
|
|
@@ -2403,7 +2409,7 @@
|
|
|
2403
2409
|
// (a steer box or the search box is left where it is).
|
|
2404
2410
|
function revealComposer() {
|
|
2405
2411
|
var a = document.activeElement, de = document.documentElement;
|
|
2406
|
-
if (!a || !de || !de.classList || !de.classList.contains('kb-chat')) return false;
|
|
2412
|
+
if (!a || !de || !de.classList || !(de.classList.contains('kb-chat') || de.classList.contains('kb-reply'))) return false;
|
|
2407
2413
|
var form = a.closest ? a.closest('[data-cos-form]') : null;
|
|
2408
2414
|
if (!form || !form.scrollIntoView) return false;
|
|
2409
2415
|
form.scrollIntoView({ block: 'nearest' });
|
|
@@ -28,6 +28,10 @@
|
|
|
28
28
|
.mt-day:not(:disabled):hover { background: var(--qopen); }
|
|
29
29
|
.mt-day[aria-pressed='true'] { background: color-mix(in srgb, var(--tint) 18%, transparent); font-weight: 600; }
|
|
30
30
|
.mt-day:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
|
|
31
|
+
/* 7.0.30 item 4: today is marked with a ring in the theme's accent (no fill, so a picked day still reads as picked). */
|
|
32
|
+
.mt-day.mt-today { box-shadow: inset 0 0 0 1.5px var(--accent); }
|
|
33
|
+
.mt-day.mt-today .mt-dnum { font-weight: 600; }
|
|
34
|
+
.mt-jump-row { margin: 0 2px 12px; }
|
|
31
35
|
.mt-dot { width: 5px; height: 5px; border-radius: 50%; background: transparent; }
|
|
32
36
|
.mt-dot.on { background: var(--tint); }
|
|
33
37
|
.mt-all { margin-top: 8px; }
|