create-pmos 0.134.0 → 0.136.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 +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/bin/notes-replay.js +235 -0
- package/workspace/c3-briefing-test.js +130 -0
- package/workspace/c3-foundation-test.js +159 -0
- package/workspace/c3-ib-items-test.js +105 -0
- package/workspace/c3-ib-more-test.js +120 -0
- package/workspace/c3-ib-triage-backlog-test.js +119 -0
- package/workspace/c3-ini-harness.js +66 -0
- package/workspace/c3-initiatives-test.js +140 -0
- package/workspace/c3-one-home-test.js +14 -1
- package/workspace/c3-reports-test.js +57 -18
- package/workspace/c3-settings-test.js +139 -0
- package/workspace/c3-work-test.js +42 -10
- package/workspace/fixtures/one-home-baseline.json +1 -9
- package/workspace/guide/briefing-cards.md +15 -3
- package/workspace/guide/briefing.md +12 -8
- package/workspace/guide/calendar-popup.md +2 -2
- package/workspace/guide/console-calendar.md +39 -10
- package/workspace/guide/index.md +1 -0
- package/workspace/guide/initiative-boards.md +65 -0
- package/workspace/guide/jira.md +3 -1
- package/workspace/guide/layout.md +2 -0
- package/workspace/guide/my-work.md +5 -3
- package/workspace/guide/notes.md +16 -1
- package/workspace/guide/reports-dashboards.md +29 -5
- package/workspace/guide/settings.md +68 -0
- package/workspace/guide/welcome.md +10 -10
- package/workspace/lib/calendar-cli.js +3 -2
- package/workspace/lib/calendar-layers.js +21 -10
- package/workspace/lib/calendar-tidy.js +230 -0
- package/workspace/lib/engine/boards.js +542 -0
- package/workspace/lib/engine/breakdown-ops.js +180 -0
- package/workspace/lib/engine/calendar-import.js +84 -0
- package/workspace/lib/engine/calendar-model.js +185 -0
- package/workspace/lib/engine/card-state.js +92 -0
- package/workspace/lib/engine/derived/derive.js +92 -0
- package/workspace/lib/engine/derived/plan.js +3 -0
- package/workspace/lib/engine/derived/trust.js +3 -0
- package/workspace/lib/engine/doc-health.js +142 -0
- package/workspace/lib/engine/doc-sheet.js +75 -0
- package/workspace/lib/engine/intake.js +78 -0
- package/workspace/lib/engine/outbox.js +86 -0
- package/workspace/lib/engine/plan-forecast.js +342 -0
- package/workspace/lib/engine/promises.js +29 -0
- package/workspace/lib/engine/records/index.js +114 -0
- package/workspace/lib/engine/releases.js +96 -0
- package/workspace/lib/engine/replies.js +99 -0
- package/workspace/lib/engine/reply-contract/reply-contract.js +372 -0
- package/workspace/lib/engine/report-families.js +175 -0
- package/workspace/lib/engine/roadmap-ops.js +150 -0
- package/workspace/lib/engine/sessions-list.js +27 -0
- package/workspace/lib/engine/signal-rules.js +95 -0
- package/workspace/lib/engine/signals/cards.js +57 -0
- package/workspace/lib/engine/signals/detectors.js +370 -0
- package/workspace/lib/engine/signals/facts-cli.js +8 -0
- package/workspace/lib/engine/signals/facts.js +197 -0
- package/workspace/lib/engine/signals/index.js +33 -0
- package/workspace/lib/engine/signals/kinds.js +110 -0
- package/workspace/lib/engine/signals/layout-paths.js +44 -0
- package/workspace/lib/engine/signals/options.js +24 -0
- package/workspace/lib/engine/signals/score.js +72 -0
- package/workspace/lib/engine/signals/select.js +47 -0
- package/workspace/lib/engine/step-planner.js +65 -0
- package/workspace/lib/engine/trust/claim-rules.json +93 -0
- package/workspace/lib/engine/trust/import-calendar.js +21 -0
- package/workspace/lib/engine/trust/import-guard.js +36 -0
- package/workspace/lib/engine/trust/ledger.js +53 -0
- package/workspace/lib/engine/trust/outbox.js +45 -0
- package/workspace/lib/engine/trust/plan.js +5 -0
- package/workspace/lib/engine/trust/trust.js +21 -0
- package/workspace/lib/engine/truth-table.js +57 -0
- package/workspace/lib/engine/workspace-lib.js +24 -0
- package/workspace/lib/factory-join.js +1 -1
- package/workspace/lib/facts-async.js +4 -4
- package/workspace/lib/headless.js +95 -0
- package/workspace/lib/import-overlay.js +2 -1
- package/workspace/lib/jira-status.js +30 -3
- package/workspace/lib/notes-capture.js +9 -1
- package/workspace/lib/notes-final.js +38 -13
- package/workspace/lib/notes-replay.js +125 -0
- package/workspace/lib/notes-screen.js +180 -0
- package/workspace/lib/notes-whisper.js +10 -3
- package/workspace/lib/outbox-store.js +10 -6
- package/workspace/lib/pmos-verbs.js +3 -1
- package/workspace/lib/proactive-cli.js +7 -0
- package/workspace/lib/proactive.js +15 -2
- package/workspace/lib/records-dir.js +29 -0
- package/workspace/lib/repo-script.js +36 -0
- package/workspace/lib/report-dashboards.js +5 -2
- package/workspace/lib/report-frame.js +122 -0
- package/workspace/lib/report-schedules.js +113 -0
- package/workspace/lib/route-modules.js +9 -2
- package/workspace/lib/routes-boards.js +734 -0
- package/workspace/lib/routes-brief-page.js +13 -4
- package/workspace/lib/routes-briefing.js +22 -11
- package/workspace/lib/routes-c3-init.js +9 -8
- package/workspace/lib/routes-c3-life.js +2 -1
- package/workspace/lib/routes-c3-reports.js +99 -15
- package/workspace/lib/routes-c3-strategy.js +7 -5
- package/workspace/lib/routes-c3-timeline.js +3 -2
- package/workspace/lib/routes-calendar-import.js +3 -2
- package/workspace/lib/routes-calendar-views.js +9 -4
- package/workspace/lib/routes-doc-health.js +2 -1
- package/workspace/lib/routes-doc-sheet.js +2 -1
- package/workspace/lib/routes-forecast.js +2 -1
- package/workspace/lib/routes-hooks.js +3 -2
- package/workspace/lib/routes-initiatives.js +2 -1
- package/workspace/lib/routes-intake.js +2 -1
- package/workspace/lib/routes-notes.js +55 -9
- package/workspace/lib/routes-outbox.js +23 -3
- package/workspace/lib/routes-pm-items.js +19 -6
- package/workspace/lib/routes-records.js +10 -7
- package/workspace/lib/routes-releases.js +2 -1
- package/workspace/lib/routes-report-facts.js +4 -3
- package/workspace/lib/routes-report-families.js +2 -1
- package/workspace/lib/routes-roadmap-ops.js +17 -14
- package/workspace/lib/routes-runs.js +6 -5
- package/workspace/lib/routes-settings.js +51 -0
- package/workspace/lib/routes-signals.js +3 -2
- package/workspace/lib/routes-stage-tools.js +220 -0
- package/workspace/lib/run-answers.js +2 -1
- package/workspace/lib/settings-store.js +226 -0
- package/workspace/lib/stage-tools.js +208 -0
- package/workspace/lib/vendor/scripts/signals/layout-paths.js +2 -1
- package/workspace/lib/vendor/scripts/workspace-lib.js +24 -0
- package/workspace/lib/work-evidence.js +73 -0
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/briefing-deck.js +63 -17
- package/workspace/public/pm/briefing-model.js +36 -7
- package/workspace/public/pm/c3-ambient.js +100 -0
- package/workspace/public/pm/c3-brief-needs.js +26 -6
- package/workspace/public/pm/c3-brief-pulse.js +12 -4
- package/workspace/public/pm/c3-brief-today.js +56 -21
- package/workspace/public/pm/c3-brief.js +2 -1
- package/workspace/public/pm/c3-calendar.js +161 -42
- package/workspace/public/pm/c3-calpop.js +5 -5
- package/workspace/public/pm/c3-demo-boards.js +649 -0
- package/workspace/public/pm/c3-demo-brief-page.js +1 -1
- package/workspace/public/pm/c3-demo-cal4.js +3 -2
- package/workspace/public/pm/c3-demo-cal5.js +52 -0
- package/workspace/public/pm/c3-demo-data.js +17 -27
- package/workspace/public/pm/c3-demo-rep3.js +46 -5
- package/workspace/public/pm/c3-demo.js +2 -1
- package/workspace/public/pm/c3-dock.js +1 -1
- package/workspace/public/pm/c3-ib-docs.js +80 -0
- package/workspace/public/pm/c3-ib-initiatives.js +134 -0
- package/workspace/public/pm/c3-ib-items.js +201 -0
- package/workspace/public/pm/c3-ib-releases.js +51 -0
- package/workspace/public/pm/c3-ib-sprints.js +55 -0
- package/workspace/public/pm/c3-ib-triage.js +178 -0
- package/workspace/public/pm/c3-ii-tabs.js +240 -0
- package/workspace/public/pm/c3-ini-records.js +197 -0
- package/workspace/public/pm/c3-ini-timeline.js +278 -0
- package/workspace/public/pm/c3-ini-ui.js +229 -0
- package/workspace/public/pm/c3-ini-work.js +139 -0
- package/workspace/public/pm/c3-initiatives.css +218 -0
- package/workspace/public/pm/c3-initiatives.js +741 -0
- package/workspace/public/pm/c3-plan.js +2 -2
- package/workspace/public/pm/c3-reports.css +47 -47
- package/workspace/public/pm/c3-reports.js +223 -185
- package/workspace/public/pm/c3-settings.css +74 -0
- package/workspace/public/pm/c3-settings.js +703 -0
- package/workspace/public/pm/c3-shell.js +102 -36
- package/workspace/public/pm/c3-terminal-icons.js +2 -2
- package/workspace/public/pm/c3-terminal.js +2 -2
- package/workspace/public/pm/calendar-views.js +1 -1
- package/workspace/public/pm/components.css +319 -213
- package/workspace/public/pm/console3.css +99 -23
- package/workspace/public/pm/dest-notes.js +26 -4
- package/workspace/public/pm/notes-screen-capture.js +165 -0
- package/workspace/public/pm/notes-screen-view.js +98 -0
- package/workspace/public/pm/panel-trees.js +5 -2
- package/workspace/public/pm/pm-work-view.js +233 -146
- package/workspace/public/pm/shell-bell.js +3 -2
- package/workspace/public/pm/shell-recording.js +29 -8
- package/workspace/public/pm/tokens.css +2 -2
- package/workspace/public/pm/view-notes.js +46 -9
- package/workspace/public/pm/welcome.js +197 -89
- package/workspace/public/welcome.html +6 -4
- package/workspace/server.js +105 -6
- package/workspace/skills/day-check.skill +1 -1
- package/workspace/skills/jira-sync.skill +6 -4
- package/workspace/skills/morning-brief.skill +1 -1
- package/workspace/test-files.json +12 -3
- package/workspace/test-lists/c3-ib-items.json +9 -0
- package/workspace/test-lists/c3-ib-more.json +9 -0
- package/workspace/test-lists/c3-ib-triage-backlog.json +9 -0
- package/workspace/test-lists/c3-initiative-boards.json +6 -0
- package/workspace/test-lists/calendar-tidy-up.json +6 -0
- package/workspace/test-lists/console3-briefing.json +9 -0
- package/workspace/test-lists/console3-foundation.json +9 -0
- package/workspace/test-lists/console3-reports.json +3 -3
- package/workspace/test-lists/meeting-notes.json +10 -4
- package/workspace/test-lists/workspace-headless-jobs.json +5 -0
- package/workspace/test-lists/workspace-package-script-closure.json +6 -0
- package/workspace/lib/vendor/workspace/lib/actor.js +0 -91
- package/workspace/lib/vendor/workspace/lib/board-cache.js +0 -156
- package/workspace/lib/vendor/workspace/lib/console-state.js +0 -295
- package/workspace/lib/vendor/workspace/lib/factory-join.js +0 -158
- package/workspace/lib/vendor/workspace/lib/git-info.js +0 -306
- package/workspace/lib/vendor/workspace/lib/plan-items.js +0 -922
- package/workspace/lib/vendor/workspace/lib/plan-tree.js +0 -419
- package/workspace/lib/vendor/workspace/lib/review-gate.js +0 -39
- package/workspace/lib/vendor/workspace/lib/sink.js +0 -321
- package/workspace/lib/vendor/workspace/lib/state-root.js +0 -587
|
@@ -201,7 +201,6 @@
|
|
|
201
201
|
.c3 .vz .chart{max-width:540px}
|
|
202
202
|
.c3{container-type:inline-size}
|
|
203
203
|
.c3 .brief{position:relative;isolation:isolate;display:grid;gap:16px}
|
|
204
|
-
.c3 .brief::before{content:"";position:absolute;left:-24px;right:-24px;top:-28px;height:380px;background:radial-gradient(60% 100% at 12% 0%,var(--accw),transparent 70%);z-index:-1;pointer-events:none}
|
|
205
204
|
.c3 .bcols{display:grid;grid-template-columns:minmax(0,1.45fr) minmax(300px,1fr);gap:20px;align-items:start}
|
|
206
205
|
.c3 .bcol{display:grid;gap:16px;min-width:0}
|
|
207
206
|
.c3 .bcol.r{position:sticky;top:12px}
|
|
@@ -494,8 +493,6 @@
|
|
|
494
493
|
/* console3.css addition: the Calendar page (c3-calendar.js, initiative calendar-time-page). Classes start with cal3-, colours are tokens.
|
|
495
494
|
* The controls are primitives (button()), so each rule below sets the box itself: it wins over components.css when that sheet is loaded. */
|
|
496
495
|
.c3 .cal3{display:grid;gap:var(--sp-3);min-width:0}
|
|
497
|
-
.c3 .cal3-head{display:flex;gap:4px 12px;align-items:baseline;flex-wrap:wrap}
|
|
498
|
-
.c3 .cal3-head h2{margin:0;font:600 var(--fs-5) var(--sans)}
|
|
499
496
|
.c3 .cal3 .calbar{margin-bottom:0}
|
|
500
497
|
.c3 .cal3-chips{display:flex;gap:6px;flex-wrap:wrap;align-items:center}
|
|
501
498
|
.c3 .cal3 button.cal3-chip{display:inline-flex;align-items:center;gap:6px;height:auto;min-height:30px;padding:3px 10px;border:1px solid var(--border);border-radius:8px;background:var(--card);color:var(--muted-foreground);font:500 var(--fs-2) var(--sans);cursor:pointer;white-space:nowrap}
|
|
@@ -528,7 +525,9 @@
|
|
|
528
525
|
.c3 .tag.cal3-tone-warn{color:var(--warning)}
|
|
529
526
|
.c3 .tag.cal3-tone-bad{color:var(--destructive)}
|
|
530
527
|
.c3 .tag.cal3-up{color:var(--acc)}
|
|
531
|
-
.c3 .cal3-more,.c3 .cal3-
|
|
528
|
+
.c3 .cal3-more,.c3 .cal3 button.cal3-more{font:var(--fs-1) var(--mono);color:var(--muted-foreground)}
|
|
529
|
+
.c3 .cal3 button.cal3-more{display:inline-flex;align-items:center;height:auto;min-height:20px;padding:0 6px;border:0;border-radius:6px;background:transparent;cursor:pointer}
|
|
530
|
+
@media (hover:hover){.c3 .cal3 button.cal3-more:hover{background:var(--accw);color:var(--foreground)}}
|
|
532
531
|
.c3 .cal3-cnt{font:var(--fs-1) var(--mono);color:var(--acc)}
|
|
533
532
|
/* Day and Week: the timeline, its day heads and the all-day marker strip */
|
|
534
533
|
.c3 .cal3-tg{min-width:0}
|
|
@@ -545,9 +544,29 @@
|
|
|
545
544
|
.c3 .vday.cal3-col{height:var(--cal3-hgt);margin-top:0;border-left:1px solid var(--border);background:repeating-linear-gradient(to bottom,var(--border) 0 1px,transparent 1px var(--cal3-pph))}
|
|
546
545
|
.c3 .vday.cal3-col.we{background-color:var(--muted)}
|
|
547
546
|
.c3 .vday.cal3-col.today{background-color:var(--accw)}
|
|
548
|
-
.c3 .vday .cal3-free{left:2px;right:2px;top:var(--cal3-y);height:var(--cal3-h);display:flex;flex-direction:column;align-items:
|
|
549
|
-
.c3 .vday .cal3-free b{font:
|
|
550
|
-
|
|
547
|
+
.c3 .vday .cal3-free{left:2px;right:2px;top:var(--cal3-y);height:var(--cal3-h);display:flex;flex-direction:column;align-items:center;justify-content:center;padding:2px 6px;overflow:hidden;border:1px dashed var(--focus-window);background:color-mix(in oklab,var(--focus-window) 7%,transparent);z-index:1}
|
|
548
|
+
.c3 .vday .cal3-free b{font:var(--fs-2) var(--mono);color:var(--focus-window)}
|
|
549
|
+
/* the day header: the day button with its hours, and the load bar (tied to a promise or decision, routine, the rest) */
|
|
550
|
+
.c3 .cal3-dh{display:grid;gap:2px;align-content:start}
|
|
551
|
+
.c3 .cal3-dh .dhl{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:0 6px}
|
|
552
|
+
.c3 .cal3-dh .hrs{font:var(--fs-1) var(--mono);color:var(--muted-foreground);white-space:nowrap}
|
|
553
|
+
.c3 .cal3-dh .dhl button.cal3-dbtn{white-space:nowrap;padding:2px 2px;min-width:0}
|
|
554
|
+
.c3 .cal3-dh .lbar{height:5px;margin:0 4px 4px}
|
|
555
|
+
.c3 .cal3-dh .lbar i{width:var(--cal3-w,0);background:var(--muted)}
|
|
556
|
+
.c3 .cal3-dh .lbar i.tied{background:var(--acc)}
|
|
557
|
+
.c3 .cal3-dh .lbar i.rt{background:var(--ring)}
|
|
558
|
+
.c3 .cal3-dh .lbar i.rest{flex:1;width:auto;background:var(--muted)}
|
|
559
|
+
/* the legend keys: a shape and a word */
|
|
560
|
+
.c3 .legend .lgk{display:inline-block;width:9px;height:9px;margin-right:4px;background:var(--muted-foreground)}
|
|
561
|
+
.c3 .legend .lgk.tied{background:var(--acc)}
|
|
562
|
+
.c3 .legend .lgk.fw{border:1px dashed var(--focus-window);background:transparent}
|
|
563
|
+
.c3 .legend .lgk.rel{transform:rotate(45deg) scale(.8)}
|
|
564
|
+
.c3 .legend .lgk.pro{transform:rotate(45deg) scale(.8);background:transparent;border:1px solid var(--muted-foreground)}
|
|
565
|
+
.c3 .legend .lgk.rep{width:6px;height:6px}
|
|
566
|
+
.c3 .legend .lgfw{color:var(--focus-window)}
|
|
567
|
+
.c3 .sheet.calpop .calpop-esc{font:var(--fs-1) var(--mono)}
|
|
568
|
+
.c3 .cal3-grid .legend{padding:var(--sp-2) var(--sp-2) 0}
|
|
569
|
+
.c3 .cal3-tidyl .iacts,.c3 .cal3-tidy-i .iacts{display:flex;flex-wrap:wrap;gap:8px;align-items:center;flex:1 1 100%}
|
|
551
570
|
.c3 .vday button.cal3-ev{top:var(--cal3-y);height:var(--cal3-h);min-height:0;left:calc(2px + (100% - 4px) * var(--cal3-l) / var(--cal3-lc));right:auto;width:calc((100% - 4px) / var(--cal3-lc) - 2px);display:block;padding:3px 8px;z-index:2;white-space:nowrap;cursor:pointer}
|
|
552
571
|
.c3 .vday button.cal3-ev>span{display:block;overflow:hidden}
|
|
553
572
|
.c3 .vday .cal3-ev b{display:block;overflow:hidden;text-overflow:ellipsis;font-weight:500}
|
|
@@ -590,6 +609,17 @@
|
|
|
590
609
|
.c3 .cal3 a.cal3-ref{display:inline-flex;gap:4px;align-items:center;color:var(--acc);font:500 var(--fs-2) var(--sans);text-decoration:underline;text-underline-offset:2px}
|
|
591
610
|
.c3 .cal3-lane{display:grid;grid-template-columns:auto auto minmax(0,1fr);gap:8px;align-items:center;font-size:var(--fs-2)}
|
|
592
611
|
.c3 .cal3-why{text-align:right}
|
|
612
|
+
/* the tidy-up: a meeting a suggestion names carries an inset ring in the tone of its most severe suggestion; the panel groups by severity */
|
|
613
|
+
.c3 .cal3 button.cal3-tidy-ev{--cal3-tc:var(--foreground);box-shadow:inset 0 0 0 2px var(--cal3-tc)}
|
|
614
|
+
.c3 .cal3 button.cal3-tidy-ev.cal3-tidy-warn{--cal3-tc:var(--warning)}
|
|
615
|
+
.c3 .cal3 button.cal3-tidy-ev.cal3-tidy-bad{--cal3-tc:var(--destructive)}
|
|
616
|
+
.c3 .cal3-ctx h4.cal3-tone-warn{color:var(--warning)}
|
|
617
|
+
.c3 .cal3-ctx h4.cal3-tone-bad{color:var(--destructive)}
|
|
618
|
+
.c3 .cal3-tidy-i{--cal3-tc:var(--border);border-left:3px solid var(--cal3-tc);padding-left:8px;overflow-wrap:anywhere;min-width:0}
|
|
619
|
+
.c3 .cal3-tidy-i.cal3-tidy-warn{--cal3-tc:var(--warning)}
|
|
620
|
+
.c3 .cal3-tidy-i.cal3-tidy-bad{--cal3-tc:var(--destructive)}
|
|
621
|
+
.c3 .cal3-tidy-t,.c3 .cal3-tidy-why,.c3 .cal3-tidy-fix{flex:1 1 100%;min-width:0}
|
|
622
|
+
.c3 .cal3-tidy-refs{display:flex;flex-wrap:wrap;gap:4px 12px;min-width:0}
|
|
593
623
|
.c3 .cal3 button.cal3-close{margin-left:auto;height:auto;min-height:24px;padding:2px 8px;border:1px solid var(--border);border-radius:6px;background:transparent;color:var(--muted-foreground);font:500 var(--fs-1) var(--mono);cursor:pointer}
|
|
594
624
|
.c3 .cal3 button:focus-visible,.c3 .cal3 a:focus-visible{outline:2px solid color-mix(in oklab, var(--ring) 50%, var(--foreground));outline-offset:2px}
|
|
595
625
|
@media (hover:hover){
|
|
@@ -1212,13 +1242,13 @@
|
|
|
1212
1242
|
.c3 .sheet:focus{outline:none}
|
|
1213
1243
|
.c3 .sheet.bs.dragging{transition:none}
|
|
1214
1244
|
.c3 .sheet .sh2 .ib{flex:none}
|
|
1215
|
-
/* the calendar popup (c3-calpop.js): the
|
|
1216
|
-
.c3 .sheet.calpop{left:12px;right:12px;margin-inline:auto;width:min(1100px,calc(100% - 24px))}
|
|
1245
|
+
/* the calendar popup (c3-calpop.js): the Calendar page's grid in a 780px drawer (.sheet.dw), the whole screen on a phone */
|
|
1217
1246
|
.c3 .sheet.calpop .sb{padding:var(--sp-3);min-width:0}
|
|
1218
1247
|
.c3 .calpop-host{min-width:0;max-width:100%}
|
|
1219
1248
|
.c3.ph .sheet.calpop{top:0;left:0;right:0;bottom:0;width:auto;max-height:none;border-radius:0;margin:0}
|
|
1220
1249
|
.c3.ph .sheet.bs.calpop{max-height:none;animation:none}
|
|
1221
1250
|
.c3 .sheet.calpop .ib,.c3 .sheet.calpop .seg button,.c3 .sheet.calpop .tdy{min-height:44px;min-width:44px}
|
|
1251
|
+
.c3 .sheet.calpop .cal3 button.cal3-chip{min-height:44px}
|
|
1222
1252
|
@media (prefers-reduced-motion:reduce){.c3 .sheet,.c3 .scrim,.c3.ph .sheet.bs{animation:none}}
|
|
1223
1253
|
/* a slide that cannot be used: the action goes outside and no outbox is connected. The reason sits under it (c3-case.js). */
|
|
1224
1254
|
.c3 .slt.off{opacity:.55;--tc:var(--muted-foreground)}
|
|
@@ -1603,9 +1633,20 @@
|
|
|
1603
1633
|
@media (max-width:760px){.c3 .psess{min-height:44px}}
|
|
1604
1634
|
.c3 .psess .c{font:var(--fs-1) var(--mono)}
|
|
1605
1635
|
/* Briefing as the brief (initiative briefing-brief): one column; Today agenda; own-item rows of Needs you; inline Portfolio pulse */
|
|
1606
|
-
|
|
1607
|
-
.c3 .
|
|
1608
|
-
.c3 .
|
|
1636
|
+
/* Briefing, console3-briefing-parity: the day-part switch in the stance bar, the tags, the Today rows */
|
|
1637
|
+
.c3 .stance .seg.dp{flex:none}
|
|
1638
|
+
.c3 .stance{flex-wrap:wrap}
|
|
1639
|
+
.c3 .tag.acc{background:var(--accw);color:var(--acc)}
|
|
1640
|
+
.c3 .tag.ok{color:var(--foreground)}
|
|
1641
|
+
.c3 .tag.warn{color:var(--warning)}
|
|
1642
|
+
.c3 .tag.bad{color:var(--destructive)}
|
|
1643
|
+
.c3 .tag.opp{color:var(--acc)}
|
|
1644
|
+
.c3 .brief .nrow.over{opacity:.6}
|
|
1645
|
+
.c3 .lh-main{min-width:0}
|
|
1646
|
+
.c3 .lh svg text{font-size:var(--fs-2)}
|
|
1647
|
+
.c3 .pulse-attn{font-size:var(--fs-1)}
|
|
1648
|
+
.c3 .pulse-foot{justify-content:space-between}
|
|
1649
|
+
.c3 .brief .lh .rg{transition:stroke-dasharray .4s}
|
|
1609
1650
|
.c3 .c3s2{font-size:var(--fs-2)}
|
|
1610
1651
|
.c3 .c3s3{font-size:var(--fs-3)}
|
|
1611
1652
|
.c3 .ki.t-bad{color:var(--destructive)}
|
|
@@ -1647,7 +1688,7 @@
|
|
|
1647
1688
|
.c3[data-daypart="afternoon"]{--dp-tint:color-mix(in oklab,var(--warning) 4%,transparent);--dp-warm:var(--warning)}
|
|
1648
1689
|
.c3[data-daypart="evening"]{--dp-tint:color-mix(in oklab,var(--destructive) 4%,transparent);--dp-warm:var(--destructive)}
|
|
1649
1690
|
.c3[data-daypart="night"]{--dp-tint:color-mix(in oklab,var(--muted-foreground) 6%,transparent);--dp-warm:var(--foreground)}
|
|
1650
|
-
|
|
1691
|
+
/* The page stays white: the day part keeps --dp-warm for the daylight strip and no longer washes the background. */
|
|
1651
1692
|
/* The daylight strip in the top bar: an arc, a sun or a moon on it, and the two times. 22px tall, well under the 32px limit. */
|
|
1652
1693
|
.c3 .c3top .c3pg-daylight{order:1;display:inline-flex;align-items:center;gap:8px;height:22px;max-height:32px;overflow:hidden;font:var(--fs-1)/1 var(--mono);color:var(--muted-foreground);white-space:nowrap}
|
|
1653
1694
|
.c3 .c3top .tools{order:2}
|
|
@@ -1736,25 +1777,37 @@
|
|
|
1736
1777
|
.c3 .wkoff-o input[type=text]{width:100%}
|
|
1737
1778
|
.c3 .wkev{display:flex;flex-wrap:wrap;gap:var(--sp-2);align-items:center;border:1px dashed var(--warning);border-radius:8px;padding:var(--sp-2) var(--sp-3);font-size:var(--fs-2)}
|
|
1738
1779
|
.c3 .wkev>.wk-min{flex:1 1 200px}
|
|
1739
|
-
.c3 .wkfg{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,240px),1fr));gap:var(--sp-3)}
|
|
1740
|
-
.c3 .wkfc{border:1px solid var(--border);background:var(--card);border-radius:8px;padding:var(--sp-3);display:grid;gap:var(--sp-2);align-content:start;min-width:0}
|
|
1741
|
-
.c3 .wkfc.on{border-color:var(--acc);background:var(--accw)}
|
|
1742
|
-
.c3 .wkfc-t{font-size:var(--fs-3);line-height:1.3;overflow-wrap:anywhere}
|
|
1743
1780
|
.c3 .wkfc-why{margin:0;font-size:var(--fs-2);color:var(--muted-foreground)}
|
|
1744
|
-
.c3 .wkfc-r{margin:0;padding:0;list-style:none;display:grid;gap:2px;font-size:var(--fs-2)}
|
|
1745
|
-
.c3 .wkfc-r li{display:flex;gap:4px;align-items:center;min-width:0}
|
|
1746
|
-
.c3 .wkfc .wkq{margin-top:0}
|
|
1747
1781
|
.c3 .wkdays{display:grid}
|
|
1748
|
-
.c3 .wkday h3{margin:0;padding:var(--sp-
|
|
1749
|
-
.c3 .wkday h3 .dim{font-weight:400}
|
|
1782
|
+
.c3 .wkday h3.wkdayh{margin:0;padding:var(--sp-3) 0 var(--sp-1);font:500 var(--fs-2) var(--mono);color:var(--muted-foreground);display:flex;gap:var(--sp-2);align-items:baseline;text-transform:none;letter-spacing:0}
|
|
1783
|
+
.c3 .wkday h3.wkdayh .dim{font-weight:400}
|
|
1750
1784
|
.c3 .wkday-none{color:var(--muted-foreground);font-size:var(--fs-2);padding:0 0 var(--sp-2);border-bottom:1px solid var(--border)}
|
|
1785
|
+
/* console3-work-parity: the mockup's Work rows have a button at the right, so the swipe glyphs give way (the swipe itself stays) */
|
|
1786
|
+
.c3 .wk .sh2{flex-wrap:wrap}
|
|
1787
|
+
.c3 .wk .brief .nrow{grid-template-columns:auto minmax(0,1fr) auto}
|
|
1788
|
+
.c3 .wk .brief .nrow .nb{padding-right:0}
|
|
1789
|
+
.c3 .wk .brief .nrow>.sg{display:none}
|
|
1790
|
+
.c3 .wk .brief .nrow>.acts{align-self:center}
|
|
1791
|
+
.c3.ph .wk .brief .nrow>.acts{grid-column:auto}
|
|
1792
|
+
.c3 .card h3 .wk-nocase{text-transform:none;letter-spacing:0}
|
|
1793
|
+
.c3 .wkfr .pbar{width:90px}
|
|
1794
|
+
.c3 .wkfr .pbar i{width:var(--wl-w,0)}
|
|
1795
|
+
.c3 .wkfr-b{margin-top:6px}
|
|
1796
|
+
.c3 .flow>div i{width:var(--wl-w,0)}
|
|
1797
|
+
.c3 .flow .wkm-captured{--c:var(--muted-foreground)}
|
|
1798
|
+
.c3 .flow .wkm-clarified{--c:var(--acc)}
|
|
1799
|
+
.c3 .flow .wkm-committed{--c:var(--warning)}
|
|
1800
|
+
.c3 .flow .wkm-done{--c:var(--foreground)}
|
|
1801
|
+
.c3 .wkjob{max-width:100%}
|
|
1802
|
+
.c3 .wkfr .form input{width:100%}
|
|
1803
|
+
|
|
1751
1804
|
.c3 .wkrv{gap:var(--sp-3)}
|
|
1752
1805
|
.c3 .wkrv-p{display:flex;justify-content:space-between;font:var(--fs-2) var(--mono);color:var(--muted-foreground)}
|
|
1753
1806
|
.c3 .wkrv-t{margin:0;font:600 var(--fs-4)/1.3 var(--sans);color:var(--foreground);overflow-wrap:anywhere}
|
|
1754
1807
|
.c3 .wkrv-x{display:grid;gap:var(--sp-2)}
|
|
1755
1808
|
@media (max-width:760px){
|
|
1756
1809
|
.c3 .wkq input[type=date],.c3 .wkq-f input[type=text]{min-height:44px;max-width:none}
|
|
1757
|
-
.c3 .wkoff-o input[type=text],.c3 .
|
|
1810
|
+
.c3 .wkoff-o input[type=text],.c3 .wkfr input[type=text]{min-height:44px}
|
|
1758
1811
|
}
|
|
1759
1812
|
|
|
1760
1813
|
.c3 .wk-sm{font-size:var(--fs-2)}
|
|
@@ -1762,3 +1815,26 @@
|
|
|
1762
1815
|
.c3 .wk-bar::-webkit-progress-bar{background:var(--muted)}
|
|
1763
1816
|
.c3 .wk-bar::-webkit-progress-value{background:var(--acc);border-radius:6px}
|
|
1764
1817
|
.c3 .wk-bar::-moz-progress-bar{background:var(--acc);border-radius:6px}
|
|
1818
|
+
|
|
1819
|
+
/* console3 foundation: ambient motion background (c3-ambient.js). One fixed canvas behind the page; the grid sits above it. */
|
|
1820
|
+
.c3 .c3amb{position:fixed;inset:0;width:100vw;height:100vh;z-index:0;pointer-events:none}
|
|
1821
|
+
.c3 .c3grid{position:relative;z-index:1}
|
|
1822
|
+
/* Build fix from the mockup (the primary button label colour is in components.css): console3 styled .sect .sh2 only under .brief. */
|
|
1823
|
+
.c3 .sect > .sh2{display:flex;justify-content:space-between;align-items:center;gap:8px;flex-wrap:wrap;padding:var(--sp-3) 0 var(--sp-2);border-bottom:1px solid var(--border)}
|
|
1824
|
+
.c3 .sect > .sh2 h2{margin:0;font:500 var(--fs-2) var(--mono);color:var(--muted-foreground);text-transform:uppercase;letter-spacing:.06em;display:flex;gap:8px;align-items:center}
|
|
1825
|
+
|
|
1826
|
+
/* console3 foundation: the front shows the number tiles; the flipped back grows to fit its content (options and the slide are never clipped). */
|
|
1827
|
+
.c3 .cfh .kps{display:flex;gap:var(--sp-2);flex-wrap:wrap;justify-content:flex-end;align-content:center}
|
|
1828
|
+
.c3 .fc.flipcard.back{height:auto;overflow:visible}
|
|
1829
|
+
.c3 .fc.flipcard.back .fb{position:relative;inset:auto;overflow:visible;min-height:var(--fc-h)}
|
|
1830
|
+
.c3 .fc.flipcard.back .ff{position:absolute;inset:0}
|
|
1831
|
+
@container card (max-width:399px){
|
|
1832
|
+
.c3 .fc .cf .cfh .kps{justify-content:stretch}
|
|
1833
|
+
}
|
|
1834
|
+
/* Number-tile tones: the tile names its colour by class, not by an inline style. */
|
|
1835
|
+
.c3 .kp.k-acc{--c:var(--acc)}
|
|
1836
|
+
.c3 .kp.k-destructive{--c:var(--destructive)}
|
|
1837
|
+
.c3 .kp.k-warning{--c:var(--warning)}
|
|
1838
|
+
.c3 .kp.k-foreground{--c:var(--foreground)}
|
|
1839
|
+
.c3 .kp.k-muted-foreground{--c:var(--muted-foreground)}
|
|
1840
|
+
.c3 .psess{display:inline-flex;gap:6px;align-items:center}
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
import { el, patchChildren } from './dom.js';
|
|
27
27
|
import { button, iconButton, field, row, menu, toast, empty, ensureComponentsCss } from './primitives.js';
|
|
28
28
|
import { onNotes } from './notes-links.js';
|
|
29
|
+
import { screenStrip, shotButton, shotFor } from './notes-screen-view.js';
|
|
29
30
|
import { agendaHeard, peopleOf } from './notes-context.js';
|
|
30
31
|
import { ensureSprite, icon } from './icons.js';
|
|
31
32
|
import { pmGet, pmPost } from './http.js';
|
|
@@ -69,7 +70,10 @@ export const COPY = Object.freeze({
|
|
|
69
70
|
transcript: (n) => `${n} transcript ${n === 1 ? 'line' : 'lines'}`,
|
|
70
71
|
del: 'Delete note',
|
|
71
72
|
delAsk: (t) => `Delete “${t}”?`,
|
|
72
|
-
delBody: 'The note, its transcript and its
|
|
73
|
+
delBody: 'The note, its transcript, its audio and its screenshots are deleted from this machine.',
|
|
74
|
+
delShots: 'Delete screenshots',
|
|
75
|
+
delShotsAsk: 'Delete this note\'s screenshots?',
|
|
76
|
+
delShotsBody: 'The screenshots of the shared screen are deleted from this machine. The notes, the transcript and the audio stay. This cannot be undone.',
|
|
73
77
|
saved: 'Saved',
|
|
74
78
|
notSaved: 'Not saved',
|
|
75
79
|
edited: 'edited',
|
|
@@ -218,6 +222,7 @@ async function loadOpen(inst) {
|
|
|
218
222
|
if (inst.open !== id) return;
|
|
219
223
|
inst.note = r && r.ok ? r.note : null;
|
|
220
224
|
inst.rec = r && r.ok ? r.recording || null : null;
|
|
225
|
+
inst.shots = r && r.ok && r.screen ? r.screen.frames || [] : [];
|
|
221
226
|
drawRight(inst);
|
|
222
227
|
}
|
|
223
228
|
/** The replay of the note's kept audio: one <audio> per note, kept across redraws so playback goes on. Null when no audio is kept. */
|
|
@@ -276,7 +281,7 @@ function drawRight(inst, force) {
|
|
|
276
281
|
const n = inst.note;
|
|
277
282
|
const live = dock.recordState();
|
|
278
283
|
const held = !!n && live.mode !== 'idle' && live.id === n.id;
|
|
279
|
-
const sig = JSON.stringify([inst.open, n && [n.updated_at, n.title, n.final_notes && n.final_notes.at, (n.transcript || []).length, (n.highlights || []).length, n.agent && n.agent.markdown, iniName(n.initiative), n.state, n.resume_until, n.ended_at, (n.sessions || []).length, n.meeting && n.meeting.context && n.meeting.context.taken_at], inst.rec && [inst.rec.bytes, inst.rec.cleaned], live.mode, live.id, !!inst.rows, inst.ctxOpen, inst.info]);
|
|
284
|
+
const sig = JSON.stringify([inst.open, n && [n.updated_at, n.title, n.final_notes && n.final_notes.at, (n.transcript || []).length, (n.highlights || []).length, n.agent && n.agent.markdown, iniName(n.initiative), n.state, n.resume_until, n.ended_at, (n.sessions || []).length, n.meeting && n.meeting.context && n.meeting.context.taken_at], inst.rec && [inst.rec.bytes, inst.rec.cleaned], (inst.shots || []).length, live.mode, live.id, !!inst.rows, inst.ctxOpen, inst.info]);
|
|
280
285
|
// a held note draws again on every render: the clock, the lines and the paused minutes move. Nothing else draws without a change.
|
|
281
286
|
if (!force && !held && sig === inst.rightSig) return;
|
|
282
287
|
// a field being typed in is not redrawn under the cursor, except in the live block, whose fields are kept across redraws
|
|
@@ -387,8 +392,9 @@ function noteParts(inst, n, live) {
|
|
|
387
392
|
{ label: COPY.linkMeeting, icon: 'link', onSelect: () => V.linkMeeting({ state: inst.ctx.state, setState: inst.ctx.setState }, n.id), attrs: { 'data-ntm-more-meeting': '' } },
|
|
388
393
|
{ label: COPY.linkInitiative, icon: 'target', onSelect: () => V.linkInitiative({ state: inst.ctx.state, setState: inst.ctx.setState }, n.id), attrs: { 'data-ntm-more-initiative': '' } },
|
|
389
394
|
{ sep: true },
|
|
395
|
+
(inst.shots || []).length ? { label: COPY.delShots, danger: true, onSelect: () => deleteShots(inst, n), attrs: { 'data-ntm-del-shots': '' } } : null,
|
|
390
396
|
{ label: COPY.del, danger: true, onSelect: () => deleteNote(inst, n) },
|
|
391
|
-
] }) });
|
|
397
|
+
].filter(Boolean) }) });
|
|
392
398
|
acts.push(more);
|
|
393
399
|
const out = [
|
|
394
400
|
el('div', { class: 'nt-pg-top' },
|
|
@@ -408,6 +414,9 @@ function noteParts(inst, n, live) {
|
|
|
408
414
|
if (replay) out.push(el('section', { class: 'nt-pg-sec nt-pg-sec--first nt-pg-replay', 'data-ntm': 'replay' },
|
|
409
415
|
el('h3', { class: 'nt-pg-sec__h' }, el('span', null, 'Replay')), replay,
|
|
410
416
|
el('p', { class: 'nt-hint' }, 'Kept on this machine. Press a time in the discussion to play from there.')));
|
|
417
|
+
// the screenshots of the shared screen, on the same clock as the replay and the transcript (notes-screen-capture)
|
|
418
|
+
const shots = held || n.private ? [] : inst.shots || [];
|
|
419
|
+
if (shots.length) out.push(screenStrip({ id: n.id, frames: shots, seek: replay ? (ms) => seekReplay(inst, ms) : null, replay }));
|
|
411
420
|
|
|
412
421
|
const cf = conflictsOf(n);
|
|
413
422
|
if (cf.length) {
|
|
@@ -460,7 +469,12 @@ function noteParts(inst, n, live) {
|
|
|
460
469
|
out.push(el('details', { class: 'nt-pg-sec nt-pg-fold', 'data-ntm': 'discussion' },
|
|
461
470
|
el('summary', { class: 'nt-pg-fold__sum' }, COPY.discussion + ' · ' + bits),
|
|
462
471
|
p.topics.map((tp) => el('div', { class: 'nt-pg-topic' }, el('h4', { class: 'nt-pg-topic__h' }, tp.title), (tp.points || []).map((pt) => el('p', { class: 'nt-pg-topic__p' }, pt)))),
|
|
463
|
-
tr.length ? el('div', { class: 'nt-pg-trans' }, tr.map((x
|
|
472
|
+
tr.length ? el('div', { class: 'nt-pg-trans' }, tr.map((x, k) => {
|
|
473
|
+
// the shot that came up while this line was said, as a small picture beside it
|
|
474
|
+
const sh = shots.length ? shotFor(shots, x.t || 0, k + 1 < tr.length ? tr[k + 1].t || 0 : Infinity) : null;
|
|
475
|
+
return el('div', { class: 'nt-pg-li' }, inst.audio && !held ? button({ label: V.fmtTime(x.t || 0), variant: 'ghost', size: 'sm', onClick: () => seekReplay(inst, x.t || 0), attrs: { 'data-ntm-seek': '', title: 'Play from here' } }) : el('span', { class: 'nt-pg-li__at' }, V.fmtTime(x.t || 0)), el('span', { class: 'nt-pg-li__text' }, x.text),
|
|
476
|
+
sh ? shotButton({ id: n.id, frames: shots, frame: sh, seek: inst.audio ? (ms) => seekReplay(inst, ms) : null }) : null);
|
|
477
|
+
})) : null));
|
|
464
478
|
}
|
|
465
479
|
return out.filter(Boolean);
|
|
466
480
|
}
|
|
@@ -498,6 +512,14 @@ function openFile(inst, doc) {
|
|
|
498
512
|
import('./panel-tabs.js').then((m) => m.openDoc(inst.repo, doc, 'notes')).catch(() => toast({ text: 'The file could not be opened.', tone: 'destructive' }));
|
|
499
513
|
}
|
|
500
514
|
|
|
515
|
+
async function deleteShots(inst, n) {
|
|
516
|
+
const sure = await askConfirm({ title: COPY.delShotsAsk, body: COPY.delShotsBody, confirmLabel: 'Delete', cancelLabel: 'Keep them' });
|
|
517
|
+
if (!sure) return;
|
|
518
|
+
const r = await pmPost('/api/notes/screen-delete', { id: n.id });
|
|
519
|
+
if (!r || !r.ok) { toast({ text: (r && r.error) || 'The screenshots were not deleted.', tone: 'destructive' }); return; }
|
|
520
|
+
inst.shots = [];
|
|
521
|
+
drawRight(inst, true);
|
|
522
|
+
}
|
|
501
523
|
async function deleteNote(inst, n) {
|
|
502
524
|
const sure = await askConfirm({ title: COPY.delAsk(n.title || 'Untitled note'), body: COPY.delBody, confirmLabel: 'Delete', cancelLabel: 'Keep it' });
|
|
503
525
|
if (!sure) return;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/* notes-screen-capture.js — screenshots of the shared screen while a note listens (notes-screen-capture).
|
|
2
|
+
*
|
|
3
|
+
* startScreen({ track, id, cap, clock, post, … }) → { stop(), cue(t, why), hear(lines), count, lost } take shots of one shared screen
|
|
4
|
+
* cueOf(line) → why | null does a transcript line point at the screen, or decide something? Pure.
|
|
5
|
+
* greyOf(source, w, h, canvas?) → Uint8Array a small grey copy of a frame, for the comparison
|
|
6
|
+
* changed(prev, cur, { pixel?, share? }) → boolean true when enough of the picture changed. Pure.
|
|
7
|
+
*
|
|
8
|
+
* WHERE THE PICTURE COMES FROM. Listen already asks the browser to share the screen, to hear the meeting (view-notes.js
|
|
9
|
+
* openSource). That share carries the picture too. The browser's own sharing window is the PM's permission, and the browser
|
|
10
|
+
* shows that it is sharing for as long as the track is live. This module never asks for a screen of its own.
|
|
11
|
+
*
|
|
12
|
+
* WHEN IT KEEPS A SHOT. It follows the meeting, not a timer. Every `every` ms (2 s) the frame is drawn into a 64 × 36 grey copy.
|
|
13
|
+
* - settled: the picture changed from the last kept shot (more than `share`, 2 %, of the small pixels moved by more than `pixel`,
|
|
14
|
+
* 24 of 255) and then held still for one check. So a slide is kept once it is drawn, a scroll once it stops, and the frames of
|
|
15
|
+
* a transition are not kept. A cursor, a pointer or compression noise is not a change. After a run of movement it takes two
|
|
16
|
+
* still checks, because a video can repeat one frame.
|
|
17
|
+
* - moving: a picture that never holds still (a video, an animation) is sampled once every `slowGap` ms (15 s) while it moves.
|
|
18
|
+
* - said: a transcript line that points at the screen ("as you can see", "this slide", "let me share") or that the workspace
|
|
19
|
+
* labels a decision or a next step keeps the picture that was on screen when the line was said. The page keeps the changed
|
|
20
|
+
* pictures of the last RING_MS (20 s) in memory for this, because a line is transcribed some seconds after it is said.
|
|
21
|
+
* - highlight: the PM's Highlight keeps the picture on screen at that moment.
|
|
22
|
+
* A picture the last kept shot already shows is never kept again, whatever the reason. Each shot carries why it was kept.
|
|
23
|
+
* The shot is a JPEG of the frame at most MAX_W px wide. It is posted with the meeting time, the note and the capture token, so
|
|
24
|
+
* the server keeps it beside the transcript and refuses a second tab (lib/notes-screen.js).
|
|
25
|
+
*
|
|
26
|
+
* WHAT IT NEVER DOES. It sends a shot only to this workspace's own server (`post`), never anywhere else.
|
|
27
|
+
*/
|
|
28
|
+
const SMALL_W = 64;
|
|
29
|
+
const SMALL_H = 36;
|
|
30
|
+
const MAX_W = 1600;
|
|
31
|
+
const QUALITY = 0.72;
|
|
32
|
+
|
|
33
|
+
export function changed(prev, cur, { pixel = 24, share = 0.02 } = {}) {
|
|
34
|
+
if (!prev || !cur || prev.length !== cur.length) return true;
|
|
35
|
+
let moved = 0;
|
|
36
|
+
for (let i = 0; i < cur.length; i++) if (Math.abs(cur[i] - prev[i]) > pixel) moved++;
|
|
37
|
+
return moved / cur.length > share;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function greyOf(source, w = SMALL_W, h = SMALL_H, canvas = null) {
|
|
41
|
+
const c = canvas || document.createElement('canvas');
|
|
42
|
+
c.width = w; c.height = h;
|
|
43
|
+
const g = c.getContext('2d', { willReadFrequently: true });
|
|
44
|
+
g.drawImage(source, 0, 0, w, h);
|
|
45
|
+
const d = g.getImageData(0, 0, w, h).data;
|
|
46
|
+
const out = new Uint8Array(w * h);
|
|
47
|
+
for (let i = 0, j = 0; i < out.length; i++, j += 4) out[i] = (d[j] * 299 + d[j + 1] * 587 + d[j + 2] * 114) / 1000;
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const b64 = (blob) => new Promise((res, rej) => {
|
|
52
|
+
const r = new FileReader();
|
|
53
|
+
r.onload = () => res(String(r.result).slice(String(r.result).indexOf(',') + 1));
|
|
54
|
+
r.onerror = () => rej(r.error);
|
|
55
|
+
r.readAsDataURL(blob);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const RING_MS = 20000;
|
|
59
|
+
const CUES = /\b(as you can see|you can see (here|this|that|it)|on (the|my|your) screen|(this|that|the next|the previous|next|previous|last) (slide|chart|graph|diagram|table|dashboard|screen|page|demo|mock-?up|figure|picture|image)|let me (share|show)|i('| a)m (sharing|showing)|look(ing)? at (this|that|the screen)|shown here|right here|over here|scroll(ing)? (up|down)|zoom in|on screen)\b/i;
|
|
60
|
+
|
|
61
|
+
/** Why a transcript line asks for the picture on screen, or null. */
|
|
62
|
+
export function cueOf(line) {
|
|
63
|
+
if (!line || typeof line.text !== 'string') return null;
|
|
64
|
+
const m = CUES.exec(line.text);
|
|
65
|
+
if (m) return 'said: "' + m[0].toLowerCase() + '"';
|
|
66
|
+
if (line.cat === 'decision') return 'decision';
|
|
67
|
+
if (line.cat === 'action') return 'next step';
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function startScreen({ track, id, cap, clock, post, every = 2000, slowGap = 15000, ringMs = RING_MS }) {
|
|
72
|
+
const video = document.createElement('video');
|
|
73
|
+
video.muted = true; video.playsInline = true;
|
|
74
|
+
video.srcObject = new MediaStream([track]);
|
|
75
|
+
// in the page, though not seen: a browser stops painting new frames into a video element that is not in the document
|
|
76
|
+
video.className = 'nt-screen-feed';
|
|
77
|
+
video.setAttribute('aria-hidden', 'true');
|
|
78
|
+
document.body.appendChild(video);
|
|
79
|
+
video.play().catch(() => {});
|
|
80
|
+
const small = document.createElement('canvas');
|
|
81
|
+
const big = document.createElement('canvas');
|
|
82
|
+
const s = { count: 0, lost: 0, stopped: false, last: null, lastAt: 0, prev: null, moving: 0, still: 0, run: 0, ring: [], kept: new Map(), cues: [], heard: new Set(), chain: Promise.resolve(), timer: null };
|
|
83
|
+
|
|
84
|
+
const snap = async (t, grey) => {
|
|
85
|
+
const w = Math.min(MAX_W, video.videoWidth);
|
|
86
|
+
const h = Math.round(video.videoHeight * (w / video.videoWidth));
|
|
87
|
+
big.width = w; big.height = h;
|
|
88
|
+
big.getContext('2d').drawImage(video, 0, 0, w, h);
|
|
89
|
+
const blob = await new Promise((res) => big.toBlob(res, 'image/jpeg', QUALITY));
|
|
90
|
+
return blob ? { t: Math.max(0, Math.round(t)), grey, blob, w, h } : null;
|
|
91
|
+
};
|
|
92
|
+
// one post at a time, in order; a shot the last kept one already shows is dropped here
|
|
93
|
+
// a shot a line or a highlight points at that is already kept is posted again, so it carries that better reason
|
|
94
|
+
const keep = (shot, why, cue = false) => {
|
|
95
|
+
s.chain = s.chain.then(async () => {
|
|
96
|
+
if (s.stopped || !shot) return;
|
|
97
|
+
const again = cue && s.kept.has(shot.t) && !s.kept.get(shot.t).cue;
|
|
98
|
+
if (!again && !changed(s.last, shot.grey)) return;
|
|
99
|
+
const r = await post({ id, cap, t: shot.t, img: await b64(shot.blob), w: shot.w, h: shot.h, why });
|
|
100
|
+
if (r && r.ok) {
|
|
101
|
+
s.kept.set(shot.t, { cue });
|
|
102
|
+
if (again) return;
|
|
103
|
+
s.count++; s.last = shot.grey; s.lastAt = Date.now(); s.ring = s.ring.filter((x) => x.t >= shot.t);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
// the server stopped taking shots (switched off, the capture ended, the note went private): stop asking
|
|
107
|
+
if (r && (r.status === 409 || r.status === 404)) { stop(); return; }
|
|
108
|
+
s.lost++;
|
|
109
|
+
}).catch(() => { s.lost++; });
|
|
110
|
+
return s.chain;
|
|
111
|
+
};
|
|
112
|
+
/** The picture on screen at meeting time t, from memory: the last changed picture at or before it, else the first one just after. */
|
|
113
|
+
const at = (t) => { let pick = null; for (const x of s.ring) if (x.t <= t) pick = x; return pick || s.ring.find((x) => x.t > t && x.t - t <= 2500) || null; };
|
|
114
|
+
|
|
115
|
+
let busy = false;
|
|
116
|
+
const tick = async () => {
|
|
117
|
+
if (s.stopped || busy || track.readyState !== 'live' || !video.videoWidth) return;
|
|
118
|
+
busy = true;
|
|
119
|
+
try {
|
|
120
|
+
const now = clock();
|
|
121
|
+
// lines said earlier that asked for the picture, and highlights: their time has passed, so the memory has the picture
|
|
122
|
+
for (const c of s.cues.splice(0)) keep(at(c.t), c.why, true);
|
|
123
|
+
const grey = greyOf(video, SMALL_W, SMALL_H, small);
|
|
124
|
+
const moved = !!s.prev && changed(s.prev, grey);
|
|
125
|
+
s.prev = grey;
|
|
126
|
+
// a still check right after a run of movement may be one repeated frame of a video: then it takes two still checks
|
|
127
|
+
if (moved) { s.moving++; s.still = 0; } else { if (s.moving) s.run = s.moving; s.moving = 0; s.still++; }
|
|
128
|
+
const settled = !moved && s.still >= (s.run >= 2 ? 2 : 1);
|
|
129
|
+
if (settled && s.still >= 2) s.run = 0;
|
|
130
|
+
const fresh = changed(s.last, grey);
|
|
131
|
+
if (fresh) {
|
|
132
|
+
const shot = await snap(now, grey);
|
|
133
|
+
if (shot) { s.ring.push(shot); s.ring = s.ring.filter((x) => x.t >= now - ringMs); }
|
|
134
|
+
// settled: changed, and still since the last check (the very first picture counts as settled)
|
|
135
|
+
if (shot && (settled || !s.last)) keep(shot, s.last ? 'the screen changed' : 'sharing started');
|
|
136
|
+
// moving: a sample while it keeps moving
|
|
137
|
+
else if (shot && (s.moving >= 2 || s.run >= 2) && Date.now() - s.lastAt >= slowGap) keep(shot, 'the screen is moving');
|
|
138
|
+
}
|
|
139
|
+
} catch { s.lost++; } finally { busy = false; }
|
|
140
|
+
};
|
|
141
|
+
s.timer = setInterval(tick, every);
|
|
142
|
+
setTimeout(tick, 600); // the first frame of the share, without waiting a whole interval
|
|
143
|
+
function stop() {
|
|
144
|
+
if (s.stopped) return;
|
|
145
|
+
s.stopped = true;
|
|
146
|
+
clearInterval(s.timer);
|
|
147
|
+
try { track.stop(); } catch { /* stopped */ }
|
|
148
|
+
video.srcObject = null;
|
|
149
|
+
video.remove();
|
|
150
|
+
s.ring = [];
|
|
151
|
+
}
|
|
152
|
+
/** Keep the picture on screen at meeting time t, for a reason (the PM's Highlight, a cue). */
|
|
153
|
+
function cue(t, why) { if (!s.stopped) s.cues.push({ t, why }); }
|
|
154
|
+
/** New transcript lines: each one that asks for the screen keeps the picture of its time, once. */
|
|
155
|
+
function hear(lines) {
|
|
156
|
+
for (const l of lines || []) {
|
|
157
|
+
const key = l.t + '|' + l.source + '|' + l.text + '|' + (l.cat || ''); // a line labelled later is heard again
|
|
158
|
+
if (s.heard.has(key)) continue;
|
|
159
|
+
s.heard.add(key);
|
|
160
|
+
const why = cueOf(l);
|
|
161
|
+
if (why) cue(l.t || 0, why);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return { stop, cue, hear, get count() { return s.count; }, get lost() { return s.lost; } };
|
|
165
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/* notes-screen-view.js — the screenshots a note kept, beside its transcript (notes-screen-capture).
|
|
2
|
+
*
|
|
3
|
+
* screenStrip({ id, frames, seek?, replay? }) → <section> | null the Screen strip: one thumbnail per shot with its meeting time
|
|
4
|
+
* shotFor(frames, from, to) → frame | null the shot that came up while a line was said, [from, to)
|
|
5
|
+
* shotButton({ id, frames, frame, seek? }) → <button> a small thumbnail that opens one shot (beside a transcript line)
|
|
6
|
+
* openShot({ id, frames, index, seek? }) the shot large: its time, why it was kept, its text, Previous, Next, Play from here
|
|
7
|
+
*
|
|
8
|
+
* The note's GET /api/notes/one carries `screen.frames` ([{ t, w, h, bytes, text? }], by meeting time); each image is
|
|
9
|
+
* GET /api/notes/screen?id=…&t=…. A shot stays on screen until the next one, so the shot "at" a time is the last one at or before it.
|
|
10
|
+
* While the replay plays, the strip marks the shot on screen at the replay's time. Used by the console3 Notes place
|
|
11
|
+
* (dest-notes.js) and the classic Notes view (view-notes.js).
|
|
12
|
+
*/
|
|
13
|
+
import { el } from './dom.js';
|
|
14
|
+
import { button, sheet } from './primitives.js';
|
|
15
|
+
|
|
16
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
17
|
+
export function fmt(ms) {
|
|
18
|
+
const s = Math.max(0, Math.floor((ms || 0) / 1000));
|
|
19
|
+
const h = Math.floor(s / 3600);
|
|
20
|
+
return (h ? h + ':' + pad(Math.floor(s / 60) % 60) : String(Math.floor(s / 60))) + ':' + pad(s % 60);
|
|
21
|
+
}
|
|
22
|
+
/** Why a shot was kept, in words: the page's reason (lib/notes-screen-capture.js) or none for an older shot. */
|
|
23
|
+
export function whyText(why) {
|
|
24
|
+
if (!why) return '';
|
|
25
|
+
const said = /^said: (.*)$/.exec(why);
|
|
26
|
+
if (said) return 'Kept when someone said ' + said[1];
|
|
27
|
+
return ({ 'the screen changed': 'Kept when the screen changed', 'sharing started': 'Kept when sharing started', 'the screen is moving': 'A sample while the screen was moving',
|
|
28
|
+
decision: 'Kept at a decision', 'next step': 'Kept at a next step', highlight: 'Kept at your highlight' })[why] || 'Kept: ' + why;
|
|
29
|
+
}
|
|
30
|
+
const srcOf = (id, t) => '/api/notes/screen?id=' + encodeURIComponent(id) + '&t=' + Math.round(t);
|
|
31
|
+
|
|
32
|
+
/** The index of the shot on screen at `ms`: the last one at or before it, or -1. */
|
|
33
|
+
export function shotIndexAt(frames, ms) {
|
|
34
|
+
let lo = 0; let hi = (frames || []).length - 1; let at = -1;
|
|
35
|
+
while (lo <= hi) { const mid = (lo + hi) >> 1; if (frames[mid].t <= ms) { at = mid; lo = mid + 1; } else hi = mid - 1; }
|
|
36
|
+
return at;
|
|
37
|
+
}
|
|
38
|
+
export function shotFor(frames, from, to) {
|
|
39
|
+
for (const f of frames || []) if (f.t >= from && f.t < to) return f;
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function thumb({ id, f, label, cls, onClick, attrs }) {
|
|
44
|
+
const b = button({ label, variant: 'ghost', size: 'sm', onClick, attrs: { class: cls, 'aria-label': 'Screen at ' + fmt(f.t), title: 'Screen at ' + fmt(f.t), ...attrs } });
|
|
45
|
+
b.prepend(el('img', { class: 'nt-shot__img', src: srcOf(id, f.t), alt: '', loading: 'lazy', width: 160, height: Math.round(160 * ((f.h || 9) / (f.w || 16))) }));
|
|
46
|
+
return b;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function shotButton({ id, frames, frame, seek }) {
|
|
50
|
+
const index = frames.indexOf(frame);
|
|
51
|
+
return thumb({ id, f: frame, label: '', cls: 'nt-shot nt-shot--line', onClick: () => openShot({ id, frames, index, seek }), attrs: { 'data-nt-shot-line': String(frame.t) } });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function openShot({ id, frames, index, seek }) {
|
|
55
|
+
let i = Math.max(0, Math.min(frames.length - 1, index));
|
|
56
|
+
const h = sheet({ mode: 'center', title: 'Screen', body: [], attrs: { 'data-nt-shot-sheet': '' } });
|
|
57
|
+
const paint = () => {
|
|
58
|
+
const f = frames[i];
|
|
59
|
+
h.setHead('Screen at ' + fmt(f.t), '');
|
|
60
|
+
h.body.replaceChildren(
|
|
61
|
+
el('img', { class: 'nt-shot-big', src: srcOf(id, f.t), alt: f.text ? 'Screen at ' + fmt(f.t) + ': ' + f.text.slice(0, 200) : 'Screen at ' + fmt(f.t), 'data-nt-shot-big': String(f.t) }),
|
|
62
|
+
el('p', { class: 'nt-hint', 'data-nt-shot-why': f.why || '' }, 'Shot ' + (i + 1) + ' of ' + frames.length + ' · ' + fmt(f.t) + ' into the meeting' + (f.why ? ' · ' + whyText(f.why) : '')),
|
|
63
|
+
f.text ? el('p', { class: 'nt-shot-text', 'data-nt-shot-text': '' }, f.text) : null,
|
|
64
|
+
el('div', { class: 'nt-row' },
|
|
65
|
+
button({ label: 'Previous', variant: 'ghost', size: 'sm', disabled: i === 0, onClick: () => { i--; paint(); }, attrs: { 'data-nt-shot-prev': '' } }),
|
|
66
|
+
button({ label: 'Next', variant: 'ghost', size: 'sm', disabled: i === frames.length - 1, onClick: () => { i++; paint(); }, attrs: { 'data-nt-shot-next': '' } }),
|
|
67
|
+
seek ? button({ label: 'Play from here', size: 'sm', onClick: () => { h.close(); seek(f.t); }, attrs: { 'data-nt-shot-play': '' } }) : null));
|
|
68
|
+
};
|
|
69
|
+
paint();
|
|
70
|
+
return h;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Mark the shot on screen at the replay's time; the replay's own timeupdate calls it. */
|
|
74
|
+
function follow(replay, id) {
|
|
75
|
+
if (!replay || replay.__ntShots) return;
|
|
76
|
+
replay.__ntShots = true;
|
|
77
|
+
const mark = () => {
|
|
78
|
+
const strip = document.querySelector('[data-nt-screen="' + CSS.escape(id) + '"]');
|
|
79
|
+
if (!strip) return;
|
|
80
|
+
const ms = replay.currentTime * 1000;
|
|
81
|
+
const shots = [...strip.querySelectorAll('[data-nt-shot]')];
|
|
82
|
+
let at = -1;
|
|
83
|
+
shots.forEach((b, k) => { if (Number(b.dataset.ntShot) <= ms) at = k; });
|
|
84
|
+
shots.forEach((b, k) => b.classList.toggle('nt-shot--now', k === at && (replay.currentTime > 0)));
|
|
85
|
+
if (at >= 0 && !replay.paused && shots[at].scrollIntoView) shots[at].scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
86
|
+
};
|
|
87
|
+
replay.addEventListener('timeupdate', mark);
|
|
88
|
+
replay.addEventListener('seeked', mark);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function screenStrip({ id, frames, seek, replay }) {
|
|
92
|
+
if (!frames || !frames.length) return null;
|
|
93
|
+
follow(replay, id);
|
|
94
|
+
return el('section', { class: 'nt-screen', 'data-nt-screen': id },
|
|
95
|
+
el('h3', { class: 'nt-sec__h' }, 'Screen · ' + frames.length + (frames.length === 1 ? ' shot' : ' shots')),
|
|
96
|
+
el('div', { class: 'nt-screen__strip', role: 'list' },
|
|
97
|
+
frames.map((f, k) => el('div', { role: 'listitem' }, thumb({ id, f, label: fmt(f.t), cls: 'nt-shot', onClick: () => openShot({ id, frames, index: k, seek }), attrs: { 'data-nt-shot': String(f.t) } })))));
|
|
98
|
+
}
|
|
@@ -26,8 +26,10 @@ const MUTED = 'color:var(--muted-foreground)';
|
|
|
26
26
|
|
|
27
27
|
let dataMod = null; let termMod = null; let iniMod = null; let tabsMod = null;
|
|
28
28
|
import('./data.js').then((m) => { dataMod = m; }).catch(() => { dataMod = null; });
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
// Kept as promises as well: a press that lands before these resolve (a slow first load) waits for them
|
|
30
|
+
// instead of being dropped with a note.
|
|
31
|
+
const termReady = import('./tab-term.js').then((m) => { termMod = m; return m; }).catch(() => { termMod = null; return null; });
|
|
32
|
+
const iniReady = import('./panel-initiatives.js').then((m) => { iniMod = m; return m; }).catch(() => { iniMod = null; return null; });
|
|
31
33
|
import('./panel-tabs.js').then((m) => { tabsMod = m; }).catch(() => { tabsMod = null; });
|
|
32
34
|
|
|
33
35
|
const note = (name, text, tone) => setState({ treeNote: { name, text, tone: tone || null } });
|
|
@@ -55,6 +57,7 @@ function openSession(s) {
|
|
|
55
57
|
|
|
56
58
|
/** Start a session in THIS tree: the workspace id is the tree's, so the server runs it in that folder. */
|
|
57
59
|
export async function startHere(name, t) {
|
|
60
|
+
await Promise.all([termReady, iniReady]);
|
|
58
61
|
if (!termMod || !iniMod) { note(name, 'the terminal module has not loaded — nothing was started', 'bad'); return; }
|
|
59
62
|
const label = (t.branch || 'tree').replace(/^initiative\//, '');
|
|
60
63
|
const res = await termMod.requestSpawn({ name: 'shell · ' + label, agent: 'shell', ws: t.ws, branch: null, createBranch: false, isolate: null, profile: null });
|