@lark-apaas/coding-registry-buildless 0.1.20 → 0.1.22

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.
@@ -111,11 +111,86 @@
111
111
  * @media (prefers-reduced-motion:no-preference){ [data-deck-active] .fade-up{ animation: fadeUp .5s both } }
112
112
  * @keyframes fadeUp { from{ opacity:0; transform:translateY(24px) } to{ opacity:1; transform:none } }
113
113
  * Avoid infinite decorative loops on slide content.
114
+ *
115
+ * window.__miaodaMirror (defined below) is Magic-Share sync plumbing between
116
+ * this component and the miaoda host — internal; do not call it from slide
117
+ * code or remove it.
114
118
  */
115
119
  /* END USAGE */
116
120
 
117
121
  (() => {
118
122
  if (typeof window === 'undefined') return;
123
+
124
+ // ── Mirror sync singleton ──────────────────────────────────────────────────
125
+ // Shared by every buildless script on the page (first one to load defines
126
+ // it). Inlined per script rather than shipped as its own registry file:
127
+ // call sites use the global bare, so inlining is what guarantees
128
+ // definition-before-use — a separate copy-in file would add a whole "SDK
129
+ // copied but <script> tag missing" failure mode. Keep this block identical
130
+ // across deck-stage.js / design-canvas.jsx / animations.jsx — same
131
+ // convention as the mobile-UA regex. Two halves of
132
+ // the 妙享 (Magic Share) mirror protocol:
133
+ // emit(msg, opts) — stamps msg.__mirror so the host relay treats the
134
+ // message as a broadcastable UI-state snapshot;
135
+ // unstamped messages are never mirrored. opts.key is
136
+ // the sync channel (maps to the share SDK's
137
+ // actionType): late joiners receive only the LATEST
138
+ // snapshot per key, so states that overwrite each
139
+ // other must share one key. opts.throttle is the
140
+ // suggested host-side coalescing interval in ms —
141
+ // snapshots make dropped intermediate frames
142
+ // lossless, so hint it on any continuous gesture.
143
+ // onApply(type, fn) — replay entry for a message type this script itself
144
+ // emits. The host sends {type:'miaoda:mirror:apply',
145
+ // uuid, message, latencyMs} and needs
146
+ // {type:'miaoda:mirror:apply-result', uuid, result}
147
+ // back to drive the SDK's retry: result.type
148
+ // 'failed' means retry (use it for transient
149
+ // not-ready states), 'ignored' is terminal (not
150
+ // applicable / bad payload), 'applied' is success.
151
+ // Unregistered types stay silent on purpose —
152
+ // another script on the page may own them, and the
153
+ // host's timeout is the backstop.
154
+ if (!window.__miaodaMirror) {
155
+ const appliers = new Map(); // message.type -> Set<fn>
156
+ window.__miaodaMirror = {
157
+ emit(msg, opts) {
158
+ try { window.parent.postMessage(Object.assign({}, msg, { __mirror: Object.assign({ v: 1 }, opts) }), '*'); } catch (e) {}
159
+ },
160
+ onApply(type, fn) {
161
+ if (!appliers.has(type)) appliers.set(type, new Set());
162
+ appliers.get(type).add(fn);
163
+ return () => { const s = appliers.get(type); if (s) s.delete(fn); };
164
+ },
165
+ };
166
+ window.addEventListener('message', async (e) => {
167
+ // Replays are accepted from the host frame only — generated pages embed
168
+ // arbitrary third-party iframes, any of which could otherwise forge an
169
+ // apply and drive state (plus its persisted side effects).
170
+ if (e.source !== window.parent) return;
171
+ const d = e.data;
172
+ if (!d || d.type !== 'miaoda:mirror:apply' || !d.message || !d.message.type) return;
173
+ const fns = appliers.get(d.message.type);
174
+ if (!fns || !fns.size) return;
175
+ // Merge across handlers (multi-instance case): one success is enough.
176
+ // Instances share a channel, so a sibling that can't apply yet (a lazily
177
+ // slotted second deck returning 'failed') must not poison an applied
178
+ // result into an SDK retry storm of no-ops — 'failed' only stands when
179
+ // nobody applied. Every handler still runs (no short-circuit) so each
180
+ // sibling instance gets the state.
181
+ let result = { type: 'ignored', reason: 'no applier result' };
182
+ for (const fn of Array.from(fns)) {
183
+ let r;
184
+ try { r = await fn(d.message, { latencyMs: d.latencyMs, uuid: d.uuid }); }
185
+ catch (err) { r = { type: 'failed', reason: String((err && err.message) || err) }; }
186
+ if (!r) continue;
187
+ if (r.type === 'applied') result = r;
188
+ else if (result.type !== 'applied' && (r.type === 'failed' || result.type !== 'failed')) result = r;
189
+ }
190
+ try { window.parent.postMessage({ type: 'miaoda:mirror:apply-result', uuid: d.uuid, result }, '*'); } catch (e2) {}
191
+ });
192
+ }
193
+
119
194
  const DESIGN_W_DEFAULT = 1920;
120
195
  const DESIGN_H_DEFAULT = 1080;
121
196
  // Safe margin around the scaled slide in edit/preview (non-presenting) mode,
@@ -149,7 +224,15 @@
149
224
  * reuses. It costs one thing — render() also resets that img's `sizes`,
150
225
  * discarding the rail-width hint _materialize sets; the URL still matches the
151
226
  * slide's, so the thumb reuses the slide's decoded bitmap. */
152
- const KEEP_LIVE_PREFIXES = ['cd-'];
227
+ /* The device frames (ios-/android-/macos-/browser-) get in on the *other* half
228
+ * of the rule. Their content lives in a shadow root, so cloneNode captures none
229
+ * of it — for them re-mounting is not "adds nothing", it is the only way to get
230
+ * the content at all. They do satisfy clause 2: purely presentational, read
231
+ * attributes and paint the shadow, no storage and no messages.
232
+ * design-canvas is deliberately NOT on this list — it reads localStorage,
233
+ * fetches its sidecar and posts to the host on mount, so running it once per
234
+ * thumbnail would corrupt the real state. */
235
+ const KEEP_LIVE_PREFIXES = ['cd-', 'ios-', 'android-', 'macos-', 'browser-'];
153
236
  const keepsCloneLive = (el) => KEEP_LIVE_PREFIXES.some((p) => el.localName.startsWith(p));
154
237
 
155
238
  /* Why a clone needs a substrate rule of its own: _collectAuthorCss samples
@@ -880,6 +963,57 @@
880
963
  });
881
964
  // Announce to the host that this product IS a deck scene.
882
965
  try { window.parent.postMessage({ type: 'miaoda:deck:available' }, '*'); } catch (e) {}
966
+ // Magic Share replay entries — each consumes the very message type this
967
+ // component emits, so the emit↔apply contract lives in one file and is
968
+ // version-consistent on both ends of a share by construction. Replays
969
+ // route through the same internal paths as local interaction (_go, the
970
+ // miaoda:deck:* inbound handlers); the re-emit those paths produce is
971
+ // the host role gate's to drop (the protocol's first anti-echo layer).
972
+ // Synthetic replays through _onMessage run under _applyingMirror so the
973
+ // handlers can tell "presenter state to render" from "this viewer's own
974
+ // choice to persist" (see the rail localStorage guard).
975
+ const applyViaOnMessage = (data) => {
976
+ this._applyingMirror = true;
977
+ try { this._onMessage({ data }); } finally { this._applyingMirror = false; }
978
+ };
979
+ // Capture iframes navigate on their own — the emit side stays unstamped
980
+ // there for exactly that reason, and the replay entries stay
981
+ // unregistered symmetrically so a (mis)forwarded apply can't steer a
982
+ // pinned thumbnail either.
983
+ this._mirrorOff = this._snthumb ? [] : [
984
+ window.__miaodaMirror.onApply('miaoda:deck:slide-changed', (m) => {
985
+ // isFinite, not typeof: NaN|0 would silently land on slide 1.
986
+ if (!Number.isFinite(m.index) || m.index < 0) return { type: 'ignored', reason: 'bad index' };
987
+ // Slides are collected by slotchange AFTER connectedCallback, so a
988
+ // late joiner's replay can beat them — that's transient, and only
989
+ // 'failed' makes the SDK retry ('ignored' would strand the joiner
990
+ // on slide 1).
991
+ if (!this._slides.length) return { type: 'failed', reason: 'no slides yet' };
992
+ const requested = m.index | 0;
993
+ const clamped = Math.max(0, Math.min(this._slides.length - 1, requested));
994
+ if (clamped !== requested) {
995
+ // Fewer slides than the presenter (reload lag after an agent
996
+ // edit): show the nearest page as best effort, but answer
997
+ // 'failed' so the SDK keeps retrying until the slide exists —
998
+ // 'applied' here would strand this end on the clamped page.
999
+ if (clamped !== this._index) this._go(clamped, 'api');
1000
+ return { type: 'failed', reason: 'requested slide not present yet' };
1001
+ }
1002
+ if (clamped === this._index) return { type: 'applied' };
1003
+ this._go(clamped, 'api');
1004
+ return this._index === clamped ? { type: 'applied' } : { type: 'failed', reason: 'goto did not land' };
1005
+ }),
1006
+ window.__miaodaMirror.onApply('miaoda:deck:presenting-changed', (m) => {
1007
+ if (typeof m.on !== 'boolean') return { type: 'ignored', reason: 'no on' };
1008
+ applyViaOnMessage({ type: 'miaoda:deck:presenting', on: m.on });
1009
+ return { type: 'applied' };
1010
+ }),
1011
+ window.__miaodaMirror.onApply('miaoda:deck:rail-visible-changed', (m) => {
1012
+ if (typeof m.on !== 'boolean') return { type: 'ignored', reason: 'no on' };
1013
+ applyViaOnMessage({ type: 'miaoda:deck:rail-visible', on: m.on });
1014
+ return { type: 'applied' };
1015
+ }),
1016
+ ];
883
1017
  }
884
1018
 
885
1019
  _enableRail() {
@@ -1235,6 +1369,8 @@
1235
1369
  if (this._headObserver) this._headObserver.disconnect();
1236
1370
  (this._thumbs || []).forEach((t) => clearTimeout(t._canvasRecapTimer));
1237
1371
  if (this._onTweakChange) window.removeEventListener('tweakchange', this._onTweakChange);
1372
+ (this._mirrorOff || []).forEach((off) => off());
1373
+ this._mirrorOff = null;
1238
1374
  }
1239
1375
 
1240
1376
  attributeChangedCallback() {
@@ -1689,6 +1825,16 @@
1689
1825
  if (n >= 0 && n !== this._index) this._go(n, 'api');
1690
1826
  }
1691
1827
 
1828
+ // Single home for the capture-iframe rule: presenter-popup thumbnails
1829
+ // (_snthumb) navigate on their own, so nothing they post may carry the
1830
+ // Magic Share stamp — a stamped emit from a pinned N±1 thumbnail would
1831
+ // broadcast conflicting state. Route every stamp through here so a new
1832
+ // emit site can't forget the rule.
1833
+ _stampMirror(msg, key) {
1834
+ if (!this._snthumb) msg.__mirror = { v: 1, key: key };
1835
+ return msg;
1836
+ }
1837
+
1692
1838
  _applyIndex({ broadcast = true, reason = 'init' } = {}) {
1693
1839
  if (!this._slides.length) return;
1694
1840
  const prev = this._prevIndex == null ? -1 : this._prevIndex;
@@ -1718,8 +1864,14 @@
1718
1864
  this._syncNotesBody();
1719
1865
 
1720
1866
  if (broadcast) {
1721
- // (1) Parent-window postMessage for speaker-notes renderers.
1722
- try { window.parent.postMessage({ type: 'miaoda:deck:slide-changed', index: curr, total: this._slides.length, skipped: this._skippedIndices() }, '*'); } catch (e) {}
1867
+ // (1) Parent-window postMessage for speaker-notes renderers, stamped
1868
+ // __mirror for Magic Share. Capture iframes (presenter-popup
1869
+ // thumbnails pinned to N±1) navigate on their own — stamping them
1870
+ // would broadcast conflicting indices, so they stay unstamped.
1871
+ try {
1872
+ const msg = { type: 'miaoda:deck:slide-changed', index: curr, total: this._slides.length, skipped: this._skippedIndices() };
1873
+ window.parent.postMessage(this._stampMirror(msg, 'deck:slide'), '*');
1874
+ } catch (e) {}
1723
1875
 
1724
1876
  // (2) In-page CustomEvent on the <deck-stage> element itself.
1725
1877
  // Bubbles and composes out of shadow DOM so slide code can listen:
@@ -1977,6 +2129,10 @@
1977
2129
  _onMessage(e) {
1978
2130
  const d = e.data;
1979
2131
  if (d && d.type === 'miaoda:deck:presenting' && typeof d.on === 'boolean') {
2132
+ // Same-value messages (incl. mirror replays routed back through here)
2133
+ // must not re-run the layout work or re-emit. _presenting starts
2134
+ // undefined, so the first message always lands.
2135
+ if (d.on === this._presenting) return;
1980
2136
  this._presenting = d.on;
1981
2137
  this._syncMobileMode();
1982
2138
  if (!d.on) this._toggleMobUi(false);
@@ -1985,6 +2141,9 @@
1985
2141
  this._closeMenu();
1986
2142
  this._fit();
1987
2143
  this._scaleThumbs();
2144
+ // Presenting is host-driven, so the inbound handler is the one place
2145
+ // the state settles — snapshot it out for Magic Share from here.
2146
+ try { window.parent.postMessage(this._stampMirror({ type: 'miaoda:deck:presenting-changed', on: d.on }, 'deck:presenting'), '*'); } catch (e) {}
1988
2147
  }
1989
2148
  // Host's Preview segment (ViewerMode='none'): the rail's drag-reorder /
1990
2149
  // right-click skip-delete affordances are editing chrome, so hide it
@@ -2007,9 +2166,14 @@
2007
2166
  if (d && d.type === 'miaoda:deck:rail-visible' && typeof d.on === 'boolean') {
2008
2167
  if (d.on === this._railVisible) return;
2009
2168
  this._railVisible = d.on;
2010
- try { localStorage.setItem('deck-stage.railVisible', d.on ? '1' : '0'); } catch (e) {}
2169
+ // Rail visibility is a per-viewer preference — a mirror replay drives
2170
+ // the visual state but must not overwrite what this viewer chose for
2171
+ // themselves after the share ends.
2172
+ if (!this._applyingMirror) {
2173
+ try { localStorage.setItem('deck-stage.railVisible', d.on ? '1' : '0'); } catch (e) {}
2174
+ }
2011
2175
  // Notify the parent so its toolbar state stays in sync.
2012
- try { window.parent.postMessage({ type: 'miaoda:deck:rail-visible-changed', on: d.on }, '*'); } catch (e) {}
2176
+ try { window.parent.postMessage(this._stampMirror({ type: 'miaoda:deck:rail-visible-changed', on: d.on }, 'deck:rail-visible'), '*'); } catch (e) {}
2013
2177
  // Arm the transition, commit it, then flip state — otherwise the
2014
2178
  // browser coalesces both writes and nothing animates on show.
2015
2179
  this.setAttribute('data-rail-anim', '');
@@ -2132,6 +2296,9 @@
2132
2296
  this._fit();
2133
2297
  this._toggleMobUi(false);
2134
2298
  try { window.parent.postMessage({ type: 'miaoda:deck:presenting-dismissed' }, '*'); } catch (err) {}
2299
+ // The mobile back button flips presenting locally (no host round-trip),
2300
+ // so mirror the settled state from here too.
2301
+ try { window.parent.postMessage(this._stampMirror({ type: 'miaoda:deck:presenting-changed', on: false }, 'deck:presenting'), '*'); } catch (e) {}
2135
2302
  });
2136
2303
  const strip = document.createElement('div');
2137
2304
  strip.className = 'mob-strip';
@@ -2742,7 +2909,10 @@
2742
2909
  this._emitDeckChange({ action: on ? 'skip' : 'unskip', from: i, slide });
2743
2910
  // Re-broadcast so the presenter popup's prev/next thumbnails re-pick
2744
2911
  // the nearest non-skipped slide without waiting for a nav event.
2745
- try { window.parent.postMessage({ type: 'miaoda:deck:slide-changed', index: this._index, total: this._slides.length, skipped: this._skippedIndices() }, '*'); } catch (e) {}
2912
+ try {
2913
+ const msg = { type: 'miaoda:deck:slide-changed', index: this._index, total: this._slides.length, skipped: this._skippedIndices() };
2914
+ window.parent.postMessage(this._stampMirror(msg, 'deck:slide'), '*');
2915
+ } catch (e) {}
2746
2916
  }
2747
2917
 
2748
2918
  _skippedIndices() {