create-pmos 0.527.0 → 0.529.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 +57 -29
- 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/briefing-premium-calendar-3-test.js +11 -9
- package/workspace/briefing-premium-calendar-test.js +1 -1
- package/workspace/changelog.json +56 -0
- package/workspace/ev-everywhere-test.js +2 -6
- package/workspace/fixtures/premium-viewer-fixtures.js +4 -2
- package/workspace/guide/home.md +7 -6
- package/workspace/guide/idle-lab.md +1 -1
- package/workspace/guide/notes.md +19 -7
- package/workspace/guide/reports-set-up.md +4 -1
- package/workspace/lib/bg-queue.js +10 -3
- package/workspace/lib/meeting-replay.js +11 -2
- package/workspace/lib/meeting-suggest.js +105 -0
- package/workspace/lib/meeting-terms.js +57 -17
- package/workspace/lib/okr-goals.js +23 -3
- package/workspace/lib/report-chat.js +3 -3
- package/workspace/lib/routes-lab.js +44 -2
- package/workspace/lib/routes-meeting-points.js +27 -3
- package/workspace/lib/routes-morning-goals.js +5 -1
- package/workspace/lib/routes-report-flow.js +35 -5
- package/workspace/news-viewers-test.js +2 -1
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/premium-vw-meet-test.js +17 -11
- 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-lab-ins.js +13 -4
- 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/cozy-vw-mtg.css +122 -24
- package/workspace/public/pm/good-morning.js +30 -17
- package/workspace/public/pm/home-app.js +11 -1
- 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-meeting.js +2 -1
- package/workspace/public/pm/premium-mtg-replay.js +149 -73
- package/workspace/public/pm/premium-report-read.js +11 -4
- package/workspace/public/pm/premium-report-tab.js +33 -4
- package/workspace/public/pm/premium-vw-mtg.js +176 -53
- package/workspace/public/pm/program-model.js +2 -1
- package/workspace/public/pm/report-chat.js +1 -0
- package/workspace/public/pm/report-flow.js +7 -3
- 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
|
@@ -60,7 +60,7 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
|
|
|
60
60
|
const s1 = await sum(p);
|
|
61
61
|
/* batch 25 item 11 (MvC): the summary first in the left column, the kept points with Confirm N in their header, the evidence, one strip, the docked timeline */
|
|
62
62
|
ok('S1 the summary: the overview and the meta line, no primary (Confirm sits in the kept header)', /opens next to the chat/.test(s1.text) && !s1.pri, JSON.stringify(s1));
|
|
63
|
-
ok('S1 the left column starts with the summary, then the kept points: "N kept" and Confirm N;
|
|
63
|
+
ok('S1 the left column starts with the summary, then the kept points: "N kept" and Confirm N; one player (MpD1), no dock', await p.evaluate(() => { const l = document.querySelector('#czViewer .vzr-l'); return !!l && l.firstElementChild.classList.contains('czs-sum') && /^\d+ kept$/.test(l.querySelector('.vzr-ph h3').textContent) && /^Confirm \d+$/.test(l.querySelector('[data-a="confirmall"]').textContent) && !!document.querySelector('#czViewer .vzp-card .vzr-np') && !document.querySelector('#czViewer .vzr-dock'); }));
|
|
64
64
|
const chips = await p.evaluate(() => [...document.querySelectorAll('#czViewer .vzr-fold [data-a="strip"]')].map((x) => x.dataset.k).join());
|
|
65
65
|
ok('S1 one strip: Left out, Next steps, Since the last one, Facts, Names and terms, Prep and material, Transcript; no tabs, no checkbox, no filter', chips === 'left,steps,since,facts,names,prep,tr' && !(await p.$('#czViewer [role="tablist"]')) && !(await p.$('#czViewer .vzr [role="checkbox"]')) && !(await p.$('#czViewer [data-a="pf"]')), chips);
|
|
66
66
|
ok('S1 the Viewer opens wide for the two columns', await p.evaluate(() => document.getElementById('czViewer').offsetWidth >= 900 && getComputedStyle(document.querySelector('#czViewer .vzr-cols')).gridTemplateColumns.split(' ').length === 2));
|
|
@@ -80,7 +80,9 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
|
|
|
80
80
|
const strip = (k) => press(p, `.vzr-fold [data-a="strip"][data-k="${k}"]`);
|
|
81
81
|
await strip('since'); const since = await body(p); await strip('facts'); const facts = await body(p);
|
|
82
82
|
ok('S3 Since the last one and Facts mentioned, Matches and Not checked', /Raised again/.test(since) && /Matches/.test(facts) && /Not checked/.test(facts), facts.slice(0, 600));
|
|
83
|
-
ok('S3 the
|
|
83
|
+
ok('S3 MpD4: the captures row is hidden until its icon is pressed', !(await p.$('#czViewer .vzp-card .vzp-th')));
|
|
84
|
+
await press(p, '.vzr-np:not(.vzr-ph1) [data-a="caps"]');
|
|
85
|
+
ok('S3 the captures icon shows the screen captures, with no Waiting for design marker', await until(() => p.evaluate(() => document.querySelectorAll('#czViewer .vzp-card .vzp-th').length > 0 && !document.querySelector('#czViewer [data-cz-placeholder]'))));
|
|
84
86
|
await press(p, '.vzp-card .vzp-th[data-a="seekto"]');
|
|
85
87
|
ok('S3 a thumbnail moves the replay to its time', await until(() => p.evaluate(() => /^03:10/.test((document.querySelector('#czViewer [data-clk="pos"]') || {}).textContent || ''))), await p.evaluate(() => (document.querySelector('#czViewer [data-clk="pos"]') || {}).textContent));
|
|
86
88
|
await strip('tr');
|
|
@@ -98,9 +100,9 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
|
|
|
98
100
|
|
|
99
101
|
/* S5 a correction, and what PMOS learns (XvAReplayEdit) */
|
|
100
102
|
const pid = await p.evaluate(() => { const r = document.querySelectorAll('#czViewer .vzr-row [data-a="psel"]')[1]; r.click(); return r.dataset.id; });
|
|
101
|
-
ok('S5 pressing a row selects it
|
|
102
|
-
await press(p, '.vzr-
|
|
103
|
-
ok('S5 Fix
|
|
103
|
+
ok('S5 pressing a row selects it; the selected row has Fix and Not right', await until(() => p.evaluate((id) => !!document.querySelector(`#czViewer .vzr-row.sel [data-a="psel"][data-id="${id}"]`) && !!document.querySelector(`#czViewer .vzr-row.sel [data-a="pedit"][data-id="${id}"]`) && !!document.querySelector(`#czViewer .vzr-row.sel [data-a="pwrong"][data-id="${id}"]`), pid)), pid);
|
|
104
|
+
await press(p, '.vzr-row.sel [data-a="pedit"]');
|
|
105
|
+
ok('S5 Fix on the row opens the editor in place: the kinds, the words, what PMOS will remember', !!pid && await until(() => p.evaluate(() => !!document.querySelector('#czViewer .vzp-ed [data-a="etext"]') && document.querySelectorAll('#czViewer .vzp-dt .vzp-ed [data-a="ekind"]').length === 3 && document.querySelector('#czViewer .vzp-ed [data-a="esave"]').disabled)), pid);
|
|
104
106
|
const kNow = await p.evaluate(() => document.querySelector('#czViewer .vzp-ed [data-a="ekind"][aria-checked="true"]').dataset.k);
|
|
105
107
|
const kNew = kNow === 'question' ? 'action' : 'question';
|
|
106
108
|
await press(p, `.vzp-ed [data-a="ekind"][data-k="${kNew}"]`);
|
|
@@ -111,12 +113,16 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
|
|
|
111
113
|
ok('S5 the page says Fixed and what it will remember, with Undo', await until(async () => /Fixed\./.test(await body(p)) && !!(await p.$('#czViewer .vz-msg [data-a="msgundo"]'))), (await body(p)).slice(0, 300));
|
|
112
114
|
await press(p, '.vz-msg [data-a="msgundo"]');
|
|
113
115
|
ok('S5 Undo forgets the lesson (POST /api/meeting-points/forget)', await until(() => A.S.posts.some((x) => x.path === '/api/meeting-points/forget' && x.body.lesson === 'l0123456789')), JSON.stringify(A.S.posts.map((x) => x.path)));
|
|
114
|
-
/*
|
|
115
|
-
await
|
|
116
|
-
|
|
117
|
-
await p.
|
|
118
|
-
ok('S5
|
|
119
|
-
await
|
|
116
|
+
/* MpW1, MpW2 (batch 27 item 12): words heard wrong, selected in Next steps, fixed in every place; the ✦ ask waits; Undo forgets it */
|
|
117
|
+
await strip('steps');
|
|
118
|
+
await until(() => p.evaluate(() => [...document.querySelectorAll('#czViewer .vzm-step .vzm-grow')].some((x) => /print/.test(x.textContent))));
|
|
119
|
+
await p.evaluate(() => { const g = [...document.querySelectorAll('#czViewer .vzm-step .vzm-grow')].find((x) => /print/.test(x.textContent)); const w = document.createTreeWalker(g, NodeFilter.SHOW_TEXT); let tn; while ((tn = w.nextNode()) && !/print/.test(tn.textContent)); const i = tn.textContent.indexOf('print'); const r = document.createRange(); r.setStart(tn, i); r.setEnd(tn, i + 5); const s = getSelection(); s.removeAllRanges(); s.addRange(r); g.dispatchEvent(new MouseEvent('mouseup', { bubbles: true })); });
|
|
120
|
+
ok('S5 selected words open the popup: Heard as, the fix field with ✓, the places; no ✦ line without a real match', await until(() => p.evaluate(() => { const f = document.querySelector('.vzr-wpop'); return !!f && f.dataset.w === 'print' && /Heard as “print”/.test(f.textContent) && !!f.querySelector('[data-a="wfok"]') && /in this meeting/.test(f.textContent) && !f.querySelector('.vzr-sug'); })));
|
|
121
|
+
await p.fill('.vzr-wpop [data-a="wfin"]', 'Print'); await p.press('.vzr-wpop [data-a="wfin"]', 'Enter');
|
|
122
|
+
ok('S5 Enter posts the word fix: it shows fixed, a toast with Undo, and Names and terms asks to remember it', await until(() => A.S.posts.some((x) => x.path === '/api/meeting-points/term' && x.body.heard === 'print' && x.body.means === 'Print'))
|
|
123
|
+
&& await until(async () => !!(await p.$('#czViewer .vzr-fx')) && !!(await p.$('#czViewer .vzr-toast [data-a="msgundo"]')) && !!(await p.$('#czViewer [data-a="wkeep"][data-v="1"]'))));
|
|
124
|
+
ok('S5 remembering waits for the ✓: nothing was posted to keep yet', !A.S.posts.some((x) => x.path === '/api/meeting-points/keep'));
|
|
125
|
+
await press(p, '.vzr-toast [data-a="msgundo"]');
|
|
120
126
|
ok('S5 Undo forgets the word fix', await until(() => A.S.posts.some((x) => x.path === '/api/meeting-points/forget' && x.body.lesson === 't0123456789')) && await until(async () => !(await p.$('#czViewer .vzr-fx'))));
|
|
121
127
|
await press(p, '[data-a="confirmall"]');
|
|
122
128
|
ok('S5 Confirm N in the kept header confirms what is left to confirm', await until(() => A.S.posts.some((x) => x.path === '/api/meeting-points' && x.body.verdict === 'confirmed' && x.body.ids.length > 0)), JSON.stringify(A.S.posts.filter((x) => x.path === '/api/meeting-points')));
|
|
@@ -122,7 +122,7 @@ function model() {
|
|
|
122
122
|
a && a.tm.length === 3 && a.tmc.length === 1 && /^tg-tmc dn|^tg-tmc/.test(a.tmc[0]) && a.tmc[0].endsWith('|2') && !a.tml.length, JSON.stringify(a));
|
|
123
123
|
ok('L the same ◇: late is orange, the Evaluate task IS the end ◇ (said once), a row\'s only milestone IS its end (no Deliver, hotfix 2)',
|
|
124
124
|
a && /^tg-tm late\|Design review · Passkey sign-in · Oct 16 · late$/.test(a.tm[0]) && a.tm.some((x) => /KPI evaluated · Scarlet/.test(x)) && !a.tm.some((x) => /Deliver · Passkey/.test(x)) && !a.tm.some((x) => /Committed/.test(x)), JSON.stringify(a && a.tm));
|
|
125
|
-
ok('H a single ◇\'s hover names the milestone and its initiative', a && a.tm.some((x) =>
|
|
125
|
+
ok('H a single ◇\'s hover names the milestone and its initiative', a && a.tm.some((x) => /\|Feature delivered · Scarlet — ST What’s The News · Jan 29(?: ’\d\d)?$/.test(x)), JSON.stringify(a && a.tm));
|
|
126
126
|
ok('L the other product\'s lane: Mar 2 and Mar 6 (Recovery\'s end) merge; Email OTP has no milestone, so its end is Deliver Jun 30', b && b.tmc.length === 1 && b.tm.length === 1 && /Deliver · Email OTP · Jun 30/.test(b.tm[0]), JSON.stringify(b));
|
|
127
127
|
const lg = await p.evaluate(() => (document.querySelector('.tg-lgd') || {}).textContent || '');
|
|
128
128
|
ok('L the legend gets no new entry', !/lane|product/i.test(lg), lg);
|
|
@@ -134,10 +134,10 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
|
|
|
134
134
|
await p.click('#czViewer [data-pc="pick"][data-m="pilot-live"]');
|
|
135
135
|
const mn = await until(() => p.evaluate(() => { const m = document.querySelector('#czViewer .pc-menu'); return m ? { hd: [...m.querySelectorAll('.pc-mhd')].map((x) => x.textContent), items: [...m.querySelectorAll('.pc-mi')].map((x) => x.querySelector('.t').textContent + (x.querySelector('.r') ? ' ' + x.querySelector('.r').textContent : '') + (x.getAttribute('aria-checked') === 'true' ? ' ✓' : '')) } : null; }));
|
|
136
136
|
ok('P the picker lists its initiatives per product with their ◆ Deliver days, the chosen ones ticked, then "None: I tick it myself"', !!mn && mn.hd.join('|') === 'Done when these pass ◆ Deliver|IDPF|Microsites'
|
|
137
|
-
&& mn.items.includes('Descope feature parity Nov 12 ✓') && mn.items.includes('Microsite sign-in on CIAM Nov 14 ✓') && mn.items.
|
|
137
|
+
&& mn.items.includes('Descope feature parity Nov 12 ✓') && mn.items.includes('Microsite sign-in on CIAM Nov 14 ✓') && mn.items.some((x) => /^Legacy IdP sunset Feb 20(?: ’\d\d)?$/.test(x)) && mn.items[mn.items.length - 1] === 'None: I tick it myself', JSON.stringify(mn));
|
|
138
138
|
await p.click('#czViewer .pc-menu [data-ini="legacy-idp-sunset"]');
|
|
139
139
|
const wv = await until(() => p.evaluate(() => { const w = document.querySelector('#czViewer .pc-lnw:has([data-m="pilot-live"]) .pc-warn'); return w ? w.textContent : null; }));
|
|
140
|
-
ok('P picking an initiative whose ◆ Deliver is later than the milestone warns in amber, and Save stays',
|
|
140
|
+
ok('P picking an initiative whose ◆ Deliver is later than the milestone warns in amber, and Save stays', /^Legacy IdP sunset is set to deliver on Feb 20(?: ’\d\d)?, after this date\.$/.test(String(wv)) && (await head(p)).save, String(wv));
|
|
141
141
|
await shot(p, 'MsB3Pick-built');
|
|
142
142
|
await p.click('#czViewer .pc-menu [data-ini="legacy-idp-sunset"]');
|
|
143
143
|
await p.keyboard.press('Escape');
|
|
@@ -175,7 +175,7 @@ function dashFor(r) {
|
|
|
175
175
|
function iniBody(r) {
|
|
176
176
|
const h = dashFor(r);
|
|
177
177
|
if (h && h.shows()) return h.html(() => '');
|
|
178
|
-
const md = (v) => ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][+v.slice(5, 7) - 1] + ' ' + +v.slice(8, 10);
|
|
178
|
+
const md = (v) => ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][+v.slice(5, 7) - 1] + ' ' + +v.slice(8, 10) + (+v.slice(0, 4) !== new Date().getFullYear() ? ' ’' + v.slice(2, 4) : '');
|
|
179
179
|
const self = !!((O.M() || {}).board || {}).self, M = O.M() || {};
|
|
180
180
|
// what left the ⋯ for the thing it changes (batch 16 item 5c): the workflow chip, the dashed +, the stage chip, the dates chip, Jira's ↻
|
|
181
181
|
return viewHtml(viewOf(r), { add: !!(r.id && O.addTask), adding: adding === r.key, addHint: adding === r.key && addDates ? md(addDates.start) + ' to ' + md(addDates.due) : '', openTask: !!O.openTask, quick: !!(O.setTask && O.setDates), hold: !!O.hold, tail: VW.czSections(boardSections(r), { key: 'board' }),
|
|
@@ -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; }
|
|
@@ -33,16 +33,25 @@ function pillOf(r) {
|
|
|
33
33
|
if (r.ini) return `<a class="lab-pill y" href="${esc(S.H.iniHref(r.ini))}">✦ on initiative</a>`;
|
|
34
34
|
return '';
|
|
35
35
|
}
|
|
36
|
-
|
|
36
|
+
/** When a job ran, short: "19:37" today, "Tue 14:10" this week (Wawan, 2026-11-04: the rows said what ran but not when). */
|
|
37
|
+
export function whenOf(at, now = Date.now()) {
|
|
38
|
+
const d = new Date(at); if (Number.isNaN(d.getTime())) return '';
|
|
39
|
+
const hm = String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0');
|
|
40
|
+
return new Date(now).toDateString() === d.toDateString() ? hm : ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][d.getDay()] + ' ' + hm;
|
|
41
|
+
}
|
|
42
|
+
/** One row of the record. A background job says what Claude did and what came out (routes-lab.js others); its pill shows only when a result
|
|
43
|
+
* waits on the initiative, and says so. */
|
|
44
|
+
export function recRow(r, now = Date.now()) {
|
|
37
45
|
const open = r.compare ? ib(`data-lab-cmp="${esc(r.id)}" data-lab-board="${esc(r.board || '')}"`, 'Open in the compare view', LC.I.open) : '';
|
|
38
|
-
const pill = r.kind === 'job' ? (r.ini ? `<a class="lab-pill" href="${esc(S.H.iniHref(r.ini))}">
|
|
39
|
-
|
|
46
|
+
const pill = r.kind === 'job' ? (r.ini && S.H ? `<a class="lab-pill" href="${esc(S.H.iniHref(r.ini))}">See what it found</a>` : '') : pillOf(r);
|
|
47
|
+
const meta = r.kind === 'job' ? [r.meta, whenOf(r.at, now)].filter(Boolean).join(' · ') : r.meta || '';
|
|
48
|
+
return `<div class="lab-row"><span class="lab-ico">${SVG(ICON[r.kind] || ICON.job, 18)}</span><span class="w"><b>${esc(r.title)}</b><small>${esc(meta)}</small></span>${pill}${open}</div>`;
|
|
40
49
|
}
|
|
41
50
|
function html(L) {
|
|
42
51
|
const k = L.kept;
|
|
43
52
|
const keptCard = k ? `<div class="lab-card"><small>You kept</small><b class="lab-big">${k.kept} of ${k.filed}</b><small>${[k.proto[1] ? 'prototypes ' + k.proto[0] + ' of ' + k.proto[1] : '', k.scan[1] ? 'scans ' + k.scan[0] + ' of ' + k.scan[1] : ''].filter(Boolean).join(' · ')}. Claude learns from each reason you give.</small></div>` : '';
|
|
44
53
|
const next = L.next.length ? `<div class="lab-card"><b>Next time you step away</b>${L.next.map((x) => `<div class="lab-row"><span class="lab-dot open"></span><span class="w">${esc(x.title)}${x.why ? `<small>${esc(x.why)}</small>` : ''}</span></div>`).join('')}<small>Claude picks these by itself. To skip one, say so in chat.</small></div>` : '';
|
|
45
|
-
const rec = L.record.length ? `<div class="lab-card lab-rec"><div class="lab-bar"><b>What Claude did in the background</b><small>this week, every job</small></div>${L.record.map(recRow).join('')}${L.more ? `<small>and ${L.more} more</small>` : ''}</div>` : '';
|
|
54
|
+
const rec = L.record.length ? `<div class="lab-card lab-rec"><div class="lab-bar"><b>What Claude did in the background</b><small>this week, every job</small></div>${L.record.map((r) => recRow(r)).join('')}${L.more ? `<small>and ${L.more} more</small>` : ''}</div>` : '';
|
|
46
55
|
return `<h2 class="ins-h3" id="hLab">Lab</h2>`
|
|
47
56
|
+ (L.say ? `<p class="lab-say"><span aria-hidden="true">✦</span> ${esc(L.say)}</p>` : '')
|
|
48
57
|
+ L.loose.map(looseHtml).join('')
|
|
@@ -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; }
|