@gotcos/glasses-server 7.0.27 → 7.0.30
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 +93 -0
- package/docs/web-key-verifier-contract.md +34 -8
- package/package.json +1 -1
- package/server/index.ts +29 -6
- package/server/lib/provider-failure-text.ts +4 -1
- package/server/lib/tailnet-self-name.ts +57 -10
- package/server/lib/web-key-authority.ts +1 -1
- package/server/lib/web-key-registry.ts +1 -1
- package/server/lib/web-key-signer-app.ts +25 -6
- package/server/lib/web-key-signer-tls.ts +800 -0
- package/server/routes/web-key.ts +31 -2
- package/server/web-control/.manifest.json +16 -16
- package/server/web-control/app.css +58 -2
- package/server/web-control/app.js +478 -51
- package/server/web-control/control.css +32 -2
- package/server/web-control/index.html +46 -5
- package/server/web-control/panes/cos-chat.css +5 -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-control/theme.js +169 -2
- 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
|
@@ -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); }
|
|
@@ -13,6 +13,42 @@
|
|
|
13
13
|
<link rel="icon" href="/control/mark.svg" type="image/svg+xml">
|
|
14
14
|
<!-- iOS "Add to Home Screen" ignores SVG icons: without this, a saved Control Web shortcut is a grey letter. -->
|
|
15
15
|
<link rel="apple-touch-icon" href="/control/apple-touch-icon.png">
|
|
16
|
+
<!-- 7.0.28: the sign-in card and the first paint must look right before, or without, the 10 stylesheets below
|
|
17
|
+
(Miles 19:28: "Connect this browser" with no styling, the mark twice at full size, when the phone opened the page
|
|
18
|
+
before it could reach the Mac). These rules are copies of app.css, word for word (a test compares them), so the
|
|
19
|
+
stylesheets that follow change nothing when they arrive; only the tokens it needs, and the dark set also follows
|
|
20
|
+
the system when theme.js has not run. theme.js retries a stylesheet that failed. -->
|
|
21
|
+
<style id="critical-css">
|
|
22
|
+
:root { --page: #f5f0e6; --ink: #1f1712; --sub: #66635e; --panel: #ffffff; --panel-border: #e3d9c6; --accent: #89662d; --on-accent: #ffffff; --btn-bg: #ffffff; --btn-border: #a08a66; --grey: #5c5a55; --display: 'Fraunces', 'Iowan Old Style', Georgia, serif; --body: 'DM Sans', -apple-system, BlinkMacSystemFont, 'Helvetica Neue', sans-serif; --gutter: 16px; --radius: 12px; --safe-top: env(safe-area-inset-top, 0px); --safe-bottom: env(safe-area-inset-bottom, 0px); }
|
|
23
|
+
:root[data-theme='dark'] { --page: #17120e; --ink: #efe7dc; --sub: #b3a796; --panel: #221b15; --panel-border: #3a2f25; --accent: #d9b06a; --on-accent: #17120e; --btn-bg: #221b15; --btn-border: #7a6650; --grey: #a39887; }
|
|
24
|
+
@media (prefers-color-scheme: dark) { :root:not([data-theme]) { --page: #17120e; --ink: #efe7dc; --sub: #b3a796; --panel: #221b15; --panel-border: #3a2f25; --accent: #d9b06a; --on-accent: #17120e; --btn-bg: #221b15; --btn-border: #7a6650; --grey: #a39887; } }
|
|
25
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
26
|
+
html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; background: var(--page); }
|
|
27
|
+
body { margin: 0; min-height: 100dvh; background: var(--page); color: var(--ink); font: 400 16px/1.45 var(--body); -webkit-font-smoothing: antialiased; -webkit-tap-highlight-color: transparent; }
|
|
28
|
+
button, input, select, textarea { font: inherit; color: inherit; }
|
|
29
|
+
button { cursor: pointer; background: none; border: 0; padding: 0; text-align: inherit; }
|
|
30
|
+
.hidden { display: none !important; }
|
|
31
|
+
.mark { width: 28px; height: 28px; color: var(--ink); flex: none; }
|
|
32
|
+
.first-paint { display: none; }
|
|
33
|
+
html.fp-wait .first-paint { position: fixed; inset: 0; z-index: 70; display: grid; place-items: center; background: var(--page); color: var(--ink); pointer-events: none; animation: fp-failsafe 1ms linear 25s forwards; }
|
|
34
|
+
.first-paint .mark { width: 44px; height: 44px; }
|
|
35
|
+
@keyframes fp-failsafe { to { visibility: hidden; } }
|
|
36
|
+
.noscript-note { margin: 0; min-height: 100dvh; display: grid; place-items: center; padding: 24px var(--gutter); font-size: 17px; color: var(--ink); text-align: center; }
|
|
37
|
+
.auth-gate { min-height: 100dvh; display: grid; place-items: center; padding: calc(var(--safe-top) + 24px) var(--gutter) calc(var(--safe-bottom) + 24px); }
|
|
38
|
+
.auth-card { width: 100%; max-width: 420px; }
|
|
39
|
+
.auth-card .mark { width: 44px; height: 44px; margin-bottom: 22px; }
|
|
40
|
+
.auth-card h1 { font: 500 40px/1.05 var(--display); margin: 0 0 10px; letter-spacing: -0.01em; }
|
|
41
|
+
.auth-card p { color: var(--sub); margin: 0 0 24px; }
|
|
42
|
+
.auth-card label { display: block; font-weight: 600; font-size: 15px; margin-bottom: 8px; }
|
|
43
|
+
.auth-card .btn { margin-top: 12px; }
|
|
44
|
+
.auth-foot { font-size: 14px; color: var(--sub); margin-top: 18px; }
|
|
45
|
+
.auth-offline p { color: var(--sub); }
|
|
46
|
+
.field { width: 100%; min-height: 48px; padding: 12px 14px; border-radius: var(--radius); border: 1px solid var(--panel-border); background: var(--panel); font-size: 16px; /* 16px keeps iOS Safari from zooming on focus */ }
|
|
47
|
+
.btn { display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-height: 44px; padding: 0 18px; border-radius: var(--radius); border: 1px solid var(--btn-border); background: var(--btn-bg); font-weight: 600; font-size: 15px; white-space: nowrap; transition: transform 120ms ease; }
|
|
48
|
+
.btn.primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
|
|
49
|
+
.btn.block { width: 100%; }
|
|
50
|
+
.link-btn { color: var(--accent); font-weight: 600; font-size: 15px; min-height: 44px; display: inline-flex; align-items: center; }
|
|
51
|
+
</style>
|
|
16
52
|
<link rel="stylesheet" href="/control/app.css">
|
|
17
53
|
<link rel="stylesheet" href="/control/control.css">
|
|
18
54
|
<link rel="stylesheet" href="/control/panes/speakers.css">
|
|
@@ -64,16 +100,16 @@
|
|
|
64
100
|
<!-- 7.0.19: the first paint. Shows at once, before any script or answer from the Mac: the mark on the page's own
|
|
65
101
|
background, true whichever comes next (the sign-in card or the app). Out of the layout (fixed), so nothing moves
|
|
66
102
|
when it goes. app.js removes it; a CSS failsafe hides it after 25 s if the scripts never run. -->
|
|
67
|
-
<div id="first-paint" class="first-paint" aria-hidden="true"><svg class="mark"><use href="#i-mark"/></svg></div>
|
|
103
|
+
<div id="first-paint" class="first-paint" aria-hidden="true"><svg class="mark" width="44" height="44"><use href="#i-mark"/></svg></div>
|
|
68
104
|
<noscript><p class="noscript-note">COS Control Web needs JavaScript. Turn it on for this site, then reload.</p></noscript>
|
|
69
105
|
|
|
70
106
|
<main id="auth-gate" class="auth-gate hidden" aria-labelledby="auth-title">
|
|
71
107
|
<div class="auth-card">
|
|
72
|
-
<svg class="mark" aria-hidden="true"><use href="#i-mark"/></svg>
|
|
108
|
+
<svg class="mark" width="44" height="44" aria-hidden="true"><use href="#i-mark"/></svg>
|
|
73
109
|
<h1 id="auth-title">Connect this browser</h1>
|
|
74
110
|
<p id="auth-intro">COS runs on your Mac. Sign in once. You stay signed in while you use COS here, for up to 30 days.</p>
|
|
75
111
|
<div id="auth-offline" class="auth-offline hidden" role="status">
|
|
76
|
-
<p id="auth-offline-text">Your Mac may be asleep, or this device may be offline or off Tailscale. This page checks again every 30 seconds.</p>
|
|
112
|
+
<p id="auth-offline-text">Your Mac may be asleep, or this device may be offline or off Tailscale. This page checks again in a few seconds, then every 30 seconds.</p>
|
|
77
113
|
<button type="button" class="btn primary block" id="auth-retry">Try again</button>
|
|
78
114
|
</div>
|
|
79
115
|
<form id="auth-form">
|
|
@@ -98,7 +134,7 @@
|
|
|
98
134
|
|
|
99
135
|
<div id="app-shell" class="app-shell hidden">
|
|
100
136
|
<nav class="rail" aria-label="Sections">
|
|
101
|
-
<div class="rail-head"><svg class="mark" aria-hidden="true"><use href="#i-mark"/></svg><span class="rail-brand">Control</span></div>
|
|
137
|
+
<div class="rail-head"><svg class="mark" width="28" height="28" aria-hidden="true"><use href="#i-mark"/></svg><span class="rail-brand">Control</span></div>
|
|
102
138
|
<button type="button" class="rail-btn rail-today" data-pane="today" aria-label="Today"><svg class="icon"><use href="#i-today"/></svg><span class="rail-text"><span class="rail-label">Today</span><span class="rail-story" data-story="today"></span></span><span class="badge-dot hidden" data-badge="today"></span></button>
|
|
103
139
|
<div class="rail-sep"></div>
|
|
104
140
|
<button type="button" class="rail-btn" data-pane="messages" aria-label="Messages"><svg class="icon"><use href="#i-messages"/></svg><span class="rail-text"><span class="rail-label">Messages</span><span class="rail-story" data-story="messages"></span></span><span class="badge-dot hidden" data-badge="messages"></span></button>
|
|
@@ -112,6 +148,9 @@
|
|
|
112
148
|
<div class="rail-spacer"></div>
|
|
113
149
|
<button type="button" class="rail-settings" data-pane="settings" aria-label="Settings"><svg class="icon"><use href="#i-settings"/></svg><span class="rail-label">Settings</span></button>
|
|
114
150
|
<p class="server-line" data-server-line><span class="dot ok" data-host-dot></span><span class="server-text" data-server-text>Server connected</span></p>
|
|
151
|
+
<!-- 7.0.29: the folded Duo's side rail only (app.css, html.short-screen): the top bar's Back, at thumb height. Shown
|
|
152
|
+
where the top bar shows Back today, on a reader, and it calls the same goBack. Never drawn from 600 px. -->
|
|
153
|
+
<button type="button" class="rail-back" id="rail-back" aria-label="Back" hidden><svg class="icon"><use href="#i-back"/></svg></button>
|
|
115
154
|
</nav>
|
|
116
155
|
|
|
117
156
|
<div class="main-col">
|
|
@@ -127,13 +166,15 @@
|
|
|
127
166
|
<div class="topbar-left">
|
|
128
167
|
<button type="button" id="topbar-back" class="topbar-back hidden"><svg class="icon"><use href="#i-back"/></svg><span id="topbar-back-label">Back</span></button>
|
|
129
168
|
<button type="button" id="section-btn" class="section-btn" aria-haspopup="dialog" aria-controls="section-menu">
|
|
130
|
-
<svg class="mark" aria-hidden="true"><use href="#i-mark"/></svg><span id="section-name">Today</span><span class="caret" aria-hidden="true"></span>
|
|
169
|
+
<svg class="mark" width="28" height="28" aria-hidden="true"><use href="#i-mark"/></svg><span id="section-name">Today</span><span class="caret" aria-hidden="true"></span>
|
|
131
170
|
</button>
|
|
132
171
|
</div>
|
|
133
172
|
<button type="button" class="icon-btn" data-open-ask aria-label="Ask COS or find a session"><svg class="icon"><use href="#i-ask"/></svg></button>
|
|
134
173
|
</header>
|
|
135
174
|
|
|
136
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>
|
|
137
178
|
<!-- W1: the Mac not answering, and the sign-in ending soon (app.js "Mac reachability and sign-in lifetime") -->
|
|
138
179
|
<div id="conn-notice" class="conn-notice" hidden></div>
|
|
139
180
|
<!-- SESSION QUESTIONS (panes/questions.js, 6.99.12): over every pane while one waits -->
|
|
@@ -215,7 +215,11 @@ 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; }
|
|
219
223
|
|
|
220
224
|
/* ── the Messages badge in the rail and the section menu ─ */
|
|
221
225
|
.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; }
|
|
@@ -525,10 +525,12 @@
|
|
|
525
525
|
for (var w = 0; w < 7; w++) heads += '<span class="mt-wd" aria-hidden="true">' + esc(new Date(2023, 0, 1 + (ws + w) % 7).toLocaleDateString(undefined, { weekday: 'narrow' })) + '</span>';
|
|
526
526
|
var cells = '', n = 0;
|
|
527
527
|
for (var i = 0; i < lead; i++) { cells += '<span class="mt-blank"></span>'; n++; }
|
|
528
|
+
// 7.0.30 item 4: today is marked (a ring, aria-current) and can be picked even with no meetings.
|
|
529
|
+
var todayKey = dayKey(new Date());
|
|
528
530
|
for (var day = 1; day <= dim; day++) {
|
|
529
|
-
var key = S.month + '-' + pad2(day), c = counts[key] || 0, on = S.day === key;
|
|
530
|
-
cells += '<button type="button" class="mt-day" data-mt="day" data-day="' + key + '" aria-pressed="' + on + '"' + (c === 0 ? ' disabled' : '') +
|
|
531
|
-
' aria-label="' + esc(key + (c === 0 ? ', no meetings' : ', ' + c + ' meetings')) + '"><span class="mt-dnum">' + day + '</span><span class="mt-dot' + (c > 0 ? ' on' : '') + '"></span></button>';
|
|
531
|
+
var key = S.month + '-' + pad2(day), c = counts[key] || 0, on = S.day === key, isToday = key === todayKey;
|
|
532
|
+
cells += '<button type="button" class="mt-day' + (isToday ? ' mt-today' : '') + '" data-mt="day" data-day="' + key + '" aria-pressed="' + on + '"' + (isToday ? ' aria-current="date"' : '') + (c === 0 && !isToday ? ' disabled' : '') +
|
|
533
|
+
' aria-label="' + esc(key + (isToday ? ', today' : '') + (c === 0 ? ', no meetings' : ', ' + c + ' meetings')) + '"><span class="mt-dnum">' + day + '</span><span class="mt-dot' + (c > 0 ? ' on' : '') + '"></span></button>';
|
|
532
534
|
n++;
|
|
533
535
|
}
|
|
534
536
|
while (n % 7 !== 0) { cells += '<span class="mt-blank"></span>'; n++; }
|
|
@@ -594,6 +596,20 @@
|
|
|
594
596
|
(m.sessionId ? pillsHtml(m, isNew(m.sessionId)) : '') + '</span><span class="chev"></span></button>';
|
|
595
597
|
}
|
|
596
598
|
function emptyBox(text) { return '<div class="cx-list"><p class="cx-empty mt-empty">' + esc(text) + '</p></div>'; }
|
|
599
|
+
// 7.0.30 item 4: today with no meetings says so in plain words, with one tap to the latest earlier day that has some
|
|
600
|
+
// (this month), or to last month when this month has none. The calendar stays as it is.
|
|
601
|
+
function latestDayBefore(key) {
|
|
602
|
+
var best = '';
|
|
603
|
+
(S.days || []).forEach(function (d) { if (num(d.count) > 0 && d.date < key && d.date > best) best = d.date; });
|
|
604
|
+
return best;
|
|
605
|
+
}
|
|
606
|
+
function emptyToday() {
|
|
607
|
+
var last = latestDayBefore(dayKey(new Date()));
|
|
608
|
+
var jump = last
|
|
609
|
+
? '<button type="button" class="cx-btn quiet mt-jump" data-mt="jump-day" data-day="' + esc(last) + '">Go to ' + esc(W.plainDay ? W.plainDay(last) : last) + '</button>'
|
|
610
|
+
: '<button type="button" class="cx-btn quiet mt-jump" data-mt="month" data-delta="-1">Go to last month</button>';
|
|
611
|
+
return '<div class="cx-list"><p class="cx-empty mt-empty">No meetings today.</p><p class="mt-jump-row">' + jump + '</p></div>';
|
|
612
|
+
}
|
|
597
613
|
function paintList() {
|
|
598
614
|
var el = $('mt-list'); if (!el) return;
|
|
599
615
|
var html;
|
|
@@ -611,6 +627,7 @@
|
|
|
611
627
|
var rows = visibleRows();
|
|
612
628
|
html = rows.length ? '<div class="cx-list">' + rows.map(function (m) { return rowHtml(m, false); }).join('') + '</div>' +
|
|
613
629
|
(monthCapped() ? '<p class="mt-capnote">Showing the newest ' + S.rows.length + ' of ' + monthTotal() + ' in ' + esc(monthTitle(S.month)) + '. Pick a day in the calendar to see all of its meetings.</p>' : '')
|
|
630
|
+
: S.day && S.day === dayKey(new Date()) ? emptyToday()
|
|
614
631
|
: emptyBox(S.day ? 'No meetings stored on ' + S.day + '. Pick another day, or show the whole month.' : 'No meetings stored in ' + monthTitle(S.month) + '.');
|
|
615
632
|
}
|
|
616
633
|
if (S.listHtml === html) return;
|
|
@@ -643,9 +660,10 @@
|
|
|
643
660
|
S.days = days;
|
|
644
661
|
var kept = S.dayRows; S.dayRows = {};
|
|
645
662
|
if (S.day && kept[S.day]) S.dayRows[S.day] = kept[S.day];
|
|
663
|
+
// 7.0.30 item 4 (Miles 10:15: "Meetings should SHOW the current day"): the current month opens on today, with
|
|
664
|
+
// meetings or without (the list then says so and offers the latest day that has some).
|
|
646
665
|
if (!S.autoDay && S.month === monthKey(new Date())) {
|
|
647
|
-
|
|
648
|
-
if (days.some(function (x) { return x.date === today && num(x.count) > 0; })) S.day = today;
|
|
666
|
+
S.day = dayKey(new Date());
|
|
649
667
|
S.autoDay = true;
|
|
650
668
|
}
|
|
651
669
|
S.error = ''; S.loading = false;
|
|
@@ -2089,7 +2107,7 @@
|
|
|
2089
2107
|
var hit = lists.reduce(function (all, l) { return all.concat(l); }, []).filter(function (m) { return m.sessionId === f.sid || (m.g2SessionIds || []).indexOf(f.sid) >= 0; })[0];
|
|
2090
2108
|
if (hit && S.view === 'live' && S.liveSid === f.sid) {
|
|
2091
2109
|
S.rowsStale = true;
|
|
2092
|
-
|
|
2110
|
+
W.replace('#/meetings/' + [hit.domain, hit.month, hit.filename].map(encodeURIComponent).join('/'));
|
|
2093
2111
|
} else if (!hit && f.endedAt && now - (f.retryAt || f.endedAt) >= FINAL_DEADLINE_MS + (f.start ? Math.max(0, f.endedAt - f.start) / 12 : 0)) {
|
|
2094
2112
|
f.notFound = true; paintLiveHead();
|
|
2095
2113
|
}
|
|
@@ -2667,6 +2685,7 @@
|
|
|
2667
2685
|
case 'month': shiftMonth(Number(t.getAttribute('data-delta')) || 0); return;
|
|
2668
2686
|
case 'day': { var d = t.getAttribute('data-day'); S.day = S.day === d ? null : d; S.autoDay = true; paintBrowse(); ensureDay(); return; }
|
|
2669
2687
|
case 'all-month': S.day = null; S.autoDay = true; paintBrowse(); return;
|
|
2688
|
+
case 'jump-day': { S.day = t.getAttribute('data-day'); S.autoDay = true; paintBrowse(); ensureDay(); return; }
|
|
2670
2689
|
case 'clear': {
|
|
2671
2690
|
S.q = ''; S.hits = null; S.ranFor = ''; S.searching = false; S.searchError = ''; clearTimeout(S.qTimer);
|
|
2672
2691
|
var q = $('mt-q'); if (q) { q.value = ''; q.focus(); }
|
|
@@ -105,7 +105,7 @@
|
|
|
105
105
|
function readerArgs(r) { return r.pane === 'memories' && !tabRoute(r.args) ? r.args : []; }
|
|
106
106
|
function showTab(v) {
|
|
107
107
|
var h = '#/memories/t/' + encodeURIComponent(v);
|
|
108
|
-
if (location.hash === h) paint(); else
|
|
108
|
+
if (location.hash === h) paint(); else W.replace(h);
|
|
109
109
|
}
|
|
110
110
|
function isWide() { return !!(window.matchMedia && window.matchMedia('(min-width: 760px)').matches); }
|
|
111
111
|
function el(id) { return document.getElementById(id); }
|
|
@@ -2262,7 +2262,7 @@
|
|
|
2262
2262
|
// #/memories/view/<view> (older links) and a bare #/memories become the tab's own address; an unknown tab, the stored one.
|
|
2263
2263
|
if (args[0] === 'view' || !args.length || (args[0] === 't' && !knownView(args[1]))) {
|
|
2264
2264
|
if (args[0] === 'view' && knownView(args[1])) { S.view = args[1]; persist(); }
|
|
2265
|
-
|
|
2265
|
+
W.replace('#/memories/t/' + encodeURIComponent(S.view)); return;
|
|
2266
2266
|
}
|
|
2267
2267
|
if (tabRoute(args) && S.view !== args[1]) { S.view = args[1]; persist(); }
|
|
2268
2268
|
if (args[0] === 'k') { S.view = 'knowledge'; if (KTABS.indexOf(args[1]) >= 0) { S.ktab = args[1]; S.ktabChosen = true; } persist(); }
|
|
@@ -469,9 +469,14 @@
|
|
|
469
469
|
}
|
|
470
470
|
function turnCards(t, msgKey) {
|
|
471
471
|
var a = atts(t);
|
|
472
|
-
|
|
472
|
+
// 7.0.30, Miles's pick A: who said what by side, in the COS chat's own shapes. His words on the right in the chat's
|
|
473
|
+
// gold bubble with "You" under it; COS on the left beside its mark, on the page. A screen reader hears "You said"
|
|
474
|
+
// and "COS said" first, since the side and the mark are visual only.
|
|
475
|
+
return '<div class="cx-said you"><span class="sr-only">You said</span><div class="cx-said-bubble cx-card-body" data-hl>' + esc(String(t.query || '').trim() || '(empty query)').replace(/\n/g, '<br>') + '</div>' +
|
|
476
|
+
'<span class="cx-said-who" aria-hidden="true">You</span></div>' +
|
|
473
477
|
stripHtml(a.filter(isUserAtt), true, msgKey) +
|
|
474
|
-
'<div class="cx-
|
|
478
|
+
'<div class="cx-said cos"><span class="cc-avatar" aria-hidden="true"><svg class="icon"><use href="#i-mark"/></svg></span><div class="cx-said-text"><span class="sr-only">COS said</span>' +
|
|
479
|
+
'<div class="cx-card-body md" data-hl>' + W.md(W.structureLensAnswer(String(t.text || ''))) + '</div></div></div>' +
|
|
475
480
|
stripHtml(a.filter(function (x) { return !isUserAtt(x); }), false, msgKey);
|
|
476
481
|
}
|
|
477
482
|
function copyTurnText(t) {
|
|
@@ -123,7 +123,7 @@
|
|
|
123
123
|
return '<section class="cx-set-cat"><h2 class="cx-col-title">This browser</h2>' +
|
|
124
124
|
group('Appearance', '', themeBtn('system', 'Match this device') + themeBtn('light', 'Light') + themeBtn('dark', 'Dark')) +
|
|
125
125
|
group('Sign-in', '',
|
|
126
|
-
'<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
|
|
126
|
+
'<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 phone’s 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>' +
|
|
127
127
|
'<div id="device-code-panel" class="device-code hidden" aria-live="polite"></div>' +
|
|
128
128
|
'<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>' +
|
|
129
129
|
'<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>' +
|
|
@@ -2363,8 +2363,11 @@
|
|
|
2363
2363
|
var acts = '';
|
|
2364
2364
|
if (rv && rv.recordId) acts += '<button type="button" class="cx-btn quiet" data-sp="open-meeting" title="Open this meeting’s transcript, summary and files">' + icon('meetings') + 'Open meeting</button>';
|
|
2365
2365
|
if (R.content && R.content.clipboardSummary) {
|
|
2366
|
-
|
|
2367
|
-
|
|
2366
|
+
// 7.0.30 item 3 (Anand: Summary and Full did not say they copy): the buttons say Copy, and the one tapped says
|
|
2367
|
+
// Copied for COPIED_MS (spCopied), while a status line the screen reader hears says what was copied.
|
|
2368
|
+
var cp = R.copied && Date.now() - R.copied.at < COPIED_MS ? R.copied.which : '';
|
|
2369
|
+
acts += '<button type="button" class="cx-btn quiet" id="sp-copy-summary" data-sp="copy-summary" title="Copy the meeting without the transcript">' + (cp === 'summary' ? 'Copied' : 'Copy summary') + '</button>' +
|
|
2370
|
+
'<button type="button" class="cx-btn quiet" id="sp-copy-full" data-sp="copy-full" title="Copy the meeting including the full transcript">' + (cp === 'full' ? 'Copied' : 'Copy full (' + Math.max(1, Math.floor(R.content.fullChars / 1024)) + ' KB)') + '</button>';
|
|
2368
2371
|
}
|
|
2369
2372
|
var next = S.meetings ? nextUnnamed(R.sid) : null;
|
|
2370
2373
|
acts += '<button type="button" class="cx-btn quiet" data-sp="next-unnamed"' + (next ? '' : ' disabled') + ' title="' +
|
|
@@ -2424,6 +2427,27 @@
|
|
|
2424
2427
|
|
|
2425
2428
|
// ── render dispatch ────────────────────────────────────
|
|
2426
2429
|
var expiryTimer = null;
|
|
2430
|
+
// 7.0.30 item 3: a copy confirms on its own button, then the button goes back to Copy; the words go to a status region
|
|
2431
|
+
// that lives outside the rebuilt pane, so a screen reader hears them (a region made in the same rebuild may not be).
|
|
2432
|
+
var COPIED_MS = 2500;
|
|
2433
|
+
function spAnnounce(text) {
|
|
2434
|
+
var live = document.getElementById('sp-copy-live');
|
|
2435
|
+
if (!live) {
|
|
2436
|
+
live = document.createElement('div');
|
|
2437
|
+
live.id = 'sp-copy-live'; live.className = 'sr-only';
|
|
2438
|
+
live.setAttribute('role', 'status'); live.setAttribute('aria-live', 'polite');
|
|
2439
|
+
document.body.appendChild(live);
|
|
2440
|
+
}
|
|
2441
|
+
live.textContent = '';
|
|
2442
|
+
setTimeout(function () { live.textContent = text; }, 30);
|
|
2443
|
+
}
|
|
2444
|
+
function spCopied(R, which, words) {
|
|
2445
|
+
var at = Date.now();
|
|
2446
|
+
R.copied = { which: which, at: at };
|
|
2447
|
+
render();
|
|
2448
|
+
spAnnounce(words);
|
|
2449
|
+
setTimeout(function () { if (R.copied && R.copied.at === at) { R.copied = null; render(); } }, COPIED_MS);
|
|
2450
|
+
}
|
|
2427
2451
|
function render() {
|
|
2428
2452
|
if (W.parseRoute().pane !== 'speakers') return;
|
|
2429
2453
|
var rt = route();
|
|
@@ -2641,7 +2665,8 @@
|
|
|
2641
2665
|
if (!text) { R.copyNote = 'Nothing to copy'; render(); return; }
|
|
2642
2666
|
W.copyText(text).then(function () {
|
|
2643
2667
|
var kb = Math.max(1, Math.floor((full ? c.fullChars : c.summaryChars) / 1024));
|
|
2644
|
-
R.copyNote = full ? 'Copied full meeting (' + kb + ' KB)' : 'Copied summary (' + kb + ' KB)';
|
|
2668
|
+
R.copyNote = full ? 'Copied full meeting (' + kb + ' KB)' : 'Copied summary (' + kb + ' KB)';
|
|
2669
|
+
spCopied(R, full ? 'full' : 'summary', R.copyNote);
|
|
2645
2670
|
}, function (err) { W.toast(err.message); });
|
|
2646
2671
|
return;
|
|
2647
2672
|
}
|
|
@@ -471,8 +471,8 @@
|
|
|
471
471
|
close('leave');
|
|
472
472
|
O.depth = 1;
|
|
473
473
|
if (!replace) { W.go(pane, args || []); return; }
|
|
474
|
-
if (extra) { quietBack(extra, function () {
|
|
475
|
-
|
|
474
|
+
if (extra) { quietBack(extra, function () { W.replace(hash); }); return; }
|
|
475
|
+
W.replace(hash);
|
|
476
476
|
}
|
|
477
477
|
// A Back step the overlay takes itself (X, Back to sessions, leaving for a tab): its popstate is ours, not the phone's
|
|
478
478
|
// Back, and `then` runs once it lands. If it never arrives (history.go with nothing to go back to, or a browser that
|
|
@@ -2813,7 +2813,7 @@
|
|
|
2813
2813
|
if (args[0] === 'sort') {
|
|
2814
2814
|
S.tab = 'sort'; S.sortGroupId = ''; S.sortIndex = 0; S.moreOpen = false;
|
|
2815
2815
|
if (!S.intake && !S.intakeLoading) loadIntake(true);
|
|
2816
|
-
|
|
2816
|
+
W.replace('#/work');
|
|
2817
2817
|
return;
|
|
2818
2818
|
}
|
|
2819
2819
|
startPoll(); resumeHandoffPolls();
|
|
@@ -263,7 +263,8 @@
|
|
|
263
263
|
}
|
|
264
264
|
crumbs.innerHTML = parts.join('');
|
|
265
265
|
$('nav-back').disabled = onToday;
|
|
266
|
-
//
|
|
266
|
+
// Below 600 px the section name gets the detail too. On a reader that label is hidden (the section button gives way
|
|
267
|
+
// to Back, which names where it goes), so this shows only on a list that has a detail.
|
|
267
268
|
var label = $('section-name');
|
|
268
269
|
if (label && detail && railHidden()) label.textContent = detail;
|
|
269
270
|
}
|