create-pmos 0.526.0 → 0.528.0
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/package.json +45 -31
- package/payload/KIT-VERSION +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/adler-moments-test.js +4 -3
- package/workspace/changelog.json +44 -0
- package/workspace/ev-everywhere-test.js +2 -6
- package/workspace/guide/home.md +7 -6
- package/workspace/guide/reports-set-up.md +2 -0
- package/workspace/lib/bg-queue.js +5 -2
- package/workspace/lib/briefing-premium-agent.js +9 -2
- package/workspace/lib/okr-goals.js +23 -3
- package/workspace/lib/report-chat.js +31 -4
- package/workspace/lib/routes-briefing-premium.js +18 -1
- package/workspace/lib/routes-morning-goals.js +5 -1
- package/workspace/news-viewers-test.js +2 -1
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/product-lane-test.js +1 -1
- package/workspace/program-charter-test.js +2 -2
- package/workspace/public/pm/bd-ini-panel.js +1 -1
- package/workspace/public/pm/bd-span.js +10 -7
- package/workspace/public/pm/cozy-board.css +1 -0
- package/workspace/public/pm/cozy-search.js +10 -17
- package/workspace/public/pm/cozy-settings.js +8 -2
- package/workspace/public/pm/cozy-today.css +30 -14
- package/workspace/public/pm/good-morning.js +30 -17
- package/workspace/public/pm/home-app.js +11 -1
- package/workspace/public/pm/home-chat.js +6 -2
- package/workspace/public/pm/home-data.js +4 -1
- package/workspace/public/pm/home-day.js +12 -5
- package/workspace/public/pm/premium-board.js +7 -5
- package/workspace/public/pm/premium-initiative-head.js +9 -1
- package/workspace/public/pm/premium-insights.js +5 -3
- package/workspace/public/pm/premium-report-read.js +11 -4
- package/workspace/public/pm/premium-report-tab.js +15 -3
- package/workspace/public/pm/program-model.js +2 -1
- package/workspace/public/pm/report-flow.js +7 -1
- package/workspace/public/premium-board.html +4 -4
- package/workspace/server.js +1 -1
- package/workspace/task-milestones-test.js +2 -2
- package/workspace/timeline-holds-test.js +3 -3
|
@@ -51,7 +51,10 @@ import { stageName } from './stage-chip.js'; // W1: the one stage
|
|
|
51
51
|
|
|
52
52
|
const MON = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
53
53
|
const day = (v) => { const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(v || '')); return m ? new Date(+m[1], +m[2] - 1, +m[3]) : null; };
|
|
54
|
-
|
|
54
|
+
/** Month and day only, for the axis ticks: the axis says the year once on its own (tickOf). */
|
|
55
|
+
const mdn = (d) => MON[d.getMonth()] + ' ' + d.getDate();
|
|
56
|
+
/** A date in words: "Nov 3", and the short year when it is not this year ("Jun 10 ’27", workspace grill W4). */
|
|
57
|
+
export const md = (d, now = new Date()) => mdn(d) + (d.getFullYear() !== now.getFullYear() ? ' ’' + String(d.getFullYear()).slice(2) : '');
|
|
55
58
|
/** A month and the short year: "Nov ’26" (2 yr, 5 yr and the edge tabs). */
|
|
56
59
|
export const my = (d) => MON[d.getMonth()] + ' ’' + String(d.getFullYear()).slice(2);
|
|
57
60
|
const addDays = (d, n) => new Date(d.getFullYear(), d.getMonth(), d.getDate() + n);
|
|
@@ -72,7 +75,7 @@ const colAdd = (u, d, n) => (u === 'w' ? addDays(d, 7 * n) : u === 'm' ? addMont
|
|
|
72
75
|
/** A column's head: its label, the part under it that carries no year, and its year (the first column on screen shows it, bd-scroll.js). */
|
|
73
76
|
function tickOf(u, d) {
|
|
74
77
|
const y = d.getFullYear(), q = Math.floor(d.getMonth() / 3) + 1, yy = '’' + String(y).slice(2);
|
|
75
|
-
if (u === 'w') return { at: d, label:
|
|
78
|
+
if (u === 'w') return { at: d, label: mdn(d), sub: d.getMonth() === 0 && d.getDate() <= 7 ? String(y) : '', base: '', y };
|
|
76
79
|
if (u === 'm') { const qs = d.getMonth() % 3 === 0 ? 'Q' + q : ''; return { at: d, label: MON[d.getMonth()], sub: qs ? qs + (q === 1 ? ' ' + yy : '') : '', base: qs, y }; }
|
|
77
80
|
if (u === 'q') return { at: d, label: 'Q' + q, sub: q === 1 ? yy : '', base: '', y };
|
|
78
81
|
return { at: d, label: String(y), sub: 'H1 · H2', base: 'H1 · H2', y, plain: true };
|
|
@@ -88,7 +91,7 @@ export function windowOf(span, now, phone, sprint, dates) {
|
|
|
88
91
|
const a = sprint && day(sprint.start), b = sprint && day(sprint.end);
|
|
89
92
|
const from = a && b && b > a ? a : mon, to = a && b && b > a ? addDays(b, 1) : addDays(mon, 14);
|
|
90
93
|
const n = Math.round((to - from) / 86400000), step = phone ? Math.max(2, Math.ceil(n / 4)) : n > 16 ? 2 : 1;
|
|
91
|
-
const ticks = []; for (let i = 0; i < n; i += step) { const d = addDays(from, i); ticks.push({ at: d, label:
|
|
94
|
+
const ticks = []; for (let i = 0; i < n; i += step) { const d = addDays(from, i); ticks.push({ at: d, label: mdn(d), sub: '', base: '', y: d.getFullYear() }); }
|
|
92
95
|
return { from, to, ticks, cols: ticks.length, fixed: true };
|
|
93
96
|
}
|
|
94
97
|
const S = SCALE[span] || SCALE.quarter, u = S.unit, cols = phone ? S.ph : S.cols;
|
|
@@ -197,7 +200,7 @@ function extentOf(r, now) {
|
|
|
197
200
|
}
|
|
198
201
|
|
|
199
202
|
/** A date as the calm timeline says it (batch 13 item 4): "Nov 3", and the year only when it is not this year ("Jun 10 ’27"). */
|
|
200
|
-
export const dy = (d, now) =>
|
|
203
|
+
export const dy = (d, now) => mdn(d) + (d.getFullYear() !== now.getFullYear() ? ' ’' + String(d.getFullYear()).slice(2) : '');
|
|
201
204
|
/** What lands at a row's end: its end milestone's name, else the ◆ stage of its own workflow the end stands for, else '' (no labelled ◇). */
|
|
202
205
|
const landsOf = (r, et) => (et ? et.name : endStage(r));
|
|
203
206
|
/** The one label at a bar's end (XcalmA1Desk): what lands, then when; tone '' | 'risk' | 'late' | 'q' colours the date. At the right edge, with
|
|
@@ -205,7 +208,7 @@ const landsOf = (r, et) => (et ? et.name : endStage(r));
|
|
|
205
208
|
function endLabel(at, name, d, tone, now, pxPct, hover, slip) {
|
|
206
209
|
const date = dy(d, now), est = (name.length + date.length + 1) * 6.6 + 26;
|
|
207
210
|
const flip = pxPct && (100 - at) * pxPct < est;
|
|
208
|
-
const tip = name + ' · ' +
|
|
211
|
+
const tip = name + ' · ' + mdn(d) + (d.getFullYear() !== now.getFullYear() ? ', ' + d.getFullYear() : '') + (hover && hover.length ? '. ' + hover.join('. ') : '');
|
|
209
212
|
return `<span class="tg-endl${flip ? ' l' : ''}" style="left:${P(at)}" title="${esc(tip)}"><span class="nm">${esc(name)}</span> <b${tone ? ` class="${tone}"` : ''}>${esc(date)}</b>${slip || ''}</span>`;
|
|
210
213
|
}
|
|
211
214
|
/** A slip's words for the hover (PvaAC4): "Planned May 20 → Jun 10 (+3 wk)". */
|
|
@@ -278,7 +281,7 @@ function draftBar(r, X, phone, fmt, now, pxPct) {
|
|
|
278
281
|
|
|
279
282
|
const MIN_PX = 8; // a hold or a gap narrower than this closes up (XtiA4Long, XtiAGrill 12)
|
|
280
283
|
/** A hold's dates: "Oct 12 to Mar 1, 2027" (the year when it ends in another one). */
|
|
281
|
-
export const holdRange = (h) => { const a = day(h.from), b = day(h.to || h.back); return md(a) + (b ? ' to ' +
|
|
284
|
+
export const holdRange = (h) => { const a = day(h.from), b = day(h.to || h.back); return md(a) + (b ? ' to ' + mdn(b) + (b.getFullYear() !== a.getFullYear() ? ', ' + b.getFullYear() : '') : ''); };
|
|
282
285
|
/** A hold's words for its ⏸ button and its menu (XtiAGrill 18): "On hold, Oct 12 to Mar 1, 2027, waits on the SMS vendor contract". */
|
|
283
286
|
export function holdWords(x, now) {
|
|
284
287
|
const h = x.h, why = h.reason ? ', ' + h.reason.replace(/\.$/, '').replace(/^./, (c) => c.toLowerCase()) : '';
|
|
@@ -443,7 +446,7 @@ function offDots(rows, now) {
|
|
|
443
446
|
function rollUp(rows, sp, X, W, o, ms, pxPct, now) {
|
|
444
447
|
if (!sp || sp.hi <= W.from || sp.lo >= W.to) return '';
|
|
445
448
|
const L = laneMilestones(rows, now, ms.reach);
|
|
446
|
-
const end = o.phone ? '' : `<span class="tg-endl" style="left:${P(X(sp.hi))}" title="${esc('Last end ' +
|
|
449
|
+
const end = o.phone ? '' : `<span class="tg-endl" style="left:${P(X(sp.hi))}" title="${esc('Last end ' + mdn(sp.hi) + ', ' + sp.hi.getFullYear())}"><b>${esc(my(sp.hi))}</b></span>`;
|
|
447
450
|
return progLine({ rows }, sp, X, now) + laneMarks(L, X, o.phone, pxPct, ms) + end;
|
|
448
451
|
}
|
|
449
452
|
/** The product's head row on the timeline: the name cell (tile, name, goal dots, count when folded, the fold) and the lane. Open or folded it
|
|
@@ -118,6 +118,7 @@ body.bd-page main.bd-main { position: relative; max-width: none; margin: 16px; p
|
|
|
118
118
|
/* the calmer top bar (batch 24 item 7, canvas BbB): one pill for what needs a look, the legend behind (i), Jira as Adler's face */
|
|
119
119
|
.bd-chip.bd-need { background: var(--cz-well); border-color: transparent; gap: 10px; padding: 0 10px 0 12px; font-weight: var(--fw-strong); color: var(--cz-ink); }
|
|
120
120
|
.bd-chip.bd-need > span { display: inline-flex; align-items: center; gap: 5px; }
|
|
121
|
+
.bd-chip.bd-need > .bd-nsep { color: var(--cz-ink3); font-weight: var(--fw-regular); margin: 0 -4px; }
|
|
121
122
|
.bd-chip.bd-need.on { background: var(--cz-bluebg); color: var(--cz-blueink); }
|
|
122
123
|
.bd-needm { min-width: 220px; }
|
|
123
124
|
.bd-needm .bd-mlbl, .bd-lgdm .bd-mlbl { font: var(--fw-strong) var(--fs-badge)/var(--lh-badge) var(--cz-sans); color: var(--cz-ink3); padding: 6px 10px 4px; letter-spacing: .5px; text-transform: uppercase; white-space: nowrap; }
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* pm/cozy-search.js — Search (initiative premium-uncovered-features, E1; canvas EvSearch, and EvPhone on the right).
|
|
2
2
|
*
|
|
3
3
|
* One palette over the page: the query, the kind chips with their counts (All, Initiatives, Documents, Slides, Reports, Artifacts, Meetings)
|
|
4
|
-
*
|
|
4
|
+
* (no worktree chip since workspace grill W4); the results grouped (Initiatives · Documents, slides and reports · Artifacts and designs ·
|
|
5
5
|
* Meetings), and last "Ask Claude about …", which starts a chat in Home. ↑ ↓ move, ↵ opens, Esc closes. On a phone it is the whole screen.
|
|
6
6
|
* openSearch({ go?, q? }) go: the page's own opener ({ ini } | { ses } | { doc }), used before a page change
|
|
7
7
|
* closeSearch()
|
|
@@ -10,18 +10,16 @@
|
|
|
10
10
|
* any other file read-only from its worktree (GET /api/find/file). Nothing here writes. */
|
|
11
11
|
import { esc, svg, ICON, wsParam, wsQ, skelRows, cantRead, failedItem } from './cozy-evui.js';
|
|
12
12
|
const ARROW = { up: 'M12 19V5M6 11l6-6 6 6', down: 'M12 5v14M6 13l6 6 6-6', enter: 'M20 5v7a3 3 0 0 1-3 3H5M9 11l-4 4 4 4' }; // the footer's keys, drawn (R28-13: no glyph as a control)
|
|
13
|
-
import { SEARCH_KINDS, PHONE_KINDS, searchGroups, resultSub, resultTag
|
|
13
|
+
import { SEARCH_KINDS, PHONE_KINDS, searchGroups, resultSub, resultTag } from './cozy-evmodel.js';
|
|
14
14
|
import { open as vwOpen, current as vwCurrent } from './cozy-viewer.js';
|
|
15
15
|
import { nameOf as sessionName } from './home-data.js';
|
|
16
16
|
import { adler } from './cozy-adler.js'; // session names A: the PM's name wins, then Claude's
|
|
17
17
|
|
|
18
18
|
const PHONE = 760;
|
|
19
19
|
const isPhone = () => innerWidth <= PHONE;
|
|
20
|
-
const S = { el: null, q: '', kind: 'all', at: 0, rows: [], res: null, busy: false, err: '', wt: null,
|
|
20
|
+
const S = { el: null, q: '', kind: 'all', at: 0, rows: [], res: null, busy: false, err: '', wt: null, back: null, go: null, seq: 0, boards: null, sessions: null, t: 0 };
|
|
21
21
|
const KIND_ICON = { initiative: ICON.flag, document: ICON.doc, slides: ICON.slides, report: ICON.report, artifact: ICON.design, file: ICON.file, meeting: ICON.meet, session: ICON.session };
|
|
22
22
|
async function getj(p) { try { const r = await fetch(p, { credentials: 'same-origin' }); const j = await r.json().catch(() => null); return j || { ok: false, error: 'the server did not answer' }; } catch { return { ok: false, error: 'the server did not answer' }; } }
|
|
23
|
-
const WT_KEY = 'pmos.search.wt';
|
|
24
|
-
const wtPick = () => { try { const v = JSON.parse(localStorage.getItem(WT_KEY) || 'null'); return Array.isArray(v) ? v : null; } catch { return null; } };
|
|
25
23
|
|
|
26
24
|
function mount() {
|
|
27
25
|
if (S.el && S.el.isConnected) return S.el;
|
|
@@ -31,7 +29,7 @@ function mount() {
|
|
|
31
29
|
+ `<div class="ev-sp-in">${svg(ICON.search, 20)}<label for="evQ" class="ev-vh">Search PMOS</label><input id="evQ" type="text" autocomplete="off" spellcheck="false" placeholder="Search initiatives, documents, slides, reports, artifacts and meetings" role="combobox" aria-expanded="true" aria-controls="evRes" aria-autocomplete="list"><span class="ev-kbd">Esc</span><button type="button" class="ev-ib x" data-ev="close" aria-label="Close search">${svg(ICON.close, 18)}</button></div>`
|
|
32
30
|
+ '<div class="ev-sp-chips" id="evChips" role="group" aria-label="Kind"></div>'
|
|
33
31
|
+ '<div class="ev-sp-list" id="evRes" role="listbox" aria-label="Results"></div>'
|
|
34
|
-
+ `<div class="ev-sp-ft"><span><span class="ev-kbd" role="img" aria-label="Arrow up">${svg(ARROW.up, 12)}</span> <span class="ev-kbd" role="img" aria-label="Arrow down">${svg(ARROW.down, 12)}</span> move</span><span><span class="ev-kbd" role="img" aria-label="Enter">${svg(ARROW.enter, 12)}</span> open</span><span class="ev-r">Searches
|
|
32
|
+
+ `<div class="ev-sp-ft"><span><span class="ev-kbd" role="img" aria-label="Arrow up">${svg(ARROW.up, 12)}</span> <span class="ev-kbd" role="img" aria-label="Arrow down">${svg(ARROW.down, 12)}</span> move</span><span><span class="ev-kbd" role="img" aria-label="Enter">${svg(ARROW.enter, 12)}</span> open</span><span class="ev-r">Searches your work. Opens in the Viewer when it can.</span></div>`
|
|
35
33
|
+ '</div>';
|
|
36
34
|
document.body.append(...d.childNodes);
|
|
37
35
|
S.el = document.getElementById('evSearch');
|
|
@@ -42,13 +40,11 @@ function mount() {
|
|
|
42
40
|
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { e.preventDefault(); const n = S.rows.length; if (n) { S.at = (S.at + (e.key === 'ArrowDown' ? 1 : n - 1)) % n; drawList(); } }
|
|
43
41
|
else if (e.key === 'Enter') { e.preventDefault(); const r = S.rows[S.at]; if (r) go(r); }
|
|
44
42
|
});
|
|
45
|
-
S.el.addEventListener('keydown', (e) => { if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation();
|
|
43
|
+
S.el.addEventListener('keydown', (e) => { if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); closeSearch(); } });
|
|
46
44
|
S.el.addEventListener('click', (e) => {
|
|
47
45
|
const b = e.target.closest('button, input[type="checkbox"]'); if (!b) return;
|
|
48
46
|
if (b.dataset.ev === 'close') { closeSearch(); return; }
|
|
49
47
|
if (b.dataset.evKind) { S.kind = b.dataset.evKind; S.at = 0; draw(); q.focus(); return; }
|
|
50
|
-
if ('evWt' in b.dataset) { S.wtOpen = !S.wtOpen; drawChips(); return; }
|
|
51
|
-
if (b.dataset.evWtId) { const all = (S.res && S.res.worktrees || []).filter((w) => !w.main).map((w) => w.id); const cur = new Set(S.wt || all); if (b.checked) cur.add(b.dataset.evWtId); else cur.delete(b.dataset.evWtId); S.wt = [...cur]; try { localStorage.setItem(WT_KEY, JSON.stringify(S.wt)); } catch { /* no storage */ } run(); return; }
|
|
52
48
|
if (b.dataset.evI != null) { const r = S.rows[+b.dataset.evI]; if (r) go(r); }
|
|
53
49
|
});
|
|
54
50
|
return S.el;
|
|
@@ -59,7 +55,7 @@ export function openSearch(o = {}) {
|
|
|
59
55
|
S.go = typeof o.go === 'function' ? o.go : null;
|
|
60
56
|
if (!el.hidden) { el.querySelector('#evQ').focus(); return el; }
|
|
61
57
|
const ae = document.activeElement; S.back = ae && ae !== document.body ? ae : null;
|
|
62
|
-
S.wt =
|
|
58
|
+
S.wt = null; S.kind = 'all'; S.at = 0; // every worktree: with no chip there is no pick to keep (workspace grill W4)
|
|
63
59
|
if (o.q != null) S.q = String(o.q);
|
|
64
60
|
el.hidden = false; document.getElementById('evSpScrim').hidden = false; el.setAttribute('data-esc-layer', '');
|
|
65
61
|
const q = el.querySelector('#evQ'); q.value = S.q; q.focus(); q.select();
|
|
@@ -104,12 +100,9 @@ const countsOf = (rows) => { const c = { all: rows.length }; for (const r of row
|
|
|
104
100
|
|
|
105
101
|
function drawChips() {
|
|
106
102
|
const box = S.el && S.el.querySelector('#evChips'); if (!box) return;
|
|
107
|
-
const rows = allRows(), c = countsOf(rows), K = isPhone() ? PHONE_KINDS : SEARCH_KINDS
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
box.innerHTML = K.map(([k, l]) => `<button type="button" class="ev-chip${S.kind === k ? ' on' : ''}" data-ev-kind="${k}" aria-pressed="${S.kind === k}">${esc(l)}${!isPhone() && S.q.trim().length >= 2 ? ' ' + (c[k] || 0) : ''}</button>`).join('')
|
|
111
|
-
+ `<button type="button" class="ev-chip gh ev-wt" data-ev-wt aria-expanded="${S.wtOpen}" aria-haspopup="true" title="Which worktrees Search reads">${svg(ICON.branch, 14)}${esc(wtChip(shown))}</button>`
|
|
112
|
-
+ `<div class="ev-wtm" role="group" aria-label="Worktrees"${S.wtOpen ? '' : ' hidden'}>${shown.map((w) => `<label><input type="checkbox" ${w.main ? 'checked disabled' : `data-ev-wt-id="${esc(w.id)}"${w.on ? ' checked' : ''}`}><span>${esc(w.main ? 'main' : w.name)}</span><small>${w.main ? 'always' : w.error ? esc(w.error) : esc(w.id)}</small></label>`).join('')}</div>`;
|
|
103
|
+
const rows = allRows(), c = countsOf(rows), K = isPhone() ? PHONE_KINDS : SEARCH_KINDS;
|
|
104
|
+
// no worktree chip and no "main" chip (workspace grill W4): Search reads every worktree, and the footer says "Searches your work"
|
|
105
|
+
box.innerHTML = K.map(([k, l]) => `<button type="button" class="ev-chip${S.kind === k ? ' on' : ''}" data-ev-kind="${k}" aria-pressed="${S.kind === k}">${esc(l)}${!isPhone() && S.q.trim().length >= 2 ? ' ' + (c[k] || 0) : ''}</button>`).join('');
|
|
113
106
|
}
|
|
114
107
|
function rowHtml(r, i) {
|
|
115
108
|
const tag = resultTag(r), sub = resultSub(r);
|
|
@@ -130,7 +123,7 @@ function drawList() {
|
|
|
130
123
|
if (ask) S.rows.push(ask);
|
|
131
124
|
S.at = Math.min(S.at, Math.max(0, S.rows.length - 1));
|
|
132
125
|
let h = '', i = 0;
|
|
133
|
-
if (q.length < 2) h = '<p class="ev-sp-empty">Type two letters or more. Search reads the initiatives, the documents, slides and reports, the artifacts and designs, and the meetings
|
|
126
|
+
if (q.length < 2) h = '<p class="ev-sp-empty">Type two letters or more. Search reads the initiatives, the documents, slides and reports, the artifacts and designs, and the meetings.</p>';
|
|
134
127
|
else if (S.err) h = cantRead({ what: 'Search', why: S.err, sources: false });
|
|
135
128
|
else if (S.busy && !groups.length) h = skelRows(3);
|
|
136
129
|
// nothing found: Adler with his magnifier (canvas AdU4); the words stay the same
|
|
@@ -74,7 +74,10 @@ function general() {
|
|
|
74
74
|
const theme = (() => { const t = store.get('pmos.c3.theme'); return t === 'light' || t === 'dark' ? t : 'auto'; })();
|
|
75
75
|
const vw = store.get(VIEWER_KEY) === 'popup' ? 'popup' : 'column';
|
|
76
76
|
const P = T.prods, on = P ? P.filter((x) => x.on) : null;
|
|
77
|
-
|
|
77
|
+
// the product the pages read (workspace grill W4): the page's own ?ws= first, then the pick; "No product" only when none is registered
|
|
78
|
+
const pageWs = (() => { try { return new URLSearchParams(location.search).get('ws') || ''; } catch { return ''; } })();
|
|
79
|
+
const wsRow = pageWs ? (P || []).find((x) => x.ws === pageWs) || (T.ws || []).find((x) => x.id === pageWs) : null;
|
|
80
|
+
const pname = wsRow ? wsRow.name || wsRow.label || pageWs : !P ? 'Reading' : !P.length ? 'No product' : on.length === 1 ? on[0].name : !on.length ? (P.find((x) => x.primary) || P[0]).name : on.length + ' of ' + P.length + ' products';
|
|
78
81
|
const we = T.set && Array.isArray(T.set.settings) ? T.set.settings.find((x) => x.key === 'time.workEnd') : null, end = we ? Number(we.value) : 18;
|
|
79
82
|
const hours = []; for (let h = 13; h <= 23; h++) hours.push(h);
|
|
80
83
|
const upd = ready() ? `<div class="ev-card ev-upd">${adFace('needs-you')}<span class="ev-pill y">Update</span><p><b>PMOS ${esc(latest())} is ready.</b> <span class="ev-mut">${esc(firstLine())}</span></p><button type="button" class="ev-btn sm" data-ev-go="updates">What's new</button><button type="button" class="ev-btn sm dark" data-ev-upd="update">${appBehind() ? 'Restart to update' : 'Update'}</button></div>` : '';
|
|
@@ -508,7 +511,10 @@ async function load() {
|
|
|
508
511
|
T.conn = p && p.connectors && Array.isArray(p.connectors.list) ? p.connectors.list : [];
|
|
509
512
|
T.set = s && s.ok ? s : null;
|
|
510
513
|
if (T.set) await AD.loadPrefs(async () => T.set);
|
|
511
|
-
|
|
514
|
+
// the registered products with their ticks: the pick's own read, the one Home, Board and Intelligence share (workspace grill W4: the
|
|
515
|
+
// briefing's model can hold none on a page opened for one product, which read "No product")
|
|
516
|
+
const pk = await import('./cozy-pick.js').then((PP) => PP.readPick()).catch(() => null);
|
|
517
|
+
T.prods = pk ? pk.products : b && b.ok && b.model && b.model.products ? b.model.products.available || [] : [];
|
|
512
518
|
if (!w || !Array.isArray(w.workspaces)) T.msg = 'The repositories could not be read.'; else if (!p || !p.ok) T.msg = 'The connectors could not be read.';
|
|
513
519
|
draw();
|
|
514
520
|
await Promise.all(T.ws.map(async (x) => { const r = await pmGet('/api/kit/status?ws=' + encodeURIComponent(x.id)); if (r && r.ok) T.st[x.id] = r; }));
|
|
@@ -579,7 +579,7 @@ body.cz-home .hm-hd.hv-bar > .hv-hi { flex: 1 0 auto; max-width: 100%; }
|
|
|
579
579
|
.gm-lnk { border: 0; background: none; padding: 0; min-height: 32px; font: inherit; font-size: var(--fs-label); font-weight: var(--fw-strong); color: var(--cz-blueink); cursor: pointer; text-align: left; }
|
|
580
580
|
.gm-lnk:focus-visible, .gm button:focus-visible { outline: 2px solid var(--cz-blue); outline-offset: 2px; border-radius: 6px; }
|
|
581
581
|
/* the quarter strip: the verdict, one stacked bar with the PM's line as a black tick, the legend, By product */
|
|
582
|
-
.gm-q { display: grid; grid-template-columns: minmax(220px, 1fr) minmax(260px, 1.3fr) auto; gap:
|
|
582
|
+
.gm-q { display: grid; grid-template-columns: minmax(220px, 1fr) minmax(260px, 1.3fr) auto; gap: 24px; align-items: center; background: var(--cz-well); border-radius: 18px; padding: 16px 20px; }
|
|
583
583
|
.gm-qt { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
|
|
584
584
|
.gm-qt b { font-size: var(--fs-panel); font-weight: var(--fw-strong); line-height: 1.3; color: var(--cz-ink); }
|
|
585
585
|
.gm-qt b .o { color: var(--cz-late); }
|
|
@@ -600,17 +600,32 @@ body.cz-home .hm-hd.hv-bar > .hv-hi { flex: 1 0 auto; max-width: 100%; }
|
|
|
600
600
|
.gm-leg span { display: inline-flex; align-items: center; gap: 5px; }
|
|
601
601
|
.gm-leg i { width: 10px; height: 10px; border-radius: 3px; display: inline-block; }
|
|
602
602
|
/* two columns: the reading column and the time rail */
|
|
603
|
-
.gm-x { display: grid; grid-template-columns: minmax(0, 1fr) minmax(
|
|
603
|
+
.gm-x { display: grid; grid-template-columns: minmax(0, 1fr) minmax(300px, 420px); grid-template-areas: "a rail" "b rail"; gap: 28px 48px; align-items: start; }
|
|
604
604
|
.gm-x.solo { grid-template-columns: minmax(0, 1fr); grid-template-areas: "a" "b"; }
|
|
605
|
-
.gm-a { grid-area: a; display: flex; flex-direction: column; gap:
|
|
605
|
+
.gm-a { grid-area: a; display: flex; flex-direction: column; gap: 28px; min-width: 0; }
|
|
606
606
|
.gm-b { grid-area: b; min-width: 0; }
|
|
607
|
-
.gm-rail { grid-area: rail; display: flex; flex-direction: column; gap:
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
.gm-lead .yd-open.concerned { background:
|
|
611
|
-
.gm-lead .yd-
|
|
607
|
+
.gm-rail { grid-area: rail; display: flex; flex-direction: column; gap: 16px; background: var(--cz-well); border-radius: 20px; padding: 20px 20px 16px; }
|
|
608
|
+
.gm-rh .gm-lnk { margin-left: auto; min-height: 0; }
|
|
609
|
+
/* Adler's lead (frames G1–G4 lead2): his face at 44 beside a 26 headline and a 15 line; no banner, no tinted box, in every mood */
|
|
610
|
+
.gm-lead .yd-open, .gm-lead .yd-open.concerned { background: none; padding: 0; }
|
|
611
|
+
.gm-lead .yd-hd { align-items: flex-start; gap: 16px; }
|
|
612
|
+
.gm-lead .yd-face { width: 44px; height: 44px; }
|
|
613
|
+
.gm-lead .yd-face svg { width: 44px; height: 44px; }
|
|
614
|
+
.gm-lead .yd-ht b { font-size: var(--fs-display); line-height: 1.25; letter-spacing: -.01em; }
|
|
615
|
+
.gm-lead .yd-ht em { font-size: var(--fs-body); color: var(--cz-ink2); line-height: 1.5; margin-top: 4px; }
|
|
616
|
+
.gm-lead .yd-open { position: relative; display: flex; flex-direction: column; gap: 10px; }
|
|
617
|
+
.gm-lead .yd-hd { padding-right: 56px; }
|
|
618
|
+
.gm-lead .yd-sync:not(.yd-miss) { position: absolute; top: 0; right: 0; } /* read the source again: beside the headline, not a row of its own */
|
|
619
|
+
.gm-lead .yd-miss { margin-left: 60px; background: none; padding: 0; }
|
|
620
|
+
.gm-did { margin-left: 60px; }
|
|
612
621
|
.gm-did { display: flex; align-items: flex-start; gap: 6px; font-size: var(--fs-label); color: var(--cz-ink2); line-height: 1.4; }
|
|
613
622
|
.gm-did svg { flex: none; color: var(--cz-ok); margin-top: 1px; }
|
|
623
|
+
.gm-src { align-items: center; color: var(--cz-ink3); }
|
|
624
|
+
.gm-src span { flex: 1; min-width: 0; }
|
|
625
|
+
.gm-src .yd-alt { width: 32px; height: 32px; }
|
|
626
|
+
.gm-src .yd-alt svg { color: var(--cz-ink2); margin: 0; }
|
|
627
|
+
/* say it once: "N drafted for you" and "N can wait" open their fold; until pressed the fold is not drawn a second time below */
|
|
628
|
+
.gm-wrap .czs-sec:is([data-czs-id="drafts"], [data-czs-id="wait"], [data-czs-id="more"]):not(.gm-on) { display: none; }
|
|
614
629
|
.gm-sec { display: flex; flex-direction: column; }
|
|
615
630
|
/* Your call, said once */
|
|
616
631
|
.gm-call { display: flex; align-items: center; gap: 12px; background: var(--cz-card); border-radius: 18px; padding: 14px 14px 14px 16px; border-left: 3px solid var(--cz-late); }
|
|
@@ -650,18 +665,19 @@ body.cz-home .hm-hd.hv-bar > .hv-hi { flex: 1 0 auto; max-width: 100%; }
|
|
|
650
665
|
.gm-rh span { font-size: var(--fs-label); color: var(--cz-ink3); }
|
|
651
666
|
.gm-ag { display: flex; flex-direction: column; gap: 6px; }
|
|
652
667
|
.gm-ar { display: grid; grid-template-columns: 44px minmax(0, 1fr); gap: 8px; align-items: start; }
|
|
653
|
-
.gm-tm { font-size: var(--fs-label);
|
|
654
|
-
.gm-ab { display: flex; flex-direction: column; gap:
|
|
668
|
+
.gm-tm { font-size: var(--fs-label); color: var(--cz-ink3); padding-top: 9px; text-align: right; font-variant-numeric: tabular-nums; }
|
|
669
|
+
.gm-ab { display: flex; flex-direction: column; gap: 3px; min-height: 44px; padding: 9px 12px; border: 0; border-left: 3px solid var(--cz-hair); border-radius: 12px; background: var(--cz-card); text-align: left; font: inherit; color: inherit; cursor: pointer; }
|
|
655
670
|
.gm-ab b { font-size: var(--fs-body); font-weight: var(--fw-strong); line-height: 1.3; color: var(--cz-ink); }
|
|
656
671
|
.gm-ab em { display: inline-flex; align-items: center; gap: 4px; font-style: normal; font-size: var(--fs-label); color: var(--cz-ink3); line-height: 1.35; }
|
|
657
|
-
.gm-ab.mt { border-left-color: var(--cz-
|
|
672
|
+
.gm-ab.mt { border-left-color: var(--cz-blueink); }
|
|
658
673
|
.gm-ab.key { border-left-color: var(--cz-late); }
|
|
659
|
-
.gm-ab.sp { background: var(--cz-bluebg); border
|
|
660
|
-
.gm-ab.opt { background: none; border
|
|
674
|
+
.gm-ab.sp { background: var(--cz-bluebg); border: 1.5px dashed color-mix(in srgb, var(--cz-blue) 55%, transparent); }
|
|
675
|
+
.gm-ab.opt { background: none; border: 1px solid var(--cz-hair); }
|
|
661
676
|
.gm-ab.past { opacity: .55; }
|
|
662
677
|
.gm-nl { display: flex; flex-direction: column; gap: 2px; margin-top: 4px; font-size: var(--fs-label); color: var(--cz-ink2); }
|
|
663
678
|
.gm-nl div::before { content: '· '; }
|
|
664
|
-
.gm-gap { font-size: var(--fs-label); color: var(--cz-ink3); padding:
|
|
679
|
+
.gm-gap { font-size: var(--fs-label); color: var(--cz-ink3); padding: 2px 12px; margin-left: 6px; border-left: 2px dotted var(--cz-hair); }
|
|
680
|
+
.gm-ad { padding: 8px 12px; border-radius: 12px; border: 1px solid var(--cz-hair); font-size: var(--fs-body); font-weight: var(--fw-strong); color: var(--cz-ink2); }
|
|
665
681
|
.gm-now { display: grid; grid-template-columns: 44px minmax(0, 1fr); gap: 8px; align-items: center; }
|
|
666
682
|
.gm-now span { font-size: var(--fs-label); font-weight: var(--fw-strong); color: var(--cz-late); font-variant-numeric: tabular-nums; }
|
|
667
683
|
.gm-now i { height: 2px; background: var(--cz-late); border-radius: 1px; }
|
|
@@ -96,9 +96,10 @@ export function quarterOf(Q, state = 'morning', now = Date.now()) {
|
|
|
96
96
|
const ok = okOf(c), line = Number.isFinite(Q.line) && Q.line > 0 && Q.line <= total ? Q.line : null;
|
|
97
97
|
const m = Q.morning && Q.morning.counts && totalOf(Q.morning.counts) ? okOf(Q.morning.counts) : Number.isFinite(Q.morningOk) ? Q.morningOk : null;
|
|
98
98
|
const moved = m != null && m !== ok;
|
|
99
|
-
if (state === 'soon' && !moved) return null;
|
|
99
|
+
if ((state === 'soon' || state === 'evening') && !moved) return null; // later in the day the strip says only how it moved (G2, G3)
|
|
100
100
|
const short = line ? line - ok : 0;
|
|
101
|
-
const
|
|
101
|
+
const noneRead = (c.reading || 0) === total; // nothing read yet: say that, not "0 goals on track" (W2: no signal is not a state)
|
|
102
|
+
const verdict = noneRead ? (total === 1 ? 'Your one goal has no reading yet.' : plural(total, 'goal', 'goals') + ', none read yet.') : plural(ok, 'goal', 'goals') + ' on track or better.';
|
|
102
103
|
const vline = !line ? '' : short <= 0 ? 'At or above your line of ' + line + '.' : state === 'morning' || !moved ? 'You set the line at ' + line + '.'
|
|
103
104
|
: (short === 1 ? 'One' : String(short)) + ' short of your line.';
|
|
104
105
|
// why: the morning names the product that makes most of the gap; later the day says how it moved
|
|
@@ -135,9 +136,14 @@ const tagOf = (tags, ws, ini) => { const g = tags && ini ? tags[(ws || '') + ':'
|
|
|
135
136
|
/** The compressed agenda: [{ kind: 'now'|'meet'|'pick'|'gap', time, title, sub, past, key, meeting, notes }]. Not to scale: the now line,
|
|
136
137
|
* each meeting with its prep note, ✦ picks in the free time after now, and a free stretch of 90 minutes or more as "3 h free".
|
|
137
138
|
* o: { key: the meeting id the call is decided in, picks: [{ title, sub, yd, key }] (one per free stretch, in order), notes: rows, nowLine } */
|
|
139
|
+
/** An all-day entry (leave, a holiday, an offsite): 12 hours or more. One line over the day, never a 00:00 meeting; it never counts as a meeting. */
|
|
140
|
+
export const allDay = (m) => { const s = span(m); return s.b - s.a >= 12 * 60 * MIN; };
|
|
138
141
|
export function agendaOf(t, now, o = {}) {
|
|
139
|
-
const
|
|
140
|
-
|
|
142
|
+
const all = (t || []).filter((m) => m && m.start && !Number.isNaN(Date.parse(m.start))).sort((x, y) => Date.parse(x.start) - Date.parse(y.start));
|
|
143
|
+
// an all-day event (leave, a holiday, an offsite: 12 hours or more) is one line on top, never a 00:00 meeting that fills the day
|
|
144
|
+
// (Wawan, 2026-11-04: "On Annual Leave" took the whole rail and left no free time)
|
|
145
|
+
const L = all.filter((m) => !allDay(m));
|
|
146
|
+
const out = all.filter(allDay).map((m) => ({ kind: 'allday', title: m.title || 'All day' })); const picks = (o.picks || []).slice();
|
|
141
147
|
const d1 = new Date(now); d1.setHours(18, 0, 0, 0);
|
|
142
148
|
let cur = Math.max(now, (() => { const d = new Date(now); d.setHours(9, 0, 0, 0); return d.getTime(); })());
|
|
143
149
|
let nowShown = !o.nowLine;
|
|
@@ -193,7 +199,7 @@ export function notesOf(rows, now, o = {}) {
|
|
|
193
199
|
/** The page's model. Y: dayModel's answer (Y.rank from W3); D: Home's data; o: { quarter, tags, notes, picks, today: meetings }. */
|
|
194
200
|
export function gmModel(Y, D, now = Date.now(), o = {}) {
|
|
195
201
|
D = D || {};
|
|
196
|
-
const t = (o.today || []).filter((m) => m && m.start && !Number.isNaN(Date.parse(m.start))).sort((x, y) => Date.parse(x.start) - Date.parse(y.start));
|
|
202
|
+
const t = (o.today || []).filter((m) => m && m.start && !Number.isNaN(Date.parse(m.start)) && !allDay(m)).sort((x, y) => Date.parse(x.start) - Date.parse(y.start));
|
|
197
203
|
const state = stateOf(t, now), d = new Date(now), h = d.getHours();
|
|
198
204
|
const name = D.today && D.today.me ? String(D.today.me) : '';
|
|
199
205
|
const hello = (h < 12 ? 'Good morning' : h < 17 ? 'Good afternoon' : 'Good evening') + (name ? ', ' + name : '');
|
|
@@ -213,20 +219,23 @@ export function gmModel(Y, D, now = Date.now(), o = {}) {
|
|
|
213
219
|
const move = ranked.slice(0, 3).map((r) => ({ ...r, goal: tagOf(o.tags, r.ws, r.ini), late: r.rank <= 2 && r.days != null && r.days > 0 }));
|
|
214
220
|
const more = ranked.length - move.length + (Y.rank ? Y.rank.older.length : 0);
|
|
215
221
|
|
|
216
|
-
// the lead: honest first; a call that moves a goal ties today to the quarter; else Your day's own line
|
|
217
|
-
|
|
218
|
-
|
|
222
|
+
// the lead: honest first; a call that moves a goal ties today to the quarter; else Your day's own line. A failed source is one quiet line
|
|
223
|
+
// under the lead, never the lead itself (G1, G3), and the face is never happier than the data
|
|
224
|
+
// a day read from a stale source cannot say "That was everything": it says what is on record (its sub), with a calm face
|
|
225
|
+
const B = Y.failLead && Y.calm ? (Y.calm.mood === 'delighted' ? { mood: 'calm', line: Y.calm.sub, sub: '' } : { ...Y.calm, mood: Y.calm.mood === 'glad' ? 'calm' : Y.calm.mood }) : Y;
|
|
226
|
+
let line = B.line, sub = B.sub;
|
|
227
|
+
if (state === 'soon' && B.mood !== 'concerned') {
|
|
219
228
|
const nx = t.find((m) => m.state !== 'past' && m.state !== 'now' && span(m).a > now);
|
|
220
229
|
const n = Math.max(1, Math.round((span(nx).a - now) / MIN));
|
|
221
230
|
line = (nx.title || 'Your next meeting') + ' in ' + plural(n, 'minute', 'minutes') + '.';
|
|
222
231
|
sub = Y.chips.length ? '' : Y.sub && !/meeting/.test(Y.sub) ? Y.sub : '';
|
|
223
|
-
} else if (state === 'morning' &&
|
|
232
|
+
} else if (state === 'morning' && B.mood !== 'concerned' && quarter && quarter.below && fx && better(fx.from, fx.to) && meet) {
|
|
224
233
|
line = 'Today can close part of the gap: your call is decided at ' + hm(meet.start) + '.'; // the call itself is said once, below
|
|
225
234
|
}
|
|
226
235
|
// the header already greets: the lead never says "Morning, Wawan." again (say it once); a festival's wish stays
|
|
227
236
|
const g = /^(?:Morning|Afternoon|Evening)(?:, [^.]+)?\.\s*/;
|
|
228
237
|
if (g.test(line)) { line = line.replace(g, ''); if (!line) { line = sub || ''; sub = ''; } }
|
|
229
|
-
const lead = { ...Y, line, sub, did: Y.chips.length ? 'Done for you: ' + Y.chips.join(', ') : '' };
|
|
238
|
+
const lead = { ...Y, mood: B.mood, face: B === Y ? Y.face : B.mood, failLead: false, line, sub, did: Y.chips.length ? 'Done for you: ' + Y.chips.join(', ') : '' };
|
|
230
239
|
|
|
231
240
|
// the next meeting's prep card (G2): what to agree, its bullets, Record as an icon
|
|
232
241
|
let prep = null;
|
|
@@ -249,17 +258,17 @@ export function gmModel(Y, D, now = Date.now(), o = {}) {
|
|
|
249
258
|
const freeMins = gapsOf(t, now).reduce((n, g) => n + Math.round((g.b - g.a) / MIN), 0);
|
|
250
259
|
const tm = Y.tomorrow || (state === 'evening' && D.phoneDay && D.phoneDay.tomorrow && Array.isArray(D.phoneDay.tomorrow.meetings) ? { label: D.phoneDay.tomorrow.label || 'Tomorrow', meetings: D.phoneDay.tomorrow.meetings } : null);
|
|
251
260
|
const rail = state === 'evening'
|
|
252
|
-
? { title: 'Tomorrow', sub: tm && tm.meetings.length ? (tm.label && tm.label !== 'Tomorrow' ? tm.label + ' · ' : '') + plural(tm.meetings.length, 'meeting', 'meetings') : '', rows: tm ? agendaOf(tm.meetings, dayStart(now) + DAY + 8 * 3600e3, {}) : [] }
|
|
261
|
+
? { title: 'Tomorrow', sub: tm && tm.meetings.length ? (tm.label && tm.label !== 'Tomorrow' ? tm.label + ' · ' : '') + (tm.meetings.some((m) => !allDay(m)) ? plural(tm.meetings.filter((m) => !allDay(m)).length, 'meeting', 'meetings') : 'no meetings') : '', rows: tm ? agendaOf(tm.meetings, dayStart(now) + DAY + 8 * 3600e3, {}) : [] }
|
|
253
262
|
: { title: 'Today', sub: state === 'soon' ? (() => { const nx = t.find((m) => m.state !== 'past' && span(m).a > now); return nx ? 'next at ' + hm(nx.start) : ''; })()
|
|
254
263
|
: [t.length ? plural(t.length, 'meeting', 'meetings') : '', freeMins >= 60 ? dur(freeMins) + ' free' : ''].filter(Boolean).join(' · '),
|
|
255
264
|
rows: agendaOf(t, now, { key: meet && meet.id, picks: state === 'morning' ? picks : picks.slice(1), notes: o.notes, nowLine: true }) };
|
|
256
|
-
if (!rail.rows.some((r) => r.kind === 'meet' || r.kind === 'pick')) rail.rows = [];
|
|
265
|
+
if (!rail.rows.some((r) => r.kind === 'meet' || r.kind === 'pick' || r.kind === 'allday')) rail.rows = [];
|
|
257
266
|
const notes = state === 'soon' ? [] : notesOf(o.notes, now, { today: state === 'evening', picks: o.picks });
|
|
258
267
|
|
|
259
268
|
// Since yesterday: up to 3 of New for you, neutral dots; orange only for a real rise (an item that says so)
|
|
260
269
|
const since = state === 'soon' ? [] : (Y.news || []).slice(0, 3); // the hour before a meeting shows no Since yesterday at all (G2)
|
|
261
270
|
return { state, date, hello, quarter, lead, call, move, more, made, prep, rail, notes, since, newsN: state === 'soon' ? 0 : (Y.news || []).length + (Y.digests || 0),
|
|
262
|
-
drafts: Y.drafts.length, wait: Y.wait.length, concerned:
|
|
271
|
+
drafts: Y.drafts.length, wait: Y.wait.length, concerned: B.mood === 'concerned', confirm: state === 'evening' && rc && rc.to_confirm > 0 ? rc.to_confirm : 0 };
|
|
263
272
|
}
|
|
264
273
|
|
|
265
274
|
/* ── markup ── */
|
|
@@ -286,6 +295,7 @@ function rowHtml(r) {
|
|
|
286
295
|
const doneHtml = (r) => `<div class="gm-r done"><span class="gm-tc dn" aria-hidden="true">${svg(CHECK, 13, 2.6)}</span><span class="gm-rt"><b>${esc(r.title)}</b>${r.goal ? `<span class="gm-gt"><i class="d-o"></i>${esc(r.goal)}</span>` : r.sub ? `<em>${esc(r.sub)}</em>` : ''}</span></div>`;
|
|
287
296
|
function agendaHtml(rows) {
|
|
288
297
|
return rows.map((x) => {
|
|
298
|
+
if (x.kind === 'allday') return `<div class="gm-ar"><span class="gm-tm">All day</span><div class="gm-ad">${esc(x.title)}</div></div>`;
|
|
289
299
|
if (x.kind === 'now') return `<div class="gm-now"><span>${esc(x.time)}</span><i></i></div>`;
|
|
290
300
|
if (x.kind === 'gap') return `<div class="gm-ar"><span></span><div class="gm-gap">${esc(x.title)}</div></div>`;
|
|
291
301
|
if (x.kind === 'pick') return `<div class="gm-ar"><span class="gm-tm">${esc(x.time)}</span><button type="button" class="gm-ab sp" data-yd="${esc(x.yd)}" data-key="${esc(x.key)}"${x.yd === 'gm-fold' ? ` data-fold="${esc(x.key)}"` : ''}><b>${esc(x.title)}</b><em>${esc(x.sub)}</em></button></div>`;
|
|
@@ -301,17 +311,20 @@ function notesHtml(N) {
|
|
|
301
311
|
export function gmHtml(G, openHtml, o = {}) {
|
|
302
312
|
const head = `<header class="gm-hd"><span class="gm-k">${esc(G.date)}</span><h1>${esc(G.hello)}</h1></header>`;
|
|
303
313
|
const q = G.quarter ? quarterHtml(G.quarter) : '';
|
|
304
|
-
let a = `<div class="gm-lead">${openHtml(G.lead, { ...o, gm: true })}</div>`;
|
|
314
|
+
let a = `<div class="gm-lead">${openHtml(G.lead, { ...o, gm: true, scene: !!o.scene })}</div>`;
|
|
305
315
|
if (G.prep) {
|
|
306
316
|
const P = G.prep;
|
|
307
317
|
a += `<section class="gm-prep"><span class="gm-k">${esc(P.k)}</span>${P.head ? `<h2>${esc(P.head)}</h2>` : ''}${P.bullets.length ? `<ul>${P.bullets.map((b) => `<li>${esc(b)}</li>`).join('')}</ul>` : ''}`
|
|
308
318
|
+ `<div class="gm-pr"><button type="button" class="gm-rec" data-yd="gm-rec" data-id="${esc(P.meeting.id || '')}" aria-label="Record this meeting" title="Record">${svg(MIC, 18, 2)}</button>${P.files ? `<em>${esc(P.files)}</em>` : ''}</div></section>`;
|
|
309
319
|
}
|
|
310
|
-
if (G.call
|
|
320
|
+
if (G.call) { // a call still open in the evening is still the PM's: it stays (G3 shows none only because it was decided)
|
|
311
321
|
a += `<section class="gm-sec"><h3 class="gm-sh">Your call</h3><div class="gm-call yd-call${G.call.blocked ? ' am' : ''}" data-id="${esc(G.call.id)}"><div><b>${esc(G.call.name)}</b>${G.call.why ? `<em>${esc(G.call.why)}</em>` : ''}</div>`
|
|
312
322
|
+ `<button type="button" class="yd-alt" data-yd="sel" aria-label="${esc('Open: ' + G.call.name)}" title="Open">${svg(CHEV, 18, 2)}</button></div></section>`;
|
|
313
323
|
}
|
|
314
|
-
|
|
324
|
+
// a decision caught in today's notes counts only once the PM confirms it (GET /api/phone-day recap.to_confirm): until then it is one quiet line
|
|
325
|
+
const conf = G.confirm ? `<div class="gm-more"><button type="button" data-yd="gm-notes"><b>${G.confirm}</b> ${G.confirm === 1 ? 'decision' : 'decisions'} from today’s notes to confirm${svg(CHEV, 14, 2)}</button></div>` : '';
|
|
326
|
+
if (G.made.length) a += `<section class="gm-sec"><h3 class="gm-sh">Today made possible</h3>${G.made.map(doneHtml).join('')}${conf}</section>`;
|
|
327
|
+
else a += conf;
|
|
315
328
|
const quiet = [G.more > 0 && G.state !== 'evening' ? `<button type="button" data-yd="gm-fold" data-fold="more"><b>${G.more}</b> more in Your move${svg(CHEV, 14, 2)}</button>` : '',
|
|
316
329
|
G.drafts ? `<button type="button" data-yd="gm-fold" data-fold="drafts"><b>${G.drafts}</b> drafted for you${svg(CHEV, 14, 2)}</button>` : '',
|
|
317
330
|
G.wait ? `<button type="button" data-yd="gm-fold" data-fold="wait"><b>${G.wait}</b> can wait${svg(CHEV, 14, 2)}</button>` : ''].filter(Boolean).join('');
|
|
@@ -321,7 +334,7 @@ export function gmHtml(G, openHtml, o = {}) {
|
|
|
321
334
|
+ `<div class="gm-sl">${G.since.map((it) => `<button type="button" class="gm-si${it.rise ? ' hot' : ''}" data-yd="news" data-id="${esc(it.id)}"><i aria-hidden="true"></i><span>${esc(it.kind === 'task' ? String(it.title || '').replace(/^New task: /, '') : it.title)}</span>${it.at ? `<em>${esc(at(it.at))}</em>` : ''}</button>`).join('')}</div>`
|
|
322
335
|
+ `<button type="button" class="gm-lnk" data-yd="news-all">New for you · ${G.newsN}</button></section>` : '';
|
|
323
336
|
const R = G.rail;
|
|
324
|
-
const rail = R.rows.length || G.notes.length ? `<aside class="gm-rail"><div class="gm-rh"><b>${esc(R.title)}</b>${R.sub ? `<span>${esc(R.sub)}</span>` : ''}</div>${R.rows.length ? `<div class="gm-ag">${agendaHtml(R.rows)}</div>` : ''}`
|
|
337
|
+
const rail = R.rows.length || G.notes.length ? `<aside class="gm-rail"><div class="gm-rh"><b>${esc(R.title)}</b>${R.sub ? `<span>${esc(R.sub)}</span>` : ''}<button type="button" class="gm-lnk" data-yd="gm-cal">Calendar</button></div>${R.rows.length ? `<div class="gm-ag">${agendaHtml(R.rows)}</div>` : ''}`
|
|
325
338
|
+ (G.notes.length ? `<div class="gm-notes"><h3 class="gm-sh">${G.state === 'evening' ? 'Notes today' : 'Notes since yesterday'}<button type="button" class="gm-lnk" data-yd="gm-notes">All notes</button></h3>${notesHtml(G.notes)}</div>` : '') + '</aside>' : '';
|
|
326
339
|
return `<div class="gm gm-${esc(G.state)}">${head}${q}<div class="gm-x${rail ? '' : ' solo'}"><div class="gm-a">${a}</div>${rail}${b ? `<div class="gm-b">${b}</div>` : ''}</div></div>`;
|
|
327
340
|
}
|
|
@@ -477,8 +477,10 @@ async function readNotePicks() {
|
|
|
477
477
|
async function gmPress(b) {
|
|
478
478
|
const act = b.dataset.yd, body = b.closest('.yd');
|
|
479
479
|
if (act === 'gm-intel') { location.href = insightsHref(deskWs()); return; }
|
|
480
|
+
if (act === 'gm-cal') { openCalendar(); return; }
|
|
480
481
|
if (act === 'gm-fold') {
|
|
481
482
|
const sec = body && body.querySelector('.czs-sec[data-czs-id="' + b.dataset.fold + '"]'); if (!sec) return;
|
|
483
|
+
sec.classList.add('gm-on'); // the fold the quiet line names is drawn only once it is pressed (say it once)
|
|
482
484
|
const f = sec.querySelector('[data-czs-fold]'); if (f && f.getAttribute('aria-expanded') !== 'true') f.click();
|
|
483
485
|
sec.scrollIntoView({ behavior: 'smooth', block: 'start' }); return;
|
|
484
486
|
}
|
|
@@ -1258,6 +1260,13 @@ function programTarget(id, task) {
|
|
|
1258
1260
|
empty: 'Ask about ' + pr.title + '. Claude reads the program and its initiatives in a real Claude Code session.',
|
|
1259
1261
|
context: 'Program ' + pr.title + ' (' + id + ')' + (inis.length ? ', with the initiatives ' + inis.join(', ') : '') + '.' + (task ? ' The PM opened its task "' + task.name + '"' + (task.hint ? ' (' + task.hint + ')' : '') + '.' : ''), also: null };
|
|
1260
1262
|
}
|
|
1263
|
+
/** A chat Intelligence asks for (?ask=insights|verdict): titled from the goal it names (?label), scoped to the product's goals, never "ins:insights"
|
|
1264
|
+
* and never "All work" (workspace grill W4). */
|
|
1265
|
+
function insTarget(key) {
|
|
1266
|
+
const prod = (S.D && H.productName(S.D)) || 'Product', label = (P.get('label') || '').slice(0, 80) || prod + ' goals';
|
|
1267
|
+
return { ...homeTarget(), key, title: label, sub: key === 'ins:verdict' ? 'Outcome check' : 'Goals', scope: { text: prod + ' goals', icon: I.chart },
|
|
1268
|
+
placeholder: 'Message Claude about ' + label, context: 'The PM asked from Intelligence about ' + label + ', one of the goals of ' + prod + '. Answer from that product\'s goals and readings.' };
|
|
1269
|
+
}
|
|
1261
1270
|
function homeTarget() {
|
|
1262
1271
|
return { key: 'chat:main', ws: WS || undefined, title: 'Home', scope: { text: 'All work', icon: I.home }, rich: false, placeholder: 'Message Claude', role: CLAUDE,
|
|
1263
1272
|
empty: 'Ask anything about your work. Claude reads your repositories and works in a real Claude Code session.', also: alsoOf(null),
|
|
@@ -2182,7 +2191,7 @@ drawList(); drawTop();
|
|
|
2182
2191
|
const a = P.get('about') || '', early = /^(decision|blocked|waited|src|ev|work|board|doc|ins|chat|pop):[A-Za-z0-9][A-Za-z0-9._:-]{0,100}$/.test(a) && !P.get('open') && !P.get('session') ? a : null;
|
|
2183
2192
|
const sidAt = !early && !P.get('open') && /^[A-Za-z0-9._:-]{1,120}$/.test(P.get('session') || ''); // a session link: read nothing until its session is known, so Home's own chat never shows first
|
|
2184
2193
|
if (early || sidAt) { S.phoneChat = isPhone(); drawList(); } // a phone: a conversation the address names is the page, not the list (HmPhone2)
|
|
2185
|
-
openTarget(early ? { ...homeTarget(), key: early, title: (P.get('label') || '').slice(0, 80) || early }
|
|
2194
|
+
openTarget(early ? (/^ins:(insights|verdict)$/.test(early) ? insTarget(early) : { ...homeTarget(), key: early, title: (P.get('label') || '').slice(0, 80) || early })
|
|
2186
2195
|
: sidAt ? { ...homeTarget(), key: '', title: 'Reading', empty: 'Reading the session.' }
|
|
2187
2196
|
: S.ini ? (S.ini === 'product' ? { ...homeTarget(), title: 'Reading' } : iniTarget(S.ini)) : homeTarget());
|
|
2188
2197
|
}
|
|
@@ -2218,6 +2227,7 @@ load().then(() => {
|
|
|
2218
2227
|
// the Board's program panel Discuss in chat names board:program:<id>: the program's own conversation, the one its tasks open (bug 2026-10-24)
|
|
2219
2228
|
else if (about && /^board:program:[A-Za-z0-9][A-Za-z0-9._-]{0,100}$/.test(about)) openTarget(programTarget(about.slice(14)));
|
|
2220
2229
|
else if (handed) chatAbout(handed); // a report's chat icon on another page
|
|
2230
|
+
else if (about && /^ins:(insights|verdict)$/.test(about)) openTarget(insTarget(about));
|
|
2221
2231
|
else if (about) { openTarget({ ...homeTarget(), key: about, title: (P.get('label') || '').slice(0, 80) || about }); discussed(about, P.get('vid'), P.get('label')); }
|
|
2222
2232
|
else if (P.get('view') === 'metrics') openMetrics(P.get('ws') || ''); // ?view=metrics: an old link lands on Intelligence › Delivery trends
|
|
2223
2233
|
else if (S.ini) goIni(S.ini);
|
|
@@ -36,9 +36,10 @@ import { adler } from './cozy-adler.js';
|
|
|
36
36
|
import { snapOf, turnEnded } from './chat-turn-end.js'; // a finished turn re-reads what the Viewer shows (batch 25 item 3)
|
|
37
37
|
import { nextPromptUi } from './next-prompt-ui.js'; // the suggested next prompt and the Sharper card above the box (batch 26 item 2)
|
|
38
38
|
/* The views a chat opens go in the Viewer: on a page with no dialog the old viewer's views draw in the cozy card (premium-viewer.js). */
|
|
39
|
+
let REPORT_TAB = false; // a report tab was opened from a chat on this page: only then is premium-report-tab.js read at send
|
|
39
40
|
const views = {
|
|
40
41
|
ctx: () => import('./premium-vw-ctx.js'), file: () => import('./premium-vw-file.js'), attach: () => import('./premium-attach.js'),
|
|
41
|
-
block: (b, o) => (b.kind === 'report' ? import('./premium-report-tab.js').then((M) => M.openReportTab(b, o)) : import('./premium-chart-tab.js').then((M) => M.openChartTab(b))),
|
|
42
|
+
block: (b, o) => (b.kind === 'report' ? (REPORT_TAB = true, import('./premium-report-tab.js').then((M) => M.openReportTab(b, o))) : import('./premium-chart-tab.js').then((M) => M.openChartTab(b))),
|
|
42
43
|
};
|
|
43
44
|
|
|
44
45
|
const SVG = (d, w = 16, sw = 2) => `<svg aria-hidden="true" width="${w}" height="${w}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="${sw}" stroke-linecap="round" stroke-linejoin="round"><path d="${d}"/></svg>`;
|
|
@@ -477,7 +478,10 @@ export function mountChat(el, host) {
|
|
|
477
478
|
C.pend.push(p); C.files = []; C.jump = true; draw();
|
|
478
479
|
if (t.key) {
|
|
479
480
|
const files = sentFiles.map((f) => f.saved);
|
|
480
|
-
|
|
481
|
+
// the set-up report beside this chat goes with the message, so feedback in any words reaches its draft (Wawan, 2026-11-04)
|
|
482
|
+
const report = REPORT_TAB ? await import('./premium-report-tab.js').then((M) => M.reportBeside(t.key)).catch(() => null) : null;
|
|
483
|
+
const extra = { ...(t.context ? { context: t.context } : {}), ...(report ? { report } : {}) };
|
|
484
|
+
r = await host.pmPost('/api/briefing-premium/ask' + (t.ws ? '?ws=' + encodeURIComponent(t.ws) : ''), files.length ? { key: t.key, text, files, ...extra } : { key: t.key, text, ...extra }).catch(() => null);
|
|
481
485
|
} else r = await host.pmPost('/api/session/say', { id: t.sid, text }).catch(() => null);
|
|
482
486
|
C.sending = false;
|
|
483
487
|
if (!r || r.ok !== true) {
|
|
@@ -122,6 +122,9 @@ export const boardRows = (D) => (D.board && D.board.initiatives && D.board.initi
|
|
|
122
122
|
export const live = (s) => s.shown !== 'exited' && s.state !== 'exited';
|
|
123
123
|
/** A session Home lists: a live one, or a chat that rests until its next message. One the PM put away or finished from the clean-up
|
|
124
124
|
* (batch 13 item 3: s.away) is not listed until the PM's next message in it; Search still finds it. */
|
|
125
|
+
/** The idle shell the workspace opens at startup (lib/first-shell.js) is the terminal's, not the PM's work: Home never lists it under
|
|
126
|
+
* "Not in an initiative" (workspace grill W4). A shell the PM named, or one PMOS started for a job, keeps its row. */
|
|
127
|
+
export const startShell = (s) => !!(s && s.agent === 'shell' && (s.origin === 'first-shell' || (!s.origin && String(s.name || '') === 'shell')) && s.name_by !== 'pm');
|
|
125
128
|
export const listed = (s) => (live(s) || resting(s)) && !(s && s.away);
|
|
126
129
|
/** The kids of one initiative: its needs, its live sessions and today's meetings about it, the loudest first. */
|
|
127
130
|
export function kidsOf(D, id) {
|
|
@@ -276,7 +279,7 @@ export function homeLists(D, { allNeeds = false } = {}) {
|
|
|
276
279
|
const inis = boardRows(D).filter(OPEN).map((r) => iniRow(D, r));
|
|
277
280
|
inis.sort((a, b) => (RANK[b.state] || 0) - (RANK[a.state] || 0));
|
|
278
281
|
const prod = boardRows(D).length ? productRow(D) : null;
|
|
279
|
-
const loose = (D.sessions || []).filter((s) => listed(s) && !iniOfSession(s) && s.shown !== 'needs' && s.shown !== 'blocked').map((s) => ({ ...sessionRow(s, D.desk && D.desk.roles), loose: true }))
|
|
282
|
+
const loose = (D.sessions || []).filter((s) => listed(s) && !iniOfSession(s) && !startShell(s) && s.shown !== 'needs' && s.shown !== 'blocked').map((s) => ({ ...sessionRow(s, D.desk && D.desk.roles), loose: true }))
|
|
280
283
|
.sort((a, b) => (RANK[b.state] || 0) - (RANK[a.state] || 0));
|
|
281
284
|
return { needs: { rows: allNeeds ? needs : needs.slice(0, 3), more: Math.max(0, needs.length - 3), all: needs.length }, initiatives: prod ? [prod, ...inis] : inis, loose, needN: needs.length };
|
|
282
285
|
}
|
|
@@ -160,11 +160,12 @@ export function dayModel(M, D, R, now = Date.now(), o = {}) {
|
|
|
160
160
|
// a source that could not be read (AdP5): said plainly; it leads the card unless Claude's own miss or a stuck call already does
|
|
161
161
|
const fail = syncFailOf(o.sources, now);
|
|
162
162
|
let failLead = false;
|
|
163
|
+
const calm = { mood, line, sub }; // Good morning keeps the day's own line and says a failed source in one quiet line (G1, G3)
|
|
163
164
|
if (fail && mood !== 'concerned') { const w = failWords(fail); mood = 'concerned'; line = w.line; sub = w.sub; failLead = true; }
|
|
164
165
|
// no prep run today: say so, and offer it (Prepare my day); never claim work that did not happen. No meeting, no prep line (W3)
|
|
165
166
|
if (!run && h < 17 && t.length) offer = { kind: 'prep', text: 'I haven’t prepared today yet.', sub: 'Shall I prep your meetings and draft what I can?' };
|
|
166
167
|
return {
|
|
167
|
-
mood, face: run && run.working && mood === 'calm' ? 'working' : mood, line, sub, chips, misses, offer, calls, drafts, wait, done, thanks, fail, failLead, ask: o.ask || null,
|
|
168
|
+
mood, face: run && run.working && mood === 'calm' ? 'working' : mood, line, sub, chips, misses, offer, calls, drafts, wait, done, thanks, fail, failLead, calm, ask: o.ask || null,
|
|
168
169
|
rank: o.rank ? rankMove({ ...o.rank, calls, now }) : null,
|
|
169
170
|
news: newsOf(o.news), digests: digestsOf(D), tomorrow: tomorrowOf(D, now),
|
|
170
171
|
next: next ? { meeting: next, now: next.state === 'now', time: hm(next.start), title: next.title || 'A meeting' } : null,
|
|
@@ -181,7 +182,8 @@ const alt = (act, label, id, icon) => `<button type="button" class="yd-alt" data
|
|
|
181
182
|
/** The opening card: face, line, sub, the chips of real work, and Claude's own misses with a ✓ to run them again. */
|
|
182
183
|
export function openHtml(Y, o = {}) {
|
|
183
184
|
// AdP5: a failed source that leads the card draws the small rain cloud in place of the morning scene (Face only: the honest face)
|
|
184
|
-
|
|
185
|
+
// Good morning (o.gm, frames G1–G4): the lead is Adler's face beside the line; the art banner and the rain cloud are not drawn there
|
|
186
|
+
const rain = Y.fail && Y.failLead && !o.gm ? adlerScene('rain', 'honest', { label: 'Adler, honest, under a small rain cloud', h: 170 }) : '';
|
|
185
187
|
if (rain) o = { ...o, scene: true };
|
|
186
188
|
// a dressed day's scene is wider (460 x 170), so the card's 150 px shows its garland or lights, not only the ground (AsPlaces)
|
|
187
189
|
const head = rain ? `<div class="yd-scene">${rain}</div>` : o.scene ? `<div class="yd-scene"${dress() ? ' data-ad-dress role="button" tabindex="0" aria-label="Why Adler is dressed"' : ''}>${adlerScene('morning', 'handled', { label: false, dress: true, ...(dress() ? { w: 460, h: 170 } : {}) })}</div>` : o.big ? '' : faceHtml(Y.face || Y.mood);
|
|
@@ -189,7 +191,8 @@ export function openHtml(Y, o = {}) {
|
|
|
189
191
|
const chips = o.gm && Y.did ? `<div class="gm-did">${svg(CHECK, 14, 2.6)}${esc(Y.did)}</div>` : Y.chips.length ? `<div class="yd-chips">${Y.chips.map((c) => `<span class="yd-chip">${svg(CHECK, 14, 2.6)}${esc(c)}</span>`).join('')}</div>` : '';
|
|
190
192
|
const miss = Y.misses.map((m) => `<div class="yd-miss" data-id="${esc(m.id)}"><div class="yd-mt"><b>My miss: ${esc(m.title)}</b>${m.why ? `<em>${esc(m.why)}</em>` : ''}</div>${ok('retry', 'Run it again', m.id)}</div>`).join('');
|
|
191
193
|
// the failed source under Claude's own miss: one card, no second Adler; its icon reads the source again (Settings › Connectors' ↻)
|
|
192
|
-
const sync = Y.fail ? (Y.
|
|
194
|
+
const sync = Y.fail && o.gm ? (() => { const w = failWords(Y.fail); return `<div class="gm-did gm-src" data-id="${esc(Y.fail.id)}"><span>${esc(w.line)}${Y.fail.from ? ' What I show is ' + esc(Y.fail.from) + '.' : ''}</span>${alt('sync', 'Read ' + Y.fail.name + ' again', Y.fail.id, AGAIN)}</div>`; })()
|
|
195
|
+
: Y.fail ? (Y.failLead ? `<div class="yd-sync">${alt('sync', 'Read ' + Y.fail.name + ' again', Y.fail.id, AGAIN)}</div>`
|
|
193
196
|
: (() => { const w = failWords(Y.fail); return `<div class="yd-miss yd-sync" data-id="${esc(Y.fail.id)}"><div class="yd-mt"><b>${esc(w.line)}</b><em>${esc(w.sub)}</em></div>${alt('sync', 'Read ' + Y.fail.name + ' again', Y.fail.id, AGAIN)}</div>`; })()) : '';
|
|
194
197
|
// one ask that waits on the PM in Mentions (batch 17 item 6): who, where, Claude's gist; its icon opens that conversation in Mentions
|
|
195
198
|
const A = Y.ask, ask = A ? `<div class="yd-miss yd-ask" data-id="${esc(A.id)}"><div class="yd-mt"><b>${esc(A.who)} asked you in ${esc(A.where)}${A.more ? ` · ${A.more} more` : ''}</b><em>${esc(A.gist)}</em></div>${alt('ask', 'Open in Mentions', A.id, CHEV)}</div>` : '';
|
|
@@ -248,6 +251,9 @@ const MARK2 = 'M2 12.5l4.5 4.5L15 8M9 15l2 2L22 7';
|
|
|
248
251
|
const listOf = (a) => (a.length < 2 ? a.join('') : a.slice(0, -1).join(', ') + ' and ' + a[a.length - 1]);
|
|
249
252
|
const SPARK = 'M12 3l1.8 4.7L18.5 9.5l-4.7 1.8L12 16l-1.8-4.7L5.5 9.5l4.7-1.8z';
|
|
250
253
|
export const NEWS_SHOWN = 3;
|
|
254
|
+
const MO = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
255
|
+
/** A due date in New for you: "25 Nov", with the short year when it is not this year ("9 Jun ’27"), never the ISO date (workspace grill W4). */
|
|
256
|
+
export const dueWords = (iso, now = Date.now()) => { const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(iso || '')); if (!m) return String(iso || ''); return +m[3] + ' ' + MO[+m[2] - 1] + (+m[1] !== new Date(now).getFullYear() ? ' ’' + m[1].slice(2) : ''); };
|
|
251
257
|
function newsRow(it, now) {
|
|
252
258
|
const who = it.kind === 'task' ? (it.from || 'Someone') : (it.product || 'Claude');
|
|
253
259
|
const when = it.at ? ago(new Date(it.at).toISOString(), now) : '';
|
|
@@ -256,7 +262,7 @@ function newsRow(it, now) {
|
|
|
256
262
|
// workflow's initiatives (batch 20 item 3, WmB: "Shown under your day, in ✦")
|
|
257
263
|
const sub = it.kind === 'wf-bridge' ? ['From ' + ((it.from && it.from.name) || 'the old workflow'), (it.groups || []).length ? 'into ' + it.groups.map((g) => g.name).join(', ') : ''].filter(Boolean).join(' · ')
|
|
258
264
|
: it.kind === 'overlap' ? [it.a && it.a.title, it.b && it.b.title].filter(Boolean).join(' and ') || 'Two open initiatives'
|
|
259
|
-
: it.kind === 'task' ? ['Given to you' + (it.from ? ' from ' + it.from : ''), it.due ? 'due ' + it.due : '', it.iniTitle ? 'filed under ' + it.iniTitle : ''].filter(Boolean).join(' · ')
|
|
265
|
+
: it.kind === 'task' ? ['Given to you' + (it.from ? ' from ' + it.from : ''), it.due ? 'due ' + dueWords(it.due, now) : '', it.iniTitle ? 'filed under ' + it.iniTitle : ''].filter(Boolean).join(' · ')
|
|
260
266
|
: ['Finished while you were away', it.target, when].filter(Boolean).join(' · ');
|
|
261
267
|
const av = it.kind === 'overlap' || it.kind === 'wf-bridge' ? `<span class="yd-av sp" aria-hidden="true">${svg(SPARK, 13, 2)}</span>` : `<span class="yd-av${it.kind === 'finished' ? ' b' : ''}" aria-hidden="true">${esc(initials(who))}</span>`;
|
|
262
268
|
return `<button type="button" class="yd-nw" data-yd="news" data-id="${esc(it.id)}"><i class="yd-nd" aria-hidden="true"></i>${av}<span class="yd-mt"><b>${esc(head)}</b><em>${esc(sub)}</em></span></button>`;
|
|
@@ -325,7 +331,8 @@ export function gmParts(Y, G, o = {}) {
|
|
|
325
331
|
if (o.moment) sections.push({ id: 'now', title: 'Now', open: true, html: '<div class="hd-now"></div>' });
|
|
326
332
|
sections.push({ id: 'week', title: 'This week', count: Y.week.sub, html: `<div class="hv-card"><div class="hm-r hv-r two" data-id="review"><button type="button" class="hm-rm" data-hm-review aria-label="Weekly review: ${esc(Y.week.sub)}. Open"><span class="hv-mk">${svg(CAL, 16)}</span><span class="hm-rt"><b>Weekly review</b><em>${esc(Y.week.sub)}</em></span><span class="hv-cv">${svg(CHEV, 16)}</span></button></div></div>` });
|
|
327
333
|
// one Adler per screen: the morning scene on the day's first open before noon, else his face on the lead
|
|
328
|
-
|
|
334
|
+
// the daily morning banner is gone (frames G1-G4: a face beside the line); a festival's own day keeps its scene once (AsPlaces, Deepavali)
|
|
335
|
+
const D = dress(), scene = !!o.first && !!D && !!dressDay(D, now) && Y.mood !== 'concerned' && G.state === 'morning' && new Date(now).getHours() < 12;
|
|
329
336
|
const night = Y.mood !== 'concerned' && asleep(now) ? nightHtml(Y) : '';
|
|
330
337
|
return { gm: o.html(G, openHtml, { scene }), sections, night };
|
|
331
338
|
}
|