lecodes-cli 2.4.2 → 2.4.4

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.
@@ -1,4 +1,4 @@
1
- var ft=Object.defineProperty;var mt=(e,n,t)=>n in e?ft(e,n,{enumerable:!0,configurable:!0,writable:!0,value:t}):e[n]=t;var p=(e,n,t)=>mt(e,typeof n!="symbol"?n+"":n,t);(function(){const n=document.createElement("link").relList;if(n&&n.supports&&n.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))s(i);new MutationObserver(i=>{for(const r of i)if(r.type==="childList")for(const a of r.addedNodes)a.tagName==="LINK"&&a.rel==="modulepreload"&&s(a)}).observe(document,{childList:!0,subtree:!0});function t(i){const r={};return i.integrity&&(r.integrity=i.integrity),i.referrerPolicy&&(r.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?r.credentials="include":i.crossOrigin==="anonymous"?r.credentials="omit":r.credentials="same-origin",r}function s(i){if(i.ep)return;i.ep=!0;const r=t(i);fetch(i.href,r)}})();const Y=[{id:"iphone",name:"iPhone",width:393,height:852,radius:55,bezel:10,keyboard:336,keyboardLook:"ios",insets:{portrait:{top:59,right:0,bottom:34,left:0},landscape:{top:0,right:59,bottom:21,left:59}},marks:{island:{width:126,height:37,inset:11},homeIndicator:{width:139,height:5,inset:8},status:{height:59}}},{id:"android",name:"Android",width:412,height:915,radius:28,bezel:8,keyboard:300,keyboardLook:"android",insets:{portrait:{top:24,right:0,bottom:48,left:0,bars:["top","bottom"]},landscape:{top:24,right:0,bottom:48,left:0,bars:["top","bottom"]}},marks:{camera:{size:12,inset:6},navigation:{height:48},status:{height:24}}},{id:"tablet",name:"Tablet",width:820,height:1180,radius:18,bezel:14,keyboard:0,keyboardLook:"ios",insets:{portrait:{top:24,right:0,bottom:20,left:0},landscape:{top:24,right:0,bottom:20,left:0}},marks:{homeIndicator:{width:260,height:5,inset:7},status:{height:24}}}],ot=e=>Y.find(n=>n.id===e)??null,Q=(e,n)=>n==="landscape"?{width:e.height,height:e.width}:{width:e.width,height:e.height},bt=(e,n,t)=>{const s=Q(e,n),i=s.width+e.bezel*2,r=s.height+e.bezel*2;return t.width<=0||t.height<=0?1:Math.min(1,t.width/i,t.height/r)},kt=(e,n,t)=>{if(e>=1)return 1;const s=Math.floor(n*e*t);return s>0?s/(n*t):e},yt=`
1
+ var ft=Object.defineProperty;var mt=(s,n,t)=>n in s?ft(s,n,{enumerable:!0,configurable:!0,writable:!0,value:t}):s[n]=t;var p=(s,n,t)=>mt(s,typeof n!="symbol"?n+"":n,t);(function(){const n=document.createElement("link").relList;if(n&&n.supports&&n.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))e(i);new MutationObserver(i=>{for(const r of i)if(r.type==="childList")for(const a of r.addedNodes)a.tagName==="LINK"&&a.rel==="modulepreload"&&e(a)}).observe(document,{childList:!0,subtree:!0});function t(i){const r={};return i.integrity&&(r.integrity=i.integrity),i.referrerPolicy&&(r.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?r.credentials="include":i.crossOrigin==="anonymous"?r.credentials="omit":r.credentials="same-origin",r}function e(i){if(i.ep)return;i.ep=!0;const r=t(i);fetch(i.href,r)}})();const Y=[{id:"iphone",name:"iPhone",width:393,height:852,radius:55,bezel:10,keyboard:336,keyboardLook:"ios",insets:{portrait:{top:59,right:0,bottom:34,left:0},landscape:{top:0,right:59,bottom:21,left:59}},marks:{island:{width:126,height:37,inset:11},homeIndicator:{width:139,height:5,inset:8},status:{height:59}}},{id:"android",name:"Android",width:412,height:915,radius:28,bezel:8,keyboard:300,keyboardLook:"android",insets:{portrait:{top:24,right:0,bottom:48,left:0,bars:["top","bottom"]},landscape:{top:24,right:0,bottom:48,left:0,bars:["top","bottom"]}},marks:{camera:{size:12,inset:6},navigation:{height:48},status:{height:24}}},{id:"tablet",name:"Tablet",width:820,height:1180,radius:18,bezel:14,keyboard:0,keyboardLook:"ios",insets:{portrait:{top:24,right:0,bottom:20,left:0},landscape:{top:24,right:0,bottom:20,left:0}},marks:{homeIndicator:{width:260,height:5,inset:7},status:{height:24}}}],ot=s=>Y.find(n=>n.id===s)??null,Q=(s,n)=>n==="landscape"?{width:s.height,height:s.width}:{width:s.width,height:s.height},bt=(s,n,t)=>{const e=Q(s,n),i=e.width+s.bezel*2,r=e.height+s.bezel*2;return t.width<=0||t.height<=0?1:Math.min(1,t.width/i,t.height/r)},kt=(s,n,t)=>{if(s>=1)return 1;const e=Math.floor(n*s),i=r=>Math.abs(r-Math.round(r))<1e-6;for(let r=e;r>0&&r>e-8;r--)if(i(r*t))return r/n;return e>0?e/n:s},yt=`
2
2
  .marks { position: absolute; inset: 0; pointer-events: none; color: var(--lecodes-frame-marks, #ffffff); font: 600 15px/1 system-ui, sans-serif; }
3
3
  /* what the system draws OVER the app is seen on any app: light on a dark one, dark on a light one */
4
4
  .marks > * { position: absolute; mix-blend-mode: difference; }
@@ -12,7 +12,7 @@ var ft=Object.defineProperty;var mt=(e,n,t)=>n in e?ft(e,n,{enumerable:!0,config
12
12
  .navigation .back { border: 0; width: 0; height: 0; border-style: solid; border-width: 8px 13px 8px 0; border-color: transparent currentColor transparent transparent; }
13
13
  .navigation .homeButton { border-radius: 50%; }
14
14
  .navigation .recent { border-radius: 3px; }
15
- `,y=e=>`${e}px`,I=(e,n,t)=>{const s=e.createElement("div");return s.className=n,Object.assign(s.style,t),s},wt=(e,n,t)=>{const s=e.ownerDocument,{marks:i}=n,r=t==="landscape",a=[];if(i.island){const{width:h,height:b,inset:w}=i.island;a.push(r?I(s,"island solid",{left:y(w),top:"50%",width:y(b),height:y(h),transform:"translateY(-50%)"}):I(s,"island solid",{top:y(w),left:"50%",width:y(h),height:y(b),transform:"translateX(-50%)"}))}if(i.camera){const{size:h,inset:b}=i.camera;a.push(r?I(s,"camera solid",{left:y(b),top:"50%",width:y(h),height:y(h),transform:"translateY(-50%)"}):I(s,"camera solid",{top:y(b),left:"50%",width:y(h),height:y(h),transform:"translateX(-50%)"}))}if(i.homeIndicator){const{width:h,height:b,inset:w}=i.homeIndicator;a.push(I(s,"home",{bottom:y(w),left:"50%",width:y(h),height:y(b),transform:"translateX(-50%)"}))}const g=n.insets[t].top;if(i.status&&g>0){const h=I(s,"clock",{top:"0",left:y(i.island?34:18),height:y(Math.min(g,i.status.height))});h.textContent="9:41",i.island||(h.style.fontSize="13px"),a.push(h)}if(i.navigation){const h=I(s,"navigation",{height:y(i.navigation.height)});for(const b of["back","homeButton","recent"]){const w=s.createElement("i");w.className=b,h.appendChild(w)}a.push(h)}e.replaceChildren(...a)},vt=[.38,.7,.125,1],xt=`cubic-bezier(${vt.join(", ")})`,Et=`
15
+ `,y=s=>`${s}px`,I=(s,n,t)=>{const e=s.createElement("div");return e.className=n,Object.assign(e.style,t),e},wt=(s,n,t)=>{const e=s.ownerDocument,{marks:i}=n,r=t==="landscape",a=[];if(i.island){const{width:h,height:b,inset:w}=i.island;a.push(r?I(e,"island solid",{left:y(w),top:"50%",width:y(b),height:y(h),transform:"translateY(-50%)"}):I(e,"island solid",{top:y(w),left:"50%",width:y(h),height:y(b),transform:"translateX(-50%)"}))}if(i.camera){const{size:h,inset:b}=i.camera;a.push(r?I(e,"camera solid",{left:y(b),top:"50%",width:y(h),height:y(h),transform:"translateY(-50%)"}):I(e,"camera solid",{top:y(b),left:"50%",width:y(h),height:y(h),transform:"translateX(-50%)"}))}if(i.homeIndicator){const{width:h,height:b,inset:w}=i.homeIndicator;a.push(I(e,"home",{bottom:y(w),left:"50%",width:y(h),height:y(b),transform:"translateX(-50%)"}))}const g=n.insets[t].top;if(i.status&&g>0){const h=I(e,"clock",{top:"0",left:y(i.island?34:18),height:y(Math.min(g,i.status.height))});h.textContent="9:41",i.island||(h.style.fontSize="13px"),a.push(h)}if(i.navigation){const h=I(e,"navigation",{height:y(i.navigation.height)});for(const b of["back","homeButton","recent"]){const w=e.createElement("i");w.className=b,h.appendChild(w)}a.push(h)}s.replaceChildren(...a)},vt=[.38,.7,.125,1],xt=`cubic-bezier(${vt.join(", ")})`,Et=`
16
16
  .keys { position: absolute; left: 0; right: 0; bottom: 0; overflow: hidden; box-sizing: border-box; font: 400 22px/1 -apple-system, "SF Pro Text", Roboto, system-ui, sans-serif; color: #000; transform: translateY(100%); will-change: transform; }
17
17
  .keys.shown { transform: translateY(0); }
18
18
  .keys.ios { background: #d1d3d9; }
@@ -29,13 +29,13 @@ var ft=Object.defineProperty;var mt=(e,n,t)=>n in e?ft(e,n,{enumerable:!0,config
29
29
  .keys .k.shift::before { content: ""; display: block; width: 0; height: 0; border-style: solid; border-width: 0 9px 10px 9px; border-color: transparent transparent #000 transparent; }
30
30
  .keys .k.del::before { content: "⌫"; font-size: 20px; }
31
31
  .keys .k.emoji::before { content: "☺"; font-size: 20px; }
32
- `,$t={en:[["q","w","e","r","t","y","u","i","o","p"],["a","s","d","f","g","h","j","k","l"],["z","x","c","v","b","n","m"]],ru:[["й","ц","у","к","е","н","г","ш","щ","з","х"],["ф","ы","в","а","п","р","о","л","д","ж","э"],["я","ч","с","м","и","т","ь","б","ю"]]},Lt={en:{space:"space",enter:"return"},ru:{space:"Пробел",enter:"Ввод"}},N=e=>`${Math.round(e*2)/2}px`,St=(e,n)=>{const{look:t,width:s,height:i,inset:r}=n,a=n.language.slice(0,2)==="ru"?"ru":"en",g=$t[a],h=Lt[a],b=t==="ios",w=[],f=(k,z,F,V,G,gt)=>{const q=e.createElement("div");q.className=`k${z?` ${z}`:""}`,q.textContent=k,Object.assign(q.style,{left:N(F),top:N(V),width:N(G),height:N(gt)}),w.push(q)},B=b?44:40,j=e.createElement("div");j.className="bar",Object.assign(j.style,{top:"0",height:N(B)});for(let k=0;k<(b?2:3);k++)j.append(e.createElement("i"));w.push(j);const m=3,x=6,M=b?11:9,P=g[0].length,S=(s-m*2-x*(P-1))/P,o=i-B-r,c=Math.min(b?42:46,(o-M*5)/4),d=B+Math.max(M,(o-(c*4+M*3))/2-2),l=k=>d+k*(c+M),E=k=>(s-k*S-(k-1)*x)/2;g.forEach((k,z)=>{const F=E(k.length);k.forEach((V,G)=>f(V,"",F+G*(S+x),l(z),S,c))});const C=g[2],L=E(C.length)-x-m;f("","s shift",m,l(2),L,c),f("","s del",s-m-L,l(2),L,c);const v=l(3),A=S*2+x;if(b){f("123","s",m,v,A,c),f("","s emoji",m+A+x,v,S,c);const k=m+A+x+S+x;f(h.space,"space",k,v,s-k-m-A-x,c),f(h.enter,"s",s-m-A,v,A,c)}else{f("?123","s",m,v,A,c),f(",","",m+A+x,v,S,c);const k=m+A+x+S+x,z=s-m-A-x-S;f("","space",k,v,z-x-k,c),f(".","",z,v,S,c),f("↵","s",s-m-A,v,A,c)}return w},rt=(e,n)=>{e.style.transition=n>0?`transform ${n}ms ${xt}`:""},At=(e,n,t=0)=>{const s=e.classList.contains("shown");rt(e,t),e.className=`keys ${n.look}${s?" shown":""}`,e.style.height=N(n.height),e.replaceChildren(...St(e.ownerDocument,n)),s||(t>0&&e.offsetHeight,e.classList.add("shown"))},Ct=(e,n=0)=>{const t=()=>{e.classList.contains("shown")||(e.replaceChildren(),e.style.height="0")};if(!e.classList.contains("shown")){t();return}rt(e,n),e.classList.remove("shown"),n>0?e.addEventListener("transitionend",t,{once:!0}):t()},Rt=e=>{if(e.dataset.pointer)return;e.dataset.pointer="taken",e.style.pointerEvents="auto";const n=t=>{t.preventDefault(),t.stopPropagation()};for(const t of["pointerdown","mousedown","touchstart","click"])e.addEventListener(t,n,{passive:!1})},Ot=`
32
+ `,St={en:[["q","w","e","r","t","y","u","i","o","p"],["a","s","d","f","g","h","j","k","l"],["z","x","c","v","b","n","m"]],ru:[["й","ц","у","к","е","н","г","ш","щ","з","х"],["ф","ы","в","а","п","р","о","л","д","ж","э"],["я","ч","с","м","и","т","ь","б","ю"]]},$t={en:{space:"space",enter:"return"},ru:{space:"Пробел",enter:"Ввод"}},N=s=>`${Math.round(s*2)/2}px`,Lt=(s,n)=>{const{look:t,width:e,height:i,inset:r}=n,a=n.language.slice(0,2)==="ru"?"ru":"en",g=St[a],h=$t[a],b=t==="ios",w=[],f=(k,z,F,V,G,gt)=>{const q=s.createElement("div");q.className=`k${z?` ${z}`:""}`,q.textContent=k,Object.assign(q.style,{left:N(F),top:N(V),width:N(G),height:N(gt)}),w.push(q)},B=b?44:40,j=s.createElement("div");j.className="bar",Object.assign(j.style,{top:"0",height:N(B)});for(let k=0;k<(b?2:3);k++)j.append(s.createElement("i"));w.push(j);const m=3,x=6,M=b?11:9,P=g[0].length,L=(e-m*2-x*(P-1))/P,o=i-B-r,c=Math.min(b?42:46,(o-M*5)/4),d=B+Math.max(M,(o-(c*4+M*3))/2-2),l=k=>d+k*(c+M),E=k=>(e-k*L-(k-1)*x)/2;g.forEach((k,z)=>{const F=E(k.length);k.forEach((V,G)=>f(V,"",F+G*(L+x),l(z),L,c))});const C=g[2],$=E(C.length)-x-m;f("","s shift",m,l(2),$,c),f("","s del",e-m-$,l(2),$,c);const v=l(3),A=L*2+x;if(b){f("123","s",m,v,A,c),f("","s emoji",m+A+x,v,L,c);const k=m+A+x+L+x;f(h.space,"space",k,v,e-k-m-A-x,c),f(h.enter,"s",e-m-A,v,A,c)}else{f("?123","s",m,v,A,c),f(",","",m+A+x,v,L,c);const k=m+A+x+L+x,z=e-m-A-x-L;f("","space",k,v,z-x-k,c),f(".","",z,v,L,c),f("↵","s",e-m-A,v,A,c)}return w},rt=(s,n)=>{s.style.transition=n>0?`transform ${n}ms ${xt}`:""},At=(s,n,t=0)=>{const e=s.classList.contains("shown");rt(s,t),s.className=`keys ${n.look}${e?" shown":""}`,s.style.height=N(n.height),s.replaceChildren(...Lt(s.ownerDocument,n)),e||(t>0&&s.offsetHeight,s.classList.add("shown"))},Ct=(s,n=0)=>{const t=()=>{s.classList.contains("shown")||(s.replaceChildren(),s.style.height="0")};if(!s.classList.contains("shown")){t();return}rt(s,n),s.classList.remove("shown"),n>0?s.addEventListener("transitionend",t,{once:!0}):t()},Ot=s=>{if(s.dataset.pointer)return;s.dataset.pointer="taken",s.style.pointerEvents="auto";const n=t=>{t.preventDefault(),t.stopPropagation()};for(const t of["pointerdown","mousedown","touchstart","click"])s.addEventListener(t,n,{passive:!1})},Rt=`
33
33
  :host { display: block; position: relative; overflow: hidden; min-width: 0; min-height: 0; }
34
34
  .stage { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; }
35
35
  .device { position: relative; flex: none; box-sizing: content-box; background: var(--lecodes-frame-body, #0b0b0d); box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.10), 0 24px 60px rgba(0, 0, 0, 0.35); }
36
36
  .screen { position: relative; overflow: hidden; background: #000; width: 100%; height: 100%; }
37
37
  iframe { display: block; width: 100%; height: 100%; border: 0; background: #000; }
38
- .over { position: absolute; inset: 0; pointer-events: none; }
38
+ .over { position: absolute; left: 0; top: 0; width: 100%; height: 100%; pointer-events: none; }
39
39
  :host([bare]) .device { box-shadow: none; background: none; }
40
40
  .pick { position: absolute; inset: 0; pointer-events: none; }
41
41
  :host([picking]) .pick { pointer-events: auto; cursor: crosshair; }
@@ -45,5 +45,5 @@ iframe { display: block; width: 100%; height: 100%; border: 0; background: #000;
45
45
  .found.low > b { bottom: auto; top: 100%; margin: 3px 0 0; }
46
46
  ${yt}
47
47
  ${Et}
48
- `,jt=600,Bt=["player","src","device","orientation","language","bare","inspect"],zt=3e3,H=class H extends HTMLElement{constructor(){super();p(this,"stage");p(this,"body");p(this,"screen");p(this,"over");p(this,"marks");p(this,"keys");p(this,"pick");p(this,"found");p(this,"asked",new Map);p(this,"requests",0);p(this,"pointing",null);p(this,"pointed",!1);p(this,"iframe",null);p(this,"observer");p(this,"loaded",!1);p(this,"mounting",!1);p(this,"turned",null);p(this,"locked",!1);p(this,"shownScale",1);p(this,"onWindow",()=>{this.layout(),this.tellSafeArea()});p(this,"onMessage",t=>{var i;if(!this.iframe||t.source!==this.iframe.contentWindow)return;const s=t.data;if(!(!s||typeof s!="object"))switch(s.type){case"lecodes:ready":this.highlight(null),this.emit("app-ready",{src:String(s.src)});break;case"lecodes:error":this.emit("app-error",s.error);break;case"lecodes:log":this.emit("app-log",{level:s.level,text:String(s.text)});break;case"lecodes:inspected":(i=this.asked.get(s.request))==null||i(s.answer);break;case"lecodes:keyboard":this.drawKeyboard(s.height,s.duration);break;case"lecodes:orientation":{s.mode==="landscape"||s.mode==="portrait"?(this.turned=s.mode,this.locked=!0):this.locked=!1,this.layout(),this.tellSafeArea(),this.emit("app-orientation",{orientation:this.orientation,locked:this.locked});break}}});const t=this.attachShadow({mode:"open"}),s=this.ownerDocument,i=s.createElement("style");i.textContent=Ot;const r=a=>{const g=s.createElement("div");return g.className=a,g};this.stage=r("stage"),this.body=r("device"),this.screen=r("screen"),this.over=r("over"),this.marks=r("marks"),this.keys=r("keys"),Rt(this.keys),this.pick=r("pick"),this.found=r("found"),this.found.append(s.createElement("i"),s.createElement("b")),this.over.append(this.found,this.keys,this.marks,this.pick),this.pick.addEventListener("pointermove",a=>this.point(a)),this.pick.addEventListener("pointerleave",()=>{this.pointing=null,this.picking&&(this.highlight(null),this.emit("app-point",null))}),this.pick.addEventListener("click",a=>{this.at(a).then(g=>{g&&(this.highlight(g),this.emit("app-pick",g))})}),this.screen.appendChild(this.over),this.body.appendChild(this.screen),this.stage.appendChild(this.body),t.append(i,this.stage),this.observer=new ResizeObserver(()=>this.layout())}static get noRoom(){return window.innerWidth<jt}get player(){return this.getAttribute("player")??""}set player(t){this.setAttribute("player",t)}get src(){return this.getAttribute("src")??""}set src(t){this.setAttribute("src",t)}get device(){return this.getAttribute("device")??Y[0].id}set device(t){this.setAttribute("device",t)}get language(){return this.getAttribute("language")??""}set language(t){this.setAttribute("language",t)}get orientation(){return this.turned??(this.getAttribute("orientation")==="landscape"?"landscape":"portrait")}set orientation(t){this.turned=null,this.locked=!1,this.setAttribute("orientation",t)}get orientationLocked(){return this.locked}get bare(){return this.hasAttribute("bare")||this.current===null}get inspect(){return this.hasAttribute("inspect")}set inspect(t){this.toggleAttribute("inspect",t)}get picking(){return this.hasAttribute("picking")}set picking(t){this.toggleAttribute("picking",t),t||this.highlight(null)}get scale(){return this.shownScale}get current(){return this.hasAttribute("bare")||H.noRoom?null:ot(this.device)}get playerUrl(){return this.urlFor(null)}rotate(){this.locked||(this.orientation=this.orientation==="portrait"?"landscape":"portrait",this.emit("app-orientation",{orientation:this.orientation,locked:!1}))}ask(t){return new Promise(s=>{const i=++this.requests,r=a=>{this.asked.delete(i)&&s(a&&a.ask===t.ask?a:null)};this.asked.set(i,r),window.setTimeout(()=>r(null),zt),this.post({type:"lecodes:inspect",request:i,question:t})})}highlight(t){const s=this.found;if(!t){s.style.display="none";return}const{rect:i,padding:r}=t;Object.assign(s.style,{display:"block",left:`${i.x}px`,top:`${i.y}px`,width:`${i.width}px`,height:`${i.height}px`}),Object.assign(s.firstElementChild.style,{left:`${r.left}px`,top:`${r.top}px`,right:`${r.right}px`,bottom:`${r.bottom}px`}),s.lastElementChild.textContent=`${t.type}${t.name?` "${t.name}"`:""} ${Math.round(i.width)} × ${Math.round(i.height)}`,s.classList.toggle("low",i.y<24)}reload(){this.post({type:"lecodes:reload"})}connectedCallback(){this.observer.observe(this),window.addEventListener("message",this.onMessage),window.addEventListener("resize",this.onWindow),this.remount()}disconnectedCallback(){var t;this.observer.disconnect(),window.removeEventListener("message",this.onMessage),window.removeEventListener("resize",this.onWindow),(t=this.iframe)==null||t.remove(),this.iframe=null}attributeChangedCallback(t,s,i){if(!(!this.isConnected||s===i))switch(t){case"player":this.remount();break;case"src":this.loaded?i&&this.post({type:"lecodes:run",src:i}):this.remount();break;case"language":this.loaded?i&&this.post({type:"lecodes:language",language:i}):this.remount();break;case"inspect":this.remount();break;default:this.layout(),this.tellSafeArea()}}toApp(t){const s=this.screen.getBoundingClientRect();return{x:(t.clientX-s.left)/this.shownScale,y:(t.clientY-s.top)/this.shownScale}}async at(t){var s;return((s=await this.ask({ask:"at",...this.toApp(t)}))==null?void 0:s.node)??null}point(t){this.pointing=this.toApp(t),!this.pointed&&(this.pointed=!0,(async()=>{var s;for(;this.pointing&&this.picking;){const{x:i,y:r}=this.pointing;this.pointing=null;const a=((s=await this.ask({ask:"at",x:i,y:r}))==null?void 0:s.node)??null;if(!this.picking)break;this.highlight(a),this.emit("app-point",a)}this.pointed=!1})())}remount(){this.mounting||(this.mounting=!0,queueMicrotask(()=>{this.mounting=!1,this.isConnected&&this.mount()}))}mount(){var s;if((s=this.iframe)==null||s.remove(),this.iframe=null,this.loaded=!1,!this.player)return;const t=this.ownerDocument.createElement("iframe");t.addEventListener("load",()=>{this.iframe===t&&(this.loaded=!0,this.tellSafeArea())}),t.setAttribute("allow","autoplay; fullscreen; camera; microphone; geolocation; clipboard-read; clipboard-write; gamepad"),t.title="LeCodes app",t.src=this.urlFor(this.current),this.screen.prepend(t),this.iframe=t,this.layout()}urlFor(t){var i;const s=new URL(this.player,this.ownerDocument.baseURI);if(this.src&&s.searchParams.set("src",this.src),this.language&&s.searchParams.set("lang",this.language),this.inspect&&s.searchParams.set("inspect",""),t){const r=t.insets[this.orientation];s.searchParams.set("safe-area",[r.top,r.right,r.bottom,r.left].join(",")),(i=r.bars)!=null&&i.length&&s.searchParams.set("bars",r.bars.join(","));const a=this.keyboardBand(t);a>0&&s.searchParams.set("keyboard-band",String(a))}return s.href}layout(){const t=this.current;this.toggleAttribute("framed",t!==null);const s=this.body.style;if(!t){this.shownScale=1,Object.assign(s,{width:"100%",height:"100%",padding:"0",borderRadius:"0",zoom:""}),this.screen.style.borderRadius="0",this.marks.replaceChildren();return}const i=this.orientation,r=Q(t,i);this.shownScale=kt(bt(t,i,{width:this.clientWidth,height:this.clientHeight}),r.width,window.devicePixelRatio||1),Object.assign(s,{width:`${r.width}px`,height:`${r.height}px`,padding:`${t.bezel}px`,borderRadius:`${t.radius+t.bezel}px`,zoom:String(this.shownScale)}),this.screen.style.borderRadius=`${t.radius}px`,wt(this.marks,t,i)}post(t){var i;const s=(i=this.iframe)==null?void 0:i.contentWindow;!s||!this.player||s.postMessage(t,new URL(this.player,this.ownerDocument.baseURI).origin)}tellSafeArea(){const t=this.current;this.post({type:"lecodes:safe-area",insets:t?{...t.insets[this.orientation]}:null});const s=t?this.keyboardBand(t):0;this.post({type:"lecodes:keyboard-band",height:s>0?s:null}),s===0&&this.drawKeyboard(0)}keyboardBand(t){return this.orientation==="portrait"?t.keyboard:0}drawKeyboard(t,s=0){const i=this.current;if(!i||t<=0){Ct(this.keys,s);return}const r=Q(i,this.orientation);At(this.keys,{look:i.keyboardLook,language:this.language,width:r.width,height:t,inset:i.insets[this.orientation].bottom},s)}emit(t,s){this.dispatchEvent(new CustomEvent(t,{detail:s}))}};p(H,"observedAttributes",Bt);let D=H;const It=(e="lecodes-frame")=>{customElements.get(e)||customElements.define(e,D)};typeof customElements<"u"&&It();const $=(e,n,t)=>{const s=document.createElement(e);return n&&(s.className=n),t!==void 0&&(s.textContent=t),s},st=e=>e.type+(e.name?` "${e.name}"`:""),it=/^(width|height|minWidth|minHeight|maxWidth|maxHeight|aspectRatio|flex|flexGrow|flexShrink|flexBasis|flexDirection|flexWrap|alignItems|alignSelf|alignContent|justifyContent|gap|rowGap|columnGap|position|left|top|right|bottom|inset|display|overflow|zIndex|m|mx|my|mt|mr|mb|ml|margin\w*|p|px|py|pt|pr|pb|pl|padding\w*)$/,R=e=>typeof e=="string"?e:typeof e=="number"?String(Math.round(e*100)/100):Array.isArray(e)?e.map(R).join(" "):typeof e=="object"&&e!==null?Object.entries(e).map(([n,t])=>`${n}: ${R(t)}`).join(", "):String(e),Mt=(e,n,t)=>{const s=n.querySelector("#pick"),i=n.querySelector("#tree"),r=n.querySelector("#details"),a=new Set;let g=0,h=!1;const b=(o,c,d)=>{const l=$("div","node");l.dataset.id=String(o.id),l.style.paddingLeft=`${8+c*14}px`,l.classList.toggle("chosen",o.id===g);const E=$("span","fold",o.children.length===0?"":a.has(o.id)?"▸":"▾");if(E.addEventListener("click",C=>{C.stopPropagation(),a.has(o.id)?a.delete(o.id):a.add(o.id),w()}),l.append(E,$("span","type",o.type)),o.name&&l.append($("span","name",` "${o.name}"`)),o.text&&l.append($("span","text",` ${o.text.replace(/\s+/g," ")}`)),l.addEventListener("mouseenter",()=>e.highlight({...o,padding:{top:0,right:0,bottom:0,left:0}})),l.addEventListener("click",()=>void x(o.id)),d.append(l),!a.has(o.id))for(const C of o.children)b(C,c+1,d)},w=async()=>{if(!h)return;const o=await e.ask({ask:"tree"}),c=$("div");o||c.append($("div","empty","No app is running."));for(const d of(o==null?void 0:o.layers)??[])d.kind==="widget"&&c.append($("div","layer","widget")),b(d.tree,0,c);i.replaceChildren(c)};i.addEventListener("mouseleave",()=>{M()});const f=(o,c)=>{if(c.length===0)return null;const d=$("div","section");d.append($("div","title",o));for(const[l,E]of c){const C=$("div","pair");C.append($("span","key",l),$("span","value",E)),d.append(C)}return d},B=o=>typeof o=="object"&&o!==null&&!Array.isArray(o),j=(o,c)=>Object.entries(o??{}).filter(([d,l])=>c(d,l)).map(([d,l])=>[d,R(l)]),m=o=>{var _;if(!o){r.replaceChildren($("div","empty","Pick an element: in the app, or in the outline."));return}const c=$("div","head",st(o)),d=$("div","path",o.path.slice(0,-1).map(st).join(" › ")),{rect:l,padding:E}=o,C=[f("Box",[["size",`${R(l.width)} × ${R(l.height)}`],["position",`${R(l.x)}, ${R(l.y)}`],...E.top||E.right||E.bottom||E.left?[["padding",`${E.top} ${E.right} ${E.bottom} ${E.left}`]]:[],...o.scroll?[["scroll",`${o.scroll.axis}: ${R(o.scroll.offset)} of ${R(o.scroll.content)}`]]:[],...(_=o.state)!=null&&_.length?[["state",o.state.join(", ")]]:[]]),o.text!==void 0?f("Text",[["",o.text]]):null,f("Layout",j(o.set,(L,v)=>it.test(L)&&!B(v))),f("Set by the app",j(o.set,(L,v)=>!it.test(L)&&!B(v))),...Object.entries(o.set??{}).filter(([,L])=>B(L)).map(([L,v])=>f(L,j(v,()=>!0))),f("Painted",Object.entries(o.style).map(([L,v])=>[L,R(v)])),o.source?f("Made at",[["",`${o.source.file}:${o.source.line}:${o.source.column}`]]):null];r.replaceChildren(c,d,...C.filter(L=>L!==null))},x=async o=>{var d;g=o;const c=((d=await e.ask({ask:"node",node:o}))==null?void 0:d.node)??null;e.highlight(c),m(c);for(const l of i.querySelectorAll(".node"))l.classList.toggle("chosen",l.dataset.id===String(o))},M=async()=>{var o;e.picking||e.highlight(g?((o=await e.ask({ask:"node",node:g}))==null?void 0:o.node)??null:null)},P=o=>{e.picking=o,s.setAttribute("aria-pressed",String(o))};s.addEventListener("click",()=>P(!e.picking)),e.addEventListener("app-pick",o=>{const c=o.detail;P(!1),e.highlight(c),g=c.id;for(const d of c.path)a.delete(d.id);m(c),w().then(()=>{var d;return(d=i.querySelector(".node.chosen"))==null?void 0:d.scrollIntoView({block:"nearest"})})}),e.addEventListener("app-ready",()=>{g=0,a.clear(),m(null),w()});const S=o=>{h=o,n.hidden=!o,t.setAttribute("aria-pressed",String(o)),o?w():(P(!1),e.highlight(null))};return t.addEventListener("click",()=>S(!h)),n.querySelector("#refresh").addEventListener("click",()=>void w()),m(null),{setOpen:S,get open(){return h}}},W=new URLSearchParams(location.search),u=document.getElementById("frame");u.player=W.get("player")??"player/?dev=../..";W.get("src")&&(u.src=W.get("src"));u.inspect=!0;D.noRoom&&location.replace(u.playerUrl);const K=e=>{try{return localStorage.getItem(`lecodes-dev:${e}`)}catch{return null}},U=(e,n)=>{try{localStorage.setItem(`lecodes-dev:${e}`,n)}catch{}},at="none",J=W.get("device")??K("device")??Y[0].id;u.device=J===at||ot(J)?J:Y[0].id;u.orientation=(W.get("orientation")??K("orientation"))==="landscape"?"landscape":"portrait";const ct=document.getElementById("devices"),Z=document.getElementById("rotate"),Pt=document.getElementById("scale"),X=()=>{for(const n of ct.querySelectorAll("button"))n.setAttribute("aria-pressed",String(n.dataset.device===u.device));const e=!u.bare;Z.disabled=!e||u.orientationLocked,Z.title=u.orientationLocked?"The app holds the orientation (app.setOrientation)":"Turn the device",Pt.textContent=e&&u.scale<1?`${Math.round(u.scale*100)}%`:""};for(const{id:e,name:n}of[...Y,{id:at,name:"No frame"}]){const t=document.createElement("button");t.type="button",t.dataset.device=e,t.textContent=n,t.addEventListener("click",()=>{u.device=e,U("device",e),X()}),ct.appendChild(t)}Z.addEventListener("click",()=>{u.rotate(),U("orientation",u.orientation)});document.getElementById("reload").addEventListener("click",()=>u.reload());u.addEventListener("app-orientation",X);new ResizeObserver(X).observe(u);X();const lt=document.getElementById("console"),O=document.getElementById("console-lines"),dt=document.getElementById("console-toggle"),nt=document.getElementById("console-count"),Nt=500;let T=0;const tt=e=>{lt.hidden=!e,dt.setAttribute("aria-pressed",String(e)),U("console",e?"1":"0")},et=()=>{nt.textContent=T>0?String(T):"",nt.classList.toggle("errors",T>0)},ht=(e,n,t)=>{const s=O.scrollTop+O.clientHeight>=O.scrollHeight-4,i=document.createElement("div");if(i.className=`line ${e}`,i.textContent=n,t){const r=document.createElement("span");r.className="where",r.textContent=`
49
- at ${t}`,i.appendChild(r)}for(O.appendChild(i);O.childElementCount>Nt;)O.firstElementChild.remove();s&&(O.scrollTop=O.scrollHeight)};u.addEventListener("app-log",e=>{var i;const{level:n,text:t}=e.detail,s=(i=/^\[(ERROR|WARN)\]/.exec(t))==null?void 0:i[1];ht(s==="ERROR"?"error":s==="WARN"?"warn":n,t)});u.addEventListener("app-error",e=>{var s,i;const n=e.detail,t=(s=n.frames)==null?void 0:s[0];T++,et(),ht("error",n.detail?`${n.message}: ${n.detail}`:n.message,t?`${t.source}:${t.line}:${t.column}${t.fn?` (${t.fn})`:""}`:(i=n.nativeFrames)==null?void 0:i[0]),tt(!0)});u.addEventListener("app-ready",()=>{T=0,et()});const pt=document.getElementById("inspect-toggle"),ut=Mt(u,document.getElementById("inspector"),pt);pt.addEventListener("click",()=>U("inspect",ut.open?"1":"0"));ut.setOpen(K("inspect")==="1");dt.addEventListener("click",()=>tt(lt.hidden));document.getElementById("console-clear").addEventListener("click",()=>{O.replaceChildren(),T=0,et()});tt(K("console")==="1");
48
+ `,jt=600,Bt=["player","src","device","orientation","language","bare","inspect"],zt=3e3,H=class H extends HTMLElement{constructor(){super();p(this,"stage");p(this,"body");p(this,"screen");p(this,"over");p(this,"marks");p(this,"keys");p(this,"pick");p(this,"found");p(this,"asked",new Map);p(this,"requests",0);p(this,"pointing",null);p(this,"pointed",!1);p(this,"iframe",null);p(this,"observer");p(this,"loaded",!1);p(this,"mounting",!1);p(this,"turned",null);p(this,"locked",!1);p(this,"shownScale",1);p(this,"toldScale",1);p(this,"onWindow",()=>{this.layout(),this.tellSafeArea()});p(this,"onMessage",t=>{var i;if(!this.iframe||t.source!==this.iframe.contentWindow)return;const e=t.data;if(!(!e||typeof e!="object"))switch(e.type){case"lecodes:ready":this.highlight(null),this.emit("app-ready",{src:String(e.src)});break;case"lecodes:error":this.emit("app-error",e.error);break;case"lecodes:log":this.emit("app-log",{level:e.level,text:String(e.text)});break;case"lecodes:inspected":(i=this.asked.get(e.request))==null||i(e.answer);break;case"lecodes:keyboard":this.drawKeyboard(e.height,e.duration);break;case"lecodes:orientation":{e.mode==="landscape"||e.mode==="portrait"?(this.turned=e.mode,this.locked=!0):this.locked=!1,this.layout(),this.tellSafeArea(),this.emit("app-orientation",{orientation:this.orientation,locked:this.locked});break}}});const t=this.attachShadow({mode:"open"}),e=this.ownerDocument,i=e.createElement("style");i.textContent=Rt;const r=a=>{const g=e.createElement("div");return g.className=a,g};this.stage=r("stage"),this.body=r("device"),this.screen=r("screen"),this.over=r("over"),this.marks=r("marks"),this.keys=r("keys"),Ot(this.keys),this.pick=r("pick"),this.found=r("found"),this.found.append(e.createElement("i"),e.createElement("b")),this.over.append(this.found,this.keys,this.marks,this.pick),this.pick.addEventListener("pointermove",a=>this.point(a)),this.pick.addEventListener("pointerleave",()=>{this.pointing=null,this.picking&&(this.highlight(null),this.emit("app-point",null))}),this.pick.addEventListener("click",a=>{this.at(a).then(g=>{g&&(this.highlight(g),this.emit("app-pick",g))})}),this.screen.appendChild(this.over),this.body.appendChild(this.screen),this.stage.appendChild(this.body),t.append(i,this.stage),this.observer=new ResizeObserver(()=>this.layout())}static get noRoom(){return window.innerWidth<jt}get player(){return this.getAttribute("player")??""}set player(t){this.setAttribute("player",t)}get src(){return this.getAttribute("src")??""}set src(t){this.setAttribute("src",t)}get device(){return this.getAttribute("device")??Y[0].id}set device(t){this.setAttribute("device",t)}get language(){return this.getAttribute("language")??""}set language(t){this.setAttribute("language",t)}get orientation(){return this.turned??(this.getAttribute("orientation")==="landscape"?"landscape":"portrait")}set orientation(t){this.turned=null,this.locked=!1,this.setAttribute("orientation",t)}get orientationLocked(){return this.locked}get bare(){return this.hasAttribute("bare")||this.current===null}get inspect(){return this.hasAttribute("inspect")}set inspect(t){this.toggleAttribute("inspect",t)}get picking(){return this.hasAttribute("picking")}set picking(t){this.toggleAttribute("picking",t),t||this.highlight(null)}get scale(){return this.shownScale}get current(){return this.hasAttribute("bare")||H.noRoom?null:ot(this.device)}get playerUrl(){return this.urlFor(null)}rotate(){this.locked||(this.orientation=this.orientation==="portrait"?"landscape":"portrait",this.emit("app-orientation",{orientation:this.orientation,locked:!1}))}ask(t){return new Promise(e=>{const i=++this.requests,r=a=>{this.asked.delete(i)&&e(a&&a.ask===t.ask?a:null)};this.asked.set(i,r),window.setTimeout(()=>r(null),zt),this.post({type:"lecodes:inspect",request:i,question:t})})}highlight(t){const e=this.found;if(!t){e.style.display="none";return}const{rect:i,padding:r}=t;Object.assign(e.style,{display:"block",left:`${i.x}px`,top:`${i.y}px`,width:`${i.width}px`,height:`${i.height}px`}),Object.assign(e.firstElementChild.style,{left:`${r.left}px`,top:`${r.top}px`,right:`${r.right}px`,bottom:`${r.bottom}px`}),e.lastElementChild.textContent=`${t.type}${t.name?` "${t.name}"`:""} ${Math.round(i.width)} × ${Math.round(i.height)}`,e.classList.toggle("low",i.y<24)}reload(){this.post({type:"lecodes:reload"})}connectedCallback(){this.observer.observe(this),window.addEventListener("message",this.onMessage),window.addEventListener("resize",this.onWindow),this.remount()}disconnectedCallback(){var t;this.observer.disconnect(),window.removeEventListener("message",this.onMessage),window.removeEventListener("resize",this.onWindow),(t=this.iframe)==null||t.remove(),this.iframe=null}attributeChangedCallback(t,e,i){if(!(!this.isConnected||e===i))switch(t){case"player":this.remount();break;case"src":this.loaded?i&&this.post({type:"lecodes:run",src:i}):this.remount();break;case"language":this.loaded?i&&this.post({type:"lecodes:language",language:i}):this.remount();break;case"inspect":this.remount();break;default:this.layout(),this.tellSafeArea()}}toApp(t){const e=this.screen.getBoundingClientRect();return{x:(t.clientX-e.left)/this.shownScale,y:(t.clientY-e.top)/this.shownScale}}async at(t){var e;return((e=await this.ask({ask:"at",...this.toApp(t)}))==null?void 0:e.node)??null}point(t){this.pointing=this.toApp(t),!this.pointed&&(this.pointed=!0,(async()=>{var e;for(;this.pointing&&this.picking;){const{x:i,y:r}=this.pointing;this.pointing=null;const a=((e=await this.ask({ask:"at",x:i,y:r}))==null?void 0:e.node)??null;if(!this.picking)break;this.highlight(a),this.emit("app-point",a)}this.pointed=!1})())}remount(){this.mounting||(this.mounting=!0,queueMicrotask(()=>{this.mounting=!1,this.isConnected&&this.mount()}))}mount(){var e;if((e=this.iframe)==null||e.remove(),this.iframe=null,this.loaded=!1,!this.player)return;this.layout();const t=this.ownerDocument.createElement("iframe");t.addEventListener("load",()=>{this.iframe===t&&(this.loaded=!0,this.tellSafeArea(),this.tellScale())}),t.setAttribute("allow","autoplay; fullscreen; camera; microphone; geolocation; clipboard-read; clipboard-write; gamepad"),t.title="LeCodes app",t.src=this.urlFor(this.current),this.screen.prepend(t),this.iframe=t}urlFor(t){var i;const e=new URL(this.player,this.ownerDocument.baseURI);if(this.src&&e.searchParams.set("src",this.src),this.language&&e.searchParams.set("lang",this.language),this.inspect&&e.searchParams.set("inspect",""),t&&this.shownScale!==1&&(e.searchParams.set("scale",String(this.shownScale)),this.toldScale=this.shownScale),t){const r=t.insets[this.orientation];e.searchParams.set("safe-area",[r.top,r.right,r.bottom,r.left].join(",")),(i=r.bars)!=null&&i.length&&e.searchParams.set("bars",r.bars.join(","));const a=this.keyboardBand(t);a>0&&e.searchParams.set("keyboard-band",String(a))}return e.href}layout(){const t=this.current;this.toggleAttribute("framed",t!==null);const e=this.body.style;if(!t){this.shownScale=1,Object.assign(e,{width:"100%",height:"100%",padding:"0",borderRadius:"0"}),this.screen.style.borderRadius="0",Object.assign(this.over.style,{width:"",height:"",zoom:""}),this.marks.replaceChildren(),this.tellScale();return}const i=this.orientation,r=Q(t,i),a=this.shownScale=kt(bt(t,i,{width:this.clientWidth,height:this.clientHeight}),r.width,window.devicePixelRatio||1);Object.assign(e,{width:`${r.width*a}px`,height:`${r.height*a}px`,padding:`${t.bezel*a}px`,borderRadius:`${(t.radius+t.bezel)*a}px`}),this.screen.style.borderRadius=`${t.radius*a}px`,Object.assign(this.over.style,{width:`${r.width}px`,height:`${r.height}px`,zoom:a===1?"":String(a)}),wt(this.marks,t,i),this.tellScale()}post(t){var i;const e=(i=this.iframe)==null?void 0:i.contentWindow;!e||!this.player||e.postMessage(t,new URL(this.player,this.ownerDocument.baseURI).origin)}tellSafeArea(){const t=this.current;this.post({type:"lecodes:safe-area",insets:t?{...t.insets[this.orientation]}:null});const e=t?this.keyboardBand(t):0;this.post({type:"lecodes:keyboard-band",height:e>0?e:null}),e===0&&this.drawKeyboard(0)}tellScale(){!this.loaded||this.toldScale===this.shownScale||(this.toldScale=this.shownScale,this.post({type:"lecodes:scale",scale:this.shownScale}))}keyboardBand(t){return this.orientation==="portrait"?t.keyboard:0}drawKeyboard(t,e=0){const i=this.current;if(!i||t<=0){Ct(this.keys,e);return}const r=Q(i,this.orientation);At(this.keys,{look:i.keyboardLook,language:this.language,width:r.width,height:t,inset:i.insets[this.orientation].bottom},e)}emit(t,e){this.dispatchEvent(new CustomEvent(t,{detail:e}))}};p(H,"observedAttributes",Bt);let D=H;const It=(s="lecodes-frame")=>{customElements.get(s)||customElements.define(s,D)};typeof customElements<"u"&&It();const S=(s,n,t)=>{const e=document.createElement(s);return n&&(e.className=n),t!==void 0&&(e.textContent=t),e},st=s=>s.type+(s.name?` "${s.name}"`:""),it=/^(width|height|minWidth|minHeight|maxWidth|maxHeight|aspectRatio|flex|flexGrow|flexShrink|flexBasis|flexDirection|flexWrap|alignItems|alignSelf|alignContent|justifyContent|gap|rowGap|columnGap|position|left|top|right|bottom|inset|display|overflow|zIndex|m|mx|my|mt|mr|mb|ml|margin\w*|p|px|py|pt|pr|pb|pl|padding\w*)$/,O=s=>typeof s=="string"?s:typeof s=="number"?String(Math.round(s*100)/100):Array.isArray(s)?s.map(O).join(" "):typeof s=="object"&&s!==null?Object.entries(s).map(([n,t])=>`${n}: ${O(t)}`).join(", "):String(s),Mt=(s,n,t)=>{const e=n.querySelector("#pick"),i=n.querySelector("#tree"),r=n.querySelector("#details"),a=new Set;let g=0,h=!1;const b=(o,c,d)=>{const l=S("div","node");l.dataset.id=String(o.id),l.style.paddingLeft=`${8+c*14}px`,l.classList.toggle("chosen",o.id===g);const E=S("span","fold",o.children.length===0?"":a.has(o.id)?"▸":"▾");if(E.addEventListener("click",C=>{C.stopPropagation(),a.has(o.id)?a.delete(o.id):a.add(o.id),w()}),l.append(E,S("span","type",o.type)),o.name&&l.append(S("span","name",` "${o.name}"`)),o.text&&l.append(S("span","text",` ${o.text.replace(/\s+/g," ")}`)),l.addEventListener("mouseenter",()=>s.highlight({...o,padding:{top:0,right:0,bottom:0,left:0}})),l.addEventListener("click",()=>void x(o.id)),d.append(l),!a.has(o.id))for(const C of o.children)b(C,c+1,d)},w=async()=>{if(!h)return;const o=await s.ask({ask:"tree"}),c=S("div");o||c.append(S("div","empty","No app is running."));for(const d of(o==null?void 0:o.layers)??[])d.kind==="widget"&&c.append(S("div","layer","widget")),b(d.tree,0,c);i.replaceChildren(c)};i.addEventListener("mouseleave",()=>{M()});const f=(o,c)=>{if(c.length===0)return null;const d=S("div","section");d.append(S("div","title",o));for(const[l,E]of c){const C=S("div","pair");C.append(S("span","key",l),S("span","value",E)),d.append(C)}return d},B=o=>typeof o=="object"&&o!==null&&!Array.isArray(o),j=(o,c)=>Object.entries(o??{}).filter(([d,l])=>c(d,l)).map(([d,l])=>[d,O(l)]),m=o=>{var _;if(!o){r.replaceChildren(S("div","empty","Pick an element: in the app, or in the outline."));return}const c=S("div","head",st(o)),d=S("div","path",o.path.slice(0,-1).map(st).join(" › ")),{rect:l,padding:E}=o,C=[f("Box",[["size",`${O(l.width)} × ${O(l.height)}`],["position",`${O(l.x)}, ${O(l.y)}`],...E.top||E.right||E.bottom||E.left?[["padding",`${E.top} ${E.right} ${E.bottom} ${E.left}`]]:[],...o.scroll?[["scroll",`${o.scroll.axis}: ${O(o.scroll.offset)} of ${O(o.scroll.content)}`]]:[],...(_=o.state)!=null&&_.length?[["state",o.state.join(", ")]]:[]]),o.text!==void 0?f("Text",[["",o.text]]):null,f("Layout",j(o.set,($,v)=>it.test($)&&!B(v))),f("Set by the app",j(o.set,($,v)=>!it.test($)&&!B(v))),...Object.entries(o.set??{}).filter(([,$])=>B($)).map(([$,v])=>f($,j(v,()=>!0))),f("Painted",Object.entries(o.style).map(([$,v])=>[$,O(v)])),o.source?f("Made at",[["",`${o.source.file}:${o.source.line}:${o.source.column}`]]):null];r.replaceChildren(c,d,...C.filter($=>$!==null))},x=async o=>{var d;g=o;const c=((d=await s.ask({ask:"node",node:o}))==null?void 0:d.node)??null;s.highlight(c),m(c);for(const l of i.querySelectorAll(".node"))l.classList.toggle("chosen",l.dataset.id===String(o))},M=async()=>{var o;s.picking||s.highlight(g?((o=await s.ask({ask:"node",node:g}))==null?void 0:o.node)??null:null)},P=o=>{s.picking=o,e.setAttribute("aria-pressed",String(o))};e.addEventListener("click",()=>P(!s.picking)),s.addEventListener("app-pick",o=>{const c=o.detail;P(!1),s.highlight(c),g=c.id;for(const d of c.path)a.delete(d.id);m(c),w().then(()=>{var d;return(d=i.querySelector(".node.chosen"))==null?void 0:d.scrollIntoView({block:"nearest"})})}),s.addEventListener("app-ready",()=>{g=0,a.clear(),m(null),w()});const L=o=>{h=o,n.hidden=!o,t.setAttribute("aria-pressed",String(o)),o?w():(P(!1),s.highlight(null))};return t.addEventListener("click",()=>L(!h)),n.querySelector("#refresh").addEventListener("click",()=>void w()),m(null),{setOpen:L,get open(){return h}}},W=new URLSearchParams(location.search),u=document.getElementById("frame");u.player=W.get("player")??"player/?dev=../..";W.get("src")&&(u.src=W.get("src"));u.inspect=!0;D.noRoom&&location.replace(u.playerUrl);const K=s=>{try{return localStorage.getItem(`lecodes-dev:${s}`)}catch{return null}},U=(s,n)=>{try{localStorage.setItem(`lecodes-dev:${s}`,n)}catch{}},at="none",J=W.get("device")??K("device")??Y[0].id;u.device=J===at||ot(J)?J:Y[0].id;u.orientation=(W.get("orientation")??K("orientation"))==="landscape"?"landscape":"portrait";const ct=document.getElementById("devices"),Z=document.getElementById("rotate"),Pt=document.getElementById("scale"),X=()=>{for(const n of ct.querySelectorAll("button"))n.setAttribute("aria-pressed",String(n.dataset.device===u.device));const s=!u.bare;Z.disabled=!s||u.orientationLocked,Z.title=u.orientationLocked?"The app holds the orientation (app.setOrientation)":"Turn the device",Pt.textContent=s&&u.scale<1?`${Math.round(u.scale*100)}%`:""};for(const{id:s,name:n}of[...Y,{id:at,name:"No frame"}]){const t=document.createElement("button");t.type="button",t.dataset.device=s,t.textContent=n,t.addEventListener("click",()=>{u.device=s,U("device",s),X()}),ct.appendChild(t)}Z.addEventListener("click",()=>{u.rotate(),U("orientation",u.orientation)});document.getElementById("reload").addEventListener("click",()=>u.reload());u.addEventListener("app-orientation",X);new ResizeObserver(X).observe(u);X();const lt=document.getElementById("console"),R=document.getElementById("console-lines"),dt=document.getElementById("console-toggle"),nt=document.getElementById("console-count"),Nt=500;let T=0;const tt=s=>{lt.hidden=!s,dt.setAttribute("aria-pressed",String(s)),U("console",s?"1":"0")},et=()=>{nt.textContent=T>0?String(T):"",nt.classList.toggle("errors",T>0)},ht=(s,n,t)=>{const e=R.scrollTop+R.clientHeight>=R.scrollHeight-4,i=document.createElement("div");if(i.className=`line ${s}`,i.textContent=n,t){const r=document.createElement("span");r.className="where",r.textContent=`
49
+ at ${t}`,i.appendChild(r)}for(R.appendChild(i);R.childElementCount>Nt;)R.firstElementChild.remove();e&&(R.scrollTop=R.scrollHeight)};u.addEventListener("app-log",s=>{var i;const{level:n,text:t}=s.detail,e=(i=/^\[(ERROR|WARN)\]/.exec(t))==null?void 0:i[1];ht(e==="ERROR"?"error":e==="WARN"?"warn":n,t)});u.addEventListener("app-error",s=>{var e,i;const n=s.detail,t=(e=n.frames)==null?void 0:e[0];T++,et(),ht("error",n.detail?`${n.message}: ${n.detail}`:n.message,t?`${t.source}:${t.line}:${t.column}${t.fn?` (${t.fn})`:""}`:(i=n.nativeFrames)==null?void 0:i[0]),tt(!0)});u.addEventListener("app-ready",()=>{T=0,et()});const pt=document.getElementById("inspect-toggle"),ut=Mt(u,document.getElementById("inspector"),pt);pt.addEventListener("click",()=>U("inspect",ut.open?"1":"0"));ut.setOpen(K("inspect")==="1");dt.addEventListener("click",()=>tt(lt.hidden));document.getElementById("console-clear").addEventListener("click",()=>{R.replaceChildren(),T=0,et()});tt(K("console")==="1");
@@ -11,16 +11,17 @@ import {
11
11
  type AppConfig, ANDROID_SDK_VARIANTS, type AndroidSdkVariant, VERSION_RE, androidSdkArtifact, androidVariantEngines,
12
12
  childEnv, cmpVersion, ensureShellIgnored, fetchLatestSdkVersion,
13
13
  projectRoot, readAppConfigAt, readJson, resolvePlugin, resolveSdkVersion, resolveVariant, runTool, sdkSatisfies, slugify,
14
- writeJson, type LockEntry, resolveUpdateUrl, urlSchemeOf,
14
+ writeJson, type LockEntry, resolveUpdateUrl, urlSchemeOf, appLinksOf, appLinksSiteNote, type AppLink,
15
15
  } from "./shared"
16
16
  import { loadIconMaster, restampManifestIcon, writeAndroidIcons } from "./icon"
17
+ import { androidSplashThemeHint, loadSplashMaster, writeAndroidSplash } from "./splash"
17
18
  import {
18
19
  ANDROID_GITIGNORE, ANDROID_README, APP_PROGUARD_RULES, COLORS_XML, GRADLE_PROPERTIES, GRADLE_VERSION,
19
20
  GRADLE_PLUGIN_VERSIONS, IC_LAUNCHER_XML, aarWrapperBuildGradle, appBuildGradleTemplate,
20
21
  appManifestTemplate, appViewModelTemplate, gradleWrapperProperties, mainActivityTemplate,
21
22
  pluginBuildGradleTemplate, rootBuildGradleTemplate, runtimeBuildGradleTemplate,
22
- runtimeKotlinTemplate, settingsGradleTemplate, stringsXmlTemplate, themesXmlTemplate,
23
- type AndroidRuntimePlugin, type AndroidShellIdentity, runtimeManifestTemplate, urlSchemeFilter,
23
+ runtimeKotlinTemplate, runtimeThemeTemplate, settingsGradleTemplate, stringsXmlTemplate, themesXmlTemplate,
24
+ type AndroidRuntimePlugin, type AndroidShellIdentity, runtimeManifestTemplate, urlSchemeFilter, appLinksFilter,
24
25
  } from "./templates/android"
25
26
  import { GRADLEW_BAT, GRADLEW_SH } from "./templates/gradlew"
26
27
 
@@ -38,6 +39,10 @@ export const ANDROID_SDK_REPO = "letary/lecodes-android-sdk"
38
39
  * :lecodes-ui). A 2.x bundle is refused by a 1.x runtime (`_creatorApp.run()` gates on the major),
39
40
  * so a shell pinned below this would install and show nothing; `lecodes app update` moves a 1.x
40
41
  * shell here. (1.2.0 was the 1.x floor: system back moved into engine.attach().) */
42
+ // TODO(docs/plans/ota-splash-plan.md step 1): raise to the first lecodes-android-sdk release with
43
+ // `LeCodesShell.boot(engine, activity, …)` (the first-launch wait + the splash hold), which the
44
+ // AppViewModel template now calls — the owner fills the version in at release time. (Against an
45
+ // older AAR the call still compiles: it lands on the context form, which never waits.)
41
46
  export const ANDROID_SDK_MIN_VERSION = "2.0.13"
42
47
 
43
48
  /** First lecodes-android-sdk release that publishes the variant matrix (lecodes-sdk-3d / -2d /
@@ -171,7 +176,7 @@ export const initAndroid = async (opts: AndroidInitOptions) => {
171
176
 
172
177
  write("app/build.gradle.kts", appBuildGradleTemplate(identity, config.android!.minify ?? true))
173
178
  write("app/proguard-rules.pro", APP_PROGUARD_RULES)
174
- write("app/src/main/AndroidManifest.xml", appManifestTemplate(urlSchemeOf(config)))
179
+ write("app/src/main/AndroidManifest.xml", appManifestTemplate(urlSchemeOf(config), appLinksOf(config)))
175
180
  write(`app/src/main/java/${pkgPath}/MainActivity.kt`, mainActivityTemplate(identity.applicationId))
176
181
  write(`app/src/main/java/${pkgPath}/AppViewModel.kt`, appViewModelTemplate(identity.applicationId))
177
182
  write("app/src/main/res/values/themes.xml", themesXmlTemplate())
@@ -184,6 +189,8 @@ export const initAndroid = async (opts: AndroidInitOptions) => {
184
189
  const initArtifact = cmpVersion(sdkVersion, ANDROID_SDK_VARIANTS_MIN) >= 0 ? androidSdkArtifact(initVariant, sdkVersion) : "lecodes-sdk"
185
190
  write("lecodes-runtime/build.gradle.kts", runtimeBuildGradleTemplate(sdkVersion, [], initArtifact))
186
191
  write("lecodes-runtime/src/main/kotlin/io/letary/lecodes/runtime/LeCodesRuntime.kt", runtimeKotlinTemplate([]))
192
+ // The parent of app/'s Theme.LeCodesApp (black until a sync renders app.json "splash" into it).
193
+ write("lecodes-runtime/src/main/res/values/lecodes_splash.xml", runtimeThemeTemplate())
187
194
 
188
195
  log("")
189
196
  log(`${c.green("sdk")} lecodes-android-sdk ${sdkVersion}${sdk === "latest" ? c.dim(" (android.sdk: latest — re-resolved on every sync)") : c.dim(" (pinned in app.json android.sdk — `lecodes app update` moves it)")}`)
@@ -191,6 +198,7 @@ export const initAndroid = async (opts: AndroidInitOptions) => {
191
198
  log(` ${c.bold("lecodes app sync")} embed the compiled bundle (+ plugins) into the shell`)
192
199
  log(` ${c.bold("lecodes app open android")} then run on your device from Android Studio`)
193
200
  log(` App icon: set app.json "icon" to a square ≥1024px image — sync renders every size (or use Android Studio → Image Asset)`)
201
+ log(` Splash: set app.json "splash": {"background": "#RRGGBB", "image": "./splash.png"} — sync renders the splash (the image centred at 160 dp, "size" overrides; Android 12+'s system splash included)`)
194
202
  }
195
203
 
196
204
  /* ---------------------------------------- sync ---------------------------------------- */
@@ -378,7 +386,14 @@ export const syncAndroid = async (root: string, config: AppConfig, prior: SyncAr
378
386
  // LeCodesShell.boot reads it — app.json "update" resolved like the iOS plist key ----
379
387
  const updateUrl = resolveUpdateUrl(config.update, root, sdkVersion)
380
388
  writeFileSync(join(androidDir, "lecodes-runtime", "src", "main", "AndroidManifest.xml"), runtimeManifestTemplate(updateUrl))
381
- if (updateUrl) log(`${c.green("update")} OTA source ${updateUrl} — checked natively at launch (LeCodesShell.boot), new code applies next launch`)
389
+ if (updateUrl) log(`${c.green("update")} OTA source ${updateUrl} — checked natively at launch (LeCodesShell.boot): the first launch of a build waits (≤ 5 s) for the newest bundle, later launches apply new code next launch`)
390
+ // ---- splash: Theme.LeCodesRuntime (the parent of the app's theme) in the runtime module's res —
391
+ // black without app.json "splash", the splash colour / image / Android 12+ system splash with it ----
392
+ await writeAndroidSplash(androidDir, await loadSplashMaster(root, config))
393
+ if (config.splash !== undefined) {
394
+ const themeHint = androidSplashThemeHint(androidDir)
395
+ if (themeHint) warn(`splash: ${themeHint}`)
396
+ }
382
397
 
383
398
  if (pluginSpecs.length > 0 || existsSync(lockPath)) writeJson(lockPath, { plugins: newLock })
384
399
 
@@ -393,38 +408,59 @@ export const syncAndroid = async (root: string, config: AppConfig, prior: SyncAr
393
408
  restampManifestIcon(androidDir)
394
409
  }
395
410
  restampManifestUrlScheme(androidDir, urlSchemeOf(config))
411
+ const appLinks = appLinksOf(config)
412
+ restampManifestAppLinks(androidDir, appLinks)
413
+ if (appLinks.length) {
414
+ log(`${c.green("links")} ${appLinks.map(l => l.url).join(", ")} — verified by the site's proof:`)
415
+ for (const line of appLinksSiteNote(appLinks, { platform: "android", applicationId: config.android?.applicationId })) log(` ${line}`)
416
+ }
396
417
 
397
418
  log("")
398
419
  success(`Android shell synced: app.js + ${runtimePlugins.length} plugin(s), SDK ${sdkVersion}, v${config.version ?? "1.0.0"} (${config.android?.versionCode ?? 1}).`)
399
420
  note("Build & run from Android Studio (`lecodes app open android`) — user files in android/app/ were not touched.")
400
421
  }
401
422
 
402
- /** The `lecodes:urlScheme` block of the USER-owned manifest follows app.json: written into the
403
- * activity when the key appears, rewritten when it changes, removed when it goes. A manifest
423
+ /** A `lecodes:<tag>` block of the USER-owned manifest follows app.json: written into the
424
+ * activity when its key appears, rewritten when it changes, removed when it goes. A manifest
404
425
  * without the tag and without a MainActivity to put it in is left alone, with a warning. */
405
- export const restampManifestUrlScheme = (androidDir: string, urlScheme: string | undefined): void => {
426
+ const restampManifestBlock = (androidDir: string, tag: string, block: string, what: string, missing: string, after: RegExp[] = []): void => {
406
427
  const path = join(androidDir, "app", "src", "main", "AndroidManifest.xml")
407
428
  if (!existsSync(path)) return
408
429
  const content = readFileSync(path, "utf8")
409
- const tagged = /\n[ \t]*<!-- lecodes:urlScheme[\s\S]*?<!-- \/lecodes:urlScheme -->/
410
- const block = urlSchemeFilter(urlScheme)
430
+ const tagged = new RegExp(`\\n[ \\t]*<!-- lecodes:${tag}[\\s\\S]*?<!-- \\/lecodes:${tag} -->`)
411
431
  let next: string
412
- if (tagged.test(content)) next = content.replace(tagged, block)
413
- else if (!urlScheme) return
432
+ if (tagged.test(content)) next = content.replace(tagged, () => block)
433
+ else if (!block) return
414
434
  else {
415
- // Inside the MainActivity element: right after its LAUNCHER intent-filter.
435
+ // Inside the MainActivity element: after the block it follows in the template when that one
436
+ // is there, else right after the LAUNCHER intent-filter — so a restamped manifest reads as a
437
+ // fresh init's.
416
438
  const launcher = /(<category android:name="android\.intent\.category\.LAUNCHER" \/>\s*<\/intent-filter>)/
417
- if (!launcher.test(content)) {
418
- warn(`couldn't find the launcher activity in app/src/main/AndroidManifest.xml — add an intent-filter for the "${urlScheme}" scheme yourself.`)
439
+ const anchor = [...after, launcher].find(re => re.test(content))
440
+ if (!anchor) {
441
+ warn(`couldn't find the launcher activity in app/src/main/AndroidManifest.xml — ${missing}`)
419
442
  return
420
443
  }
421
- next = content.replace(launcher, `$1${block}`)
444
+ next = content.replace(anchor, (m) => `${m}${block}`)
422
445
  }
423
446
  if (next === content) return
424
447
  writeFileSync(path, next)
425
- log(`${c.green("updated")} android/app/src/main/AndroidManifest.xml: urlScheme ${urlScheme ? `→ ${urlScheme}://` : "removed"}`)
448
+ log(`${c.green("updated")} android/app/src/main/AndroidManifest.xml: ${what}`)
426
449
  }
427
450
 
451
+ /** app.json "urlScheme" → the tagged BROWSABLE intent-filter (`urlSchemeFilter`). */
452
+ export const restampManifestUrlScheme = (androidDir: string, urlScheme: string | undefined): void =>
453
+ restampManifestBlock(androidDir, "urlScheme", urlSchemeFilter(urlScheme),
454
+ `urlScheme ${urlScheme ? `→ ${urlScheme}://` : "removed"}`,
455
+ `add an intent-filter for the "${urlScheme}" scheme yourself.`)
456
+
457
+ /** app.json "appLinks" → the tagged autoVerify intent-filters (`appLinksFilter`). */
458
+ export const restampManifestAppLinks = (androidDir: string, links: AppLink[]): void =>
459
+ restampManifestBlock(androidDir, "appLinks", appLinksFilter(links),
460
+ `appLinks ${links.length ? `→ ${links.map(l => l.url).join(", ")}` : "removed"}`,
461
+ `add the autoVerify intent-filters for ${links.map(l => l.url).join(", ")} yourself.`,
462
+ [/<!-- \/lecodes:urlScheme -->/])
463
+
428
464
  /** Values-only restamps + tagged-line management in the USER-owned app/build.gradle.kts —
429
465
  * the Android analog of the pbxproj MARKETING_VERSION restamp + CODE_SIGN_ENTITLEMENTS
430
466
  * injection: warn-not-fail when the user restructured, and the ONLY lines sync ever removes
@@ -1025,9 +1061,22 @@ export const doctorAndroidChecks = async (
1025
1061
  check(existsSync(manifest) && readFileSync(manifest, "utf8").includes("io.letary.lecodes.UPDATE_URL"), "OTA update URL in the runtime manifest", "run `lecodes app sync`", false)
1026
1062
  const appSrc = join(androidDir, "app", "src", "main", "java")
1027
1063
  const kt = existsSync(appSrc) ? readdirSync(appSrc, { recursive: true }).map(String).filter((f) => f.endsWith(".kt")) : []
1028
- const boots = kt.some((f) => readFileSync(join(appSrc, f), "utf8").includes("LeCodesShell.boot"))
1064
+ const sources = kt.map((f) => readFileSync(join(appSrc, f), "utf8"))
1065
+ const boots = sources.some((src) => src.includes("LeCodesShell.boot"))
1029
1066
  check(boots, "App code launches via the updater (LeCodesShell.boot)",
1030
- "no app/src/main/java/**/*.kt calls LeCodesShell.boot(engine, context) — downloaded updates would never run; see \"Over-the-air updates\" in android/README.md for the AppViewModel snippet", false)
1067
+ "no app/src/main/java/**/*.kt calls LeCodesShell.boot(engine, activity) — downloaded updates would never run; see \"Over-the-air updates\" in android/README.md for the AppViewModel snippet", false)
1068
+ // The context form (the pre-wait shape, `boot(engine, getApplication())`) still runs downloads,
1069
+ // but only from the NEXT launch: no view to hold, so the first launch of a build never waits.
1070
+ const waits = sources.some((src) => /LeCodesShell\.boot\(\s*engine\s*,\s*activity\b/.test(src))
1071
+ if (boots) check(waits, "The first launch waits for the newest bundle (LeCodesShell.boot(engine, activity))",
1072
+ "AppViewModel calls the context form of LeCodesShell.boot — pass the activity instead (onActivityCreate's parameter) so a fresh install runs the newest bundle at once; see \"Over-the-air updates\" in android/README.md", false)
1073
+ }
1074
+
1075
+ if (config.splash !== undefined) {
1076
+ // The splash lives in Theme.LeCodesRuntime (the runtime module); the user-owned app theme must
1077
+ // inherit it — a shell generated before the key existed is parented on AppCompat directly.
1078
+ const themeHint = androidSplashThemeHint(androidDir)
1079
+ check(!themeHint, "The app theme inherits the splash (Theme.LeCodesRuntime)", themeHint ?? "", false)
1031
1080
  }
1032
1081
 
1033
1082
  const signing = resolveAndroidSigning(androidDir)
@@ -3,18 +3,20 @@ import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statS
3
3
  import { homedir } from "node:os"
4
4
  import { basename, dirname, join, resolve } from "node:path"
5
5
  import { readManifest } from "../../project/manifest"
6
+ import { binaryName } from "../../plugin/manifest"
6
7
  import { compileShell, type BundleResource } from "../compile"
7
8
  import { CliError, UsageError, bool, c, defineCommand, log, note, str, success, warn } from "../../cli"
8
9
  import {
9
10
  ACCENT_COLOR_JSON, APP_DELEGATE, APP_ICON_JSON, ASSETS_ROOT_JSON, IOS_README, LAUNCH_STORYBOARD,
10
11
  SCENE_DELEGATE, SCHEME_XML, VIEW_CONTROLLER, entitlementsTemplate, infoPlistTemplate,
11
- pbxprojTemplate, runtimePackageTemplate, runtimeSwiftTemplate,
12
+ pbxprojTemplate, runtimeBinaryPath, runtimePackageTemplate, runtimeSwiftTemplate,
12
13
  type RuntimePlugin, type SdkVariant, type ShellIdentity,
13
14
  } from "./templates/ios"
14
15
  import {
15
16
  FIRST_PARTY_PLUGINS, SDK_VARIANTS, cmpVersion, ensureShellIgnored,
16
17
  fetchLatestSdkVersion, latestPluginTag, localPlugin, localPluginKey, pluginSpecsFor, projectRoot, readAppConfigAt, readJson, resolvePlugin, resolveSdkVersion, sdkSatisfies,
17
18
  resolveVariant, runTool, slugify, writeJson, VERSION_RE, type AppConfig, type LockEntry, resolveUpdateUrl, urlSchemeOf,
19
+ appLinksOf, appLinksSiteNote, associatedDomainsOf,
18
20
  } from "./shared"
19
21
  import { isLocalPluginSpec, type PluginEntry } from "lecodes-sdk/compile"
20
22
  import {
@@ -23,6 +25,14 @@ import {
23
25
  } from "./android"
24
26
  import { desktopBuild, initDesktop } from "./desktop"
25
27
  import { loadIconMaster, writeIosAppIcon } from "./icon"
28
+ import { loadSplashMaster, writeIosSplash } from "./splash"
29
+
30
+ /** The entitlements of app.json alone (init, before any plugin): the associated domains of
31
+ * "appLinks". Sync merges the plugins' values on top of these. */
32
+ const iosEntitlements = (base: Record<string, unknown>, config: AppConfig): Record<string, unknown> => {
33
+ const domains = associatedDomainsOf(appLinksOf(config))
34
+ return domains.length ? { ...base, "com.apple.developer.associated-domains": domains } : base
35
+ }
26
36
 
27
37
  /*
28
38
  * `lecodes app` — native app shells for a LeCodes project (docs/xcode-app-plan.md). This file is
@@ -68,6 +78,10 @@ const SDK_REPO = "letary/lecodes-ios-sdk"
68
78
  // it); this floor is only the last-resort fallback when the network is unavailable at init. A 1.x
69
79
  // shell is moved here by `lecodes app update` (its CLI-owned files are regenerated; its
70
80
  // App/ViewController.swift keeps compiling: LeCodesViewController and LeCodesUpdater exist in 2.x).
81
+ // TODO(docs/plans/ota-splash-plan.md step 1): raise to the first lecodes-ios-sdk release with the
82
+ // first-launch wait (LeCodesShell.boot's `firstLaunchTimeout` + the launch-screen hold of
83
+ // LeCodesShellViewController) — the owner fills the version in at release time. (An older SDK
84
+ // still builds the generated shell: it just never waits.)
71
85
  const SDK_MIN_VERSION = "2.0.7"
72
86
 
73
87
  // First lecodes-ios-sdk release with `LeCodesEngine.bootProjectUuid` — the shell's world
@@ -167,7 +181,7 @@ const initIos = async (opts: IosInitOptions): Promise<void> => {
167
181
  write("App/SceneDelegate.swift", SCENE_DELEGATE)
168
182
  write("App/ViewController.swift", VIEW_CONTROLLER)
169
183
  write("App/Info.plist", infoPlistTemplate({}, undefined, urlSchemeOf(config)))
170
- write("App/App.entitlements", entitlementsTemplate({}))
184
+ write("App/App.entitlements", entitlementsTemplate(iosEntitlements({}, config)))
171
185
  write("App/Base.lproj/LaunchScreen.storyboard", LAUNCH_STORYBOARD)
172
186
  write("App/Assets.xcassets/Contents.json", ASSETS_ROOT_JSON)
173
187
  write("App/Assets.xcassets/AccentColor.colorset/Contents.json", ACCENT_COLOR_JSON)
@@ -183,6 +197,7 @@ const initIos = async (opts: IosInitOptions): Promise<void> => {
183
197
  log(` ${c.bold("lecodes app sync")} embed the compiled bundle (+ plugins) into the shell`)
184
198
  log(` ${c.bold("open ios/App.xcodeproj")} then run on your device from Xcode`)
185
199
  log(` App icon: set app.json "icon" to a square ≥1024px image — sync renders every size (or drop a PNG into App/Assets.xcassets/AppIcon.appiconset/)`)
200
+ log(` Splash: set app.json "splash": {"background": "#RRGGBB", "image": "./splash.png"} — sync writes the launch screen (the image centred at 160 pt, "size" overrides; or edit App/Base.lproj/LaunchScreen.storyboard yourself)`)
186
201
  }
187
202
 
188
203
  const init = defineCommand({
@@ -256,7 +271,9 @@ const syncIos = async (root: string, config: AppConfig): Promise<SyncArtifacts>
256
271
  const entitlementValues: Record<string, unknown[]> = {}
257
272
  const newLock: Record<string, LockEntry> = {}
258
273
 
259
- const sourcesDir = join(iosDir, "LeCodesRuntime", "Sources")
274
+ const runtimeDir = join(iosDir, "LeCodesRuntime")
275
+ const sourcesDir = join(runtimeDir, "Sources")
276
+ const binariesDir = join(runtimeDir, "Binaries")
260
277
  for (const [id, spec] of pluginSpecs) {
261
278
  const plugin = resolvePlugin(id, spec, oldLock[id], "ios")
262
279
  const ios = plugin.manifest.ios!
@@ -269,6 +286,11 @@ const syncIos = async (root: string, config: AppConfig): Promise<SyncArtifacts>
269
286
  const dest = join(sourcesDir, module)
270
287
  rmSync(dest, { recursive: true, force: true })
271
288
  cpSync(src, dest, { recursive: true })
289
+ // Its prebuilt xcframeworks (manifest ios.binaries) beside the sources, replaced the same way:
290
+ // a binary target's path is inside the package (SwiftPM takes none outside it).
291
+ const binaries = (ios.binaries ?? []).map(path => ({ path, name: binaryName(path) }))
292
+ rmSync(join(binariesDir, id), { recursive: true, force: true })
293
+ for (const b of binaries) cpSync(join(plugin.dir, b.path), join(runtimeDir, runtimeBinaryPath(id, b.name)), { recursive: true, verbatimSymlinks: true })
272
294
 
273
295
  // Hard error, not a warning: outside the plugin's `sdk` range the vendored sources reference
274
296
  // an SDK API the linked release does not have (or another contract major), so the shell
@@ -287,9 +309,9 @@ const syncIos = async (root: string, config: AppConfig): Promise<SyncArtifacts>
287
309
  throw new CliError(`Plugin "${id}": manifest ios.packages entries need { url, exact | from, products: [...] } — got ${JSON.stringify(pkg)}`)
288
310
  }
289
311
  }
290
- runtimePlugins.push({ id, module, register, packages: ios.packages })
312
+ runtimePlugins.push({ id, module, register, packages: ios.packages, binaries: binaries.map(b => b.name) })
291
313
  newLock[id] = plugin.lock
292
- log(`${c.green("plugin")} ${id}@${plugin.lock.version} (${plugin.lock.tag} → Sources/${module})`)
314
+ log(`${c.green("plugin")} ${id}@${plugin.lock.version} (${plugin.lock.tag} → Sources/${module}${binaries.length ? `, Binaries/${id}` : ""})`)
293
315
  }
294
316
 
295
317
  // Remove targets of uninstalled plugins (everything under Sources/ except the runtime itself).
@@ -300,6 +322,15 @@ const syncIos = async (root: string, config: AppConfig): Promise<SyncArtifacts>
300
322
  log(`${c.yellow("removed")} ios/LeCodesRuntime/Sources/${entry} (plugin uninstalled)`)
301
323
  }
302
324
  }
325
+ // …and their binaries (Binaries/<id>/); the folder goes with the last of them.
326
+ const installed = new Set(runtimePlugins.map(p => p.id))
327
+ for (const entry of existsSync(binariesDir) ? readdirSync(binariesDir) : []) {
328
+ if (!installed.has(entry) && statSync(join(binariesDir, entry)).isDirectory()) {
329
+ rmSync(join(binariesDir, entry), { recursive: true })
330
+ log(`${c.yellow("removed")} ios/LeCodesRuntime/Binaries/${entry} (plugin uninstalled)`)
331
+ }
332
+ }
333
+ if (existsSync(binariesDir) && readdirSync(binariesDir).length === 0) rmSync(binariesDir, { recursive: true })
303
334
 
304
335
  // ---- bundle: compile self-contained into Resources/, then embed the assets it references ----
305
336
  const remotePatterns = config.resources?.remote ?? []
@@ -375,7 +406,7 @@ const syncIos = async (root: string, config: AppConfig): Promise<SyncArtifacts>
375
406
  for (const [key, values] of Object.entries(infoValues)) mergedKeys[key] = [...new Set(values)].join(" ")
376
407
  const updateUrl = resolveUpdateUrl(config.update, root, sdkVersion)
377
408
  writeFileSync(join(iosDir, "App", "Info.plist"), infoPlistTemplate(mergedKeys, updateUrl, urlSchemeOf(config)))
378
- if (updateUrl) log(`${c.green("update")} OTA source ${updateUrl} — checked natively at launch, new code applies next launch${config.update === undefined ? c.dim(' ("update": false in app.json turns it off)') : ""}`)
409
+ if (updateUrl) log(`${c.green("update")} OTA source ${updateUrl} — checked natively at launch: the first launch of a build waits (≤ 5 s) for the newest bundle, later launches apply new code next launch${config.update === undefined ? c.dim(' ("update": false in app.json turns it off)') : ""}`)
379
410
  else if (config.update === false) log(`${c.green("update")} off (app.json "update": false)`)
380
411
  else note(`no OTA: a local-only project has no bundle on the platform — push it, or set "update": {"url": "https://…/app.js"} for custom hosting`)
381
412
 
@@ -393,6 +424,13 @@ const syncIos = async (root: string, config: AppConfig): Promise<SyncArtifacts>
393
424
  // get the CODE_SIGN_ENTITLEMENTS reference injected the first time a plugin brings a
394
425
  // capability — the ONE structural pbxproj edit sync ever makes, idempotent and line-shaped.
395
426
  const mergedEntitlements: Record<string, unknown> = {}
427
+ // app.json "appLinks" → associated-domains, merged like a plugin's array value (a plugin may
428
+ // declare the key too, e.g. a web-credentials domain). The key gone = the domains gone: the
429
+ // file is regenerated whole.
430
+ const appLinks = appLinksOf(config)
431
+ if (appLinks.length) {
432
+ (entitlementValues["com.apple.developer.associated-domains"] ??= []).unshift(associatedDomainsOf(appLinks))
433
+ }
396
434
  for (const [key, values] of Object.entries(entitlementValues)) {
397
435
  if (values.every(v => Array.isArray(v))) {
398
436
  const seen = new Set<string>()
@@ -405,6 +443,10 @@ const syncIos = async (root: string, config: AppConfig): Promise<SyncArtifacts>
405
443
  mergedEntitlements[key] = values[0]
406
444
  }
407
445
  }
446
+ if (appLinks.length) {
447
+ log(`${c.green("links")} ${appLinks.map(l => l.url).join(", ")} — verified by the site's proof:`)
448
+ for (const line of appLinksSiteNote(appLinks, { platform: "ios", teamId: config.ios?.teamId, bundleId: config.ios?.bundleId })) log(` ${line}`)
449
+ }
408
450
  const hasEntitlements = Object.keys(mergedEntitlements).length > 0
409
451
  if (pbx.includes("CODE_SIGN_ENTITLEMENTS") || hasEntitlements) {
410
452
  writeFileSync(join(iosDir, "App", "App.entitlements"), entitlementsTemplate(mergedEntitlements))
@@ -426,6 +468,11 @@ const syncIos = async (root: string, config: AppConfig): Promise<SyncArtifacts>
426
468
  if (config.icon) {
427
469
  await writeIosAppIcon(iosDir, await loadIconMaster(root, config))
428
470
  }
471
+ // ---- splash: the launch storyboard from app.json "splash" when set (it becomes CLI-owned with
472
+ // its colorset / imageset) — absent, the storyboard stays the person's ----
473
+ if (config.splash !== undefined) {
474
+ await writeIosSplash(iosDir, await loadSplashMaster(root, config))
475
+ }
429
476
 
430
477
  log("")
431
478
  success(`Shell synced: app.js + ${runtimePlugins.length} plugin(s), SDK ${sdkVersion}, variant ${variant}, v${config.version ?? "1.0.0"} (${config.ios.buildNumber ?? 1}).`)
@@ -437,7 +484,7 @@ const sync = defineCommand({
437
484
  name: "sync",
438
485
  summary: "Make every existing shell match the project: bundle + assets + SDK variant + plugins + CLI-owned files",
439
486
  usage: "[ios|android]",
440
- description: "Compiles the bundle and embeds the assets it references (ios/App/Resources/ resp. the android assets/; offline by default, also for server projects — app.json \"resources\": {\"remote\": [globs]} keeps heavy assets streaming), picks the SDK variant from the bundle's engine use, vendors the app.json plugins (pinned in plugins.lock.json) and regenerates the CLI-owned shell files. User files (ios/App/*.swift, android/app/) are never touched.\n\nNative OTA JS updates at launch (both shells; the current launch runs the newest LOCAL bundle, a download applies on the next): on by default for a pushed project (its bundle on the platform, the launcher's own URL), app.json \"update\": {\"url\": …} for custom hosting, \"update\": false turns it off. A download runs only when it is of the shell's SDK major and not older than the bundle running. app.json \"icon\" (a square ≥1024px image) renders the app icons every sync (\"iconBackground\": the adaptive / flatten backdrop). app.json \"urlScheme\" (\"myshop\") is the scheme the shells open on: iOS CFBundleURLTypes, Android a BROWSABLE intent-filter — a link arrives as app.launchUrl / the \"url\" event (the return of Browser.open with a returnUrl). No platform = every shell that exists; a dual-shell project compiles once.",
487
+ description: "Compiles the bundle and embeds the assets it references (ios/App/Resources/ resp. the android assets/; offline by default, also for server projects — app.json \"resources\": {\"remote\": [globs]} keeps heavy assets streaming), picks the SDK variant from the bundle's engine use, vendors the app.json plugins (pinned in plugins.lock.json) and regenerates the CLI-owned shell files. User files (ios/App/*.swift, android/app/) are never touched.\n\nNative OTA JS updates at launch (both shells): the first launch of an installed build waits (≤ 5 s, the splash held) for the check and runs the newest bundle (a source not newer than the build answers 304 to the build's compile date: nothing downloaded, the embedded bundle runs); every later launch runs the newest LOCAL bundle and a download applies on the next. On by default for a pushed project (its bundle on the platform, the launcher's own URL), app.json \"update\": {\"url\": …} for custom hosting, \"update\": false turns it off. A download runs only when it is of the shell's SDK major and not older than the bundle running; a store update keeps a downloaded bundle only when it is newer code than the one it ships. app.json \"icon\" (a square ≥1024px image) renders the app icons every sync (\"iconBackground\": the adaptive / flatten backdrop). app.json \"splash\" ({\"background\": \"#RRGGBB\", \"image\": \"./splash.png\", \"size\": 160}, all optional) renders the launch screen of both shells every sync — the colour with the image fitted centred into a size × size pt / dp square, 160 by default (iOS LaunchScreen.storyboard, Android the system splash of Theme.LeCodesRuntime, which the app's theme inherits). app.json \"urlScheme\" (\"myshop\") is the scheme the shells open on: iOS CFBundleURLTypes, Android a BROWSABLE intent-filter — a link arrives as app.launchUrl / the \"url\" event (the return of Browser.open with a returnUrl). app.json \"appLinks\" ([\"https://shop.example/pay/\"]) are the verified https links they open on — iOS universal links (associated-domains in App.entitlements), Android App Links (autoVerify intent-filters in the manifest); sync prints the apple-app-site-association / assetlinks.json each site must serve. No platform = every shell that exists; a dual-shell project compiles once.",
441
488
  examples: ["lecodes app sync", "lecodes app sync android"],
442
489
  run: ({ args }) => runSync(args[0]),
443
490
  })
@@ -766,7 +813,7 @@ const doctorIosChecks = async (root: string, config: AppConfig, check: Check) =>
766
813
  const doctor = defineCommand({
767
814
  name: "doctor",
768
815
  summary: "Preflight checks for building / uploading the shells",
769
- description: "Toolchain (Xcode / JDK + Android SDK), the synced bundle, SDK variant + version vs app.json, plugin lock, signing (team id / release keystore), icons, OTA wiring and the App Store Connect key. Fatal checks fail the command (exit 1); soft ones print a `!` and a hint.",
816
+ description: "Toolchain (Xcode / JDK + Android SDK), the synced bundle, SDK variant + version vs app.json, plugin lock, signing (team id / release keystore), icons, the splash theme (Android), OTA wiring and the App Store Connect key. Fatal checks fail the command (exit 1); soft ones print a `!` and a hint.",
770
817
  examples: ["lecodes app doctor"],
771
818
  run: async () => {
772
819
  const root = projectRoot()
@@ -782,6 +829,9 @@ const doctor = defineCommand({
782
829
  if (config?.icon) {
783
830
  check(existsSync(join(root, config.icon)), `Icon master ${config.icon}`, `app.json "icon" points at a missing file — sync will fail`)
784
831
  }
832
+ if (typeof config?.splash?.image === "string") {
833
+ check(existsSync(join(root, config.splash.image)), `Splash image ${config.splash.image}`, `app.json "splash.image" points at a missing file — sync will fail`)
834
+ }
785
835
  if (hasIos) await doctorIosChecks(root, config!, check)
786
836
  if (hasAndroid) {
787
837
  await doctorAndroidChecks(root, config!, check)