@lark-apaas/coding-registry-buildless 0.1.25 → 0.1.26
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/components/animations.jsx +3 -3
- package/components/design-canvas.jsx +3 -3
- package/package.json +1 -1
- package/scripts/card-stage.js +304 -11
- package/scripts/deck-stage.js +3 -3
|
@@ -31,9 +31,9 @@
|
|
|
31
31
|
// it). Inlined per script rather than shipped as its own registry file:
|
|
32
32
|
// call sites use the global bare, so inlining is what guarantees
|
|
33
33
|
// definition-before-use — a separate copy-in file would add a whole "SDK
|
|
34
|
-
// copied but <script> tag missing" failure mode. Keep
|
|
35
|
-
//
|
|
36
|
-
//
|
|
34
|
+
// copied but <script> tag missing" failure mode. Keep every inlined copy
|
|
35
|
+
// identical (find them by this block's heading) — same convention as the
|
|
36
|
+
// mobile-UA regex. Two halves of
|
|
37
37
|
// the 妙享 (Magic Share) mirror protocol:
|
|
38
38
|
// emit(msg, opts) — stamps msg.__mirror so the host relay treats the
|
|
39
39
|
// message as a broadcastable UI-state snapshot;
|
|
@@ -101,9 +101,9 @@ function dcFitTransformToBounds(bounds, vp, minScale, maxScale) {
|
|
|
101
101
|
// it). Inlined per script rather than shipped as its own registry file:
|
|
102
102
|
// call sites use the global bare, so inlining is what guarantees
|
|
103
103
|
// definition-before-use — a separate copy-in file would add a whole "SDK
|
|
104
|
-
// copied but <script> tag missing" failure mode. Keep
|
|
105
|
-
//
|
|
106
|
-
//
|
|
104
|
+
// copied but <script> tag missing" failure mode. Keep every inlined copy
|
|
105
|
+
// identical (find them by this block's heading) — same convention as the
|
|
106
|
+
// mobile-UA regex. Two halves of
|
|
107
107
|
// the 妙享 (Magic Share) mirror protocol:
|
|
108
108
|
// emit(msg, opts) — stamps msg.__mirror so the host relay treats the
|
|
109
109
|
// message as a broadcastable UI-state snapshot;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lark-apaas/coding-registry-buildless",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.26",
|
|
4
4
|
"description": "Miaoda registry (buildless runtime) — shadcn-style copy-in SDKs (globals + CDN-pinned, browser Babel) for buildless static HTML stacks",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
package/scripts/card-stage.js
CHANGED
|
@@ -80,6 +80,76 @@
|
|
|
80
80
|
/* END USAGE */
|
|
81
81
|
|
|
82
82
|
(() => {
|
|
83
|
+
// ── Mirror sync singleton ──────────────────────────────────────────────────
|
|
84
|
+
// Shared by every buildless script on the page (first one to load defines
|
|
85
|
+
// it). Inlined per script rather than shipped as its own registry file:
|
|
86
|
+
// call sites use the global bare, so inlining is what guarantees
|
|
87
|
+
// definition-before-use — a separate copy-in file would add a whole "SDK
|
|
88
|
+
// copied but <script> tag missing" failure mode. Keep every inlined copy
|
|
89
|
+
// identical (find them by this block's heading) — same convention as the
|
|
90
|
+
// mobile-UA regex. Two halves of
|
|
91
|
+
// the 妙享 (Magic Share) mirror protocol:
|
|
92
|
+
// emit(msg, opts) — stamps msg.__mirror so the host relay treats the
|
|
93
|
+
// message as a broadcastable UI-state snapshot;
|
|
94
|
+
// unstamped messages are never mirrored. opts.key is
|
|
95
|
+
// the sync channel (maps to the share SDK's
|
|
96
|
+
// actionType): late joiners receive only the LATEST
|
|
97
|
+
// snapshot per key, so states that overwrite each
|
|
98
|
+
// other must share one key. opts.throttle is the
|
|
99
|
+
// suggested host-side coalescing interval in ms —
|
|
100
|
+
// snapshots make dropped intermediate frames
|
|
101
|
+
// lossless, so hint it on any continuous gesture.
|
|
102
|
+
// onApply(type, fn) — replay entry for a message type this script itself
|
|
103
|
+
// emits. The host sends {type:'miaoda:mirror:apply',
|
|
104
|
+
// uuid, message, latencyMs} and needs
|
|
105
|
+
// {type:'miaoda:mirror:apply-result', uuid, result}
|
|
106
|
+
// back to drive the SDK's retry: result.type
|
|
107
|
+
// 'failed' means retry (use it for transient
|
|
108
|
+
// not-ready states), 'ignored' is terminal (not
|
|
109
|
+
// applicable / bad payload), 'applied' is success.
|
|
110
|
+
// Unregistered types stay silent on purpose —
|
|
111
|
+
// another script on the page may own them, and the
|
|
112
|
+
// host's timeout is the backstop.
|
|
113
|
+
if (!window.__miaodaMirror) {
|
|
114
|
+
const appliers = new Map(); // message.type -> Set<fn>
|
|
115
|
+
window.__miaodaMirror = {
|
|
116
|
+
emit(msg, opts) {
|
|
117
|
+
try { window.parent.postMessage(Object.assign({}, msg, { __mirror: Object.assign({ v: 1 }, opts) }), '*'); } catch (e) {}
|
|
118
|
+
},
|
|
119
|
+
onApply(type, fn) {
|
|
120
|
+
if (!appliers.has(type)) appliers.set(type, new Set());
|
|
121
|
+
appliers.get(type).add(fn);
|
|
122
|
+
return () => { const s = appliers.get(type); if (s) s.delete(fn); };
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
window.addEventListener('message', async (e) => {
|
|
126
|
+
// Replays are accepted from the host frame only — generated pages embed
|
|
127
|
+
// arbitrary third-party iframes, any of which could otherwise forge an
|
|
128
|
+
// apply and drive state (plus its persisted side effects).
|
|
129
|
+
if (e.source !== window.parent) return;
|
|
130
|
+
const d = e.data;
|
|
131
|
+
if (!d || d.type !== 'miaoda:mirror:apply' || !d.message || !d.message.type) return;
|
|
132
|
+
const fns = appliers.get(d.message.type);
|
|
133
|
+
if (!fns || !fns.size) return;
|
|
134
|
+
// Merge across handlers (multi-instance case): one success is enough.
|
|
135
|
+
// Instances share a channel, so a sibling that can't apply yet (a lazily
|
|
136
|
+
// slotted second deck returning 'failed') must not poison an applied
|
|
137
|
+
// result into an SDK retry storm of no-ops — 'failed' only stands when
|
|
138
|
+
// nobody applied. Every handler still runs (no short-circuit) so each
|
|
139
|
+
// sibling instance gets the state.
|
|
140
|
+
let result = { type: 'ignored', reason: 'no applier result' };
|
|
141
|
+
for (const fn of Array.from(fns)) {
|
|
142
|
+
let r;
|
|
143
|
+
try { r = await fn(d.message, { latencyMs: d.latencyMs, uuid: d.uuid }); }
|
|
144
|
+
catch (err) { r = { type: 'failed', reason: String((err && err.message) || err) }; }
|
|
145
|
+
if (!r) continue;
|
|
146
|
+
if (r.type === 'applied') result = r;
|
|
147
|
+
else if (result.type !== 'applied' && (r.type === 'failed' || result.type !== 'failed')) result = r;
|
|
148
|
+
}
|
|
149
|
+
try { window.parent.postMessage({ type: 'miaoda:mirror:apply-result', uuid: d.uuid, result }, '*'); } catch (e2) {}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
83
153
|
/** Shorthands for the common frames; anything else is computed by parseRatio — this is not an allowlist */
|
|
84
154
|
const RATIOS = {
|
|
85
155
|
'3:4': [1080, 1440],
|
|
@@ -414,13 +484,20 @@
|
|
|
414
484
|
(paging / zoom / copy / download).
|
|
415
485
|
overflow:auto is what lets you scroll a card that has been zoomed past the viewport,
|
|
416
486
|
and the holder centres with margin:auto — flex centring clips the top once the content
|
|
417
|
-
outgrows its container.
|
|
487
|
+
outgrows its container.
|
|
488
|
+
The enlarged card must stay commentable while everything around it must not: the host
|
|
489
|
+
exempts a whole event path once any ancestor carries the chrome mark (csChrome), so
|
|
490
|
+
the mark cannot sit on .cs-focus. The dark ground is a separate scrim sibling carrying the mark,
|
|
491
|
+
and the holder is exactly the card's size — every empty pixel then belongs to the
|
|
492
|
+
scrim. pointer-events cannot carve that out instead: comment mode forces
|
|
493
|
+
pointer-events:auto on every element. */
|
|
418
494
|
.cs-focus {
|
|
419
495
|
position: fixed; inset: 0; z-index: 100;
|
|
420
|
-
background: rgba(16,17,20,.92);
|
|
421
496
|
display: grid; overflow: auto;
|
|
497
|
+
padding: 56px 24px 104px; box-sizing: border-box;
|
|
422
498
|
}
|
|
423
|
-
.cs-focus
|
|
499
|
+
.cs-focus-scrim { position: fixed; inset: 0; background: rgba(16,17,20,.92); }
|
|
500
|
+
.cs-focus .cs-holder { position: relative; margin: auto; }
|
|
424
501
|
.cs-focus .cs-holder > section { outline: none !important; box-shadow: none !important; }
|
|
425
502
|
.cs-focus-close {
|
|
426
503
|
position: fixed; top: 20px; right: 24px; z-index: 2;
|
|
@@ -1239,6 +1316,9 @@
|
|
|
1239
1316
|
connectedCallback() {
|
|
1240
1317
|
if (this.dataset.ready) return;
|
|
1241
1318
|
this.dataset.ready = '1';
|
|
1319
|
+
// Before init, not after: a late joiner's replay can land while init still awaits the
|
|
1320
|
+
// sidecar, and only a registered applier can answer 'failed' to make the SDK retry
|
|
1321
|
+
this.wireMirror();
|
|
1242
1322
|
// The script is usually loaded synchronously from <head>, at which point the child
|
|
1243
1323
|
// <section>s are not parsed yet — wait for the document before initialising
|
|
1244
1324
|
if (document.readyState === 'loading') {
|
|
@@ -1364,10 +1444,15 @@
|
|
|
1364
1444
|
// themselves; the "fit" button and the 0 key do not count as framing, since following
|
|
1365
1445
|
// the viewport is exactly what they ask for.
|
|
1366
1446
|
addEventListener('resize', () => {
|
|
1447
|
+
if (this._remoteBounds) { this.fitBounds(this._remoteBounds); return; }
|
|
1367
1448
|
if (this.userFramed) { this.placeHeads(); return; }
|
|
1368
1449
|
this.fit();
|
|
1369
1450
|
});
|
|
1370
1451
|
}
|
|
1452
|
+
this._ready = true;
|
|
1453
|
+
// Baseline for late joiners (the viewport baseline already went out through fit)
|
|
1454
|
+
this.emitStructure();
|
|
1455
|
+
this.emitFocus();
|
|
1371
1456
|
ready();
|
|
1372
1457
|
void this.reportOverflow();
|
|
1373
1458
|
}
|
|
@@ -1572,6 +1657,7 @@
|
|
|
1572
1657
|
section.dataset.label = label.textContent.trim();
|
|
1573
1658
|
this._labelOverrides[section.dataset.cardKey] = section.dataset.label;
|
|
1574
1659
|
this.saveState();
|
|
1660
|
+
this.emitStructure();
|
|
1575
1661
|
});
|
|
1576
1662
|
label.addEventListener('keydown', (e) => {
|
|
1577
1663
|
if (e.key === 'Enter') { e.preventDefault(); label.blur(); }
|
|
@@ -1679,20 +1765,27 @@
|
|
|
1679
1765
|
deleteCard(n) {
|
|
1680
1766
|
const section = this.cardAt(n);
|
|
1681
1767
|
if (!section) return;
|
|
1682
|
-
if (this.focusIndex === n) this.closeFocus();
|
|
1683
1768
|
const key = section.dataset.cardKey;
|
|
1769
|
+
this.dropCard(section);
|
|
1770
|
+
if (key && !this._hiddenKeys.includes(key)) this._hiddenKeys.push(key);
|
|
1771
|
+
this.saveState();
|
|
1772
|
+
this.emitStructure();
|
|
1773
|
+
this.toast('已删除');
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
/** Takes a card off the page — shared by a local delete and a replayed one, which must not persist */
|
|
1777
|
+
dropCard(section) {
|
|
1778
|
+
if (this.focusIndex === Number(section.dataset.cardIndex)) this.closeFocus();
|
|
1684
1779
|
const head = this.headFor && this.headFor.get(section);
|
|
1685
1780
|
if (head) head.remove();
|
|
1686
1781
|
if (this.headFor) this.headFor.delete(section);
|
|
1687
1782
|
section.remove();
|
|
1688
|
-
if (key && !this._hiddenKeys.includes(key)) this._hiddenKeys.push(key);
|
|
1689
|
-
this.saveState();
|
|
1690
1783
|
|
|
1691
1784
|
const cards = this.cards();
|
|
1692
1785
|
cards.forEach((s, i) => { s.dataset.cardIndex = String(i + 1); });
|
|
1693
|
-
this.heads = cards.map((s) => this.headFor.get(s));
|
|
1786
|
+
if (this.headFor) this.heads = cards.map((s) => this.headFor.get(s));
|
|
1694
1787
|
this.placeHeads();
|
|
1695
|
-
this.
|
|
1788
|
+
if (csIsMobile) this.goToPage(this.page, false);
|
|
1696
1789
|
}
|
|
1697
1790
|
|
|
1698
1791
|
/**
|
|
@@ -1811,10 +1904,18 @@
|
|
|
1811
1904
|
|
|
1812
1905
|
// A click on a card opens focus mode (but not when the click lands on a link, button or
|
|
1813
1906
|
// the label)
|
|
1907
|
+
// Releasing a text-selection drag still dispatches a click on the card. Opening focus
|
|
1908
|
+
// there would bury the selection under the overlay before the host's selection-comment
|
|
1909
|
+
// toolbar can use it, so a press that moved or left a selection is not a click.
|
|
1910
|
+
let downX = 0, downY = 0;
|
|
1911
|
+
this.addEventListener('pointerdown', (e) => { downX = e.clientX; downY = e.clientY; });
|
|
1814
1912
|
this.addEventListener('click', (e) => {
|
|
1815
1913
|
const section = e.target.closest('card-stage > section');
|
|
1816
1914
|
if (!section) return;
|
|
1817
1915
|
if (e.target.closest('a, button, input, textarea, [contenteditable]')) return;
|
|
1916
|
+
if (Math.hypot(e.clientX - downX, e.clientY - downY) > 4) return;
|
|
1917
|
+
const sel = getSelection();
|
|
1918
|
+
if (sel && !sel.isCollapsed && section.contains(sel.anchorNode)) return;
|
|
1818
1919
|
this.openFocus(Number(section.dataset.cardIndex));
|
|
1819
1920
|
});
|
|
1820
1921
|
|
|
@@ -1849,6 +1950,9 @@
|
|
|
1849
1950
|
this.style.transform = `translate(${this.panX}px, ${this.panY}px) scale(${this.zoom})`;
|
|
1850
1951
|
this.syncZoomBar();
|
|
1851
1952
|
this.placeHeads();
|
|
1953
|
+
// Any local framing takes the view back from the presenter, resize included
|
|
1954
|
+
if (!this._applyingMirror) this._remoteBounds = null;
|
|
1955
|
+
this.emitViewport();
|
|
1852
1956
|
}
|
|
1853
1957
|
|
|
1854
1958
|
zoomAt(cx, cy, factor) {
|
|
@@ -1938,6 +2042,7 @@
|
|
|
1938
2042
|
this._slideTimer = setTimeout(() => { this.style.transition = 'none'; }, 300);
|
|
1939
2043
|
}
|
|
1940
2044
|
this.syncDots();
|
|
2045
|
+
this.emitFocus();
|
|
1941
2046
|
}
|
|
1942
2047
|
|
|
1943
2048
|
wireSwipe() {
|
|
@@ -2250,6 +2355,9 @@
|
|
|
2250
2355
|
this.focusZoom = this.focusFit;
|
|
2251
2356
|
this._focusFrame = frame;
|
|
2252
2357
|
this._focusClone = clone;
|
|
2358
|
+
// Mirrored by key, not index: indices shift under deletes, and the two ends may have
|
|
2359
|
+
// applied a different set of them when a snapshot crosses
|
|
2360
|
+
this._focusKey = src.dataset.cardKey;
|
|
2253
2361
|
|
|
2254
2362
|
const close = document.createElement('button');
|
|
2255
2363
|
close.className = 'cs-focus-close';
|
|
@@ -2299,9 +2407,20 @@
|
|
|
2299
2407
|
};
|
|
2300
2408
|
bar.append(prev, counter, next, sep(), zoomOut, this._focusPct, zoomIn, sep(), copy, download);
|
|
2301
2409
|
|
|
2302
|
-
|
|
2410
|
+
const scrim = document.createElement('div');
|
|
2411
|
+
scrim.className = 'cs-focus-scrim';
|
|
2412
|
+
// The scrim is fixed, so its containing block is the viewport and a wheel over it would
|
|
2413
|
+
// chain to the document rather than the overlay — scrolling a zoomed-in card from the
|
|
2414
|
+
// dark margin is forwarded by hand. Ctrl/⌘ is left to bubble to the zoom handler below.
|
|
2415
|
+
scrim.addEventListener('wheel', (e) => {
|
|
2416
|
+
if (e.ctrlKey || e.metaKey) return;
|
|
2417
|
+
e.preventDefault();
|
|
2418
|
+
overlay.scrollBy(e.deltaX, e.deltaY);
|
|
2419
|
+
}, { passive: false });
|
|
2420
|
+
|
|
2421
|
+
overlay.append(csChrome(scrim), holder, csChrome(close), csChrome(bar));
|
|
2303
2422
|
overlay.addEventListener('click', (e) => {
|
|
2304
|
-
if (e.target === overlay || e.target ===
|
|
2423
|
+
if (e.target === overlay || e.target === scrim) this.closeFocus();
|
|
2305
2424
|
});
|
|
2306
2425
|
// Focus mode's right-click shares the canvas menu
|
|
2307
2426
|
clone.addEventListener('contextmenu', (e) => {
|
|
@@ -2315,7 +2434,9 @@
|
|
|
2315
2434
|
this.setFocusZoom(this.focusZoom * Math.exp(-e.deltaY / 300));
|
|
2316
2435
|
}, { passive: false });
|
|
2317
2436
|
|
|
2318
|
-
|
|
2437
|
+
overlay.addEventListener('scroll', () => this.emitFocus(), { passive: true });
|
|
2438
|
+
|
|
2439
|
+
document.body.appendChild(overlay);
|
|
2319
2440
|
this.overlay = overlay;
|
|
2320
2441
|
this.applyFocusZoom();
|
|
2321
2442
|
}
|
|
@@ -2331,16 +2452,188 @@
|
|
|
2331
2452
|
this._focusFrame.style.height = this.H * this.focusZoom + 'px';
|
|
2332
2453
|
this._focusClone.style.transform = `scale(${this.focusZoom})`;
|
|
2333
2454
|
if (this._focusPct) this._focusPct.textContent = Math.round(this.focusZoom * 100) + '%';
|
|
2455
|
+
this.emitFocus();
|
|
2334
2456
|
}
|
|
2335
2457
|
|
|
2336
2458
|
closeFocus() {
|
|
2337
2459
|
this.closeContextMenu();
|
|
2460
|
+
const wasOpen = !!this.overlay;
|
|
2338
2461
|
if (this.overlay) this.overlay.remove();
|
|
2339
2462
|
this.overlay = null;
|
|
2340
2463
|
this.focusIndex = null;
|
|
2464
|
+
this._focusKey = null;
|
|
2341
2465
|
this._focusFrame = null;
|
|
2342
2466
|
this._focusClone = null;
|
|
2343
2467
|
this._focusPct = null;
|
|
2468
|
+
if (wasOpen) this.emitFocus();
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
// —— Magic Share (妙享) ——
|
|
2472
|
+
// Three keys, because a late joiner receives only the latest snapshot per key: canvas
|
|
2473
|
+
// framing, the focused card, and deletes / renames never overwrite one another.
|
|
2474
|
+
// Every applier answers 'failed' until init has laid the cards out — that is transient and
|
|
2475
|
+
// only 'failed' makes the SDK retry.
|
|
2476
|
+
wireMirror() {
|
|
2477
|
+
// Capture iframes (?thumbnail= product screenshots, ?export=1) sit out in both directions
|
|
2478
|
+
// — same rule as design-canvas's dcCaptureMode
|
|
2479
|
+
if (/[?&](_snthumb|thumbnail)=|[?&]export=1(&|$)/.test(location.search || '')) return;
|
|
2480
|
+
this._mirrorOn = true;
|
|
2481
|
+
// The applier registry is page-global and outlives this element: once it is taken off the
|
|
2482
|
+
// page it must not answer for (or draw overlays on behalf of) the stage that replaced it
|
|
2483
|
+
const on = (type, fn) => window.__miaodaMirror.onApply(type, (m) =>
|
|
2484
|
+
this.isConnected ? fn(m) : { type: 'ignored', reason: 'stage detached' });
|
|
2485
|
+
on('miaoda:cards:viewport-changed', (m) => this.applyMirrorViewport(m));
|
|
2486
|
+
on('miaoda:cards:focus-changed', (m) => this.applyMirrorFocus(m));
|
|
2487
|
+
on('miaoda:cards:structure-changed', (m) => this.applyMirrorStructure(m));
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2490
|
+
/** Runs a replay so that the emits it triggers are not echoed back as this end's own */
|
|
2491
|
+
asMirror(fn) {
|
|
2492
|
+
this._applyingMirror = true;
|
|
2493
|
+
try { fn(); } finally { this._applyingMirror = false; }
|
|
2494
|
+
}
|
|
2495
|
+
|
|
2496
|
+
// The transform is local screen geometry and cannot be replayed on another window size, so
|
|
2497
|
+
// the world rectangle in view is mirrored instead and each end fits it into its own window
|
|
2498
|
+
// — same scheme as design-canvas's viewport v2.
|
|
2499
|
+
emitViewport() {
|
|
2500
|
+
if (!this._mirrorOn || this._applyingMirror || csIsMobile || this._viewportT) return;
|
|
2501
|
+
// Trailing 120ms: applyTransform runs at pointer rate; reading at fire time loses nothing
|
|
2502
|
+
this._viewportT = setTimeout(() => {
|
|
2503
|
+
this._viewportT = 0;
|
|
2504
|
+
if (!this.isConnected) return;
|
|
2505
|
+
const z = this.zoom;
|
|
2506
|
+
window.__miaodaMirror.emit(
|
|
2507
|
+
{ type: 'miaoda:cards:viewport-changed', bounds: { x: -this.panX / z, y: -this.panY / z, width: innerWidth / z, height: innerHeight / z } },
|
|
2508
|
+
{ key: 'cards:viewport', throttle: 60 },
|
|
2509
|
+
);
|
|
2510
|
+
}, 120);
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
applyMirrorViewport(m) {
|
|
2514
|
+
// Mobile pages card by card; it follows cards:focus instead
|
|
2515
|
+
if (csIsMobile) return { type: 'ignored', reason: 'mobile follows the focused card' };
|
|
2516
|
+
if (!this._ready) return { type: 'failed', reason: 'not laid out yet' };
|
|
2517
|
+
const b = m.bounds;
|
|
2518
|
+
if (!b || ![b.x, b.y, b.width, b.height].every(Number.isFinite) || !(b.width > 0) || !(b.height > 0)) {
|
|
2519
|
+
return { type: 'ignored', reason: 'bad bounds' };
|
|
2520
|
+
}
|
|
2521
|
+
// A timer armed by a local gesture would fire with the presenter's view and echo it back
|
|
2522
|
+
clearTimeout(this._viewportT);
|
|
2523
|
+
this._viewportT = 0;
|
|
2524
|
+
this.fitBounds(b);
|
|
2525
|
+
return { type: 'applied' };
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
fitBounds(b) {
|
|
2529
|
+
const z = Math.min(MAX_PCT / 100, Math.max(MIN_PCT / 100, Math.min(innerWidth / b.width, innerHeight / b.height)));
|
|
2530
|
+
this.asMirror(() => {
|
|
2531
|
+
this.zoom = z;
|
|
2532
|
+
this.panX = innerWidth / 2 - (b.x + b.width / 2) * z;
|
|
2533
|
+
// Top-anchored: a contain fit's spare height would otherwise become a per-device offset
|
|
2534
|
+
this.panY = -b.y * z;
|
|
2535
|
+
this.applyTransform();
|
|
2536
|
+
});
|
|
2537
|
+
// Kept so a resize on this end refits the presenter's view rather than auto-fitting
|
|
2538
|
+
this._remoteBounds = b;
|
|
2539
|
+
}
|
|
2540
|
+
|
|
2541
|
+
focusSnapshot() {
|
|
2542
|
+
const r3 = (v) => Math.round(v * 1000) / 1000;
|
|
2543
|
+
if (csIsMobile) {
|
|
2544
|
+
const s = this.cardAt(this.page);
|
|
2545
|
+
return { key: s ? s.dataset.cardKey : null, zoom: null, sx: 0, sy: 0 };
|
|
2546
|
+
}
|
|
2547
|
+
const ov = this.overlay;
|
|
2548
|
+
if (!ov || !this._focusKey) return { key: null, zoom: null, sx: 0, sy: 0 };
|
|
2549
|
+
const frac = (pos, range) => (range > 0 ? r3(pos / range) : 0);
|
|
2550
|
+
return {
|
|
2551
|
+
key: this._focusKey,
|
|
2552
|
+
// Relative to the entry fit, not the absolute %: the fit follows each end's window, and
|
|
2553
|
+
// what has to match is how much of the card is in view
|
|
2554
|
+
zoom: r3(this.focusZoom / this.focusFit),
|
|
2555
|
+
sx: frac(ov.scrollLeft, ov.scrollWidth - ov.clientWidth),
|
|
2556
|
+
sy: frac(ov.scrollTop, ov.scrollHeight - ov.clientHeight),
|
|
2557
|
+
};
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2560
|
+
emitFocus() {
|
|
2561
|
+
if (!this._mirrorOn || this._applyingMirror || this._focusT) return;
|
|
2562
|
+
this._focusT = setTimeout(() => {
|
|
2563
|
+
this._focusT = 0;
|
|
2564
|
+
if (!this.isConnected) return;
|
|
2565
|
+
const snap = this.focusSnapshot();
|
|
2566
|
+
// Scroll events from a replay arrive after the replay returns, so the flag cannot catch
|
|
2567
|
+
// them; an unchanged snapshot is that echo
|
|
2568
|
+
const sig = JSON.stringify(snap);
|
|
2569
|
+
if (sig === this._focusSig) return;
|
|
2570
|
+
this._focusSig = sig;
|
|
2571
|
+
window.__miaodaMirror.emit({ type: 'miaoda:cards:focus-changed', ...snap }, { key: 'cards:focus', throttle: 60 });
|
|
2572
|
+
}, 120);
|
|
2573
|
+
}
|
|
2574
|
+
|
|
2575
|
+
applyMirrorFocus(m) {
|
|
2576
|
+
if (!this._ready) return { type: 'failed', reason: 'not laid out yet' };
|
|
2577
|
+
if (m.key !== null && typeof m.key !== 'string') return { type: 'ignored', reason: 'bad key' };
|
|
2578
|
+
const section = m.key === null ? null : this.cards().find((s) => s.dataset.cardKey === m.key);
|
|
2579
|
+
if (m.key !== null && !section) return { type: 'ignored', reason: 'card not present' };
|
|
2580
|
+
clearTimeout(this._focusT);
|
|
2581
|
+
this._focusT = 0;
|
|
2582
|
+
this.asMirror(() => {
|
|
2583
|
+
const n = section ? Number(section.dataset.cardIndex) : null;
|
|
2584
|
+
if (csIsMobile) {
|
|
2585
|
+
if (n) this.goToPage(n);
|
|
2586
|
+
return;
|
|
2587
|
+
}
|
|
2588
|
+
if (!n) { this.closeFocus(); return; }
|
|
2589
|
+
if (this.focusIndex !== n) this.openFocus(n);
|
|
2590
|
+
if (Number.isFinite(m.zoom) && m.zoom > 0) this.setFocusZoom(this.focusFit * m.zoom);
|
|
2591
|
+
const ov = this.overlay;
|
|
2592
|
+
if (Number.isFinite(m.sx)) ov.scrollLeft = m.sx * (ov.scrollWidth - ov.clientWidth);
|
|
2593
|
+
if (Number.isFinite(m.sy)) ov.scrollTop = m.sy * (ov.scrollHeight - ov.clientHeight);
|
|
2594
|
+
});
|
|
2595
|
+
this._focusSig = JSON.stringify(this.focusSnapshot());
|
|
2596
|
+
return { type: 'applied' };
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
emitStructure() {
|
|
2600
|
+
if (!this._mirrorOn || this._applyingMirror) return;
|
|
2601
|
+
window.__miaodaMirror.emit(
|
|
2602
|
+
{ type: 'miaoda:cards:structure-changed', srcKey: this.srcKey, hidden: this._hiddenKeys.slice(), labels: { ...this._labelOverrides } },
|
|
2603
|
+
{ key: 'cards:structure' },
|
|
2604
|
+
);
|
|
2605
|
+
}
|
|
2606
|
+
|
|
2607
|
+
applyMirrorStructure(m) {
|
|
2608
|
+
if (!this._ready) return { type: 'failed', reason: 'not laid out yet' };
|
|
2609
|
+
if (typeof m.srcKey !== 'string' || !Array.isArray(m.hidden) || !m.labels || typeof m.labels !== 'object') {
|
|
2610
|
+
return { type: 'ignored', reason: 'malformed structure' };
|
|
2611
|
+
}
|
|
2612
|
+
// Built from another version of the source (an agent edit one end has not reloaded): card
|
|
2613
|
+
// keys no longer line up, same rule the sidecar follows
|
|
2614
|
+
if (m.srcKey !== this.srcKey) return { type: 'ignored', reason: 'source mismatch' };
|
|
2615
|
+
// Replayed state is the presenter's, never persisted through this end's write bridge — but
|
|
2616
|
+
// it is folded into this end's state, or this end's next emit (say, a rename) would carry
|
|
2617
|
+
// the pre-replay hidden list and overwrite the latest snapshot late joiners receive.
|
|
2618
|
+
// Hidden is a union: a dropped section cannot come back.
|
|
2619
|
+
for (const key of m.hidden) {
|
|
2620
|
+
if (typeof key === 'string' && !this._hiddenKeys.includes(key)) this._hiddenKeys.push(key);
|
|
2621
|
+
}
|
|
2622
|
+
for (const [key, label] of Object.entries(m.labels)) {
|
|
2623
|
+
if (typeof label === 'string') this._labelOverrides[key] = label;
|
|
2624
|
+
}
|
|
2625
|
+
this.asMirror(() => {
|
|
2626
|
+
for (const section of this.cards()) {
|
|
2627
|
+
const key = section.dataset.cardKey;
|
|
2628
|
+
if (this._hiddenKeys.includes(key)) { this.dropCard(section); continue; }
|
|
2629
|
+
const label = m.labels[key];
|
|
2630
|
+
if (typeof label !== 'string' || label === section.dataset.label) continue;
|
|
2631
|
+
section.dataset.label = label;
|
|
2632
|
+
const head = this.headFor && this.headFor.get(section);
|
|
2633
|
+
if (head) head.querySelector('.cs-label').textContent = label;
|
|
2634
|
+
}
|
|
2635
|
+
});
|
|
2636
|
+
return { type: 'applied' };
|
|
2344
2637
|
}
|
|
2345
2638
|
|
|
2346
2639
|
}
|
package/scripts/deck-stage.js
CHANGED
|
@@ -139,9 +139,9 @@
|
|
|
139
139
|
// it). Inlined per script rather than shipped as its own registry file:
|
|
140
140
|
// call sites use the global bare, so inlining is what guarantees
|
|
141
141
|
// definition-before-use — a separate copy-in file would add a whole "SDK
|
|
142
|
-
// copied but <script> tag missing" failure mode. Keep
|
|
143
|
-
//
|
|
144
|
-
//
|
|
142
|
+
// copied but <script> tag missing" failure mode. Keep every inlined copy
|
|
143
|
+
// identical (find them by this block's heading) — same convention as the
|
|
144
|
+
// mobile-UA regex. Two halves of
|
|
145
145
|
// the 妙享 (Magic Share) mirror protocol:
|
|
146
146
|
// emit(msg, opts) — stamps msg.__mirror so the host relay treats the
|
|
147
147
|
// message as a broadcastable UI-state snapshot;
|