create-pmos 0.495.0 → 0.496.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 -14
- package/payload/AGENTS.md +2 -0
- package/payload/KIT-VERSION +2 -2
- 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/payload/state/log.md +1 -1
- package/workspace/adler-moments-test.js +3 -1
- package/workspace/board-programs-test.js +6 -6
- package/workspace/board-shared-ui-test.js +2 -2
- package/workspace/briefing-premium-adddoc-test.js +1 -48
- package/workspace/briefing-premium-board-test.js +18 -19
- package/workspace/changelog.json +37 -0
- package/workspace/guide/adler.md +1 -1
- package/workspace/guide/briefing-premium.md +1 -0
- package/workspace/guide/claude-proposes.md +6 -2
- package/workspace/guide/data-freshness.md +2 -0
- package/workspace/guide/home.md +1 -1
- package/workspace/guide/published-board.md +1 -1
- package/workspace/guide/workflows.md +4 -3
- package/workspace/lib/board-premium.js +17 -2
- package/workspace/lib/initiative-chat.js +4 -1
- package/workspace/lib/proposals.js +18 -4
- package/workspace/lib/routes-board-premium.js +2 -2
- package/workspace/lib/routes-proposals.js +14 -2
- package/workspace/lib/split-merge-suggest.js +37 -3
- package/workspace/lib/split-merge.js +10 -4
- package/workspace/lib/task-cli.js +5 -1
- package/workspace/lib/workflow-cli.js +70 -15
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/premium-board-actions-test.js +7 -9
- package/workspace/premium-core-doc-issue-test.js +9 -38
- package/workspace/public/pm/cozy-board.css +17 -5
- package/workspace/public/pm/home-app.js +12 -4
- package/workspace/public/pm/home-today.js +11 -4
- package/workspace/public/pm/load-shapes.css +57 -0
- package/workspace/public/pm/load-shapes.js +57 -0
- package/workspace/public/pm/premium-board.js +82 -57
- package/workspace/public/pm/readonly.js +3 -3
- package/workspace/public/pm/split-merge.js +3 -2
- package/workspace/public/premium-board.html +7 -10
- package/workspace/public/premium.html +1 -0
- package/workspace/split-merge-ui-test.js +1 -1
- package/workspace/test-lists/page-open-speed.json +1 -1
|
@@ -87,28 +87,14 @@ const BODY = '## Purpose\n\nWhy this board exists.\n\n## Scope\n\nWhat it covers
|
|
|
87
87
|
const H = await T.pressHeaders(BASE);
|
|
88
88
|
const nonce = H['x-pmos-nonce'];
|
|
89
89
|
|
|
90
|
-
/* B
|
|
91
|
-
|
|
92
|
-
const bToast = (p, re) => until(() => p.evaluate((s) => { const t = document.querySelector('#bdToast'); return t && !t.hidden && new RegExp(s).test(t.textContent) ? t.textContent : null; }, re.source));
|
|
93
|
-
const P = await open('/premium-board.html'), p = P.page;
|
|
94
|
-
await until(() => p.evaluate(() => document.querySelectorAll('#ibBody .ini2').length > 0), 60000);
|
|
95
|
-
await p.click('#tabDocs');
|
|
96
|
-
ok('B1 a board document is a row whose title opens the reader', await until(() => p.evaluate((k) => !!document.querySelector(`#docBody [data-opendoc="core:${k}"]`), bkey), 60000));
|
|
97
|
-
ok('B1 every document row has a title that opens it', await p.evaluate(() => [...document.querySelectorAll('#docBody .bd-dt[data-docrow]')].every((r) => !!r.querySelector('[data-opendoc]'))));
|
|
98
|
-
await p.click(`#docBody [data-opendoc="core:${bkey}"]`);
|
|
99
|
-
ok('B2 the reader shows the file\'s text and path, and the bar says nothing is sent', await until(() => p.evaluate((pt) => { const o = document.querySelector('#czViewer'); return !o.hidden && /Why this board exists/.test(o.textContent) && o.textContent.includes(pt) && !!o.querySelector('[data-issue-bar="draft"]') && /Nothing is sent/.test(o.querySelector('[data-issue-bar]').textContent) && !!o.querySelector('[data-act="issue"]'); }, d1.path)), await p.evaluate(() => document.querySelector('#czViewer').textContent));
|
|
100
|
-
ok('B2 no old dialog opens', await p.evaluate(() => !!!document.querySelector('#dlg.on')));
|
|
90
|
+
/* B a board's own core document. Batch 24 item 7 took the Documents tab off the Board (Search's Documents filter reaches the file), so no Board press
|
|
91
|
+
issues it; the issue route still takes a board scope */
|
|
101
92
|
const n0 = ledger().filter((l) => l.ev === 'issue').length;
|
|
102
|
-
await
|
|
103
|
-
ok('
|
|
104
|
-
const sent = P.posts.filter((x) => x.path === '/api/core-docs/issue');
|
|
105
|
-
ok('B4 it posted the workspace, the scope, the key and the slot, and nothing else, with the launch nonce', sent.length === 1 && Object.keys(sent[0].body).sort().join() === 'key,scope,slot,ws' && sent[0].body.scope === bscope && sent[0].body.key === bkey && sent[0].body.slot === d1.slot && sent[0].body.ws === B.ws && sent[0].headers['x-pmos-nonce'] === nonce && !!nonce, JSON.stringify(sent));
|
|
93
|
+
const bres = await fetch(BASE + '/api/core-docs/issue', { method: 'POST', headers: H, body: JSON.stringify({ ws: B.ws, scope: bscope, key: bkey, slot: d1.slot }) });
|
|
94
|
+
ok('B4 the issue route takes a board scope with the launch nonce', bres.status === 200, String(bres.status));
|
|
106
95
|
const il = ledger().filter((l) => l.ev === 'issue');
|
|
107
|
-
ok('B5 the ledger has one new issue line for that draft
|
|
96
|
+
ok('B5 the ledger has one new issue line for that draft', il.length === n0 + 1 && il[il.length - 1].slot === d1.slot && il[il.length - 1].scope === bscope, JSON.stringify(il));
|
|
108
97
|
ok('B5 the file now says state: issued', /^state: issued$/m.test(fs.readFileSync(path.join(team, d1.path), 'utf8')));
|
|
109
|
-
ok('B6 the reader says it is issued, the Issue it press is gone, and the row reads Up to date', await until(() => p.evaluate(() => { const o = document.querySelector('#czViewer'); return !o.hidden && !!o.querySelector('[data-issue-bar="done"]') && !o.querySelector('[data-act="issue"]'); }), 30000) && await until(() => p.evaluate((k) => { const r = document.querySelector(`#docBody [data-docrow="core:${k}"]`); return r && /Up to date/.test(r.textContent); }, bkey), 30000), await p.evaluate(() => document.querySelector('#czViewer').textContent + ' // ' + document.querySelector('#docBody').textContent));
|
|
110
|
-
await p.screenshot({ path: path.join(R.base, 'board-issued.png') });
|
|
111
|
-
await P.ctx.close();
|
|
112
98
|
|
|
113
99
|
/* I the initiative: a layout inside Home (premium-agreed-design). The Board's expand link lands there; the card's Overview lists the core documents (its Documents view),
|
|
114
100
|
and a draft opens a minimal reader in the Viewer (Waiting for design) with the Issue it bar, Read and Edit */
|
|
@@ -163,33 +149,18 @@ const BODY = '## Purpose\n\nWhy this board exists.\n\n## Scope\n\nWhat it covers
|
|
|
163
149
|
await jp.screenshot({ path: path.join(R.base, 'ini-issued.png') });
|
|
164
150
|
|
|
165
151
|
/* P the POST allow-list and the refusals */
|
|
166
|
-
const all =
|
|
152
|
+
const all = I.posts.concat(J.posts);
|
|
167
153
|
const other = all.filter((x) => !ALLOWED.test(x.path) && x.path !== '/api/loose-props/rows'); // what Home shows loose, sent when it changed so Claude can propose a clean-up (batch 13 item 3); it changes no work
|
|
168
|
-
ok('P1 the pages post only to the issue route (and the conversation note)', all.length >=
|
|
154
|
+
ok('P1 the pages post only to the issue route (and the conversation note)', all.length >= 2 && !other.length, JSON.stringify(other));
|
|
169
155
|
const sentAll = all.filter((x) => x.path === '/api/core-docs/issue');
|
|
170
|
-
ok('P2 every issue press carried the nonce and no writer field', sentAll.length >=
|
|
156
|
+
ok('P2 every issue press carried the nonce and no writer field', sentAll.length >= 2 && sentAll.every((x) => x.headers['x-pmos-nonce'] === nonce && !['by', 'via', 'at', 'actor', 'pm', 'nonce'].some((k) => k in x.body)));
|
|
171
157
|
const bare = await fetch(BASE + '/api/core-docs/issue', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: BASE }, body: JSON.stringify({ ws: B.ws, scope: bscope, key: bkey, slot: d1.slot }) });
|
|
172
158
|
ok('P3 a press without the launch nonce is refused', bare.status === 403);
|
|
173
159
|
const agent = await fetch(BASE + '/api/core-docs/issue', { method: 'POST', headers: { ...H, 'x-pmos-session': 's1' }, body: JSON.stringify({ ws: B.ws, scope: bscope, key: bkey, slot: d1.slot }) });
|
|
174
160
|
ok('P4 D98: an agent session is not refused as an agent (the press is checked like the PM\'s)', agent.status !== 403, String(agent.status));
|
|
175
|
-
ok('P5 no script error on any page', !
|
|
161
|
+
ok('P5 no script error on any page', !I.errors.length && !J.errors.length, I.errors.concat(J.errors).join(' | '));
|
|
176
162
|
await J.ctx.close();
|
|
177
163
|
|
|
178
|
-
/* F floors and fidelity: a fresh draft, at 390 and 1280, light and dark */
|
|
179
|
-
const d4 = CD.writeDraft(team, { scope: bscope, key: (board2.model.docs.rows.filter((d) => d.act && d.act.kind === 'draft' && d.st === 'missing' && d.key !== bkey)[0] || {}).key, markdown: BODY, by: 'agent:test', now: Date.now() });
|
|
180
|
-
const k4 = d4.path.split('/').slice(-2)[0];
|
|
181
|
-
for (const [w, h, theme] of [[390, 844, 'light'], [390, 844, 'dark'], [1280, 900, 'dark']]) {
|
|
182
|
-
const S = await open('/premium-board.html', { w, h, theme });
|
|
183
|
-
await until(() => S.page.evaluate(() => document.querySelectorAll('#ibBody .ini2').length > 0), 60000);
|
|
184
|
-
await S.page.click(w <= 720 ? '#chDocs' : '#tabDocs');
|
|
185
|
-
await until(() => S.page.evaluate((k) => !!document.querySelector(`#docBody [data-opendoc="core:${k}"]`), k4), 60000);
|
|
186
|
-
await S.page.click(`#docBody [data-opendoc="core:${k4}"]`);
|
|
187
|
-
await until(() => S.page.evaluate(() => !!document.querySelector('#czViewer [data-act="issue"]')));
|
|
188
|
-
const f = await S.page.evaluate(() => ({ wide: document.documentElement.scrollWidth > innerWidth + 1, small: [...document.querySelectorAll('#czViewer [data-act="issue"]')].filter((b) => b.getBoundingClientRect().height < (innerWidth <= 720 ? 44 : 34)).map((b) => b.textContent) }));
|
|
189
|
-
ok(`F1 ${w} px ${theme}: no sideways scroll and the bar's button is 44 px on a phone`, !f.wide && !f.small.length, JSON.stringify(f));
|
|
190
|
-
await S.page.screenshot({ path: path.join(R.base, `bar-${w}-${theme}.png`) });
|
|
191
|
-
await S.ctx.close();
|
|
192
|
-
}
|
|
193
164
|
if (process.env.KEEP_SHOTS) { for (const f of fs.readdirSync(R.base).filter((x) => /\.png$/.test(x))) fs.copyFileSync(path.join(R.base, f), path.join(process.env.KEEP_SHOTS, f)); }
|
|
194
165
|
finish();
|
|
195
166
|
})();
|
|
@@ -115,9 +115,22 @@ body.bd-page main.bd-main { position: relative; max-width: none; margin: 16px; p
|
|
|
115
115
|
.bd-chip em { font-style: normal; font-weight: 600; color: var(--cz-ink3); margin-left: 2px; }
|
|
116
116
|
.bd-chip:disabled { opacity: .6; }
|
|
117
117
|
.bd-sp { margin-left: auto; }
|
|
118
|
-
|
|
119
|
-
.bd-
|
|
120
|
-
.bd-
|
|
118
|
+
/* the calmer top bar (batch 24 item 7, canvas BbB): one pill for what needs a look, the legend behind (i), Jira as Adler's face */
|
|
119
|
+
.bd-chip.bd-need { background: var(--cz-well); border-color: transparent; gap: 10px; padding: 0 10px 0 12px; font-weight: 800; color: var(--cz-ink); }
|
|
120
|
+
.bd-chip.bd-need > span { display: inline-flex; align-items: center; gap: 5px; }
|
|
121
|
+
.bd-chip.bd-need.on { background: var(--cz-bluebg); color: var(--cz-blueink); }
|
|
122
|
+
.bd-needm { min-width: 220px; }
|
|
123
|
+
.bd-needm .bd-mlbl, .bd-lgdm .bd-mlbl { font: 800 var(--fs-1) var(--cz-sans); color: var(--cz-ink3); padding: 6px 10px 4px; letter-spacing: .5px; text-transform: uppercase; white-space: nowrap; }
|
|
124
|
+
.bd-needm .bd-nr { display: flex; align-items: center; gap: 10px; }
|
|
125
|
+
.bd-needm .bd-nr .k { margin-left: auto; color: var(--cz-ink3); font-weight: 700; }
|
|
126
|
+
.bd-needm .bd-nr.on { background: var(--cz-bluebg); color: var(--cz-blueink); }
|
|
127
|
+
.bd-needm .cz-dot { width: 7px; height: 7px; }
|
|
128
|
+
.bd-lgdm { min-width: 220px; padding: 6px 12px 10px; }
|
|
129
|
+
.bd-lgdm .tg-lgd { position: static; flex-direction: column; gap: 9px; padding: 4px 0 0; background: none; font-weight: 600; color: var(--cz-ink2); white-space: nowrap; }
|
|
130
|
+
.bd-chip.bd-jf { width: 36px; padding: 0; justify-content: center; border-color: transparent; background: transparent; position: relative; }
|
|
131
|
+
.bd-chip.bd-jf:hover { background: var(--cz-well); }
|
|
132
|
+
.bd-chip.bd-jf svg.ad-svg { width: 24px; height: 24px; }
|
|
133
|
+
.bd-pip { position: absolute; right: 3px; top: 3px; width: 9px; height: 9px; border-radius: 50%; background: var(--cz-risk); border: 2px solid var(--cz-card); }
|
|
121
134
|
|
|
122
135
|
/* ── the "Waiting for design" card, where the shell has none yet; messages ── */
|
|
123
136
|
.bd-wait { border: 1px dashed var(--cz-hair); border-radius: 18px; padding: 22px; color: var(--cz-ink3); font-size: var(--fs-2); }
|
|
@@ -1195,7 +1208,6 @@ html[data-theme="dark"] body.bd-page { --bd-prog: rgb(180,170,240); --bd-progbg:
|
|
|
1195
1208
|
.sc .cur .eta { top: 19px; }
|
|
1196
1209
|
.sc .eta.late { color: var(--cz-late); }
|
|
1197
1210
|
.bd-kb.lanes .bd-kstrip span { margin-top: 58px; }
|
|
1198
|
-
.bd-chipset .tg-lgd { position: static; padding: 0; background: none; font-weight: 600; white-space: nowrap; }
|
|
1199
1211
|
/* the phone (BdPhone): a program is a head card with its next milestone; its initiatives and the product heads follow as cards */
|
|
1200
1212
|
@media (max-width: 720px) {
|
|
1201
1213
|
.bt-pg { display: grid; grid-template-columns: minmax(0, 1fr); border: 0; border-radius: 14px; padding: 8px 6px 10px 4px; min-height: 0; }
|
|
@@ -1218,7 +1230,7 @@ html[data-theme="dark"] body.bd-page { --bd-prog: rgb(180,170,240); --bd-progbg:
|
|
|
1218
1230
|
.bd-spw .bd-spk svg { width: 14px; height: 14px; }
|
|
1219
1231
|
body.bd-spick .bd-hd .bd-title { display: none; }
|
|
1220
1232
|
body.bd-spick .bd-hd .bd-tools { margin-left: auto; }
|
|
1221
|
-
@media (max-width: 720px) { .bd-spw .bd-spk, .bd-spw .bd-grpm button { min-height: 44px; } }
|
|
1233
|
+
@media (max-width: 720px) { .bd-spw .bd-spk, .bd-spw .bd-grpm button, .bd-needm .bd-nr { min-height: 44px; } .bd-chip.bd-jf { width: 44px; } .bd-chip.bd-jf::before { display: none; } }
|
|
1222
1234
|
/* programs first on every span (PgTimeline's rule): the program row brackets its initiatives' bars and carries its milestones */
|
|
1223
1235
|
.sp-pg { background: var(--bd-progbg); min-height: 56px; }
|
|
1224
1236
|
.sp-pg .sp-n { flex-direction: row; align-items: center; justify-content: flex-start; }
|
|
@@ -48,7 +48,8 @@ import * as AD from './cozy-adler.js'; // Adler, Claude's face (batch 16 item
|
|
|
48
48
|
import * as OKL from './okr-list.js'; // the product page's Goals line (batch 16 item 6, GsProduct)
|
|
49
49
|
import { canDictate, dictate } from './home-dictate.js';
|
|
50
50
|
import { mountIni, iniList, iniOverview, iniPage, iniAddTask } from './home-ini.js'; // the initiative's page, its stage move, Tasks and Core documents (premium-uncovered-features, HbC)
|
|
51
|
-
import * as MEN from './home-mentions.js';
|
|
51
|
+
import * as MEN from './home-mentions.js';
|
|
52
|
+
import * as LS from './load-shapes.js'; // the last view, the grey shapes and the held Viewer column while Home reads (page-open-speed) // @Mentions, the permanent row at the top of Home and its Viewer (Viewer kit, XvANew)
|
|
52
53
|
import { mountRecRow, clockOf as recClock } from './home-rec-row.js';
|
|
53
54
|
import { mountSesName } from './home-ses-name.js'; // session names A: tap the session's name to rename it (canvas XmsNames)
|
|
54
55
|
import * as SM from './split-merge.js'; // split one initiative in two, or merge two: Claude suggests, the PM approves (batch 11 item 14)
|
|
@@ -173,13 +174,14 @@ async function getRetry(path, tries = 20) {
|
|
|
173
174
|
}
|
|
174
175
|
return { ok: false, error: 'still reading' };
|
|
175
176
|
}
|
|
177
|
+
const LAST_KEY = (WS || '') + '|' + (BOARD || ''); // the last view Home drew, by its product and board (SpA)
|
|
176
178
|
let pickGen = 0; // bumped by every saved pick, so a read that started before the save does not put the old pick back
|
|
177
179
|
async function load() {
|
|
178
180
|
const gen = pickGen;
|
|
179
181
|
const [brief, board, ses, desk, today, sch, work, day, rev, progs, , pick] = await Promise.all([
|
|
180
182
|
getRetry('/api/briefing-premium' + wsQ()), getRetry('/api/board-premium' + wsQ() + boardQ(WS ? '&' : '?')), pmGet('/api/sessions'), pmGet('/api/home-desk' + wsQ()),
|
|
181
183
|
pmGet('/api/brief-page' + wsQ()), pmGet('/api/schedules' + wsQ()), pmGet('/api/my-work-premium' + wsQ()), pmGet('/api/phone-day'), pmGet('/api/home-review' + wsQ()),
|
|
182
|
-
pmGet('/api/programs'), MEN.load(menCtx()), PP.readPick(), SO.load(),
|
|
184
|
+
pmGet('/api/programs'), MEN.load(menCtx()).then((x) => { S.menIn = true; if (!S.ready) drawList(); return x; }), PP.readPick(), SO.load(),
|
|
183
185
|
]);
|
|
184
186
|
const D = S.D;
|
|
185
187
|
D.brief = brief && brief.ok && brief.model ? brief.model : null;
|
|
@@ -345,7 +347,8 @@ function homeListHtml() {
|
|
|
345
347
|
S.lo = { sel, ses: S.ses, restOpen: (k) => S.rest.has(k), pin: S.pin, phone: false }; // the folded rail's card draws Home's rows with these
|
|
346
348
|
return TD.listHtml(M, todayModel(), {
|
|
347
349
|
sel, selHead: S.head, ses: S.ses, pOpen, here: isPhone() ? null : TD.groupOfOpen(M, sel) || S.head, restOpen: (k) => S.rest.has(k), cap: S.cap, pin: S.pin, avHtml, ready: S.ready, err: S.D.err.length ? S.D.err[0] : '',
|
|
348
|
-
pick: S.D.pick, tones: pickTones(), review: R, mic: canDictate(), phone: isPhone(), day: S.day, news: S.news && S.news.ok ? S.news.count : 0, top: S.ready ? RECROW.html() + MEN.rowHtml(S.D, { sel: S.sel }) + (S.impact ? IMP.rowHtml(S.impact.c, { esc, sel: S.sel === 'impact' }) : '') : '',
|
|
350
|
+
pick: S.D.pick, tones: pickTones(), review: R, mic: canDictate(), phone: isPhone(), day: S.day, news: S.news && S.news.ok ? S.news.count : 0, top: S.ready ? RECROW.html() + MEN.rowHtml(S.D, { sel: S.sel }) + (S.impact ? IMP.rowHtml(S.impact.c, { esc, sel: S.sel === 'impact' }) : '') : S.menIn ? '<div class="ls-stale" inert>' + LS.inert(MEN.rowHtml(S.D, {})) + '</div>' : LS.menShape(), // Mentions on its own clock, seen but not pressed until Home is fresh (SpG)
|
|
351
|
+
last: S.ready ? '' : LS.last(LAST_KEY), keep: S.ready && S.filter === 'today' && !S.ini ? (b) => LS.keepLast(LAST_KEY, b) : null,
|
|
349
352
|
imFace: IM.faceHtml(S.im, { joy: S.imJoy }), grill: S.ready ? grillNews() : null,
|
|
350
353
|
});
|
|
351
354
|
}
|
|
@@ -1312,7 +1315,10 @@ addEventListener('pp:changed', (e) => { if (WS) { location.assign('/premium.html
|
|
|
1312
1315
|
|
|
1313
1316
|
/* ── the Viewer: needs rows, the weekly update, the overview, files, people, routines ── */
|
|
1314
1317
|
let lastViewer = null;
|
|
1315
|
-
VW.onChange((d) => { if (d.open) lastViewer = d.id; chat.refresh(); document.body.classList.toggle('hm-vw-open', d.open); });
|
|
1318
|
+
VW.onChange((d) => { if (d.open) { lastViewer = d.id; LS.holdViewer($('.hm-vw'), false); } chat.refresh(); document.body.classList.toggle('hm-vw-open', d.open); });
|
|
1319
|
+
// the Viewer's column held from the first frame when Home opened it last time (SpJ): the chat has its final width at once
|
|
1320
|
+
const VW_HELD = 'pmos.home.vwheld';
|
|
1321
|
+
if (!isPhone() && store.get(VW_HELD) === '1' && !VW.isOpen()) LS.holdViewer($('.hm-vw'), true);
|
|
1316
1322
|
function reopenViewer() {
|
|
1317
1323
|
if (/^vw:/.test(lastViewer || '')) { import('./premium-viewer.js').then((M) => { if (!M.vwReopen()) { lastViewer = null; reopenViewer(); } }); return; } // the last file, chart or report shown
|
|
1318
1324
|
const r = S.sel ? rowById(S.sel) : null;
|
|
@@ -1991,6 +1997,8 @@ load().then(() => {
|
|
|
1991
1997
|
if (say) setTimeout(() => { const ta = $('#hmIn'); if (ta) { ta.value = say; ta.dispatchEvent(new Event('input', { bubbles: true })); ta.focus(); ta.setSelectionRange(say.length, say.length); } }, 0);
|
|
1992
1998
|
if (mt) openMeeting({ note: mt });
|
|
1993
1999
|
if (/^(prop|men|pulse|scan|rep|src)$/.test(P.get('around') || '')) MORE.openAround(P.get('around')); // ?around=<row>: that Around you view (home-around.js)
|
|
2000
|
+
// the Viewer opens now or not at all: the held column goes either way, and the next open holds it only if it opened (SpJ)
|
|
2001
|
+
setTimeout(() => { if (!isPhone()) store.set(VW_HELD, VW.isOpen() ? '1' : null); LS.holdViewer($('.hm-vw'), false); }, 0);
|
|
1994
2002
|
poll();
|
|
1995
2003
|
});
|
|
1996
2004
|
addEventListener('resize', () => { if (!isPhone() && S.sheet) sheet(false); drawList(); });
|
|
@@ -18,7 +18,8 @@ import { gripHtml, sugHtml } from './prog-order.js'; // a program's order: the
|
|
|
18
18
|
import * as SO from './sort-order.js'; // the sort button and ONE order with the Board (batch 14 item 8, canvas SoAHomePhone)
|
|
19
19
|
import { prodKey } from './order-model.js';
|
|
20
20
|
import * as CW from './ctx-wedge.js'; // how full a session's context is: the pie, and Claude's ✦ Compact (batch 13 item 8, XctxA1Desk)
|
|
21
|
-
import * as AD from './cozy-adler.js';
|
|
21
|
+
import * as AD from './cozy-adler.js';
|
|
22
|
+
import * as LS from './load-shapes.js'; // the ring, the grey shapes and the last view while Home reads (page-open-speed, SpA SpG SpM) // Adler greets on Home (batch 16 item 2, canvas AdH1): the M face, honest when the read failed
|
|
22
23
|
|
|
23
24
|
export const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
24
25
|
export const P = {
|
|
@@ -321,8 +322,11 @@ export function listHtml(M, T, o) {
|
|
|
321
322
|
// the blue dot after "Good morning": something is new for you, in Your day's New for you fold (feedback batch 16 item 5b, FmA4Desk)
|
|
322
323
|
const nw = o.news > 0 ? '. ' + o.news + ' new for you' : '';
|
|
323
324
|
// Your impact (batch 16 item 7, GmA1Home): with it on, the greeting's face is its own press, Adler with the week's leaves (pm/impact.js faceHtml)
|
|
324
|
-
|
|
325
|
-
|
|
325
|
+
// Adler beside the greeting from the first frame (SpM): thinking while Home reads, handled when it is in, honest if the read failed.
|
|
326
|
+
// While Home reads, a hollow ring follows the greeting (SpA); a fresh page shows no sign of having been checked
|
|
327
|
+
const own = o.imFace && !o.err && o.ready;
|
|
328
|
+
const face = own ? '' : AD.adler(o.err ? 'honest' : o.ready ? 'handled' : 'thinking', 36, { label: false, cls: 'hv-ad' });
|
|
329
|
+
const head = `<div class="hm-hd hv-bar">${own ? o.imFace : ''}<button type="button" class="hv-hi" ${o.ready ? 'data-hm-day' : 'aria-disabled="true"'} aria-expanded="${!!o.day}" aria-label="${esc(M.word || 'Today')}${nw}. Open your day">${face}<b>${esc(M.word || 'Today')}</b>${o.ready ? '' : LS.ringHtml()}${o.news > 0 ? '<i class="hv-nd" aria-hidden="true"></i>' : ''}${svg(P.chevD, 18)}</button><span class="hv-acts">`
|
|
326
330
|
// the product chip, always (it is also the way to add one): the ticked products' tiles, or All (canvas XhpA2States)
|
|
327
331
|
+ (o.pick ? chipHtml(o.pick, { id: 'hmPick', attrs: 'data-hm-pick', label: 'Products on Home', tones: o.tones }) : '')
|
|
328
332
|
+ (o.phone ? '' : `<button type="button" class="ut-ib hv-ib${flt ? ' on' : ''}" data-hm-flt aria-haspopup="menu" aria-label="Show: Today, Kept or Done${flt ? '. Now ' + esc(flt.label) : ''}" title="Show: Today, Kept or Done">${svg(P.filter, 18)}</button>`)
|
|
@@ -336,7 +340,9 @@ export function listHtml(M, T, o) {
|
|
|
336
340
|
// the Today card only while the day has a moment; the plain day (its meetings and load) is in Your day
|
|
337
341
|
const card = MOMENT.has(T.mode) ? todayHtml(T, { greet: M.greet, sub: '', phone: o.phone }) : '';
|
|
338
342
|
// start-up and errors (canvas AdU9, AdU6): Adler wears his glasses while Home reads, and says a failure plainly; the greeting's face hides
|
|
339
|
-
|
|
343
|
+
// start-up (SpA, SpG): the list Home drew last, drawn at once and inert until the fresh read is in; with none kept, grey rows that claim
|
|
344
|
+
// nothing. o.top is Mentions (or its grey shape) on its own clock. Errors (canvas AdU6): Adler says a failure plainly
|
|
345
|
+
if (!o.ready) return head + card + `<p class="vh" role="status">Reading your work.</p><div class="hv-body">${o.top || ''}${o.last ? `<div class="ls-stale" inert>${o.last}</div>` : LS.listShapes()}</div>`;
|
|
340
346
|
const errLine = `<div class="hm-none hm-ad" role="status">${AD.adler('honest', 48, { label: false })}<p>Home could not read the repository: ${esc(o.err)}.</p></div>`;
|
|
341
347
|
if (o.err && !M.groups.some((g) => (g.kind === 'product' && g.count) || (g.rows && g.rows.length))) return head + card + errLine;
|
|
342
348
|
// the repository's read failed but sessions or notes still answered: say so above what did come back, never an empty product as if calm
|
|
@@ -353,6 +359,7 @@ export function listHtml(M, T, o) {
|
|
|
353
359
|
if (o.cap) body += `<form class="hm-cap" id="hmCapF"><label class="vh" for="hmCap">Add a task to your own list</label><input id="hmCap" autocomplete="off" maxlength="250" placeholder="A task for your own list. Esc to cancel">`
|
|
354
360
|
+ (o.mic ? `<button type="button" class="ut-ib" data-hm-capmic aria-label="Say it instead" title="Say it instead">${svg(P.mic, 16)}</button>` : '') + '</form>';
|
|
355
361
|
}
|
|
362
|
+
if (o.keep && !o.err && !M.list && !o.cap) o.keep(body); // the last view, for the next open (SpA)
|
|
356
363
|
return head + card + `<div class="hv-body">${o.top || ''}${body}</div>` + (o.phone && o.ses && !M.list ? sesSheetHtml(M, o) : ''); // o.top: the pinned recording row and Mentions (home-rec-row.js, home-mentions.js)
|
|
357
364
|
}
|
|
358
365
|
/** Your day (HvAOpen): a sheet over the list from "Good morning". The date, the day's meetings and their load (a press opens the calendar,
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/* Loading shapes (initiative page-open-speed; canvas boards SpA, SpD, SpG, SpJ, SpM, agreed A 2026-11-02).
|
|
2
|
+
* Honest state: grey shapes say only "something goes here" (no names, no counts); a hollow ring says Home is checking; nothing moves;
|
|
3
|
+
* no bar and no percent. Every shape is static (principle 12: no motion on data). */
|
|
4
|
+
|
|
5
|
+
/* the hollow ring beside "Good morning" while Home checks (SpA) */
|
|
6
|
+
.ls-ringdot { display: inline-block; flex: none; width: 9px; height: 9px; margin-left: 6px; border-radius: 50%; border: 1.6px solid var(--cz-blue); box-sizing: border-box; }
|
|
7
|
+
|
|
8
|
+
/* grey shapes */
|
|
9
|
+
.ls-sk { display: block; height: 9px; border-radius: 6px; background: var(--cz-well2); }
|
|
10
|
+
.ls-dot { flex: none; width: 12px; height: 12px; border-radius: 50%; background: var(--cz-well2); }
|
|
11
|
+
.ls-box { flex: none; width: 12px; height: 12px; border-radius: 6px; background: var(--cz-well2); }
|
|
12
|
+
.ls-row { display: flex; align-items: center; gap: 10px; min-height: 44px; padding: 0 12px; box-sizing: border-box; }
|
|
13
|
+
.ls-row + .ls-row { border-top: 1px solid var(--cz-hair); }
|
|
14
|
+
.ls-head { display: flex; align-items: center; min-height: 32px; padding: 0 12px; }
|
|
15
|
+
.ls-group + .ls-group { margin-top: 14px; }
|
|
16
|
+
.ls-men { display: flex; align-items: center; gap: 10px; min-height: 44px; padding: 0 12px; margin-bottom: 12px; border-radius: 8px; background: var(--cz-card); box-sizing: border-box; }
|
|
17
|
+
|
|
18
|
+
/* the last view, drawn at once while the fresh read runs (SpA): it can be read, not acted on; a press that acts is dashed */
|
|
19
|
+
.ls-stale { pointer-events: none; }
|
|
20
|
+
.ls-stale .hm-cta, .ls-stale .hv-cta, .ls-stale .solid, .ls-stale [data-ls-was].solid { background: transparent !important; color: var(--cz-ink3) !important; border: 1px dashed var(--cz-ink3) !important; box-shadow: none !important; }
|
|
21
|
+
.ls-stale .hr-bd { visibility: hidden; }
|
|
22
|
+
|
|
23
|
+
/* the Viewer's column held from frame one (SpJ): a dashed empty panel, the same width the Viewer opens at, no text, no icon */
|
|
24
|
+
.ls-vw { flex: none; border: 1.5px dashed var(--cz-hair); border-radius: 8px; box-sizing: border-box; width: min(500px, max(380px, calc(100vw - 808px))); }
|
|
25
|
+
@media (max-width: 760px) { .ls-vw { display: none; } }
|
|
26
|
+
|
|
27
|
+
/* the Board's skeleton (SpD): the real toolbar and tabs are the page's own; the body is grey rows shaped like programs and initiatives */
|
|
28
|
+
.ls-bd { display: flex; flex-direction: column; }
|
|
29
|
+
.ls-bd .ls-bh { display: grid; grid-template-columns: minmax(160px, 260px) 1fr minmax(120px, 220px); gap: 16px; align-items: center; min-height: 36px; padding: 0 14px; border-radius: 8px; background: var(--cz-well); }
|
|
30
|
+
.ls-bd .ls-stg { display: flex; justify-content: space-between; gap: 6px; }
|
|
31
|
+
.ls-bd .ls-stg .ls-sk { width: 28px; height: 7px; }
|
|
32
|
+
.ls-bd .ls-br { display: grid; grid-template-columns: minmax(160px, 260px) 1fr minmax(120px, 220px); gap: 16px; align-items: center; min-height: 46px; padding: 0 14px; }
|
|
33
|
+
.ls-bd .ls-br.pg { min-height: 40px; margin-top: 8px; border-radius: 8px; background: var(--cz-well); }
|
|
34
|
+
.ls-bd .ls-trk { position: relative; display: flex; align-items: center; justify-content: space-between; height: 12px; }
|
|
35
|
+
.ls-bd .ls-trk::before { content: ""; position: absolute; left: 4px; right: 4px; top: 5px; height: 2px; background: var(--cz-well2); }
|
|
36
|
+
.ls-bd .ls-trk i { position: relative; width: 7px; height: 7px; background: var(--cz-well2); }
|
|
37
|
+
.ls-bd .ls-trk i.ls-tdot { border-radius: 50%; }
|
|
38
|
+
.ls-bd .ls-trk i.on { width: 12px; height: 12px; }
|
|
39
|
+
.ls-slow { margin: 10px 14px 4px; font-size: var(--fs-2); font-weight: 700; color: var(--cz-ink3); }
|
|
40
|
+
@media (max-width: 760px) {
|
|
41
|
+
.ls-bd .ls-bh, .ls-bd .ls-br { grid-template-columns: 1fr; gap: 6px; }
|
|
42
|
+
.ls-bd .ls-bh .ls-stg, .ls-bd .ls-br .ls-trk, .ls-bd .ls-br > .ls-sk:last-child { display: none; }
|
|
43
|
+
}
|
|
44
|
+
.ls-stale > .hm-pg + .hm-pg { margin-top: 12px; }
|
|
45
|
+
.ls-stale > .hm-pg.loose { margin-top: 0; }
|
|
46
|
+
.ls-w30 { width: 30%; }
|
|
47
|
+
.ls-w34 { width: 34%; }
|
|
48
|
+
.ls-w38 { width: 38%; }
|
|
49
|
+
.ls-w40 { width: 40%; }
|
|
50
|
+
.ls-w55 { width: 55%; }
|
|
51
|
+
.ls-w60 { width: 60%; }
|
|
52
|
+
.ls-w70 { width: 70%; }
|
|
53
|
+
.ls-w75 { width: 75%; }
|
|
54
|
+
.ls-w48 { width: 48%; }
|
|
55
|
+
.ls-w54 { width: 54%; }
|
|
56
|
+
.ls-w62 { width: 62%; }
|
|
57
|
+
.ls-w78 { width: 78%; }
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/* Loading shapes (initiative page-open-speed; canvas SpA, SpD, SpG, SpJ, SpM, agreed A 2026-11-02).
|
|
2
|
+
*
|
|
3
|
+
* ringHtml() the hollow ring beside "Good morning" while Home checks
|
|
4
|
+
* listShapes() Home's list as grey rows, before the first read and with no last view kept
|
|
5
|
+
* menShape() the Mentions row's grey shape, until Mentions is read
|
|
6
|
+
* keepLast(key, html) keep the list Home drew last in this browser (not over 300 KB), for the next open
|
|
7
|
+
* last(key) that list, or ''
|
|
8
|
+
* holdViewer(host, on) the Viewer's column as a dashed empty panel, so the chat never narrows when the Viewer opens
|
|
9
|
+
* slowLine(host, M, ms) 'Still reading the board.' once the wait runs past ms, while there is still no model
|
|
10
|
+
*
|
|
11
|
+
* Grey shapes carry no names and no counts, and nothing moves. The last view is drawn inert: it can be read, not acted on, until the
|
|
12
|
+
* fresh read is in. */
|
|
13
|
+
|
|
14
|
+
const store = {
|
|
15
|
+
get(k) { try { return localStorage.getItem(k); } catch { return null; } },
|
|
16
|
+
set(k, v) { try { if (v == null) localStorage.removeItem(k); else localStorage.setItem(k, v); } catch { /* a private window, or full */ } },
|
|
17
|
+
};
|
|
18
|
+
const LAST = 'pmos.home.last:';
|
|
19
|
+
const MAX = 300000;
|
|
20
|
+
|
|
21
|
+
export const ringHtml = () => '<span class="ls-ringdot" role="img" aria-label="Checking"></span>';
|
|
22
|
+
|
|
23
|
+
const W = [62, 78, 54, 70, 48];
|
|
24
|
+
const row = (w, box) => `<div class="ls-row">${box ? '<i class="ls-box"></i>' : '<i class="ls-dot"></i>'}<i class="ls-sk ls-w${w}"></i></div>`;
|
|
25
|
+
export function listShapes() {
|
|
26
|
+
return '<div class="ls-list" aria-hidden="true">'
|
|
27
|
+
+ `<div class="ls-group"><div class="ls-head"><i class="ls-sk ls-w38"></i></div><div class="hv-card">${W.map((w, i) => row(w, i > 0)).join('')}</div></div>`
|
|
28
|
+
+ `<div class="ls-group"><div class="ls-head"><i class="ls-sk ls-w30"></i></div><div class="hv-card">${W.slice(0, 3).map((w) => row(w, true)).join('')}</div></div>`
|
|
29
|
+
+ '</div>';
|
|
30
|
+
}
|
|
31
|
+
export const menShape = () => '<div class="ls-men" aria-hidden="true"><i class="ls-dot"></i><i class="ls-sk ls-w34"></i></div>';
|
|
32
|
+
|
|
33
|
+
const kept = new Map(); // what this page wrote last, so a redraw that changed nothing writes nothing
|
|
34
|
+
export function keepLast(key, html) { if (kept.get(key) === html) return; kept.set(key, html); store.set(LAST + key, html && html.length <= MAX ? html : null); }
|
|
35
|
+
/** The last view, drawn as a picture of itself: no button, link or field in it, so nothing in it can be pressed or found as a control
|
|
36
|
+
* until the fresh read is in (SpA: it can be read, not acted on). */
|
|
37
|
+
export function last(key) { return inert(store.get(LAST + key) || ''); }
|
|
38
|
+
/** html with its buttons, links and fields drawn as spans, and its press marks renamed: seen, not pressed, until the fresh read is in. */
|
|
39
|
+
export function inert(html) {
|
|
40
|
+
return String(html || '').replace(/<(\/?)(button|a|input|select|textarea)\b/gi, (m, end, tag) => '<' + end + 'span' + (end ? '' : ' data-ls-was="' + tag.toLowerCase() + '"'))
|
|
41
|
+
.replace(/\sdata-(hm|hr)-/g, ' data-ls-'); // and without the presses' own marks, so nothing reads it as the live list
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** on: a dashed empty panel in the host (once); off: the panel goes. */
|
|
45
|
+
export function holdViewer(host, on) {
|
|
46
|
+
if (!host) return;
|
|
47
|
+
const was = host.querySelector(':scope > .ls-vw');
|
|
48
|
+
if (on && !was) { const p = document.createElement('div'); p.className = 'ls-vw'; p.setAttribute('aria-hidden', 'true'); host.prepend(p); }
|
|
49
|
+
if (!on && was) was.remove();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The Board's quiet line past ms of waiting (SpD): it says only what is true, and never shows once the board is in (M() is set). */
|
|
53
|
+
export function slowLine(host, M, ms = 2000) {
|
|
54
|
+
const show = () => { if (M() || !host || !host.querySelector('.ls-bd') || host.querySelector('.ls-slow')) return; host.insertAdjacentHTML('afterbegin', '<p class="ls-slow" role="status">Still reading the board.</p>'); };
|
|
55
|
+
const left = ms - (typeof performance !== 'undefined' ? performance.now() : 0);
|
|
56
|
+
if (left <= 0) show(); else setTimeout(show, left);
|
|
57
|
+
}
|