create-pmos 0.510.0 → 0.512.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 +239 -134
- 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 +6 -7
- package/workspace/changelog.json +349 -912
- package/workspace/ev-everywhere-test.js +4 -4
- package/workspace/fixtures/next-prompt-replay.json +40 -0
- package/workspace/guide/home.md +2 -1
- package/workspace/guide/index.md +1 -0
- package/workspace/guide/initiative-boards.md +3 -1
- package/workspace/guide/next-prompt.md +62 -0
- package/workspace/guide/settings.md +11 -5
- package/workspace/lib/board-premium.js +3 -2
- package/workspace/lib/kit-update.js +5 -2
- package/workspace/lib/next-prompt.js +199 -0
- package/workspace/lib/route-modules.js +1 -0
- package/workspace/lib/routes-boards.js +15 -1
- package/workspace/lib/routes-next-prompt.js +248 -0
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/panels-test.js +68 -1
- package/workspace/public/pm/bd-cols.js +2 -1
- package/workspace/public/pm/bd-deliveries.js +1 -1
- package/workspace/public/pm/bd-span.js +15 -10
- package/workspace/public/pm/cozy-ev.css +26 -10
- package/workspace/public/pm/cozy-home.css +45 -1
- package/workspace/public/pm/cozy-live-pill.css +9 -0
- package/workspace/public/pm/cozy-live-pill.js +50 -3
- package/workspace/public/pm/cozy-panels.css +9 -0
- package/workspace/public/pm/cozy-panels.js +99 -16
- package/workspace/public/pm/cozy-settings.js +70 -16
- package/workspace/public/pm/cozy-viewer.js +15 -3
- package/workspace/public/pm/home-chat.js +25 -4
- package/workspace/public/pm/next-prompt-ui.js +117 -0
- package/workspace/public/pm/page-chat.css +9 -0
- package/workspace/public/pm/page-chat.js +90 -0
- package/workspace/public/pm/premium-board.js +5 -0
- package/workspace/public/pm/upd-weeks.js +21 -7
- package/workspace/public/premium-board.html +2 -2
- package/workspace/test-lists/closed-not-late.json +5 -0
- package/workspace/test-lists/next-prompt.json +5 -0
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
* mountLivePill(doc = document, opts) once per page, after mountNav: the pill lives inside #czNav, so it hides whenever the menu hides
|
|
16
16
|
* opts.go(item) → true the page opens the item itself (Home: the session's chat, the meeting); else the row's link is followed
|
|
17
17
|
* opts.open() → id the session open on this page now: its row is tinted
|
|
18
|
+
* Docking (batch 26 item 4): on a desktop the list has a grab bar on top. Drag it to the left or the right half of the window and the
|
|
19
|
+
* list docks on that side (above the pill, or at the window's right edge at the same height); a press or Enter on the bar moves it to the
|
|
20
|
+
* other side. The side is kept (localStorage 'cz-live-side'). A phone's sheet has none.
|
|
18
21
|
* Reads GET /api/live every 10 s while the page is shown, and on the cz:live event (Home sends it after the PM opens or answers a
|
|
19
22
|
* session); the recording comes from pm/rec-state.js. The recording's row has one icon-only Stop beside it (Wawan, 2026-11-03: finish
|
|
20
23
|
* from anywhere): the first press asks "Press again", the second ends the meeting (rec-state.js stopPress). Nothing else is written.
|
|
@@ -32,13 +35,16 @@ const STOP = '<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="7" y="7" widt
|
|
|
32
35
|
const CHECK = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg>';
|
|
33
36
|
const PHONE = '(max-width: 760px)';
|
|
34
37
|
const mac = () => { try { return /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent || ''); } catch { return false; } };
|
|
38
|
+
const SIDE = 'cz-live-side';
|
|
39
|
+
const sideGet = () => { try { return localStorage.getItem(SIDE) === 'right' ? 'right' : 'left'; } catch { return 'left'; } };
|
|
40
|
+
const sideSet = (v) => { try { if (v === 'right') localStorage.setItem(SIDE, 'right'); else localStorage.removeItem(SIDE); } catch { /* a private window */ } };
|
|
35
41
|
const markHtml = (m, once) => (m === 'done' ? `<i class="lv-m done${once ? ' once' : ''}">${CHECK}</i>` : `<i class="lv-m ${m}${once ? ' once' : ''}"></i>`);
|
|
36
42
|
|
|
37
43
|
export function mountLivePill(doc = document, opts = {}) {
|
|
38
44
|
if (doc.getElementById('czLive')) return;
|
|
39
45
|
if (!doc.querySelector(`link[href="${SHEET}"]`)) { const l = doc.createElement('link'); l.rel = 'stylesheet'; l.href = SHEET; doc.head.appendChild(l); }
|
|
40
46
|
const L = { sessions: [], titles: {}, rec: null, open: false, shown: [], marks: new Map(), tick: null, tries: 0 };
|
|
41
|
-
let pill = null, tray = null, scrim = null;
|
|
47
|
+
let pill = null, tray = null, scrim = null, rowsEl = null, grabB = null;
|
|
42
48
|
const phone = () => typeof matchMedia === 'function' && matchMedia(PHONE).matches;
|
|
43
49
|
const place = () => {
|
|
44
50
|
const nav = doc.getElementById('czNav');
|
|
@@ -48,6 +54,9 @@ export function mountLivePill(doc = document, opts = {}) {
|
|
|
48
54
|
scrim = doc.createElement('div'); scrim.className = 'lv-scrim'; scrim.hidden = true;
|
|
49
55
|
tray = doc.createElement('div'); tray.id = 'czLiveList'; tray.className = 'lv-tray'; tray.hidden = true;
|
|
50
56
|
tray.setAttribute('role', 'dialog'); tray.setAttribute('aria-label', 'What you work on now');
|
|
57
|
+
tray.innerHTML = '<button type="button" class="lv-side"><i></i></button><div class="lv-rows"></div>';
|
|
58
|
+
grabB = tray.querySelector('.lv-side'); rowsEl = tray.querySelector('.lv-rows');
|
|
59
|
+
sideWire();
|
|
51
60
|
nav.append(scrim, tray, pill);
|
|
52
61
|
pill.addEventListener('click', () => set(!L.open));
|
|
53
62
|
scrim.addEventListener('click', () => set(false));
|
|
@@ -113,7 +122,7 @@ export function mountLivePill(doc = document, opts = {}) {
|
|
|
113
122
|
const h = pillHtml(now); if (pill.__h !== h) { pill.__h = h; pill.innerHTML = h; }
|
|
114
123
|
if (L.open && !tray.querySelector('.lv-stop.armed, .lv-stop:disabled')) { // a Stop waiting for its second press keeps the list as it is
|
|
115
124
|
const f = doc.activeElement && tray.contains(doc.activeElement) ? doc.activeElement.dataset.lv : null;
|
|
116
|
-
|
|
125
|
+
rowsEl.innerHTML = rowsHtml(list);
|
|
117
126
|
if (f) { const a = [...tray.querySelectorAll('a[data-lv]')].find((n) => n.dataset.lv === f); if (a) a.focus(); }
|
|
118
127
|
L.shown = list;
|
|
119
128
|
}
|
|
@@ -127,7 +136,7 @@ export function mountLivePill(doc = document, opts = {}) {
|
|
|
127
136
|
pill.setAttribute('aria-expanded', String(open)); pill.classList.toggle('open', open);
|
|
128
137
|
tray.classList.toggle('sheet', open && phone());
|
|
129
138
|
tray.hidden = !open; scrim.hidden = !(open && phone());
|
|
130
|
-
if (open) { L.shown = items(); L.marks = new Map(L.shown.map((x) => [x.id, x.mark]));
|
|
139
|
+
if (open) { L.shown = items(); L.marks = new Map(L.shown.map((x) => [x.id, x.mark])); rowsEl.innerHTML = rowsHtml(L.shown); dockTray(); const a = tray.querySelector('a'); if (a) a.focus(); read(); }
|
|
131
140
|
else { L.shown = []; if (tray.contains(doc.activeElement)) pill.focus(); }
|
|
132
141
|
draw();
|
|
133
142
|
}
|
|
@@ -137,6 +146,44 @@ export function mountLivePill(doc = document, opts = {}) {
|
|
|
137
146
|
if ((down || e.key === 'ArrowUp') && n) { e.preventDefault(); rows[i < 0 ? (down ? 0 : n - 1) : (i + (down ? 1 : n - 1)) % n].focus(); }
|
|
138
147
|
else if (/^[1-5]$/.test(e.key) && !e.ctrlKey && !e.metaKey && !e.altKey) { const a = rows[Number(e.key) - 1]; if (a) { e.preventDefault(); a.click(); } }
|
|
139
148
|
}
|
|
149
|
+
/* the side the list docks on (a desktop): left, above the pill (today), or right, at the window's right edge at the same height */
|
|
150
|
+
function dockTray(side = sideGet()) {
|
|
151
|
+
const right = side === 'right' && !phone();
|
|
152
|
+
tray.classList.toggle('lv-r', right);
|
|
153
|
+
tray.style.bottom = right ? Math.round(innerHeight - pill.getBoundingClientRect().top + 8) + 'px' : '';
|
|
154
|
+
grabB.setAttribute('aria-label', right ? 'Move this list to the left side' : 'Move this list to the right side');
|
|
155
|
+
grabB.title = 'Drag to the left or the right side';
|
|
156
|
+
}
|
|
157
|
+
function sideWire() {
|
|
158
|
+
let g = null;
|
|
159
|
+
grabB.addEventListener('pointerdown', (e) => {
|
|
160
|
+
if (e.button !== 0 || phone()) return;
|
|
161
|
+
e.preventDefault(); grabB.setPointerCapture(e.pointerId);
|
|
162
|
+
g = { x0: e.clientX, on: false, r: tray.getBoundingClientRect() };
|
|
163
|
+
});
|
|
164
|
+
grabB.addEventListener('pointermove', (e) => {
|
|
165
|
+
if (!g) return;
|
|
166
|
+
const dx = e.clientX - g.x0;
|
|
167
|
+
if (!g.on && Math.abs(dx) < 8) return;
|
|
168
|
+
g.on = true; tray.classList.add('lv-moving');
|
|
169
|
+
tray.style.transform = `translateX(${Math.round(Math.max(-g.r.left + 8, Math.min(innerWidth - g.r.right - 8, dx)))}px)`;
|
|
170
|
+
});
|
|
171
|
+
const up = (e, ok) => {
|
|
172
|
+
if (!g) return;
|
|
173
|
+
const was = g; g = null;
|
|
174
|
+
tray.classList.remove('lv-moving'); tray.style.transform = '';
|
|
175
|
+
if (!was.on) return;
|
|
176
|
+
grabB.__drag = Date.now(); // the release is not also a press
|
|
177
|
+
if (ok) { const side = e.clientX > innerWidth / 2 ? 'right' : 'left'; sideSet(side); dockTray(side); }
|
|
178
|
+
};
|
|
179
|
+
grabB.addEventListener('pointerup', (e) => up(e, true));
|
|
180
|
+
grabB.addEventListener('pointercancel', (e) => up(e, false));
|
|
181
|
+
// a press (a mouse with no drag, Enter, Space): the other side
|
|
182
|
+
grabB.addEventListener('click', (e) => {
|
|
183
|
+
if (e.detail && grabB.__drag && Date.now() - grabB.__drag < 400) return; // a key press (detail 0) always moves it
|
|
184
|
+
const side = sideGet() === 'right' ? 'left' : 'right'; sideSet(side); dockTray(side); grabB.focus();
|
|
185
|
+
});
|
|
186
|
+
}
|
|
140
187
|
// the meeting clock ticks once a second while a recording runs; nothing else moves
|
|
141
188
|
function tickRec(list) {
|
|
142
189
|
const run = list.some((x) => x.kind === 'rec' && !x.paused);
|
|
@@ -47,6 +47,7 @@ body.cz-pn-ctl .hm-chat > .hm-reopen { display: none; }
|
|
|
47
47
|
.cz-pn-menu button:hover { background: var(--cz-well); }
|
|
48
48
|
.cz-pn-menu button[aria-checked="true"] { background: var(--cz-bluebg); color: var(--cz-blueink); }
|
|
49
49
|
.cz-pn-menu button:focus-visible { outline: 2px solid var(--cz-blue); outline-offset: 2px; }
|
|
50
|
+
.cz-pn-menu .cz-pn-sep { align-self: stretch; width: 1px; margin: 6px 2px; background: var(--cz-hair); }
|
|
50
51
|
|
|
51
52
|
/* open beside (TbT5DragRow): the row follows the pointer, and the half it will take turns blue */
|
|
52
53
|
.cz-pn-ghost { position: fixed; z-index: 92; pointer-events: none; max-width: 260px; padding: 10px 14px; border-radius: 14px; background: var(--cz-card); color: var(--cz-ink); box-shadow: var(--cz-sheet); font: 600 var(--fs-3, 14px)/1.25 var(--cz-sans); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
@@ -73,7 +74,15 @@ body.cz-pn-dragrow, body.cz-pn-dragrow * { user-select: none !important; cursor:
|
|
|
73
74
|
body.cz-pn-page .cz-pn-div { margin: 0 -11px 0 11px; position: sticky; top: 16px; height: calc(100vh - 32px); align-self: flex-start; }
|
|
74
75
|
body.cz-pn-page .cz-vw.cz-pn-over { position: fixed !important; top: 16px; right: 16px; bottom: 16px; height: auto; z-index: 70; }
|
|
75
76
|
body.cz-pn-page > main.cz-pn-away { display: none !important; }
|
|
77
|
+
/* docked on the left (dock side): it slides over on the left too */
|
|
78
|
+
body.cz-pn-page .cz-vw.cz-pn-over.cz-pn-left { right: auto; left: 16px; animation-name: cz-pn-inl; }
|
|
79
|
+
/* a Viewer's header is its handle for docking: the header shows it can move */
|
|
80
|
+
body.cz-pn-page .cz-vw .cz-vw-hd { cursor: grab; }
|
|
81
|
+
body.cz-pn-page .cz-vw .cz-vw-hd :is(button, input, a):not([data-cz-vw-rn]) { cursor: pointer; }
|
|
82
|
+
body.cz-pn-page .cz-vw .cz-vw-hd [data-cz-vw-rn] { cursor: text; }
|
|
83
|
+
.cz-vw.cz-pn-docking { opacity: .6; }
|
|
76
84
|
}
|
|
85
|
+
@keyframes cz-pn-inl { from { transform: translateX(-32px); opacity: 0; } to { transform: none; opacity: 1; } }
|
|
77
86
|
|
|
78
87
|
@media (prefers-reduced-motion: reduce) { .cz-pn-over { animation: none; } .cz-pn-shut { transition: none; } }
|
|
79
88
|
/* a phone keeps today's one panel: nothing here applies */
|
|
@@ -27,6 +27,12 @@
|
|
|
27
27
|
* Remembered Home: each initiative reopens with its panels and shares (localStorage 'cz-pn:<initiative id>', like Android app pairs).
|
|
28
28
|
* A page: only the divider share (localStorage 'cz-pn:<key>'); the open Viewers are not restored. A private window
|
|
29
29
|
* forgets it, and the page falls back to today's split.
|
|
30
|
+
* Dock side a page (Board, Intelligence): each Viewer docks on the right of the page (as today) or on its left. Drag the Viewer's
|
|
31
|
+
* header (not a press on it) over the left or the right half: that half turns blue, and the drop docks it there. The ⧉
|
|
32
|
+
* menu has the same two places as icons (Dock on the left, Dock on the right), so a keyboard reaches it. A Viewer that
|
|
33
|
+
* slides over slides over on its side. The side is kept per kind of item (localStorage 'cz-pn-side': { kind: 'left' }),
|
|
34
|
+
* so the initiative panel or the chat comes back on the side it was left on after a reload. The divider resizes it on
|
|
35
|
+
* either side (its drag, its keys, a drag to the edge).
|
|
30
36
|
* Present Home: while a recording presents (BroadcastChannel 'pmos-present': present or show), the chat slides away by itself; on
|
|
31
37
|
* Stop (off, or the recording ends) it comes back.
|
|
32
38
|
*
|
|
@@ -40,7 +46,7 @@
|
|
|
40
46
|
* (560 px at first) but never so wide that the page goes under fit(). key: the name the share is kept under ('board', 'intel').
|
|
41
47
|
* desk defaults to the page's parent; a Viewer host (data-cz-viewer-host) is added to it when it has none.
|
|
42
48
|
* window.czPanels = { list(), place(id, 'full'|'split'|'over'), slideOver(item?), openBeside(row|item), putAway(), away(), onChange(fn),
|
|
43
|
-
* scope(id) }
|
|
49
|
+
* scope(id), dock(id, 'left'|'right') → true when it docked (a page only), side(id) → 'left'|'right' }
|
|
44
50
|
* panel ids: 'chat' or 'page' (the first panel), 'v0', 'v1', 'v2' (v0 is #czViewer). list() → [{ id, kind: 'chat'|'page'|'viewer',
|
|
45
51
|
* mode: 'split'|'over'|'away', full, active, title }]. slideOver(item): a Live pill row (item.sid, a session) opens in the chat, and the
|
|
46
52
|
* chat floats over when it is not in the row; a Viewer item ({ kind, title, render | html }) opens in a Viewer that floats over; no
|
|
@@ -59,8 +65,9 @@ const HOME_SRC = '#hmList button[data-hm-sel], #czLiveList a[data-lv], #hmChat [
|
|
|
59
65
|
const IC = {
|
|
60
66
|
full: 'M3.5 5h17v14h-17z', split: 'M3.5 5h17v14h-17zM12 5v14', over: 'M3.5 5h17v14h-17zM14 8h4v8h-4z',
|
|
61
67
|
place: 'M3.5 5h17v14h-17zM12 5v14', close: 'M6 6l12 12M18 6L6 18',
|
|
68
|
+
left: 'M3.5 5h17v14h-17zM10 5v14M5.5 9h2M5.5 12h2', right: 'M3.5 5h17v14h-17zM14 5v14M16.5 9h2M16.5 12h2',
|
|
62
69
|
};
|
|
63
|
-
const NAME = { full: 'Full screen', split: 'Split', over: 'Slide over' };
|
|
70
|
+
const NAME = { full: 'Full screen', split: 'Split', over: 'Slide over', left: 'Dock on the left', right: 'Dock on the right' };
|
|
64
71
|
const svg = (d, w = 18) => `<svg aria-hidden="true" width="${w}" height="${w}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="${d}"/></svg>`;
|
|
65
72
|
const store = {
|
|
66
73
|
get(k) { try { return JSON.parse(localStorage.getItem(k) || 'null'); } catch { return null; } },
|
|
@@ -72,6 +79,27 @@ const maxRow = () => (innerWidth >= WIDE ? 3 : 2);
|
|
|
72
79
|
let C = 'chat';
|
|
73
80
|
const S = { on: false, page: false, desk: null, chat: null, src: HOME_SRC, clicks: '', fitFn: null, key: '', row: ['chat'], over: null, away: [], fr: [], used: {}, subs: [], go: null, reopen: null, pend: null, scope: null, auto: false, restoring: false, keepList: false, noClick: 0, divs: [] };
|
|
74
81
|
|
|
82
|
+
/* ── the side a Viewer docks on (a page): kept per kind of item ── */
|
|
83
|
+
const SIDES = 'cz-pn-side';
|
|
84
|
+
const kindOf = (id) => { const v = P.list().find((p) => p.id === id); return String((v && v.item && v.item.kind) || '').trim().toLowerCase(); };
|
|
85
|
+
/** 'left' or 'right' (today's place). Only a page docks: Home keeps the chat first. */
|
|
86
|
+
function sideOf(id) {
|
|
87
|
+
if (!S.page || !isV(id)) return 'right';
|
|
88
|
+
const m = store.get(SIDES) || {}, k = kindOf(id);
|
|
89
|
+
return k && m[k] === 'left' ? 'left' : 'right';
|
|
90
|
+
}
|
|
91
|
+
/** Dock the Viewer id on a side, keep it for its kind, and lay the panels out again. */
|
|
92
|
+
function dock(id, side) {
|
|
93
|
+
if (!S.on || !S.page || isPhone() || !isV(id) || !known(id) || !['left', 'right'].includes(side)) return false;
|
|
94
|
+
const k = kindOf(id), m = { ...(store.get(SIDES) || {}) };
|
|
95
|
+
if (k) { if (side === 'left') m[k] = 'left'; else delete m[k]; store.set(SIDES, Object.keys(m).length ? m : null); }
|
|
96
|
+
mark(id); S.fr = [];
|
|
97
|
+
layout();
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** The page's own sheet sets its margin with !important (cozy-panels.css), so the gap after a Viewer docked left is set the same way. */
|
|
102
|
+
const gapLeft = (el, on) => { if (on) el.style.setProperty('margin-left', GAP + 'px', 'important'); else el.style.removeProperty('margin-left'); };
|
|
75
103
|
/* ── the panels and their elements ── */
|
|
76
104
|
const isV = (id) => /^v\d$/.test(id || '');
|
|
77
105
|
const vs = () => P.list().filter((p) => p.open);
|
|
@@ -83,7 +111,9 @@ const drop = (id) => { S.row = S.row.filter((x) => x !== id); if (S.over === id)
|
|
|
83
111
|
const mark = (id) => { if (id) S.used[id] = performance.now(); };
|
|
84
112
|
const lru = (ids) => ids.slice().sort((a, b) => (S.used[a] || 0) - (S.used[b] || 0))[0];
|
|
85
113
|
/** Today's split, laid out by cozy-viewer.js: the first panel alone, or it then one Viewer, nothing over and nothing put away. */
|
|
86
|
-
const simple = () => (S.page
|
|
114
|
+
const simple = () => (S.page ? S.row.includes(C) : !S.over && !S.away.length && S.row[0] === C) && S.row.length <= 2;
|
|
115
|
+
/** Today's split on a page with its one Viewer docked on the left. */
|
|
116
|
+
const leftSplit = () => S.page && !managed() && S.row.length === 2 && S.row[1] === C; // a page: a Viewer over leaves the split as it is
|
|
87
117
|
const managed = () => S.on && !isPhone() && !simple();
|
|
88
118
|
const pageW = () => (S.fitFn ? Math.max(0, +S.fitFn() || 0) : 0);
|
|
89
119
|
|
|
@@ -97,7 +127,10 @@ function sync() {
|
|
|
97
127
|
else { const v = [...S.away].reverse().find(isV); if (v) { drop(v); S.row = [v]; } else { drop(C); S.row = [C]; } }
|
|
98
128
|
}
|
|
99
129
|
if (!open.length && !S.row.includes(C)) { drop(C); S.row.unshift(C); } // no Viewer: the chat (or the page) is the page, as today
|
|
100
|
-
if (S.page && S.row.includes(C)
|
|
130
|
+
if (S.page && S.row.includes(C)) { // the Viewers docked left, the page, then the ones docked right
|
|
131
|
+
const want = [...S.row.filter((x) => x !== C && sideOf(x) === 'left'), C, ...S.row.filter((x) => x !== C && sideOf(x) !== 'left')];
|
|
132
|
+
if (want.join() !== S.row.join()) { S.row = want; S.fr = []; }
|
|
133
|
+
}
|
|
101
134
|
fit();
|
|
102
135
|
}
|
|
103
136
|
/** Over the limit (a new panel, a narrower window): the panel used least recently gives way. The chat slides over (or away when a panel
|
|
@@ -138,11 +171,14 @@ function layout() {
|
|
|
138
171
|
el.style.order = !phone && man && at >= 0 ? String(at * 2) : '';
|
|
139
172
|
el.style.flex = !phone && man && at >= 0 ? `${fr[at]} 1 0px` : '';
|
|
140
173
|
if (S.page && id !== C) el.style.marginLeft = !phone && man && at > 0 ? GAP + 'px' : ''; // a page's row has no flex gap (stray body children)
|
|
174
|
+
if (S.page && id === C) gapLeft(el, !phone && (man ? at > 0 : leftSplit())); // the page after a Viewer docked left
|
|
175
|
+
if (S.page && isV(id)) el.classList.toggle('cz-pn-left', !phone && sideOf(id) === 'left');
|
|
141
176
|
grab(el, !phone && S.over === id);
|
|
142
177
|
});
|
|
143
178
|
const host = S.desk.querySelector('[data-cz-viewer-host]');
|
|
144
|
-
|
|
145
|
-
if (host
|
|
179
|
+
const lft = !phone && leftSplit();
|
|
180
|
+
if (host) host.style.order = !phone && !man ? (lft ? '-2' : '2') : '';
|
|
181
|
+
if (host && S.page) host.style.marginLeft = !phone && !man && !lft ? GAP + 'px' : '';
|
|
146
182
|
if (S.chat) S.chat.style.order = !phone && !man ? '0' : S.chat.style.order;
|
|
147
183
|
divs(phone ? 0 : S.row.length - 1, man);
|
|
148
184
|
if (man && !phone && !S.page) {
|
|
@@ -219,7 +255,7 @@ function divs(n, man) {
|
|
|
219
255
|
while (S.divs.length < n) S.divs.push(divider());
|
|
220
256
|
S.divs.forEach((d, k) => {
|
|
221
257
|
d.dataset.k = String(k);
|
|
222
|
-
d.style.order = man ? String(k * 2 + 1) : '1';
|
|
258
|
+
d.style.order = man ? String(k * 2 + 1) : leftSplit() ? '-1' : '1';
|
|
223
259
|
if (d.__first !== (k === 0)) { d.__first = k === 0; d.querySelector('.cz-pn-in').innerHTML = dividerHtml({ first: k === 0 }); }
|
|
224
260
|
if (d.parentNode !== S.desk) S.desk.appendChild(d);
|
|
225
261
|
});
|
|
@@ -257,8 +293,8 @@ function snap(x, g) {
|
|
|
257
293
|
* the share is what is kept (rule 7), not the kind's width. */
|
|
258
294
|
function setAt(k, x, g, keep) {
|
|
259
295
|
if (!managed()) {
|
|
260
|
-
const w = g.R - x - g.gap / 2;
|
|
261
|
-
if (S.page) { if (keep) store.set('cz-pn:' + S.key, { v: 2, share: Math.min(0.95, Math.max(0.05, w / (g.R - g.L - g.gap))) }); P.size(
|
|
296
|
+
const v = isV(g.b) ? g.b : g.a, w = v === g.b ? g.R - x - g.gap / 2 : x - g.L - g.gap / 2; // a Viewer docked left grows to the right
|
|
297
|
+
if (S.page) { if (keep) store.set('cz-pn:' + S.key, { v: 2, share: Math.min(0.95, Math.max(0.05, w / (g.R - g.L - g.gap))) }); P.size(v, w, false); if (keep) layout(); return; }
|
|
262
298
|
P.size(g.b, w, keep); return;
|
|
263
299
|
}
|
|
264
300
|
const wa = Math.max(60, x - g.ra.left - g.gap / 2), wb = Math.max(60, g.rb.right - x - g.gap / 2), fr = shares(), s = fr[k] + fr[k + 1];
|
|
@@ -286,7 +322,7 @@ function dragDiv(d, e) {
|
|
|
286
322
|
d.classList.remove('drag', 'snap'); document.body.classList.remove('cz-pn-drag', 'cz-vw-drag');
|
|
287
323
|
[g.a, g.b].forEach((id) => { const el = elOf(id); if (el) el.classList.remove('cz-pn-shut'); });
|
|
288
324
|
if (!last) return;
|
|
289
|
-
if (last.shut && pinned(last.shut)) place(g.b, 'full'); // the page never closes: the Viewer takes the screen
|
|
325
|
+
if (last.shut && pinned(last.shut)) place(last.shut === g.a ? g.b : g.a, 'full'); // the page never closes: the Viewer takes the screen
|
|
290
326
|
else if (last.shut) shut(last.shut); else setAt(k, last.x, g, true);
|
|
291
327
|
};
|
|
292
328
|
d.addEventListener('pointermove', mv); d.addEventListener('pointerup', up); d.addEventListener('pointercancel', up);
|
|
@@ -326,8 +362,14 @@ function openMenu(id, anchor) {
|
|
|
326
362
|
const now = modeOf(id);
|
|
327
363
|
const m = document.createElement('div');
|
|
328
364
|
m.className = 'cz-pn-menu'; m.setAttribute('role', 'menu'); m.setAttribute('aria-label', 'Arrange'); m.dataset.escLayer = '1';
|
|
329
|
-
|
|
330
|
-
|
|
365
|
+
const btn = (k, on, at) => `<button type="button" role="menuitemradio" aria-checked="${on}" ${at}="${k}" aria-label="${NAME[k]}" title="${NAME[k]}">${svg(IC[k], 20)}</button>`;
|
|
366
|
+
// a page's Viewer also docks on the left or the right of the page: the keyboard's way to what a drag of its header does
|
|
367
|
+
const sides = S.page && isV(id) ? '<i class="cz-pn-sep" aria-hidden="true"></i>' + ['left', 'right'].map((k) => btn(k, sideOf(id) === k, 'data-cz-pn-dock')).join('') : '';
|
|
368
|
+
m.innerHTML = ['full', 'split', 'over'].map((k) => btn(k, now === k, 'data-cz-pn-place')).join('') + sides;
|
|
369
|
+
m.addEventListener('click', (e) => {
|
|
370
|
+
const d = e.target.closest('[data-cz-pn-dock]'); if (d) { closeMenu(false); dock(id, d.dataset.czPnDock); return; }
|
|
371
|
+
const b = e.target.closest('[data-cz-pn-place]'); if (!b) return; closeMenu(false); place(id, b.dataset.czPnPlace);
|
|
372
|
+
});
|
|
331
373
|
m.addEventListener('keydown', (e) => {
|
|
332
374
|
const bs = [...m.querySelectorAll('button')], at = bs.indexOf(document.activeElement);
|
|
333
375
|
if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); closeMenu(true); }
|
|
@@ -464,14 +506,14 @@ function grab(el, want) {
|
|
|
464
506
|
let g = el.querySelector(':scope > .cz-pn-grab');
|
|
465
507
|
if (!want) { if (g) g.remove(); el.style.transform = ''; return; }
|
|
466
508
|
if (g) return;
|
|
467
|
-
g = document.createElement('div'); g.className = 'cz-pn-grab'; g.setAttribute('aria-hidden', 'true'); g.title = 'Drag
|
|
509
|
+
g = document.createElement('div'); g.className = 'cz-pn-grab'; g.setAttribute('aria-hidden', 'true'); g.title = 'Drag off the edge to put it away';
|
|
468
510
|
g.innerHTML = '<i></i>';
|
|
469
511
|
g.addEventListener('pointerdown', (e) => {
|
|
470
512
|
if (e.button !== 0) return;
|
|
471
513
|
e.preventDefault(); g.setPointerCapture(e.pointerId);
|
|
472
|
-
const x0 = e.clientX; let dx = 0;
|
|
514
|
+
const x0 = e.clientX, f = el.classList.contains('cz-pn-left') ? -1 : 1; let dx = 0; // docked left: off the left edge
|
|
473
515
|
el.classList.add('cz-pn-moving');
|
|
474
|
-
const mv = (ev) => { dx = Math.max(0, ev.clientX - x0); el.style.transform = `translateX(${dx}px)`; };
|
|
516
|
+
const mv = (ev) => { dx = Math.max(0, (ev.clientX - x0) * f); el.style.transform = `translateX(${dx * f}px)`; };
|
|
475
517
|
const up = () => {
|
|
476
518
|
g.removeEventListener('pointermove', mv); g.removeEventListener('pointerup', up); g.removeEventListener('pointercancel', up);
|
|
477
519
|
el.classList.remove('cz-pn-moving'); el.style.transform = '';
|
|
@@ -590,6 +632,46 @@ function dragWire() {
|
|
|
590
632
|
}
|
|
591
633
|
const besideSel = () => [S.src, S.clicks].filter(Boolean).join(', ');
|
|
592
634
|
|
|
635
|
+
/* ── dock a Viewer on the left or the right (a page): drag its header; the half it will dock on turns blue ── */
|
|
636
|
+
const NODRAG = 'button.cz-vw-ib, .cz-vw-pri, .cz-vw-chip, .cz-vw-chk, input, textarea, select, a[href], .cz-vw-menu, .cz-vw-trail, .cz-pn-grab';
|
|
637
|
+
let K = null;
|
|
638
|
+
function dockBox(side) {
|
|
639
|
+
const a = area(); if (!a) return null;
|
|
640
|
+
const top = Math.max(a.top, 0), h = Math.min(a.bottom, innerHeight) - top, w = (a.right - a.left) / 2;
|
|
641
|
+
return { left: side === 'left' ? a.left : a.left + w, top, width: w, height: h };
|
|
642
|
+
}
|
|
643
|
+
function dockWire() {
|
|
644
|
+
document.addEventListener('pointerdown', (e) => {
|
|
645
|
+
if (!S.on || !S.page || isPhone() || e.button !== 0 || e.pointerType === 'touch') return;
|
|
646
|
+
const hd = e.target.closest && e.target.closest('.cz-vw-hd'); if (!hd || e.target.closest(NODRAG)) return;
|
|
647
|
+
const p = hd.closest('[data-cz-panel]'); if (!p || !isV(p.dataset.czPanel) || !known(p.dataset.czPanel)) return;
|
|
648
|
+
K = { id: p.dataset.czPanel, el: p, x0: e.clientX, y0: e.clientY, pid: e.pointerId, on: false, side: null };
|
|
649
|
+
}, true);
|
|
650
|
+
document.addEventListener('pointermove', (e) => {
|
|
651
|
+
if (!K || e.pointerId !== K.pid) return;
|
|
652
|
+
if (!K.on) {
|
|
653
|
+
if (Math.hypot(e.clientX - K.x0, e.clientY - K.y0) < 8) return;
|
|
654
|
+
K.on = true; K.zone = document.createElement('div'); K.zone.className = 'cz-pn-drop'; K.zone.hidden = true;
|
|
655
|
+
document.body.appendChild(K.zone); document.body.classList.add('cz-pn-dragrow'); K.el.classList.add('cz-pn-docking');
|
|
656
|
+
}
|
|
657
|
+
e.preventDefault();
|
|
658
|
+
const a = area(); if (!a) return;
|
|
659
|
+
K.side = e.clientX < (a.left + a.right) / 2 ? 'left' : 'right';
|
|
660
|
+
const b = dockBox(K.side); K.zone.hidden = !b;
|
|
661
|
+
if (b) { Object.assign(K.zone.style, { left: b.left + 'px', top: b.top + 'px', width: b.width + 'px', height: b.height + 'px' }); K.zone.dataset.side = K.side; }
|
|
662
|
+
}, true);
|
|
663
|
+
const end = (e, ok) => {
|
|
664
|
+
if (!K || (e && e.pointerId !== K.pid)) return;
|
|
665
|
+
const k = K; K = null;
|
|
666
|
+
if (!k.on) return;
|
|
667
|
+
k.zone.remove(); document.body.classList.remove('cz-pn-dragrow'); k.el.classList.remove('cz-pn-docking');
|
|
668
|
+
S.noClick = Date.now(); // the release over the title is not also its rename press
|
|
669
|
+
if (ok && k.side && k.side !== sideOf(k.id)) dock(k.id, k.side);
|
|
670
|
+
};
|
|
671
|
+
document.addEventListener('pointerup', (e) => end(e, true), true);
|
|
672
|
+
document.addEventListener('pointercancel', (e) => end(e, false), true);
|
|
673
|
+
}
|
|
674
|
+
|
|
593
675
|
/* ── remembered per initiative on Home (rule 7) ── */
|
|
594
676
|
function snapshot() {
|
|
595
677
|
return {
|
|
@@ -675,6 +757,7 @@ export function mountPanels(o = {}) {
|
|
|
675
757
|
if (p) { mark(p.dataset.czPanel); used(p.dataset.czPanel); } else if (S.chat.contains(t)) mark(C);
|
|
676
758
|
}, { capture: true, passive: true }));
|
|
677
759
|
dragWire();
|
|
760
|
+
if (S.page) dockWire();
|
|
678
761
|
let rz = 0;
|
|
679
762
|
addEventListener('resize', () => { cancelAnimationFrame(rz); rz = requestAnimationFrame(() => { fit(null, true); S.keepList = false; layout(); }); });
|
|
680
763
|
if (S.page && typeof ResizeObserver !== 'undefined') new ResizeObserver(() => narrow()).observe(S.chat);
|
|
@@ -686,7 +769,7 @@ export function mountPanels(o = {}) {
|
|
|
686
769
|
onRecording((s) => { if (!s || !s.on) presentOff(); });
|
|
687
770
|
}
|
|
688
771
|
window.czPanels = {
|
|
689
|
-
list, place, slideOver, putAway, scope, openBeside,
|
|
772
|
+
list, place, slideOver, putAway, scope, openBeside, dock, side: sideOf,
|
|
690
773
|
away: lastAway,
|
|
691
774
|
onChange(fn) { if (typeof fn === 'function') S.subs.push(fn); },
|
|
692
775
|
};
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* (this device), Product (which products Home reads: the Products card), Your day (when the end-of-day card shows:
|
|
4
4
|
* time.workEnd, POST /api/settings), Your impact (impact.on, POST /api/settings; canvas GmP1Switch, batch 16 item 7: the one switch
|
|
5
5
|
* for Adler's leaf, the impact log and the weekly check; the page hears `pmos:impact`), Agents write to Jira (fixed: only after Commit)
|
|
6
|
+
* Claude the Claude profile, the API key, and Sharpen prompts: suggestions after a reply, Sharpen as I type, the style and Your own rules
|
|
7
|
+
* (GET/POST /api/next-prompt/prefs; batch 26 item 2)
|
|
6
8
|
* Claude’s checks Grill by itself (Full, Only at stage exits, Off) and the monthly credit for checks with what this month used (grill.mode,
|
|
7
9
|
* grill.cap, POST /api/settings; GET /api/grill; canvas GrillEntry). At the cap Claude pauses and says so once here.
|
|
8
10
|
* Jev and Laya the TypeSafe key, Laya on this computer and the per-repository switches (pm/cozy-jl.js; canvas JlA1Empty–JlA4Phone)
|
|
@@ -17,8 +19,9 @@
|
|
|
17
19
|
* Workspaces the repositories PMOS works in (GET and POST /api/workspaces, POST /api/workspaces/forget; the folder is never touched)
|
|
18
20
|
* Workflows a link to its own page, /premium-workflows.html (pm/wf-page.js, D99): the stages each kind of initiative goes through
|
|
19
21
|
* Updates the versions and Check now, Update, Restart to update (GET /api/kit/status, POST /api/kit/check | update | upgrade-app); opening it
|
|
20
|
-
* asks for the latest kit. Under it, what's new
|
|
21
|
-
* week
|
|
22
|
+
* asks for the latest kit. Under it, what's new (design E, UpE): this week's summary lines, the newest kit at version level,
|
|
23
|
+
* then "Earlier this week" as one flat list of Claude's plain-language stories (a blue dot New, an amber dot Fixed, no dot
|
|
24
|
+
* Improved), and every earlier week folded into an Archive (pm/upd-weeks.js). No PR numbers and no kit ranges.
|
|
22
25
|
* Adler's moments (batch 17 item 4-B, AdM4): one face per section, where Claude speaks. Updates: glasses while it checks or installs,
|
|
23
26
|
* holding the card when a version is ready, calm when up to date. General: holding the update card, else the Jira promise (only after
|
|
24
27
|
* Commit). A failure in the status line takes the honest face, and then the section's face steps back (one Adler per screen).
|
|
@@ -33,7 +36,7 @@ import { esc, svg, ICON, toast, wsParam, wsQ } from './cozy-evui.js';
|
|
|
33
36
|
import { sourceRows, staleOf, partsOf } from './cozy-evmodel.js';
|
|
34
37
|
import * as AD from './cozy-adler.js';
|
|
35
38
|
import * as SE from './cozy-seasons.js';
|
|
36
|
-
import { weeksOf, lineOf,
|
|
39
|
+
import { weeksOf, lineOf, storiesOf } from './upd-weeks.js';
|
|
37
40
|
|
|
38
41
|
/** PUT or DELETE with the page's launch nonce (pressFetch). Only the key row needs them. */
|
|
39
42
|
async function pmSend(method, path, body) {
|
|
@@ -44,7 +47,7 @@ const SECTIONS = [['general', 'General'], ['claude', 'Claude'], ['checks', 'Clau
|
|
|
44
47
|
const PAGE_LINKS = { workflows: '/premium-workflows.html' }; // a section that is a page of its own: the row goes there
|
|
45
48
|
const SRC = { calendar: 'Calendar', wispr: 'Wispr Flow', mail: 'Mail', slack: 'Slack', jira: 'Jira', confluence: 'Confluence', github: 'GitHub', drive: 'Google Drive', figma: 'Figma', website: 'Your websites' };
|
|
46
49
|
const VIEWER_KEY = 'pmos.cz.viewer';
|
|
47
|
-
const T = { el: null, sec: 'general', ws: null, st: {}, src: null, con: {}, conn: [], set: null, prods: null, msg: '', busy: false, phase: '', say: '', back: null, fresh: null, reading: '', note: {}, lines: null, dress: false, more: false, bday: false, dset: {}, wkOpen: new Set(), arc: false };
|
|
50
|
+
const T = { el: null, sec: 'general', ws: null, st: {}, src: null, con: {}, conn: [], set: null, prods: null, msg: '', busy: false, phase: '', say: '', back: null, fresh: null, reading: '', note: {}, lines: null, dress: false, more: false, bday: false, dset: {}, wkOpen: new Set(), wkMore: new Set(), arc: false };
|
|
48
51
|
const cmpVer = (a, b) => { const x = String(a || '0').split('-')[0].split('.').map(Number), y = String(b || '0').split('-')[0].split('.').map(Number); for (let i = 0; i < 3; i++) if ((x[i] || 0) !== (y[i] || 0)) return (x[i] || 0) < (y[i] || 0) ? -1 : 1; return 0; };
|
|
49
52
|
const home = () => (T.ws && T.ws[0] ? T.st[T.ws[0].id] : null);
|
|
50
53
|
const latest = () => { const s = home(); return (s && (s.latest || (s.lastCheck && s.lastCheck.latest))) || null; };
|
|
@@ -211,24 +214,41 @@ function updates() {
|
|
|
211
214
|
const last = s.lastCheck ? 'Last checked ' + new Date(s.lastCheck.at).toLocaleString() + '. ' : '';
|
|
212
215
|
return `<div class="ev-card ev-upd first">${mood ? adFace(mood) : ''}<div class="ev-tx ev-grow">${head}</div>${acts}</div>${kits}<p class="ev-small ev-mut">${esc(last)}${s.checkDaily ? 'PMOS checks when you open Updates and once a day while a page is open; Check now asks again.' : 'PMOS checks only when you press Check now.'}</p>${weeks()}`;
|
|
213
216
|
}
|
|
214
|
-
/**
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
217
|
+
/** Design E (UpE). A story line: a blue dot for New, an amber dot for Fixed, no dot for Improved; Claude's plain title, then the sentence. */
|
|
218
|
+
const SPARK = 'M12 3l1.8 5.2L19 10l-5.2 1.8L12 17l-1.8-5.2L5 10l5.2-1.8z';
|
|
219
|
+
const DOT = { new: ['you', 'New'], fix: ['risk', 'Fixed'] };
|
|
220
|
+
function story(p) {
|
|
221
|
+
const x = lineOf(p), d = DOT[p.k];
|
|
222
|
+
return `<li class="ev-sy"><span class="ev-dc">${d ? `<i class="ev-dot ${d[0]}" title="${d[1]}"></i><span class="ev-vh">${d[1]}: </span>` : ''}</span><span class="ev-sx">${x.b ? `<b>${esc(x.b)}</b>` : ''}${x.b && x.s ? '<br>' : ''}${esc(x.s)}</span></li>`;
|
|
219
223
|
}
|
|
220
|
-
|
|
224
|
+
/** A flat list of stories, the first SHOWN of them, then a "N more" row that opens the rest. */
|
|
225
|
+
const SHOWN = 7;
|
|
226
|
+
function storyList(list, key) {
|
|
227
|
+
const all = T.wkMore.has(key), vis = all ? list : list.slice(0, SHOWN), rest = list.length - vis.length;
|
|
228
|
+
return `<ul class="ev-sl">${vis.map(story).join('')}</ul>${rest > 0 ? `<button type="button" class="ev-more" data-ev-more="${esc(key)}">${rest} more</button>` : ''}`;
|
|
229
|
+
}
|
|
230
|
+
const sumCard = (w) => (w.sum.length ? `<div class="ev-sum">${w.sum.map((l) => `<p>${svg(SPARK, 14)}<span>${esc(l)}</span></p>`).join('')}</div>` : '');
|
|
231
|
+
const changesOf = (w) => w.count + (w.count === 1 ? ' change' : ' changes');
|
|
232
|
+
/** The newest week: summary lines, the newest kit at version level, then "Earlier this week" as one flat list. */
|
|
233
|
+
function weekTop(w) {
|
|
234
|
+
const { kit, latest: top, earlier } = storiesOf(w), dots = w.groups.new.length || w.groups.fix.length;
|
|
235
|
+
const legend = dots ? '<div class="ev-leg" aria-hidden="true"><span><i class="ev-dot you"></i>New</span><span><i class="ev-dot risk"></i>Fixed</span><span>no dot: improved</span></div>' : '';
|
|
236
|
+
const kitRow = top.length ? `<div class="ev-rel"><div class="ev-rv"><b>${esc(kit)}</b>${kit === latest() ? '<span class="ev-small">latest</span>' : ''}</div><ul class="ev-sl">${top.map(story).join('')}</ul></div>` : '';
|
|
237
|
+
const more = earlier.length ? `<div class="ev-grp"><div class="ev-gh"><b>Earlier ${w.name === 'This week' ? 'this' : 'that'} week</b><span class="ev-small ev-mut">${earlier.length}</span></div>${storyList(earlier, w.key)}</div>` : '';
|
|
238
|
+
return sumCard(w) + legend + kitRow + more;
|
|
239
|
+
}
|
|
240
|
+
/** An archived week: its newest kit's stories, then the rest with the highlights first. */
|
|
241
|
+
const archList = (x) => { const o = storiesOf(x); return [...o.latest, ...o.earlier]; };
|
|
221
242
|
/** What's new, under the update card: this week (the newest week) open, and every earlier week folded into the Archive. */
|
|
222
243
|
function weeks() {
|
|
223
244
|
const s = home(), mine = s && s.app && !fromGit() ? s.app.version : null;
|
|
224
245
|
const W = weeksOf(changes(), Date.now(), mine);
|
|
225
246
|
if (!W.length) return '<p class="ev-small ev-mut">No kit summaries yet. Each kit published from now on carries one, and Check now reads the newest ones.</p>';
|
|
226
247
|
const [w, ...old] = W.slice(0, 13);
|
|
227
|
-
const head =
|
|
228
|
-
const
|
|
229
|
-
const rows = old.map((x) => { const on = T.wkOpen.has(x.key); return `<div class="ev-wkr"><button type="button" class="ev-wkt" data-ev-wk="${esc(x.key)}" aria-expanded="${on}"><b>${esc(x.name)}</b><span class="ev-small ev-mut">${esc(x.range)}</span><span class="ev-small ev-mut ev-tl">${esc(tally(x))}</span>${svg(ICON.down, 14)}</button>${on ? weekBody(x) : ''}</div>`; }).join('');
|
|
248
|
+
const head = `<div class="ev-wkh"><b>${esc(w.name)}</b><span class="ev-small ev-mut">${esc([w.range, changesOf(w)].filter(Boolean).join(' · '))}</span>${w.newer ? '<span class="ev-pill y">Ready to install</span>' : ''}</div>`;
|
|
249
|
+
const rows = old.map((x) => { const on = T.wkOpen.has(x.key); return `<div class="ev-wkr"><button type="button" class="ev-wkt" data-ev-wk="${esc(x.key)}" aria-expanded="${on}"><b>${esc(x.name)}</b><span class="ev-small ev-mut">${esc(x.range)}</span><span class="ev-small ev-mut ev-tl">${esc(changesOf(x))}</span>${svg(ICON.down, 14)}</button>${on ? sumCard(x) + storyList(archList(x), x.key) : ''}</div>`; }).join('');
|
|
230
250
|
const arc = old.length ? `<div class="ev-arc"><button type="button" class="ev-arcb" data-ev-arc aria-expanded="${T.arc}"><b>Archive</b><span class="ev-small ev-mut">${old.length} earlier ${old.length === 1 ? 'week' : 'weeks'}</span>${svg(ICON.down, 14)}</button>${T.arc ? rows : ''}</div>` : '';
|
|
231
|
-
return `<div class="ev-wk">${head
|
|
251
|
+
return `<div class="ev-wk">${head}${weekTop(w)}${arc}</div>`;
|
|
232
252
|
}
|
|
233
253
|
/* Settings > Claude: which Claude profile PMOS starts Claude sessions with, and the API key for Claude's own runs (D101).
|
|
234
254
|
* The key is typed here and goes to this computer only; the page never gets it back, only whether one is set and where from. */
|
|
@@ -244,7 +264,35 @@ function claudeSec() {
|
|
|
244
264
|
const keyRight = `<span class="ev-dr-pl"><label class="ev-vh" for="evKey">Anthropic API key</label><input id="evKey" class="ev-inp" type="password" autocomplete="off" spellcheck="false" placeholder="${k.present ? 'Replace the key' : 'sk-ant-\u2026'}" data-ev-key><button type="button" class="ev-btn sm pri" data-ev-keysave>Save</button>${k.source === 'settings' ? '<button type="button" class="ev-btn sm" data-ev-keyclear>Remove</button>' : ''}</span>`;
|
|
245
265
|
return '<h4>Claude login and profile</h4><div data-claude-setup></div>'
|
|
246
266
|
+ trow('API key', keySub, keyRight)
|
|
247
|
-
+ (k.present && C.off ? trow('Runs on the API', 'Off: PMOS_GRILL_API is off in the environment.', '<span class="ev-pill">Off</span>') : '')
|
|
267
|
+
+ (k.present && C.off ? trow('Runs on the API', 'Off: PMOS_GRILL_API is off in the environment.', '<span class="ev-pill">Off</span>') : '')
|
|
268
|
+
+ sharpenRows(k.present);
|
|
269
|
+
}
|
|
270
|
+
/* Settings › Claude › Sharpen prompts (batch 26 item 2; pm/next-prompt-ui.js, lib/routes-next-prompt.js): the suggestions after a reply, Sharpen as I
|
|
271
|
+
* type ('auto' is on when Claude runs on the API key, where a sharpen takes about 2 s), the style, and the PM's own rules for Your own.
|
|
272
|
+
* The same style and switch are in the chat's Sharper card menu; a save here tells the chat (the event pmos:prompt-prefs). */
|
|
273
|
+
const NPK = { 'prompt.next': ['nextSet', 'next'], 'prompt.sharpen': ['sharpenSet', 'sharpen'], 'prompt.style': ['style', 'style'], 'prompt.rules': ['rulesText', 'rules'] }; // row key → [GET field, POST key]
|
|
274
|
+
const setVal = (k, d) => { const v = T.np ? T.np[NPK[k][0]] : undefined; return v !== undefined && v !== null ? v : d; };
|
|
275
|
+
function sharpenRows(api) {
|
|
276
|
+
const next = setVal('prompt.next', true) !== false, sh = setVal('prompt.sharpen', 'auto'), style = setVal('prompt.style', 'engineer'), rules = String(setVal('prompt.rules', '') || '');
|
|
277
|
+
const on = sh === 'on' || (sh === 'auto' && api);
|
|
278
|
+
const shSub = sh === 'auto' ? (api ? 'On, because Claude runs on your API key: the sharper version shows in about 2 seconds.' : 'Off for now: without an API key each one takes 10 seconds or more. Turn it on to use it anyway.')
|
|
279
|
+
: on ? 'When you pause on a rough prompt, a sharper version shows above the box.' : 'Off. No spinner and no Sharper card.';
|
|
280
|
+
const styleSub = style === 'polish' ? 'Clearer and shorter, your meaning kept.' : style === 'own' ? (rules.trim() ? 'Your rules below.' : 'Write your rules below first.') : 'Builds the best prompt for the job.';
|
|
281
|
+
return '<h4>Sharpen prompts</h4>'
|
|
282
|
+
+ trow('Suggest the next prompt', 'After a reply, up to two prompts above the box. A tap puts one in the box. Nothing sends.', sw(next, 'data-ev-np="prompt.next"', 'Suggest the next prompt'))
|
|
283
|
+
+ trow('Sharpen as I type', shSub, sw(on, 'data-ev-np="prompt.sharpen"', 'Sharpen as I type'))
|
|
284
|
+
+ trow('Sharpen as', styleSub, seg('Sharpen as', 'prompt.style', style, [['polish', 'Polish'], ['engineer', 'Prompt engineer'], ['own', 'Your own']]))
|
|
285
|
+
+ `<div class="ev-trow"><div class="ev-tx"><label for="evRules"><b>Your own rules</b></label><p class="ev-small ev-mut">For Your own. For example: short, always name the initiative, ask for sources.</p>`
|
|
286
|
+
+ `<textarea id="evRules" class="ev-inp ev-rules" rows="3" maxlength="1000" spellcheck="true">${esc(rules)}</textarea></div><button type="button" class="ev-btn sm pri" data-ev-rulesave>Save</button></div>`;
|
|
287
|
+
}
|
|
288
|
+
async function savePrompt(key, value, said) {
|
|
289
|
+
const r = await pmPost('/api/next-prompt/prefs', { key: NPK[key][1], value });
|
|
290
|
+
if (r && r.ok) {
|
|
291
|
+
T.np = { ...(T.np || {}), [NPK[key][0]]: value };
|
|
292
|
+
document.dispatchEvent(new CustomEvent('pmos:prompt-prefs', { detail: { key, value } }));
|
|
293
|
+
}
|
|
294
|
+
T.msg = r && r.ok ? said : (r && r.error) || 'Not saved.';
|
|
295
|
+
draw();
|
|
248
296
|
}
|
|
249
297
|
/* Settings > Claude's checks (GrillEntry): Claude grills by itself, so this is the only control: when, and how much it may spend a month */
|
|
250
298
|
const CAPS = [5, 10, 20, 50];
|
|
@@ -315,6 +363,7 @@ async function press(e) {
|
|
|
315
363
|
if ('evClose' in t.dataset) { closeSettings(); return; }
|
|
316
364
|
if (t.dataset.evWk) { const k = t.dataset.evWk; if (T.wkOpen.has(k)) T.wkOpen.delete(k); else T.wkOpen.add(k); draw(); return; }
|
|
317
365
|
if ('evArc' in t.dataset) { T.arc = !T.arc; draw(); return; }
|
|
366
|
+
if (t.dataset.evMore) { T.wkMore.add(t.dataset.evMore); draw(); return; }
|
|
318
367
|
if (t.dataset.evGo) { T.sec = t.dataset.evGo; draw(); return; }
|
|
319
368
|
if (t.dataset.evSeg === 'theme') { const M = await import('./cozy-nav.js'); M.setTheme(t.dataset.v); draw(); return; }
|
|
320
369
|
if (t.dataset.evSeg && t.dataset.evSeg.startsWith('grill.')) {
|
|
@@ -324,6 +373,10 @@ async function press(e) {
|
|
|
324
373
|
T.msg = r && r.ok ? (key === 'grill.mode' ? (value === 'off' ? 'Saved. Claude no longer grills by itself.' : value === 'exits' ? 'Saved. Claude grills only before a stage ends.' : 'Saved. Claude grills by itself when it matters.') : 'Saved. Claude’s checks spend at most ' + usd(value) + ' a month.') : (r && r.error) || 'Not saved.';
|
|
325
374
|
draw(); return;
|
|
326
375
|
}
|
|
376
|
+
if (t.dataset.evSeg === 'prompt.style') { await savePrompt('prompt.style', t.dataset.v, 'Saved. Sharper prompts follow this style.'); return; }
|
|
377
|
+
if (t.dataset.evNp === 'prompt.next') { const on = setVal('prompt.next', true) === false; await savePrompt('prompt.next', on, on ? 'Saved. Claude suggests the next prompt after a reply.' : 'Saved. No suggestions after a reply.'); return; }
|
|
378
|
+
if (t.dataset.evNp === 'prompt.sharpen') { const api = !!(T.cl && T.cl.key && T.cl.key.present), sh = setVal('prompt.sharpen', 'auto'), was = sh === 'on' || (sh === 'auto' && api); await savePrompt('prompt.sharpen', was ? 'off' : 'on', was ? 'Saved. Sharpen as I type is off.' : 'Saved. Sharpen as I type is on.'); return; }
|
|
379
|
+
if ('evRulesave' in t.dataset) { const ta = T.el.querySelector('#evRules'); await savePrompt('prompt.rules', ta ? ta.value.slice(0, 1000) : '', 'Saved. Your own rules are used when Sharpen as is Your own.'); return; }
|
|
327
380
|
if (t.dataset.evSeg && t.dataset.evSeg.startsWith('adler.')) { const r = await AD.savePref(pmPost, t.dataset.evSeg, t.dataset.v); T.msg = r && r.ok ? 'Saved.' : (r && r.error) || 'Not saved.'; draw(); return; }
|
|
328
381
|
if (t.dataset.evDress) {
|
|
329
382
|
const a = t.dataset.evDress;
|
|
@@ -440,6 +493,7 @@ async function readAgain(id) {
|
|
|
440
493
|
async function load() {
|
|
441
494
|
readFresh();
|
|
442
495
|
pmGet('/api/grill').then((g) => { T.gr = g && g.ok ? g : { ok: false, error: (g && g.error) || '' }; draw(); }).catch(() => { T.gr = { ok: false }; draw(); });
|
|
496
|
+
pmGet('/api/next-prompt/prefs').then((n) => { T.np = n && n.ok ? n : null; draw(); }).catch(() => { T.np = null; });
|
|
443
497
|
pmGet('/api/profiles').then(async (pr) => { const k = await pmGet('/api/anthropic'); T.cl = { profiles: pr && Array.isArray(pr.profiles) ? pr.profiles : [], prefs: (pr && pr.prefs) || {}, perr: pr && pr.ok === false ? (pr.error || 'the server did not answer') : '', key: k && k.ok ? k.key : { present: false, source: null }, off: !!(k && k.off) }; draw(); }).catch(() => { T.cl = { profiles: [], prefs: {}, perr: 'the server did not answer', key: { present: false, source: null } }; draw(); });
|
|
444
498
|
const [w, p, s, b] = await Promise.all([pmGet('/api/workspaces'), pmGet('/api/proactive/settings'), pmGet('/api/settings'), pmGet('/api/briefing-premium')]);
|
|
445
499
|
T.ws = w && Array.isArray(w.workspaces) ? w.workspaces : [];
|
|
@@ -458,7 +512,7 @@ async function load() {
|
|
|
458
512
|
export function openSettings(section) {
|
|
459
513
|
const el = mount();
|
|
460
514
|
if (section === 'dress') { T.sec = 'adler'; T.dress = true; } else if (section && BODY[section]) { T.sec = section; T.dress = false; }
|
|
461
|
-
if (el.hidden) { const ae = document.activeElement; T.back = ae && ae !== document.body ? ae : null; T.ws = null; T.src = null; T.msg = ''; T.phase = ''; T.auto = false; T.wkOpen = new Set(); T.arc = false; T.fresh = null; T.note = {}; T.dset = {}; T.cl = null; T.gr = null; JL.reset(); }
|
|
515
|
+
if (el.hidden) { const ae = document.activeElement; T.back = ae && ae !== document.body ? ae : null; T.ws = null; T.src = null; T.msg = ''; T.phase = ''; T.auto = false; T.wkOpen = new Set(); T.wkMore = new Set(); T.arc = false; T.fresh = null; T.note = {}; T.dset = {}; T.cl = null; T.gr = null; T.np = null; JL.reset(); }
|
|
462
516
|
el.hidden = false; document.getElementById('evStScrim').hidden = false; el.setAttribute('data-esc-layer', '');
|
|
463
517
|
draw();
|
|
464
518
|
const f = el.querySelector('[aria-current="page"]'); if (f) f.focus();
|
|
@@ -44,6 +44,9 @@
|
|
|
44
44
|
* nothing leaves until the caller runs go() (canvas MsB5Leave: Save or Discard). false or none: it leaves at once.
|
|
45
45
|
* render called with the body element to draw into; html is the same as a string. Neither: the body is left empty for the caller.
|
|
46
46
|
* id optional; current() returns it, so a caller can tell whether its item is the one shown
|
|
47
|
+
* outward true: a press inside this Viewer opens what it names in the other Viewer used last, not here (a chat panel, as Home's chat
|
|
48
|
+
* column opens into the Viewer); with no other Viewer it drills in here
|
|
49
|
+
* into a panel id ('v0'..'v2'): the item goes in that Viewer (the Board's one chat panel changes its conversation in place)
|
|
47
50
|
* onClose called once when this item leaves the card (Close, Escape, close(), or another open())
|
|
48
51
|
* close() · back() · forward() · isOpen() · current() → { id, kind, title, url } | null · body() → the body element or null
|
|
49
52
|
* drill() the next open() keeps the item shown now behind it, as back: true does (a page opens a task or a document from its own
|
|
@@ -117,7 +120,7 @@ const kindKey = (k) => String(k || 'item').trim().toLowerCase().replace(/^file
|
|
|
117
120
|
* key or the last open() went to, so the exported API always speaks of the Viewer used last. G is what the page has once: its list's rail,
|
|
118
121
|
* the listeners, the rail's card. Only the first instance (#czViewer) writes the browser history; another keeps its own ← → trail.
|
|
119
122
|
* Without cozy-panels.js (Board, Intelligence) the limit stays 1 and there is one Viewer, as before. */
|
|
120
|
-
const G = { subs: [], card: null, railKey: null, rail: false, lw: 0, hold: 0, tick: 0, limit: 1, beside: null, place: null, managed: null, own: null };
|
|
123
|
+
const G = { chat: null, subs: [], card: null, railKey: null, rail: false, lw: 0, hold: 0, tick: 0, limit: 1, beside: null, place: null, managed: null, own: null };
|
|
121
124
|
/** The list's fold, when the page owns it (Home, batch 20 item 6): { folded() → bool, show() }. Then only the list's own icon folds or unfolds it:
|
|
122
125
|
* a Viewer's width, its reset, open wide and its close never do. null: the Viewer folds the list to a rail as it needs (a page without one). */
|
|
123
126
|
export function listFold(o) { G.own = o && typeof o.folded === 'function' ? o : null; }
|
|
@@ -145,7 +148,12 @@ function pick(o) {
|
|
|
145
148
|
return ALL.filter(shown).sort((a, c) => a.used - c.used)[0];
|
|
146
149
|
}
|
|
147
150
|
const p = H.press && Date.now() - H.press.t < 10000 ? H.press : null;
|
|
148
|
-
if (p && p.inst && shown(p.inst) && !o.fresh)
|
|
151
|
+
if (p && p.inst && shown(p.inst) && !o.fresh) {
|
|
152
|
+
// a Viewer that holds a chat (outward: the Board's chat panel) opens what it names in the other Viewer, as Home's chat column does;
|
|
153
|
+
// with no other Viewer it drills in, and ← comes back to the chat
|
|
154
|
+
const other = p.inst.cur && p.inst.cur.args && p.inst.cur.args.outward ? ALL.filter((x) => shown(x) && x !== p.inst).sort((a, c) => c.used - a.used)[0] : null;
|
|
155
|
+
return other || p.inst;
|
|
156
|
+
}
|
|
149
157
|
return mru() || ALL[0];
|
|
150
158
|
}
|
|
151
159
|
|
|
@@ -431,10 +439,14 @@ function moreMenu(b) {
|
|
|
431
439
|
export const aboutPrefix = (k) => /initiative/.test(k) ? 'init' : /(^|-)(doc|document|prd|brd|spec|file|page)(-|$)/.test(k) ? 'doc'
|
|
432
440
|
: /(task|work|epic|ticket|item)/.test(k) ? 'work' : /(insight|goal|metric|measure|report|chart|dashboard)/.test(k) ? 'ins'
|
|
433
441
|
: /(program|board|stage|lane|release)/.test(k) ? 'board' : 'chat';
|
|
442
|
+
/** A page that opens the chat itself (the Board, batch 26 item 4): fn({ id, kind, title }) → true when it opened the chat about the item
|
|
443
|
+
* on the page, false to go to Home as before. Only for discuss: true, and not on a phone. */
|
|
444
|
+
export function chatHere(fn) { G.chat = typeof fn === 'function' ? fn : null; }
|
|
434
445
|
function discuss() {
|
|
435
446
|
const d = V.cur && V.cur.discuss; if (!d) return;
|
|
436
447
|
if (typeof d === 'function') { d(); return; }
|
|
437
448
|
if (d && d.href) { location.href = d.href; return; }
|
|
449
|
+
if (G.chat && !isPhone() && G.chat({ id: V.cur.id, kind: V.cur.kind, title: V.cur.title }) !== false) return;
|
|
438
450
|
// Home opens the chat about it, with the item in the Viewer beside it (XvABoardChat): ?about=<prefix>:<id> is the chat's key, and ?vk/?vid
|
|
439
451
|
// carry the item's own kind and id, so Home can reopen it (an initiative, a document by its path, a work-item)
|
|
440
452
|
const k = kindKey(V.cur.kind), raw = String(V.cur.id || V.cur.title);
|
|
@@ -572,7 +584,7 @@ function retitle0(id, title) { if (V.cur && V.cur.id === String(id) && title) {
|
|
|
572
584
|
/** Show one item in the Viewer, in place of the one shown. Returns the body element the content is in. */
|
|
573
585
|
export function open(it) {
|
|
574
586
|
const o = it || {};
|
|
575
|
-
use(o._inst || pick(o));
|
|
587
|
+
use(o._inst || (o.into && byP(o.into)) || pick(o)); // into: a panel id ('v1'), the Viewer that shows it (the Board's chat panel)
|
|
576
588
|
// an item with unsaved changes asks first; the open goes on when it says so (and keeps a drill-in asked for before it)
|
|
577
589
|
if (!o._asked && V.cur && typeof V.cur.guard === 'function') {
|
|
578
590
|
const dr = V.drill; let now = true, ran = false;
|