create-pmos 0.241.0 → 0.242.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/state/discoveries.md +1 -1
- package/workspace/guide/console-calendar.md +14 -0
- package/workspace/guide/notes.md +22 -1
- package/workspace/lib/meeting-material.js +151 -0
- package/workspace/lib/meeting-replay.js +170 -0
- package/workspace/lib/notes-screen.js +128 -8
- package/workspace/lib/route-modules.js +2 -0
- package/workspace/lib/routes-meeting-focus.js +5 -3
- package/workspace/lib/routes-meeting-material.js +143 -0
- package/workspace/lib/routes-meeting-replay.js +75 -0
- package/workspace/lib/routes-notes.js +37 -4
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/c3-calendar.js +15 -3
- package/workspace/public/pm/c3-demo-material.js +58 -0
- package/workspace/public/pm/c3-demo.js +5 -1
- package/workspace/public/pm/c3-focus.js +83 -16
- package/workspace/public/pm/c3-listen.css +10 -0
- package/workspace/public/pm/c3-material.css +22 -0
- package/workspace/public/pm/c3-material.js +311 -0
- package/workspace/public/pm/c3-notes.js +12 -4
- package/workspace/public/pm/c3-replay.css +127 -0
- package/workspace/public/pm/c3-replay.js +413 -0
- package/workspace/public/pm/dest-notes.js +4 -2
- package/workspace/public/pm/listen-sheet.js +94 -0
- package/workspace/public/pm/listen-sources.js +82 -0
- package/workspace/public/pm/notes-aside.js +62 -0
- package/workspace/public/pm/notes-screen-capture.js +23 -10
- package/workspace/public/pm/notes-screen-view.js +18 -5
- package/workspace/public/pm/notes-shots.js +55 -0
- package/workspace/public/pm/shell-recording.js +10 -2
- package/workspace/public/pm/view-notes.js +48 -8
- package/workspace/test-lists/meeting-material.json +6 -0
- package/workspace/test-lists/meeting-replay.json +6 -0
- package/workspace/test-lists/mm-capture.json +15 -0
|
@@ -26,6 +26,7 @@ import { pmPost } from './http.js';
|
|
|
26
26
|
import { notesView, openNote, noteOfEvent, primeNotes } from './c3-notes.js'; // Calendar › Notes (initiative c3-notes-place)
|
|
27
27
|
import { onNotes } from './notes-links.js';
|
|
28
28
|
import { openSheet } from './c3-sheet.js';
|
|
29
|
+
import { materialSection } from './c3-material.js'; // Material on the meeting card (initiative meeting-material-replay)
|
|
29
30
|
|
|
30
31
|
const LAYERS = [
|
|
31
32
|
['meetings', 'Meetings'],
|
|
@@ -669,6 +670,15 @@ const focusBtn = (e, date, ui) => button({ label: [c3ic('target', 'sm'), 'Focus'
|
|
|
669
670
|
attrs: { 'data-cal3': 'focus', title: 'Open the meeting focus page: the clock, the topics, the material and PMOS beside the conversation' },
|
|
670
671
|
onClick: () => { if (ui) { ui.st.sel = null; closeMeetingSheet(ui.r); ui.redraw(); } import('./c3-focus.js').then((m) => m.openFocus({ event: e, date })).catch(() => toast({ text: 'The focus page could not load.', tone: 'error' })); } });
|
|
671
672
|
|
|
673
|
+
/* "Replay": the full-screen replay of a meeting that has ended and has a recorded note (c3-replay.js); it closes the popup first, as Focus does */
|
|
674
|
+
const replayBtn = (e, ui) => {
|
|
675
|
+
const n = noteOfEvent(e);
|
|
676
|
+
if (!n || n.private || n.state === 'recording' || n.state === 'paused' || !(n.segments > 0 || (n.words || 0) > 0)) return null;
|
|
677
|
+
return button({ label: [c3ic('play', 'sm'), 'Replay'], variant: 'ghost', size: 'sm',
|
|
678
|
+
attrs: { 'data-cal3': 'replay', title: 'Open the meeting replay: the topics, the key points, the screen and the conversation along the audio' },
|
|
679
|
+
onClick: () => { if (ui) { ui.st.sel = null; closeMeetingSheet(ui.r); ui.redraw(); } import('./c3-replay.js').then((m) => m.openReplay({ note: n.id })).catch(() => toast({ text: 'The replay could not load.', tone: 'error' })); } });
|
|
680
|
+
};
|
|
681
|
+
|
|
672
682
|
function meetingDetail(ui, hit, inSheet) {
|
|
673
683
|
const { M, h } = ui; const e = hit.e;
|
|
674
684
|
const s = h.timeOf(e.at); const f = s === null ? null : h.endH(e);
|
|
@@ -692,7 +702,7 @@ function meetingDetail(ui, hit, inSheet) {
|
|
|
692
702
|
if (!M.ctx) body = [fold('info', 'The calendar sent no meeting context, so PMOS shows nothing for this meeting.', { 'data-cal3': 'no-context' })];
|
|
693
703
|
else if (past && (!rec || rec.held === false)) body = [fold('moon', 'Nothing held for this meeting: no note, decisions or linked items yet.', { 'data-cal3': 'nothing-held' })];
|
|
694
704
|
else if (past) body = [list(['decision', 'action'], 'What came out of it', 'out', 'No decision or action is recorded.'), list(['moved'], 'What moved after it', 'moved', 'Nothing moved within 48 hours that refers to it.')];
|
|
695
|
-
else body = upcomingBody(ui, rec, items);
|
|
705
|
+
else body = upcomingBody(ui, rec, items, e, hit.date);
|
|
696
706
|
const b = !past && rec && rec.brief ? rec.brief : null;
|
|
697
707
|
const lv = !past && rec ? rec.level || null : null;
|
|
698
708
|
const iniId = lv && lv.kind !== 'initiative' ? null : typeof e.initiative === 'string' && !(lv && lv.by === 'pm') ? e.initiative : b && b.initiative ? b.initiative.id : null;
|
|
@@ -707,7 +717,7 @@ function meetingDetail(ui, hit, inSheet) {
|
|
|
707
717
|
// the popup: the sheet's header carries the title and Close; at Half and wider the note sits beside what came out of it
|
|
708
718
|
const side = noteCard(ui, e);
|
|
709
719
|
return [
|
|
710
|
-
el('div', { class: 'cal3-dt' }, el('span', { class: 'tag' + (past ? '' : ' cal3-up') }, past ? 'past' : 'upcoming'), !past && s !== null && hit.date === ui.d.today ? focusBtn(e, hit.date, ui) : null),
|
|
720
|
+
el('div', { class: 'cal3-dt' }, el('span', { class: 'tag' + (past ? '' : ' cal3-up') }, past ? 'past' : 'upcoming'), !past && s !== null && hit.date === ui.d.today ? focusBtn(e, hit.date, ui) : null, past ? replayBtn(e, ui) : null),
|
|
711
721
|
el('div', { class: 'dim mono cal3-when' }, h.dayLabel(hit.date) + (s === null ? ', all day' : ' · ' + h.hhmm(s) + ' to ' + h.hhmm(f))),
|
|
712
722
|
chipsRow(),
|
|
713
723
|
past || !M.ctx ? null : levelRow(ui, e, lv),
|
|
@@ -827,7 +837,7 @@ function openBoardTab(ui, board, tab) {
|
|
|
827
837
|
ui.h.gotoTab('init');
|
|
828
838
|
}
|
|
829
839
|
|
|
830
|
-
function upcomingBody(ui, rec, items) {
|
|
840
|
+
function upcomingBody(ui, rec, items, e, date) {
|
|
831
841
|
const { h } = ui;
|
|
832
842
|
const b = rec && rec.brief ? rec.brief : null;
|
|
833
843
|
const lv = rec && rec.level ? rec.level : null;
|
|
@@ -857,6 +867,8 @@ function upcomingBody(ui, rec, items) {
|
|
|
857
867
|
el('span', { class: 'tag' + (x.done ? ' cal3-up' : '') }, CTXWORD[x.kind] || x.kind), el('span', { class: 'cal3-ct' }, x.text),
|
|
858
868
|
x.at ? el('span', { class: 'dim mono cal3-at' }, h.dayLabel(x.at) + (h.timeOf(x.at) !== null ? ' ' + h.hhmm(h.timeOf(x.at)) : '')) : null,
|
|
859
869
|
x.ref && REF[x.ref.type] ? refLink(ui, x.ref, { title: x.text }) : null))])));
|
|
870
|
+
if (e) out.push(materialSection({ event: e, date, level: lv, brief: b,
|
|
871
|
+
back: () => { if (!ui.st.sel) { ui.st.sel = { type: 'event', id: e.id }; ui.redraw(); } } }));
|
|
860
872
|
if (b && ((b.links || []).length || (b.refs || []).length)) out.push(el('div', { class: 'chips2', 'data-cal3': 'meeting-links' },
|
|
861
873
|
...(b.links || []).map((l) => el('a', { class: 'chip2', href: l.url, target: '_blank', rel: 'noopener noreferrer' }, c3ic('out', 'sm'), l.label)),
|
|
862
874
|
...(b.refs || []).map((r) => r.link ? el('a', { class: 'chip2', href: r.link, target: '_blank', rel: 'noopener noreferrer' }, c3ic('out', 'sm'), r.label, r.source ? el('span', { class: 'dim' }, ' ' + r.source) : null)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/* pm/c3-demo-material.js — DEMO answers for the material of a meeting (initiative meeting-material-replay, part 1) on /c3.html?demo=1.
|
|
2
|
+
* c3-demo.js passes GET and POST /api/meeting-material and GET /api/meeting-material/suggest here, and adds the material of the meeting to the
|
|
3
|
+
* answer of GET /api/meeting-focus. The data is the approved mockup's: PMOS suggests the Partner API decision deck, the weekly report and a
|
|
4
|
+
* policy document; a search finds three more. The store lives in the page: a reload forgets it and nothing reaches the server. Real mode never loads this file. */
|
|
5
|
+
|
|
6
|
+
const rows = []; // { key, scope, event, item, topic, present, removed? } in the order they were added; the last of an item wins
|
|
7
|
+
const TOPICS = ['Context and goal', 'Options: soft vs hard limits', 'Partner impact: top 20 traffic', 'Partner comms and timing', 'Decide and next steps']
|
|
8
|
+
.map((text) => ({ id: text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''), text }));
|
|
9
|
+
const WS = '881665c933';
|
|
10
|
+
const SUGGESTED = [
|
|
11
|
+
{ kind: 'pmos-deck', ref: 'planning/reports/rs1/2026-10-21.md', title: 'Partner API rate limits: decision deck', meta: 'Reports · deck · draft', why: 'Your deck, edited Fri 9 Oct', present: true, ws: WS },
|
|
12
|
+
{ kind: 'report', ref: 'planning/reports/rs3/2026-10-14.md', title: 'Partner API weekly report · cycle 41', meta: 'Reports · document · issued', why: 'The report this review reads each week', present: false, ws: WS },
|
|
13
|
+
{ kind: 'pmos-doc', ref: 'planning/prd/partner-agreement-prd.md', title: 'Partner agreement v3: fair use (section 4.2)', meta: 'IDPF-07 documents', why: 'Named in the last note: "check the agreement"', present: false, ws: WS },
|
|
14
|
+
];
|
|
15
|
+
const FIND = [
|
|
16
|
+
{ kind: 'pmos-deck', ref: 'planning/reports/rs1/2026-10-07.md', title: 'Payments 1.5 release review deck', meta: 'Reports · deck · issued', present: true, ws: WS },
|
|
17
|
+
{ kind: 'report', ref: 'planning/reports/rs3/2026-10-02.md', title: 'Monthly product review · September', meta: 'Reports · document · issued', present: false, ws: WS },
|
|
18
|
+
{ kind: 'pmos-doc', ref: 'planning/prd/idpf-07-charter.md', title: 'IDPF-07 charter', meta: 'IDPF-07 documents', present: false, ws: WS },
|
|
19
|
+
];
|
|
20
|
+
const norm = (t) => String(t || '').toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
|
|
21
|
+
const keyOf = (e) => (e && e.series ? 's:' + e.series : 't:' + norm(e && e.title));
|
|
22
|
+
const idOf = (r) => [r.key, r.scope === 'series' ? '' : r.event || '', r.item.kind, r.item.ref || r.item.url].join('|');
|
|
23
|
+
const classify = (u) => {
|
|
24
|
+
const s = String(u || '').trim(); let x; try { x = new URL(/^[a-z][a-z0-9+.-]*:/i.test(s) ? s : 'https://' + s); } catch { return null; }
|
|
25
|
+
if (x.protocol !== 'https:') return null;
|
|
26
|
+
if (x.hostname === 'docs.google.com' && /^\/presentation\/d\//.test(x.pathname)) return { kind: 'slides', url: x.href };
|
|
27
|
+
if (x.hostname === 'docs.google.com' && /^\/document\/d\//.test(x.pathname)) return { kind: 'gdoc', url: x.href };
|
|
28
|
+
if (/\.atlassian\.net$/.test(x.hostname) && x.pathname.startsWith('/wiki')) return { kind: 'conf', url: x.href };
|
|
29
|
+
return { kind: 'link', url: x.href };
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** The items of one meeting, in the shape of GET /api/meeting-material. event: { id?, title, series? } */
|
|
33
|
+
export function materialFor(event) {
|
|
34
|
+
const key = keyOf(event); const held = new Map();
|
|
35
|
+
for (const r of rows) { const id = idOf(r); if (r.removed) held.delete(id); else held.set(id, r); }
|
|
36
|
+
return [...held.values()].filter((r) => r.key === key && (r.scope === 'series' || r.event === String(event.id || ''))).map((r) => ({
|
|
37
|
+
id: Math.abs([...idOf(r)].reduce((h, c) => (h * 31 + c.charCodeAt(0)) | 0, 7)).toString(16), kind: r.item.kind, ref: r.item.ref || null, url: r.item.url || null, title: r.item.title,
|
|
38
|
+
ws: r.item.ws || null, topic: r.topic || null, present: r.present === true, scope: r.scope, from: 'you', by: 'pm:wawan', at: null }));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function materialGet(path, url) {
|
|
42
|
+
const q = new URL(url, location.origin).searchParams;
|
|
43
|
+
if (path === '/api/meeting-material/suggest') {
|
|
44
|
+
const t = (q.get('q') || '').toLowerCase();
|
|
45
|
+
return { ok: true, suggested: SUGGESTED, found: t.length >= 2 ? FIND.filter((x) => x.title.toLowerCase().includes(t)) : [] };
|
|
46
|
+
}
|
|
47
|
+
const event = { id: q.get('event') || '', title: q.get('title') || '', series: q.get('series') || '' };
|
|
48
|
+
return { ok: true, key: keyOf(event), items: materialFor(event), ...(q.get('topics') ? { topics: TOPICS } : {}) };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function materialPost(b) {
|
|
52
|
+
const e = b.event || {}; const it = b.item || {};
|
|
53
|
+
const item = it.ref ? { kind: it.kind, ref: it.ref, title: it.title || it.ref, ws: it.ws } : (() => { const c = classify(it.url); return c ? { ...c, title: it.title || c.url } : null; })();
|
|
54
|
+
if (!item) return { ok: false, error: 'a link must be an https address' };
|
|
55
|
+
const scope = b.scope === 'series' ? 'series' : 'meeting';
|
|
56
|
+
rows.push({ key: keyOf(e), scope, event: scope === 'meeting' ? String(e.id || '') : '', item, topic: b.topic || null, present: b.op !== 'remove' && b.present === true, removed: b.op === 'remove' });
|
|
57
|
+
return { ok: true, items: materialFor(e) };
|
|
58
|
+
}
|
|
@@ -12,6 +12,7 @@ import { el } from './dom.js';
|
|
|
12
12
|
import * as D from './c3-demo-data.js';
|
|
13
13
|
import { installInit } from './c3-demo-init.js';
|
|
14
14
|
import { focusGet, focusPost } from './c3-demo-focus.js'; // Meeting focus: the mockup's Partner API review (GET /api/meeting-focus, its ask poll, and the topic and ask POSTs)
|
|
15
|
+
import { materialGet, materialPost, materialFor } from './c3-demo-material.js'; // Meeting material: GET and POST /api/meeting-material, its suggestions, and the material in the Focus read
|
|
15
16
|
import { installBoards } from './c3-demo-boards.js'; // Initiatives boards: answers every /api/boards GET and POST (installed outermost, in installDemo)
|
|
16
17
|
import { installCoreDocs } from './c3-demo-coredocs.js'; // Core documents of each level: GET /api/core-docs and its POSTs (installed outermost, after installBoards)
|
|
17
18
|
import './c3-demo-cal-ix.js'; // Calendar and the ribbon: sets window.__c3calix (calendar-views.js reads it in demo only)
|
|
@@ -53,7 +54,9 @@ function getRoute(path, url) {
|
|
|
53
54
|
switch (path) {
|
|
54
55
|
case '/api/briefing': return D.briefing(url);
|
|
55
56
|
case '/api/calendar-views': return withCalendarPage(D.calendarViews(url));
|
|
56
|
-
case '/api/meeting-focus':
|
|
57
|
+
case '/api/meeting-focus': { const o = focusGet(path, url); return o && o.ok ? { ...o, material: [...materialFor({ id: q.get('event') || '', title: q.get('title') || '' }), ...materialFor(o.event)] } : o; } // what was asked for, and the sample meeting the demo answers with
|
|
58
|
+
case '/api/meeting-focus/ask': case '/api/meeting-focus/now': return focusGet(path, url);
|
|
59
|
+
case '/api/meeting-material': case '/api/meeting-material/suggest': return materialGet(path, url);
|
|
57
60
|
case '/api/pm-items': return D.pmItems();
|
|
58
61
|
case '/api/runs': return D.runs();
|
|
59
62
|
case '/api/releases': return D.releases();
|
|
@@ -78,6 +81,7 @@ function postRoute(path, body) {
|
|
|
78
81
|
let b = {};
|
|
79
82
|
try { b = body ? JSON.parse(body) : {}; } catch { b = {}; }
|
|
80
83
|
if (path.startsWith('/api/meeting-focus/')) return focusPost(path, body);
|
|
84
|
+
if (path === '/api/meeting-material') return materialPost(b);
|
|
81
85
|
if (path === '/api/pm-items/add') return { ok: true, item: { id: 'item-demo-new' + (++added), title: String(b.line || '').trim() || 'New item', due: null, priority: null, waiting_on: null, job: null, steps: [], created_at: D.NOW_ISO + 'Z' } };
|
|
82
86
|
if (path === '/api/pm-items/remove') return { ok: true, undo_ms: 8000 };
|
|
83
87
|
if (path === '/api/calendar-links') return (window.__c3links = (window.__c3links || []).concat([b])), { ok: true, link: b.kind === 'none' ? null : { kind: b.kind, ws: b.ws || null, board: b.board || null, initiative: b.initiative || null, title: b.title || null, by: 'pm:wawan' } }; // nothing is saved
|
|
@@ -28,6 +28,8 @@ import { ensureComponentsCss, button } from './primitives.js';
|
|
|
28
28
|
import { icon, sect, pbtn, lk, ib, put } from './c3-ini-ui.js';
|
|
29
29
|
import { openSheet, closeSheet } from './c3-sheet.js';
|
|
30
30
|
import { toast } from './c3-toast.js';
|
|
31
|
+
import * as Shots from './notes-shots.js';
|
|
32
|
+
import { openAddMaterial, openItem, presentItem, audienceItem, pmosUrl } from './c3-material.js'; // the PM's own material (initiative meeting-material-replay)
|
|
31
33
|
|
|
32
34
|
const POLL_MS = 3000;
|
|
33
35
|
const LAST_MINUTES = 5; // the last minutes of the meeting: the captured items show under the stage
|
|
@@ -43,8 +45,12 @@ export function clockText(sec) {
|
|
|
43
45
|
return (neg ? '-' : '') + (h ? h + ':' + pad(m) : String(m)) + ':' + pad(x);
|
|
44
46
|
}
|
|
45
47
|
/** What a link is, and how the page shows it: every link is a card; a Google Slides or Docs link also has a /preview address that the page loads only when the PM presses Show the preview here (no-network: the page reaches nothing by itself). Pure. */
|
|
48
|
+
const PMOS_APP = { 'pmos-deck': 'PMOS deck', 'pmos-doc': 'PMOS document', report: 'PMOS report' };
|
|
46
49
|
export function classify(url) {
|
|
47
50
|
const raw = String(url || '').trim();
|
|
51
|
+
// an item PMOS itself holds (a deck, a document, a report cycle): pmos:<kind>:<workspace>:<path>. It opens in PMOS, not in a tab.
|
|
52
|
+
const pm = /^pmos:(pmos-deck|pmos-doc|report):([^:]*):(.+)$/.exec(raw);
|
|
53
|
+
if (pm) return { kind: 'pmos', pk: pm[1], app: PMOS_APP[pm[1]], url: raw, host: 'PMOS', ref: pm[3] };
|
|
48
54
|
// A link pasted without its scheme (docs.google.com/…) is a web link.
|
|
49
55
|
let u; try { u = new URL(/^[a-z][a-z0-9+.-]*:/i.test(raw) ? raw : 'https://' + raw); } catch { return { kind: 'link', app: 'Link', url: String(url), host: '' }; }
|
|
50
56
|
const m = /^\/(presentation|document)\/d\/([A-Za-z0-9_-]+)/.exec(u.pathname);
|
|
@@ -60,7 +66,7 @@ export function classify(url) {
|
|
|
60
66
|
let F = null; // the open page: { id, date, root, r (regions), sig, data, offset, ui, timers, sheet, ... }
|
|
61
67
|
|
|
62
68
|
function ensureCss() {
|
|
63
|
-
ensureC3Css(); ensureComponentsCss();
|
|
69
|
+
ensureC3Css(); ensureComponentsCss(); Shots.ensureListenCss();
|
|
64
70
|
if (document.querySelector('link[data-c3-focus]')) return;
|
|
65
71
|
const l = document.createElement('link');
|
|
66
72
|
l.rel = 'stylesheet'; l.href = new URL('./c3-focus.css', import.meta.url).href; l.setAttribute('data-c3-focus', '');
|
|
@@ -211,7 +217,24 @@ function region(name, sig, build) {
|
|
|
211
217
|
return true;
|
|
212
218
|
}
|
|
213
219
|
const phaseOf = () => { const n = nowMs(); const s = startMs(); const e = endMs(); return Number.isFinite(s) && n < s ? 'before' : Number.isFinite(e) && n >= e ? 'after' : 'during'; };
|
|
214
|
-
|
|
220
|
+
/** The meeting's material: the links of the invite and the brief, then the PM's own (d.material, with topic and present). A PMOS deck is presented in PMOS. */
|
|
221
|
+
function linksOf(d) {
|
|
222
|
+
const mine = ((d && d.material) || []).map((m) => {
|
|
223
|
+
const c = classify(['pmos-deck', 'pmos-doc', 'report'].includes(m.kind) ? pmosUrl(m) : m.url);
|
|
224
|
+
return { label: m.title || c.url, ...c, mat: m, topic: m.topic || null, you: m.present === true, ...(c.kind === 'pmos' ? { present: m.kind === 'pmos-deck' } : {}) };
|
|
225
|
+
});
|
|
226
|
+
const own = new Set(mine.map((l) => l.url));
|
|
227
|
+
const given = ((d && d.event && d.event.links) || []).map((l) => ({ label: l.label, ...classify(l.url) })).filter((l) => !own.has(l.url));
|
|
228
|
+
return [...given, ...mine].map((l, i) => ({ ...l, i }));
|
|
229
|
+
}
|
|
230
|
+
const normTopic = (t) => String(t || '').toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
|
|
231
|
+
/** The material tied to the topic the room is on (by its text or its id), or null. */
|
|
232
|
+
const forTopic = (links, cur) => (cur ? links.find((l) => l.topic && (normTopic(l.topic) === normTopic(cur.text) || normTopic(l.topic) === normTopic(cur.id))) || null : null);
|
|
233
|
+
/** The item on the stage: the one you pressed, else the material for the topic the room is on, else the first. */
|
|
234
|
+
function stagePick(d) {
|
|
235
|
+
const links = linksOf(d);
|
|
236
|
+
return (F.ui.stagePin ? links[F.ui.stage] : forTopic(links, currentOf(d))) || links[F.ui.stage] || links.find((l) => l.preview) || links[0] || null;
|
|
237
|
+
}
|
|
215
238
|
const topicNo = (d, id) => d.topics.findIndex((t) => t.id === id) + 1;
|
|
216
239
|
const currentOf = (d) => d.topics.find((t) => t.id === d.current) || null;
|
|
217
240
|
const openRunOf = (d) => (d.runs || []).filter((x) => x.to_min == null).pop() || null;
|
|
@@ -332,7 +355,7 @@ function stopBuild() {
|
|
|
332
355
|
return [el('div', { class: 'fc-stopsheet', 'data-fc': 'stop-sheet' },
|
|
333
356
|
el('p', { class: 'fc-p' }, d && d.talk ? 'PMOS keeps the notes and shows you what it caught.'
|
|
334
357
|
: left > 0 ? left + ' min of the meeting is left. If you stop now, the notes use what was recorded so far.' : 'PMOS keeps the notes and writes them up from the recording.'),
|
|
335
|
-
el('div', { class: 'iacts' }, pbtn('primary', 'Stop recording', 'stop', { 'data-fc': 'stop-ok' }), pbtn('ghost', 'Keep recording', null, { 'data-fc': 'stop-keep' })))];
|
|
358
|
+
el('div', { class: 'iacts' }, pbtn('primary', 'Stop recording', 'stop', { 'data-fc': 'stop-ok' }), d && d.note && !isDemo() && !(F && F.phone) ? pbtn('outline', 'Stop and open the replay', 'play', { 'data-fc': 'stop-replay' }) : null, pbtn('ghost', 'Keep recording', null, { 'data-fc': 'stop-keep' })))];
|
|
336
359
|
}
|
|
337
360
|
function materialBuild() { return stageBody(F.data, phaseOf()); }
|
|
338
361
|
|
|
@@ -419,9 +442,17 @@ function trackEl(d) {
|
|
|
419
442
|
return el('div', { class: 'fc-track', role: 'img', 'aria-label': 'What the room talked about, in order. Numbers match the topic list.' }, el('div', { class: 'fc-runs' }, ...track), el('span', { class: 'fc-nowm', 'data-fc': 'nowm' }));
|
|
420
443
|
}
|
|
421
444
|
|
|
445
|
+
/* Pause shots: the chip beside REC stops the pictures and keeps the sound (notes-shots.js; the same switch is on the recording chip) */
|
|
446
|
+
const shotsSig = () => (Shots.active() ? Shots.chipText() : '');
|
|
447
|
+
function shotsChip() {
|
|
448
|
+
const off = Shots.paused();
|
|
449
|
+
return lk({ class: 'lsn-chip' + (off ? ' off' : ''), 'data-fc': 'shots', 'aria-pressed': String(off), title: Shots.chipTitle(),
|
|
450
|
+
onclick: () => { const now = Shots.setPaused(!Shots.paused()); toast(now ? 'Shots paused. The sound is still recorded.' : 'Shots back on.'); draw(); } }, icon(off ? 'play' : 'eye', 'sm'), Shots.chipText());
|
|
451
|
+
}
|
|
452
|
+
|
|
422
453
|
function drawTop(d, ph) {
|
|
423
454
|
const e = d.event; const rec = recState(d); const ini = (d.brief && d.brief.initiative) || null; const lt = leftText(d, ph);
|
|
424
|
-
const sig = [e.title, e.at, e.end, e.attendees, ini, e.initiative, ph, rec, lt.txt, d.topics.map((t) => t.id), d.runs, F.status === 'ok' && F.stale ? 1 : 0];
|
|
455
|
+
const sig = [e.title, e.at, e.end, e.attendees, ini, e.initiative, ph, rec, shotsSig(), lt.txt, d.topics.map((t) => t.id), d.runs, F.status === 'ok' && F.stale ? 1 : 0];
|
|
425
456
|
region('top', sig, () => {
|
|
426
457
|
const s = startMs(); const f = endMs();
|
|
427
458
|
const people = (e.attendees || []);
|
|
@@ -440,6 +471,7 @@ function drawTop(d, ph) {
|
|
|
440
471
|
el('div', { class: 'fc-clock', role: 'timer', 'aria-live': 'off' }, ...clock, el('div', { class: 'fc-end' }, el('span', { class: 'tag' + (lt.warn ? ' fc-warn' : ''), 'data-fc': 'proj', hidden: !lt.txt }, lt.txt))),
|
|
441
472
|
el('div', { class: 'fc-acts' },
|
|
442
473
|
recLabel ? el('span', { class: 'dim mono fc-recd', 'data-fc': 'rec', 'data-rec': rec, title: rec === 'recording' ? 'Recording this meeting' : '' }, rec === 'recording' ? el('span', { class: 'fc-dot' }) : null, recLabel) : null,
|
|
474
|
+
rec === 'recording' && Shots.active() ? shotsChip() : null,
|
|
443
475
|
rec === 'recording' || rec === 'paused' ? pbtn('outline', 'End meeting', 'stop', { 'data-fc': 'end', title: 'Stops the recording and saves the note' })
|
|
444
476
|
: rec !== 'ended' && ph !== 'after' ? pbtn('primary', 'Start recording', 'play', { 'data-fc': 'start', title: 'Records this meeting in a note' }) : null),
|
|
445
477
|
trackEl(d),
|
|
@@ -484,8 +516,13 @@ function linkRow(l) {
|
|
|
484
516
|
el('div', { class: 'nb' }, el('span', { class: 'tt' }, l.label), el('div', { class: 'chips' }, el('span', { class: 'chip2' }, icon('out', 'sm'), l.app))),
|
|
485
517
|
el('div', { class: 'acts' }, openBtn(l), l.present ? presentBtn(l) : null));
|
|
486
518
|
}
|
|
487
|
-
const openBtn = (l) =>
|
|
488
|
-
|
|
519
|
+
const openBtn = (l) => (l.kind === 'pmos' ? pbtn('outline', 'Open', null, { 'data-fc': 'pm-open', 'data-i': String(l.i), title: 'Opens it in PMOS' })
|
|
520
|
+
: pbtn('outline', l.kind === 'link' ? 'Open' : 'Open in ' + l.app, null, { 'data-fc': 'open-link', 'data-url': l.url, title: 'Opens it in a new tab' }));
|
|
521
|
+
const presentBtn = (l) => (l.kind === 'pmos'
|
|
522
|
+
? [pbtn('ghost', 'Audience view', 'eye', { 'data-fc': 'pm-audience', 'data-i': String(l.i), title: 'Opens the slides only, full screen, with no notes. Share the screen in the call.' }),
|
|
523
|
+
pbtn('primary', 'Present', 'play', { 'data-fc': 'pm-present', 'data-i': String(l.i), title: 'Opens the presenter view in PMOS with your speaker notes. Press Audience view before you share the screen.' })]
|
|
524
|
+
: pbtn('ghost', 'Present in ' + l.app, 'play', { 'data-fc': 'present', 'data-url': l.present, title: 'Opens the deck in present mode in a new tab. Share that tab in the call.' }));
|
|
525
|
+
const addMaterialBtn = () => pbtn('outline', 'Add material', 'plus', { 'data-fc': 'mat-add', title: 'Link a PMOS document, a report or deck, or a web page to this meeting' });
|
|
489
526
|
|
|
490
527
|
function beforeList(d, links) {
|
|
491
528
|
const b = d.brief || {};
|
|
@@ -502,28 +539,28 @@ function beforeList(d, links) {
|
|
|
502
539
|
function drawStage(d, ph) {
|
|
503
540
|
const links = linksOf(d);
|
|
504
541
|
const cur = currentOf(d);
|
|
505
|
-
const sig = [ph, links, F.ui.stage, F.ui.preview, cur && cur.text, d.brief && [d.brief.prep, d.brief.matters, d.brief.last]];
|
|
542
|
+
const sig = [ph, links, F.ui.stage, F.ui.stagePin, F.ui.preview, cur && cur.text, d.brief && [d.brief.prep, d.brief.matters, d.brief.last]];
|
|
506
543
|
region('stage', sig, () => stageBody(d, ph));
|
|
507
544
|
}
|
|
508
545
|
/** The stage: the material the meeting links, as a card, or the loaded preview; before the start, what is prepared. The phone shows it in a sheet. */
|
|
509
546
|
function stageBody(d, ph) {
|
|
510
547
|
const links = linksOf(d);
|
|
511
548
|
const cur = currentOf(d);
|
|
512
|
-
const
|
|
513
|
-
const pick = links[F.ui.stage] || embeddable[0] || links[0] || null;
|
|
549
|
+
const pick = stagePick(d);
|
|
514
550
|
if (ph === 'before') {
|
|
515
551
|
const rows = beforeList(d, links);
|
|
516
552
|
return [sect({ ic: 'doc', title: 'Before it starts', attrs: { class: 'fc-stage', 'data-fc': 'stage', 'data-fc-stage': 'before' } },
|
|
517
|
-
rows.length ? el('ul', { class: 'list', role: 'list' }, ...rows) : el('div', { class: 'fold' }, icon('moon', 'sm'), 'Nothing is prepared for this meeting yet. Nothing is linked in the invite.'))];
|
|
553
|
+
rows.length ? el('ul', { class: 'list', role: 'list' }, ...rows) : [el('div', { class: 'fold' }, icon('moon', 'sm'), 'Nothing is prepared for this meeting yet. Nothing is linked in the invite.'), el('div', { class: 'iacts' }, addMaterialBtn())])];
|
|
518
554
|
}
|
|
519
|
-
if (!pick) return [sect({ ic: 'doc', title: 'Material', attrs: { class: 'fc-stage', 'data-fc': 'stage', 'data-fc-stage': 'none' } }, el('div', { class: 'fold' }, icon('moon', 'sm'), 'No material is linked to this meeting.'))];
|
|
555
|
+
if (!pick) return [sect({ ic: 'doc', title: 'Material', attrs: { class: 'fc-stage', 'data-fc': 'stage', 'data-fc-stage': 'none' } }, el('div', { class: 'fold' }, icon('moon', 'sm'), 'No material is linked to this meeting.'), el('div', { class: 'iacts' }, addMaterialBtn()))];
|
|
520
556
|
const right = links.length > 1 ? el('div', { class: 'seg', role: 'group', 'aria-label': 'Material', 'data-fc': 'material' },
|
|
521
557
|
...links.map((l, i) => button({ variant: 'ghost', label: [icon(l.preview ? 'doc' : 'out', 'sm'), l.app], attrs: { 'aria-pressed': String(l === pick), 'data-fc': 'material-pick', 'data-i': String(i), title: l.label } }))) : null;
|
|
522
558
|
const body = pick.preview && F.ui.preview === pick.url
|
|
523
559
|
? [el('div', { class: 'fc-frame', 'data-fc': 'preview-wrap' }, el('iframe', { src: pick.preview, title: 'Preview of ' + pick.label + ' in ' + pick.app, loading: 'lazy', referrerpolicy: 'no-referrer', allow: 'fullscreen',
|
|
524
560
|
sandbox: 'allow-scripts allow-same-origin allow-popups allow-forms', 'data-fc': 'preview', 'data-app': pick.kind })),
|
|
525
561
|
el('div', { class: 'fc-strip' }, el('span', { class: 'dim mono fc-src' }, icon('lock', 'sm'), 'Shown by ' + pick.app + ' itself. PMOS shows nothing to the room.'), el('span', { class: 'fc-grow' }), openBtn(pick), pick.present ? presentBtn(pick) : null)]
|
|
526
|
-
: [el('div', { class: 'fc-card', 'data-fc': 'link-card'
|
|
562
|
+
: [el('div', { class: 'fc-card', 'data-fc': 'link-card', 'data-fc-kind': pick.kind }, el('span', { class: 'ki' }, icon(pick.kind === 'pmos' ? (pick.pk === 'pmos-doc' ? 'doc' : 'report') : pick.preview ? 'doc' : 'out')),
|
|
563
|
+
el('div', { class: 'nb' }, el('b', null, pick.label), el('span', { class: 'dim mono' }, pick.app + (pick.host && pick.host !== pick.app ? ' · ' + pick.host : '') + (pick.topic ? ' · for ' + pick.topic : '') + (pick.you ? ' · you present it' : ''))), openBtn(pick), pick.present ? presentBtn(pick) : null),
|
|
527
564
|
pick.preview ? el('div', { class: 'fc-strip' }, el('span', { class: 'dim mono fc-src' }, icon('lock', 'sm'), 'PMOS loads nothing from ' + pick.app + ' until you ask.'), el('span', { class: 'fc-grow' }),
|
|
528
565
|
button({ variant: 'outline', label: [icon('eye', 'sm'), 'Show the preview here'], attrs: { 'data-fc': 'preview-on', title: 'Load the ' + pick.app + ' preview into this page' } })) : null];
|
|
529
566
|
return [sect({ ic: 'doc', title: cur ? 'Material · ' + cur.text : 'Material', right, attrs: { class: 'fc-stage', 'data-fc': 'stage', 'data-fc-stage': pick.preview && F.ui.preview === pick.url ? pick.kind : 'card' } }, ...body)];
|
|
@@ -719,6 +756,7 @@ async function ask(question) {
|
|
|
719
756
|
function tick() {
|
|
720
757
|
if (!F || !F.data) return;
|
|
721
758
|
const d = F.data; const n = nowMs(); const s = startMs(); const f = endMs();
|
|
759
|
+
if (F.shotsSeen !== shotsSig()) { const first = F.shotsSeen === undefined; F.shotsSeen = shotsSig(); if (!first) { draw(); return; } } // Pause shots was pressed elsewhere
|
|
722
760
|
if (phaseOf() !== F.sig.phase) { F.sig.phase = phaseOf(); if (F.sig.phaseSet) { draw(); return; } F.sig.phaseSet = true; }
|
|
723
761
|
const set = (k, v) => { for (const x of document.querySelectorAll('[data-fc="' + k + '"]')) if (x.textContent !== v) x.textContent = v; };
|
|
724
762
|
const ph = phaseOf();
|
|
@@ -858,6 +896,22 @@ async function endMeeting() {
|
|
|
858
896
|
|
|
859
897
|
/* ── presses ──────────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
860
898
|
|
|
899
|
+
/** Add material, and Open, Present and Audience view of the PM's PMOS material. True when the press was one of these. */
|
|
900
|
+
function materialPress(k, t) {
|
|
901
|
+
const d = F && F.data; if (!d) return false;
|
|
902
|
+
if (k === 'mat-add') {
|
|
903
|
+
const e = d.event || {};
|
|
904
|
+
openAddMaterial({ event: { id: e.id, title: e.title, at: e.at, series: e.series, initiative: e.initiative, links: e.links }, date: d.date, brief: d.brief });
|
|
905
|
+
return true;
|
|
906
|
+
}
|
|
907
|
+
if (k !== 'pm-open' && k !== 'pm-present' && k !== 'pm-audience') return false;
|
|
908
|
+
const l = linksOf(d)[Number(t.dataset.i)];
|
|
909
|
+
if (l && l.mat) (k === 'pm-present' ? presentItem : k === 'pm-audience' ? audienceItem : openItem)(l.mat);
|
|
910
|
+
return true;
|
|
911
|
+
}
|
|
912
|
+
// the material was added or removed on the Calendar card or in the sheet: read the meeting again, so it shows at once
|
|
913
|
+
window.addEventListener('c3-material-changed', () => { if (F && !F.noteId) load(); });
|
|
914
|
+
|
|
861
915
|
function onClick(ev) {
|
|
862
916
|
const t = ev.target.closest('[data-fc]'); if (!t || !F || !F.root.contains(t)) return;
|
|
863
917
|
const k = t.dataset.fc; const d = F.data;
|
|
@@ -866,6 +920,7 @@ function onClick(ev) {
|
|
|
866
920
|
if (k === 'pop-open') { openFull(); return; }
|
|
867
921
|
if (k === 'choice') { const c = (F.choices || [])[Number(t.dataset.i)]; if (c) chooseMeeting(c, String(c.at || '').slice(0, 10)); return; }
|
|
868
922
|
if (!d) return;
|
|
923
|
+
if (materialPress(k, t)) return;
|
|
869
924
|
if (k === 'topics') { showSheet('topics', 'Topics', 'todo', topicsBuild); return; }
|
|
870
925
|
if (k === 'captured') { showSheet('captured', 'Captured so far', 'check', capturedBuild); return; }
|
|
871
926
|
if (k === 'note' && F.phone) { openNoteSheet(); return; }
|
|
@@ -873,8 +928,8 @@ function onClick(ev) {
|
|
|
873
928
|
if (k === 'stop') { showSheet('stop', d.talk ? 'Stop recording?' : 'Stop recording ' + d.event.title + '?', 'stop', stopBuild); return; }
|
|
874
929
|
if (k === 'note') { const id = d.note && d.note.id; closeFocus(); if (id) window.dispatchEvent(new CustomEvent('c3-open-note', { detail: { id } })); return; }
|
|
875
930
|
if (k === 'chip') { openChip(F.chips[Number(t.dataset.i)]); return; }
|
|
876
|
-
if (k === 'preview-on') { const
|
|
877
|
-
if (k === 'material-pick') { F.ui.stage = Number(t.dataset.i) || 0; drawStage(d, phaseOf()); return; }
|
|
931
|
+
if (k === 'preview-on') { const pk = stagePick(d); F.ui.preview = pk && pk.url; drawStage(d, phaseOf()); return; }
|
|
932
|
+
if (k === 'material-pick') { F.ui.stage = Number(t.dataset.i) || 0; F.ui.stagePin = true; drawStage(d, phaseOf()); return; }
|
|
878
933
|
if (k === 'open-link' || k === 'present') { openUrl(t.dataset.url); return; }
|
|
879
934
|
if (k === 'start') { startRecording(); return; }
|
|
880
935
|
if (k === 'end') { endMeeting(); return; }
|
|
@@ -896,6 +951,7 @@ document.addEventListener('click', (ev) => {
|
|
|
896
951
|
if (!F || !F.sheetOpen || !F.sheet) return;
|
|
897
952
|
const t = ev.target.closest && ev.target.closest('[data-shell="sheet"] [data-fc]'); if (!t) return;
|
|
898
953
|
const k = t.dataset.fc;
|
|
954
|
+
if (materialPress(k, t)) return;
|
|
899
955
|
if (k === 'picker') { F.ui.picker = !F.ui.picker; F.ui.newTopic = false; refreshSheet(); return; }
|
|
900
956
|
if (k === 'new-topic') { F.ui.newTopic = true; refreshSheet(); return; }
|
|
901
957
|
if (k === 'pick') { const text = t.dataset.text; postTopic('set', text).then((ok) => { if (ok) { closeSheet(); toast('Now on: ' + text + '.'); } }); return; }
|
|
@@ -907,10 +963,21 @@ document.addEventListener('click', (ev) => {
|
|
|
907
963
|
}
|
|
908
964
|
if (k === 'add-topic') { F.ui.addTopic = !F.ui.addTopic; refreshSheet(); return; }
|
|
909
965
|
if (k === 'open-link' || k === 'present') { openUrl(t.dataset.url); return; }
|
|
910
|
-
if (k === 'preview-on') { const
|
|
911
|
-
if (k === 'material-pick') { F.ui.stage = Number(t.dataset.i) || 0; refreshSheet(true); return; }
|
|
966
|
+
if (k === 'preview-on') { const pk = stagePick(F.data); F.ui.preview = pk && pk.url; refreshSheet(true); return; }
|
|
967
|
+
if (k === 'material-pick') { F.ui.stage = Number(t.dataset.i) || 0; F.ui.stagePin = true; refreshSheet(true); return; }
|
|
912
968
|
if (k === 'note-tab') { F.ui.noteTab = t.dataset.tab === 'said' ? 'said' : 'note'; refreshSheet(true); return; }
|
|
913
969
|
if (k === 'stop-keep') { closeSheet(); return; }
|
|
914
970
|
if (k === 'pull') { const id = t.dataset.id; F.ui.pulled.push(id); (F.pulledAt ||= {})[id] = nowMs(); F.ui.ni = null; closeSheet(); drawConversation(F.data); return; }
|
|
915
971
|
if (k === 'stop-ok') { closeSheet(); endMeeting().then(() => { if (F && F.phone) closeFocus(); }); return; }
|
|
972
|
+
if (k === 'stop-replay') { endAndReplay(); return; } // End and open the replay
|
|
916
973
|
});
|
|
974
|
+
|
|
975
|
+
/** Stop the recording, leave Focus and open the meeting replay on the note (c3-replay.js). */
|
|
976
|
+
async function endAndReplay() {
|
|
977
|
+
const f = F; const note = f && f.data && f.data.note && f.data.note.id; if (!f || !note) return;
|
|
978
|
+
closeSheet();
|
|
979
|
+
await endMeeting();
|
|
980
|
+
if (F !== f) return;
|
|
981
|
+
closeFocus();
|
|
982
|
+
import('./c3-replay.js').then((m) => m.openReplay({ note })).catch(() => toast('The replay could not load.'));
|
|
983
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/* c3-listen.css — the Listen sheet, the set-aside review and the Pause-shots chip (initiative meeting-material-replay, part 3).
|
|
2
|
+
* Loaded by listen-sheet.js (ensureListenCss) and by the Focus page. Every rule is under .lsn- or .fc-shots so it changes nothing else. */
|
|
3
|
+
.c3 .lsn { display: grid; gap: var(--sp-3); }
|
|
4
|
+
.c3 .lsn-warn, .c3 .tag.lsn-warn { color: var(--warning); }
|
|
5
|
+
.c3 .lsn-help { margin: 0; }
|
|
6
|
+
.c3 .lsn-aside { align-items: center; }
|
|
7
|
+
.c3 .lsn-thumb { width: 148px; height: 84px; flex: 0 0 auto; object-fit: cover; border: 1px solid var(--border); background: var(--muted); }
|
|
8
|
+
.c3 .lsn-chip { font-size: 12px; }
|
|
9
|
+
.c3 .lsn-chip.off { color: var(--warning); }
|
|
10
|
+
@media (max-width: 640px) { .c3 .lsn-thumb { width: 96px; height: 60px; } }
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/* pm/c3-material.css — meeting material (initiative meeting-material-replay, part 1): the Material part of the Calendar's meeting card, the Add material sheet
|
|
2
|
+
* and the PMOS deck on the Focus stage. Tokens only. Classes are .mm- under .c3; the sheet and the card are drawn with the console3 classes they already use. */
|
|
3
|
+
.c3 .mm h4 .bdg { margin-left: 4px; }
|
|
4
|
+
.c3 .mm-row { align-items: center; }
|
|
5
|
+
.c3 .mm-row .cal3-ct { display: grid; gap: 2px; min-width: 0; }
|
|
6
|
+
.c3 .mm-meta { font-size: var(--fs-1); }
|
|
7
|
+
.c3 .mm-row .ib.sm { width: 24px; height: 24px; }
|
|
8
|
+
.c3 .mm .mm-foot { align-items: center; margin-top: 6px; }
|
|
9
|
+
.c3 .mm-sheet { display: grid; gap: var(--sp-3); }
|
|
10
|
+
.c3 .mm-in { width: 100%; box-sizing: border-box; height: 34px; padding: 0 10px; border: 1px solid var(--border); border-radius: 6px; background: var(--background); color: var(--foreground); font: inherit; font-size: var(--fs-3); }
|
|
11
|
+
.c3 .mm-in:focus-visible { outline: 2px solid color-mix(in oklab, var(--ring) 50%, var(--foreground)); outline-offset: 1px; }
|
|
12
|
+
.c3 .mm-opt.on { background: color-mix(in srgb, var(--acc) 7%, transparent); border-radius: 8px; }
|
|
13
|
+
.c3 .mm-why, .c3 .mm-sel { color: var(--acc); }
|
|
14
|
+
.c3 .mm-opt .nb { min-width: 0; }
|
|
15
|
+
.c3 .mm-opt .chips { flex-wrap: wrap; min-width: 0; }
|
|
16
|
+
.c3 .mm-opt .tag { white-space: normal; overflow-wrap: anywhere; max-width: 100%; }
|
|
17
|
+
.c3 .mm-set { border-color: color-mix(in srgb, var(--acc) 40%, var(--border)); }
|
|
18
|
+
.c3 .mm-f { display: grid; grid-template-columns: 70px minmax(0, 1fr); gap: 8px; align-items: center; padding: 4px 0; }
|
|
19
|
+
.c3 .mm-f .mk-lbl { padding: 0; }
|
|
20
|
+
.c3 .mm-wrap { flex-wrap: wrap; height: auto; }
|
|
21
|
+
@media (max-width: 640px) { .c3 .mm-f { grid-template-columns: minmax(0, 1fr); } }
|
|
22
|
+
@media (pointer: coarse) { .c3 .mm-in { min-height: 44px; } }
|