@lark-apaas/coding-registry-buildless 0.1.5-alpha.20260717102042 → 0.1.5
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/design-canvas.jsx +23 -21
- package/package.json +1 -1
- package/scripts/deck-stage.js +94 -89
|
@@ -29,10 +29,9 @@
|
|
|
29
29
|
// suppress the card chrome and let the artboard size to its content:
|
|
30
30
|
// <DCArtboard chromeless><IOSDevice>…</IOSDevice></DCArtboard>
|
|
31
31
|
//
|
|
32
|
-
// On mobile
|
|
33
|
-
// (
|
|
34
|
-
//
|
|
35
|
-
// label still opens the focus overlay.
|
|
32
|
+
// On mobile devices the canvas automatically becomes a browse-only surface
|
|
33
|
+
// (touch pan/zoom, editing disabled). This needs NO authoring changes:
|
|
34
|
+
// never add custom touch handling or mobile-specific layouts.
|
|
36
35
|
/* END USAGE */
|
|
37
36
|
|
|
38
37
|
const DC = {
|
|
@@ -45,10 +44,11 @@ const DC = {
|
|
|
45
44
|
font: '-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif',
|
|
46
45
|
};
|
|
47
46
|
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
47
|
+
// Mirrors the host platform's isMobile() (UA regex + iPad maxTouchPoints);
|
|
48
|
+
// the product iframe is cross-origin, so the shared helper can't be imported.
|
|
49
|
+
// Deliberately NOT viewport-based: the desktop workbench shows canvases in a
|
|
50
|
+
// narrow preview iframe that a width breakpoint would misread as mobile.
|
|
51
|
+
// Evaluated once at load. Keep the regex in sync with deck-stage.js's copy.
|
|
52
52
|
const DC_MOBILE_UA_RE = /(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i;
|
|
53
53
|
const dcIsMobile = typeof navigator !== 'undefined' &&
|
|
54
54
|
(DC_MOBILE_UA_RE.test(navigator.userAgent) ||
|
|
@@ -112,14 +112,14 @@ if (typeof document !== 'undefined' && !document.getElementById('dc-styles')) {
|
|
|
112
112
|
// Mobile is browse-only: kill text selection + iOS long-press callout
|
|
113
113
|
// inside the canvas. iOS interprets a drag over selectable TEXT as a
|
|
114
114
|
// selection gesture and steals the pointer (pointercancel) — touch-action
|
|
115
|
-
// only suppresses native pan/zoom, not selection
|
|
116
|
-
//
|
|
115
|
+
// only suppresses native pan/zoom, not selection, so a drag
|
|
116
|
+
// starting on selectable text never reaches the pan.
|
|
117
117
|
'.dc-mobile, .dc-mobile *{-webkit-user-select:none;user-select:none;-webkit-touch-callout:none}',
|
|
118
118
|
// Prototype content routinely carries inner scrollers (the device/window
|
|
119
119
|
// frames all have overflow:auto bodies). On iOS a touch starting inside a
|
|
120
120
|
// scrollable element is consumed by THAT scroller (native scroll attempt
|
|
121
|
-
// + pointercancel), so
|
|
122
|
-
//
|
|
121
|
+
// + pointercancel), so a pan could
|
|
122
|
+
// never start there. Mobile canvas is browse-only: kill native
|
|
123
123
|
// touch handling on everything inside the cards so gestures always reach
|
|
124
124
|
// the viewport state machine. Desktop (no .dc-mobile) keeps inner scroll.
|
|
125
125
|
'.dc-mobile [data-dc-slot] *{touch-action:none !important}',
|
|
@@ -740,9 +740,9 @@ function DCViewport({ children, minScale = 0.1, maxScale = 8, style = {} }) {
|
|
|
740
740
|
const touches = new Map(); // pointerId -> last {x, y}
|
|
741
741
|
let mobMode = null; // null | 'tap' | 'pan' | 'pinch'
|
|
742
742
|
let tapStart = null; // down point for slop arbitration
|
|
743
|
-
let pinchDist = 0; //
|
|
743
|
+
let pinchDist = 0; // finger distance as of the last applied frame
|
|
744
744
|
let pinchMid = null; // last-applied midpoint
|
|
745
|
-
let panApplied = null; //
|
|
745
|
+
let panApplied = null; // pointer position as of the last applied pan frame
|
|
746
746
|
const MOB_SLOP = 9; // px of travel before a tap becomes a pan
|
|
747
747
|
|
|
748
748
|
// First two insertion-ordered touches define the pinch; a third finger is
|
|
@@ -758,7 +758,7 @@ function DCViewport({ children, minScale = 0.1, maxScale = 8, style = {} }) {
|
|
|
758
758
|
mobMode = 'tap';
|
|
759
759
|
tapStart = { x: e.clientX, y: e.clientY };
|
|
760
760
|
} else if (touches.size === 2) {
|
|
761
|
-
mobFlushNow(); //
|
|
761
|
+
mobFlushNow(); // apply any pending pan frame before rebasing as a pinch
|
|
762
762
|
mobMode = 'pinch';
|
|
763
763
|
const g = pinchGeom();
|
|
764
764
|
pinchDist = g.d;
|
|
@@ -766,11 +766,13 @@ function DCViewport({ children, minScale = 0.1, maxScale = 8, style = {} }) {
|
|
|
766
766
|
try { vp.setPointerCapture(e.pointerId); } catch {}
|
|
767
767
|
}
|
|
768
768
|
};
|
|
769
|
-
//
|
|
770
|
-
//
|
|
771
|
-
//
|
|
772
|
-
//
|
|
773
|
-
//
|
|
769
|
+
// rAF-coalesced application: move handlers only record pointer
|
|
770
|
+
// positions; geometry is applied at most once per frame. pointermove can
|
|
771
|
+
// far outrun the render rate (120Hz touch sampling, fast pinches), and
|
|
772
|
+
// running the full zoomAt/apply per event creates engine-heap garbage
|
|
773
|
+
// faster than mobile WebKit reclaims it — which reads as unbounded
|
|
774
|
+
// memory growth on device. Only the last position per frame can ever
|
|
775
|
+
// be painted anyway.
|
|
774
776
|
let mobRaf = 0;
|
|
775
777
|
const mobFlushFrame = () => {
|
|
776
778
|
mobRaf = 0;
|
|
@@ -811,7 +813,7 @@ function DCViewport({ children, minScale = 0.1, maxScale = 8, style = {} }) {
|
|
|
811
813
|
if (mobMode === 'pan' || mobMode === 'pinch') mobSchedule();
|
|
812
814
|
};
|
|
813
815
|
const onTouchUp = (e) => {
|
|
814
|
-
mobFlushNow(); //
|
|
816
|
+
mobFlushNow(); // apply the pending frame first so the gesture tail isn't lost
|
|
815
817
|
if (!touches.delete(e.pointerId)) return;
|
|
816
818
|
try { vp.releasePointerCapture(e.pointerId); } catch {}
|
|
817
819
|
if (touches.size === 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lark-apaas/coding-registry-buildless",
|
|
3
|
-
"version": "0.1.5
|
|
3
|
+
"version": "0.1.5",
|
|
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/deck-stage.js
CHANGED
|
@@ -12,9 +12,8 @@
|
|
|
12
12
|
* hidden while presenting / in preview / on narrow / noscale / no-rail.
|
|
13
13
|
* (b) keyboard navigation — ←/→, PgUp/PgDn, Space, Home/End, number keys.
|
|
14
14
|
* On touch devices, tapping the left/right half of the stage goes
|
|
15
|
-
* prev/next (
|
|
16
|
-
*
|
|
17
|
-
* slide content are left alone.
|
|
15
|
+
* prev/next (not in mobile browse mode — see (g)) — taps on links,
|
|
16
|
+
* buttons and other interactive slide content are left alone.
|
|
18
17
|
* (c) press R to reset to slide 0.
|
|
19
18
|
* (d) auto-scaling — inner canvas is a fixed design size (default 1920×1080)
|
|
20
19
|
* scaled with `transform: scale()` to fit the viewport, letterboxed.
|
|
@@ -35,18 +34,10 @@
|
|
|
35
34
|
* (≤640px), and via the `no-rail` attribute. Rail mutations dispatch
|
|
36
35
|
* a `deckchange`
|
|
37
36
|
* CustomEvent on the element: detail = {action, from, to, slide}.
|
|
38
|
-
* (g) mobile — on mobile
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* fades in while scrolling (16|20×12, ink 20% bg, fixed 8px digits)
|
|
43
|
-
* and fades out 2s after scrolling stops. Presenting mode
|
|
44
|
-
* (miaoda:deck:presenting) rotates the stage 90° (CSS pseudo-
|
|
45
|
-
* landscape, no orientation.lock): tap left/right third to navigate,
|
|
46
|
-
* tap center to toggle the controls overlay (back button +
|
|
47
|
-
* filmstrip; auto-hides after 3s). The back button exits presenting
|
|
48
|
-
* and posts miaoda:deck:presenting-dismissed to the parent.
|
|
49
|
-
* Desktop behaviour is unchanged.
|
|
37
|
+
* (g) mobile — on mobile devices the same deck automatically renders as
|
|
38
|
+
* a scrollable vertical card list (browse) and a rotated fullscreen
|
|
39
|
+
* presenting mode. This needs NO authoring changes: never hand-roll
|
|
40
|
+
* mobile/responsive slide layouts. Desktop behaviour is unchanged.
|
|
50
41
|
*
|
|
51
42
|
* Slides are HIDDEN, not unmounted. Non-active slides stay in the DOM with
|
|
52
43
|
* `visibility: hidden` + `opacity: 0`, so their state (videos, iframes,
|
|
@@ -129,20 +120,21 @@
|
|
|
129
120
|
// Slide-authored controls that should keep a tap instead of it navigating.
|
|
130
121
|
const INTERACTIVE_SEL = 'a[href], button, input, select, textarea, summary, label, video[controls], audio[controls], [role="button"], [onclick], [tabindex]:not([tabindex^="-"]), [contenteditable]:not([contenteditable="false" i])';
|
|
131
122
|
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
//
|
|
123
|
+
// Mirrors the host platform's isMobile() (UA regex + iPad maxTouchPoints);
|
|
124
|
+
// the product iframe is cross-origin, so the shared helper can't be
|
|
125
|
+
// imported. Deliberately NOT viewport-based: the desktop workbench renders
|
|
126
|
+
// decks in a narrow preview iframe that a width breakpoint would misread
|
|
127
|
+
// as mobile. Keep the regex in sync with design-canvas.jsx's copy.
|
|
136
128
|
const MOBILE_UA_RE = /(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i;
|
|
137
129
|
const isMobileUA = () =>
|
|
138
130
|
MOBILE_UA_RE.test(navigator.userAgent) ||
|
|
139
131
|
(/iPad|Tab|Tablet/i.test(navigator.userAgent) && !!navigator.maxTouchPoints && navigator.maxTouchPoints > 1);
|
|
140
|
-
//
|
|
141
|
-
const MOB_MARGIN = 12;
|
|
142
|
-
const MOB_GAP = 8;
|
|
143
|
-
const MOB_RADIUS = 8;
|
|
144
|
-
const MOB_BADGE_IDLE_MS = 2000; //
|
|
145
|
-
const MOB_UI_HIDE_MS = 3000; //
|
|
132
|
+
// Mobile visual metrics (values from the platform's mobile design spec).
|
|
133
|
+
const MOB_MARGIN = 12; // card-stream edge margin
|
|
134
|
+
const MOB_GAP = 8; // gap between cards
|
|
135
|
+
const MOB_RADIUS = 8; // card corner radius
|
|
136
|
+
const MOB_BADGE_IDLE_MS = 2000; // scroll idle before the page badge fades
|
|
137
|
+
const MOB_UI_HIDE_MS = 3000; // presenting controls auto-hide
|
|
146
138
|
|
|
147
139
|
const pad2 = (n) => String(n).padStart(2, '0');
|
|
148
140
|
|
|
@@ -408,7 +400,7 @@
|
|
|
408
400
|
.rail[data-presenting] + .rail-resize,
|
|
409
401
|
.rail[data-user-hidden] + .rail-resize { display: none; }
|
|
410
402
|
|
|
411
|
-
/* ──
|
|
403
|
+
/* ── Mobile browse: vertical card stream ── */
|
|
412
404
|
:host([data-mobile-list]) .stage {
|
|
413
405
|
display: block;
|
|
414
406
|
overflow-y: auto;
|
|
@@ -435,11 +427,12 @@
|
|
|
435
427
|
(The equivalent rule further down lives inside @media print only.) */
|
|
436
428
|
:host([data-mobile-list]) ::slotted([data-deck-skip]) { display: none !important; }
|
|
437
429
|
|
|
438
|
-
/* ──
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
430
|
+
/* ── Mobile presenting: CSS pseudo-landscape (no orientation.lock) ──
|
|
431
|
+
Rotate only while the device is physically portrait. Once the user
|
|
432
|
+
turns the phone (system auto-rotate) the viewport itself is landscape,
|
|
433
|
+
and stacking another 90° would render slides sideways — so landscape
|
|
434
|
+
falls back to a plain full-bleed fit. _fitMobilePresent and
|
|
435
|
+
_mobPresentZone branch on the same orientation test. */
|
|
443
436
|
:host([data-mobile-present]) .stage { background: #000; }
|
|
444
437
|
@media (orientation: portrait) {
|
|
445
438
|
:host([data-mobile-present]) .stage {
|
|
@@ -452,7 +445,7 @@
|
|
|
452
445
|
}
|
|
453
446
|
}
|
|
454
447
|
|
|
455
|
-
/* ──
|
|
448
|
+
/* ── Mobile page badge ── */
|
|
456
449
|
.mob-badges { display: none; }
|
|
457
450
|
:host([data-mobile-list]) .mob-badges {
|
|
458
451
|
display: block;
|
|
@@ -478,7 +471,7 @@
|
|
|
478
471
|
}
|
|
479
472
|
:host([data-mob-scrolling]) .mob-badge { opacity: 1; }
|
|
480
473
|
|
|
481
|
-
/* ──
|
|
474
|
+
/* ── Mobile presenting controls (back button + filmstrip) ── */
|
|
482
475
|
.mob-ui { display: none; }
|
|
483
476
|
:host([data-mobile-present][data-mob-ui]) .mob-ui {
|
|
484
477
|
display: block;
|
|
@@ -514,7 +507,8 @@
|
|
|
514
507
|
display: flex;
|
|
515
508
|
gap: 8px;
|
|
516
509
|
padding: 10px 16px calc(10px + env(safe-area-inset-bottom));
|
|
517
|
-
/*
|
|
510
|
+
/* Transparent: thumbnails carry their own white frames, and a solid
|
|
511
|
+
strip would blanket the slide behind the overlay. */
|
|
518
512
|
background: transparent;
|
|
519
513
|
overflow-x: auto;
|
|
520
514
|
}
|
|
@@ -540,7 +534,7 @@
|
|
|
540
534
|
font-size: 12px;
|
|
541
535
|
line-height: 20px;
|
|
542
536
|
font-weight: 500;
|
|
543
|
-
/*
|
|
537
|
+
/* The strip floats over the (dark) presenting stage — numbers must be light. */
|
|
544
538
|
color: rgba(255, 255, 255, 0.9);
|
|
545
539
|
}
|
|
546
540
|
.mob-strip .mthumb[data-current] .mnum { color: #336DF4; }
|
|
@@ -1044,7 +1038,7 @@
|
|
|
1044
1038
|
this._onMobScroll = this._onMobScroll.bind(this);
|
|
1045
1039
|
stage.addEventListener('scroll', this._onMobScroll, { passive: true });
|
|
1046
1040
|
|
|
1047
|
-
//
|
|
1041
|
+
// Per-slide placement rules for the mobile card stream; rebuilt by _fitMobileList().
|
|
1048
1042
|
this._mobStyle = document.createElement('style');
|
|
1049
1043
|
|
|
1050
1044
|
this._mobBadges = document.createElement('div');
|
|
@@ -1472,8 +1466,9 @@
|
|
|
1472
1466
|
this._prevIndex = curr;
|
|
1473
1467
|
}
|
|
1474
1468
|
|
|
1475
|
-
//
|
|
1476
|
-
//
|
|
1469
|
+
// The two mobile presentations are mutually exclusive. noscale and
|
|
1470
|
+
// _snthumb are capture modes whose output must keep desktop geometry,
|
|
1471
|
+
// so they opt out of both.
|
|
1477
1472
|
_syncMobileMode() {
|
|
1478
1473
|
const eligible = this._mobile && !this.hasAttribute('noscale') && !this._snthumb;
|
|
1479
1474
|
this.toggleAttribute('data-mobile-list', eligible && !this._presenting);
|
|
@@ -1483,9 +1478,9 @@
|
|
|
1483
1478
|
_fitMobileList() {
|
|
1484
1479
|
const dw = this.designWidth, dh = this.designHeight;
|
|
1485
1480
|
const vw = window.innerWidth;
|
|
1486
|
-
this._mobLastVw = vw; // _onResize
|
|
1481
|
+
this._mobLastVw = vw; // baseline for _onResize's height-only skip
|
|
1487
1482
|
const stage = this._canvas.parentElement;
|
|
1488
|
-
//
|
|
1483
|
+
// Clear the stage inset the desktop _fit() wrote inline.
|
|
1489
1484
|
if (stage) { stage.style.left = '0'; stage.style.right = '0'; stage.style.top = '0'; stage.style.bottom = '0'; }
|
|
1490
1485
|
this.removeAttribute('data-windowed');
|
|
1491
1486
|
const cardW = vw - 2 * MOB_MARGIN;
|
|
@@ -1504,15 +1499,17 @@
|
|
|
1504
1499
|
' inset: auto !important; top: 0 !important; left: 0 !important;' +
|
|
1505
1500
|
' width: ' + dw + 'px !important; height: ' + dh + 'px !important;' +
|
|
1506
1501
|
' transform: translate(' + MOB_MARGIN + 'px, ' + y + 'px) scale(' + s + ') !important;' +
|
|
1507
|
-
//
|
|
1502
|
+
// Radius/border are pre-divided by the scale so the slide's transform
|
|
1503
|
+
// lands them at 8px / 0.5px on screen.
|
|
1508
1504
|
' border-radius: ' + (MOB_RADIUS / s) + 'px; overflow: hidden !important;' +
|
|
1509
1505
|
' border: ' + (0.5 / s) + 'px solid #DEE0E3; }'
|
|
1510
1506
|
);
|
|
1511
1507
|
cards.push({ slide, i, y, h: cardH });
|
|
1512
1508
|
y += cardH + MOB_GAP;
|
|
1513
1509
|
});
|
|
1514
|
-
//
|
|
1515
|
-
//
|
|
1510
|
+
// Screen-only: these per-slide rules out-specify the print block's
|
|
1511
|
+
// ::slotted reset, so unwrapped they would drag the card-stream
|
|
1512
|
+
// transforms into the paginated print output.
|
|
1516
1513
|
this._mobStyle.textContent = '@media screen {\n' + rules.join('\n') + '\n}';
|
|
1517
1514
|
this._canvas.style.transform = 'none';
|
|
1518
1515
|
this._canvas.style.width = vw + 'px';
|
|
@@ -1520,8 +1517,8 @@
|
|
|
1520
1517
|
this._mobCards = cards;
|
|
1521
1518
|
this._mobScale = s;
|
|
1522
1519
|
|
|
1523
|
-
//
|
|
1524
|
-
//
|
|
1520
|
+
// Badge geometry is fully determined by _mobCards, so badges are laid
|
|
1521
|
+
// out directly — no IntersectionObserver needed.
|
|
1525
1522
|
this._mobBadges.textContent = '';
|
|
1526
1523
|
cards.forEach((c, ordinal) => {
|
|
1527
1524
|
const b = document.createElement('div');
|
|
@@ -1540,7 +1537,8 @@
|
|
|
1540
1537
|
_onMobScroll() {
|
|
1541
1538
|
if (!this.hasAttribute('data-mobile-list') || !this._mobCards || !this._mobCards.length) return;
|
|
1542
1539
|
|
|
1543
|
-
//
|
|
1540
|
+
// Programmatic scrolls count as scrolling too — either source means
|
|
1541
|
+
// "the position is changing", which is what the badge signals.
|
|
1544
1542
|
this.setAttribute('data-mob-scrolling', '');
|
|
1545
1543
|
clearTimeout(this._mobScrollIdleTimer);
|
|
1546
1544
|
this._mobScrollIdleTimer = setTimeout(
|
|
@@ -1552,16 +1550,18 @@
|
|
|
1552
1550
|
const stage = this._canvas.parentElement;
|
|
1553
1551
|
const top = stage.scrollTop;
|
|
1554
1552
|
const moved = this._mobLastTop == null ? Infinity : Math.abs(top - this._mobLastTop);
|
|
1555
|
-
this._mobLastTop = top; //
|
|
1553
|
+
this._mobLastTop = top; // keep tracking even while suppressed
|
|
1556
1554
|
if (this._mobProgUntil && performance.now() < this._mobProgUntil) {
|
|
1557
|
-
//
|
|
1558
|
-
//
|
|
1555
|
+
// Events are still streaming from a programmatic scroll: extend
|
|
1556
|
+
// the window so any animation length is covered. It expires 120ms
|
|
1557
|
+
// after the stream stops; user scrolls then resume inference.
|
|
1559
1558
|
this._mobProgUntil = Math.max(this._mobProgUntil, performance.now() + 120);
|
|
1560
1559
|
return;
|
|
1561
1560
|
}
|
|
1562
|
-
if (moved < 1) return; //
|
|
1563
|
-
//
|
|
1564
|
-
//
|
|
1561
|
+
if (moved < 1) return; // no real displacement (synthetic/terminal event)
|
|
1562
|
+
// A pure viewport-centre rule can never select the first/last page:
|
|
1563
|
+
// 16:9 cards are far shorter than half a portrait viewport, so their
|
|
1564
|
+
// centring targets clamp. Clamp at the extremes, centre in between.
|
|
1565
1565
|
const max = stage.scrollHeight - stage.clientHeight;
|
|
1566
1566
|
let cur;
|
|
1567
1567
|
if (max <= 0 || stage.scrollTop <= 1) {
|
|
@@ -1577,8 +1577,8 @@
|
|
|
1577
1577
|
}
|
|
1578
1578
|
if (cur.i !== this._index) {
|
|
1579
1579
|
this._index = cur.i;
|
|
1580
|
-
//
|
|
1581
|
-
// data-deck-active
|
|
1580
|
+
// Broadcast through the existing core (hash + slide-changed +
|
|
1581
|
+
// slidechange). data-deck-active toggling is visually inert here.
|
|
1582
1582
|
this._applyIndex({ broadcast: true, reason: 'api' });
|
|
1583
1583
|
}
|
|
1584
1584
|
});
|
|
@@ -1589,14 +1589,14 @@
|
|
|
1589
1589
|
if (!card) return;
|
|
1590
1590
|
const stage = this._canvas.parentElement;
|
|
1591
1591
|
const top = Math.max(0, card.y - Math.max(0, (stage.clientHeight - card.h) / 2));
|
|
1592
|
-
//
|
|
1593
|
-
// _index
|
|
1594
|
-
//
|
|
1595
|
-
//
|
|
1592
|
+
// Suppress scroll-position inference while a programmatic scroll is
|
|
1593
|
+
// in flight: goto/deep-link already set _index directly, and the
|
|
1594
|
+
// centring targets of several edge cards clamp to the same scrollTop,
|
|
1595
|
+
// so the position alone cannot tell those pages apart.
|
|
1596
1596
|
this._mobProgUntil = performance.now() + (smooth ? 800 : 200);
|
|
1597
|
-
//
|
|
1598
|
-
//
|
|
1599
|
-
//
|
|
1597
|
+
// Seed the baseline with the target: a scrollTo that lands where the
|
|
1598
|
+
// stage already is fires no scroll event, and a null baseline would
|
|
1599
|
+
// make the next no-op event read as a real first displacement.
|
|
1600
1600
|
this._mobLastTop = top;
|
|
1601
1601
|
stage.scrollTo({ top, behavior: smooth ? 'smooth' : 'auto' });
|
|
1602
1602
|
}
|
|
@@ -1607,9 +1607,9 @@
|
|
|
1607
1607
|
this.removeAttribute('data-windowed');
|
|
1608
1608
|
this._canvas.style.width = this.designWidth + 'px';
|
|
1609
1609
|
this._canvas.style.height = this.designHeight + 'px';
|
|
1610
|
-
//
|
|
1611
|
-
//
|
|
1612
|
-
// CSS
|
|
1610
|
+
// Portrait: the stage is CSS-rotated, so its logical size is the
|
|
1611
|
+
// swapped viewport. Landscape (user physically rotated): no rotation,
|
|
1612
|
+
// plain fit. The CSS branches on the same orientation query.
|
|
1613
1613
|
const portrait = window.innerHeight >= window.innerWidth;
|
|
1614
1614
|
const lw = portrait ? window.innerHeight : window.innerWidth;
|
|
1615
1615
|
const lh = portrait ? window.innerWidth : window.innerHeight;
|
|
@@ -1669,10 +1669,10 @@
|
|
|
1669
1669
|
}
|
|
1670
1670
|
|
|
1671
1671
|
_onResize() {
|
|
1672
|
-
//
|
|
1673
|
-
//
|
|
1674
|
-
//
|
|
1675
|
-
//
|
|
1672
|
+
// Card-stream geometry depends only on viewport width, and mobile
|
|
1673
|
+
// browser URL bars fire height-only resizes as they collapse/expand —
|
|
1674
|
+
// a full refit would rebuild everything and yank the list back to the
|
|
1675
|
+
// current page mid-scroll. Width changes (rotate/split) refit as usual.
|
|
1676
1676
|
if (this.hasAttribute('data-mobile-list')) {
|
|
1677
1677
|
if (this._mobLastVw === window.innerWidth) return;
|
|
1678
1678
|
this._mobLastVw = window.innerWidth;
|
|
@@ -1774,7 +1774,7 @@
|
|
|
1774
1774
|
if (n === this._stage) break;
|
|
1775
1775
|
if (n.matches && n.matches(INTERACTIVE_SEL)) return;
|
|
1776
1776
|
}
|
|
1777
|
-
//
|
|
1777
|
+
// Mobile browse is a scroll list — taps must not navigate.
|
|
1778
1778
|
if (this.hasAttribute('data-mobile-list')) return;
|
|
1779
1779
|
e.preventDefault();
|
|
1780
1780
|
if (this.hasAttribute('data-mobile-present')) {
|
|
@@ -1791,12 +1791,13 @@
|
|
|
1791
1791
|
this._advance(e.clientX < mid ? -1 : 1, 'tap');
|
|
1792
1792
|
}
|
|
1793
1793
|
|
|
1794
|
-
// rotate(90deg)
|
|
1795
|
-
//
|
|
1794
|
+
// After rotate(90deg) the visual-landscape x axis maps onto viewport y
|
|
1795
|
+
// (direction confirmed on device; if prev/next ever read reversed, flip
|
|
1796
|
+
// these comparisons — single point of change).
|
|
1796
1797
|
_mobPresentZone(e) {
|
|
1797
1798
|
const rect = this._stage.getBoundingClientRect();
|
|
1798
|
-
//
|
|
1799
|
-
//
|
|
1799
|
+
// Portrait = stage is rotated, visual x comes from viewport y;
|
|
1800
|
+
// landscape = no rotation, viewport x directly. Same branch as the fit.
|
|
1800
1801
|
const portrait = window.innerHeight >= window.innerWidth;
|
|
1801
1802
|
const x = portrait
|
|
1802
1803
|
? (e.clientY - rect.top) / Math.max(1, rect.height)
|
|
@@ -1810,9 +1811,11 @@
|
|
|
1810
1811
|
this.toggleAttribute('data-mob-ui', on);
|
|
1811
1812
|
clearTimeout(this._mobUiTimer);
|
|
1812
1813
|
if (!on) return;
|
|
1813
|
-
//
|
|
1814
|
-
//
|
|
1815
|
-
//
|
|
1814
|
+
// Rebuild only when opening from hidden: the strip is a static
|
|
1815
|
+
// snapshot outside _liveObserver's refresh pipeline, so reopening is
|
|
1816
|
+
// what absorbs slide edits/reorders/skips. Calls while already open
|
|
1817
|
+
// (thumbnail taps resetting the timer) only re-highlight — rebuilding
|
|
1818
|
+
// on that hot path would clone every slide per tap.
|
|
1816
1819
|
if (!wasOpen || !this._mobUi) {
|
|
1817
1820
|
if (this._mobUi) { this._mobUi.remove(); this._mobUi = null; }
|
|
1818
1821
|
this._buildMobUi();
|
|
@@ -1822,9 +1825,10 @@
|
|
|
1822
1825
|
}
|
|
1823
1826
|
|
|
1824
1827
|
_buildMobUi() {
|
|
1825
|
-
//
|
|
1826
|
-
//
|
|
1827
|
-
//
|
|
1828
|
+
// A pure-viewer host may never send rail-enabled, so the author-CSS
|
|
1829
|
+
// snapshot (normally built by _enableRail) can be missing and the
|
|
1830
|
+
// clones would render unstyled — build it here as a fallback.
|
|
1831
|
+
// _snapshotAuthorCss is re-entrant and generation-guarded.
|
|
1828
1832
|
if (!this._adoptedSheet && this._authorCss == null) this._snapshotAuthorCss();
|
|
1829
1833
|
const ui = document.createElement('div');
|
|
1830
1834
|
ui.className = 'mob-ui export-hidden';
|
|
@@ -1860,19 +1864,19 @@
|
|
|
1860
1864
|
t.addEventListener('click', (ev) => {
|
|
1861
1865
|
ev.stopPropagation();
|
|
1862
1866
|
this._go(e2.i, 'click');
|
|
1863
|
-
this._toggleMobUi(true); //
|
|
1867
|
+
this._toggleMobUi(true); // reset the auto-hide timer, keep the overlay up
|
|
1864
1868
|
});
|
|
1865
1869
|
strip.appendChild(t);
|
|
1866
|
-
//
|
|
1867
|
-
//
|
|
1868
|
-
//
|
|
1869
|
-
//
|
|
1870
|
+
// _materialize provides the full clone pipeline (media neutralised,
|
|
1871
|
+
// canvas snapshots) and mounts a host into entry.frame itself — but
|
|
1872
|
+
// it scales clones by the rail's _thumbScale, so rewrite the
|
|
1873
|
+
// transform for this strip's fixed 132px frames.
|
|
1870
1874
|
const entry = { slide: e2.slide, frame, host: null, clone: null };
|
|
1871
1875
|
this._materialize(entry);
|
|
1872
1876
|
if (entry.clone) entry.clone.style.transform = 'scale(' + (132 / this.designWidth) + ')';
|
|
1873
1877
|
return { el: t, i: e2.i };
|
|
1874
1878
|
});
|
|
1875
|
-
//
|
|
1879
|
+
// Clicks inside the overlay must not bubble into tap navigation.
|
|
1876
1880
|
ui.addEventListener('click', (e) => e.stopPropagation());
|
|
1877
1881
|
ui.append(back, strip);
|
|
1878
1882
|
this._stage.appendChild(ui);
|
|
@@ -1926,13 +1930,14 @@
|
|
|
1926
1930
|
_go(i, reason = 'api') {
|
|
1927
1931
|
if (!this._slides.length) return;
|
|
1928
1932
|
const clamped = Math.max(0, Math.min(this._slides.length - 1, i));
|
|
1929
|
-
//
|
|
1930
|
-
//
|
|
1931
|
-
//
|
|
1933
|
+
// Mobile browse: navigation sets _index directly (desktop parity) and
|
|
1934
|
+
// the scroll is only a visual follow. Inferring the page back from the
|
|
1935
|
+
// scroll position is unreliable here — edge cards' centring targets
|
|
1936
|
+
// clamp to the same scrollTop.
|
|
1932
1937
|
if (this.hasAttribute('data-mobile-list')) {
|
|
1933
1938
|
const changed = clamped !== this._index;
|
|
1934
1939
|
this._index = clamped;
|
|
1935
|
-
//
|
|
1940
|
+
// Same-page goto doesn't rebroadcast (desktop parity) but still recentres.
|
|
1936
1941
|
if (changed) this._applyIndex({ broadcast: true, reason });
|
|
1937
1942
|
this._mobScrollTo(clamped, true);
|
|
1938
1943
|
return;
|