create-pmos 0.200.0 → 0.202.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-foundation-test.js +3 -2
- package/workspace/c3-phone-test.js +4 -2
- package/workspace/guide/core-documents.md +1 -1
- package/workspace/guide/phone-companion.md +4 -0
- package/workspace/lib/routes-core-docs.js +6 -11
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/c3.html +2 -2
- package/workspace/public/pm/c3-boot.js +10 -0
- package/workspace/public/pm/c3-phone-boot.js +47 -0
- package/workspace/public/pm/c3-phone.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.202.0",
|
|
4
4
|
"description": "Scaffold the PMOS portable kit — run the PMOS product-management workflow (initiative → lane-fit spec → contract → build → three gates, with work-type lanes for features, research, maintenance, incidents, and content) entirely inside your own repo, with all state as git-versioned files. Includes `workflow-sweep`, which audits the state every board derives from and separates real defects from work you simply have not done. `create-pmos workspace` also installs + launches the local Terminal Workspace GUI. No database, no network, no telemetry.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
package/payload/KIT-VERSION
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Eval calibration log"
|
|
4
4
|
description: "Append-only record of every PM override of an evaluator verdict."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-13T11:
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-13T11:45:43Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Health budgets"
|
|
4
4
|
description: "Closed registry of health-budget anchors (D59), edited only by the PM."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-13T11:
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-13T11:45:43Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Discoveries and friction log"
|
|
4
4
|
description: "Append-only record of what runs discovered and where the process rubbed."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-13T11:
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-13T11:45:43Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -153,8 +153,9 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
|
|
|
153
153
|
ok('R2 where canvas is unavailable the page still draws and raises no error', N.up && N.errors.length === 0 && await N.page.evaluate(() => document.querySelectorAll('canvas.c3amb').length === 0), N.errors.join(' | '));
|
|
154
154
|
await N.ctx.close();
|
|
155
155
|
|
|
156
|
-
/* phone width: no horizontal scroll
|
|
157
|
-
|
|
156
|
+
/* phone width: no horizontal scroll. A phone shows the companion (c3-phone-test.js); this holds the desktop layout at that width
|
|
157
|
+
* (lib/c3-full-console.js). */
|
|
158
|
+
const M = await open({ viewport: { width: 400, height: 900 } }, null, () => { try { localStorage.setItem('pmos.c3.testDesk', 'desk'); } catch { /* no storage */ } });
|
|
158
159
|
ok('0 at 400 wide the page does not scroll sideways', M.up && await M.page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1), 'scrollWidth');
|
|
159
160
|
ok('0 no page error (400 wide)', M.errors.length === 0, M.errors.join(' | '));
|
|
160
161
|
await M.ctx.close();
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
|
-
/* c3-phone-test.js — the phone companion (initiative c3-phone-companion; PRD done-criteria 1 to 6 and 8): c3-phone.js, its sheets c3-phone-detail.js and its demo notes c3-demo-phone.js
|
|
3
|
+
/* c3-phone-test.js — the phone companion (initiative c3-phone-companion; PRD done-criteria 1 to 6 and 8): c3-phone.js, its sheets c3-phone-detail.js and its demo notes c3-demo-phone.js;
|
|
4
|
+
* c3-boot.js and c3-phone-boot.js, which load the companion alone at phone width.
|
|
4
5
|
* A real server and a real Chromium drive /c3.html in demo mode (clock frozen at Thu 8 Oct 2026 08:12; ?at=HH:MM moves the companion's clock).
|
|
5
6
|
*
|
|
6
|
-
* phone at 400 wide the companion shows and the desktop grid does not
|
|
7
|
+
* phone at 400 wide the companion shows and the desktop grid does not; only the companion's modules load
|
|
7
8
|
* moments 08:12 morning; 10:10 before Checkout sync; 10:45 in the meeting with the Record offer
|
|
8
9
|
* done Done on the waiting item records a write to /api/pm-items/done
|
|
9
10
|
* tell text sent from the ask bar records a capture and clears the bar
|
|
@@ -73,6 +74,7 @@ const writes = (p) => (window.__c3demoWrites || []).filter((w) => w.path === p |
|
|
|
73
74
|
let page = await open(pctx, '/c3.html?demo=1');
|
|
74
75
|
ok('phone: the companion shows at 400 wide', await page.evaluate(shown, '[data-c3ph]'));
|
|
75
76
|
ok('phone: the desktop grid is hidden', !(await page.evaluate(shown, '.c3grid')));
|
|
77
|
+
ok('phone: only the companion loads, not the desktop console', await page.evaluate(() => { const js = performance.getEntriesByType('resource').map((r) => r.name); return js.some((u) => /c3-phone-boot\.js/.test(u)) && !js.some((u) => /c3-shell\.js|calendar-views\.js|c3-initiatives\.js/.test(u)); }));
|
|
76
78
|
ok('phone: no sideways scroll', await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1));
|
|
77
79
|
ok('moments: 08:12 is morning', (await moment(page)) === 'morning', await moment(page));
|
|
78
80
|
ok('phone: the ask bar and the red Record button are in the bottom bar', await page.evaluate(() => !!document.querySelector('[data-c3ph-foot] [data-c3ph-input]') && !!document.querySelector('[data-c3ph-foot] [data-c3ph-record]')));
|
|
@@ -45,7 +45,7 @@ A row that is not started always has a next step:
|
|
|
45
45
|
## How PMOS drafts a document
|
|
46
46
|
|
|
47
47
|
- **When you ask.** Press **Draft it** on a row, or **Ask PMOS to change it** on a draft. PMOS starts one background run. The run reads the sources of that document and writes one draft. It has no tool that sends, posts or edits.
|
|
48
|
-
- **On a schedule.** Every 30 minutes inside your working hours, PMOS drafts each portfolio and board document that has no draft and is due, out of date or not started yet. Due documents go first, then out-of-date ones, then the first draft of documents not started. While no OKR files exist, it also proposes the OKRs.
|
|
48
|
+
- **On a schedule.** Every 30 minutes inside your working hours, PMOS drafts each portfolio and board document that has no draft and is due, out of date or not started yet. Due documents go first, then out-of-date ones, then the first draft of documents not started. While no OKR files exist, it also proposes the OKRs. There is no limit on how many drafts run at once. If a run writes nothing, PMOS tries again on the next tick. If you drop a draft, PMOS does not draft that document again until the next day. It never drafts a document that you write (the product strategy).
|
|
49
49
|
- **The Initiative Driver.** When the current stage of an initiative needs a document that is not there or is stale, the driver drafts it and files a proposal. **Keep** issues the draft. The driver does not propose a stage move while a document of the current stage is due.
|
|
50
50
|
|
|
51
51
|
## Issue a draft
|
|
@@ -65,6 +65,10 @@ A phone always shows the companion. There is no way to the desktop pages on a ph
|
|
|
65
65
|
|
|
66
66
|
The number of things that wait on you, at the top right, opens the list of all of them.
|
|
67
67
|
|
|
68
|
+
## Loading
|
|
69
|
+
|
|
70
|
+
On a phone, PMOS loads only the companion, not the pages of the computer. This makes the first load faster on a phone connection. If you turn the screen so that it is wider than a phone, the page loads again and shows the pages of the computer.
|
|
71
|
+
|
|
68
72
|
## Limits
|
|
69
73
|
|
|
70
74
|
- No notification on the lock screen, and no spoken brief.
|
|
@@ -13,11 +13,11 @@
|
|
|
13
13
|
*
|
|
14
14
|
* THE RUN. A press on Draft it (and the schedule tick) starts ONE headless run, kind `core-doc` (lib/headless.js), origin
|
|
15
15
|
* `core-doc:<scope>:<key>`. Its tools are Read, Glob, Grep and `pmos coredoc write`; the server pins the entry it may write with
|
|
16
|
-
* PMOS_CORE_DOC_SCOPE and PMOS_CORE_DOC_KEY.
|
|
16
|
+
* PMOS_CORE_DOC_SCOPE and PMOS_CORE_DOC_KEY. There is no cap on how many run at once (PM, 2026-10-13: no run limit).
|
|
17
17
|
*
|
|
18
18
|
* THE TICK (PRD section 4). tick(ctx, { now }) runs every 30 minutes inside the working hours (lib/driver-run.js dayCheckOpen: the day-check
|
|
19
19
|
* window and the quiet hours). For each repository (the lead tree) it drafts every `doc` entry of the portfolio and of each board whose cadence
|
|
20
|
-
* has passed (state `due`) and that has no open draft
|
|
20
|
+
* has passed (state `due`), is out of date or is not started, and that has no open draft and no run; never an entry the PM writes (`by: you`). Initiative
|
|
21
21
|
* documents are the Initiative Driver's (lib/driver.js doc-due). ctx adds to the route ctx: { proSettings(), inQuiet(now, quiet), isLead(w) }.
|
|
22
22
|
*/
|
|
23
23
|
const guard = require('./pm-guard.js');
|
|
@@ -27,11 +27,9 @@ const headless = require('./headless.js');
|
|
|
27
27
|
const text = (v) => (typeof v === 'string' ? v : '');
|
|
28
28
|
const bad = (status, error) => ({ status, body: { ok: false, error } });
|
|
29
29
|
const POSTS = ['draft', 'issue', 'drop'];
|
|
30
|
-
const RUN_CAP = 3;
|
|
31
30
|
const TICK_EVERY_MS = 30 * 60 * 1000;
|
|
32
31
|
|
|
33
32
|
const runs = new Map(); // `${ws}|${scope}|${key}` → session id, for the core-doc runs this server started
|
|
34
|
-
const tried = new Map(); // `${ws}|${scope}|${key}` → the local day the tick last started a run for it
|
|
35
33
|
let lastTick = null;
|
|
36
34
|
|
|
37
35
|
const runKey = (ws, scope, key) => `${ws}|${scope}|${key}`;
|
|
@@ -60,7 +58,6 @@ function startRun(ctx, w, scope, entry, { label, now } = {}) {
|
|
|
60
58
|
if (headless.kindFor({ agent: 'claude', origin }) !== 'core-doc') return { ok: false, status: 409, error: 'Background runs are switched off here (PMOS_HEADLESS=off), so PMOS cannot draft this document.' };
|
|
61
59
|
const k = runKey(w.id, scope, entry.key);
|
|
62
60
|
if (runs.has(k)) return { ok: false, status: 409, error: `PMOS is drafting the ${entry.title.toLowerCase()} already` };
|
|
63
|
-
if (runs.size >= RUN_CAP) return { ok: false, status: 409, error: `${RUN_CAP} documents are being drafted already. Try again when one is done.` };
|
|
64
61
|
const prompt = CD.runPrompt(entry, scope, { today: CD.localDay(now == null ? Date.now() : now), label });
|
|
65
62
|
const s = ctx.spawnSession({ name: `Core document · ${entry.title}`, agent: 'claude', ws: w.id, origin, initialInput: prompt + '\r',
|
|
66
63
|
initiative: scope.startsWith('initiative:') ? scope.slice('initiative:'.length) : undefined,
|
|
@@ -90,7 +87,7 @@ function tick(ctx, { now = Date.now(), force = false } = {}) {
|
|
|
90
87
|
const out = [];
|
|
91
88
|
const list = typeof ctx.workspaceList === 'function' ? ctx.workspaceList() : [ctx.getWs(null)].filter(Boolean);
|
|
92
89
|
// Every candidate first, then the most pressing: due (issued, cadence passed), out of date, then not started (the first draft, and the
|
|
93
|
-
// proposed OKRs while no OKR files exist).
|
|
90
|
+
// proposed OKRs while no OKR files exist). No cap: every candidate starts; a failed run is tried again on the next tick.
|
|
94
91
|
const RANK = { due: 0, stale: 1, missing: 2 };
|
|
95
92
|
const todo = [];
|
|
96
93
|
for (const w of list) {
|
|
@@ -107,17 +104,15 @@ function tick(ctx, { now = Date.now(), force = false } = {}) {
|
|
|
107
104
|
const st = CD.stateOf(entry, g.facts);
|
|
108
105
|
if (!(st.st in RANK) || st.draft) continue;
|
|
109
106
|
const k = runKey(w.id, g.facts.scope, entry.key);
|
|
110
|
-
if (runs.has(k)
|
|
107
|
+
if (runs.has(k)) continue;
|
|
111
108
|
const x = g.facts.store.get(`${g.facts.scope}|${entry.key}`);
|
|
112
|
-
if (x && x.drafts.some((d) => CD.localDay(Date.parse(d.at)) === day)) continue; //
|
|
109
|
+
if (x && x.drafts.some((d) => x.drops.has(d.slot) && CD.localDay(Date.parse(d.at)) === day)) continue; // the PM dropped today's draft: not again until tomorrow
|
|
113
110
|
todo.push({ w, g, entry, k, order: [RANK[st.st], li, ei] });
|
|
114
111
|
}
|
|
115
112
|
}
|
|
116
113
|
}
|
|
117
114
|
todo.sort((a, b) => a.order[0] - b.order[0] || a.order[1] - b.order[1] || a.order[2] - b.order[2]);
|
|
118
115
|
for (const t of todo) {
|
|
119
|
-
if (runs.size >= RUN_CAP) break;
|
|
120
|
-
tried.set(t.k, day);
|
|
121
116
|
const r = startRun(ctx, t.w, t.g.facts.scope, t.entry, { label: labelOf(t.g.facts), now });
|
|
122
117
|
out.push({ ws: t.w.id, scope: t.g.facts.scope, key: t.entry.key, ok: r.ok, ...(r.ok ? { run: r.run } : { error: r.error }) });
|
|
123
118
|
}
|
|
@@ -175,4 +170,4 @@ async function handle(req, res, url, ctx) {
|
|
|
175
170
|
return send({ status: 200, body: { ok: true } });
|
|
176
171
|
}
|
|
177
172
|
|
|
178
|
-
module.exports = { prefixes: ['/api/core-docs'], handle, tick, startRun,
|
|
173
|
+
module.exports = { prefixes: ['/api/core-docs'], handle, tick, startRun, TICK_EVERY_MS, _runs: runs, _reset: () => { runs.clear(); lastTick = null; } };
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.202.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "pmos-workspace",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.202.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/workspace/package.json
CHANGED
package/workspace/public/c3.html
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
|
|
16
16
|
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png" />
|
|
17
17
|
<!-- The console3 shell (header, tabs, three columns, dock), served as the default page at / (and still at /c3.html). It
|
|
18
|
-
composes the real views from pm/; the older shell (index.html) is served at /classic.html. Tokens and fonts are shared, so light and dark both resolve. pm/c3-shell.js sets the
|
|
18
|
+
composes the real views from pm/; the older shell (index.html) is served at /classic.html. Tokens and fonts are shared, so light and dark both resolve. pm/c3-boot.js loads the phone companion alone at phone width (c3-phone-boot.js) and the desktop console (pm/c3-shell.js) otherwise; the shell sets the
|
|
19
19
|
theme attribute (default light, like the mockup). The nonce meta is filled by the server the way it is for /, so a press
|
|
20
20
|
from a view (Add, Accept, Allow) carries it. -->
|
|
21
21
|
<link rel="preload" href="/vendor/ibm-plex-mono-400.woff2" as="font" type="font/woff2" crossorigin />
|
|
@@ -30,6 +30,6 @@
|
|
|
30
30
|
<body>
|
|
31
31
|
<div id="c3-root" class="c3 c3pg" data-pm-theme="light"></div>
|
|
32
32
|
<noscript>This page needs JavaScript.</noscript>
|
|
33
|
-
<script type="module" src="/pm/c3-
|
|
33
|
+
<script type="module" src="/pm/c3-boot.js"></script>
|
|
34
34
|
</body>
|
|
35
35
|
</html>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/* pm/c3-boot.js — the first script of /c3.html: it picks which page to load (initiative c3-phone-companion).
|
|
2
|
+
* At phone width the companion loads alone (c3-phone-boot.js): about 30 modules instead of the desktop console's 100 and its
|
|
3
|
+
* reads of every tab. The desktop console (c3-shell.js) loads only on a wider screen. Turning a phone to a wide screen reloads
|
|
4
|
+
* the page, so the desktop console starts from the beginning. pmos.c3.testDesk is for the browser suites that test the
|
|
5
|
+
* desktop layout at phone width (lib/c3-full-console.js), as in c3-phone.js phoneWanted. */
|
|
6
|
+
const narrow = typeof matchMedia === 'function' && matchMedia('(max-width:640px)').matches;
|
|
7
|
+
let desk = false;
|
|
8
|
+
try { desk = localStorage.getItem('pmos.c3.testDesk') === 'desk'; } catch { /* no storage */ }
|
|
9
|
+
if (narrow && !desk) import('./c3-phone-boot.js');
|
|
10
|
+
else import('./c3-shell.js');
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/* pm/c3-phone-boot.js — the phone companion on its own (initiative c3-phone-companion). c3-boot.js loads it at phone width
|
|
2
|
+
* instead of the desktop console, so a phone reads only what the companion shows. It does what c3-shell.js boot() does for
|
|
3
|
+
* the companion: the demo, the workspace, the theme, then mountPhone. A wider screen reloads into the desktop console. */
|
|
4
|
+
import { pmGet } from './http.js';
|
|
5
|
+
import { ensureC3Css, c3Phone } from './pm-work-view.js';
|
|
6
|
+
import { mountPhone } from './c3-phone.js';
|
|
7
|
+
|
|
8
|
+
const q = new URLSearchParams(location.search);
|
|
9
|
+
const DEMO = q.get('demo') === '1';
|
|
10
|
+
const store = { get(k) { try { return localStorage.getItem(k); } catch { return null; } } };
|
|
11
|
+
|
|
12
|
+
/* A GET to /api/ with no ws is sent for this page's workspace, as c3-shell.js withWs() does. */
|
|
13
|
+
function withWs(ws) {
|
|
14
|
+
const f = window.fetch.bind(window);
|
|
15
|
+
window.fetch = (input, init) => {
|
|
16
|
+
try {
|
|
17
|
+
if (typeof input === 'string' && ws && input.startsWith('/api/') && !/[?&]ws=/.test(input) && (!init || !init.method || init.method === 'GET')) input += (input.includes('?') ? '&' : '?') + 'ws=' + encodeURIComponent(ws);
|
|
18
|
+
} catch { /* leave the request as it is */ }
|
|
19
|
+
return f(input, init);
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function boot() {
|
|
24
|
+
const root = document.getElementById('c3-root');
|
|
25
|
+
let ws = q.get('ws') || '';
|
|
26
|
+
if (DEMO) { const m = await import('./c3-demo.js'); m.installDemo(); if (window.__c3demoReady) await window.__c3demoReady; if (!ws) ws = m.DEMO_WS; root.setAttribute('data-c3-demo', '1'); }
|
|
27
|
+
const pref = q.get('theme') || store.get('pmos.c3.theme') || 'light';
|
|
28
|
+
const theme = pref === 'dark' || (pref === 'system' && typeof matchMedia === 'function' && matchMedia('(prefers-color-scheme: dark)').matches) ? 'dark' : 'light';
|
|
29
|
+
document.documentElement.setAttribute('data-pm-page-theme', theme);
|
|
30
|
+
root.setAttribute('data-pm-theme', theme);
|
|
31
|
+
try { if (localStorage.getItem('pmos.c3.phone')) localStorage.setItem('pmos.c3.phone', ''); } catch { /* no storage */ } // the dropped Full console choice
|
|
32
|
+
ensureC3Css();
|
|
33
|
+
c3Phone(root);
|
|
34
|
+
if (!DEMO || !ws) {
|
|
35
|
+
const list = await pmGet('/api/workspaces').then((j) => (j && j.workspaces) || []).catch(() => []);
|
|
36
|
+
if (!ws || !list.some((w) => w.id === ws)) ws = (list[0] && list[0].id) || ws;
|
|
37
|
+
}
|
|
38
|
+
withWs(ws);
|
|
39
|
+
root.replaceChildren();
|
|
40
|
+
root.classList.add('c3-phone-on');
|
|
41
|
+
mountPhone(root, { ws, demo: DEMO });
|
|
42
|
+
if (q.get('focus') || q.get('fdate')) import('./c3-focus.js').then((m) => m.openFocus({ event: { id: q.get('focus') || '' }, date: q.get('fdate') || '', phone: true })).catch(() => { /* the focus page is not available */ });
|
|
43
|
+
const mq = matchMedia('(max-width:640px)');
|
|
44
|
+
mq.addEventListener('change', () => { if (!mq.matches) location.reload(); });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
boot().catch((e) => { const r = document.getElementById('c3-root'); if (r) r.textContent = 'PMOS could not start: ' + String((e && e.message) || e); });
|
|
@@ -74,7 +74,7 @@ export function mountPhone(root, ctx = {}) {
|
|
|
74
74
|
iconButton({ icon: 'mic', ariaLabel: 'Dictate', size: 'sm', attrs: { 'data-c3ph-dictate': '' }, onClick: () => { input.focus(); toast('Use the mic on your keyboard to talk. PMOS files what you say.'); } }));
|
|
75
75
|
const recBtn = button({ label: '', variant: 'outline', attrs: { class: 'c3ph-recdot', 'data-c3ph-record': '', 'aria-label': 'Record a conversation or meeting', title: 'Record a conversation or meeting' }, onClick: () => startRec(null) });
|
|
76
76
|
recBtn.replaceChildren(el('span', { class: 'c3ph-reddot', 'aria-hidden': 'true' }));
|
|
77
|
-
const foot = el('div', { class: 'c3ph-ft', 'data-c3ph-foot': '' });
|
|
77
|
+
const foot = el('div', { class: 'c3ph-ft', 'data-c3ph-foot': '' }, form, recBtn); // shown at once; syncRec swaps in the recording bar
|
|
78
78
|
host.append(head, bannerHost, body, foot);
|
|
79
79
|
root.append(host);
|
|
80
80
|
|