create-pmos 0.525.0 → 0.526.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 +38 -41
- 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-board-test.js +1 -1
- package/workspace/changelog.json +37 -0
- package/workspace/ev-everywhere-test.js +2 -2
- package/workspace/guide/agent-capabilities.md +28 -2
- package/workspace/guide/board.md +14 -1
- package/workspace/guide/claude-proposes.md +3 -1
- package/workspace/guide/home.md +11 -5
- package/workspace/guide/idle-lab.md +2 -2
- package/workspace/guide/lifecycle.md +16 -3
- package/workspace/guide/today.md +11 -3
- package/workspace/lib/bg-queue.js +7 -3
- package/workspace/lib/board-premium.js +4 -1
- package/workspace/lib/engine/boards.js +3 -2
- package/workspace/lib/headless.js +19 -4
- package/workspace/lib/idle-lab-engine.js +531 -0
- package/workspace/lib/initiative-chat.js +12 -2
- package/workspace/lib/initiative-cli.js +1 -1
- package/workspace/lib/initiative-premium.js +11 -3
- package/workspace/lib/lab-cli.js +220 -0
- package/workspace/lib/lifecycle-stage.js +17 -3
- package/workspace/lib/one-stage.js +131 -0
- package/workspace/lib/pmos-verbs.js +3 -0
- package/workspace/lib/proposals.js +5 -2
- package/workspace/lib/report-draft.js +1 -1
- package/workspace/lib/route-modules.js +1 -0
- package/workspace/lib/routes-board-premium.js +22 -0
- package/workspace/lib/routes-boards.js +61 -12
- package/workspace/lib/routes-notes.js +1 -1
- package/workspace/lib/routes-your-move.js +64 -0
- package/workspace/lib/status-signal.js +123 -0
- package/workspace/lib/workflow-cli.js +3 -1
- package/workspace/lib/your-move.js +61 -0
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/bd-acts.js +2 -1
- package/workspace/public/pm/bd-cols.js +8 -5
- package/workspace/public/pm/bd-prog-panel.js +3 -2
- package/workspace/public/pm/bd-span.js +2 -1
- package/workspace/public/pm/bd-views.js +8 -6
- package/workspace/public/pm/cozy-home.css +3 -0
- package/workspace/public/pm/cozy-today.css +151 -0
- package/workspace/public/pm/cozy.css +9 -0
- package/workspace/public/pm/good-morning.js +327 -0
- package/workspace/public/pm/grill-words.js +16 -1
- package/workspace/public/pm/home-app.js +187 -18
- package/workspace/public/pm/home-data.js +45 -13
- package/workspace/public/pm/home-day.js +66 -14
- package/workspace/public/pm/home-ini-model.js +1 -1
- package/workspace/public/pm/home-stage-card.js +7 -0
- package/workspace/public/pm/ini-tasks.js +2 -2
- package/workspace/public/pm/ini-view-model.js +10 -3
- package/workspace/public/pm/ini-view.css +9 -0
- package/workspace/public/pm/ini-view.js +19 -5
- package/workspace/public/pm/move-rank.js +155 -0
- package/workspace/public/pm/premium-board.js +7 -4
- package/workspace/public/pm/premium-initiative-head.js +39 -4
- package/workspace/public/pm/stage-chip.js +44 -0
- package/workspace/run-tests.js +1 -0
- package/workspace/server.js +43 -1
- package/workspace/skills/idle-lab-prototype.skill +36 -0
- package/workspace/skills/idle-lab-scan.skill +32 -0
- package/workspace/test-lists/good-morning.json +9 -0
- package/workspace/test-lists/idle-lab-engine.json +9 -0
- package/workspace/test-lists/one-stage.json +5 -0
- package/workspace/test-lists/status-signal.json +5 -0
- package/workspace/test-lists/your-move.json +9 -0
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
* "Good morning" opens Your day as an item in the Viewer (a phone only since batch 14 item 4). Batch 15 item 3 (design E, frames YdE1Ready,
|
|
4
4
|
* YdE2AllClear, YdE3Evening, YdE6BigDay, YdE7Rough): the opening card is Claude's small mood face and one first-person line, with chips for
|
|
5
5
|
* the work Claude really did; Your move holds only the calls that need the PM; the rest folds:
|
|
6
|
-
* Your move
|
|
6
|
+
* Your move one ranked list (workspace grill W3, frame WgW3Move; pm/move-rank.js): every open ✦ of the products Home reads, in
|
|
7
|
+
* order of consequence, each with its age and one press, then the calls only the PM can make (homeList's move rows
|
|
8
|
+
* with call: true). Proposals older than two weeks fold under "Older", never gone. Open by default
|
|
7
9
|
* New for you (feedback batch 16 item 5b, canvas FmA2Day) the FYI items of GET /api/news-for-you: tasks given to the PM and work
|
|
8
|
-
* finished while away. The
|
|
10
|
+
* finished while away. The tasks given to the PM are one row, "32 tasks given to you" (W3), that opens the list; the 3
|
|
11
|
+
* newest other items, then "N more" (the whole list in the Viewer, cozy-news.js); ✓✓ in the fold's header
|
|
9
12
|
* marks all seen; today's digests of the morning brief are one row. No item and no digest, no fold. It replaces the New
|
|
10
13
|
* for you page of the place menu.
|
|
11
14
|
* Drafted for you the drafts of today's morning prep run (GET /api/phone-day finished) and the weekly update; a draft the PM is done
|
|
@@ -32,13 +35,14 @@
|
|
|
32
35
|
import { todayOf, hm } from './home-data.js';
|
|
33
36
|
import { adler, adlerScene, asleep, firstOpenToday, dress, dressDay } from './cozy-adler.js'; // Adler, Claude's face (batch 16 item 2): AdB1-3, AdP1, AdP4; dressed for the season (batch 18 item 8-B)
|
|
34
37
|
import { initials, ago } from './cozy-evmodel.js';
|
|
38
|
+
import { rankMove } from './move-rank.js'; // Your move is one ranked list (workspace grill W3)
|
|
35
39
|
|
|
36
40
|
const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
37
41
|
const plural = (n, one, many) => n + ' ' + (n === 1 ? one : many);
|
|
38
42
|
const clip = (s, n) => { const t = String(s || '').replace(/\s+/g, ' ').trim(); return t.length > n ? t.slice(0, n - 1) + '…' : t; };
|
|
39
43
|
export const DAY_ICON = 'M12 3l1.8 4.6L18 9l-4.2 1.4L12 15l-1.8-4.6L6 9l4.2-1.4z';
|
|
40
44
|
const CLOCK = 'M12 4a8 8 0 1 0 0 16a8 8 0 1 0 0-16M12 8v4l3 2', CAL = 'M4 5h16v15H4zM8 3v4M16 3v4M4 10h16', CHEV = 'M10 7l5 5-5 5';
|
|
41
|
-
const CHECK = 'M5 12.5l4.5 4.5L19 7', OPEN = 'M14 4h6v6M20 4l-9 9M18 14v6H4V6h6';
|
|
45
|
+
const CHECK = 'M5 12.5l4.5 4.5L19 7', OPEN = 'M14 4h6v6M20 4l-9 9M18 14v6H4V6h6', PEN = 'M4 20h4L19 9l-4-4L4 16zM13.5 6.5l4 4', XX = 'M6 6l12 12M18 6L6 18';
|
|
42
46
|
const AGAIN = 'M4 12a8 8 0 0 1 14-5M20 12a8 8 0 0 1-14 5M18 3v4h-4M6 21v-4h4';
|
|
43
47
|
const svg = (d, w = 15, sw = 1.8) => `<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>`;
|
|
44
48
|
|
|
@@ -96,7 +100,7 @@ export function tomorrowOf(D, now) {
|
|
|
96
100
|
|
|
97
101
|
/** Your day's words. M: homeList's model (move); D: Home's data (today's meetings, phoneDay, the brief page's me and mentions);
|
|
98
102
|
* R: reviewModel. o: { delightOk: false when the delighted face showed on another day this week, news: the GET /api/news-for-you answer,
|
|
99
|
-
* ask: home-mentions dayAskOf (the one waiting ask the opening card names) }.
|
|
103
|
+
* ask: home-mentions dayAskOf (the one waiting ask the opening card names), rank: { products, programs, move } for pm/move-rank.js (W3) }.
|
|
100
104
|
* → { mood, line, sub, chips, misses, offer, calls, drafts, wait, done, next, week, thanks, news, tomorrow } */
|
|
101
105
|
export function dayModel(M, D, R, now = Date.now(), o = {}) {
|
|
102
106
|
D = D || {};
|
|
@@ -104,7 +108,8 @@ export function dayModel(M, D, R, now = Date.now(), o = {}) {
|
|
|
104
108
|
const t = todayOf(D).filter((m) => m && m.start && !Number.isNaN(Date.parse(m.start))).sort((x, y) => Date.parse(x.start) - Date.parse(y.start));
|
|
105
109
|
const mins = t.reduce((n, m) => { const s = span(m); return n + Math.max(0, Math.round((s.b - s.a) / 60000)); }, 0);
|
|
106
110
|
const next = t.find((m) => m.state === 'now') || t.find((m) => m.state !== 'past' && span(m).b > now) || null;
|
|
107
|
-
|
|
111
|
+
// no meeting, no meeting line (W3): the day says nothing about meetings it does not have
|
|
112
|
+
const meetText = !t.length ? '' : plural(t.length, 'meeting', 'meetings') + ', ' + dur(mins) + '. '
|
|
108
113
|
+ (!next ? 'They are all over.' : next.state === 'now' ? (next.title || 'A meeting') + ' is on now.' : 'The ' + (next === t[0] ? 'first' : 'next') + ' is at ' + hm(next.start) + '.');
|
|
109
114
|
const un = R && R.started && !R.done ? 'Step ' + (R.at + 1) + ' of 4' : '';
|
|
110
115
|
|
|
@@ -134,7 +139,7 @@ export function dayModel(M, D, R, now = Date.now(), o = {}) {
|
|
|
134
139
|
mood = 'concerned';
|
|
135
140
|
line = 'Straight answer: ' + (misses.length ? (misses.length === 1 ? 'I couldn’t finish one of today’s drafts.' : 'I couldn’t finish ' + misses.length + ' of today’s drafts.')
|
|
136
141
|
: (blocked.length === 1 ? clip(blocked[0].name, 80) + ' is stuck.' : blocked.length + ' pieces of work are stuck.'));
|
|
137
|
-
sub = misses.length ? 'That one’s on me. I can run it again.' : 'It’s your call,
|
|
142
|
+
sub = misses.length ? 'That one’s on me. I can run it again.' : 'It’s your call, and it’s in Your move.';
|
|
138
143
|
} else if (!calls.length && done > 0 && o.delightOk !== false) {
|
|
139
144
|
// delighted: earned, at most once a week (YdE2AllClear)
|
|
140
145
|
mood = 'delighted'; line = 'That was everything that needed you today.';
|
|
@@ -156,10 +161,11 @@ export function dayModel(M, D, R, now = Date.now(), o = {}) {
|
|
|
156
161
|
const fail = syncFailOf(o.sources, now);
|
|
157
162
|
let failLead = false;
|
|
158
163
|
if (fail && mood !== 'concerned') { const w = failWords(fail); mood = 'concerned'; line = w.line; sub = w.sub; failLead = true; }
|
|
159
|
-
// no prep run today: say so, and offer it (Prepare my day); never claim work that did not happen
|
|
160
|
-
if (!run && h < 17) offer = { kind: 'prep', text: 'I haven’t prepared today yet.', sub: 'Shall I prep your meetings and draft what I can?' };
|
|
164
|
+
// 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
|
+
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?' };
|
|
161
166
|
return {
|
|
162
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
|
+
rank: o.rank ? rankMove({ ...o.rank, calls, now }) : null,
|
|
163
169
|
news: newsOf(o.news), digests: digestsOf(D), tomorrow: tomorrowOf(D, now),
|
|
164
170
|
next: next ? { meeting: next, now: next.state === 'now', time: hm(next.start), title: next.title || 'A meeting' } : null,
|
|
165
171
|
week: { sub: un || 'Weekly review' },
|
|
@@ -179,7 +185,8 @@ export function openHtml(Y, o = {}) {
|
|
|
179
185
|
if (rain) o = { ...o, scene: true };
|
|
180
186
|
// 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)
|
|
181
187
|
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);
|
|
182
|
-
|
|
188
|
+
// on Good morning (o.gm) the chips are one quiet line, "Done for you: …" (G1)
|
|
189
|
+
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>` : '';
|
|
183
190
|
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('');
|
|
184
191
|
// the failed source under Claude's own miss: one card, no second Adler; its icon reads the source again (Settings › Connectors' ↻)
|
|
185
192
|
const sync = Y.fail ? (Y.failLead ? `<div class="yd-sync">${alt('sync', 'Read ' + Y.fail.name + ' again', Y.fail.id, AGAIN)}</div>`
|
|
@@ -195,6 +202,23 @@ function callHtml(r) {
|
|
|
195
202
|
const why = r.pick ? 'I’d ' + r.pick.charAt(0).toLowerCase() + r.pick.slice(1).replace(/\.?$/, '.') + ' It’s your call.' : r.when ? 'Goes out ' + r.when + '.' : r.sub || '';
|
|
196
203
|
return `<div class="yd-card yd-call${r.state === 'blocked' ? ' am' : ''}" data-id="${esc(r.id)}"><div class="yd-mt"><b>${esc(r.name)}</b>${why ? `<em>${esc(why)}</em>` : ''}</div>${alt('sel', 'Open: ' + r.name, '', CHEV)}</div>`;
|
|
197
204
|
}
|
|
205
|
+
/* one row of the ranked Your move (W3, WgW3Move): ✦, the sentence and what it rests on, its age, and its presses, icon only. The text opens what
|
|
206
|
+
* the row is about (the initiative's ✦, the program, Intelligence); ✓ does the one thing it says; the second icon picks, decides or drops. */
|
|
207
|
+
const ALT_ICON = { verdict: PEN, meeting: PEN, grill: XX, other: CHEV, kr: CHEV };
|
|
208
|
+
function moveRowHtml(r) {
|
|
209
|
+
if (r.kind === 'call') return callHtml(r.call);
|
|
210
|
+
const late = r.rank <= 2 && r.days != null && r.days > 0;
|
|
211
|
+
const yes = r.yes ? `<button type="button" class="yd-ok sm" data-yd="mv-yes" data-key="${esc(r.key)}" aria-label="${esc(r.yes + ': ' + r.title)}" title="${esc(r.yes)}">${svg(CHECK, 18, 2.6)}</button>` : '';
|
|
212
|
+
const alt = r.alt ? `<button type="button" class="yd-alt sm" data-yd="mv-alt" data-key="${esc(r.key)}" aria-label="${esc(r.alt + ': ' + r.title)}" title="${esc(r.alt)}">${svg(ALT_ICON[r.kind] || CHEV, 16, 2)}</button>` : '';
|
|
213
|
+
return `<div class="yd-mv" data-key="${esc(r.key)}"><span class="yd-mvs" aria-hidden="true">✦</span><button type="button" class="yd-mvt" data-yd="mv-open" data-key="${esc(r.key)}"><b>${esc(r.title)}</b>${r.sub ? `<em>${esc(r.sub)}</em>` : ''}</button>`
|
|
214
|
+
+ `${r.age ? `<span class="yd-age${late ? ' late' : ''}">${esc(r.age)}</span>` : ''}<span class="yd-mva">${yes}${alt}</span></div>`;
|
|
215
|
+
}
|
|
216
|
+
/** Your move's fold from the ranked list: the rows, then "Older than two weeks · N" folded (never gone). null when nothing is there. */
|
|
217
|
+
export function moveSection(R) {
|
|
218
|
+
if (!R || (!R.rows.length && !R.older.length)) return null;
|
|
219
|
+
const older = R.older.length ? `<details class="yd-older"><summary>Older than two weeks · ${R.older.length}</summary>${R.older.map(moveRowHtml).join('')}</details>` : '';
|
|
220
|
+
return { id: 'move', title: 'Your move', count: R.rows.length + (R.older.length ? ' · ' + R.older.length + ' older' : ''), open: true, html: `<div class="yd-mvl">${R.rows.map(moveRowHtml).join('')}${older}</div>` };
|
|
221
|
+
}
|
|
198
222
|
/* one row that can wait: the list's own row, so a press opens what the list row opens; no "late" words (here and now) */
|
|
199
223
|
function waitRow(r) {
|
|
200
224
|
const mark = r.check ? '<span class="hv-ck" role="img" aria-label="Your task"></span>' : '<span class="hv-d" role="img" aria-label="Waits on you"></span>';
|
|
@@ -221,6 +245,7 @@ export function nightHtml(Y) {
|
|
|
221
245
|
|
|
222
246
|
/* one New for you item (FmA2Day): the blue dot, who, the words and where it came from; a press opens it in the Viewer (cozy-news.js) */
|
|
223
247
|
const MARK2 = 'M2 12.5l4.5 4.5L15 8M9 15l2 2L22 7';
|
|
248
|
+
const listOf = (a) => (a.length < 2 ? a.join('') : a.slice(0, -1).join(', ') + ' and ' + a[a.length - 1]);
|
|
224
249
|
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';
|
|
225
250
|
export const NEWS_SHOWN = 3;
|
|
226
251
|
function newsRow(it, now) {
|
|
@@ -241,11 +266,15 @@ function newsRow(it, now) {
|
|
|
241
266
|
export function newsSection(items, now = Date.now(), digests = 0) {
|
|
242
267
|
items = items || [];
|
|
243
268
|
if (!items.length && !(digests > 0)) return null;
|
|
244
|
-
|
|
269
|
+
// the tasks given to the PM are one row (W3: "32 tasks given to you"), which opens the whole list in the Viewer; one task stays itself
|
|
270
|
+
const tasks = items.filter((x) => x.kind === 'task'), froms = [...new Set(tasks.map((x) => x.from).filter(Boolean))], rest = tasks.length > 1 ? items.filter((x) => x.kind !== 'task') : items;
|
|
271
|
+
const trow = tasks.length > 1 ? `<button type="button" class="yd-nw" data-yd="news-all"><i class="yd-nd" aria-hidden="true"></i><span class="yd-av sp" aria-hidden="true">${tasks.length}</span><span class="yd-mt"><b>${esc(tasks.length + ' tasks given to you')}</b><em>${esc(froms.length ? 'From ' + listOf(froms.slice(0, 2)) + (froms.length > 2 ? ' and ' + (froms.length - 2) + ' more' : '') : 'Open the list')}</em></span></button>` : '';
|
|
272
|
+
const shown = Math.max(0, NEWS_SHOWN - (trow ? 1 : 0));
|
|
273
|
+
const more = rest.length - shown;
|
|
245
274
|
const dig = digests > 0 ? `<button type="button" class="yd-more" data-yd="news-all">${digests === 1 ? 'Today’s digest' : 'Today’s ' + digests + ' digests'}</button>` : '';
|
|
246
275
|
return { id: 'news', title: 'New for you', count: items.length + (digests > 0 ? digests : 0), open: true,
|
|
247
276
|
tail: items.length ? `<button type="button" class="yd-all" data-yd="seen" aria-label="Mark all ${items.length} seen" title="Mark all seen">${svg(MARK2, 20, 1.9)}</button>` : '',
|
|
248
|
-
html: `<div class="yd-news">${
|
|
277
|
+
html: `<div class="yd-news">${trow}${rest.slice(0, shown).map((x) => newsRow(x, now)).join('')}${more > 0 ? `<button type="button" class="yd-more" data-yd="news-all">${more} more</button>` : ''}${dig}</div>` };
|
|
249
278
|
}
|
|
250
279
|
/** The morning brief's digests of today (GET /api/briefing-premium model.today), which New for you's list shows. */
|
|
251
280
|
export function digestsOf(D) {
|
|
@@ -263,8 +292,11 @@ export function dayParts(Y, o = {}) {
|
|
|
263
292
|
const sections = [], now = o.now || Date.now();
|
|
264
293
|
// the tea figure only after a real day (something done, drafted or waiting): nothing at all stays silent (zero is silence)
|
|
265
294
|
const real = Y.done > 0 || Y.chips.length || Y.drafts.length || Y.wait.length;
|
|
266
|
-
const
|
|
267
|
-
|
|
295
|
+
const ranked = Y.rank && (Y.rank.rows.length || Y.rank.older.length);
|
|
296
|
+
const big = Y.calls.length || ranked || Y.mood === 'concerned' ? null : Y.mood === 'delighted' ? 'win' : real ? 'tea' : null;
|
|
297
|
+
const mv = Y.rank ? moveSection(Y.rank) : null;
|
|
298
|
+
if (mv) sections.push(mv);
|
|
299
|
+
else if (Y.calls.length) sections.push({ id: 'move', title: 'Your move', count: Y.calls.length, open: true, html: Y.calls.map(callHtml).join('') });
|
|
268
300
|
else if (big) sections.push({ id: 'move', title: 'Your move', open: true, html: bigHtml(big) });
|
|
269
301
|
{ const nw = newsSection(Y.news || [], o.now, Y.digests || 0); if (nw) sections.push(nw); }
|
|
270
302
|
if (Y.drafts.length) sections.push({ id: 'drafts', title: 'Drafted for you', count: Y.drafts.length, html: `<div class="hv-card">${Y.drafts.map((d, i) => draftRow(d, i)).join('')}</div>` });
|
|
@@ -278,6 +310,26 @@ export function dayParts(Y, o = {}) {
|
|
|
278
310
|
return { open: openHtml(Y, { big: !!big, scene }), sections, night };
|
|
279
311
|
}
|
|
280
312
|
|
|
313
|
+
/** Good morning (nyt-ui GM2, pm/good-morning.js): the page replaces the top of Your day. G: gmModel's answer. The opening card becomes its
|
|
314
|
+
* lead, Your move its top 3, New for you its Since yesterday; what stays folds under it: the rest of Your move ("N more in Your move"),
|
|
315
|
+
* Drafted for you, Can wait, Now (a phone's moment card) and This week. Nothing is dropped. → { gm, sections, night } */
|
|
316
|
+
export function gmParts(Y, G, o = {}) {
|
|
317
|
+
const now = o.now || Date.now(), sections = [];
|
|
318
|
+
const rest = Y.rank ? Y.rank.rows.filter((r) => !G.move.some((m) => m.key === r.key) && !(G.call && r.kind === 'call' && r.call && r.call.id === G.call.id)) : [];
|
|
319
|
+
if (G.state !== 'evening' && (rest.length || (Y.rank && Y.rank.older.length))) {
|
|
320
|
+
const mv = moveSection({ rows: rest, older: Y.rank.older });
|
|
321
|
+
if (mv) sections.push({ ...mv, id: 'more', title: 'More in Your move', open: false });
|
|
322
|
+
}
|
|
323
|
+
if (Y.drafts.length) sections.push({ id: 'drafts', title: 'Drafted for you', count: Y.drafts.length, html: `<div class="hv-card">${Y.drafts.map((d, i) => draftRow(d, i)).join('')}</div>` });
|
|
324
|
+
if (Y.wait.length) sections.push({ id: 'wait', title: 'Can wait', count: Y.wait.length, html: `<div class="hv-card hd-move">${Y.wait.map(waitRow).join('')}</div>` });
|
|
325
|
+
if (o.moment) sections.push({ id: 'now', title: 'Now', open: true, html: '<div class="hd-now"></div>' });
|
|
326
|
+
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
|
+
// one Adler per screen: the morning scene on the day's first open before noon, else his face on the lead
|
|
328
|
+
const scene = !!o.first && Y.mood !== 'concerned' && G.state === 'morning' && new Date(now).getHours() < 12;
|
|
329
|
+
const night = Y.mood !== 'concerned' && asleep(now) ? nightHtml(Y) : '';
|
|
330
|
+
return { gm: o.html(G, openHtml, { scene }), sections, night };
|
|
331
|
+
}
|
|
332
|
+
|
|
281
333
|
/** Draws Your day into the Viewer's body: the opening card, then the folds; moment(el) puts the phone's moment card in Now. */
|
|
282
334
|
export { firstOpenToday };
|
|
283
335
|
export function drawDay(body, P, VW, moment) {
|
|
@@ -286,6 +338,6 @@ export function drawDay(body, P, VW, moment) {
|
|
|
286
338
|
const b = body.querySelector('[data-yd="awake"]'); if (b) b.addEventListener('click', (e) => { e.stopPropagation(); drawDay(body, { ...P, night: '' }, VW, moment); });
|
|
287
339
|
return;
|
|
288
340
|
}
|
|
289
|
-
body.innerHTML = `<div class="yd">${P.open}${VW.czSections(P.sections, { key: 'day' })}</div>`;
|
|
341
|
+
body.innerHTML = P.gm ? `<div class="yd gm-wrap">${P.gm}${VW.czSections(P.sections, { key: 'day' })}</div>` : `<div class="yd">${P.open}${VW.czSections(P.sections, { key: 'day' })}</div>`;
|
|
290
342
|
const now = body.querySelector('.hd-now'); if (now && moment) moment(now);
|
|
291
343
|
}
|
|
@@ -109,7 +109,7 @@ const PILL = { draft: ['Draft', ''], ok: ['Up to date', 'ok'], live: ['Live', 'o
|
|
|
109
109
|
export function docPhases(M) {
|
|
110
110
|
const D = (M || {}).docs || {};
|
|
111
111
|
if (Array.isArray(D.phases) && D.phases.length) return { phases: D.phases, cur: D.cur || D.phases[0] };
|
|
112
|
-
return { phases: PHASES, cur: phaseOf(M && M.acts && M.acts.stage) };
|
|
112
|
+
return { phases: PHASES, cur: phaseOf((M && M.oneStage && M.oneStage.step) || (M && M.acts && M.acts.stage)) }; // W1: the one stage's step
|
|
113
113
|
}
|
|
114
114
|
export function docRows(M) {
|
|
115
115
|
const D = (M || {}).docs || {}, { phases } = docPhases(M);
|
|
@@ -99,5 +99,12 @@ export function stageCardHtml(h, o = {}) {
|
|
|
99
99
|
return `<section class="hm-card hm-pcard hm-sc" aria-label="${esc(o.title || '')}">`
|
|
100
100
|
+ `<div class="hm-box hm-scb ${esc(turn)}"><div class="hm-sch"><b>${esc(h.cur || 'No stage')}</b>${R.done.length + R.open.length ? `<span class="of">${esc(h.doneOf)}</span>` : ''}`
|
|
101
101
|
+ (o.wf || '') + (clock ? `<span class="hm-sct${h.clock.over ? ' over' : ''}">${CLOCK}${esc(clock)}</span>` : '') + '</div>'
|
|
102
|
+
+ (h.source ? `<p class="hm-scsrc">${esc(h.source)}</p>` : '') + gateHtml(h.gate)
|
|
102
103
|
+ bars + none + list + '</div></section>';
|
|
103
104
|
}
|
|
105
|
+
/** W1: the gate line under the stage ("Gate to Build · PRD ✓ · rubric ✓ · contract not approved"): the spec ladder, which no longer sets the
|
|
106
|
+
* stage. '' when the engine gave no gate check. */
|
|
107
|
+
export function gateHtml(g) {
|
|
108
|
+
if (!g || !Array.isArray(g.parts) || !g.parts.length) return '';
|
|
109
|
+
return `<p class="hm-gate"><span class="lbl">${esc(g.label)}</span>${g.parts.map((p) => `<span class="g${p.ok ? '' : ' no'}"${p.ok ? ` aria-label="${esc(p.text + ' done')}"` : ''}${p.note ? ` title="${esc(p.note)}"` : ''}>${esc(p.text)}${p.ok ? '<svg class="cz-ck" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12l4.5 4.5L19 7"/></svg>' : ''}</span>`).join('')}</p>`;
|
|
110
|
+
}
|
|
@@ -464,7 +464,7 @@ export function allHtml(v, o, isOpen) {
|
|
|
464
464
|
else if (D <= 1100) { if (m % 3 === 0) ticks.push({ n, t: 'Q' + (m / 3 + 1) + yr, b: m === 0 }); }
|
|
465
465
|
else if (m === 0) ticks.push({ n, t: String(d.getUTCFullYear()), b: true });
|
|
466
466
|
}
|
|
467
|
-
const stageTone = (sec) => (sec.status === 'done' || sec.kind === 'past' ? 'dn' : sec.status === 'now' ? 'now' : '');
|
|
467
|
+
const stageTone = (sec) => ((sec.status === 'done' || sec.kind === 'past') && !(sec.openN > 0) ? 'dn' : sec.status === 'now' ? 'now' : ''); // W1: done only when every task is
|
|
468
468
|
const msMark = (x) => { const b = dn(x.end); if (b == null) return '';
|
|
469
469
|
return `<i class="iva-md${x.done ? ' dn' : x.late ? ' lt' : ''}" data-tlx="${pc(b)}" role="img" aria-label="${esc('Milestone ' + x.title + ', ' + md(b))}" title="${esc(x.title + ' · ' + md(b))}"></i>`; };
|
|
470
470
|
const name = (sec) => sec.label || sec.name || 'Not in a stage';
|
|
@@ -475,7 +475,7 @@ export function allHtml(v, o, isOpen) {
|
|
|
475
475
|
const has = rs.length || dn_.length || jr.length || tk.length;
|
|
476
476
|
const mk = o.mark(sec);
|
|
477
477
|
let mark = '';
|
|
478
|
-
if (sp && sec.ms) mark = `<i class="iva-md st${sec.status === 'done' ? ' dn' : ''}" data-tlx="${pc(sp[1])}"></i>`;
|
|
478
|
+
if (sp && sec.ms) mark = `<i class="iva-md st${sec.status === 'done' && !(sec.openN > 0) ? ' dn' : ''}" data-tlx="${pc(sp[1])}"></i>`;
|
|
479
479
|
else if (sp) mark = `<i class="iva-b ${stageTone(sec)}" data-tlx="${pc(sp[0])}" data-tlw="${pw(sp[0], sp[1])}"></i>` + rowsOf(sec).filter((x) => x.ms && !x.wont).map(msMark).join('');
|
|
480
480
|
const when = sp ? (sec.ms ? md(sp[1]) : rng(sp[0], sp[1])) : '';
|
|
481
481
|
const say = name(sec) + (sec.count ? ', ' + sec.count : '') + (sp ? ', ' + rng(sp[0], sp[1]) : ', no dates yet') + (has ? '. ' + (op ? 'Hide' : 'Show') + ' its tasks' : '');
|
|
@@ -36,6 +36,8 @@
|
|
|
36
36
|
* 19 a big section shows the 3 most urgent first, then folds the rest by the next WBS level, each with its count; done folds last. */
|
|
37
37
|
import { ticketGroups } from './tk-model.js'; // the drafted Jira tickets by epic (kit 2, T1): By epic's groups
|
|
38
38
|
|
|
39
|
+
import { stageSource } from './stage-chip.js'; // W1: what set the one stage, and when it was read
|
|
40
|
+
const endWord = (r) => (r && r.oneStage && ['shipped', 'on-hold', 'cancelled'].includes(r.oneStage.stage) ? r.oneStage.label : '');
|
|
39
41
|
import { lineOf } from './stage-line.js'; // the stage card's one line (batch 11 item 21)
|
|
40
42
|
|
|
41
43
|
const MON = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
@@ -244,7 +246,9 @@ export function iniView(r, ctx = {}) {
|
|
|
244
246
|
};
|
|
245
247
|
const sTotal = jira ? tk.length : curRows.length, sDone = jira ? tk.filter((k) => k.status === 'Done').length : curRows.filter((x) => x.closed).length;
|
|
246
248
|
const stage = {
|
|
247
|
-
n: cur >= 0 ? cur + 1 : null, of: list.length, name: st ? st.name : (t.label || r.stage || 'No stage on record'), ms: !!(st && st.ms), key: st ? st.key : '',
|
|
249
|
+
n: cur >= 0 ? cur + 1 : null, of: list.length, name: (endWord(r) || (st ? st.name : (t.label || r.stage || 'No stage on record'))), ms: !!(st && st.ms), key: st ? st.key : '',
|
|
250
|
+
// W1: what set the stage and when it was read, and the gate line (the spec ladder) the page model sends
|
|
251
|
+
source: stageSource(r), gate: ctx.gate || null,
|
|
248
252
|
doneOf, nDone: sDone, nTotal: sTotal, nLate: curRows.filter((x) => !x.closed && x.late).length, gates, day: days != null ? 'day ' + days + (usual != null ? ' of ~' + usual : '') : '', dayOver: days != null && usual != null && days > usual,
|
|
249
253
|
clock: days != null && !line.chip ? (usual != null ? days + ' of ~' + usual + ' days' : plural(days, 'day', 'days')) : '', line, ready,
|
|
250
254
|
ladder: list.map((s) => ({ name: s.name, ms: s.ms, cls: s.status })), dates, next: nx,
|
|
@@ -265,8 +269,8 @@ export function iniView(r, ctx = {}) {
|
|
|
265
269
|
else if (s.status === 'done' || s.status === 'skip') sub = s.status === 'skip' ? 'Skipped' + (s.why ? ': ' + s.why : '') : span(ran[i].from, ran[i].to, today);
|
|
266
270
|
else { const p = planOf(rs); if (p) { sub = 'Plan ' + dateY(p, today); if (dayOf(p) < today && op.length) { sub += ', missed'; subWarn = true; } } }
|
|
267
271
|
const h = headOf(rs); if (h) sub = [sub, h].filter(Boolean).join(' · ');
|
|
268
|
-
return { kind: 'stage', key: s.key, n: i + 1, name: s.name, ms: s.ms, status: s.status, sub, subWarn, count: countOf(op.length, lt.length, dn), warn: lt.length > 0,
|
|
269
|
-
open: s.status === 'now' || lt.length > 0, ...bodyOf(rs, archOf([s.key])), jira: null, why, ...(s.status === 'skip' ? { skipWhy: s.why || '' } : {}) };
|
|
272
|
+
return { kind: 'stage', key: s.key, n: i + 1, name: s.name, ms: s.ms, status: s.status, sub, subWarn, count: countOf(op.length, lt.length, dn), warn: lt.length > 0, openN: op.length,
|
|
273
|
+
open: s.status === 'now' || lt.length > 0 || (s.status === 'done' && op.length > 0), ...bodyOf(rs, archOf([s.key])), jira: null, why, ...(s.status === 'skip' ? { skipWhy: s.why || '' } : {}) };
|
|
270
274
|
};
|
|
271
275
|
const anyTasks = rows.length > 0 || jiraIn;
|
|
272
276
|
// one row per stage, in workflow order (batch 11 item 5, Wawan: "one row/group per stage. Do not combine"): past and later stages are no
|
|
@@ -281,6 +285,7 @@ export function iniView(r, ctx = {}) {
|
|
|
281
285
|
x.count = countOf(opn.length + x.top.length + x.groups.reduce((n, g) => n + g.n, 0), x.top.filter((y) => y.late).length, 0);
|
|
282
286
|
x.sub = x.sub + (x.sub ? ' · ' : '') + 'Jira tickets, by epic';
|
|
283
287
|
if (opn.length) x.open = true; // passed but still open in Jira (Deliver): the tickets stay in view, not folded under a done stage
|
|
288
|
+
x.openN = (x.openN || 0) + opn.length;
|
|
284
289
|
}
|
|
285
290
|
sections.push(x);
|
|
286
291
|
});
|
|
@@ -350,6 +355,8 @@ export function linkedOf(r) {
|
|
|
350
355
|
* group it again; each group is a section of kind 'group' (or 'epic'), so the three views draw it with no code of their own. Links,
|
|
351
356
|
* the ◇ and the trash belong to the task, so they come with it. Jira's own tickets (Build's section) have no owner or epic here: in any
|
|
352
357
|
* grouping but Stage they sit in one "In Jira" section. */
|
|
358
|
+
/** W1: a task group shows ✓ only when every task in it is done: a passed stage with open tasks (Plan 1 open, Build 21 open) reads open. */
|
|
359
|
+
export const ticks = (sec) => !!sec && (sec.status === 'done' || sec.kind === 'past') && !(sec.openN > 0);
|
|
353
360
|
export const GROUP_BY = { stage: 'Stage', epic: 'Epic', mine: 'Your groups', owner: 'Owner', status: 'Status' };
|
|
354
361
|
/** An old layout's 'stage' (from before tasks were the WBS) means what Stage means again; 'none' is no longer offered and reads as Stage. */
|
|
355
362
|
export function groupBy(L, T) {
|
|
@@ -1301,3 +1301,12 @@ body.iv-dragging { user-select: none; -webkit-user-select: none; cursor: grabbin
|
|
|
1301
1301
|
.tkc.ivq .ive-m .ck { color: var(--cz-ink2); }
|
|
1302
1302
|
/* the open row has its own status chip: the box's caret stays out */
|
|
1303
1303
|
.iv-tr.iv-ed .iv-car { display: none !important; }
|
|
1304
|
+
/* the grill findings on the header (workspace grill W3, WgW3Move): the two best, each with its angle and day, and one ✦ for all of them */
|
|
1305
|
+
.iv-gf { position: relative; display: flex; flex-direction: column; gap: 6px; background: var(--cz-card); border: 1px solid var(--cz-hair); border-radius: 16px; padding: 10px 52px 10px 12px; margin-top: 8px; }
|
|
1306
|
+
.iv-gfr { margin: 0; display: flex; gap: 8px; align-items: flex-start; font-size: var(--fs-row); line-height: var(--lh-row); color: var(--cz-ink); }
|
|
1307
|
+
.iv-gfr b { font-weight: var(--fw-strong); }
|
|
1308
|
+
.iv-gfd { flex: none; width: 8px; height: 8px; border-radius: 4px; margin-top: 6px; background: var(--cz-risk); }
|
|
1309
|
+
.iv-gfd.late { background: var(--cz-late); }
|
|
1310
|
+
.iv-gfm { color: var(--cz-ink3); font-size: var(--fs-label); }
|
|
1311
|
+
.iv-gfo { position: absolute; top: 6px; right: 6px; width: 44px; height: 44px; border-radius: 22px; border: 0; background: var(--cz-bluebg); color: var(--cz-blue); display: inline-flex; align-items: center; justify-content: center; cursor: pointer; padding: 0; }
|
|
1312
|
+
.iv-gfo:focus-visible { outline: 2px solid var(--cz-blue); outline-offset: 2px; }
|
|
@@ -44,7 +44,9 @@ import * as MI from './mission.js'; // the initiative as a mission, the stage
|
|
|
44
44
|
import * as AD from './cozy-adler.js'; // Adler on the card: his face for the ask, his full body for a real win
|
|
45
45
|
import * as MD from './metric-drafts.js'; // the success measure Claude drafts from the PRD (batch 15 item 1, canvas Iq9Initiative)
|
|
46
46
|
import * as AB from './ini-about.js'; // About: the description and the success measure, folded to one line (initiative-mission-control R6)
|
|
47
|
+
import { headerFlags } from './grill-words.js'; // the grill findings on the header (workspace grill W3)
|
|
47
48
|
import * as ED from './ini-edit.js'; // a task changed where it sits in the List: open in place, ⋯, + per stage, drag (batch 18 item 16)
|
|
49
|
+
import { ticks } from './ini-view-model.js'; // W1: a task group ticks only when every task in it is done
|
|
48
50
|
|
|
49
51
|
const CSS = new URL('./ini-view.css', import.meta.url).href;
|
|
50
52
|
if (typeof document !== 'undefined' && document.head && !document.querySelector(`link[href="${CSS}"]`)) {
|
|
@@ -133,6 +135,12 @@ const pipsOf = (st, cls) => {
|
|
|
133
135
|
const d = Math.min(n, st.nDone || 0), l = Math.min(n - d, st.nLate || 0);
|
|
134
136
|
return `<span class="iv-pips${cls ? ' ' + cls : ''}" aria-hidden="true">${Array.from({ length: n }, (_, i) => `<i class="${i < d ? 'd' : i < d + l ? 'l' : 'o'}"></i>`).join('')}</span>`;
|
|
135
137
|
};
|
|
138
|
+
/** W1: under the stage, what set it ("Jira IDPF-2815 · Design Discovery · read 07:16") and the gate line ("Gate to Build · PRD ✓ · contract not
|
|
139
|
+
* approved"): the spec ladder no longer sets the stage. '' when neither is known. */
|
|
140
|
+
function srcGateHtml(st) {
|
|
141
|
+
const g = st.gate && Array.isArray(st.gate.parts) && st.gate.parts.length ? `<p class="hm-gate iv-gate"><span class="lbl">${esc(st.gate.label)}</span>${st.gate.parts.map((x) => `<span class="g${x.ok ? '' : ' no'}"${x.ok ? ` aria-label="${esc(x.text + ' done')}"` : ''}${x.note ? ` title="${esc(x.note)}"` : ''}>${esc(x.text)}${x.ok ? '<svg class="cz-ck" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12l4.5 4.5L19 7"/></svg>' : ''}</span>`).join('')}</p>` : '';
|
|
142
|
+
return (st.source ? `<p class="hm-scsrc iv-src">${esc(st.source)}</p>` : '') + g;
|
|
143
|
+
}
|
|
136
144
|
/** The telemetry card, and with it the banner it folds into (R2, R3). */
|
|
137
145
|
function telHtml(v, o, pri, mc) {
|
|
138
146
|
const st = v.stage, L = st.line, id = v.id || v.key || '', M = MI.missionOf(L), D = st.dates;
|
|
@@ -145,11 +153,11 @@ function telHtml(v, o, pri, mc) {
|
|
|
145
153
|
// the banner (hidden): the stage, the dates, the days left, the count, the measure, the next move
|
|
146
154
|
const left = D && D.left ? `<span class="iv-chp2${D.over ? ' late' : ''}">${esc(D.left)}</span>` : L && L.chip ? `<span class="iv-chp2">${esc(L.chip.text)}</span>` : '';
|
|
147
155
|
const count = st.nTotal ? `<span class="iv-chp2${st.nLate ? ' late' : ''}">${pipsOf(st, 'sm')}${esc(st.doneOf || st.nDone + ' of ' + st.nTotal + ' done')}</span>` : '';
|
|
148
|
-
const bn = mc ? '' : `<div class="iv-bn"><div class="iv-bt"><span class="iv-shn">${tileOf(st)}<b>${esc(st.name)}</b></span>${dt ? `<span class="s">${esc(dt)}</span>` : ''}</div><div class="iv-chips">${left}${count}${AB.chipHtml(v, o)}</div>${st.before ? `<p class="iv-bf s">${esc(st.before)}</p>` : ''}${nextHtml(st, o, pri)}</div>`;
|
|
156
|
+
const bn = mc ? '' : `<div class="iv-bn"><div class="iv-bt"><span class="iv-shn">${tileOf(st)}<b>${esc(st.name)}</b></span>${dt ? `<span class="s">${esc(dt)}</span>` : ''}</div><div class="iv-chips">${left}${count}${AB.chipHtml(v, o)}</div>${srcGateHtml(st)}${st.before ? `<p class="iv-bf s">${esc(st.before)}</p>` : ''}${nextHtml(st, o, pri)}</div>`;
|
|
149
157
|
const tg = `<button type="button" class="iv-mctg" data-iv-mc="mc" aria-expanded="${!!mc}" aria-label="${mc ? 'Hide the mission' : 'Show the mission'}" title="${mc ? 'Hide' : 'Show'}">${SVG(CHEV, 16, 2)}</button>`;
|
|
150
158
|
return `<div class="iv-tel" data-iv-tel aria-label="${esc(st.aria || 'Stage · ' + st.name)}">`
|
|
151
159
|
+ (mc ? `<div class="iv-tj" data-iv-tj="${esc(id)}">${svg}</div>`
|
|
152
|
-
+ `<div class="iv-tel-ct"><div class="iv-tel-t"><span class="iv-shn">${tileOf(st)}${nameOf(st, o)}</span>${holdChip(L, o)}${set}${dates}</div>${readoutsHtml(st)}</div>` : bn)
|
|
160
|
+
+ `<div class="iv-tel-ct"><div class="iv-tel-t"><span class="iv-shn">${tileOf(st)}${nameOf(st, o)}</span>${holdChip(L, o)}${set}${dates}</div>${srcGateHtml(st)}${readoutsHtml(st)}</div>` : bn)
|
|
153
161
|
+ `${tg}<span class="iv-mi-tip" role="tooltip" hidden></span></div>`;
|
|
154
162
|
}
|
|
155
163
|
/** The way to finish this stage (R5): the count with its marks, the next move, the open milestone tasks, and the people. */
|
|
@@ -171,6 +179,12 @@ function gatesHtml(v, o, pri, open) {
|
|
|
171
179
|
return `<section class="iv-gx"${ph ? ' data-ph' : ''} aria-label="${esc(title)}"><button type="button" class="iv-gx-t" data-iv-mc="gx" aria-expanded="${!!open}"><span class="chev" aria-hidden="true">${SVG(CHEV, 16, 2)}</span><span class="tx dk"><b>${title}</b>${sub ? `<span class="s">${esc(sub)}</span>` : ''}</span>${ph}</button>`
|
|
172
180
|
+ `${st.nTotal ? `<span class="iv-gx-c">${pipsOf(st)}<small>${esc(st.doneOf || st.nDone + ' of ' + st.nTotal + ' done')}</small></span>` : ''}<div class="iv-gx-a">${nextHtml(st, o, pri)}</div>${open ? rows : ''}<div class="iv-gx-ft">${metaHtml(s, o)}</div></section>`;
|
|
173
181
|
}
|
|
182
|
+
/* the grill findings on the header (workspace grill W3, WgW3Move): pm/grill-words.js headerFlags; not behind a pill, zero is silence */
|
|
183
|
+
function flagsHtml(v) {
|
|
184
|
+
const F = headerFlags(v && v.props); if (!F) return '';
|
|
185
|
+
return `<div class="iv-gf" role="group" aria-label="What Claude found when it grilled it">${F.rows.map((r) => `<p class="iv-gfr"><i class="iv-gfd${r.hot ? ' late' : ''}" aria-hidden="true"></i><span><b>${esc(r.title)}</b> <span class="iv-gfm">${esc([r.label, r.day].filter(Boolean).join(' · '))}</span></span></p>`).join('')}`
|
|
186
|
+
+ `<button type="button" class="iv-gfo" data-iv-gf aria-label="${esc('Open the ' + plural(F.n, 'finding', 'findings'))}" title="Open the findings">${SVG('M12 3l1.8 4.7L18.5 9.5l-4.7 1.8L12 16l-1.8-4.7L5.5 9.5l4.7-1.8z', 16, 2)}</button></div>`;
|
|
187
|
+
}
|
|
174
188
|
function ctrlHtml(v, o = {}) {
|
|
175
189
|
const s = v.summary, st = v.stage, id = v.id || v.key || '';
|
|
176
190
|
const on = AD.prefs()['adler.amount'] !== 'off';
|
|
@@ -187,7 +201,7 @@ function ctrlHtml(v, o = {}) {
|
|
|
187
201
|
top = `<div class="iv-mc-top adl"><div class="iv-ask">${AD.adler('needs-you', 'm', { label: false })}<p class="iv-why">${esc(s.say || s.text || s.primary.label)}</p>${n}`
|
|
188
202
|
+ `<button type="button" class="iv-go" data-iv-primary="${esc(s.primary.act)}" aria-label="${esc(s.primary.label)}" title="${esc(s.primary.label)}">${SVG(P.go, 16, 2.2)}</button></div></div>`;
|
|
189
203
|
} else if (claude) top = `<div class="iv-mc-top adl"><div class="iv-ask cl">${AD.adler('thinking', 'm', { label: false })}<p class="iv-why">${esc(s.claude)}</p></div></div>`;
|
|
190
|
-
return `<section class="iv-sum iv-mc${S.mc ? '' : ' shut-mc'}${S.gx ? '' : ' shut-gx'}${S.ab ? '' : ' shut-ab'}${top ? ' has-top' : ''}" aria-label="Mission">${top}`
|
|
204
|
+
return `<section class="iv-sum iv-mc${S.mc ? '' : ' shut-mc'}${S.gx ? '' : ' shut-gx'}${S.ab ? '' : ' shut-ab'}${top ? ' has-top' : ''}" aria-label="Mission">${top}${flagsHtml(v)}`
|
|
191
205
|
+ `<div class="iv-mc-row">${telHtml(v, o, pri, S.mc)}${S.mc ? gatesHtml(v, o, pri, S.gx) : ''}</div>${S.mc ? AB.aboutHtml(v, o, S.ab) : ''}</section>`;
|
|
192
206
|
}
|
|
193
207
|
/** The date of an ISO day, as the model says dates ("Oct 30"). */
|
|
@@ -292,10 +306,9 @@ function bodyHtml(v, sec, o) {
|
|
|
292
306
|
return h;
|
|
293
307
|
}
|
|
294
308
|
function markOf(sec) {
|
|
295
|
-
if (sec
|
|
309
|
+
if (ticks(sec)) return `<span class="iv-no ok" aria-hidden="true">${SVG(P.check, 12, 3)}</span>`; // W1: only when every task is done
|
|
296
310
|
if (sec.kind === 'later' || sec.kind === 'none') return '<span class="iv-no dash" aria-hidden="true"></span>';
|
|
297
311
|
if (sec.status === 'skip') return `<span class="iv-no skip" aria-hidden="true">${SVG(P.skip, 12, 2)}</span>`;
|
|
298
|
-
if (sec.status === 'done') return `<span class="iv-no ok" aria-hidden="true">${SVG(P.check, 12, 3)}</span>`;
|
|
299
312
|
if (sec.kind === 'group' || sec.kind === 'epic') return ''; // a group of another grouping has no stage number (batch 11 item 20)
|
|
300
313
|
if (sec.ms) return '<span class="iv-no ms" aria-hidden="true"></span>';
|
|
301
314
|
return `<span class="iv-no${sec.status === 'now' ? ' now' : ''}" aria-hidden="true">${esc(sec.n)}</span>`;
|
|
@@ -470,6 +483,7 @@ export function wireView(body, h) {
|
|
|
470
483
|
if (d.sm && H.sm) { e.preventDefault(); H.sm(d.sm, b); return; }
|
|
471
484
|
if (d.smOpen && H.openIni) { e.preventDefault(); H.openIni(d.smOpen, b); return; }
|
|
472
485
|
if (d.ivPrimary && H.primary) { e.preventDefault(); H.primary(d.ivPrimary, b); return; }
|
|
486
|
+
if ('ivGf' in d) { e.preventDefault(); const v = VIEWED.get(id); if (v) PP.open(v, body.__iv || H); return; }
|
|
473
487
|
if ('ivAsks' in d) { e.preventDefault(); if (H.asks) H.asks(b); else if (H.primary) { const p = root.querySelector('[data-iv-primary]'); if (p) H.primary(p.dataset.ivPrimary, p); } return; }
|
|
474
488
|
if ('ivWf' in d && H.wf) { e.preventDefault(); H.wf(b); return; }
|
|
475
489
|
if ('ivPerson' in d && H.person) { e.preventDefault(); H.person(b); return; }
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/* pm/move-rank.js — Your move is one ranked list (workspace grill W3, frame WgW3Move on the main canvas, In development; brief
|
|
2
|
+
* /mnt/project-files/workspace-grill/brief-W1-W4.md). Pure: Node imports it (home-day.test.js).
|
|
3
|
+
*
|
|
4
|
+
* rankMove(o) → { rows, older, n } every open ✦ of the products Home reads, in order of consequence, each with its age
|
|
5
|
+
* ageWord(days) → 'today' · '1 day' · '86 days'
|
|
6
|
+
*
|
|
7
|
+
* THE ORDER (frame WgW3Move, "The order"):
|
|
8
|
+
* 1 verdict an outcome check past its date, with Claude's drafted Keep or Iterate (GET /api/your-move verdicts); ✓ records it, ✎ opens
|
|
9
|
+
* the full verdict on Intelligence. With nothing read: "I could not read enough to judge" and ✎ only.
|
|
10
|
+
* 2 grill a grill finding (lib/grill.js: red team, premortem, …) on an initiative that shipped (Deliver or later, or done) or is late
|
|
11
|
+
* 3 meeting a risk or date change a meeting named, grouped per program (else per product): Claude's milestone changes of the program
|
|
12
|
+
* (GET /api/programs review.open) and the date and status proposals whose source is a meeting. One ✓ confirms all; ✎ picks.
|
|
13
|
+
* 4 ahead a grill finding on work still ahead
|
|
14
|
+
* 5 kr the first reading for a key result with none (GET /api/your-move kr); the press opens Intelligence
|
|
15
|
+
* 6 other the rest Claude proposes on an initiative (descriptions, outcomes, names, found elsewhere, other dates, links): one row per
|
|
16
|
+
* initiative that opens its ✦; folded under Older once its newest proposal is more than 14 days old, never gone
|
|
17
|
+
* The calls only the PM can make that are not ✦ (a session that asks, a decision, blocked work) follow rank 5, as before.
|
|
18
|
+
*
|
|
19
|
+
* ONE FINDING, ONE ROW. Findings of one angle on one initiative are one row with their parts (Inline login's three red-team ✦), and two
|
|
20
|
+
* proposals that say the same words on one initiative count once. Each row names its age: the day Claude filed it (a verdict: the days since
|
|
21
|
+
* its check date; a late task's dates: the days since the date it missed).
|
|
22
|
+
*
|
|
23
|
+
* o: { products: [{ ws, name, board, rows }] (Board rows: r.props from lib/proposals.js viewOf, r.track, r.status),
|
|
24
|
+
* programs: GET /api/programs' programs, move: GET /api/your-move answers by ws ({ [ws]: { verdicts, kr } }), calls: homeList's call
|
|
25
|
+
* rows, now }
|
|
26
|
+
* A row: { key, rank, kind, title, sub, tag, at, age, older, ws, board, ini, program, props: [{ ws, board, ini, ids, sigs }], ms: { program,
|
|
27
|
+
* keys } | null, verdict: { ini, board, verdict, text } | null, yes: label | '', alt: label | '', call } */
|
|
28
|
+
import { findingOf } from './grill-words.js';
|
|
29
|
+
|
|
30
|
+
const DAY = 864e5;
|
|
31
|
+
export const OLDER_DAYS = 14;
|
|
32
|
+
const MON = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
33
|
+
const plural = (n, a, b) => n + ' ' + (n === 1 ? a : b);
|
|
34
|
+
const clip = (s, n) => { const t = String(s || '').replace(/\s+/g, ' ').trim(); return t.length > n ? t.slice(0, n - 1).replace(/\s+\S*$/, '') + '…' : t; };
|
|
35
|
+
const list = (a) => (a.length < 2 ? a.join('') : a.slice(0, -1).join(', ') + ' and ' + a[a.length - 1]);
|
|
36
|
+
const tsOf = (v) => { const t = Date.parse(v || ''); return Number.isFinite(t) ? t : null; };
|
|
37
|
+
const dayOf = (iso) => { const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(iso || '')); return m ? new Date(+m[1], +m[2] - 1, +m[3]).getTime() : null; };
|
|
38
|
+
/** "10 Aug", or "10 Aug 2027" outside this year. */
|
|
39
|
+
const dm = (iso, now) => { const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(iso || '')); return m ? +m[3] + ' ' + MON[+m[2] - 1] + (+m[1] !== new Date(now).getFullYear() ? ' ' + m[1] : '') : ''; };
|
|
40
|
+
const daysSince = (t, now) => (t == null ? null : Math.max(0, Math.floor((now - t) / DAY)));
|
|
41
|
+
export const ageWord = (d) => (d == null ? '' : d === 0 ? 'today' : plural(d, 'day', 'days'));
|
|
42
|
+
const norm = (s) => String(s || '').toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
|
|
43
|
+
const MEET = /\b(meeting|notes?|calendar|sync|standup|stand-up|review|steering|weekly)\b/i;
|
|
44
|
+
const OTHER_WORD = { description: 'a description', outcome: 'an expected outcome', found: 'what it found elsewhere', dates: 'new dates', status: 'work that looks done', names: 'tidy names', links: 'task links', grill: 'grill findings' };
|
|
45
|
+
|
|
46
|
+
/** Shipped (Deliver or later, or done) or late: a grill finding on it is rank 2. */
|
|
47
|
+
const shippedOrLate = (r) => !!r && (r.late || r.status === 'late' || r.status === 'done' || (r.acts && r.acts.life === 'done') || (r.track && Number.isFinite(r.track.k) && r.track.k >= 5));
|
|
48
|
+
const live = (r) => !!r && r.id && !(r.acts && (r.acts.archived || r.acts.life === 'cancelled'));
|
|
49
|
+
/** Every open proposal of a row, the grill findings folded behind the ✦'s first three included. */
|
|
50
|
+
function propsOf(r) {
|
|
51
|
+
const out = [];
|
|
52
|
+
for (const g of (r.props && r.props.groups) || []) {
|
|
53
|
+
for (const p of g.props || []) out.push(p);
|
|
54
|
+
for (const p of g.hidden || []) out.push(p);
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
/** When a proposal was filed; a computed date proposal: the earliest date its tasks missed. */
|
|
59
|
+
function atOf(p) {
|
|
60
|
+
const t = tsOf(p.at); if (t != null) return t;
|
|
61
|
+
if (p.group === 'dates') { const ds = (p.items || []).map((x) => dayOf(x.from && x.from.due)).filter((x) => x != null); return ds.length ? Math.min(...ds) : null; }
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const fromMeeting = (p) => (p.group === 'dates' || p.group === 'status') && (p.sources || []).some((s) => MEET.test(String((s && s.label) || '')));
|
|
65
|
+
const programOf = (programs, ws, ini) => (programs || []).find((p) => p && !p.archived && (p.members || []).some((m) => m.initiative === ini && (!m.ws || !ws || m.ws === ws))) || null;
|
|
66
|
+
|
|
67
|
+
export function rankMove(o = {}) {
|
|
68
|
+
const now = o.now || Date.now(), rows = [];
|
|
69
|
+
const add = (r) => { const d = daysSince(r.at, now); rows.push({ props: [], ms: null, verdict: null, yes: '', alt: '', ...r, age: r.age != null ? r.age : ageWord(d), days: d }); };
|
|
70
|
+
const meet = new Map(); // program id or 'p:' + ws → the meeting row being built
|
|
71
|
+
const meetRow = (key, title, extra) => { if (!meet.has(key)) meet.set(key, { key: 'mt:' + key, rank: 3, kind: 'meeting', title, parts: [], at: null, props: [], ms: null, ...extra }); return meet.get(key); };
|
|
72
|
+
for (const P of o.products || []) {
|
|
73
|
+
const ws = P.ws || '';
|
|
74
|
+
// 1 the verdicts past their check date (GET /api/your-move)
|
|
75
|
+
const M = (o.move && o.move[ws]) || null;
|
|
76
|
+
for (const v of (M && M.verdicts) || []) {
|
|
77
|
+
const d = v.draft;
|
|
78
|
+
add({ key: 'vd:' + ws + ':' + v.ini, rank: 1, kind: 'verdict', ws, board: v.board || P.board || '', ini: v.ini, at: dayOf(v.horizon), tag: 'Did it work?',
|
|
79
|
+
title: v.title + ': did it work?' + (d ? ' Verdict drafted: ' + d.verdict : ''),
|
|
80
|
+
sub: (d ? d.text + ' ' : 'I could not read enough to judge. ') + 'The check was due ' + dm(v.horizon, now) + '.',
|
|
81
|
+
verdict: d ? { ini: v.ini, board: v.board || P.board || '', verdict: d.verdict, text: d.text } : null, yes: d ? 'Record ' + d.verdict : '', alt: 'Decide on Intelligence' });
|
|
82
|
+
}
|
|
83
|
+
for (const r of (P.rows || []).filter(live)) {
|
|
84
|
+
const name = r.title || r.id, hot = shippedOrLate(r), prog = programOf(o.programs, ws, r.id);
|
|
85
|
+
const grill = new Map(), other = [], seen = new Set();
|
|
86
|
+
for (const p of propsOf(r)) {
|
|
87
|
+
const said = norm(p.group + ' ' + (p.title || '') + ' ' + (p.group === 'grill' ? '' : p.why || ''));
|
|
88
|
+
if (seen.has(said)) continue; // the same words twice on one initiative are one proposal
|
|
89
|
+
seen.add(said);
|
|
90
|
+
if (p.group === 'grill') { const f = findingOf(p); if (!grill.has(f.angle)) grill.set(f.angle, []); grill.get(f.angle).push({ p, f }); continue; }
|
|
91
|
+
if (fromMeeting(p)) {
|
|
92
|
+
const m = prog ? meetRow(prog.id, prog.title, { program: prog.id }) : meetRow('p:' + ws, P.name, { ws });
|
|
93
|
+
m.parts.push(name + ': ' + clip(p.title, 60));
|
|
94
|
+
let e = m.props.find((x) => x.ws === ws && x.ini === r.id); if (!e) m.props.push(e = { ws, board: P.board || '', ini: r.id, ids: [], sigs: [] });
|
|
95
|
+
e.ids.push(p.id); if (p.sig) e.sigs.push(p.sig);
|
|
96
|
+
const t = atOf(p); if (t != null && (m.at == null || t < m.at)) m.at = t;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
other.push(p);
|
|
100
|
+
}
|
|
101
|
+
// 2 and 4: one row per angle on one initiative, its parts counted
|
|
102
|
+
for (const [, L] of grill) {
|
|
103
|
+
L.sort((a, b) => ((a.p.data && a.p.data.rank) || 5) - ((b.p.data && b.p.data.rank) || 5));
|
|
104
|
+
const top = L[0], ats = L.map((x) => atOf(x.p)).filter((t) => t != null);
|
|
105
|
+
add({ key: 'gr:' + ws + ':' + r.id + ':' + top.f.angle, rank: hot ? 2 : 4, kind: 'grill', ws, board: P.board || '', ini: r.id, tag: top.f.label, at: ats.length ? Math.min(...ats) : null,
|
|
106
|
+
title: name + ': ' + clip(top.f.title, 120),
|
|
107
|
+
sub: [top.f.label + (L.length > 1 ? ' · ' + plural(L.length - 1, 'more finding', 'more findings') + ' like it' : ''), top.f.next ? 'Next: ' + clip(top.f.next, 110) : clip(top.f.why, 110)].filter(Boolean).join(' · '),
|
|
108
|
+
props: [{ ws, board: P.board || '', ini: r.id, ids: L.map((x) => x.p.id), sigs: L.map((x) => x.p.sig).filter(Boolean) }], yes: top.f.next ? 'Add the next step as a task' : 'Keep', alt: 'Not this' });
|
|
109
|
+
}
|
|
110
|
+
// 6 the rest: one row per initiative, opening its ✦
|
|
111
|
+
if (other.length) {
|
|
112
|
+
const ats = other.map(atOf).filter((t) => t != null), newest = ats.length ? Math.max(...ats) : null;
|
|
113
|
+
const kinds = [...new Set(other.map((p) => p.group))].map((g) => OTHER_WORD[g] || g);
|
|
114
|
+
add({ key: 'ot:' + ws + ':' + r.id, rank: 6, kind: 'other', ws, board: P.board || '', ini: r.id, tag: '', at: ats.length ? Math.min(...ats) : null,
|
|
115
|
+
title: name + ': ' + plural(other.length, 'proposal', 'proposals'), sub: 'Claude proposes ' + list(kinds) + '.',
|
|
116
|
+
older: newest != null && daysSince(newest, now) > OLDER_DAYS, alt: 'Open what Claude proposes' });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// 5 the first key result with no reading
|
|
120
|
+
const K = M && M.kr;
|
|
121
|
+
// products that share one OKR file wait on the same key result: it is one row, the products named once
|
|
122
|
+
const kk = K && K.first ? norm(K.first.title + ' ' + K.first.need) : '';
|
|
123
|
+
const seenK = kk && rows.find((x) => x.kind === 'kr' && x.krKey === kk);
|
|
124
|
+
if (seenK) { seenK.shared.push(P.name || ws); seenK.sub = seenK.sub.replace(/ Shared by .*$/, '') + ' Shared by ' + list(seenK.shared) + '.'; }
|
|
125
|
+
else if (K && K.first) add({ key: 'kr:' + ws, krKey: kk, shared: [P.name || ws], rank: 5, kind: 'kr', ws, tag: 'Key result', at: null, age: '',
|
|
126
|
+
title: K.first.token + ' ' + K.first.title + ': it waits on ' + K.first.need,
|
|
127
|
+
sub: (K.waiting > 1 ? 'The first of ' + K.waiting + ' key results waiting on a source. ' : 'One key result waits on a source. ') + (K.read ? K.read + ' of ' + K.total + ' have a reading.' : 'None has a reading.'),
|
|
128
|
+
alt: 'Open Intelligence' });
|
|
129
|
+
}
|
|
130
|
+
// 3 the program's milestone changes join its meeting row (one row per program, never one per change)
|
|
131
|
+
for (const pr of (o.programs || []).filter((p) => p && !p.archived)) {
|
|
132
|
+
const L = (pr.review && Array.isArray(pr.review.open)) ? pr.review.open : [];
|
|
133
|
+
if (!L.length) continue;
|
|
134
|
+
const m = meetRow(pr.id, pr.title, { program: pr.id });
|
|
135
|
+
m.ms = { program: pr.id, keys: L.map((i) => i.key).filter(Boolean) };
|
|
136
|
+
// a change names its milestone by id; the program's milestones carry the name (as ms-suggest.js says it)
|
|
137
|
+
const msName = (i) => { const x = i.milestone ? (pr.milestones || []).find((y) => y && y.id === i.milestone) : null; return (x && x.name) || i.name || 'A milestone'; };
|
|
138
|
+
// honest words: a change read from a record, not a meeting, makes the row "to confirm"
|
|
139
|
+
if (L.some((i) => i.src && i.src.kind && i.src.kind !== 'meeting')) m.notMeet = true;
|
|
140
|
+
for (const i of L) m.parts.unshift(i.op === 'risk' ? msName(i) + ' at risk' : i.op === 'move' ? msName(i) + ' to ' + dm(i.date, now) : i.op === 'rename' && i.name ? msName({ milestone: i.milestone }) + ' renamed ' + i.name : msName(i));
|
|
141
|
+
const t = tsOf(pr.review.checked); if (t != null && (m.at == null || t < m.at)) m.at = t;
|
|
142
|
+
}
|
|
143
|
+
for (const m of meet.values()) {
|
|
144
|
+
const n = m.parts.length, msN = m.ms ? m.ms.keys.length : 0;
|
|
145
|
+
const said = [...new Set(m.parts)]; // three risks on one milestone are said once
|
|
146
|
+
add({ ...m, title: m.title + ': ' + (msN === n ? plural(n, 'milestone change', 'milestone changes') : plural(n, 'change', 'changes')) + (m.notMeet ? ' to confirm' : ' from your meetings'),
|
|
147
|
+
sub: clip(list(said.slice(0, 4)) + (said.length > 4 ? ' and ' + (said.length - 4) + ' more' : ''), 160) + (n > 1 ? '. One press confirms all ' + n + '; ✎ to pick.' : '.'),
|
|
148
|
+
yes: n > 1 ? 'Confirm all ' + n : 'Confirm', alt: 'Pick one by one' });
|
|
149
|
+
}
|
|
150
|
+
// the calls only the PM can make that are not ✦ (Claude's milestone changes are rank 3 now: said once)
|
|
151
|
+
for (const c of (o.calls || []).filter((x) => x && x.kind !== 'msreview')) add({ key: 'call:' + c.id, rank: 5.5, kind: 'call', call: c, title: c.name, sub: '', at: null, age: '' });
|
|
152
|
+
rows.sort((a, b) => a.rank - b.rank || (a.at == null) - (b.at == null) || (a.at || 0) - (b.at || 0) || a.key.localeCompare(b.key));
|
|
153
|
+
const older = rows.filter((r) => r.older), shown = rows.filter((r) => !r.older);
|
|
154
|
+
return { rows: shown, older, n: rows.length };
|
|
155
|
+
}
|
|
@@ -264,14 +264,16 @@ function needHtml(C, open) {
|
|
|
264
264
|
const rows = [];
|
|
265
265
|
if (C.asks) rows.push({ k: 'asks', dot: 'you m-info', n: C.asks, name: plural(C.asks, 'New ask', 'New asks'), say: C.asks + ' new ' + plural(C.asks, 'ask', 'asks') });
|
|
266
266
|
for (const [k, tone, mo, name] of CHIP_F) if (C[k]) rows.push({ k, dot: tone + (mo ? ' ' + mo : ''), n: C[k], name, say: C[k] + ' ' + name.toLowerCase() });
|
|
267
|
-
|
|
267
|
+
// workspace grill W2: the open initiatives nothing read says anything about. One ✦ reads Jira for them; they are never counted as on track
|
|
268
|
+
const nosig = C.none ? `<button type="button" class="bd-nr" id="chNoSig"><i class="cz-dot q"></i>✦ Read Jira now for the ${C.none} with no signal</button>` : '';
|
|
269
|
+
if (!rows.length && !nosig) return '';
|
|
268
270
|
const on = rows.some((r) => F.chips.has(r.k));
|
|
269
271
|
const item = (r) => (r.k === 'asks'
|
|
270
272
|
? `<button type="button" class="bd-nr" id="chAsks" aria-haspopup="dialog"><i class="cz-dot ${r.dot}"></i>${esc(r.name)}<span class="k">${r.n}</span></button>`
|
|
271
273
|
: `<button type="button" class="bd-nr${F.chips.has(r.k) ? ' on' : ''}" data-f="${r.k}" aria-pressed="${F.chips.has(r.k)}"><i class="cz-dot ${r.dot}"></i>${esc(r.name)}<span class="k">${r.n}</span></button>`);
|
|
272
|
-
return `<span class="bd-grpw2 bd-needw" id="bdNeedw"><button type="button" class="bd-chip bd-need${on ? ' on' : ''}" id="chNeed" aria-haspopup="true" aria-expanded="${open}" aria-controls="bdNeedMenu" aria-label="${esc(rows.map((r) => r.say).join(', '))}. Show them" title="${esc(rows.map((r) => r.say).join(' · '))}">`
|
|
273
|
-
+ rows.map((r) => `<span><i class="cz-dot ${r.dot}"></i>${r.n}</span>`).join('') + SV(I.chev) + '</button>'
|
|
274
|
-
+ `<span class="bd-grpm bd-needm" id="bdNeedMenu" role="group" aria-label="Needs a look"${open ? '' : ' hidden'}><span class="bd-mlbl">Needs a look</span>${rows.map(item).join('')}</span></span>`;
|
|
274
|
+
return `<span class="bd-grpw2 bd-needw" id="bdNeedw"><button type="button" class="bd-chip bd-need${on ? ' on' : ''}" id="chNeed" aria-haspopup="true" aria-expanded="${open}" aria-controls="bdNeedMenu" aria-label="${esc(rows.map((r) => r.say).concat(C.none ? [C.none + ' no signal'] : []).join(', '))}. Show them" title="${esc(rows.map((r) => r.say).join(' · '))}">`
|
|
275
|
+
+ rows.map((r) => `<span><i class="cz-dot ${r.dot}"></i>${r.n}</span>`).join('') + (C.none ? `<span><i class="cz-dot q"></i>${C.none}</span>` : '') + SV(I.chev) + '</button>'
|
|
276
|
+
+ `<span class="bd-grpm bd-needm" id="bdNeedMenu" role="group" aria-label="Needs a look"${open ? '' : ' hidden'}><span class="bd-mlbl">Needs a look</span>${rows.map(item).join('')}${nosig}</span></span>`;
|
|
275
277
|
}
|
|
276
278
|
const menuOpen = (id) => !!$('#' + id) && !$('#' + id).hidden;
|
|
277
279
|
function drawChips() {
|
|
@@ -314,6 +316,7 @@ $('#bdChipset').addEventListener('click', (e) => {
|
|
|
314
316
|
}
|
|
315
317
|
const b = e.target.closest('button'); if (!b) return;
|
|
316
318
|
if (b.id === 'chJira') refreshJira(b);
|
|
319
|
+
else if (b.id === 'chNoSig') { closeMenu('bdNeedMenu', 'chNeed'); refreshJira($('#chJira') || b); }
|
|
317
320
|
else if (b.id === 'chSprint') Acts.open.sprint(b);
|
|
318
321
|
else if (b.id === 'chAsks') { closeMenu('bdNeedMenu', 'chNeed'); Acts.open.asks($('#chNeed')); }
|
|
319
322
|
else if (b.id === 'chShared') setTab('shared');
|