@scenar/embed 0.9.1 → 0.11.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 +20 -0
- package/element.d.ts +7 -0
- package/element.d.ts.map +1 -1
- package/element.js +9 -2
- package/element.js.map +1 -1
- package/embed.global.js +1 -1
- package/mount.d.ts +21 -0
- package/mount.d.ts.map +1 -1
- package/mount.js +61 -0
- package/mount.js.map +1 -1
- package/package.json +2 -2
- package/react.d.ts +9 -0
- package/react.d.ts.map +1 -1
- package/react.js +16 -2
- package/react.js.map +1 -1
- package/src/element.test.ts +10 -0
- package/src/element.ts +9 -2
- package/src/mount.test.ts +100 -0
- package/src/mount.ts +63 -0
- package/src/react.test.tsx +14 -0
- package/src/react.tsx +16 -2
package/README.md
CHANGED
|
@@ -64,6 +64,26 @@ const mount = createEmbedMount(iframeElement, {
|
|
|
64
64
|
`theme` is `auto` (default), `light`, or `dark`. `auto` tracks the host page's
|
|
65
65
|
`dark` class on `<html>` and re-themes the embed when it toggles.
|
|
66
66
|
|
|
67
|
+
## Corners
|
|
68
|
+
|
|
69
|
+
The embed paints edge-to-edge; the host boundary owns the corner radius. Round
|
|
70
|
+
the React component's wrapper (via `className` or `style`) or the
|
|
71
|
+
`<scenar-embed>` element itself (via CSS `border-radius`) — the boundary
|
|
72
|
+
carries `overflow: hidden`, so everything inside clips to your radius:
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
<ScenarEmbed src="…" style={{ borderRadius: 12 }} />
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
```css
|
|
79
|
+
scenar-embed { border-radius: 12px; }
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Do not style the internal iframe. Under iframe-as-screen scaling it is laid
|
|
83
|
+
out at the tour's canonical viewport and transform-scaled to fit, so a radius
|
|
84
|
+
on the iframe would shrink with the scale factor instead of matching your
|
|
85
|
+
layout.
|
|
86
|
+
|
|
67
87
|
## License
|
|
68
88
|
|
|
69
89
|
Apache-2.0
|
package/element.d.ts
CHANGED
|
@@ -18,6 +18,13 @@ declare const HTMLElementBase: typeof HTMLElement;
|
|
|
18
18
|
* - `title` — accessible iframe title (defaults to a generic label).
|
|
19
19
|
* - `theme` — `auto` (default) | `light` | `dark`.
|
|
20
20
|
*
|
|
21
|
+
* Corners: the element is the one clipping surface — it carries
|
|
22
|
+
* `overflow: hidden`, and the host rounds by styling `border-radius` on the
|
|
23
|
+
* element itself. The internal iframe must NOT carry a radius: under
|
|
24
|
+
* iframe-as-screen the mount lays it out at the canonical viewport and scales
|
|
25
|
+
* it down with a transform, so an iframe radius lives in pre-transform space
|
|
26
|
+
* and renders at `radius x scale` — a mismatched, mostly ineffective clip.
|
|
27
|
+
*
|
|
21
28
|
* Events: re-dispatches every embed event as a DOM `CustomEvent` named
|
|
22
29
|
* `scenar:<type>` (e.g. `scenar:ready`, `scenar:completed`) with the event in
|
|
23
30
|
* `detail`, so a vanilla host can listen without touching `postMessage`.
|
package/element.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"element.d.ts","sourceRoot":"","sources":["../src/element.ts"],"names":[],"mappings":"AAOA,mCAAmC;AACnC,eAAO,MAAM,gBAAgB,iBAAiB,CAAC;AAK/C;;;;;GAKG;AACH,QAAA,MAAM,eAAe,EAAE,OAAO,WAGmB,CAAC;AAElD
|
|
1
|
+
{"version":3,"file":"element.d.ts","sourceRoot":"","sources":["../src/element.ts"],"names":[],"mappings":"AAOA,mCAAmC;AACnC,eAAO,MAAM,gBAAgB,iBAAiB,CAAC;AAK/C;;;;;GAKG;AACH,QAAA,MAAM,eAAe,EAAE,OAAO,WAGmB,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,qBAAa,kBAAmB,SAAQ,eAAe;IACrD,MAAM,KAAK,kBAAkB,IAAI,MAAM,EAAE,CAExC;IAED,OAAO,CAAC,MAAM,CAAkC;IAChD,OAAO,CAAC,KAAK,CAA2B;IAExC,iBAAiB,IAAI,IAAI;IAKzB,oBAAoB,IAAI,IAAI;IAK5B,wBAAwB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAU9F,IAAI,IAAI,IAAI;IAIZ,KAAK,IAAI,IAAI;IAIb,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAI1B,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI;IAI9B,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAI/B,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,SAAS;IAKjB,OAAO,CAAC,cAAc;IAItB,uDAAuD;IACvD,OAAO,CAAC,MAAM;IAkBd,2DAA2D;IAC3D,OAAO,CAAC,MAAM;CAaf;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,MAAyB,GAAG,IAAI,CAI1E"}
|
package/element.js
CHANGED
|
@@ -25,6 +25,13 @@ const HTMLElementBase = typeof HTMLElement !== "undefined"
|
|
|
25
25
|
* - `title` — accessible iframe title (defaults to a generic label).
|
|
26
26
|
* - `theme` — `auto` (default) | `light` | `dark`.
|
|
27
27
|
*
|
|
28
|
+
* Corners: the element is the one clipping surface — it carries
|
|
29
|
+
* `overflow: hidden`, and the host rounds by styling `border-radius` on the
|
|
30
|
+
* element itself. The internal iframe must NOT carry a radius: under
|
|
31
|
+
* iframe-as-screen the mount lays it out at the canonical viewport and scales
|
|
32
|
+
* it down with a transform, so an iframe radius lives in pre-transform space
|
|
33
|
+
* and renders at `radius x scale` — a mismatched, mostly ineffective clip.
|
|
34
|
+
*
|
|
28
35
|
* Events: re-dispatches every embed event as a DOM `CustomEvent` named
|
|
29
36
|
* `scenar:<type>` (e.g. `scenar:ready`, `scenar:completed`) with the event in
|
|
30
37
|
* `detail`, so a vanilla host can listen without touching `postMessage`.
|
|
@@ -89,14 +96,14 @@ export class ScenarEmbedElement extends HTMLElementBase {
|
|
|
89
96
|
this.style.display = "block";
|
|
90
97
|
this.style.position = "relative";
|
|
91
98
|
this.style.width = "100%";
|
|
99
|
+
this.style.overflow = "hidden";
|
|
92
100
|
this.setAspectRatio(EMBED_BASE_ASPECT_WIDTH, EMBED_BASE_ASPECT_HEIGHT);
|
|
93
101
|
const iframe = document.createElement("iframe");
|
|
94
102
|
iframe.setAttribute("title", this.titleAttr());
|
|
95
103
|
iframe.setAttribute("loading", "lazy");
|
|
96
104
|
iframe.setAttribute("allow", "autoplay; fullscreen");
|
|
97
105
|
iframe.setAttribute("allowfullscreen", "");
|
|
98
|
-
iframe.style.cssText =
|
|
99
|
-
"position:absolute;inset:0;width:100%;height:100%;border:0;border-radius:inherit";
|
|
106
|
+
iframe.style.cssText = "position:absolute;inset:0;width:100%;height:100%;border:0";
|
|
100
107
|
this.appendChild(iframe);
|
|
101
108
|
this.iframe = iframe;
|
|
102
109
|
}
|
package/element.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"element.js","sourceRoot":"","sources":["../src/element.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAC/D,OAAO,EAEL,wBAAwB,EACxB,uBAAuB,GACxB,MAAM,cAAc,CAAC;AAEtB,mCAAmC;AACnC,MAAM,CAAC,MAAM,gBAAgB,GAAG,cAAc,CAAC;AAE/C,uDAAuD;AACvD,MAAM,aAAa,GAAG,0BAA0B,CAAC;AAEjD;;;;;GAKG;AACH,MAAM,eAAe,GACnB,OAAO,WAAW,KAAK,WAAW;IAChC,CAAC,CAAC,WAAW;IACb,CAAC,CAAE;KAA0C,CAAC;AAElD
|
|
1
|
+
{"version":3,"file":"element.js","sourceRoot":"","sources":["../src/element.ts"],"names":[],"mappings":"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"}
|
package/embed.global.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var ScenarEmbed=function(
|
|
1
|
+
var ScenarEmbed=function(l){"use strict";var W=Object.defineProperty;var G=(l,c,m)=>c in l?W(l,c,{enumerable:!0,configurable:!0,writable:!0,value:m}):l[c]=m;var S=(l,c,m)=>G(l,typeof c!="symbol"?c+"":c,m);const c="scenar-embed";function M(t){return{source:c,v:1,...t}}function T(t){if(typeof t!="object"||t===null)return!1;const n=t;return n.source===c&&n.v===1}function u(t){return typeof t=="number"&&Number.isFinite(t)}function _(t){if(t===void 0)return{};if(typeof t!="object"||t===null)return null;const n=t;return u(n.widthPx)&&n.widthPx>0&&u(n.heightPx)&&n.heightPx>0?{viewport:{widthPx:n.widthPx,heightPx:n.heightPx}}:null}function O(t){if(!T(t))return null;const n=t.type;switch(n){case"started":case"paused":case"completed":case"audioBlocked":return{type:n};case"ready":{if(!u(t.totalSteps)||typeof t.hasNarration!="boolean")return null;const e=_(t.viewport);return e===null?null:{type:n,totalSteps:t.totalSteps,hasNarration:t.hasNarration,...e}}case"resize":return u(t.widthPx)&&u(t.heightPx)?{type:n,widthPx:t.widthPx,heightPx:t.heightPx}:null;case"stepchange":return u(t.stepIndex)&&u(t.totalSteps)?{type:n,stepIndex:t.stepIndex,totalSteps:t.totalSteps}:null;case"progress":return u(t.stepIndex)&&u(t.totalSteps)&&u(t.fraction)?{type:n,stepIndex:t.stepIndex,totalSteps:t.totalSteps,fraction:t.fraction}:null;case"error":return typeof t.message=="string"?{type:n,message:t.message}:null;default:return null}}function k(t,n={}){const{iframe:e,origin:s}=t,{onEvent:o}=n,d=typeof window<"u",h=r=>{var a;(a=e.contentWindow)==null||a.postMessage(M(r),s)},E=r=>{if(r.source!==e.contentWindow||r.origin!==s)return;const a=O(r.data);a&&(o==null||o(a))};return d&&window.addEventListener("message",E),{play:()=>h({type:"play"}),pause:()=>h({type:"pause"}),seek:r=>h({type:"seek",timeMs:r}),setMuted:r=>h({type:"setMuted",muted:r}),setVolume:r=>h({type:"setVolume",volume:r}),setHostScale:r=>h({type:"setHostScale",scale:r}),prefetch:()=>h({type:"prefetch"}),destroy:()=>{h({type:"destroy"}),d&&window.removeEventListener("message",E)}}}const L=896,I=480;function v(t,n){return t==="light"?"light":t==="dark"||n?"dark":"light"}function B(t,n){const e=new URL(t);return e.searchParams.set("theme",n),e.toString()}function N(t){return new URL(t).origin}function V(){return typeof document<"u"&&document.documentElement.classList.contains("dark")}function D(t,n){const{src:e,theme:s="auto",onAspectRatio:o,onEvent:d}=n,h=N(e);let E=null;const r=()=>{const i=v(s,V());i!==E&&(E=i,t.src=B(e,i))};let a=null,p,A=null;const R=()=>{const i=t.parentElement;if(!a||!i)return;const{widthPx:w,heightPx:C}=a,g=i.getBoundingClientRect();if(g.width<=0)return;const f=Math.min(g.width/w,1),U=(g.width-w*f)/2,j=(g.height-C*f)/2;t.style.width=`${w}px`,t.style.height=`${C}px`,t.style.transformOrigin="0 0",t.style.transform=`translate(${U}px, ${j}px) scale(${f})`,f!==A&&(A=f,b.setHostScale(f))},F=i=>{a=i,R(),!p&&typeof ResizeObserver<"u"&&t.parentElement&&(p=new ResizeObserver(R),p.observe(t.parentElement))},b=k({iframe:t,origin:h},{onEvent:i=>{i.type==="resize"&&(o==null||o({widthPx:i.widthPx,heightPx:i.heightPx})),i.type==="ready"&&i.viewport&&F(i.viewport),d==null||d(i)}});let y;return s==="auto"&&typeof MutationObserver<"u"&&typeof document<"u"&&(y=new MutationObserver(r),y.observe(document.documentElement,{attributes:!0,attributeFilter:["class"]})),r(),{controller:b,destroy(){y==null||y.disconnect(),p==null||p.disconnect(),b.destroy()}}}const H="scenar-embed",$="Interactive product tour",z=typeof HTMLElement<"u"?HTMLElement:class{};class x extends z{constructor(){super(...arguments);S(this,"iframe",null);S(this,"mount",null)}static get observedAttributes(){return["src","title","theme"]}connectedCallback(){this.iframe||this.render(),this.attach()}disconnectedCallback(){var e;(e=this.mount)==null||e.destroy(),this.mount=null}attributeChangedCallback(e,s,o){if(s!==o){if(e==="title"){this.iframe&&this.iframe.setAttribute("title",this.titleAttr());return}this.isConnected&&this.attach()}}play(){var e;(e=this.mount)==null||e.controller.play()}pause(){var e;(e=this.mount)==null||e.controller.pause()}seek(e){var s;(s=this.mount)==null||s.controller.seek(e)}setMuted(e){var s;(s=this.mount)==null||s.controller.setMuted(e)}setVolume(e){var s;(s=this.mount)==null||s.controller.setVolume(e)}titleAttr(){return this.getAttribute("title")??$}themeAttr(){const e=this.getAttribute("theme");return e==="light"||e==="dark"?e:"auto"}setAspectRatio(e,s){this.style.aspectRatio=`${e} / ${s}`}render(){this.style.display="block",this.style.position="relative",this.style.width="100%",this.style.overflow="hidden",this.setAspectRatio(L,I);const e=document.createElement("iframe");e.setAttribute("title",this.titleAttr()),e.setAttribute("loading","lazy"),e.setAttribute("allow","autoplay; fullscreen"),e.setAttribute("allowfullscreen",""),e.style.cssText="position:absolute;inset:0;width:100%;height:100%;border:0",this.appendChild(e),this.iframe=e}attach(){var s;const e=this.getAttribute("src");!e||!this.iframe||((s=this.mount)==null||s.destroy(),this.mount=D(this.iframe,{src:e,theme:this.themeAttr(),onAspectRatio:({widthPx:o,heightPx:d})=>this.setAspectRatio(o,d),onEvent:o=>this.dispatchEvent(new CustomEvent(`scenar:${o.type}`,{detail:o}))}))}}function P(t=H){typeof customElements>"u"||customElements.get(t)||customElements.define(t,x)}return P(),l.ScenarEmbedElement=x,l.defineScenarEmbed=P,Object.defineProperty(l,Symbol.toStringTag,{value:"Module"}),l}({});
|
package/mount.d.ts
CHANGED
|
@@ -33,6 +33,27 @@ export interface EmbedMount {
|
|
|
33
33
|
* + own layout, then hand it here — so neither re-implements the bridge, exactly
|
|
34
34
|
* as {@link createEmbedHostController} is shared by the console and this loader.
|
|
35
35
|
*
|
|
36
|
+
* Iframe-as-screen (scenar-cloud DD-008's principle, applied to the frame):
|
|
37
|
+
* when the bundle's `ready` event carries its canonical viewport, the mount
|
|
38
|
+
* lays the iframe out at exactly that CSS-pixel size and scales it to the
|
|
39
|
+
* wrapper with a `transform` — instead of letting the iframe track the wrapper
|
|
40
|
+
* and scaling *inside* the document. The embedded document's viewport is then
|
|
41
|
+
* the canonical viewport, so its media queries resolve as they would in a
|
|
42
|
+
* real browser window of that size; the one scale factor per frame lives out
|
|
43
|
+
* here, at the true boundary. `transform` and not CSS `zoom` on purpose:
|
|
44
|
+
* transform is paint-level and never propagates into the child document, so
|
|
45
|
+
* the inner viewport stays canonical. The mount reports the factor back over
|
|
46
|
+
* `setHostScale` so the player's chrome layer can counter-scale its controls
|
|
47
|
+
* to native pixel size. Bundles packed before the viewport field keep the
|
|
48
|
+
* fit-inside-the-iframe behavior — both directions of version skew degrade
|
|
49
|
+
* to exactly the pre-mode rendering.
|
|
50
|
+
*
|
|
51
|
+
* Layout contract with the adapters: the iframe fills a positioned wrapper
|
|
52
|
+
* (`position: relative/absolute` box whose aspect ratio tracks the reported
|
|
53
|
+
* embed size). The mount measures that wrapper — `iframe.parentElement` — to
|
|
54
|
+
* derive the scale, and owns the iframe's `width`/`height`/`transform` once
|
|
55
|
+
* (and only once) a viewport-carrying `ready` arrives.
|
|
56
|
+
*
|
|
36
57
|
* The caller owns the iframe element (and removes it for full teardown); this
|
|
37
58
|
* function owns only the listeners it adds, all released by {@link EmbedMount.destroy}.
|
|
38
59
|
*/
|
package/mount.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mount.d.ts","sourceRoot":"","sources":["../src/mount.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,gBAAgB,EACrB,KAAK,yBAAyB,
|
|
1
|
+
{"version":3,"file":"mount.d.ts","sourceRoot":"","sources":["../src/mount.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,gBAAgB,EACrB,KAAK,yBAAyB,EAG/B,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,KAAK,gBAAgB,EAItB,MAAM,cAAc,CAAC;AAEtB,yEAAyE;AACzE,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,4CAA4C;AAC5C,MAAM,WAAW,iBAAiB;IAChC,yEAAyE;IACzE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,uCAAuC;IACvC,QAAQ,CAAC,KAAK,CAAC,EAAE,gBAAgB,CAAC;IAClC,8EAA8E;IAC9E,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,IAAI,CAAC;IAC3D,gEAAgE;IAChE,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,IAAI,CAAC;CACtD;AAED,2EAA2E;AAC3E,MAAM,WAAW,UAAU;IACzB,iFAAiF;IACjF,QAAQ,CAAC,UAAU,EAAE,yBAAyB,CAAC;IAC/C,2EAA2E;IAC3E,OAAO,IAAI,IAAI,CAAC;CACjB;AAUD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,iBAAiB,EACzB,OAAO,EAAE,iBAAiB,GACzB,UAAU,CA2FZ"}
|
package/mount.js
CHANGED
|
@@ -15,6 +15,27 @@ function hostPrefersDark() {
|
|
|
15
15
|
* + own layout, then hand it here — so neither re-implements the bridge, exactly
|
|
16
16
|
* as {@link createEmbedHostController} is shared by the console and this loader.
|
|
17
17
|
*
|
|
18
|
+
* Iframe-as-screen (scenar-cloud DD-008's principle, applied to the frame):
|
|
19
|
+
* when the bundle's `ready` event carries its canonical viewport, the mount
|
|
20
|
+
* lays the iframe out at exactly that CSS-pixel size and scales it to the
|
|
21
|
+
* wrapper with a `transform` — instead of letting the iframe track the wrapper
|
|
22
|
+
* and scaling *inside* the document. The embedded document's viewport is then
|
|
23
|
+
* the canonical viewport, so its media queries resolve as they would in a
|
|
24
|
+
* real browser window of that size; the one scale factor per frame lives out
|
|
25
|
+
* here, at the true boundary. `transform` and not CSS `zoom` on purpose:
|
|
26
|
+
* transform is paint-level and never propagates into the child document, so
|
|
27
|
+
* the inner viewport stays canonical. The mount reports the factor back over
|
|
28
|
+
* `setHostScale` so the player's chrome layer can counter-scale its controls
|
|
29
|
+
* to native pixel size. Bundles packed before the viewport field keep the
|
|
30
|
+
* fit-inside-the-iframe behavior — both directions of version skew degrade
|
|
31
|
+
* to exactly the pre-mode rendering.
|
|
32
|
+
*
|
|
33
|
+
* Layout contract with the adapters: the iframe fills a positioned wrapper
|
|
34
|
+
* (`position: relative/absolute` box whose aspect ratio tracks the reported
|
|
35
|
+
* embed size). The mount measures that wrapper — `iframe.parentElement` — to
|
|
36
|
+
* derive the scale, and owns the iframe's `width`/`height`/`transform` once
|
|
37
|
+
* (and only once) a viewport-carrying `ready` arrives.
|
|
38
|
+
*
|
|
18
39
|
* The caller owns the iframe element (and removes it for full teardown); this
|
|
19
40
|
* function owns only the listeners it adds, all released by {@link EmbedMount.destroy}.
|
|
20
41
|
*/
|
|
@@ -31,11 +52,50 @@ export function createEmbedMount(iframe, options) {
|
|
|
31
52
|
appliedTheme = resolved;
|
|
32
53
|
iframe.src = applyThemeToSrc(src, resolved);
|
|
33
54
|
};
|
|
55
|
+
// Iframe-as-screen state: set by the first viewport-carrying `ready`, then
|
|
56
|
+
// kept in sync with the wrapper by a ResizeObserver. A theme flip reloads
|
|
57
|
+
// the iframe and replays `ready`, which simply re-applies.
|
|
58
|
+
let canonicalViewport = null;
|
|
59
|
+
let wrapperObserver;
|
|
60
|
+
let lastPostedScale = null;
|
|
61
|
+
const applyViewportScale = () => {
|
|
62
|
+
const wrapper = iframe.parentElement;
|
|
63
|
+
if (!canonicalViewport || !wrapper)
|
|
64
|
+
return;
|
|
65
|
+
const { widthPx, heightPx } = canonicalViewport;
|
|
66
|
+
const rect = wrapper.getBoundingClientRect();
|
|
67
|
+
if (rect.width <= 0)
|
|
68
|
+
return;
|
|
69
|
+
// Cap at 1 like the in-document viewport does: upscaling past native
|
|
70
|
+
// renders soft. Wider-than-canonical wrappers center the frame instead.
|
|
71
|
+
const scale = Math.min(rect.width / widthPx, 1);
|
|
72
|
+
const offsetX = (rect.width - widthPx * scale) / 2;
|
|
73
|
+
const offsetY = (rect.height - heightPx * scale) / 2;
|
|
74
|
+
iframe.style.width = `${widthPx}px`;
|
|
75
|
+
iframe.style.height = `${heightPx}px`;
|
|
76
|
+
iframe.style.transformOrigin = "0 0";
|
|
77
|
+
iframe.style.transform = `translate(${offsetX}px, ${offsetY}px) scale(${scale})`;
|
|
78
|
+
if (scale !== lastPostedScale) {
|
|
79
|
+
lastPostedScale = scale;
|
|
80
|
+
controller.setHostScale(scale);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
const adoptViewport = (viewport) => {
|
|
84
|
+
canonicalViewport = viewport;
|
|
85
|
+
applyViewportScale();
|
|
86
|
+
if (!wrapperObserver && typeof ResizeObserver !== "undefined" && iframe.parentElement) {
|
|
87
|
+
wrapperObserver = new ResizeObserver(applyViewportScale);
|
|
88
|
+
wrapperObserver.observe(iframe.parentElement);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
34
91
|
const controller = createEmbedHostController({ iframe, origin }, {
|
|
35
92
|
onEvent: (event) => {
|
|
36
93
|
if (event.type === "resize") {
|
|
37
94
|
onAspectRatio?.({ widthPx: event.widthPx, heightPx: event.heightPx });
|
|
38
95
|
}
|
|
96
|
+
if (event.type === "ready" && event.viewport) {
|
|
97
|
+
adoptViewport(event.viewport);
|
|
98
|
+
}
|
|
39
99
|
onEvent?.(event);
|
|
40
100
|
},
|
|
41
101
|
});
|
|
@@ -56,6 +116,7 @@ export function createEmbedMount(iframe, options) {
|
|
|
56
116
|
controller,
|
|
57
117
|
destroy() {
|
|
58
118
|
observer?.disconnect();
|
|
119
|
+
wrapperObserver?.disconnect();
|
|
59
120
|
controller.destroy();
|
|
60
121
|
},
|
|
61
122
|
};
|
package/mount.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mount.js","sourceRoot":"","sources":["../src/mount.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"mount.js","sourceRoot":"","sources":["../src/mount.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scenar/embed",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Drop-in host-side embedding for Scenar tours — a framework-agnostic <scenar-embed> web component and a matching React wrapper.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"./loader": "./embed.global.js"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@scenar/core": "0.
|
|
47
|
+
"@scenar/core": "0.11.0"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
50
|
"react": ">=18.0.0",
|
package/react.d.ts
CHANGED
|
@@ -30,6 +30,15 @@ export interface ScenarEmbedProps extends EmbedSource {
|
|
|
30
30
|
* SSR-safe: the iframe renders without a `src` on the server and on the first
|
|
31
31
|
* client render, so hydration never mismatches; the effect then assigns the
|
|
32
32
|
* themed `src` and adopts the embed's reported aspect ratio.
|
|
33
|
+
*
|
|
34
|
+
* Corners: the wrapper is the one clipping surface — it carries
|
|
35
|
+
* `overflow: hidden`, and the host supplies any corner radius via `className`
|
|
36
|
+
* or `style`. The iframe itself must NOT carry a `border-radius`: under
|
|
37
|
+
* iframe-as-screen the mount lays the iframe out at the canonical viewport and
|
|
38
|
+
* scales it down with a transform, so a radius on the iframe lives in
|
|
39
|
+
* pre-transform space and renders at `radius x scale` — a mismatched, mostly
|
|
40
|
+
* ineffective clip. The embed paints edge-to-edge and the host boundary owns
|
|
41
|
+
* the corners.
|
|
33
42
|
*/
|
|
34
43
|
export declare const ScenarEmbed: import("react").ForwardRefExoticComponent<ScenarEmbedProps & import("react").RefAttributes<ScenarEmbedHandle>>;
|
|
35
44
|
//# sourceMappingURL=react.d.ts.map
|
package/react.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,aAAa,EAMnB,MAAM,OAAO,CAAC;AACf,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,gBAAgB,EAItB,MAAM,cAAc,CAAC;AAKtB,sEAAsE;AACtE,MAAM,WAAW,iBAAiB;IAChC,IAAI,IAAI,IAAI,CAAC;IACb,KAAK,IAAI,IAAI,CAAC;IACd,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IAC/B,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AAED,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACnD,6DAA6D;IAC7D,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,sEAAsE;IACtE,QAAQ,CAAC,KAAK,CAAC,EAAE,gBAAgB,CAAC;IAClC,4EAA4E;IAC5E,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,IAAI,CAAC;IACrD,+CAA+C;IAC/C,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,uDAAuD;IACvD,QAAQ,CAAC,KAAK,CAAC,EAAE,aAAa,CAAC;CAChC;AAED
|
|
1
|
+
{"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,aAAa,EAMnB,MAAM,OAAO,CAAC;AACf,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,gBAAgB,EAItB,MAAM,cAAc,CAAC;AAKtB,sEAAsE;AACtE,MAAM,WAAW,iBAAiB;IAChC,IAAI,IAAI,IAAI,CAAC;IACb,KAAK,IAAI,IAAI,CAAC;IACd,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IAC/B,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AAED,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACnD,6DAA6D;IAC7D,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,sEAAsE;IACtE,QAAQ,CAAC,KAAK,CAAC,EAAE,gBAAgB,CAAC;IAClC,4EAA4E;IAC5E,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,IAAI,CAAC;IACrD,+CAA+C;IAC/C,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,uDAAuD;IACvD,QAAQ,CAAC,KAAK,CAAC,EAAE,aAAa,CAAC;CAChC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,WAAW,gHAsEvB,CAAC"}
|
package/react.js
CHANGED
|
@@ -14,6 +14,15 @@ const DEFAULT_TITLE = "Interactive product tour";
|
|
|
14
14
|
* SSR-safe: the iframe renders without a `src` on the server and on the first
|
|
15
15
|
* client render, so hydration never mismatches; the effect then assigns the
|
|
16
16
|
* themed `src` and adopts the embed's reported aspect ratio.
|
|
17
|
+
*
|
|
18
|
+
* Corners: the wrapper is the one clipping surface — it carries
|
|
19
|
+
* `overflow: hidden`, and the host supplies any corner radius via `className`
|
|
20
|
+
* or `style`. The iframe itself must NOT carry a `border-radius`: under
|
|
21
|
+
* iframe-as-screen the mount lays the iframe out at the canonical viewport and
|
|
22
|
+
* scales it down with a transform, so a radius on the iframe lives in
|
|
23
|
+
* pre-transform space and renders at `radius x scale` — a mismatched, mostly
|
|
24
|
+
* ineffective clip. The embed paints edge-to-edge and the host boundary owns
|
|
25
|
+
* the corners.
|
|
17
26
|
*/
|
|
18
27
|
export const ScenarEmbed = forwardRef(function ScenarEmbed({ src, id, base, title, theme = "auto", onEvent, className, style }, ref) {
|
|
19
28
|
const iframeRef = useRef(null);
|
|
@@ -46,13 +55,18 @@ export const ScenarEmbed = forwardRef(function ScenarEmbed({ src, id, base, titl
|
|
|
46
55
|
setMuted: (muted) => mountRef.current?.controller.setMuted(muted),
|
|
47
56
|
setVolume: (volume) => mountRef.current?.controller.setVolume(volume),
|
|
48
57
|
}), []);
|
|
49
|
-
return (_jsx("div", { className: className, style: {
|
|
58
|
+
return (_jsx("div", { className: className, style: {
|
|
59
|
+
position: "relative",
|
|
60
|
+
width: "100%",
|
|
61
|
+
aspectRatio: ratio,
|
|
62
|
+
overflow: "hidden",
|
|
63
|
+
...style,
|
|
64
|
+
}, children: _jsx("iframe", { ref: iframeRef, title: title ?? DEFAULT_TITLE, loading: "lazy", allow: "autoplay; fullscreen", allowFullScreen: true, style: {
|
|
50
65
|
position: "absolute",
|
|
51
66
|
inset: 0,
|
|
52
67
|
width: "100%",
|
|
53
68
|
height: "100%",
|
|
54
69
|
border: 0,
|
|
55
|
-
borderRadius: "inherit",
|
|
56
70
|
} }) }));
|
|
57
71
|
});
|
|
58
72
|
//# sourceMappingURL=react.js.map
|
package/react.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"react.js","sourceRoot":"","sources":["../src/react.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAEL,UAAU,EACV,SAAS,EACT,mBAAmB,EACnB,MAAM,EACN,QAAQ,GACT,MAAM,OAAO,CAAC;AAEf,OAAO,EAAmB,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAC/D,OAAO,EAGL,wBAAwB,EACxB,uBAAuB,EACvB,eAAe,GAChB,MAAM,cAAc,CAAC;AAEtB,yDAAyD;AACzD,MAAM,aAAa,GAAG,0BAA0B,CAAC;AAwBjD
|
|
1
|
+
{"version":3,"file":"react.js","sourceRoot":"","sources":["../src/react.tsx"],"names":[],"mappings":"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"}
|
package/src/element.test.ts
CHANGED
|
@@ -65,6 +65,16 @@ describe("<scenar-embed>", () => {
|
|
|
65
65
|
expect(el.style.aspectRatio).toBe("896 / 480");
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
+
it("clips its content so a host-applied border-radius rounds the embed", () => {
|
|
69
|
+
const { el, iframe } = mountElement({ src: SRC });
|
|
70
|
+
// The element is the one clipping surface: the host rounds the element,
|
|
71
|
+
// and everything inside — including the transform-scaled iframe — clips
|
|
72
|
+
// to it. The iframe itself must carry no radius (it would live in
|
|
73
|
+
// pre-transform space and render at radius x scale).
|
|
74
|
+
expect(el.style.overflow).toBe("hidden");
|
|
75
|
+
expect(iframe.style.borderRadius).toBe("");
|
|
76
|
+
});
|
|
77
|
+
|
|
68
78
|
it("falls back to a generic accessible title", () => {
|
|
69
79
|
const { iframe } = mountElement({ src: SRC });
|
|
70
80
|
expect(iframe.getAttribute("title")).toBe("Interactive product tour");
|
package/src/element.ts
CHANGED
|
@@ -33,6 +33,13 @@ const HTMLElementBase: typeof HTMLElement =
|
|
|
33
33
|
* - `title` — accessible iframe title (defaults to a generic label).
|
|
34
34
|
* - `theme` — `auto` (default) | `light` | `dark`.
|
|
35
35
|
*
|
|
36
|
+
* Corners: the element is the one clipping surface — it carries
|
|
37
|
+
* `overflow: hidden`, and the host rounds by styling `border-radius` on the
|
|
38
|
+
* element itself. The internal iframe must NOT carry a radius: under
|
|
39
|
+
* iframe-as-screen the mount lays it out at the canonical viewport and scales
|
|
40
|
+
* it down with a transform, so an iframe radius lives in pre-transform space
|
|
41
|
+
* and renders at `radius x scale` — a mismatched, mostly ineffective clip.
|
|
42
|
+
*
|
|
36
43
|
* Events: re-dispatches every embed event as a DOM `CustomEvent` named
|
|
37
44
|
* `scenar:<type>` (e.g. `scenar:ready`, `scenar:completed`) with the event in
|
|
38
45
|
* `detail`, so a vanilla host can listen without touching `postMessage`.
|
|
@@ -106,6 +113,7 @@ export class ScenarEmbedElement extends HTMLElementBase {
|
|
|
106
113
|
this.style.display = "block";
|
|
107
114
|
this.style.position = "relative";
|
|
108
115
|
this.style.width = "100%";
|
|
116
|
+
this.style.overflow = "hidden";
|
|
109
117
|
this.setAspectRatio(EMBED_BASE_ASPECT_WIDTH, EMBED_BASE_ASPECT_HEIGHT);
|
|
110
118
|
|
|
111
119
|
const iframe = document.createElement("iframe");
|
|
@@ -113,8 +121,7 @@ export class ScenarEmbedElement extends HTMLElementBase {
|
|
|
113
121
|
iframe.setAttribute("loading", "lazy");
|
|
114
122
|
iframe.setAttribute("allow", "autoplay; fullscreen");
|
|
115
123
|
iframe.setAttribute("allowfullscreen", "");
|
|
116
|
-
iframe.style.cssText =
|
|
117
|
-
"position:absolute;inset:0;width:100%;height:100%;border:0;border-radius:inherit";
|
|
124
|
+
iframe.style.cssText = "position:absolute;inset:0;width:100%;height:100%;border:0";
|
|
118
125
|
|
|
119
126
|
this.appendChild(iframe);
|
|
120
127
|
this.iframe = iframe;
|
package/src/mount.test.ts
CHANGED
|
@@ -178,6 +178,106 @@ describe("createEmbedMount — ignores messages that do not match the contract",
|
|
|
178
178
|
});
|
|
179
179
|
});
|
|
180
180
|
|
|
181
|
+
/** Build a framed ready message (protocol v1), optionally carrying a viewport. */
|
|
182
|
+
function readyMessage(viewport?: { widthPx: number; heightPx: number }) {
|
|
183
|
+
return {
|
|
184
|
+
source: "scenar-embed",
|
|
185
|
+
v: 1,
|
|
186
|
+
type: "ready",
|
|
187
|
+
totalSteps: 3,
|
|
188
|
+
hasNarration: false,
|
|
189
|
+
...(viewport ? { viewport } : {}),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Give an iframe a positioned wrapper of a fixed layout size (the adapter
|
|
195
|
+
* contract the mount scales against). jsdom computes no layout, so the
|
|
196
|
+
* wrapper's rect is stubbed.
|
|
197
|
+
*/
|
|
198
|
+
function wrapInSizedWrapper(
|
|
199
|
+
iframe: HTMLIFrameElement,
|
|
200
|
+
width: number,
|
|
201
|
+
height: number,
|
|
202
|
+
): HTMLDivElement {
|
|
203
|
+
const wrapper = document.createElement("div");
|
|
204
|
+
wrapper.style.position = "relative";
|
|
205
|
+
vi.spyOn(wrapper, "getBoundingClientRect").mockReturnValue({
|
|
206
|
+
width,
|
|
207
|
+
height,
|
|
208
|
+
top: 0,
|
|
209
|
+
left: 0,
|
|
210
|
+
right: width,
|
|
211
|
+
bottom: height,
|
|
212
|
+
x: 0,
|
|
213
|
+
y: 0,
|
|
214
|
+
toJSON: () => ({}),
|
|
215
|
+
} as DOMRect);
|
|
216
|
+
wrapper.appendChild(iframe);
|
|
217
|
+
document.body.appendChild(wrapper);
|
|
218
|
+
return wrapper;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
describe("createEmbedMount — iframe-as-screen (ready carries a viewport)", () => {
|
|
222
|
+
it("lays the iframe out at the canonical size, scaled to fit the wrapper", () => {
|
|
223
|
+
const { iframe, frame } = makeIframe();
|
|
224
|
+
const send = vi.fn();
|
|
225
|
+
(frame as { postMessage: unknown }).postMessage = send;
|
|
226
|
+
// 896-wide wrapper for a 1440x900 canonical viewport: scale 896/1440.
|
|
227
|
+
wrapInSizedWrapper(iframe, 896, 560);
|
|
228
|
+
track(createEmbedMount(iframe, { src: SRC }));
|
|
229
|
+
|
|
230
|
+
post(readyMessage({ widthPx: 1440, heightPx: 900 }), { source: frame });
|
|
231
|
+
|
|
232
|
+
const scale = 896 / 1440;
|
|
233
|
+
expect(iframe.style.width).toBe("1440px");
|
|
234
|
+
expect(iframe.style.height).toBe("900px");
|
|
235
|
+
expect(iframe.style.transformOrigin).toBe("0 0");
|
|
236
|
+
expect(iframe.style.transform).toBe(`translate(0px, 0px) scale(${scale})`);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("reports the applied scale back to the embed over setHostScale", () => {
|
|
240
|
+
const { iframe, frame } = makeIframe();
|
|
241
|
+
const send = vi.fn();
|
|
242
|
+
(frame as { postMessage: unknown }).postMessage = send;
|
|
243
|
+
wrapInSizedWrapper(iframe, 720, 450);
|
|
244
|
+
track(createEmbedMount(iframe, { src: SRC }));
|
|
245
|
+
|
|
246
|
+
post(readyMessage({ widthPx: 1440, heightPx: 900 }), { source: frame });
|
|
247
|
+
|
|
248
|
+
expect(send).toHaveBeenCalledWith(
|
|
249
|
+
expect.objectContaining({ type: "setHostScale", scale: 0.5 }),
|
|
250
|
+
ORIGIN,
|
|
251
|
+
);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("caps the scale at 1 and centers the frame in a wider-than-canonical wrapper", () => {
|
|
255
|
+
const { iframe, frame } = makeIframe();
|
|
256
|
+
(frame as { postMessage: unknown }).postMessage = vi.fn();
|
|
257
|
+
wrapInSizedWrapper(iframe, 2000, 1100);
|
|
258
|
+
track(createEmbedMount(iframe, { src: SRC }));
|
|
259
|
+
|
|
260
|
+
post(readyMessage({ widthPx: 1440, heightPx: 900 }), { source: frame });
|
|
261
|
+
|
|
262
|
+
// scale 1; centered: x = (2000 - 1440) / 2 = 280, y = (1100 - 900) / 2 = 100.
|
|
263
|
+
expect(iframe.style.transform).toBe("translate(280px, 100px) scale(1)");
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("leaves the iframe's fill layout untouched for pre-viewport bundles", () => {
|
|
267
|
+
const { iframe, frame } = makeIframe();
|
|
268
|
+
(frame as { postMessage: unknown }).postMessage = vi.fn();
|
|
269
|
+
wrapInSizedWrapper(iframe, 896, 560);
|
|
270
|
+
track(createEmbedMount(iframe, { src: SRC }));
|
|
271
|
+
|
|
272
|
+
post(readyMessage(), { source: frame });
|
|
273
|
+
|
|
274
|
+
// The version-skew contract: an old bundle (no viewport on ready) keeps
|
|
275
|
+
// exactly the fit-inside-the-iframe behavior — no size, no transform.
|
|
276
|
+
expect(iframe.style.width).toBe("");
|
|
277
|
+
expect(iframe.style.transform).toBe("");
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
181
281
|
describe("createEmbedMount — teardown", () => {
|
|
182
282
|
it("stops reacting to resizes after destroy", () => {
|
|
183
283
|
const ratios: EmbedAspectRatio[] = [];
|
package/src/mount.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type ScenarEmbedEvent,
|
|
3
3
|
type ScenarEmbedHostController,
|
|
4
|
+
type ScenarEmbedViewport,
|
|
4
5
|
createEmbedHostController,
|
|
5
6
|
} from "@scenar/core";
|
|
6
7
|
import {
|
|
@@ -54,6 +55,27 @@ function hostPrefersDark(): boolean {
|
|
|
54
55
|
* + own layout, then hand it here — so neither re-implements the bridge, exactly
|
|
55
56
|
* as {@link createEmbedHostController} is shared by the console and this loader.
|
|
56
57
|
*
|
|
58
|
+
* Iframe-as-screen (scenar-cloud DD-008's principle, applied to the frame):
|
|
59
|
+
* when the bundle's `ready` event carries its canonical viewport, the mount
|
|
60
|
+
* lays the iframe out at exactly that CSS-pixel size and scales it to the
|
|
61
|
+
* wrapper with a `transform` — instead of letting the iframe track the wrapper
|
|
62
|
+
* and scaling *inside* the document. The embedded document's viewport is then
|
|
63
|
+
* the canonical viewport, so its media queries resolve as they would in a
|
|
64
|
+
* real browser window of that size; the one scale factor per frame lives out
|
|
65
|
+
* here, at the true boundary. `transform` and not CSS `zoom` on purpose:
|
|
66
|
+
* transform is paint-level and never propagates into the child document, so
|
|
67
|
+
* the inner viewport stays canonical. The mount reports the factor back over
|
|
68
|
+
* `setHostScale` so the player's chrome layer can counter-scale its controls
|
|
69
|
+
* to native pixel size. Bundles packed before the viewport field keep the
|
|
70
|
+
* fit-inside-the-iframe behavior — both directions of version skew degrade
|
|
71
|
+
* to exactly the pre-mode rendering.
|
|
72
|
+
*
|
|
73
|
+
* Layout contract with the adapters: the iframe fills a positioned wrapper
|
|
74
|
+
* (`position: relative/absolute` box whose aspect ratio tracks the reported
|
|
75
|
+
* embed size). The mount measures that wrapper — `iframe.parentElement` — to
|
|
76
|
+
* derive the scale, and owns the iframe's `width`/`height`/`transform` once
|
|
77
|
+
* (and only once) a viewport-carrying `ready` arrives.
|
|
78
|
+
*
|
|
57
79
|
* The caller owns the iframe element (and removes it for full teardown); this
|
|
58
80
|
* function owns only the listeners it adds, all released by {@link EmbedMount.destroy}.
|
|
59
81
|
*/
|
|
@@ -74,6 +96,43 @@ export function createEmbedMount(
|
|
|
74
96
|
iframe.src = applyThemeToSrc(src, resolved);
|
|
75
97
|
};
|
|
76
98
|
|
|
99
|
+
// Iframe-as-screen state: set by the first viewport-carrying `ready`, then
|
|
100
|
+
// kept in sync with the wrapper by a ResizeObserver. A theme flip reloads
|
|
101
|
+
// the iframe and replays `ready`, which simply re-applies.
|
|
102
|
+
let canonicalViewport: ScenarEmbedViewport | null = null;
|
|
103
|
+
let wrapperObserver: ResizeObserver | undefined;
|
|
104
|
+
let lastPostedScale: number | null = null;
|
|
105
|
+
|
|
106
|
+
const applyViewportScale = (): void => {
|
|
107
|
+
const wrapper = iframe.parentElement;
|
|
108
|
+
if (!canonicalViewport || !wrapper) return;
|
|
109
|
+
const { widthPx, heightPx } = canonicalViewport;
|
|
110
|
+
const rect = wrapper.getBoundingClientRect();
|
|
111
|
+
if (rect.width <= 0) return;
|
|
112
|
+
// Cap at 1 like the in-document viewport does: upscaling past native
|
|
113
|
+
// renders soft. Wider-than-canonical wrappers center the frame instead.
|
|
114
|
+
const scale = Math.min(rect.width / widthPx, 1);
|
|
115
|
+
const offsetX = (rect.width - widthPx * scale) / 2;
|
|
116
|
+
const offsetY = (rect.height - heightPx * scale) / 2;
|
|
117
|
+
iframe.style.width = `${widthPx}px`;
|
|
118
|
+
iframe.style.height = `${heightPx}px`;
|
|
119
|
+
iframe.style.transformOrigin = "0 0";
|
|
120
|
+
iframe.style.transform = `translate(${offsetX}px, ${offsetY}px) scale(${scale})`;
|
|
121
|
+
if (scale !== lastPostedScale) {
|
|
122
|
+
lastPostedScale = scale;
|
|
123
|
+
controller.setHostScale(scale);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const adoptViewport = (viewport: ScenarEmbedViewport): void => {
|
|
128
|
+
canonicalViewport = viewport;
|
|
129
|
+
applyViewportScale();
|
|
130
|
+
if (!wrapperObserver && typeof ResizeObserver !== "undefined" && iframe.parentElement) {
|
|
131
|
+
wrapperObserver = new ResizeObserver(applyViewportScale);
|
|
132
|
+
wrapperObserver.observe(iframe.parentElement);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
77
136
|
const controller = createEmbedHostController(
|
|
78
137
|
{ iframe, origin },
|
|
79
138
|
{
|
|
@@ -81,6 +140,9 @@ export function createEmbedMount(
|
|
|
81
140
|
if (event.type === "resize") {
|
|
82
141
|
onAspectRatio?.({ widthPx: event.widthPx, heightPx: event.heightPx });
|
|
83
142
|
}
|
|
143
|
+
if (event.type === "ready" && event.viewport) {
|
|
144
|
+
adoptViewport(event.viewport);
|
|
145
|
+
}
|
|
84
146
|
onEvent?.(event);
|
|
85
147
|
},
|
|
86
148
|
},
|
|
@@ -107,6 +169,7 @@ export function createEmbedMount(
|
|
|
107
169
|
controller,
|
|
108
170
|
destroy() {
|
|
109
171
|
observer?.disconnect();
|
|
172
|
+
wrapperObserver?.disconnect();
|
|
110
173
|
controller.destroy();
|
|
111
174
|
},
|
|
112
175
|
};
|
package/src/react.test.tsx
CHANGED
|
@@ -45,6 +45,20 @@ describe("ScenarEmbed (React)", () => {
|
|
|
45
45
|
expect(wrapper.style.aspectRatio).toBe("896 / 480");
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
+
it("clips the wrapper so a host-applied border-radius rounds the embed", () => {
|
|
49
|
+
render(<ScenarEmbed src={SRC} title="t" />);
|
|
50
|
+
const iframe = screen.getByTitle("t") as HTMLIFrameElement;
|
|
51
|
+
stubFrame(iframe);
|
|
52
|
+
|
|
53
|
+
// The wrapper is the one clipping surface: the host rounds it via
|
|
54
|
+
// className/style, and everything inside — including the transform-scaled
|
|
55
|
+
// iframe — clips to it. The iframe itself must carry no radius (it would
|
|
56
|
+
// live in pre-transform space and render at radius x scale).
|
|
57
|
+
const wrapper = iframe.parentElement as HTMLElement;
|
|
58
|
+
expect(wrapper.style.overflow).toBe("hidden");
|
|
59
|
+
expect(iframe.style.borderRadius).toBe("");
|
|
60
|
+
});
|
|
61
|
+
|
|
48
62
|
it("resolves id + base into the embed src", () => {
|
|
49
63
|
render(<ScenarEmbed id="welcome" base="https://embed.example/demos" title="t" />);
|
|
50
64
|
const iframe = screen.getByTitle("t") as HTMLIFrameElement;
|
package/src/react.tsx
CHANGED
|
@@ -52,6 +52,15 @@ export interface ScenarEmbedProps extends EmbedSource {
|
|
|
52
52
|
* SSR-safe: the iframe renders without a `src` on the server and on the first
|
|
53
53
|
* client render, so hydration never mismatches; the effect then assigns the
|
|
54
54
|
* themed `src` and adopts the embed's reported aspect ratio.
|
|
55
|
+
*
|
|
56
|
+
* Corners: the wrapper is the one clipping surface — it carries
|
|
57
|
+
* `overflow: hidden`, and the host supplies any corner radius via `className`
|
|
58
|
+
* or `style`. The iframe itself must NOT carry a `border-radius`: under
|
|
59
|
+
* iframe-as-screen the mount lays the iframe out at the canonical viewport and
|
|
60
|
+
* scales it down with a transform, so a radius on the iframe lives in
|
|
61
|
+
* pre-transform space and renders at `radius x scale` — a mismatched, mostly
|
|
62
|
+
* ineffective clip. The embed paints edge-to-edge and the host boundary owns
|
|
63
|
+
* the corners.
|
|
55
64
|
*/
|
|
56
65
|
export const ScenarEmbed = forwardRef<ScenarEmbedHandle, ScenarEmbedProps>(
|
|
57
66
|
function ScenarEmbed({ src, id, base, title, theme = "auto", onEvent, className, style }, ref) {
|
|
@@ -98,7 +107,13 @@ export const ScenarEmbed = forwardRef<ScenarEmbedHandle, ScenarEmbedProps>(
|
|
|
98
107
|
return (
|
|
99
108
|
<div
|
|
100
109
|
className={className}
|
|
101
|
-
style={{
|
|
110
|
+
style={{
|
|
111
|
+
position: "relative",
|
|
112
|
+
width: "100%",
|
|
113
|
+
aspectRatio: ratio,
|
|
114
|
+
overflow: "hidden",
|
|
115
|
+
...style,
|
|
116
|
+
}}
|
|
102
117
|
>
|
|
103
118
|
<iframe
|
|
104
119
|
ref={iframeRef}
|
|
@@ -112,7 +127,6 @@ export const ScenarEmbed = forwardRef<ScenarEmbedHandle, ScenarEmbedProps>(
|
|
|
112
127
|
width: "100%",
|
|
113
128
|
height: "100%",
|
|
114
129
|
border: 0,
|
|
115
|
-
borderRadius: "inherit",
|
|
116
130
|
}}
|
|
117
131
|
/>
|
|
118
132
|
</div>
|