create-pmos 0.79.0 → 0.81.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 +1 -1
- 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/scripts/workflow-state.js +2 -2
- package/payload/state/discoveries.md +1 -1
- package/workspace/guide/layout.md +3 -1
- package/workspace/guide/notes.md +9 -5
- package/workspace/lib/engine/workflow-state.js +2 -2
- package/workspace/lib/notes-final.js +93 -13
- package/workspace/lib/notes-qwen.js +182 -0
- package/workspace/lib/notes-qwen.py +62 -0
- package/workspace/lib/notes-whisper.js +3 -2
- package/workspace/lib/routes-notes.js +21 -3
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/app.js +3 -1
- package/workspace/public/pm/components.css +61 -56
- package/workspace/public/pm/dest-today.js +3 -2
- package/workspace/public/pm/dom.js +60 -0
- package/workspace/public/pm/roadmap.js +2 -2
- package/workspace/public/pm/topbar.js +4 -1
- package/workspace/public/pm/view-notes.js +9 -7
- package/workspace/run-tests.js +5 -5
- package/workspace/test-lists/meeting-notes.json +5 -3
- package/workspace/test-timings.json +1 -0
|
@@ -61,6 +61,7 @@ const whisper = require('./notes-whisper.js');
|
|
|
61
61
|
const notesRepo = require('./notes-repo.js');
|
|
62
62
|
const notesAudio = require('./notes-audio.js');
|
|
63
63
|
const notesFinal = require('./notes-final.js');
|
|
64
|
+
const notesQwen = require('./notes-qwen.js');
|
|
64
65
|
const notesLinks = require('./notes-links.js');
|
|
65
66
|
const jevNotes = require('./jev-notes.js');
|
|
66
67
|
const tsSettings = require('./typesafe-settings.js');
|
|
@@ -283,7 +284,20 @@ function vocabFor(ctx, id) {
|
|
|
283
284
|
return text;
|
|
284
285
|
}
|
|
285
286
|
|
|
286
|
-
/* ── local
|
|
287
|
+
/* ── local transcription: one transcriber for the process ─────────────────────────────────────── */
|
|
288
|
+
/* THE LIVE ENGINE (Run 10). Qwen3-ASR when it is set up (lib/notes-qwen.js: more accurate, Chinese and Indonesian too, kept
|
|
289
|
+
* loaded so each cut is quick), else whisper.cpp. ctx.notesQwenDetect and ctx.notesQwenRun replace them in the suites. */
|
|
290
|
+
function qwenDetect(ctx) { return ctx.notesQwenDetect ? ctx.notesQwenDetect() : notesQwen.detect(ctx.runtimeDir); }
|
|
291
|
+
function liveDetect(ctx) {
|
|
292
|
+
const q = qwenDetect(ctx);
|
|
293
|
+
if (q.ready) return { ready: true, engine: 'qwen', model: q.live, bin: null };
|
|
294
|
+
return { ...whisper.detect(ctx.runtimeDir), engine: 'whisper' };
|
|
295
|
+
}
|
|
296
|
+
function liveRun(ctx, args) {
|
|
297
|
+
if (args.engine !== 'qwen') return whisper.runWhisper(args);
|
|
298
|
+
if (ctx.notesQwenRun) return ctx.notesQwenRun(args);
|
|
299
|
+
return notesQwen.runQwen(notesQwen.workerFor(ctx.runtimeDir), args);
|
|
300
|
+
}
|
|
287
301
|
let transcriber = null; let transcriberDir = null;
|
|
288
302
|
const audioErrors = new Map(); // note id → the last thing that went wrong, for the page
|
|
289
303
|
function transcriberFor(ctx) {
|
|
@@ -291,6 +305,8 @@ function transcriberFor(ctx) {
|
|
|
291
305
|
transcriberDir = ctx.runtimeDir;
|
|
292
306
|
transcriber = whisper.createTranscriber({
|
|
293
307
|
runtimeDir: ctx.runtimeDir,
|
|
308
|
+
detect: () => liveDetect(ctx),
|
|
309
|
+
run: (args) => liveRun(ctx, args),
|
|
294
310
|
vocab: ({ id }) => vocabFor(ctx, id),
|
|
295
311
|
onText: async (c) => {
|
|
296
312
|
const r = notes.appendTranscript(ctx.runtimeDir, { id: c.id, text: c.text, source: c.source, t: c.t, ms: c.ms });
|
|
@@ -302,7 +318,8 @@ function transcriberFor(ctx) {
|
|
|
302
318
|
}
|
|
303
319
|
function engineInfo(ctx, w) {
|
|
304
320
|
const d = whisper.detect(ctx.runtimeDir);
|
|
305
|
-
const
|
|
321
|
+
const live = liveDetect(ctx);
|
|
322
|
+
const out = { ready: live.ready, live: live.ready ? live.engine : null, missing: d.missing, setup: d.setup, model_dir: d.model_dir, model_size: d.model_size,
|
|
306
323
|
language: whisper.getLanguage(ctx.runtimeDir), languages: whisper.LANGUAGES.map(([value, label]) => ({ value, label })),
|
|
307
324
|
model: whisper.getModel(ctx.runtimeDir), models: whisper.MODELS.map((m) => ({ value: m.id, label: m.label })),
|
|
308
325
|
accuracy: whisper.getAccuracy(ctx.runtimeDir), accuracies: whisper.ACCURACIES.map((a) => ({ value: a.id, label: a.label })), keep_audio: notesAudio.getKeep(ctx.runtimeDir),
|
|
@@ -336,6 +353,7 @@ function finalFor(ctx) {
|
|
|
336
353
|
runtimeDir: ctx.runtimeDir,
|
|
337
354
|
...(ctx.notesFinalRun ? { run: ctx.notesFinalRun } : {}),
|
|
338
355
|
...(ctx.notesFinalDetect ? { detect: ctx.notesFinalDetect } : {}),
|
|
356
|
+
...(ctx.notesQwenRun ? { qwen: ctx.notesQwenRun } : {}),
|
|
339
357
|
onState: (id, st) => { if (st.state !== 'done') notes.setFinalState(ctx.runtimeDir, { id, state: st }); },
|
|
340
358
|
onDone: (id, final) => {
|
|
341
359
|
const dir = ctx.runtimeDir;
|
|
@@ -564,7 +582,7 @@ async function handle(req, res, url, ctx) {
|
|
|
564
582
|
if (b64.length > Math.ceil(MAX_PCM_BYTES / 3) * 4) return done({ ok: false, status: 413, error: 'that audio chunk is too long' });
|
|
565
583
|
const pcm = Buffer.from(b64, 'base64');
|
|
566
584
|
if (pcm.length % 2) return done({ ok: false, status: 400, error: 'the audio must be 16-bit samples' });
|
|
567
|
-
if (!
|
|
585
|
+
if (!liveDetect(ctx).ready) return done({ ok: false, status: 409, error: 'local transcription is not set up' });
|
|
568
586
|
const ms = (pcm.length / 2 / whisper.RATE) * 1000;
|
|
569
587
|
const t = Math.max(0, notes.meetingMs(note.note) - ms);
|
|
570
588
|
// the audio is kept for replay unless the PM switched that off (D92); a private note never reaches this line
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.81.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "pmos-workspace",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.81.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/workspace/package.json
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
* the shell still comes up.
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
|
-
import { el, clear, setChildren } from './dom.js';
|
|
33
|
+
import { el, clear, setChildren, keepScroll } from './dom.js';
|
|
34
34
|
import { state, setState, subscribe, flush, FIXTURE, LEGACY_LAYOUT_FLAG } from './state.js';
|
|
35
35
|
/* The phone surface (≤760px). It owns its own chrome and decides which single panel is visible;
|
|
36
36
|
* the shell's only part in it is to keep all four hosts mounted below the breakpoint and to hand
|
|
@@ -563,6 +563,8 @@ let frDetail = null;
|
|
|
563
563
|
const hostMain = el('div', { class: 'fr-main', 'data-fr': 'main' });
|
|
564
564
|
const destHosts = {};
|
|
565
565
|
for (const id of FRAME_DESTS) destHosts[id] = el('div', { class: 'fr-dest', 'data-fr-host': id, hidden: true });
|
|
566
|
+
// each place keeps its reading position when it redraws through a short state (dom.js keepScroll)
|
|
567
|
+
for (const id of FRAME_DESTS) keepScroll(destHosts[id]);
|
|
566
568
|
const frEnd = el('span', { class: 'fr-end', 'aria-hidden': 'true' });
|
|
567
569
|
|
|
568
570
|
function setupFrame() {
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
* primitives `.pmc-*` — written only by primitives.js and icons.js (R28-9)
|
|
6
6
|
* layouts `.bd-*` — the Breakdown's own arrangement of those primitives
|
|
7
7
|
* Every selector starts with `.pmc-` or `.bd-`. Colours are tokens.css variables only; there is no hex and no oklch here.
|
|
8
|
-
* Radius
|
|
8
|
+
* Radius (R27 as amended by the PM 2026-10-07): 6px on controls (buttons, inputs, selects, tabs, segmented controls,
|
|
9
|
+
* badges, menu items, checkboxes), 8px on cards, sheets, dialogs, menus, tiles and panels; a dot (50% or a pill) stays round.
|
|
10
|
+
* Buttons, tabs and segmented controls use sentence case; small non-interactive eyebrows may stay uppercase at 0.04em. Hover rules sit inside (hover: hover). Under a coarse pointer every
|
|
9
11
|
* control is at least 44px and every row and card at least 48px.
|
|
10
12
|
*/
|
|
11
13
|
|
|
@@ -53,15 +55,13 @@
|
|
|
53
55
|
min-height: 32px;
|
|
54
56
|
padding: 0 14px;
|
|
55
57
|
border: 1px solid var(--border);
|
|
56
|
-
border-radius:
|
|
58
|
+
border-radius: 6px;
|
|
57
59
|
background: transparent;
|
|
58
60
|
color: var(--foreground);
|
|
59
61
|
font-family: var(--sans);
|
|
60
62
|
font-size: 12px;
|
|
61
63
|
line-height: 1;
|
|
62
|
-
font-weight:
|
|
63
|
-
letter-spacing: 0.1em;
|
|
64
|
-
text-transform: uppercase;
|
|
64
|
+
font-weight: 500;
|
|
65
65
|
white-space: nowrap;
|
|
66
66
|
cursor: pointer;
|
|
67
67
|
user-select: none;
|
|
@@ -128,6 +128,7 @@
|
|
|
128
128
|
display: none;
|
|
129
129
|
}
|
|
130
130
|
.pmc-field {
|
|
131
|
+
position: relative;
|
|
131
132
|
display: flex;
|
|
132
133
|
flex-direction: column;
|
|
133
134
|
gap: 2px;
|
|
@@ -141,6 +142,8 @@
|
|
|
141
142
|
}
|
|
142
143
|
.pmc-field__label--hidden {
|
|
143
144
|
position: absolute;
|
|
145
|
+
top: 0;
|
|
146
|
+
left: 0;
|
|
144
147
|
width: 1px;
|
|
145
148
|
height: 1px;
|
|
146
149
|
overflow: hidden;
|
|
@@ -158,7 +161,7 @@
|
|
|
158
161
|
padding: 0;
|
|
159
162
|
border: 0;
|
|
160
163
|
border-bottom: 1px solid var(--input);
|
|
161
|
-
border-radius:
|
|
164
|
+
border-radius: 6px;
|
|
162
165
|
background: transparent;
|
|
163
166
|
color: var(--foreground);
|
|
164
167
|
font-family: var(--sans);
|
|
@@ -218,7 +221,7 @@
|
|
|
218
221
|
height: 16px;
|
|
219
222
|
margin: 0;
|
|
220
223
|
border: 1px solid var(--muted-foreground);
|
|
221
|
-
border-radius:
|
|
224
|
+
border-radius: 6px;
|
|
222
225
|
background: transparent;
|
|
223
226
|
cursor: pointer;
|
|
224
227
|
}
|
|
@@ -249,7 +252,7 @@
|
|
|
249
252
|
font-size: 11px;
|
|
250
253
|
line-height: 16px;
|
|
251
254
|
font-weight: 600;
|
|
252
|
-
letter-spacing: 0.
|
|
255
|
+
letter-spacing: 0.04em;
|
|
253
256
|
text-transform: uppercase;
|
|
254
257
|
white-space: nowrap;
|
|
255
258
|
color: var(--muted-foreground);
|
|
@@ -452,7 +455,7 @@
|
|
|
452
455
|
min-width: 0;
|
|
453
456
|
min-height: 36px;
|
|
454
457
|
padding: 10px 12px;
|
|
455
|
-
border-radius:
|
|
458
|
+
border-radius: 8px;
|
|
456
459
|
background: var(--card);
|
|
457
460
|
color: var(--foreground);
|
|
458
461
|
box-shadow: 0 0 0 1px color-mix(in oklab, var(--foreground) 9%, transparent),
|
|
@@ -553,14 +556,14 @@
|
|
|
553
556
|
min-height: 26px;
|
|
554
557
|
padding: 0 12px;
|
|
555
558
|
border: 1px solid transparent;
|
|
556
|
-
border-radius:
|
|
559
|
+
border-radius: 6px;
|
|
557
560
|
background: transparent;
|
|
558
561
|
color: color-mix(in oklab, var(--foreground) 68%, transparent);
|
|
559
562
|
font-family: var(--sans);
|
|
560
563
|
font-size: 12px;
|
|
561
564
|
line-height: 1;
|
|
562
565
|
font-weight: 600;
|
|
563
|
-
letter-spacing: 0.
|
|
566
|
+
letter-spacing: 0.04em;
|
|
564
567
|
text-transform: uppercase;
|
|
565
568
|
white-space: nowrap;
|
|
566
569
|
cursor: pointer;
|
|
@@ -615,7 +618,7 @@
|
|
|
615
618
|
overflow: auto;
|
|
616
619
|
padding: 4px;
|
|
617
620
|
border: 0;
|
|
618
|
-
border-radius:
|
|
621
|
+
border-radius: 8px;
|
|
619
622
|
background: var(--card);
|
|
620
623
|
color: var(--foreground);
|
|
621
624
|
font-family: var(--sans);
|
|
@@ -633,7 +636,7 @@
|
|
|
633
636
|
min-height: 32px;
|
|
634
637
|
padding: 0 12px;
|
|
635
638
|
border: 0;
|
|
636
|
-
border-radius:
|
|
639
|
+
border-radius: 6px;
|
|
637
640
|
background: transparent;
|
|
638
641
|
color: var(--foreground);
|
|
639
642
|
font: inherit;
|
|
@@ -663,7 +666,7 @@
|
|
|
663
666
|
margin: 0;
|
|
664
667
|
padding: 0;
|
|
665
668
|
border: 0;
|
|
666
|
-
border-radius:
|
|
669
|
+
border-radius: 8px;
|
|
667
670
|
background: var(--card);
|
|
668
671
|
color: var(--foreground);
|
|
669
672
|
font-family: var(--sans);
|
|
@@ -794,7 +797,7 @@
|
|
|
794
797
|
min-height: 44px;
|
|
795
798
|
padding: 4px 4px 4px 16px;
|
|
796
799
|
border: 0;
|
|
797
|
-
border-radius:
|
|
800
|
+
border-radius: 8px;
|
|
798
801
|
background: var(--card);
|
|
799
802
|
color: var(--foreground);
|
|
800
803
|
font-family: var(--sans);
|
|
@@ -936,7 +939,7 @@
|
|
|
936
939
|
margin: 0;
|
|
937
940
|
padding: 0;
|
|
938
941
|
border: 0;
|
|
939
|
-
border-radius:
|
|
942
|
+
border-radius: 6px;
|
|
940
943
|
background: transparent;
|
|
941
944
|
color: inherit;
|
|
942
945
|
transform: translateY(-50%);
|
|
@@ -1084,7 +1087,7 @@
|
|
|
1084
1087
|
margin-right: 10px;
|
|
1085
1088
|
font-size: 12px;
|
|
1086
1089
|
font-weight: 600;
|
|
1087
|
-
letter-spacing: 0.
|
|
1090
|
+
letter-spacing: 0.04em;
|
|
1088
1091
|
text-transform: uppercase;
|
|
1089
1092
|
}
|
|
1090
1093
|
.bd-entry__meta {
|
|
@@ -1200,7 +1203,7 @@
|
|
|
1200
1203
|
font-size: 11px;
|
|
1201
1204
|
line-height: 16px;
|
|
1202
1205
|
font-weight: 600;
|
|
1203
|
-
letter-spacing: 0.
|
|
1206
|
+
letter-spacing: 0.04em;
|
|
1204
1207
|
text-transform: uppercase;
|
|
1205
1208
|
color: var(--muted-foreground);
|
|
1206
1209
|
}
|
|
@@ -1287,7 +1290,7 @@
|
|
|
1287
1290
|
font-size: 12px;
|
|
1288
1291
|
line-height: 16px;
|
|
1289
1292
|
font-weight: 600;
|
|
1290
|
-
letter-spacing: 0.
|
|
1293
|
+
letter-spacing: 0.04em;
|
|
1291
1294
|
text-transform: uppercase;
|
|
1292
1295
|
}
|
|
1293
1296
|
.bd-col__n {
|
|
@@ -1545,10 +1548,9 @@
|
|
|
1545
1548
|
.in-head__title {
|
|
1546
1549
|
flex: 1 1 auto;
|
|
1547
1550
|
margin: 0;
|
|
1548
|
-
font-size:
|
|
1549
|
-
line-height:
|
|
1550
|
-
font-weight:
|
|
1551
|
-
letter-spacing: -0.01em;
|
|
1551
|
+
font-size: 20px;
|
|
1552
|
+
line-height: 28px;
|
|
1553
|
+
font-weight: 500;
|
|
1552
1554
|
}
|
|
1553
1555
|
.in-body {
|
|
1554
1556
|
display: flex;
|
|
@@ -1645,7 +1647,7 @@
|
|
|
1645
1647
|
font-size: 12px;
|
|
1646
1648
|
line-height: 16px;
|
|
1647
1649
|
font-weight: 600;
|
|
1648
|
-
letter-spacing: 0.
|
|
1650
|
+
letter-spacing: 0.04em;
|
|
1649
1651
|
text-transform: uppercase;
|
|
1650
1652
|
font-variant-numeric: tabular-nums;
|
|
1651
1653
|
}
|
|
@@ -1670,7 +1672,7 @@
|
|
|
1670
1672
|
font-size: 12px;
|
|
1671
1673
|
line-height: 16px;
|
|
1672
1674
|
font-weight: 600;
|
|
1673
|
-
letter-spacing: 0.
|
|
1675
|
+
letter-spacing: 0.04em;
|
|
1674
1676
|
text-transform: uppercase;
|
|
1675
1677
|
}
|
|
1676
1678
|
.in-col__count {
|
|
@@ -1910,7 +1912,7 @@
|
|
|
1910
1912
|
font-size: 12px;
|
|
1911
1913
|
line-height: 16px;
|
|
1912
1914
|
font-weight: 600;
|
|
1913
|
-
letter-spacing: 0.
|
|
1915
|
+
letter-spacing: 0.04em;
|
|
1914
1916
|
text-transform: uppercase;
|
|
1915
1917
|
}
|
|
1916
1918
|
.in-starters {
|
|
@@ -2133,7 +2135,7 @@
|
|
|
2133
2135
|
margin: 0;
|
|
2134
2136
|
font-size: 12px;
|
|
2135
2137
|
font-weight: 600;
|
|
2136
|
-
letter-spacing: 0.
|
|
2138
|
+
letter-spacing: 0.04em;
|
|
2137
2139
|
text-transform: uppercase;
|
|
2138
2140
|
color: var(--muted-foreground);
|
|
2139
2141
|
}
|
|
@@ -2266,6 +2268,8 @@
|
|
|
2266
2268
|
min-width: 0;
|
|
2267
2269
|
min-height: 0;
|
|
2268
2270
|
overflow-y: auto;
|
|
2271
|
+
/* the destination is the one thing that scrolls: a wheel at its end does not move the page and its bars */
|
|
2272
|
+
overscroll-behavior: contain;
|
|
2269
2273
|
}
|
|
2270
2274
|
.fr-dest[hidden] {
|
|
2271
2275
|
display: none;
|
|
@@ -2476,7 +2480,7 @@
|
|
|
2476
2480
|
color: var(--muted-foreground);
|
|
2477
2481
|
font-size: 11px;
|
|
2478
2482
|
font-weight: 600;
|
|
2479
|
-
letter-spacing: 0.
|
|
2483
|
+
letter-spacing: 0.04em;
|
|
2480
2484
|
text-transform: uppercase;
|
|
2481
2485
|
text-overflow: ellipsis;
|
|
2482
2486
|
white-space: nowrap;
|
|
@@ -2765,7 +2769,7 @@
|
|
|
2765
2769
|
color: var(--muted-foreground);
|
|
2766
2770
|
font-size: 11px;
|
|
2767
2771
|
font-weight: 600;
|
|
2768
|
-
letter-spacing: 0.
|
|
2772
|
+
letter-spacing: 0.04em;
|
|
2769
2773
|
text-transform: uppercase;
|
|
2770
2774
|
list-style: none;
|
|
2771
2775
|
cursor: pointer;
|
|
@@ -3007,7 +3011,7 @@
|
|
|
3007
3011
|
color: var(--muted-foreground);
|
|
3008
3012
|
font-size: 11px;
|
|
3009
3013
|
font-weight: 600;
|
|
3010
|
-
letter-spacing: 0.
|
|
3014
|
+
letter-spacing: 0.04em;
|
|
3011
3015
|
text-transform: uppercase;
|
|
3012
3016
|
}
|
|
3013
3017
|
.fr-rail__graph {
|
|
@@ -3089,7 +3093,7 @@
|
|
|
3089
3093
|
color: var(--muted-foreground);
|
|
3090
3094
|
font-size: 11px;
|
|
3091
3095
|
font-weight: 600;
|
|
3092
|
-
letter-spacing: 0.
|
|
3096
|
+
letter-spacing: 0.04em;
|
|
3093
3097
|
text-transform: uppercase;
|
|
3094
3098
|
}
|
|
3095
3099
|
.fr-rail__finding[data-status="stale"] .fr-rail__finding-s,
|
|
@@ -3245,7 +3249,7 @@
|
|
|
3245
3249
|
.td-day__warn { color: var(--warning); }
|
|
3246
3250
|
.td-day__mono { font-family: var(--mono); font-variant-numeric: tabular-nums; font-size: 12px; }
|
|
3247
3251
|
.td-day__steps { display: flex; flex-wrap: wrap; gap: 2px; list-style: none; margin: 0; padding: 0; }
|
|
3248
|
-
.td-day__ts { gap: 6px; padding: 0 6px; letter-spacing: 0.04em; color: var(--muted-foreground); }
|
|
3252
|
+
.td-day__ts { border-radius: 6px; gap: 6px; padding: 0 6px; letter-spacing: 0.04em; color: var(--muted-foreground); }
|
|
3249
3253
|
.td-day__ts[data-step-state="done"], .td-day__ts[data-step-state="now"] { color: var(--foreground); }
|
|
3250
3254
|
.td-day__ts[data-step-state="now"] { background: var(--muted); }
|
|
3251
3255
|
.td-day__ts[aria-current="true"] { border-color: var(--ring); }
|
|
@@ -3260,14 +3264,15 @@
|
|
|
3260
3264
|
.td-day__run--failed { color: var(--destructive); }
|
|
3261
3265
|
.td-day__note { margin: 0; font-size: 12px; line-height: 16px; color: var(--muted-foreground); }
|
|
3262
3266
|
|
|
3263
|
-
.td-day__cols { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.25fr); gap: 12px; align-items:
|
|
3264
|
-
.td-day__left { display: grid; gap: 12px; min-width: 0; }
|
|
3267
|
+
.td-day__cols { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.25fr); gap: 12px; align-items: stretch; }
|
|
3268
|
+
.td-day__left { display: grid; gap: 12px; min-width: 0; align-content: start; }
|
|
3265
3269
|
.td-day__panel, .td-day__detail {
|
|
3266
3270
|
display: grid;
|
|
3267
3271
|
gap: 8px;
|
|
3268
3272
|
min-width: 0;
|
|
3269
3273
|
padding: 10px 12px;
|
|
3270
3274
|
border: 1px solid var(--border);
|
|
3275
|
+
border-radius: 8px;
|
|
3271
3276
|
background: var(--card);
|
|
3272
3277
|
}
|
|
3273
3278
|
.td-day__detail { position: sticky; top: 8px; max-height: calc(100vh - 16px); overflow: auto; align-content: start; }
|
|
@@ -3282,7 +3287,7 @@
|
|
|
3282
3287
|
margin: 0;
|
|
3283
3288
|
font-size: 12px;
|
|
3284
3289
|
font-weight: 600;
|
|
3285
|
-
letter-spacing: 0.
|
|
3290
|
+
letter-spacing: 0.04em;
|
|
3286
3291
|
text-transform: uppercase;
|
|
3287
3292
|
color: var(--muted-foreground);
|
|
3288
3293
|
}
|
|
@@ -3295,7 +3300,7 @@
|
|
|
3295
3300
|
.td-day__lead { min-width: 44px; font: 12px var(--mono); color: var(--muted-foreground); font-variant-numeric: tabular-nums; }
|
|
3296
3301
|
.td-day__chips { display: inline-flex; gap: 4px; }
|
|
3297
3302
|
.td-day__chips { align-items: center; flex-wrap: wrap; justify-content: flex-end; }
|
|
3298
|
-
.td-day__repo { font: inherit; font-size: 12px; color: var(--foreground); background: var(--background); border: 1px solid var(--border); border-radius:
|
|
3303
|
+
.td-day__repo { font: inherit; font-size: 12px; color: var(--foreground); background: var(--background); border: 1px solid var(--border); border-radius: 6px; padding: 2px 6px; max-width: 160px; }
|
|
3299
3304
|
.td-day__repo:focus-visible { outline: 2px solid color-mix(in oklab, var(--ring) 50%, var(--foreground)); outline-offset: 2px; }
|
|
3300
3305
|
.td-day__nowrow { display: flex; align-items: center; gap: 8px; padding: 0 8px; font: 11px var(--mono); color: var(--destructive); }
|
|
3301
3306
|
.td-day__nowrow::after { content: ""; flex: 1; border-top: 2px solid var(--destructive); }
|
|
@@ -3306,7 +3311,7 @@
|
|
|
3306
3311
|
|
|
3307
3312
|
/* the detail */
|
|
3308
3313
|
.td-day__dhead { display: grid; gap: 4px; }
|
|
3309
|
-
.td-day__kind { font: 11px var(--mono); letter-spacing: 0.
|
|
3314
|
+
.td-day__kind { font: 11px var(--mono); letter-spacing: 0.04em; text-transform: uppercase; color: var(--muted-foreground); }
|
|
3310
3315
|
.td-day__dhead h3 { margin: 0; font-size: 16px; line-height: 24px; color: var(--foreground); }
|
|
3311
3316
|
.td-day__detail p { margin: 0; font-size: 14px; line-height: 20px; color: var(--foreground); }
|
|
3312
3317
|
.td-day__detail p.td-day__note { font-size: 12px; line-height: 16px; color: var(--muted-foreground); }
|
|
@@ -3317,11 +3322,10 @@
|
|
|
3317
3322
|
height: 28px;
|
|
3318
3323
|
padding: 0 10px;
|
|
3319
3324
|
border: 1px solid var(--border);
|
|
3325
|
+
border-radius: 6px;
|
|
3320
3326
|
color: var(--foreground);
|
|
3321
3327
|
font-size: 12px;
|
|
3322
|
-
font-weight:
|
|
3323
|
-
letter-spacing: 0.1em;
|
|
3324
|
-
text-transform: uppercase;
|
|
3328
|
+
font-weight: 500;
|
|
3325
3329
|
text-decoration: none;
|
|
3326
3330
|
white-space: nowrap;
|
|
3327
3331
|
}
|
|
@@ -3329,7 +3333,7 @@
|
|
|
3329
3333
|
@media (pointer: coarse) { .td-day__a { min-height: 44px; } }
|
|
3330
3334
|
.td-day__phase, .td-day__rel { display: grid; gap: 4px; }
|
|
3331
3335
|
.td-day__phase h4, .td-day__rel h4, .td-day__notes h4, .td-day__draftbox h4, .td-day__more h4 {
|
|
3332
|
-
margin: 0; font-size: 11px; letter-spacing: 0.
|
|
3336
|
+
margin: 0; font-size: 11px; letter-spacing: 0.04em; text-transform: uppercase; color: var(--muted-foreground);
|
|
3333
3337
|
}
|
|
3334
3338
|
.td-day__chk { list-style: none; margin: 0; padding: 0; display: grid; gap: 4px; font-size: 14px; color: var(--foreground); }
|
|
3335
3339
|
.td-day__chk li { display: grid; grid-template-columns: 16px minmax(0, 1fr); gap: 6px; }
|
|
@@ -3351,7 +3355,7 @@
|
|
|
3351
3355
|
.td-day__draft { margin: 0; max-height: 220px; overflow: auto; padding: 8px 10px; white-space: pre-wrap; font: 12px/1.5 var(--mono); background: var(--muted); color: var(--foreground); }
|
|
3352
3356
|
.td-day__revise { display: grid; gap: 6px; }
|
|
3353
3357
|
/* act and prepare (initiative today-act-prepare): a verb first, one sentence, the steps, one button that fits, the marks */
|
|
3354
|
-
.td-day__verb { display: inline-block; min-width: 58px; padding: 2px 5px; border: 1px solid currentColor; font: 600 10px/1.3 var(--mono); letter-spacing: 0.
|
|
3358
|
+
.td-day__verb { display: inline-block; min-width: 58px; padding: 2px 5px; border: 1px solid currentColor; font: 600 10px/1.3 var(--mono); letter-spacing: 0.04em; text-transform: uppercase; text-align: center; color: var(--foreground); white-space: nowrap; }
|
|
3355
3359
|
.td-day__verb[data-verb="decide"], .td-day__verb[data-verb="prepare"] { color: var(--warning); }
|
|
3356
3360
|
.td-day__acth { margin: 0; font-size: 16px; font-weight: 600; line-height: 1.3; text-wrap: balance; }
|
|
3357
3361
|
.td-day__todo { margin: 0; padding: 7px 10px; border-left: 3px solid var(--foreground); background: var(--muted); font-size: 14px; line-height: 1.45; color: var(--foreground); }
|
|
@@ -3459,7 +3463,7 @@
|
|
|
3459
3463
|
gap: 8px;
|
|
3460
3464
|
min-width: 0;
|
|
3461
3465
|
padding: 10px 12px;
|
|
3462
|
-
border-radius:
|
|
3466
|
+
border-radius: 8px;
|
|
3463
3467
|
background: var(--card);
|
|
3464
3468
|
color: var(--foreground);
|
|
3465
3469
|
box-shadow: 0 0 0 1px var(--border);
|
|
@@ -3553,7 +3557,7 @@
|
|
|
3553
3557
|
}
|
|
3554
3558
|
.td-health {
|
|
3555
3559
|
padding: 8px 12px;
|
|
3556
|
-
border-radius:
|
|
3560
|
+
border-radius: 8px;
|
|
3557
3561
|
background: var(--muted);
|
|
3558
3562
|
box-shadow: inset 2px 0 0 var(--warning);
|
|
3559
3563
|
font-size: 12px;
|
|
@@ -4011,13 +4015,13 @@
|
|
|
4011
4015
|
margin: 16px 16px 0;
|
|
4012
4016
|
padding: 12px;
|
|
4013
4017
|
border: 1px solid var(--border);
|
|
4014
|
-
border-radius:
|
|
4018
|
+
border-radius: 8px;
|
|
4015
4019
|
}
|
|
4016
4020
|
.bo-releases__head {
|
|
4017
4021
|
font-size: 12px;
|
|
4018
4022
|
line-height: 16px;
|
|
4019
4023
|
font-weight: 600;
|
|
4020
|
-
letter-spacing: 0.
|
|
4024
|
+
letter-spacing: 0.04em;
|
|
4021
4025
|
text-transform: uppercase;
|
|
4022
4026
|
}
|
|
4023
4027
|
.bo-releases__note {
|
|
@@ -4036,7 +4040,7 @@
|
|
|
4036
4040
|
.bo-rel--today {
|
|
4037
4041
|
padding: 10px 12px;
|
|
4038
4042
|
border: 1px solid var(--border);
|
|
4039
|
-
border-radius:
|
|
4043
|
+
border-radius: 8px;
|
|
4040
4044
|
background: var(--card);
|
|
4041
4045
|
}
|
|
4042
4046
|
.bo-rel__main {
|
|
@@ -4119,7 +4123,7 @@
|
|
|
4119
4123
|
font-size: 11px;
|
|
4120
4124
|
line-height: 16px;
|
|
4121
4125
|
font-weight: 500;
|
|
4122
|
-
letter-spacing: 0.
|
|
4126
|
+
letter-spacing: 0.04em;
|
|
4123
4127
|
text-transform: uppercase;
|
|
4124
4128
|
color: var(--muted-foreground);
|
|
4125
4129
|
}
|
|
@@ -4266,7 +4270,7 @@
|
|
|
4266
4270
|
font-family: var(--mono);
|
|
4267
4271
|
font-size: 11px;
|
|
4268
4272
|
font-weight: 500;
|
|
4269
|
-
letter-spacing: 0.
|
|
4273
|
+
letter-spacing: 0.04em;
|
|
4270
4274
|
text-transform: uppercase;
|
|
4271
4275
|
color: var(--muted-foreground);
|
|
4272
4276
|
}
|
|
@@ -5883,6 +5887,8 @@
|
|
|
5883
5887
|
}
|
|
5884
5888
|
.nt-sr {
|
|
5885
5889
|
position: absolute;
|
|
5890
|
+
top: 0;
|
|
5891
|
+
left: 0;
|
|
5886
5892
|
width: 1px;
|
|
5887
5893
|
height: 1px;
|
|
5888
5894
|
overflow: hidden;
|
|
@@ -5925,7 +5931,7 @@
|
|
|
5925
5931
|
margin: 0;
|
|
5926
5932
|
font-size: 11px;
|
|
5927
5933
|
font-weight: 500;
|
|
5928
|
-
letter-spacing: .
|
|
5934
|
+
letter-spacing: 0.04em;
|
|
5929
5935
|
text-transform: uppercase;
|
|
5930
5936
|
color: var(--muted-foreground);
|
|
5931
5937
|
}
|
|
@@ -6200,7 +6206,7 @@
|
|
|
6200
6206
|
gap: 2px;
|
|
6201
6207
|
padding: 8px 12px;
|
|
6202
6208
|
border: 1px solid var(--border);
|
|
6203
|
-
border-radius:
|
|
6209
|
+
border-radius: 8px;
|
|
6204
6210
|
background: var(--background);
|
|
6205
6211
|
color: var(--foreground);
|
|
6206
6212
|
font: inherit;
|
|
@@ -6585,10 +6591,9 @@
|
|
|
6585
6591
|
color: var(--foreground);
|
|
6586
6592
|
}
|
|
6587
6593
|
|
|
6588
|
-
/* R27
|
|
6589
|
-
|
|
6590
|
-
.
|
|
6591
|
-
|
|
6592
|
-
}
|
|
6594
|
+
/* R27 as amended by the PM 2026-10-07: the dock follows the same radii as every other surface (6px controls, 8px
|
|
6595
|
+
containers). The tab strip and the scratchpad are drawn with inline radii (workspace-cockpit-nav); the inline radii
|
|
6596
|
+
(8px tab strip, 6px controls) stay. A tab's colour dot is a dot and keeps its circle. */
|
|
6597
|
+
|
|
6593
6598
|
|
|
6594
6599
|
/* -- end of the layout sections: add nothing below this line -- */
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* The legacy Today view (view-today.js) calls mountToday on its own host, so both surfaces draw the one list.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import { el } from './dom.js';
|
|
15
|
+
import { el, withAnchor } from './dom.js';
|
|
16
16
|
import { empty, tabs, button, toast, ensureComponentsCss } from './primitives.js';
|
|
17
17
|
import { registerTodaySource, todayCards, todayCount, openInitiative } from './registry.js';
|
|
18
18
|
import { repoTuples } from './panel-repos.js';
|
|
@@ -200,7 +200,8 @@ export function mountToday(host, ctx) {
|
|
|
200
200
|
if (out.sig === r.sig) return;
|
|
201
201
|
const k = focusKey(r.root);
|
|
202
202
|
r.sig = out.sig;
|
|
203
|
-
|
|
203
|
+
// a card added or removed above the reader must not move what they are reading (dom.js withAnchor)
|
|
204
|
+
withAnchor(host, r.root, 'data-today-id', () => r.root.replaceChildren(...out.children.filter(Boolean)));
|
|
204
205
|
refocus(r.root, k);
|
|
205
206
|
}
|
|
206
207
|
|
|
@@ -114,3 +114,63 @@ export function setChildren(parent, kids) {
|
|
|
114
114
|
});
|
|
115
115
|
return parent;
|
|
116
116
|
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Keep the reader's place across a rebuild (UI comfort, 2026-10-07: "when I scroll down into a section it pages up").
|
|
120
|
+
* Before `rebuild()` runs, the first element carrying `attr` that is still on screen in `scroller` is remembered with its
|
|
121
|
+
* offset from the scroller's top; afterwards the scroller is moved so that same element sits at that same offset. A
|
|
122
|
+
* card added or removed above the reader therefore no longer pushes what they are reading off the screen. With the
|
|
123
|
+
* scroller at its top, or no such element, it only runs `rebuild()`.
|
|
124
|
+
*/
|
|
125
|
+
export function withAnchor(scroller, root, attr, rebuild) {
|
|
126
|
+
let key = null;
|
|
127
|
+
let off = 0;
|
|
128
|
+
if (scroller && scroller.scrollTop > 0 && root) {
|
|
129
|
+
const top = scroller.getBoundingClientRect().top;
|
|
130
|
+
for (const n of root.querySelectorAll('[' + attr + ']')) {
|
|
131
|
+
const r = n.getBoundingClientRect();
|
|
132
|
+
if (r.height && r.bottom > top) { key = n.getAttribute(attr); off = r.top - top; break; }
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
rebuild();
|
|
136
|
+
if (key == null) return;
|
|
137
|
+
const again = [...root.querySelectorAll('[' + attr + ']')].find((n) => n.getAttribute(attr) === key);
|
|
138
|
+
if (!again) return;
|
|
139
|
+
const drift = again.getBoundingClientRect().top - scroller.getBoundingClientRect().top - off;
|
|
140
|
+
if (Math.abs(drift) >= 1) scroller.scrollTop += drift;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Give a scroller its reading position back when a rebuild took it away (UI comfort, 2026-10-07). A destination that
|
|
145
|
+
* redraws through a short state — a "Reading…" line, an emptied list — collapses the scroller, the browser clamps the
|
|
146
|
+
* offset towards the top, and when the content returns the reader is left at the top. This watches the scroller's
|
|
147
|
+
* subtree: when a change leaves the offset ABOVE where the reader last was and the content has shrunk below it, that
|
|
148
|
+
* place is held, and the next change that makes the content tall enough again returns to it. Anything the reader does
|
|
149
|
+
* (wheel, touch, a key, a click) drops what is held, so their own scrolling always wins. Returns its undo.
|
|
150
|
+
*/
|
|
151
|
+
export function keepScroll(scroller) {
|
|
152
|
+
let top = scroller.scrollTop;
|
|
153
|
+
let held = null;
|
|
154
|
+
let until = 0;
|
|
155
|
+
const drop = () => { held = null; };
|
|
156
|
+
const onScroll = () => { if (held == null) top = scroller.scrollTop; };
|
|
157
|
+
const intents = ['wheel', 'touchstart', 'keydown', 'pointerdown'];
|
|
158
|
+
for (const ev of intents) scroller.addEventListener(ev, drop, { passive: true, capture: true });
|
|
159
|
+
scroller.addEventListener('scroll', onScroll, { passive: true });
|
|
160
|
+
const mo = new MutationObserver(() => {
|
|
161
|
+
if (scroller.hidden || !scroller.isConnected) return;
|
|
162
|
+
const max = scroller.scrollHeight - scroller.clientHeight;
|
|
163
|
+
if (held == null) {
|
|
164
|
+
if (top > 0 && scroller.scrollTop < top - 1 && max < top) { held = top; until = Date.now() + 5000; }
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (Date.now() > until) { held = null; top = scroller.scrollTop; return; }
|
|
168
|
+
if (max >= held - 1) { scroller.scrollTop = held; top = held; held = null; }
|
|
169
|
+
});
|
|
170
|
+
mo.observe(scroller, { childList: true, subtree: true, characterData: true });
|
|
171
|
+
return () => {
|
|
172
|
+
mo.disconnect();
|
|
173
|
+
for (const ev of intents) scroller.removeEventListener(ev, drop, { capture: true });
|
|
174
|
+
scroller.removeEventListener('scroll', onScroll);
|
|
175
|
+
};
|
|
176
|
+
}
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* env = { repo, ws, openInitiative(repo, slug), onData?(answer) }
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
import { el } from './dom.js';
|
|
23
|
+
import { el, withAnchor } from './dom.js';
|
|
24
24
|
import { card, iconButton, dragHandle, button, menu, empty, toast, SEL } from './primitives.js';
|
|
25
25
|
import { watchSig } from './sig-watch.js';
|
|
26
26
|
import { loadSortable, baseOptions, dragSet } from './bd-drag.js';
|
|
@@ -211,7 +211,7 @@ export function mountRoadmap(host, env) {
|
|
|
211
211
|
const kids = [headLine(), el('p', { class: 'in-note in-hint', 'data-in-hint': '' }, COPY.menuHintCard)];
|
|
212
212
|
if (data.flags && data.flags.length) kids.push(el('p', { class: 'in-note', 'data-in-flags': '' }, COPY.flags(data.flags.length)));
|
|
213
213
|
kids.push(el('div', { class: 'in-cols', 'data-in-cols': '' }, HORIZONS.map(column)));
|
|
214
|
-
root.replaceChildren(...kids);
|
|
214
|
+
withAnchor(root.closest('.fr-dest'), root, 'data-in-card', () => root.replaceChildren(...kids));
|
|
215
215
|
if (refocus) { const t = root.querySelector('[data-in-open="' + CSS.escape(refocus) + '"]'); if (t) t.focus({ preventScroll: true }); }
|
|
216
216
|
bindDrag();
|
|
217
217
|
}
|
|
@@ -36,6 +36,9 @@ export function unsyncedOf(st) {
|
|
|
36
36
|
&& ((st.behind | 0) > 0 || (st.ahead | 0) > 0 || (st.dirty_records | 0) > 0 || st.remote_stale === true);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
/** The lucide symbol each place wears in the bar (UI comfort, 2026-10-07: a word alone is slow to find; the phone's tab bar already wears these). */
|
|
40
|
+
const NAV_ICON = Object.freeze({ today: 'house', mywork: 'list-checks', initiatives: 'target', board: 'square-kanban', reports: 'chart-column', docs: 'file-text', people: 'share-2', settings: 'settings' });
|
|
41
|
+
|
|
39
42
|
export function createTopbar({ go, openPalette, repoMenu, sync } = {}) {
|
|
40
43
|
const node = el('header', { class: 'fr-bar', 'data-shell': 'top', 'data-fr': 'bar' });
|
|
41
44
|
|
|
@@ -48,7 +51,7 @@ export function createTopbar({ go, openPalette, repoMenu, sync } = {}) {
|
|
|
48
51
|
const links = {};
|
|
49
52
|
for (const id of [...BAR_DESTS, 'settings']) {
|
|
50
53
|
links[id] = button({
|
|
51
|
-
label: COPY.nav[id], variant: 'ghost', icon: id
|
|
54
|
+
label: COPY.nav[id], variant: 'ghost', icon: NAV_ICON[id],
|
|
52
55
|
attrs: { class: 'fr-link', 'data-fr-dest': id, 'aria-current': 'false' },
|
|
53
56
|
onClick: () => go && go(id),
|
|
54
57
|
});
|