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 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.1.1).** The authoring syntax, CLI, engine, and in-file
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. The canonical spec lives in the orz-mdhtml repo:
188
- [PROTOCOL.md](https://github.com/wangyu16/orz-mdhtml/blob/main/PROTOCOL.md).
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 always fully inline.
118
- const renderer = { mode: 'inline', js: readEngineBundle() };
119
- const theme = {
120
- mode: 'inline',
121
- base: readFileSync(findAsset('themes/base.css'), 'utf8'),
122
- themes: THEME_DEFS.map((t) => ({ id: t.id, css: themeOnly(t.id) })),
123
- };
124
- const reveal = readRevealCss();
125
- const revealCss = {
126
- mode: 'inline',
127
- reset: reveal.reset,
128
- core: reveal.core,
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({