create-pmos 0.162.0 → 0.164.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/c3-sessions-panel-test.js +49 -0
- package/workspace/guide/console-calendar.md +17 -0
- package/workspace/guide/console3-sessions.md +12 -0
- package/workspace/lib/headless.js +1 -1
- package/workspace/lib/meeting-focus.js +292 -0
- package/workspace/lib/route-modules.js +1 -0
- package/workspace/lib/routes-meeting-focus.js +291 -0
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/c3-calendar.js +7 -1
- package/workspace/public/pm/c3-demo-focus.js +133 -0
- package/workspace/public/pm/c3-demo.js +3 -0
- package/workspace/public/pm/c3-focus.css +169 -0
- package/workspace/public/pm/c3-focus.js +587 -0
- package/workspace/public/pm/c3-session-term.js +69 -3
- package/workspace/public/pm/c3-sheet.js +5 -4
- package/workspace/public/pm/c3-shell.js +1 -0
- package/workspace/public/pm/components.css +1 -0
- package/workspace/public/pm/console3.css +16 -0
- package/workspace/public/pm/shell-recording.js +18 -2
- package/workspace/test-lists/c3-meeting-focus.json +6 -0
|
@@ -6,7 +6,21 @@
|
|
|
6
6
|
import { el } from './dom.js';
|
|
7
7
|
import { openSheet } from './c3-sheet.js';
|
|
8
8
|
import { toast } from './c3-toast.js';
|
|
9
|
-
import { pmGet } from './http.js';
|
|
9
|
+
import { pmGet, pmPost } from './http.js';
|
|
10
|
+
import { button } from './primitives.js';
|
|
11
|
+
import { c3ic } from './pm-work-view.js';
|
|
12
|
+
|
|
13
|
+
/* a primitives button (keyboard, role) carrying the console3 look and children (the pattern c3-dock.js uses) */
|
|
14
|
+
const c3btn = (attrs, onClick, ...kids) => { const b = button({ variant: 'ghost', attrs, onClick }); b.replaceChildren(...kids); return b; };
|
|
15
|
+
const store = {
|
|
16
|
+
get(k) { try { return localStorage.getItem(k); } catch { return null; } },
|
|
17
|
+
set(k, v) { try { localStorage.setItem(k, v); } catch { /* no storage */ } },
|
|
18
|
+
};
|
|
19
|
+
export const SIZE_KEY = 'pmos.workspace.c3.term-size';
|
|
20
|
+
export const PAD_KEY = 'pmos.workspace.c3.term-pad';
|
|
21
|
+
/* the sheet widths: Normal is the 600px wide sheet; the others are classes in console3.css (desktop only) */
|
|
22
|
+
export const SIZES = [['normal', 'Normal', 'Normal'], ['half', 'Half', 'Half of the window'], ['two-thirds', '2/3', 'Two-thirds of the window'], ['whole', 'Whole', 'Whole window']];
|
|
23
|
+
export const sizeOf = (v) => (SIZES.some((x) => x[0] === v) ? v : 'normal');
|
|
10
24
|
|
|
11
25
|
const VENDOR = ['/vendor/xterm.js', '/vendor/addon-fit.js', '/vendor/addon-web-links.js'];
|
|
12
26
|
let vendor = null;
|
|
@@ -29,6 +43,43 @@ function loadVendor() {
|
|
|
29
43
|
return vendor;
|
|
30
44
|
}
|
|
31
45
|
|
|
46
|
+
/* the scratchpad under the terminal: the classic app's per-session pad (panel-tabs.js), kept through /api/scratchpad */
|
|
47
|
+
function makePad(id, term, tab) {
|
|
48
|
+
const url = '/api/scratchpad?session=' + encodeURIComponent(id);
|
|
49
|
+
let typed = false; let timer = null; let pending = null; let closed = false;
|
|
50
|
+
const flush = () => {
|
|
51
|
+
if (timer) { clearTimeout(timer); timer = null; }
|
|
52
|
+
if (pending === null) return;
|
|
53
|
+
const text = pending; pending = null;
|
|
54
|
+
pmPost('/api/scratchpad', { session: id, text }).catch(() => { /* the text stays in the box */ });
|
|
55
|
+
};
|
|
56
|
+
const ta = el('textarea', { class: 'c3t-pad-ta', rows: '4', placeholder: 'Scratchpad. One pad for each session. Saved on this machine.', 'aria-label': 'Scratchpad', 'data-c3-pad': 'text',
|
|
57
|
+
oninput: () => { typed = true; pending = ta.value; if (timer) clearTimeout(timer); timer = setTimeout(flush, 500); sync(); } });
|
|
58
|
+
const send = c3btn({ class: 'pill', 'data-c3-pad': 'send', title: 'Type the pad into this session. Enter is not pressed.' }, () => {
|
|
59
|
+
if (!ta.value.trim()) return;
|
|
60
|
+
if (!term.sendToTerm(tab, ta.value)) { toast({ text: 'The text was not sent. The terminal is not connected.', tone: 'error' }); return; }
|
|
61
|
+
if (typeof term.focusTerm === 'function') term.focusTerm(tab);
|
|
62
|
+
}, c3ic('out', 'sm'), 'Send to terminal');
|
|
63
|
+
const open0 = store.get(PAD_KEY) !== 'closed';
|
|
64
|
+
const box = el('div', { class: 'c3t-pad', 'data-c3-pad': 'box' });
|
|
65
|
+
const toggle = c3btn({ class: 'chip2 lk', 'data-c3-pad': 'toggle' }, () => { const on = box.dataset.open !== 'true'; store.set(PAD_KEY, on ? 'open' : 'closed'); set(on); },
|
|
66
|
+
c3ic('edit', 'sm'), 'Scratchpad');
|
|
67
|
+
function set(on) {
|
|
68
|
+
box.dataset.open = String(on);
|
|
69
|
+
toggle.setAttribute('aria-expanded', String(on));
|
|
70
|
+
toggle.title = on ? 'Hide the scratchpad. The text is kept.' : 'Show the scratchpad';
|
|
71
|
+
ta.hidden = !on;
|
|
72
|
+
requestAnimationFrame(() => term.refitVisible());
|
|
73
|
+
}
|
|
74
|
+
function sync() { send.disabled = !ta.value.trim(); }
|
|
75
|
+
box.append(el('div', { class: 'c3t-pad-h' }, toggle, send), ta);
|
|
76
|
+
set(open0); sync();
|
|
77
|
+
pmGet(url).then((r) => { if (!typed && r && typeof r.text === 'string') { ta.value = r.text; sync(); } }).catch(() => { /* an empty pad */ });
|
|
78
|
+
const onHide = () => flush();
|
|
79
|
+
window.addEventListener('pagehide', onHide);
|
|
80
|
+
return { box, ta, dispose() { if (closed) return; closed = true; window.removeEventListener('pagehide', onHide); flush(); } };
|
|
81
|
+
}
|
|
82
|
+
|
|
32
83
|
export async function openSessionTerm({ id, name } = {}) {
|
|
33
84
|
if (!id) return null;
|
|
34
85
|
if (!name) { const r = await pmGet('/api/sessions').catch(() => null); const x = r && Array.isArray(r.sessions) ? r.sessions.find((y) => y.id === id) : null; name = x ? x.name : id; }
|
|
@@ -36,8 +87,23 @@ export async function openSessionTerm({ id, name } = {}) {
|
|
|
36
87
|
if (!ok || !term) { toast({ text: 'The terminal could not be loaded on this page.', tone: 'error' }); return null; }
|
|
37
88
|
const tab = { id: 'c3-session:' + id, kind: 'term', sessionId: id, spawn: false, sess: id };
|
|
38
89
|
const host = el('div', { class: 'c3t-term', 'data-c3-session-term': id });
|
|
39
|
-
|
|
40
|
-
const
|
|
90
|
+
const pad = makePad(id, term, tab);
|
|
91
|
+
const wrap = el('div', { class: 'c3t-tw' }, host, pad.box);
|
|
92
|
+
// the size control: Normal is the 600px sheet, the others are the classic dock's presets. Hidden on a phone, where the sheet is full width.
|
|
93
|
+
let size = sizeOf(store.get(SIZE_KEY));
|
|
94
|
+
const sizeBtns = SIZES.map(([k, label, title]) => c3btn({ 'aria-pressed': String(k === size), 'data-c3-size': k, title, 'aria-label': title }, () => setSize(k), label));
|
|
95
|
+
const seg = el('div', { class: 'seg c3t-size', role: 'group', 'aria-label': 'Terminal size', 'data-c3-size-seg': '' }, ...sizeBtns);
|
|
96
|
+
const cls = (k) => 'tsz-' + k;
|
|
97
|
+
// Esc and Tab belong to the shell while the terminal has focus (Esc interrupts an agent); the close button and a press outside still close.
|
|
98
|
+
// The scratchpad is outside the terminal host, so its keys are the page's own.
|
|
99
|
+
const sheet = openSheet({ title: name || id, icon: 'term', wide: true, body: wrap, actions: seg, cls: cls(size),
|
|
100
|
+
onClose: () => { pad.dispose(); term.disposeTerm(tab); }, passKeys: (e) => host.contains(e.target) });
|
|
101
|
+
function setSize(k) {
|
|
102
|
+
sheet.sheet.classList.remove(cls(size)); size = sizeOf(k); sheet.sheet.classList.add(cls(size));
|
|
103
|
+
store.set(SIZE_KEY, size);
|
|
104
|
+
sizeBtns.forEach((b) => b.setAttribute('aria-pressed', String(b.dataset.c3Size === size)));
|
|
105
|
+
requestAnimationFrame(() => { term.refitVisible(); requestAnimationFrame(() => term.refitVisible()); });
|
|
106
|
+
}
|
|
41
107
|
term.mountTerm(host, tab);
|
|
42
108
|
requestAnimationFrame(() => { term.refitVisible(); term.focusTerm(tab); });
|
|
43
109
|
return sheet;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/* pm/c3-sheet.js — the console3 sheet, as a primitive (console3.html: bsHtml line 2502, the .sheet rules 143 to 145, 180, 496 to 502,
|
|
2
2
|
* the Esc handler 2536 and the phone drag 2538 to 2540).
|
|
3
3
|
*
|
|
4
|
-
* openSheet({ title, icon, body, wide, dw, scrim, onClose, passKeys }) -> { close, root, sheet, body } passKeys(e): true leaves that key to the page
|
|
4
|
+
* openSheet({ title, icon, body, wide, dw, scrim, onClose, passKeys, actions, cls }) -> { close, root, sheet, body } passKeys(e): true leaves that key to the page
|
|
5
|
+
* actions: a Node or an array of Nodes placed in the header before the close button; cls: extra class names on the sheet
|
|
5
6
|
* closeSheet() closes the open sheet, if any
|
|
6
7
|
*
|
|
7
8
|
* On a desktop window the sheet is the right-hand panel (340px; `wide` 600px; `dw` the document sheet, up to 780px) over a
|
|
@@ -50,7 +51,7 @@ export function closeSheet() {
|
|
|
50
51
|
if (typeof c.onClose === 'function') { try { c.onClose(); } catch { /* the caller reports its own errors */ } }
|
|
51
52
|
}
|
|
52
53
|
|
|
53
|
-
export function openSheet({ title = '', icon = 'doc', body, wide = false, dw = false, scrim = true, onClose, passKeys } = {}) {
|
|
54
|
+
export function openSheet({ title = '', icon = 'doc', body, wide = false, dw = false, scrim = true, onClose, passKeys, actions, cls = '' } = {}) {
|
|
54
55
|
closeSheet();
|
|
55
56
|
ensureC3Css();
|
|
56
57
|
const opener = document.activeElement && document.activeElement !== document.body ? document.activeElement : null;
|
|
@@ -60,8 +61,8 @@ export function openSheet({ title = '', icon = 'doc', body, wide = false, dw = f
|
|
|
60
61
|
const sbb = el('div', { class: 'brief sbb' }, ...bodyNodes(body));
|
|
61
62
|
const closeBtn = el('button', { type: 'button', class: 'ib sm', 'aria-label': 'Close', title: 'Close (Esc)', onclick: close }, c3ic('x', 'sm'));
|
|
62
63
|
const grab = el('div', { class: 'grab' });
|
|
63
|
-
const head = el('div', { class: 'sh2' }, el('span', { class: 'chips2' }, c3ic(icon), el('b', null, title)), el('span', { class: 'chips2' }, closeBtn));
|
|
64
|
-
const sheet = el('div', { class: 'sheet bs' + (wide ? ' wide' : '') + (dw ? ' dw' : ''), role: 'dialog', 'aria-modal': String(!!scrim), 'aria-label': title, tabindex: '-1' },
|
|
64
|
+
const head = el('div', { class: 'sh2' }, el('span', { class: 'chips2' }, c3ic(icon), el('b', null, title)), el('span', { class: 'chips2' }, ...bodyNodes(actions), closeBtn));
|
|
65
|
+
const sheet = el('div', { class: 'sheet bs' + (wide ? ' wide' : '') + (dw ? ' dw' : '') + (cls ? ' ' + cls : ''), role: 'dialog', 'aria-modal': String(!!scrim), 'aria-label': title, tabindex: '-1' },
|
|
65
66
|
grab, head, el('div', { class: 'sb' }, sbb));
|
|
66
67
|
/* the mockup's person and playbooks sheets have no scrim (they are panels beside the page, closed by their own button or Esc): `scrim: false` */
|
|
67
68
|
root.append(...(scrim ? [el('div', { class: 'scrim', onclick: close })] : []), sheet);
|
|
@@ -616,6 +616,7 @@ async function boot() {
|
|
|
616
616
|
let start = q.get('tab');
|
|
617
617
|
const saved = DEMO ? null : await pmGet('/api/settings' + (S.ws ? '?ws=' + encodeURIComponent(S.ws) : '')).catch(() => null); // demo mode's workspace is not registered
|
|
618
618
|
if (saved && saved.ok) { applyLook(root, saved.settings); if (!start) { const s = (saved.settings || []).find((x) => x.key === 'look.start'); if (s && BUILD[s.value]) start = s.value; } }
|
|
619
|
+
if (q.get('focus') || q.get('fdate')) import('./c3-focus.js').then((m) => m.openFocus({ event: { id: q.get('focus') || '' }, date: q.get('fdate') || '' })).catch(() => { /* the focus page is not available */ }); // a reload keeps the Meeting focus page open
|
|
619
620
|
if (!S.userNav) showTab(start || 'brief', false); // a press during the read above (the gear, a tab) wins over the start page
|
|
620
621
|
new ResizeObserver(moveInd).observe(S.tabNav);
|
|
621
622
|
workCount(); briefCount(); askCount(); S.dock.start();
|
|
@@ -8356,4 +8356,5 @@
|
|
|
8356
8356
|
}
|
|
8357
8357
|
|
|
8358
8358
|
/* -- layouts · .cal3- the console3 Calendar page (calendar-time-page): its rules sit in console3.css beside the .vday rules they extend -- */
|
|
8359
|
+
/* -- layouts · .fc- the console3 meeting focus page (c3-meeting-focus): its rules sit in c3-focus.css -- */
|
|
8359
8360
|
/* -- end of the layout sections: add nothing below this line -- */
|
|
@@ -1660,6 +1660,22 @@
|
|
|
1660
1660
|
.c3 .c3t-panel{display:grid;gap:var(--sp-2);border:1px solid var(--border);border-radius:10px;padding:var(--sp-3)}
|
|
1661
1661
|
/* a session's terminal in a sheet (c3-session-term.js) */
|
|
1662
1662
|
.c3 .c3t-term{height:calc(100vh - 150px);min-height:240px;display:flex;flex-direction:column}
|
|
1663
|
+
/* the terminal plus its scratchpad (c3-session-term.js): the terminal shrinks to make room for the pad */
|
|
1664
|
+
.c3 .c3t-tw{height:calc(100vh - 150px);min-height:240px;display:flex;flex-direction:column;gap:var(--sp-2);min-width:0}
|
|
1665
|
+
.c3 .c3t-tw .c3t-term{height:auto;flex:1 1 0;min-height:120px}
|
|
1666
|
+
.c3 .c3t-pad{display:grid;gap:var(--sp-2);flex:none;border-top:1px solid var(--border);padding-top:var(--sp-2)}
|
|
1667
|
+
.c3 .c3t-pad-h{display:flex;gap:var(--sp-2);align-items:center;justify-content:space-between}
|
|
1668
|
+
.c3 .c3t-pad-h .pill:disabled{opacity:.35;cursor:not-allowed}
|
|
1669
|
+
.c3 .c3t-pad-ta{width:100%;min-height:72px;max-height:40vh;resize:vertical;font:var(--fs-2) var(--mono);box-sizing:border-box}
|
|
1670
|
+
/* terminal sheet sizes, as the classic dock has them; the phone sheet is already full width */
|
|
1671
|
+
.c3 .c3t-size{flex:none}
|
|
1672
|
+
.c3 .c3t-size button{padding:4px 8px}
|
|
1673
|
+
.c3.ph .c3t-size{display:none}
|
|
1674
|
+
@media (min-width:761px){
|
|
1675
|
+
.c3 .sheet.wide.tsz-half{width:50vw}
|
|
1676
|
+
.c3 .sheet.wide.tsz-two-thirds{width:66.667vw}
|
|
1677
|
+
.c3 .sheet.wide.tsz-whole{width:calc(100vw - 24px)}
|
|
1678
|
+
}
|
|
1663
1679
|
.c3 .c3t-panel .feed{display:flex;flex-direction:column;gap:var(--sp-2);max-height:220px;overflow:auto}
|
|
1664
1680
|
.c3 .c3t-panel .m{padding:6px 12px;border-radius:12px;max-width:88%;overflow-wrap:anywhere}
|
|
1665
1681
|
.c3 .c3t-panel .m.me{align-self:flex-end;background:var(--acc);color:var(--on-accent)}.c3 .c3t-panel .m.bot{background:var(--muted)}
|
|
@@ -15,6 +15,7 @@ import { el } from './dom.js';
|
|
|
15
15
|
import { button } from './primitives.js';
|
|
16
16
|
import { ensureC3Css, c3ic } from './pm-work-view.js';
|
|
17
17
|
import * as dock from './note-dock.js';
|
|
18
|
+
import { noteById } from './notes-links.js';
|
|
18
19
|
import { recView, clockOf } from './shell-model.js';
|
|
19
20
|
import { reducedMotion } from './shell-ticker.js';
|
|
20
21
|
import { toast } from './c3-toast.js';
|
|
@@ -141,7 +142,7 @@ export function disposeRecording(host) {
|
|
|
141
142
|
|
|
142
143
|
const NS_SVG = 'http://www.w3.org/2000/svg';
|
|
143
144
|
const glyph = (d) => { const s = document.createElementNS(NS_SVG, 'svg'); s.setAttribute('class', 'ic sm'); s.setAttribute('viewBox', '0 0 24 24'); s.setAttribute('aria-hidden', 'true'); const p = document.createElementNS(NS_SVG, 'path'); p.setAttribute('d', d); s.appendChild(p); return s; };
|
|
144
|
-
const RGL = { highlighter: () => c3ic('flag', 'sm'), pause: () => glyph('M8 5v14M16 5v14'), play: () => c3ic('play', 'sm'), square: () => c3ic('stop', 'sm'), mic: () => c3ic('mic', 'sm'), down: () => glyph('M6 9l6 6 6-6') };
|
|
145
|
+
const RGL = { highlighter: () => c3ic('flag', 'sm'), pause: () => glyph('M8 5v14M16 5v14'), play: () => c3ic('play', 'sm'), square: () => c3ic('stop', 'sm'), mic: () => c3ic('mic', 'sm'), focus: () => c3ic('target', 'sm'), down: () => glyph('M6 9l6 6 6-6') };
|
|
145
146
|
|
|
146
147
|
/** The chip opens the note: on console3, Calendar › Notes on the During stage (calendar-views.js listens for c3-open-note);
|
|
147
148
|
* elsewhere the main app, where the full note is. */
|
|
@@ -165,6 +166,19 @@ async function engineLabel() {
|
|
|
165
166
|
} catch { return ''; }
|
|
166
167
|
}
|
|
167
168
|
|
|
169
|
+
/** The calendar meeting a note is recording, or null for a note that is not linked to one. */
|
|
170
|
+
function meetingOfNote(id) {
|
|
171
|
+
let n = null; try { n = noteById(id); } catch { n = null; }
|
|
172
|
+
const m = n && n.meeting;
|
|
173
|
+
return m && m.kind === 'event' && m.start ? m : null;
|
|
174
|
+
}
|
|
175
|
+
/** Meeting focus (initiative c3-meeting-focus): the page for the meeting being recorded. The event id is empty for a meeting without a stored id; the page then finds it by start and title. */
|
|
176
|
+
function openFocusFor(m) {
|
|
177
|
+
const day = String(m.start || '').slice(0, 10);
|
|
178
|
+
import('./c3-focus.js').then((f) => f.openFocus({ event: { id: m.event_id || '', at: m.start, end: m.end || null, title: m.title }, date: day }))
|
|
179
|
+
.catch(() => toast({ text: 'The focus page could not load.', tone: 'error' }));
|
|
180
|
+
}
|
|
181
|
+
|
|
168
182
|
function mountReal(host) {
|
|
169
183
|
ensureC3Css();
|
|
170
184
|
let ready = false;
|
|
@@ -201,7 +215,8 @@ function mountReal(host) {
|
|
|
201
215
|
let st; try { st = dock.recordState(); } catch { st = { mode: 'idle' }; }
|
|
202
216
|
if (st.mode !== 'idle' && eng.id !== st.id) { eng = { id: st.id, text: '' }; const want = st.id; engineLabel().then((t) => { if (eng.id === want && t) { eng.text = t; rec.key = ''; sync(); } }); }
|
|
203
217
|
const hl = st.mode === 'recording' && dock.highlightBusy ? dock.highlightBusy() : null;
|
|
204
|
-
const
|
|
218
|
+
const mt = st.mode !== 'idle' ? meetingOfNote(st.id) : null;
|
|
219
|
+
const key = [st.mode, st.id, st.title, hl ? hl.state + hl.open : '', eng.text, mt ? 'f' + (mt.event_id || '') + mt.start : ''].join(':');
|
|
205
220
|
if (key !== rec.key) {
|
|
206
221
|
rec.key = key; rec.clock = null;
|
|
207
222
|
const recording = st.mode === 'recording';
|
|
@@ -218,6 +233,7 @@ function mountReal(host) {
|
|
|
218
233
|
group.push(btn(hl ? 'Highlight in progress: show it' : 'Highlight the last 30 seconds and what is said next', 'highlighter', () => dock.highlightNow(), { 'data-fr-rec-highlight': hl ? hl.state : '', 'data-shell-rec-mark': '' }),
|
|
219
234
|
btn('Pause (you can resume within 1 hour)', 'pause', () => dock.pauseNow(), { 'data-fr-rec-pause': '', 'data-shell-rec-pause': '' }));
|
|
220
235
|
} else group.push(btn('Resume ' + st.title, 'play', () => dock.resumeNow(), { 'data-fr-rec-resume': '', 'data-shell-rec-resume': '' }));
|
|
236
|
+
if (mt) group.push(btn('Open the focus page for this meeting', 'focus', () => openFocusFor(mt), { 'data-fr-rec-focus': '', 'data-shell-rec-focus': '' }));
|
|
221
237
|
group.push(btn('Stop for good (cannot be resumed)', 'square', () => dock.stopNow(), { 'data-fr-rec-stop': '', 'data-shell-rec-stop': '' }));
|
|
222
238
|
kids.push(el('span', { class: 'recg c3rec__live' + (recording ? '' : ' paused'), role: 'group', 'aria-label': (recording ? 'Recording ' : 'Paused: ') + st.title }, group));
|
|
223
239
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Initiative c3-meeting-focus: the Meeting focus page of console3. The unit suite covers the route (read, topic, ask), the companion prompt and its parsing, and when the companion runs; the browser suite runs the page in demo mode.",
|
|
3
|
+
"modules": ["meeting-focus.test.js"],
|
|
4
|
+
"panels": ["pm-c3-meeting-focus-test.js"],
|
|
5
|
+
"seconds": { "meeting-focus.test.js": 10, "pm-c3-meeting-focus-test.js": 60 }
|
|
6
|
+
}
|