orz-slides 0.3.0 → 0.6.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/README.md +8 -4
- package/assets/app.js +160 -1
- package/dist/lib.d.ts +4 -0
- package/dist/lib.js +25 -13
- package/dist/orz-slides.browser.js +69 -73
- package/dist/template.js +15 -0
- package/orz-slides-skills/SKILL.md +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ syntax, and stays *quietly editable*. Built on
|
|
|
9
9
|
One file. Open it in a browser to present. Pop out a per-slide editor to change
|
|
10
10
|
a slide. Save it back in place. Nothing to install for the audience.
|
|
11
11
|
|
|
12
|
-
> **Status: published (v0.
|
|
12
|
+
> **Status: published (v0.4.0).** The authoring syntax, CLI, engine, and in-file
|
|
13
13
|
> editor all work (see [DESIGN.md](./DESIGN.md)). Two packages publish in
|
|
14
14
|
> lockstep: the [`orz-slides`](https://www.npmjs.com/package/orz-slides) CLI and
|
|
15
15
|
> the [`orz-slides-browser`](https://www.npmjs.com/package/orz-slides-browser)
|
|
@@ -182,10 +182,14 @@ reveal's core CSS, and all themes are embedded.
|
|
|
182
182
|
|
|
183
183
|
`.slides.html` files conform to **`orz-host-save@1`**: a platform can embed a
|
|
184
184
|
deck in an iframe, announce itself with a `postMessage` handshake, and receive
|
|
185
|
-
saves (`{ source, html }`) instead of the file-system path — standalone
|
|
185
|
+
saves (`{ source, html, theme }`) instead of the file-system path — standalone
|
|
186
186
|
behavior and Export are unchanged, and nothing activates without the
|
|
187
|
-
handshake.
|
|
188
|
-
|
|
187
|
+
handshake. Files also speak **`orz-host-ai@1`**: when the host advertises AI
|
|
188
|
+
operations, the editor shows an assistant (a chip on the current selection and a
|
|
189
|
+
toolbar button) that sends the passage to the host and applies the suggestion it
|
|
190
|
+
returns — the file owns the UI, the host owns the model. Both protocols are
|
|
191
|
+
documented in this repo's [PROTOCOL.md](./PROTOCOL.md) (the shared spec
|
|
192
|
+
originates in orz-mdhtml).
|
|
189
193
|
|
|
190
194
|
## Security — treat these as programs, not documents
|
|
191
195
|
|
package/assets/app.js
CHANGED
|
@@ -146,6 +146,7 @@
|
|
|
146
146
|
extraKeys: { Enter: 'newlineAndIndentContinueMarkdownList' },
|
|
147
147
|
});
|
|
148
148
|
cm.on('change', function () {
|
|
149
|
+
aiHideAll();
|
|
149
150
|
if (suppressChange) return;
|
|
150
151
|
markDirty();
|
|
151
152
|
if (editingDeck) {
|
|
@@ -158,6 +159,7 @@
|
|
|
158
159
|
scheduleRerender();
|
|
159
160
|
}
|
|
160
161
|
});
|
|
162
|
+
cm.on('cursorActivity', function () { aiRefresh(); });
|
|
161
163
|
});
|
|
162
164
|
}
|
|
163
165
|
|
|
@@ -207,6 +209,7 @@
|
|
|
207
209
|
function done() {
|
|
208
210
|
editing = false;
|
|
209
211
|
root.setAttribute('data-mode', 'present');
|
|
212
|
+
aiHideAll();
|
|
210
213
|
if (API.reveal) { API.reveal.layout(); API.refresh(); }
|
|
211
214
|
}
|
|
212
215
|
|
|
@@ -330,8 +333,151 @@
|
|
|
330
333
|
hostSaveTimer = null;
|
|
331
334
|
toast('Save failed — no response from the host'); // document intact, still dirty
|
|
332
335
|
}, 10000);
|
|
333
|
-
hostPost({ type: 'orz-host-save', protocol: HOST_PROTOCOL, version: HOST_VERSION, source: src, html: html });
|
|
336
|
+
hostPost({ type: 'orz-host-save', protocol: HOST_PROTOCOL, version: HOST_VERSION, source: src, html: html, theme: currentTheme });
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ---- host AI assistant (orz-host-ai@1) ------------------------------------
|
|
340
|
+
// When the host advertises AI operations, selecting text in the editor shows
|
|
341
|
+
// an "Improve selection" affordance; picking an op sends the passage to the
|
|
342
|
+
// host, which returns a suggested replacement to apply. File owns the UI; the
|
|
343
|
+
// host owns the model + governance. Additive — no host, no affordance.
|
|
344
|
+
var AI_PROTOCOL = 'orz-host-ai';
|
|
345
|
+
var AI_VERSION = 1;
|
|
346
|
+
var aiOps = null; // advertised operations, or null when no AI host
|
|
347
|
+
var aiOrigin = null; // recorded at the AI handshake
|
|
348
|
+
var aiSeq = 0;
|
|
349
|
+
var aiPending = {}; // requestId -> resolve
|
|
350
|
+
var aiTrigger = null; // the floating "Improve selection" chip
|
|
351
|
+
var aiPanel = null; // the menu / result popover
|
|
352
|
+
var aiRect = null; // anchor rect {left,top,bottom} for the active popover
|
|
353
|
+
|
|
354
|
+
function aiTarget() { return aiOrigin && aiOrigin !== 'null' ? aiOrigin : '*'; }
|
|
355
|
+
function aiPost(msg) { try { window.parent.postMessage(msg, aiTarget()); } catch (e) {} }
|
|
356
|
+
function aiRequest(op, text, sel) {
|
|
357
|
+
return new Promise(function (resolve) {
|
|
358
|
+
var id = 'ai' + (++aiSeq);
|
|
359
|
+
aiPending[id] = resolve;
|
|
360
|
+
aiPost({ type: 'orz-host-ai-request', protocol: AI_PROTOCOL, version: AI_VERSION, requestId: id, op: op, text: text, selection: !!sel });
|
|
361
|
+
setTimeout(function () { if (aiPending[id]) { delete aiPending[id]; resolve({ ok: false, error: 'No response from the host.' }); } }, 30000);
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function aiBox() {
|
|
366
|
+
var b = document.createElement('div');
|
|
367
|
+
b.style.cssText = 'position:fixed;z-index:80;background:Canvas;color:CanvasText;border:1px solid GrayText;border-radius:10px;box-shadow:0 6px 24px rgba(0,0,0,.28);font:13px system-ui,sans-serif;padding:4px;';
|
|
368
|
+
return b;
|
|
369
|
+
}
|
|
370
|
+
// Position `el` (already in the DOM, so it can be measured) just below the
|
|
371
|
+
// selection end; if it would overflow the bottom, flip it ABOVE the selection;
|
|
372
|
+
// if it fits neither, pin near the top and let it scroll. Clamp horizontally.
|
|
373
|
+
function aiSelRect() {
|
|
374
|
+
var to = cm.cursorCoords(cm.getCursor('to'), 'window');
|
|
375
|
+
var fr = cm.cursorCoords(cm.getCursor('from'), 'window');
|
|
376
|
+
return { left: to.left, top: fr.top, bottom: to.bottom };
|
|
334
377
|
}
|
|
378
|
+
function aiElRect(el) { var b = el.getBoundingClientRect(); return { left: b.left, top: b.top, bottom: b.bottom }; }
|
|
379
|
+
function aiPlace(el) {
|
|
380
|
+
if (!el) return;
|
|
381
|
+
var r = aiRect || (cm ? aiSelRect() : { left: 8, top: 8, bottom: 40 });
|
|
382
|
+
var w = el.offsetWidth || 240;
|
|
383
|
+
var h = el.offsetHeight || 40;
|
|
384
|
+
var left = Math.max(8, Math.min(r.left, window.innerWidth - w - 8));
|
|
385
|
+
var top = r.bottom + 6;
|
|
386
|
+
if (top + h > window.innerHeight - 8) {
|
|
387
|
+
var above = r.top - h - 6;
|
|
388
|
+
if (above >= 8) {
|
|
389
|
+
top = above;
|
|
390
|
+
} else {
|
|
391
|
+
top = 8;
|
|
392
|
+
el.style.maxHeight = (window.innerHeight - 16) + 'px';
|
|
393
|
+
el.style.overflowY = 'auto';
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
el.style.left = left + 'px';
|
|
397
|
+
el.style.top = top + 'px';
|
|
398
|
+
}
|
|
399
|
+
function aiHidePanel() { if (aiPanel) { try { aiPanel.remove(); } catch (e) {} aiPanel = null; } }
|
|
400
|
+
function aiHideTrigger() { if (aiTrigger) { try { aiTrigger.remove(); } catch (e) {} aiTrigger = null; } }
|
|
401
|
+
function aiHideAll() { aiHidePanel(); aiHideTrigger(); }
|
|
402
|
+
|
|
403
|
+
// Open the ops menu anchored at `rect`, operating on `text` (a selection when
|
|
404
|
+
// isSel, else the whole editor buffer — the current slide or the deck config).
|
|
405
|
+
function aiOpen(rect, text, isSel) {
|
|
406
|
+
aiHideAll();
|
|
407
|
+
aiRect = rect;
|
|
408
|
+
aiPanel = aiBox();
|
|
409
|
+
aiOps.forEach(function (op) {
|
|
410
|
+
var btn = document.createElement('button');
|
|
411
|
+
btn.textContent = op.title;
|
|
412
|
+
btn.style.cssText = 'display:block;width:220px;text-align:left;background:none;border:0;color:inherit;font:inherit;padding:6px 10px;border-radius:6px;cursor:pointer;';
|
|
413
|
+
btn.onmouseenter = function () { btn.style.background = 'rgba(127,127,127,.16)'; };
|
|
414
|
+
btn.onmouseleave = function () { btn.style.background = 'none'; };
|
|
415
|
+
btn.onclick = function () { aiRun(op, text, isSel); };
|
|
416
|
+
aiPanel.appendChild(btn);
|
|
417
|
+
});
|
|
418
|
+
document.body.appendChild(aiPanel);
|
|
419
|
+
aiPlace(aiPanel);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function aiRun(op, text, isSel) {
|
|
423
|
+
aiHideAll();
|
|
424
|
+
aiPanel = aiBox();
|
|
425
|
+
aiPanel.style.padding = '10px';
|
|
426
|
+
aiPanel.style.width = (isSel ? '340px' : '460px');
|
|
427
|
+
aiPanel.textContent = 'Thinking…';
|
|
428
|
+
document.body.appendChild(aiPanel);
|
|
429
|
+
aiPlace(aiPanel);
|
|
430
|
+
aiRequest(op.id, text, isSel).then(function (r) {
|
|
431
|
+
if (!aiPanel) return;
|
|
432
|
+
aiPanel.textContent = '';
|
|
433
|
+
if (!r.ok) { aiPanel.textContent = r.error || 'That didn’t work.'; aiPlace(aiPanel); return; }
|
|
434
|
+
var label = document.createElement('div');
|
|
435
|
+
label.textContent = isSel ? 'Suggested replacement — edit before applying'
|
|
436
|
+
: 'Suggested rewrite — edit before applying';
|
|
437
|
+
label.style.cssText = 'font-size:11px;opacity:.7;margin-bottom:4px;';
|
|
438
|
+
var ta = document.createElement('textarea');
|
|
439
|
+
ta.value = r.proposed || '';
|
|
440
|
+
ta.style.cssText = 'width:100%;height:' + (isSel ? '130px' : '240px') + ';box-sizing:border-box;font:12px ui-monospace,monospace;resize:vertical;';
|
|
441
|
+
var row = document.createElement('div');
|
|
442
|
+
row.style.cssText = 'display:flex;justify-content:flex-end;gap:6px;margin-top:6px;';
|
|
443
|
+
var cancel = document.createElement('button');
|
|
444
|
+
cancel.textContent = 'Cancel'; cancel.style.cssText = 'padding:4px 10px;cursor:pointer;';
|
|
445
|
+
cancel.onclick = aiHidePanel;
|
|
446
|
+
var apply = document.createElement('button');
|
|
447
|
+
apply.textContent = isSel ? 'Replace' : 'Replace all'; apply.style.cssText = 'padding:4px 10px;cursor:pointer;font-weight:600;';
|
|
448
|
+
apply.onclick = function () {
|
|
449
|
+
if (isSel) { cm.replaceSelection(ta.value); }
|
|
450
|
+
else { var c = cm.getCursor(); cm.setValue(ta.value); try { cm.setCursor(c); } catch (e) {} }
|
|
451
|
+
aiHidePanel(); markDirty();
|
|
452
|
+
};
|
|
453
|
+
row.appendChild(cancel); row.appendChild(apply);
|
|
454
|
+
aiPanel.appendChild(label); aiPanel.appendChild(ta); aiPanel.appendChild(row);
|
|
455
|
+
aiPlace(aiPanel);
|
|
456
|
+
ta.focus();
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Called on selection change: show the chip when there's a usable selection.
|
|
461
|
+
function aiRefresh() {
|
|
462
|
+
if (!aiOps || !aiOps.length || !cm || !editing) { aiHideAll(); return; }
|
|
463
|
+
if (aiPanel) return; // don't fight an open menu/result
|
|
464
|
+
var sel = cm.getSelection();
|
|
465
|
+
if (!sel || sel.trim().length < 2) { aiHideTrigger(); return; }
|
|
466
|
+
if (!aiTrigger) {
|
|
467
|
+
aiTrigger = document.createElement('button');
|
|
468
|
+
aiTrigger.textContent = '✦ Improve selection';
|
|
469
|
+
aiTrigger.style.cssText = 'position:fixed;z-index:78;background:Canvas;color:CanvasText;border:1px solid GrayText;border-radius:999px;font:12px system-ui,sans-serif;padding:4px 10px;cursor:pointer;box-shadow:0 4px 16px rgba(0,0,0,.22);';
|
|
470
|
+
aiTrigger.onmousedown = function (e) { e.preventDefault(); }; // keep the cm selection
|
|
471
|
+
aiTrigger.onclick = function (e) { e.stopPropagation(); aiOpen(aiSelRect(), cm.getSelection(), true); };
|
|
472
|
+
document.body.appendChild(aiTrigger);
|
|
473
|
+
}
|
|
474
|
+
aiRect = aiSelRect();
|
|
475
|
+
aiPlace(aiTrigger);
|
|
476
|
+
}
|
|
477
|
+
document.addEventListener('mousedown', function (e) {
|
|
478
|
+
if (aiPanel && !aiPanel.contains(e.target)) aiHidePanel();
|
|
479
|
+
});
|
|
480
|
+
|
|
335
481
|
function onHostMessage(event) {
|
|
336
482
|
// only the embedding parent may speak the protocol
|
|
337
483
|
if (window.parent === window || event.source !== window.parent) return;
|
|
@@ -348,6 +494,19 @@
|
|
|
348
494
|
clearTimeout(hostSaveTimer); hostSaveTimer = null;
|
|
349
495
|
if (d.ok) { clearDirty(); toast('Saved'); }
|
|
350
496
|
else { toast('Save failed' + (d.error ? ' — ' + String(d.error) : '')); }
|
|
497
|
+
} else if (d.type === 'orz-host-ai-hello' && d.protocol === AI_PROTOCOL && Array.isArray(d.operations)) {
|
|
498
|
+
aiOrigin = event.origin;
|
|
499
|
+
aiOps = d.operations.filter(function (o) { return o && o.id && o.title; });
|
|
500
|
+
aiPost({ type: 'orz-host-ai-ready', protocol: AI_PROTOCOL, version: AI_VERSION });
|
|
501
|
+
// Reveal the toolbar's page-wide AI button — runs an op on the editor buffer.
|
|
502
|
+
var aiBtn = document.getElementById('orz-ai');
|
|
503
|
+
if (aiBtn && aiOps.length) {
|
|
504
|
+
aiBtn.style.display = '';
|
|
505
|
+
aiBtn.onclick = function () { if (cm) aiOpen(aiElRect(aiBtn), cm.getValue(), false); };
|
|
506
|
+
}
|
|
507
|
+
} else if (d.type === 'orz-host-ai-result' && d.requestId && aiPending[d.requestId]) {
|
|
508
|
+
var aiRes = aiPending[d.requestId]; delete aiPending[d.requestId];
|
|
509
|
+
aiRes({ ok: !!d.ok, proposed: d.proposed, error: d.error });
|
|
351
510
|
}
|
|
352
511
|
}
|
|
353
512
|
// listen from script load so an early hello isn't missed
|
package/dist/lib.d.ts
CHANGED
|
@@ -15,6 +15,10 @@ export interface BuildSlidesOptions {
|
|
|
15
15
|
title?: string;
|
|
16
16
|
/** Theme id fallback; the deck's own `theme:` wins. Validated against THEME_DEFS. */
|
|
17
17
|
theme?: string;
|
|
18
|
+
/** Renderer + theme + reveal CSS delivery: `inline` (default, offline) or
|
|
19
|
+
* `cdn` (small file — engine, themes, and reveal.css load from jsDelivr at
|
|
20
|
+
* view time; requires orz-slides-browser to be published at this version). */
|
|
21
|
+
delivery?: 'inline' | 'cdn';
|
|
18
22
|
}
|
|
19
23
|
/**
|
|
20
24
|
* Shared inline-composition path. Both {@link buildSlidesHtml} and the CLI's
|
package/dist/lib.js
CHANGED
|
@@ -114,19 +114,31 @@ export function buildSlidesHtmlWithDocId(opts, docId) {
|
|
|
114
114
|
const defaultTheme = themes.some((t) => t.id === wanted) ? wanted : themes[0].id;
|
|
115
115
|
const ratio = deck.config.ratio || '16:9';
|
|
116
116
|
const title = deck.config.title || opts.title || 'Untitled';
|
|
117
|
-
// Engine + theme delivery
|
|
118
|
-
const
|
|
119
|
-
const
|
|
120
|
-
mode: '
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
117
|
+
// Engine + theme + reveal CSS delivery: inline (default, offline) or CDN.
|
|
118
|
+
const cdn = opts.delivery === 'cdn';
|
|
119
|
+
const renderer = cdn
|
|
120
|
+
? { mode: 'cdn', src: `https://cdn.jsdelivr.net/npm/orz-slides-browser@${ver}/orz-slides.browser.js` }
|
|
121
|
+
: { mode: 'inline', js: readEngineBundle() };
|
|
122
|
+
const theme = cdn
|
|
123
|
+
? { mode: 'cdn' }
|
|
124
|
+
: {
|
|
125
|
+
mode: 'inline',
|
|
126
|
+
base: readFileSync(findAsset('themes/base.css'), 'utf8'),
|
|
127
|
+
themes: THEME_DEFS.map((t) => ({ id: t.id, css: themeOnly(t.id) })),
|
|
128
|
+
};
|
|
129
|
+
let revealCss;
|
|
130
|
+
if (cdn) {
|
|
131
|
+
const revealVer = pkgVersion('reveal.js', '5.0.4');
|
|
132
|
+
revealCss = {
|
|
133
|
+
mode: 'cdn',
|
|
134
|
+
resetUrl: `https://cdn.jsdelivr.net/npm/reveal.js@${revealVer}/dist/reset.css`,
|
|
135
|
+
coreUrl: `https://cdn.jsdelivr.net/npm/reveal.js@${revealVer}/dist/reveal.css`,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
const reveal = readRevealCss();
|
|
140
|
+
revealCss = { mode: 'inline', reset: reveal.reset, core: reveal.core };
|
|
141
|
+
}
|
|
130
142
|
const appJs = readFileSync(findAsset('app.js'), 'utf8');
|
|
131
143
|
const CM = 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16';
|
|
132
144
|
return buildHtml({
|