create-pmos 0.204.0 → 0.206.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/c3-capture-test.js +206 -0
- package/workspace/c3-ib-more-test.js +7 -1
- package/workspace/extension/README.md +22 -7
- package/workspace/extension/capture.js +114 -0
- package/workspace/extension/manifest.json +49 -8
- package/workspace/extension/popup.css +28 -0
- package/workspace/extension/popup.html +15 -0
- package/workspace/extension/popup.js +195 -0
- package/workspace/guide/jira.md +1 -0
- package/workspace/guide/report-design.md +15 -1
- package/workspace/lib/engine/boards.js +45 -2
- package/workspace/lib/extension-cli.js +6 -4
- package/workspace/lib/jira-cli.js +26 -3
- package/workspace/lib/jira-status.js +136 -6
- package/workspace/lib/report-design.js +62 -4
- package/workspace/lib/routes-boards.js +7 -3
- package/workspace/lib/routes-report-design.js +33 -0
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/c3-boot.js +2 -1
- package/workspace/public/pm/c3-capture.css +23 -0
- package/workspace/public/pm/c3-capture.js +152 -0
- package/workspace/public/pm/c3-demo-boards.js +11 -1
- package/workspace/public/pm/c3-design-studio.js +17 -5
- package/workspace/public/pm/c3-ib-sprints.js +60 -13
- package/workspace/public/pm/c3-initiatives.css +5 -0
- package/workspace/public/pm/c3-shell.js +2 -0
- package/workspace/skills/jira-sync.skill +11 -5
- package/workspace/skills/report-design.skill +1 -0
- package/workspace/test-lists/report-design-studio-ui.json +3 -3
|
@@ -47,7 +47,8 @@ const issues = require('./issues.js');
|
|
|
47
47
|
|
|
48
48
|
const EVENTS = Object.freeze(['describe', 'sample', 'fetched', 'took', 'turn', 'version', 'accept', 'revert', 'error', 'cycle-decision']);
|
|
49
49
|
const FORMATS = Object.freeze(['deck', 'doc']);
|
|
50
|
-
const SAMPLE_KINDS = Object.freeze(['pdf', 'pptx', 'docx', 'image', 'link', 'pmos-doc']);
|
|
50
|
+
const SAMPLE_KINDS = Object.freeze(['pdf', 'pptx', 'docx', 'image', 'pictures', 'link', 'pmos-doc']);
|
|
51
|
+
const MAX_PICTURES = 12;
|
|
51
52
|
const MAX_SAMPLE_BYTES = 20 * 1024 * 1024;
|
|
52
53
|
const MAX_THUMB_BYTES = 256 * 1024;
|
|
53
54
|
const MAX_TEXT = 4000;
|
|
@@ -118,7 +119,8 @@ function fold(events) {
|
|
|
118
119
|
if (FORMATS.includes(l.format)) st.format = l.format;
|
|
119
120
|
st.log.push({ at: l.at, who: 'you', kind: 'describe', text: str(l.text) });
|
|
120
121
|
} else if (l.ev === 'sample') {
|
|
121
|
-
st.samples.set(l.id, { id: l.id, name: str(l.name), kind: l.kind, source: str(l.source), ext: str(l.ext), took: null, readable: l.readable !== false, at: l.at
|
|
122
|
+
st.samples.set(l.id, { id: l.id, name: str(l.name), kind: l.kind, source: str(l.source), ext: str(l.ext), took: null, readable: l.readable !== false, at: l.at,
|
|
123
|
+
...(l.via === 'chrome' ? { via: 'chrome' } : {}), ...(Number.isInteger(l.count) ? { count: l.count } : {}), ...(Number.isInteger(l.pages) ? { pages: l.pages } : {}) });
|
|
122
124
|
st.log.push({ at: l.at, who: 'you', kind: 'sample', text: str(l.name), sample: l.id });
|
|
123
125
|
} else if (l.ev === 'fetched') {
|
|
124
126
|
const s = st.samples.get(l.id);
|
|
@@ -220,6 +222,32 @@ function addSample(root, cadence, input, { actor, now } = {}) {
|
|
|
220
222
|
writeAtomic(path.join(p.samples, `${id}.md`), text);
|
|
221
223
|
const fm = /^---\r?\n[\s\S]*?\btitle:\s*["']?([^\n"']+)/.exec(text);
|
|
222
224
|
rec = { id, name: oneLine(input.name || (fm ? fm[1] : rel.split('/').slice(-2).join(' ')), 120), kind: 'pmos-doc', source: rel, ext: 'md', readable: true, thumb: null, text_file: null };
|
|
225
|
+
} else if (str(input.kind) === 'pictures' || Array.isArray(input.pictures)) {
|
|
226
|
+
// pictures of a whole page, scrolled top to bottom, from the Chrome extension (the page frames PMOS; see extension/popup.js)
|
|
227
|
+
const list = Array.isArray(input.pictures) ? input.pictures : null;
|
|
228
|
+
if (str(input.kind) !== 'pictures' || !list || !list.length) return refuse(400, 'pictures needs kind "pictures" and a list of pictures (base64 JPEG or PNG)');
|
|
229
|
+
if (list.length > MAX_PICTURES) return refuse(400, `at most ${MAX_PICTURES} pictures in one sample`);
|
|
230
|
+
const bufs = [];
|
|
231
|
+
let total = 0;
|
|
232
|
+
for (const [i, raw] of list.entries()) {
|
|
233
|
+
if (typeof raw !== 'string' || !raw) return refuse(400, `picture ${i + 1} is empty`);
|
|
234
|
+
const b64 = raw.replace(/^data:[^,]*,/, '');
|
|
235
|
+
total += Math.floor(b64.length * 3 / 4);
|
|
236
|
+
if (total > MAX_SAMPLE_BYTES + 4 * list.length) return refuse(413, 'a sample is at most 20 MB');
|
|
237
|
+
const buf = Buffer.from(b64, 'base64');
|
|
238
|
+
const s = buf.length ? sniff(buf) : null;
|
|
239
|
+
if (!s || s.kind !== 'image') return refuse(415, `picture ${i + 1} must be a PNG or a JPG`);
|
|
240
|
+
bufs.push({ buf, ext: s.ext });
|
|
241
|
+
}
|
|
242
|
+
if (bufs.reduce((n, b) => n + b.buf.length, 0) > MAX_SAMPLE_BYTES) return refuse(413, 'a sample is at most 20 MB');
|
|
243
|
+
fs.mkdirSync(p.samples, { recursive: true });
|
|
244
|
+
const files = bufs.map((b, i) => { const f = `${id}-${i + 1}.${b.ext}`; writeAtomic(path.join(p.samples, f), b.buf); return f; });
|
|
245
|
+
const first = bufs[0];
|
|
246
|
+
const thumb = first.buf.length <= MAX_THUMB_BYTES ? `data:image/${first.ext === 'jpg' ? 'jpeg' : 'png'};base64,${first.buf.toString('base64')}` : null;
|
|
247
|
+
const src = typeof input.source_url === 'string' ? input.source_url.trim() : '';
|
|
248
|
+
const u = parseUrl(src);
|
|
249
|
+
const source = u && /^https?:$/.test(u.protocol) && src.length <= 2000 ? src : 'upload';
|
|
250
|
+
rec = { id, name: oneLine(input.name || 'Page pictures', 120), kind: 'pictures', source, ext: first.ext, count: files.length, files, via: 'chrome', bytes: bufs.reduce((n, b) => n + b.buf.length, 0), readable: true, thumb, text_file: null };
|
|
223
251
|
} else {
|
|
224
252
|
const kind = str(input.kind);
|
|
225
253
|
if (!['pdf', 'pptx', 'docx', 'image'].includes(kind)) return refuse(400, 'kind must be pdf, pptx, docx or image (or send a link, or a report doc)');
|
|
@@ -244,19 +272,42 @@ function addSample(root, cadence, input, { actor, now } = {}) {
|
|
|
244
272
|
try { thumb = ooxmlThumb(buf); } catch { thumb = null; }
|
|
245
273
|
}
|
|
246
274
|
rec = { id, name: oneLine(input.name || `${kind} sample`, 120), kind, source: 'upload', ext, bytes: buf.length, readable, thumb, text_file: textFile };
|
|
275
|
+
if (kind === 'pdf' && input.from === 'chrome') {
|
|
276
|
+
// a PDF the Chrome extension fetched: where it came from and how many pages it has
|
|
277
|
+
const src = typeof input.source_url === 'string' ? input.source_url.trim() : '';
|
|
278
|
+
const u = parseUrl(src);
|
|
279
|
+
if (u && /^https?:$/.test(u.protocol) && src.length <= 2000) rec.source = src;
|
|
280
|
+
rec.via = 'chrome';
|
|
281
|
+
const n = Number(input.pages);
|
|
282
|
+
if (Number.isInteger(n) && n > 0 && n < 5000) rec.pages = n;
|
|
283
|
+
}
|
|
247
284
|
}
|
|
248
285
|
writeAtomic(path.join(p.samples, `${id}.json`), JSON.stringify({ ...rec, took: null, added_at: iso(now) }, null, 2) + '\n');
|
|
249
|
-
append(root, cadence, { ev: 'sample', id, name: rec.name, kind: rec.kind, source: rec.source, ext: rec.ext, readable: rec.readable
|
|
286
|
+
append(root, cadence, { ev: 'sample', id, name: rec.name, kind: rec.kind, source: rec.source, ext: rec.ext, readable: rec.readable,
|
|
287
|
+
...(rec.via ? { via: rec.via } : {}), ...(rec.count ? { count: rec.count } : {}), ...(rec.pages ? { pages: rec.pages } : {}) }, { actor, now });
|
|
250
288
|
return { ok: true, sample: sampleOut(rec) };
|
|
251
289
|
}
|
|
252
290
|
function sampleOut(s, { fetching } = {}) {
|
|
253
291
|
const out = { id: s.id, name: s.name, kind: s.kind, source: s.source, took: s.took || null, thumb: typeof s.thumb === 'string' && s.thumb.startsWith('data:image/') ? s.thumb : null, readable: s.readable !== false };
|
|
292
|
+
if (s.via === 'chrome') {
|
|
293
|
+
out.via = 'chrome';
|
|
294
|
+
out.line = chromeLine(s);
|
|
295
|
+
if (s.count) out.count = s.count;
|
|
296
|
+
if (s.pages) out.pages = s.pages;
|
|
297
|
+
}
|
|
254
298
|
if (s.kind === 'link') {
|
|
255
299
|
if (fetching) { out.fetching = true; out.hint = 'PMOS is reading this link with your connectors.'; }
|
|
256
300
|
else out.hint = linkHint(s.source, { fetched: !!(s.text_file && s.fetched), reason: s.fetch_reason });
|
|
257
301
|
}
|
|
258
302
|
return out;
|
|
259
303
|
}
|
|
304
|
+
/** The kind line of a sample the Chrome extension added: 'From Chrome, PDF, 12 slides' | 'From Chrome, PDF, 3 pages' | 'From Chrome, 3 page pictures'. */
|
|
305
|
+
function chromeLine(s) {
|
|
306
|
+
if (s.kind === 'pictures') { const n = s.count || 0; return n ? `From Chrome, ${n} page ${n === 1 ? 'picture' : 'pictures'}` : 'From Chrome, page pictures'; }
|
|
307
|
+
const g = googleOf(parseUrl(s.source));
|
|
308
|
+
const n = s.pages || 0;
|
|
309
|
+
return n ? `From Chrome, PDF, ${n} ${g && g[2] === 'Slides' ? (n === 1 ? 'slide' : 'slides') : (n === 1 ? 'page' : 'pages')}` : 'From Chrome, PDF';
|
|
310
|
+
}
|
|
260
311
|
/* Google files: name them for what they are, and say which download PMOS can read */
|
|
261
312
|
// a PDF, because the job sees its pages; from a .pptx or .docx it gets the words only, not the look
|
|
262
313
|
const GOOGLE = [[/^\/presentation\//, 'Google Slides deck', 'Slides'], [/^\/document\//, 'Google Docs document', 'Docs'], [/^\/spreadsheets\//, 'Google Sheets sheet', 'Sheets']];
|
|
@@ -309,6 +360,13 @@ function recordTook(root, cadence, id, took, readable, { now } = {}) {
|
|
|
309
360
|
/** The file the job reads for a sample: the pdf or image itself, the extracted words of a .pptx/.docx, the copy of a report doc, the text copy of a fetched link; null for any other link. */
|
|
310
361
|
function sampleReadPath(root, cadence, s) {
|
|
311
362
|
const p = paths(root, cadence);
|
|
363
|
+
if (s.kind === 'pictures') {
|
|
364
|
+
// every picture of the page, in order (samples/<id>-1.jpg, <id>-2.jpg, …); null when none is there
|
|
365
|
+
let names = [];
|
|
366
|
+
try { names = fs.readdirSync(p.samples).filter((f) => f.startsWith(`${s.id}-`) && /^\d+\.(jpg|png)$/.test(f.slice(s.id.length + 1))); } catch { names = []; }
|
|
367
|
+
names.sort((a, b) => parseInt(a.slice(s.id.length + 1), 10) - parseInt(b.slice(s.id.length + 1), 10));
|
|
368
|
+
return names.length ? names.map((f) => path.join(p.samples, f)) : null;
|
|
369
|
+
}
|
|
312
370
|
if (s.kind === 'link') { const f = path.join(p.samples, `${s.id}.txt`); return s.fetched && fs.existsSync(f) ? f : null; }
|
|
313
371
|
if (s.kind === 'pptx' || s.kind === 'docx') { const f = path.join(p.samples, `${s.id}.txt`); return fs.existsSync(f) ? f : null; }
|
|
314
372
|
const f = path.join(p.samples, `${s.id}.${s.ext}`);
|
|
@@ -352,7 +410,7 @@ function writeBrief(root, cadence, setup, { now } = {}) {
|
|
|
352
410
|
'## Description', '', st.description ? st.description : 'No description was given.', '', '## Samples', ''];
|
|
353
411
|
const samples = [...st.samples.values()];
|
|
354
412
|
if (!samples.length) lines.push('No samples.');
|
|
355
|
-
for (const s of samples) lines.push(`- ${s.name} (${s.kind}${s.kind === 'link' || s.kind === 'pmos-doc' ? ', ' + s.source : ''}): ${s.took ? 'PMOS took ' + s.took : (s.readable ? 'nothing taken yet' : 'could not be read')}`);
|
|
413
|
+
for (const s of samples) lines.push(`- ${s.name} (${s.kind}${s.kind === 'link' || s.kind === 'pmos-doc' ? ', ' + s.source : ''}${s.via === 'chrome' ? ', from Chrome' : ''}): ${s.took ? 'PMOS took ' + s.took : (s.readable ? 'nothing taken yet' : 'could not be read')}`);
|
|
356
414
|
lines.push('', '## Reference outline', '');
|
|
357
415
|
for (const o of (Array.isArray(meta.outline) ? meta.outline : [])) lines.push(`- ${o.id} (${o.kind || 'slide'}): ${o.title || ''}`);
|
|
358
416
|
const sec = (h, list) => { lines.push('', `## ${h}`, ''); if (!list || !list.length) lines.push('None written yet.'); else for (const x of list) lines.push(`- ${x}`); };
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* GET /api/boards/columns?ws&board&q&owner&cap → { ok, columns: [{ stage, cat, hint, count, rows }] }
|
|
10
10
|
* GET /api/boards/initiative?ws&board&id → { ok, row, board, history, tools }
|
|
11
11
|
* GET /api/boards/asks?ws&board&state → { ok, asks } state: open (default) | accepted | declined | all
|
|
12
|
-
* GET /api/boards/sprints?ws&board → { ok, source: jira|none, readAt, sprints }
|
|
12
|
+
* GET /api/boards/sprints?ws&board → { ok, source: jira|none, readAt, sprints } (each with goal, summary, themes, frozen)
|
|
13
13
|
* GET /api/boards/items?ws&board → { ok, build, jira, items, initiatives } PMOS tasks + Jira items (Build and deliver only)
|
|
14
14
|
* GET /api/boards/anchors?ws&board → { ok, anchors: [{ value, kind, group, label }], error } the declared KRs,
|
|
15
15
|
* health budgets and domains a new initiative can anchor to (D59)
|
|
@@ -548,7 +548,9 @@ const READS = {
|
|
|
548
548
|
} catch { /* no snapshot yet */ }
|
|
549
549
|
let whole = null;
|
|
550
550
|
try { whole = jiraStatus.readSprints(r.w.root); } catch { whole = null; }
|
|
551
|
-
|
|
551
|
+
let notes = null;
|
|
552
|
+
try { notes = jiraStatus.readSprintNotes(r.w.root); } catch { notes = null; }
|
|
553
|
+
ctx.json(res, { ok: true, ws: r.w.id, board: r.board.id, ...B.sprintsOf(snaps, (id) => ids.has(id), whole, notes) });
|
|
552
554
|
},
|
|
553
555
|
};
|
|
554
556
|
|
|
@@ -1000,7 +1002,9 @@ function meetingBoards(ctx) {
|
|
|
1000
1002
|
} catch { /* no snapshot yet */ }
|
|
1001
1003
|
let whole = null;
|
|
1002
1004
|
try { whole = jiraStatus.readSprints(w.root); } catch { whole = null; }
|
|
1003
|
-
|
|
1005
|
+
let notes = null;
|
|
1006
|
+
try { notes = jiraStatus.readSprintNotes(w.root); } catch { notes = null; }
|
|
1007
|
+
sprints = B.sprintsOf(snaps, (id) => ids.has(id), whole, notes);
|
|
1004
1008
|
}
|
|
1005
1009
|
return { ...head, health: B.health(rows), counts: { total: c.total, open: c.open, risk: c.risk, late: c.late },
|
|
1006
1010
|
rows: rows.filter((r) => r.status === 'late' || r.status === 'at risk').map((r) => ({ id: r.id, title: r.title, status: r.status, end: r.end || null, owner: r.owner || null })),
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* GET /api/report-design/version?ws&id&n → text/html, the sanitized render of D<n>
|
|
6
6
|
* POST /api/report-design/describe { ws, id, text, format? }
|
|
7
7
|
* POST /api/report-design/sample { ws, id, name, kind, data } | { ws, id, link, name? } | { ws, id, doc, name? } → { ok, sample }
|
|
8
|
+
* from Chrome: { kind: 'pdf', data, from: 'chrome', source_url?, pages? } | { kind: 'pictures', pictures: [base64 JPEG or PNG, …], source_url?, name, from: 'chrome' } (the field is `from`: pm-guard reserves `via` for the writer; the record keeps via: 'chrome')
|
|
9
|
+
* GET /api/report-design/last?ws → { ok, id, name, at } the report whose design studio was opened last (the Chrome popup's default report); id null when none
|
|
8
10
|
* POST /api/report-design/turn { ws, id, text, points: [{ n, slide, x, y, text }] } → { ok, running } ONE job per report; 409 while one runs
|
|
9
11
|
* POST /api/report-design/accept { ws, id, n } → { ok, design, brief, redraw } "This is it": writes brief.md and redraws the current
|
|
10
12
|
* cycle in the saved design (redraw: { path, slot, started, reason? }; a sent cycle keeps its look)
|
|
@@ -101,6 +103,11 @@ function promptFor(skill, { mode, root, cadence, setup, st, turnText, points, cy
|
|
|
101
103
|
lines.push(samples.length ? '- Samples:' : '- Samples: none');
|
|
102
104
|
for (const s of samples) {
|
|
103
105
|
const f = D.sampleReadPath(root, cadence, s);
|
|
106
|
+
if (s.kind === 'pictures') {
|
|
107
|
+
const fs_ = Array.isArray(f) ? f : [];
|
|
108
|
+
lines.push(` - ${s.id}${s.took ? '' : ' (NEW)'} "${s.name}", pictures of a whole web page from Chrome, top to bottom, ${fs_.length ? `${fs_.length} in all: read every one, in this order, and follow the look closely: ${fs_.join(', ')}` : 'its files could not be read'}`);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
104
111
|
const where = s.kind === 'link' ? (f ? `a link: ${s.source}. PMOS read its words and structure through the PM's connectors: read ${f}. It has the words and the order, not the look` : `a link: ${s.source} (you have no web tool)`) : f ? `read ${f}` : 'its file could not be read';
|
|
105
112
|
lines.push(` - ${s.id}${s.took ? '' : ' (NEW)'} "${s.name}", ${s.kind}: ${where}`);
|
|
106
113
|
}
|
|
@@ -352,6 +359,26 @@ function sweep(ctx) {
|
|
|
352
359
|
}
|
|
353
360
|
|
|
354
361
|
// ── the Edit log line ───────────────────────────────────────────────────────────────────────────────────────
|
|
362
|
+
/* The report whose design studio was opened last, for each workspace: <runtime>/report-design-last.json { [ws]: { id, at } }. A read of a design is an open of its studio. */
|
|
363
|
+
function lastFile(ctx) { return ctx.runtimeDir ? path.join(ctx.runtimeDir, 'report-design-last.json') : null; }
|
|
364
|
+
function lastEntry(ctx, w) {
|
|
365
|
+
const f = lastFile(ctx);
|
|
366
|
+
if (!f) return null;
|
|
367
|
+
try { const o = JSON.parse(fs.readFileSync(f, 'utf8')); const e = o && o[w.id]; return e && D.CADENCE_RE.test(String(e.id)) ? { id: e.id, at: typeof e.at === 'string' ? e.at : null } : null; } catch { return null; }
|
|
368
|
+
}
|
|
369
|
+
const lastStudio = (ctx, w) => { const e = lastEntry(ctx, w); return e ? e.id : null; };
|
|
370
|
+
function rememberStudio(ctx, w, id) {
|
|
371
|
+
const f = lastFile(ctx);
|
|
372
|
+
if (!f || lastStudio(ctx, w) === id) return false;
|
|
373
|
+
try {
|
|
374
|
+
let o = {};
|
|
375
|
+
try { o = JSON.parse(fs.readFileSync(f, 'utf8')) || {}; } catch { o = {}; }
|
|
376
|
+
o[w.id] = { id, at: new Date().toISOString() };
|
|
377
|
+
fs.mkdirSync(path.dirname(f), { recursive: true });
|
|
378
|
+
fs.writeFileSync(f, JSON.stringify(o) + '\n');
|
|
379
|
+
return true;
|
|
380
|
+
} catch { return false; }
|
|
381
|
+
}
|
|
355
382
|
function editLog(ctx, row) {
|
|
356
383
|
if (!ctx.runtimeDir) return false;
|
|
357
384
|
try {
|
|
@@ -430,8 +457,14 @@ async function handle(req, res, url, ctx) {
|
|
|
430
457
|
return sendHtml(res, D.sanitize(html));
|
|
431
458
|
}
|
|
432
459
|
try { settleStale(w.root, id); } catch { /* a read never fails on this */ }
|
|
460
|
+
rememberStudio(ctx, w, id);
|
|
433
461
|
return send({ status: 200, body: { ok: true, setup, design: D.designAnswer(w.root, id, designOpts(w.root, id)) } });
|
|
434
462
|
}
|
|
463
|
+
if (sub === 'last') {
|
|
464
|
+
const e = lastEntry(ctx, w);
|
|
465
|
+
const setup = e ? setupOf(w.root, e.id) : null;
|
|
466
|
+
return send({ status: 200, body: { ok: true, id: setup ? e.id : null, name: setup ? setup.name : null, at: setup ? e.at : null } });
|
|
467
|
+
}
|
|
435
468
|
if (sub === 'cycle' || sub === 'cycle-html') {
|
|
436
469
|
const d = reportDoc(w, text(url.searchParams.get('path'))); if (d.error) return send(d.error);
|
|
437
470
|
if (sub === 'cycle') return send({ status: 200, body: cycleAnswer(w, d.c) });
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.206.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.206.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/workspace/package.json
CHANGED
|
@@ -6,5 +6,6 @@
|
|
|
6
6
|
const narrow = typeof matchMedia === 'function' && matchMedia('(max-width:640px)').matches;
|
|
7
7
|
let desk = false;
|
|
8
8
|
try { desk = localStorage.getItem('pmos.c3.testDesk') === 'desk'; } catch { /* no storage */ }
|
|
9
|
-
if (
|
|
9
|
+
if (new URLSearchParams(location.search).get('capture') === '1') import('./c3-capture.js'); // the PMOS Chrome extension's popup page: a small page on its own, at any width
|
|
10
|
+
else if (narrow && !desk) import('./c3-phone-boot.js');
|
|
10
11
|
else import('./c3-shell.js');
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/* pm/c3-capture.css — the page the PMOS Chrome extension frames in its popup (c3-capture.js). Mockup 42-reportdeck.css .mk-cx-pop and friends. */
|
|
2
|
+
.c3.c3-cap { min-height: 0; padding: 8px; background: var(--background); }
|
|
3
|
+
.c3 .cap-pop { display: grid; gap: 10px; padding: 12px; box-sizing: border-box; }
|
|
4
|
+
.c3 .cap-head { display: flex; gap: 8px; align-items: center; font-size: var(--fs-3); font-weight: 600; }
|
|
5
|
+
.c3 .cap-head b { font-size: var(--fs-1); color: var(--acc); }
|
|
6
|
+
.c3 .cap-head span { flex: 1; min-width: 0; }
|
|
7
|
+
.c3 .cap-what { display: grid; grid-template-columns: 56px minmax(0, 1fr); gap: 10px; align-items: start; padding: 8px; border: 1px solid var(--border); border-radius: 8px; background: var(--background); }
|
|
8
|
+
.c3 .cap-what b { display: flex; gap: 6px; align-items: center; font-size: var(--fs-2); overflow-wrap: anywhere; }
|
|
9
|
+
.c3 .cap-what .mono { font-size: var(--fs-1); overflow-wrap: anywhere; }
|
|
10
|
+
.c3 .cap-thumb { display: grid; gap: 3px; align-content: start; width: 56px; aspect-ratio: 16 / 9; padding: 4px; box-sizing: border-box; border: 1px solid var(--slide-border); border-radius: 6px; background: var(--slide-bg); }
|
|
11
|
+
.c3 .cap-thumb i { display: block; height: 3px; background: var(--slide-track); }
|
|
12
|
+
.c3 .cap-thumb.deck i:first-child { height: 6px; background: var(--slide-brand); }
|
|
13
|
+
.c3 .cap-thumb.page { aspect-ratio: 3 / 4; width: 40px; justify-self: center; }
|
|
14
|
+
.c3 .cap-f { display: grid; gap: 4px; }
|
|
15
|
+
.c3 .cap-f select { width: 100%; box-sizing: border-box; padding: 6px 8px; border: 1px solid var(--border); border-radius: 6px; background: var(--background); color: var(--foreground); font: inherit; font-size: var(--fs-3); }
|
|
16
|
+
.c3 .cap-lbl { font-size: var(--fs-1); }
|
|
17
|
+
.c3 .cap-line { font-size: var(--fs-2); line-height: 1.5; }
|
|
18
|
+
.c3 .cap-step { font-size: var(--fs-1); }
|
|
19
|
+
.c3 .cap-acts { display: flex; gap: 8px; flex-wrap: wrap; }
|
|
20
|
+
.c3 .cap-msg { display: flex; gap: 8px; align-items: flex-start; margin: 0; font-size: var(--fs-3); line-height: 1.5; }
|
|
21
|
+
.c3 .cap-msg .ic { flex: 0 0 auto; margin-top: 4px; }
|
|
22
|
+
.c3 .cap-prog { height: 6px; border-radius: 6px; background: var(--muted); overflow: hidden; }
|
|
23
|
+
.c3 .cap-prog i { display: block; width: var(--rp-w, 6%); height: 100%; background: var(--acc); }
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/* pm/c3-capture.js — the page the PMOS Chrome extension frames in its popup (initiative report-design-studio; mockup 42-reportdeck.js, hashes
|
|
2
|
+
* #design-chrome and #design-chrome-page). c3-boot.js loads it alone for /c3.html?capture=1&pop=1&pkind=deck|doc|page&ptitle=…&purl=…[&pscreens=N].
|
|
3
|
+
*
|
|
4
|
+
* It draws "Add this page to a report design": the page, the report to add it to (the report whose studio was opened last is chosen), how it is
|
|
5
|
+
* saved, and Add as a sample. The extension never posts to PMOS itself: every PMOS write needs a press on a PMOS page. So this page asks its
|
|
6
|
+
* parent, the extension popup, with postMessage { pmosCapture: 'request' }; the popup takes the page and answers { pmosCapture: 'data', kind, … }, and
|
|
7
|
+
* THIS page uploads it with its own press (POST /api/report-design/sample, from 'chrome').
|
|
8
|
+
*
|
|
9
|
+
* Messages in: only from window.parent, only from a chrome-extension:// origin or this page's own origin, only these words, and 'data' only while a
|
|
10
|
+
* request is waiting. Messages out: to the parent's origin, never '*'. Nothing here sends anything outside the workspace. */
|
|
11
|
+
import { el } from './dom.js';
|
|
12
|
+
import { icon, pbtn, plural } from './c3-ini-ui.js';
|
|
13
|
+
import { pmGet, pmPost } from './http.js';
|
|
14
|
+
import { ensureC3Css } from './pm-work-view.js';
|
|
15
|
+
|
|
16
|
+
const q = new URLSearchParams(location.search);
|
|
17
|
+
const KIND = ['deck', 'doc', 'page'].includes(q.get('pkind')) ? q.get('pkind') : 'page';
|
|
18
|
+
const TITLE = (q.get('ptitle') || 'This page').slice(0, 120);
|
|
19
|
+
const URL_OF = (q.get('purl') || '').slice(0, 2000);
|
|
20
|
+
const SCREENS = Math.max(0, Math.min(500, parseInt(q.get('pscreens') || '0', 10) || 0));
|
|
21
|
+
const MAX_MB = 20;
|
|
22
|
+
const host = (() => { try { return new URL(URL_OF).hostname; } catch { return ''; } })();
|
|
23
|
+
|
|
24
|
+
let parentOrigin = '';
|
|
25
|
+
try { parentOrigin = (location.ancestorOrigins && location.ancestorOrigins[0]) || ''; } catch { parentOrigin = ''; }
|
|
26
|
+
if (!parentOrigin) { try { parentOrigin = new URL(document.referrer).origin; } catch { parentOrigin = ''; } }
|
|
27
|
+
const trusted = (o) => !!o && (o.startsWith('chrome-extension://') || o === location.origin) && (!parentOrigin || o === parentOrigin);
|
|
28
|
+
const say = (m) => { if (window.parent !== window && parentOrigin) { try { window.parent.postMessage(m, parentOrigin); } catch { /* the popup is gone */ } } };
|
|
29
|
+
|
|
30
|
+
const S = { st: 'load', reports: [], rep: null, last: null, err: '', step: 0, of: 1, text: '', got: '', waiting: false, root: null };
|
|
31
|
+
|
|
32
|
+
const WHAT = {
|
|
33
|
+
deck: { what: 'Google Slides', how: 'PMOS saves the deck as a PDF so the design can follow its look. Only your own PMOS gets it.', saving: 'Saving the deck as a PDF' },
|
|
34
|
+
doc: { what: 'Google Docs', how: 'PMOS saves the document as a PDF so the design can follow its look. Only your own PMOS gets it.', saving: 'Saving the document as a PDF' },
|
|
35
|
+
page: { what: '', how: 'PMOS saves the page as pictures, scrolled top to bottom, so the design can follow its look. Only your own PMOS gets it.', saving: 'Saving the page as pictures' },
|
|
36
|
+
};
|
|
37
|
+
const whatLine = () => (KIND === 'page' ? [host || 'Web page', SCREENS ? 'about ' + plural(SCREENS, 'screen') + ' long' : ''].filter(Boolean).join(', ') : WHAT[KIND].what);
|
|
38
|
+
|
|
39
|
+
function whatCard() {
|
|
40
|
+
return el('div', { class: 'cap-what', 'data-cap': 'page' },
|
|
41
|
+
el('span', { class: 'cap-thumb ' + (KIND === 'page' ? 'page' : 'deck'), 'aria-hidden': 'true' }, el('i'), el('i'), el('i')),
|
|
42
|
+
el('div', null, el('b', null, icon(KIND === 'page' ? 'notes' : 'img', 'sm'), TITLE), el('span', { class: 'dim mono' }, whatLine())));
|
|
43
|
+
}
|
|
44
|
+
function head() {
|
|
45
|
+
return el('div', { class: 'cap-head' }, el('b', { class: 'mono' }, 'PMOS'), el('span', null, 'Add this page to a report design'));
|
|
46
|
+
}
|
|
47
|
+
const repOf = () => S.reports.find((r) => r.key === S.rep) || null;
|
|
48
|
+
|
|
49
|
+
function bar(pct) { const i = el('i'); i.style.setProperty('--rp-w', pct + '%'); return i; }
|
|
50
|
+
|
|
51
|
+
function draw() {
|
|
52
|
+
const r = repOf();
|
|
53
|
+
let body;
|
|
54
|
+
if (S.st === 'load') body = [el('p', { class: 'dim mono cap-msg', role: 'status' }, 'Reading your reports.')];
|
|
55
|
+
else if (S.st === 'none') body = [whatCard(), el('p', { class: 'cap-msg', 'data-cap': 'none' }, icon('alert', 'sm'), el('span', null, 'There is no report set up in PMOS yet. Set one up in Reports, then add this page to its design.'))];
|
|
56
|
+
else if (S.st === 'error') body = [el('p', { class: 'cap-msg', role: 'alert', 'data-cap': 'error' }, icon('alert', 'sm'), el('span', null, S.err)), el('div', { class: 'cap-acts' }, pbtn('outline', 'Try again', 'undo', { 'data-cap': 'retry', onclick: () => { S.st = 'ready'; draw(); } }))];
|
|
57
|
+
else if (S.st === 'sending') {
|
|
58
|
+
const pct = Math.max(6, Math.min(100, Math.round((S.step / Math.max(1, S.of)) * 100)));
|
|
59
|
+
body = [whatCard(), el('p', { class: 'cap-msg', 'data-cap': 'saving' }, el('b', null, WHAT[KIND].saving)),
|
|
60
|
+
el('div', { class: 'cap-prog', role: 'progressbar', 'aria-label': WHAT[KIND].saving, 'aria-valuenow': String(pct), 'aria-valuemin': '0', 'aria-valuemax': '100' }, bar(pct)),
|
|
61
|
+
el('div', { class: 'dim mono cap-step' }, (S.text || 'Starting') + '. Keep this popup open.')];
|
|
62
|
+
} else if (S.st === 'done') {
|
|
63
|
+
body = [el('p', { class: 'cap-msg ok', 'data-cap': 'done' }, icon('check', 'sm'), el('b', null, 'Added to ' + (r ? r.name : 'the report'))),
|
|
64
|
+
el('div', { class: 'dim cap-line' }, TITLE + ', ' + S.got + '. PMOS reads its look on your next turn in the studio.'),
|
|
65
|
+
el('div', { class: 'cap-acts' }, pbtn('primary', 'Open the studio', 'spark', { 'data-cap': 'studio', onclick: openStudio }))];
|
|
66
|
+
} else {
|
|
67
|
+
body = [whatCard(),
|
|
68
|
+
el('label', { class: 'cap-f' }, el('span', { class: 'dim cap-lbl' }, 'Report'),
|
|
69
|
+
el('select', { 'data-cap-rep': '', 'aria-label': 'Report', onchange: (e) => { S.rep = e.target.value; draw(); } },
|
|
70
|
+
...S.reports.map((x) => el('option', { value: x.key, selected: x.key === S.rep }, x.label)))),
|
|
71
|
+
S.rep === S.last ? el('div', { class: 'dim cap-lbl' }, 'The report whose studio you opened last.') : null,
|
|
72
|
+
el('div', { class: 'dim cap-line' }, WHAT[KIND].how),
|
|
73
|
+
el('div', { class: 'cap-acts' }, pbtn('primary', 'Add as a sample', 'plus', { 'data-cap': 'send', onclick: send }))];
|
|
74
|
+
}
|
|
75
|
+
S.root.replaceChildren(el('div', { class: 'card cap-pop', role: 'dialog', 'aria-label': 'PMOS extension' }, head(), ...body.filter(Boolean)));
|
|
76
|
+
requestAnimationFrame(() => say({ pmosCapture: 'resize', height: Math.max(120, Math.min(600, Math.ceil(S.root.getBoundingClientRect().height) + 8)) }));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function send() {
|
|
80
|
+
if (!repOf()) return;
|
|
81
|
+
S.st = 'sending'; S.step = 0; S.of = 1; S.text = 'Asking the page'; S.waiting = true;
|
|
82
|
+
draw();
|
|
83
|
+
if (window.parent === window) { S.waiting = false; fail('Open this page from the PMOS button in Chrome.'); return; }
|
|
84
|
+
say({ pmosCapture: 'request' });
|
|
85
|
+
}
|
|
86
|
+
function fail(text) { S.waiting = false; S.st = 'error'; S.err = text; draw(); }
|
|
87
|
+
|
|
88
|
+
async function upload(m) {
|
|
89
|
+
const r = repOf();
|
|
90
|
+
if (!r) return fail('Choose a report, then try again.');
|
|
91
|
+
const body = m.kind === 'pdf'
|
|
92
|
+
? { ws: r.ws, id: r.id, name: m.name, kind: 'pdf', data: m.data, from: 'chrome', source_url: m.url, ...(m.pages ? { pages: m.pages } : {}) }
|
|
93
|
+
: { ws: r.ws, id: r.id, name: m.name, kind: 'pictures', pictures: m.pictures, from: 'chrome', source_url: m.url };
|
|
94
|
+
S.text = 'Sending it to PMOS'; S.step = S.of; draw();
|
|
95
|
+
const res = await Promise.resolve(pmPost('/api/report-design/sample', body)).catch((e) => ({ ok: false, error: String((e && e.message) || e) }));
|
|
96
|
+
if (!res || !res.ok) {
|
|
97
|
+
const e = String((res && res.error) || '');
|
|
98
|
+
return fail(res && res.status === 413 || /20 MB/.test(e) ? 'The sample is over ' + MAX_MB + ' MB. PMOS keeps a sample up to ' + MAX_MB + ' MB. Open a shorter page, or add a smaller copy.'
|
|
99
|
+
: e ? 'PMOS did not take the sample: ' + e + '.' : 'PMOS is not answering. Start PMOS, then try again.');
|
|
100
|
+
}
|
|
101
|
+
S.got = m.kind === 'pdf' ? (m.pages ? plural(m.pages, KIND === 'deck' ? 'slide' : 'page') + ' as a PDF' : 'a PDF') : plural(m.pictures.length, 'page picture');
|
|
102
|
+
S.st = 'done';
|
|
103
|
+
draw();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function openStudio() {
|
|
107
|
+
const r = repOf(); if (!r) return;
|
|
108
|
+
if (window.parent !== window) { say({ pmosCapture: 'open', report: r.id }); return; }
|
|
109
|
+
location.href = '/c3.html?studio=' + encodeURIComponent(r.id) + (r.ws ? '&ws=' + encodeURIComponent(r.ws) : '');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
window.addEventListener('message', (e) => {
|
|
113
|
+
if (e.source !== window.parent || window.parent === window || !trusted(e.origin)) return;
|
|
114
|
+
const m = e.data;
|
|
115
|
+
if (!m || typeof m !== 'object' || typeof m.pmosCapture !== 'string' || !S.waiting) return;
|
|
116
|
+
if (m.pmosCapture === 'progress' && typeof m.text === 'string' && Number.isInteger(m.step) && Number.isInteger(m.of) && m.of > 0) { S.text = m.text.slice(0, 80); S.step = m.step; S.of = m.of; draw(); return; }
|
|
117
|
+
if (m.pmosCapture === 'error' && typeof m.error === 'string') { fail(m.error.slice(0, 400)); return; }
|
|
118
|
+
if (m.pmosCapture !== 'data' || S.st !== 'sending') return;
|
|
119
|
+
if (m.kind === 'pdf' && typeof m.data === 'string' && m.data) { S.waiting = false; upload({ kind: 'pdf', name: String(m.name || TITLE).slice(0, 120), url: String(m.url || URL_OF), pages: Number.isInteger(m.pages) ? m.pages : 0, data: m.data }); return; }
|
|
120
|
+
if (m.kind === 'pictures' && Array.isArray(m.pictures) && m.pictures.length && m.pictures.length <= 12 && m.pictures.every((p) => typeof p === 'string' && p)) { S.waiting = false; upload({ kind: 'pictures', name: String(m.name || TITLE).slice(0, 120), url: String(m.url || URL_OF), pictures: m.pictures }); return; }
|
|
121
|
+
fail('The page came back in a form PMOS could not use.');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
async function load() {
|
|
125
|
+
const list = await pmGet('/api/workspaces').then((j) => (j && j.workspaces) || []).catch(() => []);
|
|
126
|
+
const want = q.get('ws');
|
|
127
|
+
const wss = want ? list.filter((w) => w.id === want) : list;
|
|
128
|
+
const per = await Promise.all(wss.map(async (w) => {
|
|
129
|
+
const [a, b] = await Promise.all([pmGet('/api/report-setups?ws=' + encodeURIComponent(w.id)).catch(() => null), pmGet('/api/report-design/last?ws=' + encodeURIComponent(w.id)).catch(() => null)]);
|
|
130
|
+
return { w, setups: ((a && a.setups) || []).filter((s) => s && s.id && s.status !== 'removed'), last: b && b.ok && b.id ? { id: b.id, at: b.at || '' } : null };
|
|
131
|
+
}));
|
|
132
|
+
const many = per.filter((x) => x.setups.length).length > 1;
|
|
133
|
+
S.reports = per.flatMap((x) => x.setups.map((s) => ({ key: x.w.id + '|' + s.id, ws: x.w.id, id: s.id, name: s.name || s.id, label: many ? (s.name || s.id) + ' (' + (x.w.name || x.w.id) + ')' : (s.name || s.id) })));
|
|
134
|
+
const lasts = per.filter((x) => x.last && x.setups.some((s) => s.id === x.last.id)).sort((a, b) => String(b.last.at).localeCompare(String(a.last.at)));
|
|
135
|
+
S.last = lasts.length ? lasts[0].w.id + '|' + lasts[0].last.id : null;
|
|
136
|
+
S.rep = S.last || (S.reports[0] && S.reports[0].key) || null;
|
|
137
|
+
S.st = S.reports.length ? 'ready' : 'none';
|
|
138
|
+
draw();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function boot() {
|
|
142
|
+
S.root = document.getElementById('c3-root');
|
|
143
|
+
const theme = q.get('theme') === 'dark' || (typeof matchMedia === 'function' && matchMedia('(prefers-color-scheme: dark)').matches && q.get('theme') !== 'light') ? 'dark' : 'light';
|
|
144
|
+
document.documentElement.setAttribute('data-pm-page-theme', theme);
|
|
145
|
+
S.root.setAttribute('data-pm-theme', theme);
|
|
146
|
+
S.root.classList.add('c3-cap');
|
|
147
|
+
ensureC3Css();
|
|
148
|
+
const l = document.createElement('link'); l.rel = 'stylesheet'; l.href = new URL('./c3-capture.css', import.meta.url).href; document.head.appendChild(l);
|
|
149
|
+
draw();
|
|
150
|
+
load().then(() => say({ pmosCapture: 'ready' }), () => { S.st = 'none'; draw(); say({ pmosCapture: 'ready' }); });
|
|
151
|
+
}
|
|
152
|
+
boot();
|
|
@@ -385,7 +385,17 @@ export function handle(S, method, path, p, body) {
|
|
|
385
385
|
const counts = { todo: issues.filter((i) => i.category === 'todo').length, doing: issues.filter((i) => i.category === 'doing').length, done: issues.filter((i) => i.category === 'done').length };
|
|
386
386
|
const num = key === 'IDPF' ? 42 : 19;
|
|
387
387
|
const extra = (k, state, name, cat, n) => { const is = Array.from({ length: n }, (_, i) => ({ key: key + '-' + (1100 + k * 20 + i), summary: (own[i % Math.max(1, own.length)] || { title: 'Sprint work' }).title + ': part ' + (i + 1), status: cat === 'done' ? 'Done' : 'To Do', category: cat, initiative: (own[i % Math.max(1, own.length)] || {}).id || null })); return { name, state, issues: is, counts: { todo: cat === 'todo' ? n : 0, doing: 0, done: cat === 'done' ? n : 0 } }; };
|
|
388
|
-
|
|
388
|
+
// The goal and summary the Jira status check writes (sprint-goal-summary): Jira's goal is too broad for the active sprint,
|
|
389
|
+
// missing for the next, and fine for the closed one, which was summarised once.
|
|
390
|
+
const ini0 = own[0] ? own[0].id : null;
|
|
391
|
+
const themes = [{ title: own[0] ? own[0].title : 'Sprint work', line: 'Most items are in progress or in review.', tickets: issues.filter((i) => i.initiative === ini0).length || 1, done: issues.filter((i) => i.initiative === ini0 && i.category === 'done').length, blocked: 1, people: 'Ana, Budi', initiative: ini0 },
|
|
392
|
+
{ title: 'Fixes and upkeep', line: 'Small fixes outside any initiative.', tickets: 2, done: 1, blocked: 0, people: 'Chen', initiative: null }];
|
|
393
|
+
const act = { name: key + ' Sprint ' + num, state: 'active', issues, counts, goal: { from: 'broad', text: 'Get the first initiative ready for internal users and clear the blocked work.', jira: 'Q4 delivery' },
|
|
394
|
+
summary: 'Most of the sprint is on the first initiative. One item is blocked. Two small fixes sit outside any initiative.', themes, writtenAt: TODAY + 'T06:30:00Z', summarisedTickets: issues.length, closed: null, frozen: false };
|
|
395
|
+
const fut = { ...extra(1, 'future', key + ' Sprint ' + (num + 1), 'todo', 3), goal: { from: 'none', text: 'Finish the first initiative and start the beta.', jira: null }, summary: 'Planned so far: three follow-up items.', themes: [], writtenAt: TODAY + 'T06:30:00Z', closed: null, frozen: false };
|
|
396
|
+
const old = { ...extra(0, 'closed', key + ' Sprint ' + (num - 1), 'done', 5), goal: { from: 'jira', text: 'Start the first initiative.', jira: 'Start the first initiative.' }, summary: 'Goal met. Five items shipped; two moved to the next sprint.', themes: [{ title: 'Sprint work', line: '', tickets: 5, done: 5, blocked: 0, people: '', initiative: ini0 }],
|
|
397
|
+
writtenAt: '2026-09-29T06:30:00Z', closed: { done: 5, total: 7, carried: 2, carriedTo: key + ' Sprint ' + num, points: '21 of 26' }, frozen: true };
|
|
398
|
+
return okb({ source: 'jira', readAt: TODAY + 'T08:10:00', sprints: [act, fut, old] });
|
|
389
399
|
}
|
|
390
400
|
return fail(404, 'No route ' + path);
|
|
391
401
|
}
|
|
@@ -19,8 +19,8 @@ import { RD, failed, splitRender, slideCard, frame, wireFrame, diffOutlines, ens
|
|
|
19
19
|
|
|
20
20
|
const MAX = 20 * 1024 * 1024;
|
|
21
21
|
const KIND_OF = (name) => { const e = String(name || '').toLowerCase().split('.').pop(); return e === 'pdf' ? 'pdf' : e === 'pptx' ? 'pptx' : e === 'docx' ? 'docx' : /^(png|jpe?g)$/.test(e) ? 'image' : ''; };
|
|
22
|
-
const SICON = { pdf: 'pdf', pptx: 'img', docx: 'doc', image: 'img', link: 'link', 'pmos-doc': 'doc' };
|
|
23
|
-
const SWORD = { pdf: 'Upload, PDF', pptx: 'Upload, .pptx', docx: 'Upload, .docx', image: 'Upload, image', link: 'Link', 'pmos-doc': 'Past PMOS document' };
|
|
22
|
+
const SICON = { pdf: 'pdf', pptx: 'img', docx: 'doc', image: 'img', pictures: 'img', link: 'link', 'pmos-doc': 'doc' };
|
|
23
|
+
const SWORD = { pdf: 'Upload, PDF', pptx: 'Upload, .pptx', docx: 'Upload, .docx', image: 'Upload, image', pictures: 'Page pictures', link: 'Link', 'pmos-doc': 'Past PMOS document' };
|
|
24
24
|
/* the studio's width, as a session's terminal sheet has it: half, three-quarters or the whole window; kept on this browser */
|
|
25
25
|
const SIZE_KEY = 'pmos.workspace.c3.studio-size';
|
|
26
26
|
export const STUDIO_SIZES = [['half', 'Half', 'Half of the window'], ['three-quarters', '3/4', 'Three-quarters of the window'], ['whole', 'Whole', 'Whole window']];
|
|
@@ -36,8 +36,8 @@ const LOCAL = {};
|
|
|
36
36
|
const local = (id) => LOCAL[id] || (LOCAL[id] = { view: null, cmp: false, cmpA: null, sidx: 0, point: true, markers: [], draft: null, mOpen: null, panel: false, text: '', fmt: null });
|
|
37
37
|
|
|
38
38
|
function sampleChip(s) {
|
|
39
|
-
const th = s.thumb && /^data:image\//.test(s.thumb) ? el('img', { class: 'rd-dks-thumb img', src: s.thumb, alt: '' }) : el('span', { class: 'rd-dks-thumb ' + (s.kind === 'pptx' || s.kind === 'image' ? 'deck' : 'page'), 'aria-hidden': 'true' }, el('i'), el('i'), el('i'));
|
|
40
|
-
return el('div', { class: 'rd-dks-smp', 'data-rd-sample': s.id }, th, el('div', null, el('b', null, icon(SICON[s.kind] || 'doc', 'sm'), s.name), el('span', { class: 'dim mono' }, SWORD[s.kind] || s.kind),
|
|
39
|
+
const th = s.thumb && /^data:image\//.test(s.thumb) ? el('img', { class: 'rd-dks-thumb img', src: s.thumb, alt: '' }) : el('span', { class: 'rd-dks-thumb ' + (s.kind === 'pptx' || s.kind === 'image' || s.kind === 'pictures' || (s.via === 'chrome' && s.kind === 'pdf') ? 'deck' : 'page'), 'aria-hidden': 'true' }, el('i'), el('i'), el('i'));
|
|
40
|
+
return el('div', { class: 'rd-dks-smp', 'data-rd-sample': s.id }, th, el('div', null, el('b', null, icon(SICON[s.kind] || 'doc', 'sm'), s.name), el('span', { class: 'dim mono' }, s.line || SWORD[s.kind] || s.kind),
|
|
41
41
|
s.hint ? [el('p', { class: 'rd-warn', 'data-rd-hint': '' }, s.hint), s.took ? el('p', null, el('span', { class: 'dim' }, 'PMOS took: '), s.took) : null]
|
|
42
42
|
: s.readable === false ? el('p', { class: 'rd-warn' }, 'PMOS could not read it with your tools. It is kept, but it does not shape the design.')
|
|
43
43
|
: el('p', null, el('span', { class: 'dim' }, 'PMOS took: '), s.took || 'PMOS reads it on the next turn.')));
|
|
@@ -52,7 +52,7 @@ export function openStudio({ ws, id, name, view, onClose } = {}) {
|
|
|
52
52
|
let size = sizeOf(sizeStore.get());
|
|
53
53
|
const sizeBtns = STUDIO_SIZES.map(([k, label, title]) => btn({ class: 'ds-btn', 'aria-pressed': String(k === size), 'data-rd-size': k, title, 'aria-label': title, onclick: () => setSize(k) }, label));
|
|
54
54
|
const seg = el('div', { class: 'seg rd-dks-size', role: 'group', 'aria-label': 'Studio width' }, ...sizeBtns);
|
|
55
|
-
const ui = openSheet({ title: 'Design studio', icon: 'spark', wide: true, dw: true, cls: 'rd-wide rdsz-' + size, body, actions: [seg], onClose: () => { S.closed = true; clearTimeout(S.timer); if (onClose) onClose(); } });
|
|
55
|
+
const ui = openSheet({ title: 'Design studio', icon: 'spark', wide: true, dw: true, cls: 'rd-wide rdsz-' + size, body, actions: [seg], onClose: () => { S.closed = true; clearTimeout(S.timer); clearInterval(S.watch); window.removeEventListener('focus', checkNew); if (onClose) onClose(); } });
|
|
56
56
|
function setSize(k) {
|
|
57
57
|
ui.sheet.classList.remove('rdsz-' + size); size = sizeOf(k); ui.sheet.classList.add('rdsz-' + size);
|
|
58
58
|
sizeStore.set(size);
|
|
@@ -74,6 +74,16 @@ export function openStudio({ ws, id, name, view, onClose } = {}) {
|
|
|
74
74
|
if (n == null || S.cache[n]) return;
|
|
75
75
|
try { S.cache[n] = splitRender(await RD.version(ws, id, n)); } catch (e) { const style = ''; S.cache[n] = { err: (e && e.message) || 'not readable', slides: [], secs: [], style, body: '' }; }
|
|
76
76
|
}
|
|
77
|
+
/* A sample added from another tab (the PMOS Chrome extension) shows here: the open studio reads the design every few seconds and when the window gets focus. */
|
|
78
|
+
const sampleSig = (D) => ((D && D.samples) || []).map((x) => x.id + (x.fetching ? '~' : '')).join(',');
|
|
79
|
+
async function checkNew() {
|
|
80
|
+
if (S.closed || S.busy || !S.D || S.D.running) return;
|
|
81
|
+
const r = await RD.design(ws, id);
|
|
82
|
+
if (S.closed || !r || r.ok === false || !r.design || !S.D || S.D.running) return;
|
|
83
|
+
if (sampleSig(r.design) !== sampleSig(S.D)) load(true);
|
|
84
|
+
}
|
|
85
|
+
S.watch = setInterval(checkNew, 4000);
|
|
86
|
+
window.addEventListener('focus', checkNew);
|
|
77
87
|
function poll() { clearTimeout(S.timer); S.timer = setTimeout(async () => { if (S.closed) return; const was = S.D && S.D.current; await load(false); if (S.D && !S.D.running && S.D.current !== was) { K.markers = []; K.draft = null; scrollLog(); } }, 1500); }
|
|
78
88
|
const ver = (n) => (S.D.versions || []).find((v) => v.n === n);
|
|
79
89
|
const cmpA = () => { const b = S.D.current; const a = K.cmpA != null && K.cmpA !== b && ver(K.cmpA) ? K.cmpA : (S.D.versions.filter((v) => v.n < b).pop() || S.D.versions.find((v) => v.n !== b) || {}).n; return a; };
|
|
@@ -104,6 +114,8 @@ export function openStudio({ ws, id, name, view, onClose } = {}) {
|
|
|
104
114
|
return el('div', { class: 'rd-dks-panel', 'data-rd-panel': '' }, el('div', { class: 'ds-h' }, 'Add a sample'),
|
|
105
115
|
btn({ class: 'rd-dks-opt', 'data-rd-smp': 'file', onclick: () => file.click() }, icon('drop', 'sm'), el('span', null, 'Upload a past deck or document', el('br'), el('span', { class: 'dim rd-small' }, 'PDF, .pptx, .docx or a screenshot, up to 20 MB'))), file,
|
|
106
116
|
el('div', { class: 'ds-whyadd' }, link, btn({ class: 'ds-btn', 'data-rd-smp': 'link', onclick: () => addLink() }, 'Add')),
|
|
117
|
+
btn({ class: 'rd-dks-opt', 'data-rd-smp': 'chrome', 'aria-expanded': String(!!K.chrome), onclick: () => { K.chrome = !K.chrome; paint(); } }, icon('out', 'sm'), el('span', null, 'From Chrome', el('br'), el('span', { class: 'dim rd-small' }, 'Open any page in Chrome and press the PMOS button. Decks come as a PDF, other pages as pictures.'))),
|
|
118
|
+
K.chrome ? el('div', { class: 'fold', 'data-rd-chrome': '' }, icon('info', 'sm'), el('span', null, 'In Chrome, open the page you want, for example a Google Slides deck, and press the PMOS button in the toolbar. Choose this report and press Add as a sample. The sample shows here in a moment. If the PMOS button is not in Chrome yet, run ', el('code', null, 'pmos extension'), ' in a terminal for the steps to load it.')) : null,
|
|
107
119
|
docs.map((d) => btn({ class: 'rd-dks-opt', 'data-rd-smp': 'doc', 'data-v': d.path, onclick: () => addDoc(d) }, icon('doc', 'sm'), el('span', null, 'Pick a past PMOS document', el('br'), el('span', { class: 'dim rd-small' }, d.name + ', ' + slotWord(d.slot))))),
|
|
108
120
|
el('div', { class: 'dim rd-small' }, 'PMOS reads a sample to learn how it looks. It does not copy its numbers. A Google or Confluence link is read through your connectors for its words and structure, not its look.'));
|
|
109
121
|
}
|