create-pmos 0.501.0 → 0.503.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 +39 -27
- 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/briefing-premium-calendar-2-test.js +4 -3
- package/workspace/briefing-premium-calendar-3-test.js +2 -3
- package/workspace/changelog.json +38 -0
- package/workspace/fixtures/premium-viewer-fixtures.js +3 -2
- package/workspace/guide/briefing-premium.md +1 -1
- package/workspace/guide/home.md +2 -1
- package/workspace/guide/notes.md +21 -1
- package/workspace/lib/meeting-terms.js +53 -0
- package/workspace/lib/routes-meeting-points.js +7 -4
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/premium-chat-line-test.js +3 -3
- package/workspace/premium-vw-meet-test.js +28 -17
- package/workspace/public/pm/bd-cols.js +5 -2
- package/workspace/public/pm/bd-deliveries.js +3 -3
- package/workspace/public/pm/bd-pin-card.js +3 -3
- package/workspace/public/pm/bd-pins.js +10 -0
- package/workspace/public/pm/bd-span.js +11 -9
- package/workspace/public/pm/cozy-vw-mtg.css +81 -0
- package/workspace/public/pm/home-line.css +2 -2
- package/workspace/public/pm/home-line.js +42 -13
- package/workspace/public/pm/ini-view.css +10 -0
- package/workspace/public/pm/premium-mtg-replay.js +155 -67
- package/workspace/public/pm/premium-vw-mtg.js +119 -25
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { pmPost } from './http.js';
|
|
8
8
|
import { esc } from './premium-shell.js';
|
|
9
9
|
import * as Pop from './bd-pop.js';
|
|
10
|
-
import { isPinned, shownMs, msMode, setMsMode, MS_MODES } from './bd-pins.js';
|
|
10
|
+
import { isPinned, shownMs, msMode, setMsMode, MS_MODES, endStage } from './bd-pins.js';
|
|
11
11
|
|
|
12
12
|
const PIN = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 17v5M8 3h8l-1 5 3 4H6l3-4z"/></svg>';
|
|
13
13
|
const MON = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
@@ -27,9 +27,9 @@ function cardHtml(r, msId) {
|
|
|
27
27
|
if (!L.length) return `<p class="bd-pc-h">${esc(r.title)}</p><p class="bd-amut bd-pc-sub">No milestone yet. Flag a task as a milestone ◇ in the initiative's Tasks.</p>`;
|
|
28
28
|
const m0 = L.find((m) => m.id === msId) || null, on = shownMs(r).length;
|
|
29
29
|
const head = m0 ? `<p class="bd-pc-h">${esc(m0.name)}</p><p class="bd-amut bd-pc-sub">${esc(when(m0.date))} · ${on} of ${L.length} ${L.length === 1 ? 'milestone' : 'milestones'} on the timeline</p>`
|
|
30
|
-
: r.end && /^\d{4}-\d{2}-\d{2}/.test(String(r.end))
|
|
30
|
+
: r.end && /^\d{4}-\d{2}-\d{2}/.test(String(r.end)) && endStage(r)
|
|
31
31
|
// the row's plain end ◇ (no milestone task stands for it): the card says what the row's label says, "Deliver Jun 9 ’27" (batch 14 item 1)
|
|
32
|
-
? `<p class="bd-pc-h">${esc(r
|
|
32
|
+
? `<p class="bd-pc-h">${esc(endStage(r))} ${esc(when(String(r.end).slice(0, 10)))}</p><p class="bd-amut bd-pc-sub">The end of ${esc(r.title)} · ${on} of ${L.length} ${L.length === 1 ? 'milestone' : 'milestones'} on the timeline</p>`
|
|
33
33
|
: `<p class="bd-pc-h">${esc(r.title)}</p><p class="bd-amut bd-pc-sub">${on} of ${L.length} ${L.length === 1 ? 'milestone' : 'milestones'} on the timeline</p>`;
|
|
34
34
|
const row = (m) => {
|
|
35
35
|
const p = isPinned(r, m), late = !m.done && m.date < today;
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* the last one. Filter switches the whole Board: "Milestones on the timeline: Pinned · All · None", remembered per person.
|
|
4
4
|
* isPinned(r, m) a milestone's own pin (m.pinned true or false) wins; else only the row's last dated milestone is pinned
|
|
5
5
|
* shownMs(r, mode) the row's dated milestones the timeline draws, by date
|
|
6
|
+
* endStage(r) the name of the row's plain end ◇: Deliver or Evaluate on Product feature, the own ◆ stage elsewhere, else ''
|
|
6
7
|
* msMode() · setMsMode the Filter choice: 'pinned' (the default), 'all' or 'none'
|
|
7
8
|
* Pure apart from the remembered choice. */
|
|
8
9
|
const MODE_KEY = 'pmos.board.msMode';
|
|
@@ -22,6 +23,15 @@ export function shownMs(r, mode = msMode()) {
|
|
|
22
23
|
const M = datedMs(r);
|
|
23
24
|
return mode === 'all' ? M : M.filter((m) => isPinned(r, m));
|
|
24
25
|
}
|
|
26
|
+
/** What a row's plain end ◇ stands for when no milestone task names it (batch 25 item 13). Product feature: its lifecycle's Deliver, or
|
|
27
|
+
* Evaluate past it. Another workflow (D99): its own ◆ stage, the first one not done, else the last. A workflow with no ◆ stage: '' (zero is
|
|
28
|
+
* silence: no labelled ◇, never a lifecycle name that the workflow does not have). */
|
|
29
|
+
export function endStage(r) {
|
|
30
|
+
if (!r || !r.wf || r.wf.id === 'product-feature') return r && r.track && r.track.k >= 6 ? 'Evaluate' : 'Deliver';
|
|
31
|
+
const S = (Array.isArray(r.wfStages) ? r.wfStages : []).filter((s) => s && s.milestone && s.status !== 'skipped');
|
|
32
|
+
const x = S.find((s) => s.status !== 'done') || S[S.length - 1];
|
|
33
|
+
return x ? String(x.name || '') : '';
|
|
34
|
+
}
|
|
25
35
|
let MODE = null;
|
|
26
36
|
export function msMode() {
|
|
27
37
|
if (MODE === null) { let v = null; try { v = localStorage.getItem(MODE_KEY); } catch { v = null; } MODE = MS_MODES.some(([k]) => k === v) ? v : 'pinned'; }
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
* the hairline and » only; its row sheet lists them. A gap never changes the row's dot or risk.
|
|
29
29
|
* Calmer timeline (feedback batch 13 item 4, A: XcalmA1Desk, A4Dark, A3Phone, A2Folded, A5Milestones): say it once, few marks, quiet rows.
|
|
30
30
|
* · One label per bar end: what lands, then when ("Tender award Nov 3"; the year only when it is not this year). The name is the end milestone's,
|
|
31
|
-
* else a milestone close enough to merge with the end ◇, else the row's latest milestone;
|
|
31
|
+
* else a milestone close enough to merge with the end ◇, else the row's latest milestone; a row with none says the ◆ stage of its own workflow (Deliver or Evaluate on Product feature), and on a workflow with no ◆ stage draws no labelled ◇ (batch 25 item 13). The date turns amber at risk and orange late; the rest (days over, the forecast, a slip, a clash) is its hover.
|
|
32
32
|
* With no room at the right edge the label sits before the ◇, on a card chip. No date under a ◇ and no second date.
|
|
33
33
|
* · Marks: the bar (done dark, to do light), a 2 px hairline for any gap or hold (a hold's hairline opens the hold card), one ◇ (filled done,
|
|
34
34
|
* orange late), the dashed Jira forecast, Today. The dotted hold, ⏸, the amber tint, the blue ◆ and the hatched out-of-window band are gone.
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
import { esc } from './premium-shell.js';
|
|
47
47
|
import { mergeClose } from './program-model.js';
|
|
48
48
|
import { stagesOfRow } from './bd-wf.js';
|
|
49
|
-
import { shownMs } from './bd-pins.js';
|
|
49
|
+
import { shownMs, endStage } from './bd-pins.js';
|
|
50
50
|
|
|
51
51
|
const MON = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
52
52
|
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; };
|
|
@@ -195,8 +195,8 @@ function extentOf(r, now) {
|
|
|
195
195
|
|
|
196
196
|
/** 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"). */
|
|
197
197
|
export const dy = (d, now) => md(d) + (d.getFullYear() !== now.getFullYear() ? ' ’' + String(d.getFullYear()).slice(2) : '');
|
|
198
|
-
/** What lands at a row's end: its end milestone's name, else the stage the end stands for. */
|
|
199
|
-
const landsOf = (r, et) => (et ? et.name : r
|
|
198
|
+
/** 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 ◇). */
|
|
199
|
+
const landsOf = (r, et) => (et ? et.name : endStage(r));
|
|
200
200
|
/** 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
|
|
201
201
|
* no room after the mark, it sits before it on a card chip. hover: the words a pointer reads (days over, the forecast, a slip, a clash). */
|
|
202
202
|
function endLabel(at, name, d, tone, now, pxPct, hover, slip) {
|
|
@@ -235,6 +235,8 @@ function qBar(r, X, now, phone, fmt, pxPct, ms = {}) {
|
|
|
235
235
|
// the ◇ and its label carry the milestone's own date; the initiative's end, when it differs, is the hover (batch 14 item 1)
|
|
236
236
|
const ed = et ? day(et.date) : e; if (+ed !== +e) hover.push('Initiative ends ' + dy(e, now));
|
|
237
237
|
const name = landsOf(r, et), eTitle = name + ' · ' + md(ed) + (et && et.stage ? ' · ◆ ' + et.stage : '') + (et && et.done ? ' · done' : isLate ? ' · late' : '');
|
|
238
|
+
// nothing lands (batch 25 item 13): a workflow with no ◆ stage and no dated milestone draws no ◇ and no label; the bar's own end says it
|
|
239
|
+
if (!name) return out + planMarks(r, X, phone, fmt);
|
|
238
240
|
out += `<button type="button" class="tg-end${et && et.done ? ' dn' : ''}${isLate ? ' late' : ''}" data-ms-pick="${esc(r.key)}" data-ms-id="${esc(et ? et.id : '')}" style="left:${P(X(ed))}" aria-haspopup="dialog" aria-label="${esc(eTitle + '. Milestones of ' + r.title)}" title="${esc(eTitle + (clash ? '. ' + clash : ''))}"></button>`;
|
|
239
241
|
// with the Next milestone column on, the column names it and the bar's end has no label (StsA8Board); the ◇ keeps the words as its hover
|
|
240
242
|
if (!ms.noLabel) out += endLabel(Math.max(X(e), X(ed), over ? X(now) : 0, late ? X(fc) : 0, fr ? X(fr.high) : 0), name, ed, isLate ? 'late' : risk, now, pxPct, hover, slipHtml(r));
|
|
@@ -244,7 +246,7 @@ function qBar(r, X, now, phone, fmt, pxPct, ms = {}) {
|
|
|
244
246
|
out += draftBar(r, X, phone, fmt, now, pxPct);
|
|
245
247
|
} else {
|
|
246
248
|
const d = s || e;
|
|
247
|
-
out += `<button type="button" class="tg-end" data-ms-pick="${esc(r.key)}" data-ms-id="" style="left:${P(X(d))}" aria-haspopup="dialog" aria-label="${esc((s ? 'Starts ' : 'Ends ') + md(d) + '. Milestones of ' + r.title)}" title="${s ? 'Starts' : 'Ends'} ${esc(md(d))}"></button>` + (e && !ms.noLabel ? endLabel(X(d), landsOf(r, et), d, risk, now, pxPct, [], '') : '');
|
|
249
|
+
out += `<button type="button" class="tg-end" data-ms-pick="${esc(r.key)}" data-ms-id="" style="left:${P(X(d))}" aria-haspopup="dialog" aria-label="${esc((s ? 'Starts ' : 'Ends ') + md(d) + '. Milestones of ' + r.title)}" title="${s ? 'Starts' : 'Ends'} ${esc(md(d))}"></button>` + (e && !ms.noLabel && landsOf(r, et) ? endLabel(X(d), landsOf(r, et), d, risk, now, pxPct, [], '') : '');
|
|
248
250
|
}
|
|
249
251
|
return out + planMarks(r, X, phone, fmt);
|
|
250
252
|
}
|
|
@@ -348,7 +350,7 @@ function gapsHtml(r, X, now, phone, fmt, pxPct, s, e, tone, note = '', lands = n
|
|
|
348
350
|
}
|
|
349
351
|
// the end ◇'s own name and date when a milestone task is it (the timeline-label hotfix: never a stage name for a milestone)
|
|
350
352
|
const ob = H.find((x) => x.kind === 'open' && x.b > (lands ? lands.d : e));
|
|
351
|
-
const clash = ob ? (lands ? lands.name : r
|
|
353
|
+
const clash = ob ? (lands ? lands.name : endStage(r) || 'The end') + ' is before the return, ' + md(ob.b) : null;
|
|
352
354
|
return { html, clash };
|
|
353
355
|
}
|
|
354
356
|
const iso = (d) => d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
|
|
@@ -363,7 +365,7 @@ const msStage = (r, m) => { if (!m.sec) return ''; const st = (r.wfStages || [])
|
|
|
363
365
|
* zoom's ~40 px: the timeline-label hotfix, "[Tender] 12.0 Award contract" on Nov 2 sat under the end ◇ labelled "Deliver Nov 4"; one mark says
|
|
364
366
|
* the milestone's own name and date), else the row's latest milestone task however far from the end (Wawan 2026-10-28, "End always names a
|
|
365
367
|
* milestone": a plain "Deliver Nov 11" whose card listed only "Tech blueprint approved, Apr 29" read as two answers). It takes that
|
|
366
|
-
* task's name, date and done, and is not drawn twice. Only a row with no dated milestone says
|
|
368
|
+
* task's name, date and done, and is not drawn twice. Only a row with no dated milestone says its workflow's ◆ stage (pm/bd-pins.js endStage). M: its dated milestones. */
|
|
367
369
|
function endTask(r, M, reach = 0) {
|
|
368
370
|
const e = day(r.end); if (!e || !M.length) return null;
|
|
369
371
|
const S = M.slice().sort((a, b) => day(a.date) - day(b.date));
|
|
@@ -404,9 +406,9 @@ export function laneMilestones(rows, now, reach = 0) {
|
|
|
404
406
|
for (const r of rows || []) {
|
|
405
407
|
const M = (r.milestones || []).filter((m) => day(m.date)), s = day(r.start), e = day(r.end), et = endTask(r, M, reach);
|
|
406
408
|
for (const m of M) if (!et || m.id !== et.id) out.push({ id: m.id, name: m.name, date: m.date, done: !!m.done, late: msLate(m, now), row: r });
|
|
407
|
-
if (!s || !e) continue;
|
|
409
|
+
if (!s || !e || (!et && !endStage(r))) continue;
|
|
408
410
|
const ob = openBack(r, now), late = (r.over && now > e) || (ob && ob > e) || (et && msLate(et, now));
|
|
409
|
-
out.push({ id: et ? et.id : '', name: et ? et.name : r
|
|
411
|
+
out.push({ id: et ? et.id : '', name: et ? et.name : endStage(r), date: et ? String(et.date).slice(0, 10) : iso(e), done: !!(et && et.done), late: !!late, row: r });
|
|
410
412
|
}
|
|
411
413
|
return out.sort((a, b) => day(a.date) - day(b.date));
|
|
412
414
|
}
|
|
@@ -228,6 +228,87 @@
|
|
|
228
228
|
.vzp-del:hover { color: var(--cz-ink); }
|
|
229
229
|
.vzp-gbtn { min-height: 34px; padding: 0 12px !important; border-radius: 999px; font-size: var(--fs-3); font-weight: 700; }
|
|
230
230
|
|
|
231
|
+
/* ── the recording view (batch 25 item 11, MvC/MvF/MvN): summary and lean kept rows left, the evidence right, the timeline docked ── */
|
|
232
|
+
/* the shell's wide grid (summary left, 75ch right) does not apply: this view is its own two columns */
|
|
233
|
+
.cz-vw-w2 .cz-vw-body:has(> .vz-rec) { display: block; }
|
|
234
|
+
.cz-vw-body > .vz.vz-rec { max-width: none; }
|
|
235
|
+
.vzr { container-type: inline-size; padding: 0; background: none; gap: 14px; }
|
|
236
|
+
.vzr-mbonly { display: none; }
|
|
237
|
+
.vzr-cols { display: grid; grid-template-columns: minmax(0, 1fr); gap: 16px; align-items: start; }
|
|
238
|
+
@container (min-width: 860px) { .vzr-cols { grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr); gap: 20px; } .vzr-ev { position: sticky; top: 0; } }
|
|
239
|
+
.vzr-l { display: flex; flex-direction: column; gap: 14px; min-width: 0; }
|
|
240
|
+
.vzr-sum .czs-p { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; font-size: var(--fs-3); line-height: 1.55; margin: 0; }
|
|
241
|
+
.vzr-sum.all .czs-p { display: block; }
|
|
242
|
+
.vzr-sm { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
|
|
243
|
+
.vzr-sm .czs-meta { margin: 0 0 0 auto; }
|
|
244
|
+
.vzr-more2 { font-size: var(--fs-2); font-weight: 800; color: var(--cz-blueink); padding: 4px 0 !important; }
|
|
245
|
+
.vzr-pts { border: 1px solid var(--cz-hair); border-radius: 20px; background: var(--cz-card); overflow: hidden; }
|
|
246
|
+
.vzr-ph { display: flex; align-items: center; gap: 12px; padding: 12px 14px 8px 18px; flex-wrap: wrap; }
|
|
247
|
+
.vzr-ph h3 { margin: 0; font-size: var(--fs-4); font-weight: 800; }
|
|
248
|
+
.vzr-cnt { display: inline-flex; align-items: center; gap: 4px; font-size: var(--fs-2); font-weight: 700; color: var(--cz-ink2); }
|
|
249
|
+
.vzr-cfall { display: inline-flex !important; align-items: center; gap: 6px; }
|
|
250
|
+
.vzr-k { display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px; flex: none; color: var(--cz-ink2); }
|
|
251
|
+
.vzr-k[data-k="decision"]::before { content: ''; width: 10px; height: 10px; border-radius: 50%; background: var(--cz-ok); }
|
|
252
|
+
.vzr-k[data-k="question"] { color: var(--cz-late); }
|
|
253
|
+
.vzr-k svg, .vzr-bx { fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
|
|
254
|
+
.vzp-kr.vzr-row { grid-template-columns: 20px minmax(0, 1fr) auto 32px; gap: 0 12px; align-items: start; padding: 10px 12px 10px 18px; }
|
|
255
|
+
.vzp-kr.vzr-row .vzr-k { margin-top: 1px; }
|
|
256
|
+
.vzp-kr.vzr-row.sel { margin: 2px 6px; padding-left: 12px; padding-right: 6px; }
|
|
257
|
+
.vzr-row .vzp-kt b { font-weight: 800; line-height: 1.35; }
|
|
258
|
+
.vzr-row .vzp-tm { padding-top: 2px !important; min-width: 44px; }
|
|
259
|
+
.vzr-cf { width: 30px; height: 30px; border-radius: 50%; border: 1.6px solid var(--cz-hair) !important; display: inline-grid !important; place-items: center; color: var(--cz-ink3); margin-top: -3px; }
|
|
260
|
+
.vzr-cf:hover { color: var(--cz-ok); border-color: var(--cz-ok) !important; }
|
|
261
|
+
.vzr-cf.on { background: var(--cz-okbg) !important; border-color: transparent !important; color: var(--cz-ok); }
|
|
262
|
+
.vzr-fu { grid-column: 2 / 5; display: flex; align-items: center; gap: 10px; margin-top: 6px; font-size: var(--fs-2); color: var(--cz-ink2); }
|
|
263
|
+
.vzr-fu.vzr-sub { margin-top: 2px; }
|
|
264
|
+
.vzr-bx { color: var(--cz-ink3); flex: none; }
|
|
265
|
+
.vzr-grow { flex: 1; min-width: 0; }
|
|
266
|
+
.vzr-who { color: var(--cz-ink3); }
|
|
267
|
+
.vzr-tk { display: inline-flex; align-items: center; height: 22px; padding: 0 9px; border-radius: 999px; background: var(--cz-bluebg); color: var(--cz-blueink); font-size: var(--fs-1); font-weight: 800; }
|
|
268
|
+
.vzr-ghost { background: var(--cz-well) !important; }
|
|
269
|
+
.vzr-chk { grid-column: 2 / 5; margin: 4px 0 0; font-size: var(--fs-2); color: var(--cz-late); }
|
|
270
|
+
.vzr-pad { padding: 4px 18px 14px; }
|
|
271
|
+
.vzr-fold { display: flex; gap: 6px; flex-wrap: wrap; }
|
|
272
|
+
.vzr-ch { min-height: 34px; padding: 0 13px !important; border-radius: 999px; background: var(--cz-well) !important; display: inline-flex !important; align-items: center; gap: 7px; font-size: var(--fs-2); color: var(--cz-ink2); }
|
|
273
|
+
.vzr-ch b { font-weight: 800; color: var(--cz-ink); }
|
|
274
|
+
.vzr-ch span { color: var(--cz-ink3); }
|
|
275
|
+
.vzr-ch[aria-expanded="true"] { background: var(--cz-ink) !important; color: var(--cz-card); }
|
|
276
|
+
.vzr-ch[aria-expanded="true"] b, .vzr-ch[aria-expanded="true"] span { color: var(--cz-card); }
|
|
277
|
+
.vzr-panel { display: flex; flex-direction: column; gap: 6px; padding: 12px 14px; border-radius: 18px; border: 1px solid var(--cz-hair); background: var(--cz-card); }
|
|
278
|
+
.vzr-ev { display: flex; flex-direction: column; gap: 12px; padding: 14px; border-radius: 20px; border: 1px solid var(--cz-hair); background: var(--cz-card); min-width: 0; }
|
|
279
|
+
.vzr-shot { aspect-ratio: 16 / 9; background: var(--cz-well) !important; }
|
|
280
|
+
.vzr-noshot { display: grid; place-items: center; min-height: 120px; border-radius: 12px; background: var(--cz-well); padding: 12px; text-align: center; }
|
|
281
|
+
.vzr-acts { display: flex; align-items: center; gap: 10px; }
|
|
282
|
+
.vzr-pri { width: 40px !important; height: 40px !important; background: var(--cz-blueink) !important; color: var(--cz-card) !important; }
|
|
283
|
+
.vzr-at { font-size: var(--fs-3); font-weight: 800; color: var(--cz-blueink); font-variant-numeric: tabular-nums; }
|
|
284
|
+
.vzr-q { margin: 0; font-size: var(--fs-3); line-height: 1.5; color: var(--cz-ink2); }
|
|
285
|
+
.vzr-film { display: flex; gap: 6px; align-items: center; overflow: hidden; }
|
|
286
|
+
.vzr-film.all { flex-wrap: wrap; }
|
|
287
|
+
.vzr-film .vzp-th { width: 72px; height: 44px; }
|
|
288
|
+
.vzr-more { height: 44px; min-width: 44px; padding: 0 8px !important; border-radius: 6px; background: var(--cz-well) !important; color: var(--cz-ink2); font-size: var(--fs-2); font-weight: 800; display: inline-grid !important; place-items: center; }
|
|
289
|
+
.vzr-dock { position: sticky; bottom: 0; z-index: 2; flex-direction: row !important; align-items: center; gap: 14px !important; padding: 10px 16px !important; border-radius: 22px; background: var(--cz-card) !important; box-shadow: 0 18px 44px -18px color-mix(in oklab, var(--cz-ink) 40%, transparent), 0 0 0 1px var(--cz-hair); }
|
|
290
|
+
.vzr-dock .vzp-ctl { flex: none; }
|
|
291
|
+
.vzr-dock .vzp-ctl .vzp-mut { flex: none; }
|
|
292
|
+
.vzr-tl { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
|
|
293
|
+
.vzr-tops { align-items: flex-end; }
|
|
294
|
+
.vzr-tops .vzp-top { white-space: normal; text-overflow: clip; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-height: 1.2; font-weight: 700; color: var(--cz-ink2); padding: 0 6px 0 0 !important; text-align: left !important; }
|
|
295
|
+
.vzr-tops .vzp-top[aria-current="true"] { color: var(--cz-ink); font-weight: 800; }
|
|
296
|
+
.vzp-kd[data-k="decision"] { fill: var(--cz-ok); }
|
|
297
|
+
.vzp-kd[data-k="action"] { fill: var(--cz-blue); }
|
|
298
|
+
.vzp-kd[data-k="question"] { fill: var(--cz-late); }
|
|
299
|
+
/* a word fixed in place (MvF): fixed in green, a guess underlined dotted, the field in place */
|
|
300
|
+
.vzr-fx { background: var(--cz-okbg); border-radius: 5px; padding: 0 3px; font-weight: 700; color: var(--cz-ink); }
|
|
301
|
+
.vzr-fl { display: inline !important; padding: 0 !important; font: inherit; color: var(--cz-ink); font-weight: 700; border-bottom: 2px dotted var(--cz-late) !important; border-radius: 0 !important; cursor: pointer; }
|
|
302
|
+
.vzr-fe { display: inline-flex; align-items: center; gap: 4px; padding: 0 2px 0 8px; border-radius: 10px; background: var(--cz-card); box-shadow: 0 0 0 2px var(--cz-blue); vertical-align: middle; }
|
|
303
|
+
.vzr-fe input { width: 12ch; height: 28px; border: 0; outline: 0; background: transparent; font: inherit; font-weight: 800; color: var(--cz-ink); }
|
|
304
|
+
.vzr-ok { width: 26px; height: 26px; border-radius: 50%; display: inline-grid !important; place-items: center; background: var(--cz-blueink) !important; color: var(--cz-card); }
|
|
305
|
+
.vzr-ok svg, .vzr-ev .vzp-ib svg, .vzr-pts svg, .vzr-cfall svg { fill: none; stroke: currentColor; stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; }
|
|
306
|
+
.vzr-ev .vzp-fill, .vzr-pri svg { fill: currentColor; stroke: none; }
|
|
307
|
+
@media (max-width: 640px) {
|
|
308
|
+
.vzr-mbonly { display: block; }
|
|
309
|
+
.vzr-mbonly > .czs-secs { margin-top: 0; }
|
|
310
|
+
}
|
|
311
|
+
|
|
231
312
|
/* one Replay at a time: the phone's below 640px, else the desk's */
|
|
232
313
|
.vzp.vzp-mb { display: none; }
|
|
233
314
|
@media (max-width: 640px) {
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
.pl-sl { margin-top: 2px; }
|
|
21
21
|
.pl-now { display: flex; align-items: center; gap: 8px; margin: 4px 0 0; padding-top: 10px; border-top: 1px solid var(--cz-hair); font-size: var(--fs-2); min-width: 0; }
|
|
22
22
|
.pl-now b { flex: none; font-weight: 800; }
|
|
23
|
-
.pl-
|
|
24
|
-
.hm-pl.open .pl-
|
|
23
|
+
.pl-nt { min-width: 0; color: var(--cz-ink2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
24
|
+
.hm-pl.open .pl-nt { white-space: normal; }
|
|
25
25
|
.pl-acts { display: flex; gap: 8px; margin-top: 10px; }
|
|
26
26
|
.pl-ib { width: 44px; height: 44px; border-radius: 50%; border: 1px solid var(--cz-hair); background: var(--cz-card); color: var(--cz-ink2); display: inline-flex; align-items: center; justify-content: center; cursor: pointer; }
|
|
27
27
|
.pl-ib.pri { background: var(--cz-solid); color: var(--cz-onsolid); border-color: transparent; }
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
* and models (the first call in a page adds /pm/home-line.css once): Node tests them (chat-line.test.js).
|
|
3
3
|
*
|
|
4
4
|
* One line at the top of a chat shows the progress of the goal or of the session (one thing: Wawan, 2026-10-25). Folded it shows a spinner,
|
|
5
|
-
* the step it is on, "1 of 4 done" and Stop. A tap opens it in place on a phone (the goal's points, a Now row with the
|
|
6
|
-
* Watch its terminal) and in the Viewer on a desktop (viewHtml). A session that never wrote a step list
|
|
7
|
-
*
|
|
5
|
+
* the step it is on, "1 of 4 done" and Stop. A tap opens it in place on a phone (the goal's points, a Now row with what the session does
|
|
6
|
+
* in plain words, Watch its terminal) and in the Viewer on a desktop (viewHtml). A session that never wrote a step list leads with the turn's
|
|
7
|
+
* goal, Claude's newest one-line status ("Checking which programs exist · 0:12"), else "Working · 3:12", never an invented count.
|
|
8
|
+
* Raw tool calls ("Bash · cat …") never show on the line or in its body: they stay behind the terminal icon (batch 25 item 14). When Claude needs the PM the line turns blue ("Claude asks before it goes on"). When the run ends the line goes away and a
|
|
8
9
|
* divider stays where it finished (dividerHtml): "Goal done", or in amber why it stopped.
|
|
9
10
|
*
|
|
10
11
|
* goalModel(goal, conv, o) a working goal (state started or waiting) → model, else null
|
|
@@ -48,6 +49,28 @@ export function clockOf(conv, now = Date.now()) {
|
|
|
48
49
|
const s = Math.max(0, Math.round((now - t0) / 1000));
|
|
49
50
|
return Math.floor(s / 60) + ':' + pad(s % 60);
|
|
50
51
|
}
|
|
52
|
+
const MD = (t) => t.replace(/[*_`]+/g, '').replace(/^\s*(?:#+|[-*•>]|\d+[.)])\s*/, '').replace(/\[([^\]]+)\]\([^)]*\)/g, '$1').trim();
|
|
53
|
+
/** The turn's goal in plain words: the last line of Claude's newest message in the turn that runs now (its status before the tool call),
|
|
54
|
+
* without Markdown, a trailing colon or code; a long line keeps its last sentence. '' when Claude wrote nothing since the PM's message. */
|
|
55
|
+
export function turnGoal(conv) {
|
|
56
|
+
const ms = (conv && conv.messages) || [];
|
|
57
|
+
for (let i = ms.length - 1; i >= 0; i--) {
|
|
58
|
+
if (ms[i].role === 'user') return '';
|
|
59
|
+
if (ms[i].role !== 'assistant' || !ms[i].text) continue;
|
|
60
|
+
let fence = false;
|
|
61
|
+
const lines = String(ms[i].text).split('\n').filter((l) => { if (/^\s*(```|~~~)/.test(l)) { fence = !fence; return false; } return !fence && !/^\s*\|/.test(l); })
|
|
62
|
+
.map(MD).filter((l) => /[a-z]/i.test(l));
|
|
63
|
+
let t = (lines.pop() || '').replace(/[::]\s*$/, '');
|
|
64
|
+
if (t.length > 120) t = t.split(/(?<=[.!?])\s+/).filter(Boolean).pop() || t;
|
|
65
|
+
if (t) return t.length > 120 ? t.slice(0, 119).trimEnd() + '…' : t;
|
|
66
|
+
}
|
|
67
|
+
return '';
|
|
68
|
+
}
|
|
69
|
+
/** What the session does now, in plain words: the to-do it marked in progress, else the turn's goal. Never a raw tool call. */
|
|
70
|
+
export function nowText(conv) {
|
|
71
|
+
const st = conv && Array.isArray(conv.steps) ? conv.steps.find((s) => s && s.state === 'now') : null;
|
|
72
|
+
return (st && st.text) || turnGoal(conv);
|
|
73
|
+
}
|
|
51
74
|
/** The newest tool call of the turn that runs now ("Edit · planning/prd/x.md"), or '' once the PM wrote after it. */
|
|
52
75
|
export function nowCall(conv) {
|
|
53
76
|
const ms = (conv && conv.messages) || [];
|
|
@@ -113,7 +136,7 @@ export function goalModel(goal, conv, o = {}) {
|
|
|
113
136
|
count: asking ? '' : `${nMet} of ${count} done`, clock: '',
|
|
114
137
|
title: d.goal || 'Goal',
|
|
115
138
|
head: `${nMet} of ${count} points met · round ${Math.max(1, (goal.rounds || 0) + 1)} of ${limit}${d.by ? ' · due ' + d.by : ''} · Claude checks its own points`,
|
|
116
|
-
items: w.items, more: w.more, now: run ? nowCall(conv) : '', term: !!o.term,
|
|
139
|
+
items: w.items, more: w.more, now: run && !asking ? nowText(conv) : '', call: run ? nowCall(conv) : '', term: !!o.term,
|
|
117
140
|
open: !!o.open, on: !!o.on, frac: count ? nMet / count : 0,
|
|
118
141
|
};
|
|
119
142
|
}
|
|
@@ -124,20 +147,23 @@ export function sessionModel(conv, o = {}) {
|
|
|
124
147
|
const steps = Array.isArray(conv.steps) && conv.steps.length ? conv.steps : null;
|
|
125
148
|
const asking = !!o.asking;
|
|
126
149
|
const clock = clockOf(conv, o.now);
|
|
150
|
+
const call = running(conv) ? nowCall(conv) : '';
|
|
127
151
|
if (!steps) {
|
|
128
|
-
|
|
129
|
-
|
|
152
|
+
// no to-do list: the turn's goal leads (Claude's newest status line), the clock sits at the end; with none, "Working · 3:12"
|
|
153
|
+
const goal = asking ? '' : turnGoal(conv);
|
|
154
|
+
return { kind: 'session', icon: asking ? 'none' : 'spin', tone: asking ? 'you' : '', text: asking ? 'Claude asks before it goes on' : goal || 'Working', clock: asking ? '' : clock, count: '',
|
|
155
|
+
clockEnd: !!goal, title: o.title || 'Working', head: goal && clock ? 'Working · ' + clock : '', items: [], more: 0, now: '', call, term: !!o.term, open: !!o.open, on: !!o.on, frac: 0, plain: true };
|
|
130
156
|
}
|
|
131
157
|
const cur = steps.findIndex((s) => s.state === 'now');
|
|
132
158
|
const done = steps.filter((s) => s.state === 'done').length;
|
|
133
|
-
const all = steps.map((s) => ({ text: s.text, state: s.state === 'done' ? 'done' : s.state === 'now' && !asking ? 'now' : 'next', sub: s.sub || ''
|
|
159
|
+
const all = steps.map((s) => ({ text: s.text, state: s.state === 'done' ? 'done' : s.state === 'now' && !asking ? 'now' : 'next', sub: s.sub || '' }));
|
|
134
160
|
const w = window5(all, cur, o.all);
|
|
135
161
|
return {
|
|
136
162
|
kind: 'session', icon: asking ? 'none' : 'spin', tone: asking ? 'you' : '',
|
|
137
163
|
text: asking ? 'Claude asks before it goes on' : cur >= 0 ? steps[cur].text : 'Working',
|
|
138
164
|
count: asking ? '' : `${done} of ${steps.length} done`, clock: '',
|
|
139
165
|
title: o.title || 'Steps', head: `${done} of ${steps.length} done${clock ? ' · ' + clock : ''}`,
|
|
140
|
-
items: w.items, more: w.more, now: cur >= 0 && steps[cur].call
|
|
166
|
+
items: w.items, more: w.more, now: '', call: (cur >= 0 && steps[cur].call) || call, term: !!o.term,
|
|
141
167
|
open: !!o.open, on: !!o.on, frac: steps.length ? done / steps.length : 0,
|
|
142
168
|
};
|
|
143
169
|
}
|
|
@@ -148,7 +174,9 @@ function listHtml(m, label) {
|
|
|
148
174
|
owner: x.avatar ? { kind: 'person', initials: x.avatar.initial, name: x.avatar.name } : null, act: x.act || null }));
|
|
149
175
|
return stepsHtml(steps, { label, cls: 'pl-sl', more: m.more ? { n: m.more, attr: 'data-pl="all"', label: 'Show all ' + m.more } : null });
|
|
150
176
|
}
|
|
151
|
-
const nowRow = (m) => (m.now ? `<p class="pl-now">${spinHtml(14)}<b>Now</b><
|
|
177
|
+
const nowRow = (m) => (m.now ? `<p class="pl-now">${spinHtml(14)}<b>Now</b><span class="pl-nt">${esc(m.now)}</span></p>` : '');
|
|
178
|
+
/** Watch its terminal: the raw tool call is only behind it, as the icon's tooltip. */
|
|
179
|
+
const termBtn = (m) => `<button type="button" class="pl-ib" data-pl="term" aria-label="Watch its terminal" title="${esc('Watch its terminal' + (m.call ? ' · ' + m.call : ''))}">${SVG(P.term, 18, 1.8)}</button>`;
|
|
152
180
|
const leadOf = (m) => (m.icon === 'spin' ? spinHtml(16) : m.icon === 'clock' ? `<span class="pl-clk">${SVG(P.clock, 16, 2)}</span>` : m.tone === 'you' ? '<i class="pl-dot" aria-hidden="true"></i>' : '');
|
|
153
181
|
|
|
154
182
|
/** The line. Folded: one row; open: the head line, the items, the Now row and Watch its terminal. */
|
|
@@ -156,13 +184,14 @@ export function lineHtml(m) {
|
|
|
156
184
|
if (!m) return '';
|
|
157
185
|
ensureCss();
|
|
158
186
|
const tail = m.count ? `<span class="pl-n">${esc(m.count)}</span>` : m.clock ? `<span class="pl-n">${esc(m.clock)}</span>` : '';
|
|
159
|
-
const
|
|
187
|
+
const inl = m.clock && !m.count && !m.clockEnd; // "Working · 3:12" reads as one; a goal line keeps its clock at the end
|
|
188
|
+
const text = inl && m.icon !== 'none' ? m.text + ' · ' + m.clock : m.text;
|
|
160
189
|
const say = [m.open ? m.title : text, m.count].filter(Boolean).join(', ');
|
|
161
190
|
const stop = `<button type="button" class="pl-stop" data-pl="stop" aria-label="${esc(m.stopLabel || 'Stop')}" title="${esc(m.stopLabel || 'Stop')}">${STOP_SQ}</button>`;
|
|
162
191
|
const row = `<div class="pl-row"><button type="button" class="pl-hit" data-pl="toggle" aria-expanded="${!!m.open}" aria-label="${esc(say)}">${leadOf(m)}`
|
|
163
|
-
+ `<span class="pl-t">${esc(m.open && m.kind === 'goal' ? m.title : text)}</span>${m.open ? '' : (
|
|
192
|
+
+ `<span class="pl-t">${esc(m.open && m.kind === 'goal' ? m.title : text)}</span>${m.open ? '' : (inl ? '' : tail)}<span class="pl-cv">${SVG(m.open ? P.up : P.down, 14, 2.2)}</span></button>${stop}</div>`;
|
|
164
193
|
const body = m.open && m.tone !== 'you' ? `<div class="pl-body">${m.head ? `<p class="pl-head">${esc(m.head)}</p>` : ''}${m.items.length ? listHtml(m, m.kind === 'goal' ? 'The goal’s points' : 'Steps') : ''}${nowRow(m)}`
|
|
165
|
-
+ (m.term ? `<div class="pl-acts"
|
|
194
|
+
+ (m.term ? `<div class="pl-acts">${termBtn(m)}</div>` : '') + '</div>' : '';
|
|
166
195
|
const cls = ['hm-pl', m.tone === 'you' ? 'you' : '', m.open ? 'open' : '', m.on ? 'on' : '', m.plain ? 'plain' : ''].filter(Boolean).join(' ');
|
|
167
196
|
return `<div class="${cls}" role="status" data-kind="${esc(m.kind || '')}">${row}${body}</div>`;
|
|
168
197
|
}
|
|
@@ -174,7 +203,7 @@ export function viewHtml(m) {
|
|
|
174
203
|
return `<div class="vwdoc pl-vw"><div class="pl-vwh"><b>${esc(m.title)}</b><button type="button" class="pl-stop" data-pl="stop" aria-label="${esc(m.stopLabel || 'Stop')}" title="${esc(m.stopLabel || 'Stop')}">${STOP_SQ}</button></div>`
|
|
175
204
|
+ (m.head ? `<p class="pl-head">${esc(m.head)}</p>` : '')
|
|
176
205
|
+ (m.items.length ? listHtml({ ...m, more: 0 }, m.kind === 'goal' ? 'The goal’s points' : 'Steps') : '') + nowRow(m)
|
|
177
|
-
+ (m.term || m.change ? `<div class="pl-acts">${m.term ?
|
|
206
|
+
+ (m.term || m.change ? `<div class="pl-acts">${m.term ? termBtn(m) : ''}`
|
|
178
207
|
// m.change: the pencil that opens the plan editor for a working goal (home-goal.js; the change applies after the current step)
|
|
179
208
|
+ `${m.change ? `<button type="button" class="pl-ib" data-goal="change" aria-label="Change the plan" title="Change the plan">${SVG('M4 20h4L19 9l-4-4L4 16zM13.5 6.5l4 4', 18, 1.8)}</button>` : ''}</div>` : '') + '</div>';
|
|
180
209
|
}
|
|
@@ -290,6 +290,15 @@ button.iv-bxw.jr:hover .iv-cb { box-shadow: 0 0 0 3px var(--cz-well); }
|
|
|
290
290
|
.ivt-n { position: sticky; left: 0; z-index: 3; width: var(--nw); flex: none; background: var(--cz-card); display: flex; align-items: center; gap: 10px; padding: 0 10px 0 14px; min-width: 0; }
|
|
291
291
|
.ivt-r.k .ivt-n { padding-left: 34px; }
|
|
292
292
|
.ivt-n .iv-tt { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; flex: 1; font-size: var(--fs-2); }
|
|
293
|
+
/* batch 25 item 4 (TaskNamesWrap, agreed A): on a desktop a task's name is never cut in the Timeline either. It wraps in the name column;
|
|
294
|
+
* the row grows and its bar stays in the middle. (A phone keeps each title on one line above its bar.) */
|
|
295
|
+
@container (min-width: 521px) {
|
|
296
|
+
.ivt-r.k .ivt-n { padding-top: 7px; padding-bottom: 7px; }
|
|
297
|
+
.ivt-r.k .ivt-n .iv-tt { white-space: normal; overflow: visible; text-overflow: clip; overflow-wrap: break-word; line-height: 1.35; flex: 1 1 0; }
|
|
298
|
+
/* the days of a hidden Start or Due column sit under the name, not beside it (the name keeps the width) */
|
|
299
|
+
.ivt-r.k .ivt-t { flex-wrap: wrap; row-gap: 2px; }
|
|
300
|
+
.ivt-r.k .ivt-wn { flex-basis: 100%; padding-left: 28px; flex-wrap: wrap; white-space: nowrap; }
|
|
301
|
+
}
|
|
293
302
|
.ivt-r.x .iv-tt { color: var(--cz-ink3); font-weight: 600; }
|
|
294
303
|
.ivt-n .iv-bxw { align-self: center; }
|
|
295
304
|
.ivt-n .iv-cb { margin-top: 0 !important; }
|
|
@@ -826,6 +835,7 @@ button.lk-mt:hover { color: var(--cz-ink) !important; }
|
|
|
826
835
|
.iva-r.k .iva-n { padding-left: 14px; }
|
|
827
836
|
.iva-r.k .iva-l { padding-left: 14px; }
|
|
828
837
|
.iva-r.k .iva-l .iv-tt { font-size: var(--fs-2); font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; flex: 0 1 auto; }
|
|
838
|
+
@container (min-width: 521px) { .iva-r.k .iva-l { padding-top: 7px; padding-bottom: 7px; } .iva-r.k .iva-l .iv-tt { white-space: normal; overflow: visible; text-overflow: clip; overflow-wrap: break-word; line-height: 1.35; } } /* the full name, as in the Timeline (batch 25 item 4) */
|
|
829
839
|
.iva-r.x .iva-l .iv-tt { color: var(--cz-ink3); font-weight: 600; }
|
|
830
840
|
.iva-nt { font-size: 11px; font-weight: 800; white-space: nowrap; flex: none; }
|
|
831
841
|
.iva-nt.lt { color: var(--cz-late); }
|