@limrun/ui 0.15.1 → 0.16.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.
@@ -0,0 +1 @@
1
+ "use strict";require('./index.css');const A=require("react/jsx-runtime"),f=require("react"),hn=require("react-dom");function Dr(n){var t,c,a="";if(typeof n=="string"||typeof n=="number")a+=n;else if(typeof n=="object")if(Array.isArray(n)){var E=n.length;for(t=0;t<E;t++)n[t]&&(c=Dr(n[t]))&&(a&&(a+=" "),a+=c)}else for(c in n)n[c]&&(a&&(a+=" "),a+=c);return a}function De(){for(var n,t,c=0,a="",E=arguments.length;c<E;c++)(n=arguments[c])&&(t=Dr(n))&&(a&&(a+=" "),a+=t);return a}const wt={INJECT_KEYCODE:0,INJECT_TOUCH_EVENT:2,INJECT_TWO_FINGER_TOUCH_EVENT:18,SET_CLIPBOARD:9},U={ACTION_DOWN:0,ACTION_UP:1,ACTION_MOVE:2,ACTION_CANCEL:3,ACTION_POINTER_DOWN:5,ACTION_POINTER_UP:6,BUTTON_PRIMARY:1,BUTTON_SECONDARY:2,BUTTON_TERTIARY:4},s={ACTION_DOWN:0,ACTION_UP:1,META_NONE:0,META_SHIFT_ON:1,META_ALT_ON:2,META_CTRL_ON:4096,META_META_ON:65536,KEYCODE_0:7,KEYCODE_1:8,KEYCODE_2:9,KEYCODE_3:10,KEYCODE_4:11,KEYCODE_5:12,KEYCODE_6:13,KEYCODE_7:14,KEYCODE_8:15,KEYCODE_9:16,DPAD_UP:19,DPAD_DOWN:20,DPAD_LEFT:21,DPAD_RIGHT:22,KEYCODE_A:29,KEYCODE_B:30,KEYCODE_C:31,KEYCODE_D:32,KEYCODE_E:33,KEYCODE_F:34,KEYCODE_G:35,KEYCODE_H:36,KEYCODE_I:37,KEYCODE_J:38,KEYCODE_K:39,KEYCODE_L:40,KEYCODE_M:41,KEYCODE_N:42,KEYCODE_O:43,KEYCODE_P:44,KEYCODE_Q:45,KEYCODE_R:46,KEYCODE_S:47,KEYCODE_T:48,KEYCODE_U:49,KEYCODE_V:50,KEYCODE_W:51,KEYCODE_X:52,KEYCODE_Y:53,KEYCODE_Z:54,KEYCODE_COMMA:55,KEYCODE_PERIOD:56,KEYCODE_ALT_LEFT:57,KEYCODE_ALT_RIGHT:58,KEYCODE_SHIFT_LEFT:59,KEYCODE_SHIFT_RIGHT:60,KEYCODE_TAB:61,KEYCODE_SPACE:62,ENTER:66,DEL:67,KEYCODE_GRAVE:68,KEYCODE_MINUS:69,KEYCODE_EQUALS:70,KEYCODE_LEFT_BRACKET:71,KEYCODE_RIGHT_BRACKET:72,KEYCODE_BACKSLASH:73,KEYCODE_SEMICOLON:74,KEYCODE_APOSTROPHE:75,KEYCODE_SLASH:76,MENU:82,KEYCODE_PAGE_UP:92,KEYCODE_PAGE_DOWN:93,KEYCODE_ESCAPE:111,FORWARD_DEL:112,KEYCODE_CTRL_LEFT:113,KEYCODE_CTRL_RIGHT:114,KEYCODE_META_LEFT:117,KEYCODE_META_RIGHT:118,KEYCODE_MOVE_HOME:122,KEYCODE_MOVE_END:123,KEYCODE_INSERT:124,KEYCODE_F1:131,KEYCODE_F2:132,KEYCODE_F3:133,KEYCODE_F4:134,KEYCODE_F5:135,KEYCODE_F6:136,KEYCODE_F7:137,KEYCODE_F8:138,KEYCODE_F9:139,KEYCODE_F10:140,KEYCODE_F11:141,KEYCODE_F12:142,KEYCODE_NUMPAD_0:144,KEYCODE_NUMPAD_1:145,KEYCODE_NUMPAD_2:146,KEYCODE_NUMPAD_3:147,KEYCODE_NUMPAD_4:148,KEYCODE_NUMPAD_5:149,KEYCODE_NUMPAD_6:150,KEYCODE_NUMPAD_7:151,KEYCODE_NUMPAD_8:152,KEYCODE_NUMPAD_9:153,KEYCODE_NUMPAD_DIVIDE:154,KEYCODE_NUMPAD_MULTIPLY:155,KEYCODE_NUMPAD_SUBTRACT:156,KEYCODE_NUMPAD_ADD:157,KEYCODE_NUMPAD_DOT:158,KEYCODE_NUMPAD_COMMA:159,KEYCODE_NUMPAD_ENTER:160,KEYCODE_NUMPAD_EQUALS:161},Rr={KeyA:s.KEYCODE_A,KeyB:s.KEYCODE_B,KeyC:s.KEYCODE_C,KeyD:s.KEYCODE_D,KeyE:s.KEYCODE_E,KeyF:s.KEYCODE_F,KeyG:s.KEYCODE_G,KeyH:s.KEYCODE_H,KeyI:s.KEYCODE_I,KeyJ:s.KEYCODE_J,KeyK:s.KEYCODE_K,KeyL:s.KEYCODE_L,KeyM:s.KEYCODE_M,KeyN:s.KEYCODE_N,KeyO:s.KEYCODE_O,KeyP:s.KEYCODE_P,KeyQ:s.KEYCODE_Q,KeyR:s.KEYCODE_R,KeyS:s.KEYCODE_S,KeyT:s.KEYCODE_T,KeyU:s.KEYCODE_U,KeyV:s.KEYCODE_V,KeyW:s.KEYCODE_W,KeyX:s.KEYCODE_X,KeyY:s.KEYCODE_Y,KeyZ:s.KEYCODE_Z,Digit0:s.KEYCODE_0,Digit1:s.KEYCODE_1,Digit2:s.KEYCODE_2,Digit3:s.KEYCODE_3,Digit4:s.KEYCODE_4,Digit5:s.KEYCODE_5,Digit6:s.KEYCODE_6,Digit7:s.KEYCODE_7,Digit8:s.KEYCODE_8,Digit9:s.KEYCODE_9,Backquote:s.KEYCODE_GRAVE,Minus:s.KEYCODE_MINUS,Equal:s.KEYCODE_EQUALS,BracketLeft:s.KEYCODE_LEFT_BRACKET,BracketRight:s.KEYCODE_RIGHT_BRACKET,Backslash:s.KEYCODE_BACKSLASH,Semicolon:s.KEYCODE_SEMICOLON,Quote:s.KEYCODE_APOSTROPHE,Comma:s.KEYCODE_COMMA,Period:s.KEYCODE_PERIOD,Slash:s.KEYCODE_SLASH,Space:s.KEYCODE_SPACE,Tab:s.KEYCODE_TAB,Escape:s.KEYCODE_ESCAPE,ArrowUp:s.DPAD_UP,ArrowDown:s.DPAD_DOWN,ArrowLeft:s.DPAD_LEFT,ArrowRight:s.DPAD_RIGHT,Enter:s.ENTER,Backspace:s.DEL,Delete:s.FORWARD_DEL,Home:s.KEYCODE_MOVE_HOME,End:s.KEYCODE_MOVE_END,PageUp:s.KEYCODE_PAGE_UP,PageDown:s.KEYCODE_PAGE_DOWN,Insert:s.KEYCODE_INSERT,F1:s.KEYCODE_F1,F2:s.KEYCODE_F2,F3:s.KEYCODE_F3,F4:s.KEYCODE_F4,F5:s.KEYCODE_F5,F6:s.KEYCODE_F6,F7:s.KEYCODE_F7,F8:s.KEYCODE_F8,F9:s.KEYCODE_F9,F10:s.KEYCODE_F10,F11:s.KEYCODE_F11,F12:s.KEYCODE_F12,ShiftLeft:s.KEYCODE_SHIFT_LEFT,ShiftRight:s.KEYCODE_SHIFT_RIGHT,ControlLeft:s.KEYCODE_CTRL_LEFT,ControlRight:s.KEYCODE_CTRL_RIGHT,AltLeft:s.KEYCODE_ALT_LEFT,AltRight:s.KEYCODE_ALT_RIGHT,MetaLeft:s.KEYCODE_META_LEFT,MetaRight:s.KEYCODE_META_RIGHT,ContextMenu:s.MENU,Numpad0:s.KEYCODE_NUMPAD_0,Numpad1:s.KEYCODE_NUMPAD_1,Numpad2:s.KEYCODE_NUMPAD_2,Numpad3:s.KEYCODE_NUMPAD_3,Numpad4:s.KEYCODE_NUMPAD_4,Numpad5:s.KEYCODE_NUMPAD_5,Numpad6:s.KEYCODE_NUMPAD_6,Numpad7:s.KEYCODE_NUMPAD_7,Numpad8:s.KEYCODE_NUMPAD_8,Numpad9:s.KEYCODE_NUMPAD_9,NumpadDivide:s.KEYCODE_NUMPAD_DIVIDE,NumpadMultiply:s.KEYCODE_NUMPAD_MULTIPLY,NumpadSubtract:s.KEYCODE_NUMPAD_SUBTRACT,NumpadAdd:s.KEYCODE_NUMPAD_ADD,NumpadDecimal:s.KEYCODE_NUMPAD_DOT,NumpadComma:s.KEYCODE_NUMPAD_COMMA,NumpadEnter:s.KEYCODE_NUMPAD_ENTER,NumpadEqual:s.KEYCODE_NUMPAD_EQUALS};function vt(n,t,c,a,E,p,C=1,O=0,K=0){const T=new ArrayBuffer(32),M=new DataView(T);let N=0;return M.setUint8(N,wt.INJECT_TOUCH_EVENT),N+=1,M.setUint8(N,n),N+=1,M.setBigInt64(N,BigInt(t),!0),N+=8,M.setInt32(N,Math.round(E),!0),N+=4,M.setInt32(N,Math.round(p),!0),N+=4,M.setUint16(N,c,!0),N+=2,M.setUint16(N,a,!0),N+=2,M.setInt16(N,Math.round(C*65535),!0),N+=2,M.setInt32(N,O,!0),N+=4,M.setInt32(N,K,!0),T}function mn(n,t,c,a,E,p,C){const O=new ArrayBuffer(24),K=new DataView(O);let T=0;return K.setUint8(T,wt.INJECT_TWO_FINGER_TOUCH_EVENT),T+=1,K.setUint8(T,n),T+=1,K.setInt32(T,Math.round(a),!0),T+=4,K.setInt32(T,Math.round(E),!0),T+=4,K.setInt32(T,Math.round(p),!0),T+=4,K.setInt32(T,Math.round(C),!0),T+=4,K.setUint16(T,t,!0),T+=2,K.setUint16(T,c,!0),T+=2,K.setUint16(T,0,!0),O}function En(n,t=!0){const a=new TextEncoder().encode(n),E=new ArrayBuffer(14+a.length),p=new DataView(E);let C=0;return p.setUint8(C,wt.SET_CLIPBOARD),C+=1,p.setBigInt64(C,BigInt(0),!1),C+=8,p.setUint8(C,t?1:0),C+=1,p.setUint32(C,a.length,!1),C+=4,new Uint8Array(E,C).set(a),E}function it(n,t,c=0,a=0){const E=new ArrayBuffer(14),p=new DataView(E);let C=0;return p.setUint8(C,wt.INJECT_KEYCODE),C+=1,p.setUint8(C,n),C+=1,p.setInt32(C,t,!0),C+=4,p.setInt32(C,c,!0),C+=4,p.setInt32(C,a,!0),E}const Dt="Accessibility unavailable on this device.",Rt=500,wr=(n,t)=>Math.abs(n.x-t.x)<.5&&Math.abs(n.y-t.y)<.5&&Math.abs(n.width-t.width)<.5&&Math.abs(n.height-t.height)<.5,pn=n=>Math.max(0,n.width)*Math.max(0,n.height),gn=n=>{const t=n.find(c=>c.frame&&c.frame.width>0&&c.frame.height>0);return t?.frame?((Math.abs(t.frame.x)>.5||Math.abs(t.frame.y)>.5)&&console.warn(`[ax-tree] iOS root frame is not anchored at (0,0): (${t.frame.x},${t.frame.y}). Element positions may be slightly off from the rendered video.`),{x:0,y:0,width:t.frame.width,height:t.frame.height}):{x:0,y:0,width:1,height:1}},Cn=n=>{const t={};return typeof n.AXUniqueId=="string"&&n.AXUniqueId.length>0&&(t.AXUniqueId=n.AXUniqueId),typeof n.AXLabel=="string"&&n.AXLabel.length>0&&(t.AXLabel=n.AXLabel),typeof n.type=="string"&&n.type.length>0&&(t.className=n.type),t},yn=n=>{const t={};for(const[c,a]of Object.entries(n))c!=="children"&&(t[c]=a);return t};function Ar(n){const t=Array.isArray(n)?n:[n],c=gn(t),a=[],E=(p,C)=>{if(a.length>=Rt)return;const O=p.frame;if(O&&O.width>0&&O.height>0&&!wr(O,c)){const T=p.role_description||p.role||"",M=(typeof p.AXLabel=="string"?p.AXLabel:"")||(typeof p.title=="string"?p.title:"")||"";a.push({id:typeof p.AXUniqueId=="string"&&p.AXUniqueId.length>0?p.AXUniqueId:C,path:C,label:M,value:typeof p.AXValue=="string"?p.AXValue:"",role:T,type:typeof p.type=="string"?p.type:"",enabled:p.enabled!==!1,focused:p.focused===!0,frame:{x:O.x,y:O.y,width:O.width,height:O.height},selectors:Cn(p),raw:yn(p)})}const K=Array.isArray(p.children)?p.children:[];for(let T=0;T<K.length&&a.length<Rt;T++)E(K[T],`${C}.${T}`)};for(let p=0;p<t.length&&a.length<Rt;p++)E(t[p],String(p));return{platform:"ios",screen:{width:c.width,height:c.height},elements:a,capturedAt:Date.now()}}const On=n=>{let t={x:0,y:0,width:1,height:1},c=0;for(const a of n){const E=a.parsedBounds;if(!E)continue;const p=E.right-E.left,C=E.bottom-E.top,O=Math.max(0,p)*Math.max(0,C);O>c&&(c=O,t={x:0,y:0,width:p,height:C})}return t},_n=n=>{const t={};return typeof n.resourceId=="string"&&n.resourceId.length>0&&(t.resourceId=n.resourceId),typeof n.contentDesc=="string"&&n.contentDesc.length>0&&(t.contentDesc=n.contentDesc),typeof n.text=="string"&&n.text.length>0&&(t.text=n.text),typeof n.className=="string"&&n.className.length>0&&(t.className=n.className),t};function Sr(n){const t=On(n),c=[];for(let a=0;a<n.length&&c.length<Rt;a++){const E=n[a],p=E.parsedBounds;if(!p)continue;const C=Math.max(0,p.right-p.left),O=Math.max(0,p.bottom-p.top);if(C<=0||O<=0)continue;const K={x:p.left,y:p.top,width:C,height:O};if(wr(K,{x:0,y:0,width:t.width,height:t.height}))continue;const T=E.contentDesc||E.text||"",M=E.className||"";c.push({id:E.resourceId&&E.resourceId.length>0?E.resourceId:E.contentDesc&&E.contentDesc.length>0?`cd:${E.contentDesc}`:String(a),path:String(a),label:T,value:typeof E.text=="string"?E.text:"",role:M,type:M,enabled:E.enabled!==!1,focused:E.focused===!0,frame:K,selectors:_n(E),raw:{...E}})}return{platform:"android",screen:{width:t.width,height:t.height},elements:c,capturedAt:Date.now()}}function Mr(n,t){const c=Math.max(0,n.x),a=Math.max(0,n.y),E=Math.min(t.width,n.x+n.width),p=Math.min(t.height,n.y+n.height),C=Math.max(0,E-c),O=Math.max(0,p-a);return C>0&&O>0?{x:c,y:a,width:C,height:O}:null}function Ht(n,t){if(n===t)return!0;if(n.id!==t.id||n.path!==t.path||n.label!==t.label||n.value!==t.value||n.role!==t.role||n.type!==t.type||n.enabled!==t.enabled||n.focused!==t.focused)return!1;const c=n.frame,a=t.frame;return c.x===a.x&&c.y===a.y&&c.width===a.width&&c.height===a.height}function $t(n,t){if(n===t)return!0;if(!n||!t||n.platform!==t.platform||n.screen.width!==t.screen.width||n.screen.height!==t.screen.height||n.elements.length!==t.elements.length)return!1;for(let c=0;c<n.elements.length;c++)if(!Ht(n.elements[c],t.elements[c]))return!1;return!0}function br(n,t,c){let a=null,E=1/0;for(const p of n.elements){const C=p.frame;if(t<C.x||c<C.y||t>C.x+C.width||c>C.y+C.height)continue;const O=pn(C);O<E&&(E=O,a=p)}return a}function Nr(n,t){if(t==="ios"){if(n.selectors.AXUniqueId)return`client.tapElement({ AXUniqueId: ${JSON.stringify(n.selectors.AXUniqueId)} })`;if(n.selectors.AXLabel){const c=n.selectors.className?`, type: ${JSON.stringify(n.selectors.className)}`:"";return`client.tapElement({ AXLabel: ${JSON.stringify(n.selectors.AXLabel)}${c} })`}return null}return n.selectors.resourceId?`client.tap({ selector: { resourceId: ${JSON.stringify(n.selectors.resourceId)} } })`:n.selectors.contentDesc?`client.tap({ selector: { contentDesc: ${JSON.stringify(n.selectors.contentDesc)} } })`:n.selectors.text?`client.tap({ selector: { text: ${JSON.stringify(n.selectors.text)} } })`:null}function Ir(n){const t=n.role||n.type||"";if(!t)return"element";let c=t.replace(/^AX/,"");return c.includes(".")&&(c=c.split(".").pop()),c==="GenericElement"?"Element":(c=c.replace(/([a-z])([A-Z])/g,"$1 $2"),c)}const yr=80;function Xt(n){const t=Ir(n),c=n.label||n.value||"";if(!c)return t;const a=c.length>yr?c.slice(0,yr).trimEnd()+"…":c;return`${t} · ${a}`}const vn=500,Tn=2e3,Or=5e3,Dn=8e3,Rn=1200,wn=250,An=()=>`ax-rc-${Date.now()}-${Math.random().toString(36).slice(2,8)}`;class Sn{platform;send;onSnapshot;onStatusChange;baseIntervalMs;maxBackoffMs;androidElementTreeOptions;pending=new Map;running=!1;timer;currentInterval;lastSnapshot=null;inflight=!1;lastBumpAtMs=0;boostUntilMs=0;status="idle";constructor(t){this.platform=t.platform,this.send=t.send,this.onSnapshot=t.onSnapshot,this.onStatusChange=t.onStatusChange,this.baseIntervalMs=t.baseIntervalMs??vn,this.maxBackoffMs=t.maxBackoffMs??Tn,this.androidElementTreeOptions=t.androidElementTreeOptions,this.currentInterval=this.baseIntervalMs}start(){this.running||(this.running=!0,this.currentInterval=this.baseIntervalMs,this.boostUntilMs=0,this.setStatus("starting"),this.runOnce())}stop(){this.running&&(this.running=!1,this.timer!==void 0&&(window.clearTimeout(this.timer),this.timer=void 0),this.failAllPending("Inspector stopped"),this.lastSnapshot=null,this.setStatus("idle"),this.onSnapshot(null))}getStatus(){return this.status}setStatus(t,c){if(this.status!==t&&(this.status=t,!!this.onStatusChange))try{this.onStatusChange(t,c)}catch(a){console.error("[AxFetcher] onStatusChange threw:",a)}}bumpActivity(){if(!this.running)return;const t=Date.now();this.boostUntilMs=t+Rn;const c=Math.max(150,Math.floor(this.baseIntervalMs/2));if(t-this.lastBumpAtMs<c){this.currentInterval=this.baseIntervalMs;return}this.lastBumpAtMs=t,this.currentInterval=this.baseIntervalMs,this.timer!==void 0&&(window.clearTimeout(this.timer),this.timer=void 0),this.inflight||this.runOnce()}async refresh(){const t=await this.requestOnce();return this.running&&this.deliver(t),t}getLatest(){return this.lastSnapshot}handleMessage(t){if(!t||typeof t.id!="string")return!1;const c=t.id;if(!this.pending.get(c))return!1;if(this.platform==="ios"){if(t.type!=="elementTreeResult")return!1;const O=typeof t.error=="string"?t.error:null;if(O)return this.settleReject(c,new Error(O)),!0;const K=typeof t.json=="string"?t.json:"";try{const T=JSON.parse(K),M=Ar(T);this.settleResolve(c,M)}catch(T){this.settleReject(c,T instanceof Error?T:new Error(String(T)))}return!0}if(t.type!=="getElementTreeResult")return!1;const E=t.error;if(E&&typeof E=="object"){const O=typeof E.message=="string"?E.message:"getElementTree failed";return this.settleReject(c,new Error(O)),!0}const p=t.payload,C=Array.isArray(p?.nodes)?p.nodes:[];try{const O=Sr(C);this.settleResolve(c,O)}catch(O){this.settleReject(c,O instanceof Error?O:new Error(String(O)))}return!0}buildRequest(t){return this.platform==="ios"?{type:"elementTree",id:t}:{type:"getElementTree",id:t,...this.androidElementTreeOptions}}async requestOnce(){const t=An();return new Promise((c,a)=>{const E=window.setTimeout(()=>{this.pending.delete(t),a(new Error("elementTree request timed out"))},Dn+(this.androidElementTreeOptions?.waitForIdleTimeoutMs??0));this.pending.set(t,{resolve:c,reject:a,timer:E}),this.send(this.buildRequest(t))||(window.clearTimeout(E),this.pending.delete(t),a(new Error("elementTree send failed (WebSocket not open)")))})}async runOnce(){if(this.running&&!this.inflight){this.inflight=!0;try{const t=await this.requestOnce();if(!this.running)return;this.deliver(t)}catch(t){if(!this.running)return;const c=t instanceof Error?t.message:String(t),a=/unavailable|not (yet )?running|failed|timeout/i.test(c);this.currentInterval=a?Or:Math.min(this.currentInterval*2,this.maxBackoffMs),this.setStatus(a?"unavailable":"error",c)}finally{this.inflight=!1,this.scheduleNext()}}}deliver(t){const c=this.lastSnapshot;if($t(c,t)){this.currentInterval=Math.min(this.currentInterval*2,this.maxBackoffMs),this.lastSnapshot=t,t.errors?.includes(Dt)||this.setStatus("ready");return}this.lastSnapshot=t,this.currentInterval=this.baseIntervalMs,t.errors?.includes(Dt)?(this.currentInterval=Or,this.setStatus("unavailable",Dt)):this.setStatus("ready"),this.onSnapshot(t)}scheduleNext(){if(!this.running)return;this.timer!==void 0&&window.clearTimeout(this.timer);const t=Date.now()<this.boostUntilMs?Math.min(wn,this.currentInterval):this.currentInterval;this.timer=window.setTimeout(()=>{this.timer=void 0,this.runOnce()},t)}settleResolve(t,c){const a=this.pending.get(t);a&&(window.clearTimeout(a.timer),this.pending.delete(t),a.resolve(c))}settleReject(t,c){const a=this.pending.get(t);a&&(window.clearTimeout(a.timer),this.pending.delete(t),a.reject(c))}failAllPending(t){for(const[c,a]of this.pending.entries())window.clearTimeout(a.timer),a.reject(new Error(t)),this.pending.delete(c)}}class Mn{generation=0;tail=Promise.resolve();get currentGeneration(){return this.generation}enqueue(t){const c=++this.generation;return this.append(c,t)}enqueueCurrent(t){return this.append(this.generation,t)}append(t,c){const a={generation:t,isCurrent:()=>t===this.generation},E=this.tail.then(()=>c(a));return this.tail=E.catch(()=>{}),E}invalidate(){++this.generation}}function bn(n){if(!n||typeof n!="object")return null;const t=n;if(t.type!=="cameraRequest"||typeof t.active!="boolean")return null;const c={active:t.active};return(t.facingMode==="user"||t.facingMode==="environment")&&(c.facingMode=t.facingMode),c}function Nn(n){const t={frameRate:{ideal:30,max:30}};return n?.facingMode&&(t.facingMode={ideal:n.facingMode}),t}function In(n,t){return!n||n.facingMode!==t.facingMode}function Yn(n){const t=n.getSettings();return{width:t.width,height:t.height,frameRate:t.frameRate,deviceId:t.deviceId,label:n.label||void 0,facingMode:t.facingMode}}function Kn(n){return{type:"cameraResult",granted:!0,camera:Yn(n)}}const kn=async n=>{if(!n)return!1;try{if(typeof navigator<"u"&&navigator.clipboard?.writeText)return await navigator.clipboard.writeText(n),!0}catch{}try{const t=document.createElement("textarea");t.value=n,t.style.position="fixed",t.style.opacity="0",document.body.appendChild(t),t.select();const c=document.execCommand("copy");return document.body.removeChild(t),c}catch{return!1}},Pn=f.memo(function({element:t,screen:c,highlighted:a,selected:E,selectable:p,onClick:C}){const O=Mr(t.frame,c);if(!O)return null;const K=Xt(t);return A.jsx("button",{type:"button",className:De("rc-inspect-box",!t.enabled&&"rc-inspect-box-disabled"),"data-ax-id":t.id,"data-ax-path":t.path,"data-ax-label":t.label||void 0,"data-ax-role":t.role||void 0,"data-highlighted":a?"true":"false","data-selected":E?"true":"false","aria-label":K,onClick:T=>{p&&(T.preventDefault(),T.stopPropagation(),C(t,{x:T.clientX,y:T.clientY}))},style:{left:`${O.x/c.width*100}%`,top:`${O.y/c.height*100}%`,width:`${O.width/c.width*100}%`,height:`${O.height/c.height*100}%`}})},(n,t)=>n.highlighted===t.highlighted&&n.selected===t.selected&&n.selectable===t.selectable&&n.screen.width===t.screen.width&&n.screen.height===t.screen.height&&n.onClick===t.onClick&&Ht(n.element,t.element)),Ln=260,Un=220,xn=6,Fn=8,Wn=f.memo(function({element:t,platform:c,anchor:a,cursorAnchored:E,showActions:p,onTap:C}){const[O,K]=f.useState(null);f.useEffect(()=>{if(!O)return;const X=window.setTimeout(()=>K(null),1100);return()=>window.clearTimeout(X)},[O]);const T=f.useMemo(()=>{const X=Ln,ne=Un,J=xn,$=Fn,fe=typeof window<"u"?window.innerWidth:1024,we=typeof window<"u"?window.innerHeight:768,F=a.x+J+X<=fe-$,he=a.y-J-ne>=$;let Xe=F?a.x+J:a.x-J,me=he?a.y-J:a.y+J;const At=`translate(${F?"0":"-100%"}, ${he?"-100%":"0"})`;return Xe=Math.max($+(F?0:X),Math.min(fe-$-(F?X:0),Xe)),me=Math.max($+(he?ne:0),Math.min(we-$-(he?0:ne),me)),{left:`${Xe}px`,top:`${me}px`,transform:At}},[a.x,a.y]),M=f.useMemo(()=>Nr(t,c),[t,c]),N=c==="ios"?t.selectors.AXUniqueId:t.selectors.resourceId,re=c==="ios"?"AXUniqueId":"resourceId",ae=f.useCallback(async(X,ne)=>{if(!X)return;await kn(X)&&K(ne)},[]),de=Ir(t),ct=t.label||t.value||de,Re=Xt(t);return hn.createPortal(A.jsxs("div",{className:"rc-inspect-card","data-cursor-anchored":E?"true":"false",style:T,onClick:X=>X.stopPropagation(),onMouseDown:X=>X.stopPropagation(),onMouseMove:X=>X.stopPropagation(),children:[A.jsxs("div",{className:"rc-inspect-card-header",children:[A.jsx("span",{className:"rc-inspect-card-role",children:de}),!t.enabled&&A.jsx("span",{className:"rc-inspect-card-tag",children:"disabled"}),t.focused&&A.jsx("span",{className:"rc-inspect-card-tag rc-inspect-card-tag-blue",children:"focused"})]}),A.jsx("p",{className:"rc-inspect-card-title",title:Re,children:ct}),N&&A.jsxs("div",{className:"rc-inspect-card-row",children:[A.jsxs("span",{className:"rc-inspect-card-row-label",children:[re,":"]}),A.jsx("span",{className:"rc-inspect-card-row-value",children:N})]}),t.value&&t.value!==t.label&&A.jsxs("div",{className:"rc-inspect-card-row",children:[A.jsx("span",{className:"rc-inspect-card-row-label",children:"value:"}),A.jsx("span",{className:"rc-inspect-card-row-value",children:t.value})]}),A.jsxs("div",{className:"rc-inspect-card-row",children:[A.jsx("span",{className:"rc-inspect-card-row-label",children:"frame:"}),A.jsxs("span",{className:"rc-inspect-card-row-value",children:[Math.round(t.frame.width),"×",Math.round(t.frame.height)," @ ",Math.round(t.frame.x),",",Math.round(t.frame.y)]})]}),p&&A.jsxs("div",{className:"rc-inspect-card-actions",children:[A.jsx("button",{type:"button",className:"rc-inspect-card-btn rc-inspect-card-btn-primary",onClick:()=>C(t,a),children:"Tap"}),A.jsx("button",{type:"button",className:De("rc-inspect-card-btn",O==="selector"&&"rc-inspect-card-btn-copied"),disabled:!M,title:M??"No usable selector for this element",onClick:()=>ae(M??void 0,"selector"),children:O==="selector"?"Copied!":"Copy selector"}),A.jsx("button",{type:"button",className:De("rc-inspect-card-btn",O==="id"&&"rc-inspect-card-btn-copied"),disabled:!N,title:N??`No ${re}`,onClick:()=>ae(N,"id"),children:O==="id"?"Copied!":`Copy ${re}`})]})]}),document.body)}),Hn=f.memo(function({snapshot:t,geometry:c,highlightedId:a,selectedId:E,mode:p,cursorPosition:C,frozenCursorPosition:O,onSelectChange:K,onTapElement:T}){const M=p==="select",N=M?a!==null&&a!==E:a!==null,re=M?a??E:a,ae=f.useMemo(()=>{const $=new Map;if(!t)return $;for(const fe of t.elements)$.set(fe.id,fe);return $},[t]),de=f.useMemo(()=>re?ae.get(re)??null:null,[re,ae]);if(!(!!t&&!!c&&t.screen.width>0&&t.screen.height>0&&t.elements.length>0))return null;const Re=N?C:O??C,X=N,ne=M&&!N&&E!==null,J=$=>{M&&$.target===$.currentTarget&&K(null,null)};return A.jsxs(A.Fragment,{children:[A.jsx("div",{className:De("rc-inspect-overlay",M&&"rc-inspect-overlay-select"),style:{left:`${c.left}px`,top:`${c.top}px`,width:`${c.width}px`,height:`${c.height}px`},onClick:J,children:t.elements.map($=>A.jsx(Pn,{element:$,screen:t.screen,highlighted:$.id===a,selected:$.id===E,selectable:M,onClick:K},$.id))}),de&&Re&&A.jsx(Wn,{element:de,platform:t.platform,anchor:Re,cursorAnchored:X,showActions:ne,onTap:T})]})}),W=(...n)=>{window.debugRemoteControl&&console.log(...n)},R=(...n)=>{window.debugRemoteControl&&console.warn(...n)},x=(n,t,...c)=>{if(t)try{t(...c)}catch(a){console.error(`[RemoteControl] customer callback "${n}" threw:`,a)}},Tt=n=>Object.entries(U).find(([c,a])=>c.startsWith("ACTION_")&&a===n)?.[0]??String(n),$n=n=>n.includes("/android_")?"android":"ios",_r=(n,t)=>{const c=new URL(n);return c.searchParams.set("token",t),c.toString()},vr=1920,Tr=1200,Wt=3,Xn=1e3,Bn=15e3,qn=3e3,Vn=4e3,jn=(n,t)=>n===vr&&t===Tr||n===Tr&&t===vr,Gn={ios:{videoBorderRadiusMultiplier:.15,loadingLogoSize:"20%",videoPosition:{portrait:{heightMultiplier:.9678},landscape:{widthMultiplier:.9678}}},android:{videoBorderRadiusMultiplier:.13,loadingLogoSize:"40%",videoPosition:{portrait:{heightMultiplier:.967},landscape:{widthMultiplier:.962}}}};function Jn(n){const t=n.code,c=Rr[t];if(!c)return R(`Unknown event.code: ${t}, key: ${n.key}`),null;let a=s.META_NONE;const E=t>="KeyA"&&t<="KeyZ",p=n.getModifierState("CapsLock"),C=n.shiftKey;let O=C;return E&&(O=C!==p),O&&(a|=s.META_SHIFT_ON),n.ctrlKey&&(a|=s.META_CTRL_ON),n.altKey&&(a|=s.META_ALT_ON),n.metaKey&&(a|=s.META_META_ON),{keycode:c,metaState:a}}const zn=f.forwardRef(({className:n,url:t,token:c,sessionId:a,openUrl:E,showFrame:p=!0,autoReconnect:C=!1,onTerminated:O,assets:K,onConnectionStateChange:T,inspectMode:M,onAxSnapshotChange:N,onInspectSelectionChange:re,onAxStatusChange:ae,axPollIntervalMs:de,axMaxBackoffMs:ct,androidElementTreeOptions:Re,onCameraDemandChange:X,onCameraStats:ne,microphoneEnabled:J=!1,onMicrophoneStateChange:$},fe)=>{const we=f.useRef(null),F=f.useRef(null),he=f.useRef(null),[Xe,me]=f.useState(!1),[Bt,Be]=f.useState(!1),[At,qt]=f.useState(!1),Ae=f.useRef(!1),St=f.useRef(!1),[Vt,Yr]=f.useState(!1),[Kr,kr]=f.useState(!1),[Pr,jt]=f.useState({}),D=f.useRef(null),H=f.useRef(null),L=f.useRef(null),Se=f.useRef(void 0),qe=f.useRef(void 0),Ve=f.useRef(void 0),st=f.useRef(void 0),Me=f.useRef(void 0),Ee=f.useRef(0),at=f.useRef(0),be=f.useRef(!1),Mt=f.useRef(C);Mt.current=C;const pe=f.useRef(null),z=f.useRef(null),ge=f.useRef(void 0),bt=f.useMemo(()=>new Mn,[]),ce=f.useRef(X);ce.current=X;const ut=f.useRef(ne);ut.current=ne;const je=f.useRef(null),lt=f.useRef(null),Nt=f.useRef(J);Nt.current=J;const It=f.useRef(!1),Ge=f.useRef($);Ge.current=$;const Je=f.useRef(null),oe=f.useRef(null),dt=f.useRef(0),Gt=f.useRef(Promise.resolve()),Ne=f.useRef(null),ze=f.useRef(!1),ue=f.useRef(new Map),se=f.useRef(new Map),Ce=f.useRef(new Map),ye=f.useRef(new Map),V=f.useRef(new Map),Oe=f.useRef(!1),[Lr,Ur]=f.useState(!1),B=f.useRef(null),Ie=f.useRef(null),Qe=f.useRef({handle:()=>{},end:()=>{}}),[_e,ft]=f.useState(null),j=f.useRef(null),[Jt,zt]=f.useState(null),[Yt,Ze]=f.useState(null),[Kt,ht]=f.useState(null),[xr,Qt]=f.useState(null),[Fr,Zt]=f.useState(null),mt=f.useRef(null),Z=f.useRef(void 0),er=e=>{mt.current=e,Z.current===void 0&&(Z.current=window.requestAnimationFrame(()=>{Z.current=void 0,Zt(mt.current)}))},[Wr,Ye]=f.useState(null),Ke=f.useRef(M);Ke.current=M;const ke=f.useRef(null);ke.current=Jt;const kt=f.useRef(N);kt.current=N;const Pe=f.useRef(re);Pe.current=re;const tr=f.useRef(ae);tr.current=ae;const rr=f.useRef(O);rr.current=O;const nr=f.useRef(T);nr.current=T;const Pt=f.useRef(null),Le=e=>{Pt.current!==e&&(Pt.current=e,x("onConnectionStateChange",nr.current,e))},et=M===!0||M==="hover-only",Hr=M==="hover-only"?"hover-only":"select",ee=f.useMemo(()=>a||Math.random().toString(36).substring(2,15)+Math.random().toString(36).substring(2,15),[a]),ie=f.useMemo(()=>$n(t),[t]),Ue=Gn[ie],tt=K?.[ie],or=ie==="android"&&Kr?Vt?tt?.tabletFrameLandscape:tt?.tabletFrame:Vt?tt?.frameLandscape:tt?.frame,ir=tt?.loadingLogo,Et=p&&!!or,k=e=>{W(e)},te=e=>{!L.current||L.current.readyState!=="open"||(L.current.send(e),j.current?.bumpActivity())},cr=-10,sr=(e,i)=>{const r=rt();if(!r)return;const o=Fe(r,e,i);if(!o)return;const{videoX:m,videoY:l,videoWidth:h,videoHeight:u}=o,g=vt(U.ACTION_DOWN,cr,h,u,m,l,1,U.BUTTON_PRIMARY,U.BUTTON_PRIMARY);g&&te(g),window.setTimeout(()=>{const _=vt(U.ACTION_UP,cr,h,u,m,l,0,U.BUTTON_PRIMARY,U.BUTTON_PRIMARY);_&&te(_)},60)},$r=(e,i)=>{const r=rt();if(!r||i.screen.width<=0||i.screen.height<=0)return;const o=e.frame.x+e.frame.width/2,m=e.frame.y+e.frame.height/2,l=o/i.screen.width*r.actualWidth,h=m/i.screen.height*r.actualHeight,u=r.videoRect.left+r.offsetX+l,g=r.videoRect.top+r.offsetY+h;sr(u,g)},pt=-1,gt=-2,ve=-3,xe=(e,i,r,o,m,l)=>{const h=vt(e,i,r,o,m,l,1,U.BUTTON_PRIMARY,U.BUTTON_PRIMARY);h&&(W("[rc-touch] sendSingleTouch",{action:e,actionName:Tt(e),pointerId:i,x:m,y:l,video:{width:r,height:o}}),te(h))},Te=(e,i,r)=>{if(!r)return;const{videoX:o,videoY:m,videoWidth:l,videoHeight:h}=r;let u=null,g=null,_=1;const b=U.BUTTON_PRIMARY;switch(i){case"down":e!==ve&&Qe.current.end(),u=V.current.size===0?U.ACTION_DOWN:U.ACTION_POINTER_DOWN,g={x:o,y:m},V.current.set(e,g),e===-1&&F.current?.focus();break;case"move":V.current.has(e)&&(u=U.ACTION_MOVE,g={x:o,y:m},V.current.set(e,g));break;case"up":case"cancel":V.current.has(e)&&(g=V.current.get(e),V.current.delete(e),i==="cancel"?u=U.ACTION_CANCEL:u=V.current.size===0?U.ACTION_UP:U.ACTION_POINTER_UP);break}if(u!==null&&g!==null){W("[rc-touch][mouse->touch] sending",{pointerId:e,eventType:i,action:u,actionName:Tt(u),positionToSend:g,video:{width:l,height:h},altHeld:Oe.current,activePointersAfter:Array.from(V.current.entries()).map(([w,y])=>({id:w,x:y.x,y:y.y}))});const v=vt(u,e,l,h,g.x,g.y,_,b,b);v&&(W("[rc-touch][mouse->touch] buffer",{pointerId:e,actionName:Tt(u),byteLength:v.byteLength}),te(v))}else(i==="up"||i==="cancel")&&V.current.delete(e)},Lt=e=>{if(Oe.current!==e&&(Oe.current=e,Ur(e),e||ft(null),W("[rc-touch][alt] updateAltHeld",{nextHeld:e,activePointerIds:Array.from(V.current.keys())}),ie==="ios"&&L.current&&L.current.readyState==="open")){const i=e?s.ACTION_DOWN:s.ACTION_UP,r=it(i,s.KEYCODE_ALT_LEFT,0,s.META_NONE);W("[rc-touch][alt] sending Indigo modifier keycode",{action:i,keycode:s.KEYCODE_ALT_LEFT}),r&&te(r)}},rt=()=>{const e=F.current,i=we.current;if(!e||!i)return null;const r=e.videoWidth,o=e.videoHeight;if(!r||!o)return null;const m=e.getBoundingClientRect(),l=i.getBoundingClientRect(),h=m.width,u=m.height,g=r/o,_=h/u;let b=h,v=u;g>_?v=h/g:b=u*g;const w=(h-b)/2,y=(u-v)/2;return{videoWidth:r,videoHeight:o,videoRect:m,containerRect:l,actualWidth:b,actualHeight:v,offsetX:w,offsetY:y}},Fe=(e,i,r)=>{const o=i-e.videoRect.left-e.offsetX,m=r-e.videoRect.top-e.offsetY,l=Math.max(0,Math.min(e.actualWidth,o)),h=Math.max(0,Math.min(e.actualHeight,m)),u=Math.max(0,Math.min(e.videoWidth,l/e.actualWidth*e.videoWidth)),g=Math.max(0,Math.min(e.videoHeight,h/e.actualHeight*e.videoHeight));return{videoX:u,videoY:g,videoWidth:e.videoWidth,videoHeight:e.videoHeight}},Xr=(e,i,r)=>{const o=i-e.videoRect.left-e.offsetX,m=r-e.videoRect.top-e.offsetY,l=Math.max(0,Math.min(e.actualWidth,o)),h=Math.max(0,Math.min(e.actualHeight,m)),u=Math.max(0,Math.min(e.videoWidth,l/e.actualWidth*e.videoWidth)),g=Math.max(0,Math.min(e.videoHeight,h/e.actualHeight*e.videoHeight)),_=e.videoWidth-u,b=e.videoHeight-g,v=e.videoRect.left+e.offsetX,w=e.videoRect.top+e.offsetY,y=v-e.containerRect.left+l,d=w-e.containerRect.top+h,I=v-e.containerRect.left+(e.actualWidth-l),P=w-e.containerRect.top+(e.actualHeight-h);return{containerX:y,containerY:d,mirrorContainerX:I,mirrorContainerY:P,videoX:u,videoY:g,mirrorVideoX:_,mirrorVideoY:b,videoWidth:e.videoWidth,videoHeight:e.videoHeight}},Br=(e,i,r,o,m,l,h)=>{const u=mn(e,i,r,o,m,l,h);W("[rc-touch2] sendTwoFingerMessage (iOS)",{actionName:Tt(e),video:{width:i,height:r},p0:{x:o,y:m},p1:{x:l,y:h},byteLength:u.byteLength}),te(u)},We=(e,i,r,o,m,l,h,u,g)=>{if(W("[rc-touch2] applyTwoFingerEvent",{platform:ie,eventType:e,video:{width:i,height:r},p0:{x:o,y:m,id:u},p1:{x:l,y:h,id:g}}),e==="down"&&Qe.current.end(),ie==="ios"){const _=e==="down"?U.ACTION_DOWN:e==="move"?U.ACTION_MOVE:U.ACTION_UP;Br(_,i,r,o,m,l,h)}else e==="down"?(xe(U.ACTION_DOWN,u,i,r,o,m),xe(U.ACTION_POINTER_DOWN,g,i,r,l,h)):e==="move"?(xe(U.ACTION_MOVE,u,i,r,o,m),xe(U.ACTION_MOVE,g,i,r,l,h)):(xe(U.ACTION_POINTER_UP,g,i,r,l,h),xe(U.ACTION_UP,u,i,r,o,m))},qr=(e,i,r)=>{if(!Oe.current){_e!==null&&ft(null);return}const o=Xr(e,i,r);ft(o)},Vr=(e,i,r)=>{const o=ke.current;if(!o||o.screen.width<=0||o.screen.height<=0)return null;const m=i-e.videoRect.left-e.offsetX,l=r-e.videoRect.top-e.offsetY;if(m<0||l<0||m>e.actualWidth||l>e.actualHeight)return null;const h=m/e.actualWidth*o.screen.width,u=l/e.actualHeight*o.screen.height;return br(o,h,u)},le=e=>{e.preventDefault(),e.stopPropagation();const i=rt();if((Ke.current===!0||Ke.current==="hover-only")&&!("touches"in e))if(e.type==="mousemove"){if(er({x:e.clientX,y:e.clientY}),i){const o=Vr(i,e.clientX,e.clientY);Ze(o?.id??null)}}else e.type==="mouseleave"&&(er(null),Ze(null));if(Ke.current!==!0&&(!("touches"in e)&&i&&(e.type==="mousemove"?qr(i,e.clientX,e.clientY):e.type==="mouseleave"&&ft(null)),!(!L.current||L.current.readyState!=="open"||!F.current||!i)))if("touches"in e){const o=e.touches,m=e.changedTouches;let l;switch(e.type){case"touchstart":l="down";break;case"touchmove":l="move";break;case"touchend":l="up";break;case"touchcancel":l="cancel";break;default:return}if(o.length===2){const h=o[0],u=o[1],g=Fe(i,h.clientX,h.clientY),_=Fe(i,u.clientX,u.clientY);if(!g||!_)return;B.current?B.current.source==="real-touch"&&(B.current.finger0={x:g.videoX,y:g.videoY},B.current.finger1={x:_.videoX,y:_.videoY},We("move",g.videoWidth,g.videoHeight,g.videoX,g.videoY,_.videoX,_.videoY,B.current.pointerId0,B.current.pointerId1)):(B.current={finger0:{x:g.videoX,y:g.videoY},finger1:{x:_.videoX,y:_.videoY},videoSize:{width:g.videoWidth,height:g.videoHeight},source:"real-touch",pointerId0:h.identifier,pointerId1:u.identifier},We("down",g.videoWidth,g.videoHeight,g.videoX,g.videoY,_.videoX,_.videoY,h.identifier,u.identifier))}else if(o.length<2&&B.current?.source==="real-touch"){const h=B.current;We("up",h.videoSize.width,h.videoSize.height,h.finger0.x,h.finger0.y,h.finger1.x,h.finger1.y,h.pointerId0,h.pointerId1),B.current=null;return}else{if(o.length>2)return;{const h=m[0];if(h){const u=Fe(i,h.clientX,h.clientY);Te(h.identifier,l,u)}}}}else{let m=null;const l=B.current?.source==="alt-mouse";switch(e.type){case"mousedown":e.button===0&&(m="down");break;case"mousemove":(l||V.current.has(-1))&&(m="move");break;case"mouseup":e.button===0&&(m="up");break;case"mouseleave":(l||V.current.has(-1))&&(m="up");break}if(m){const h=Fe(i,e.clientX,e.clientY);if(!h)return;W("[rc-touch][mouse] event",{domType:e.type,eventType:m,button:e.button,buttons:e.buttons,client:{x:e.clientX,y:e.clientY},altHeldRef:Oe.current,inTwoFingerMode:l,geometry:{videoX:h.videoX,videoY:h.videoY,videoWidth:h.videoWidth,videoHeight:h.videoHeight},activePointerIds:Array.from(V.current.keys())}),Oe.current||l?jr(m,h):Te(-1,m,h)}}},jr=(e,i)=>{const{videoX:r,videoY:o,videoWidth:m,videoHeight:l}=i,h=m-r,u=l-o;if(e==="down"){B.current={finger0:{x:r,y:o},finger1:{x:h,y:u},videoSize:{width:m,height:l},source:"alt-mouse",pointerId0:pt,pointerId1:gt},F.current?.focus(),We("down",m,l,r,o,h,u,pt,gt);return}if(e==="move"){B.current?.source==="alt-mouse"&&(B.current.finger0={x:r,y:o},B.current.finger1={x:h,y:u},We("move",m,l,r,o,h,u,pt,gt));return}if(e==="up"||e==="cancel"){const g=B.current;if(g?.source==="alt-mouse"){const{finger0:_,finger1:b,videoSize:v}=g;We("up",v.width,v.height,_.x,_.y,b.x,b.y,pt,gt),B.current=null}return}},Gr=150,ar=()=>{const e=Ie.current;e&&(window.clearTimeout(e.endTimer),Te(ve,"up",e.pos),Ie.current=null)},Jr=e=>{if(e.ctrlKey||Oe.current||B.current)return;const i=V.current.size;if(i>1||i===1&&!V.current.has(ve))return;const r=rt();if(!r)return;const o=e.clientX-r.videoRect.left-r.offsetX,m=e.clientY-r.videoRect.top-r.offsetY;if(o<0||o>r.actualWidth||m<0||m>r.actualHeight)return;const l=Fe(r,e.clientX,e.clientY);if(!l)return;e.preventDefault();const h=(e.deltaMode===1?16:e.deltaMode===2?r.actualWidth:1)*(r.videoWidth/r.actualWidth),u=(e.deltaMode===1?16:e.deltaMode===2?r.actualHeight:1)*(r.videoHeight/r.actualHeight),g=-e.deltaX*h,_=-e.deltaY*u,b=P=>{window.clearTimeout(P.endTimer),P.endTimer=window.setTimeout(ar,Gr)};if(g===0&&_===0){Ie.current&&b(Ie.current);return}const v=()=>({videoWidth:l.videoWidth,videoHeight:l.videoHeight,videoX:Math.abs(g)>Math.abs(_)?Math.min(Math.max(l.videoX,l.videoWidth*.2),l.videoWidth*.8):l.videoX,videoY:Math.abs(_)>=Math.abs(g)?Math.min(Math.max(l.videoY,l.videoHeight*.2),l.videoHeight*.8):l.videoY}),w=P=>({...P,videoX:Math.min(Math.max(P.videoX+g,0),P.videoWidth),videoY:Math.min(Math.max(P.videoY+_,0),P.videoHeight)});let y=Ie.current;if(y)window.clearTimeout(y.endTimer);else{const P=v();Te(ve,"down",P),y={pos:P,endTimer:0},Ie.current=y}let d=y.pos,I=w(d);I.videoX===d.videoX&&I.videoY===d.videoY&&(Te(ve,"up",d),d=v(),Te(ve,"down",d),I=w(d)),Te(ve,"move",I),y.pos=I,b(y)};f.useEffect(()=>{Qe.current={handle:Jr,end:ar}}),f.useEffect(()=>{const e=we.current;if(!e)return;const i=r=>Qe.current.handle(r);return e.addEventListener("wheel",i,{passive:!1}),()=>{Qe.current.end(),e.removeEventListener("wheel",i)}},[]),f.useEffect(()=>{const e=o=>{o.key==="Alt"&&Lt(!0)},i=o=>{o.key==="Alt"&&Lt(!1)},r=()=>{Lt(!1)};return window.addEventListener("keydown",e,!0),window.addEventListener("keyup",i,!0),window.addEventListener("blur",r),()=>{window.removeEventListener("keydown",e,!0),window.removeEventListener("keyup",i,!0),window.removeEventListener("blur",r)}},[]);const ur=e=>{if(e.preventDefault(),e.stopPropagation(),W("Keyboard event:",{type:e.type,key:e.key,keyCode:e.keyCode,code:e.code,target:e.target.tagName,focused:document.activeElement===F.current}),document.activeElement!==F.current){R("Video element not focused, skipping keyboard event");return}if(!L.current||L.current.readyState!=="open"){R("Data channel not ready for keyboard event:",L.current?.readyState);return}if(e.type==="keydown"){if(e.key.toLowerCase()==="v"&&(e.metaKey||e.ctrlKey)){W("Paste shortcut detected"),navigator.clipboard.readText().then(r=>{if(r){W("Pasting text via SET_CLIPBOARD:",r.substring(0,20)+(r.length>20?"...":""));const o=En(r,!0);te(o)}}).catch(r=>{console.error("Failed to read clipboard contents: ",r)});return}if(e.key.toLowerCase()==="m"&&(e.metaKey||e.ctrlKey)){W("Menu shortcut detected");const r=it(s.ACTION_DOWN,s.MENU,0,s.META_NONE);te(r);const o=it(s.ACTION_UP,s.MENU,0,s.META_NONE);te(o);return}}const i=Jn(e);if(i){const{keycode:r,metaState:o}=i,m=e.type==="keydown"?s.ACTION_DOWN:s.ACTION_UP;W(`Sending Keycode: key=${e.key}, code=${r}, action=${m}, meta=${o}`);const l=it(m,r,0,o);te(l)}else W(`Ignoring unhandled key event: type=${e.type}, key=${e.key}`)},zr=()=>{D.current&&D.current.readyState===WebSocket.OPEN&&D.current.send(JSON.stringify({type:"keepAlive",sessionId:ee}))},lr=()=>{Se.current&&window.clearInterval(Se.current),Se.current=window.setInterval(zr,1e4)},dr=()=>{Se.current&&(window.clearInterval(Se.current),Se.current=void 0)},Ct=()=>{qe.current&&(window.clearTimeout(qe.current),qe.current=void 0)},yt=()=>{Ve.current&&(window.clearTimeout(Ve.current),Ve.current=void 0)},nt=()=>{st.current&&(window.clearInterval(st.current),st.current=void 0)},ot=()=>{Me.current!==void 0&&(window.clearTimeout(Me.current),Me.current=void 0)},Qr=()=>{ze.current||(ze.current=!0,nt(),me(!0),Le("connected"))},G=e=>{for(const i of e.getTracks())try{i.stop()}catch(r){R("track.stop() failed:",r)}},Zr=()=>{const e=z.current;e&&(z.current=null,G(e))},fr=e=>{const i=Gt.current.then(e).catch(r=>{R("microphone operation failed:",r)});return Gt.current=i,i},Ut=()=>Nt.current||It.current,hr=e=>{const i=`ui-mic-${Date.now()}-${Math.random().toString(36).substring(2,9)}`;Ne.current=i,e.send(JSON.stringify({type:"microphoneStart",id:i}))},xt=()=>fr(()=>en()),en=async()=>{const e=dt.current,i=()=>e===dt.current&&Ut(),r=u=>{x("onMicrophoneStateChange",Ge.current,{active:!1,error:u})};if(!i())return;const o=D.current,m=Je.current;if(!o||o.readyState!==WebSocket.OPEN||!m||oe.current)return;if(!navigator.mediaDevices||typeof navigator.mediaDevices.getUserMedia!="function"){console.warn("[RemoteControl] navigator.mediaDevices.getUserMedia unavailable. getUserMedia requires a secure context (https or http://localhost)."),r("Microphone capture requires a secure context (https)");return}let l=null;try{l=await navigator.mediaDevices.getUserMedia({audio:!0,video:!1})}catch(u){console.warn("[RemoteControl] microphone getUserMedia denied/failed:",u)}if(!i()){l&&G(l);return}if(!l){r("Microphone permission denied or no device available");return}const h=l.getAudioTracks()[0]??null;if(!h){G(l),r("Captured stream has no audio track");return}try{await m.replaceTrack(h)}catch(u){R("replaceTrack(audioTrack) failed:",u),G(l),r("Failed to attach the microphone track to the connection");return}if(!i()){try{await m.replaceTrack(null)}catch(u){R("replaceTrack(null) on stale mic attach failed:",u)}G(l);return}oe.current=l,h.onended=()=>{oe.current===l&&Ot("microphone capture ended by the browser")},hr(o),x("onMicrophoneStateChange",Ge.current,{active:!0,label:h.label||void 0})},Ot=e=>(++dt.current,Ne.current=null,fr(async()=>{const i=Je.current,r=oe.current;if(oe.current=null,i&&r)try{await i.replaceTrack(null)}catch(m){R("replaceTrack(null) on mic detach failed:",m)}r&&G(r);const o=D.current;if(r&&o&&o.readyState===WebSocket.OPEN){const m=`ui-mic-${Date.now()}-${Math.random().toString(36).substring(2,9)}`;o.send(JSON.stringify({type:"microphoneStop",id:m}))}(r||e)&&x("onMicrophoneStateChange",Ge.current,{active:!1,...e!==void 0&&{error:e}})})),mr=(e,i)=>{let r;try{r=Kn(i),console.info(`[RemoteControl] camera capture: ${r.camera?.width}x${r.camera?.height} @ ${r.camera?.frameRate??"?"}fps`+(r.camera?.label?` — ${r.camera.label}`:""))}catch(o){R("getSettings() on outbound camera track failed:",o),r={type:"cameraResult",granted:!0,camera:{label:i.label||void 0}}}e.send(JSON.stringify(r)),x("onCameraDemandChange",ce.current,!0,!0,r.camera)},tn=e=>{if(!e)return;const i=e.indexOf("/"),o=(i>=0?e.slice(i+1):e).toUpperCase();return o==="HEVC"?"H265":o},rn=async()=>{const e=pe.current,i=ut.current;if(!e||!i)return;let r;try{r=await e.getStats()}catch{return}let o,m,l;if(r.forEach(w=>{w.type==="outbound-rtp"&&w.kind==="video"?o=w:w.type==="remote-inbound-rtp"&&w.kind==="video"&&(l=w)}),o?.codecId){const w=r.get(o.codecId);w&&(m=w.mimeType)}if(!o)return;const h=o.timestamp??Date.now(),u=lt.current;let g,_,b;if(u&&h>u.timestamp){const w=(h-u.timestamp)/1e3;if(typeof o.framesEncoded=="number"&&typeof u.framesEncoded=="number"&&(g=Math.max(0,(o.framesEncoded-u.framesEncoded)/w)),typeof o.bytesSent=="number"&&typeof u.bytesSent=="number"&&(_=Math.max(0,(o.bytesSent-u.bytesSent)*8/w)),typeof o.packetsSent=="number"&&typeof u.packetsSent=="number"&&l&&typeof l.packetsLost=="number"&&typeof u.packetsLost=="number"){const y=o.packetsSent-u.packetsSent,d=l.packetsLost-u.packetsLost;y>0&&(b=Math.max(0,Math.min(100,d/y*100)))}}lt.current={timestamp:h,framesEncoded:o.framesEncoded,bytesSent:o.bytesSent,packetsSent:o.packetsSent,packetsLost:l?.packetsLost};const v={codec:tn(m),encoderImplementation:o.encoderImplementation,hardwareAccelerated:typeof o.powerEfficientEncoder=="boolean"?o.powerEfficientEncoder:void 0,framesPerSecond:g,framesEncoded:o.framesEncoded,bitrateBps:_,width:o.frameWidth,height:o.frameHeight,qualityLimitationReason:o.qualityLimitationReason,rttMs:typeof l?.roundTripTime=="number"?l.roundTripTime*1e3:void 0,packetsLostPct:b};x("onCameraStats",ut.current,v)},Er=()=>{je.current===null&&(lt.current=null,je.current=setInterval(()=>{rn()},1e3))},_t=()=>{je.current!==null&&(clearInterval(je.current),je.current=null),lt.current=null,x("onCameraStats",ut.current,null)},nn=(e,i,r,o)=>bt.enqueue(async({isCurrent:m})=>{const l=()=>m()&&o()&&D.current===i&&pe.current===r;if(!l())return;if(!e.active){const v=z.current;try{await r.replaceTrack(null)}catch(w){R("replaceTrack(null) on camera detach failed:",w)}if(z.current===v&&(z.current=null,ge.current=void 0,v&&G(v)),_t(),!l())return;x("onCameraDemandChange",ce.current,!1);return}x("onCameraDemandChange",ce.current,!0);const h=z.current,u=h?.getVideoTracks()[0],g=ge.current;if(u?.readyState==="live"&&g&&!In(g,e)){ge.current=e,mr(i,u),Er();return}if(h){try{await r.replaceTrack(null)}catch(v){R("replaceTrack(null) before camera switch failed:",v)}if(z.current===h&&(z.current=null,ge.current=void 0,G(h)),_t(),!l())return}if(!navigator.mediaDevices||typeof navigator.mediaDevices.getUserMedia!="function"){if(console.warn("[RemoteControl] navigator.mediaDevices.getUserMedia unavailable. getUserMedia requires a secure context (https or http://localhost). Replying cameraResult granted=false so limulator falls back to black frames."),!l())return;i.send(JSON.stringify({type:"cameraResult",granted:!1})),x("onCameraDemandChange",ce.current,!0,!1);return}let _=null;try{_=await navigator.mediaDevices.getUserMedia({video:Nn(e),audio:!1})}catch(v){console.warn("[RemoteControl] getUserMedia denied/failed:",v)}if(!l()){_&&G(_);return}if(!_){i.send(JSON.stringify({type:"cameraResult",granted:!1})),x("onCameraDemandChange",ce.current,!0,!1);return}const b=_.getVideoTracks()[0]??null;if(!b){if(G(_),!l())return;i.send(JSON.stringify({type:"cameraResult",granted:!1})),x("onCameraDemandChange",ce.current,!0,!1);return}try{b.contentHint="motion"}catch{}try{await r.replaceTrack(b)}catch(v){if(R("replaceTrack(videoTrack) failed:",v),G(_),!l())return;i.send(JSON.stringify({type:"cameraResult",granted:!1})),x("onCameraDemandChange",ce.current,!0,!1);return}if(!l()){try{await r.replaceTrack(null)}catch(v){R("replaceTrack(null) for stale camera attach failed:",v)}G(_);return}try{const v=r.getParameters();(!v.encodings||v.encodings.length===0)&&(v.encodings=[{}]),v.encodings[0].maxBitrate=8e6,v.encodings[0].maxFramerate=30,v.degradationPreference="maintain-framerate",await r.setParameters(v)}catch(v){R("setParameters on outbound camera failed:",v)}if(!l()){try{await r.replaceTrack(null)}catch(v){R("replaceTrack(null) for stale camera finalize failed:",v)}G(_);return}z.current=_,ge.current=e,b.onended=()=>{z.current===_&&bt.enqueueCurrent(async({isCurrent:v})=>{if(!(!v()||z.current!==_||!o()||D.current!==i||pe.current!==r)){try{await r.replaceTrack(null)}catch(w){R("replaceTrack(null) after camera ended failed:",w)}!v()||z.current!==_||!o()||D.current!==i||pe.current!==r||(z.current=null,ge.current=void 0,G(_),_t(),i.send(JSON.stringify({type:"cameraResult",granted:!1})),x("onCameraDemandChange",ce.current,!0,!1))}}).catch(v=>R("camera ended cleanup failed:",v))},l()&&(mr(i,b),Er())}),He=()=>{yt(),ot(),nt(),j.current&&(j.current.stop(),j.current=null),bt.invalidate(),_t(),Zr(),ge.current=void 0,pe.current=null,++dt.current,It.current=!1,Ne.current=null,oe.current&&(G(oe.current),oe.current=null,x("onMicrophoneStateChange",Ge.current,{active:!1})),Je.current=null,Z.current!==void 0&&(window.cancelAnimationFrame(Z.current),Z.current=void 0),D.current&&(D.current.onopen=null,D.current.onmessage=null,D.current.onerror=null,D.current.onclose=null,D.current.close(),D.current=null),H.current&&(H.current.onconnectionstatechange=null,H.current.oniceconnectionstatechange=null,H.current.ontrack=null,H.current.onicecandidate=null,H.current.close(),H.current=null),F.current&&(F.current.srcObject=null),L.current&&(L.current.onopen=null,L.current.onclose=null,L.current.onerror=null,L.current.close(),L.current=null)},pr=()=>{document.hidden?dr():lr()},on=async()=>{if(typeof navigator<"u"&&navigator.onLine===!1)return!1;let e;try{const o=new URL(t);o.protocol=o.protocol==="wss:"?"https:":o.protocol==="ws:"?"http:":o.protocol,e=_r(o.toString(),c)}catch{return!1}const i=new AbortController,r=window.setTimeout(()=>i.abort(),Vn);try{return(await fetch(e,{method:"GET",cache:"no-store",signal:i.signal})).status===404}catch{return!1}finally{window.clearTimeout(r)}},cn=()=>{Ae.current||(Ae.current=!0,qt(!0),Be(!1),Ee.current+=1,Ct(),yt(),ot(),He(),k("Instance terminated"),Le("terminated"),x("onTerminated",rr.current))},sn=()=>{Ae.current||St.current||(St.current=!0,on().then(e=>{St.current=!1,e&&cn()}))},$e=(e,i)=>{if(Ae.current||i!==Ee.current)return;if(sn(),be.current){if(!Mt.current){k(`Connection failed after it was established: ${e}`),Be(!0),Le("failed"),He();return}k(`Reconnecting after established session dropped: ${e}`),be.current=!1,at.current=-1}Ct();const r=at.current+1;if(r>=Wt){k(`Connection failed after ${Wt} attempts: ${e}`),Be(!0),Le("failed"),He();return}k(`Retrying connection (${r+1}/${Wt})`),Le("reconnecting"),He(),qe.current=window.setTimeout(()=>{qe.current=void 0,i===Ee.current&&gr(r)},Xn)},gr=async(e=0)=>{if(Ae.current)return;const i=Ee.current+1;Ee.current=i,at.current=e,be.current=!1,Be(!1),e===0&&Pt.current!=="reconnecting"&&Le("connecting"),Ct(),yt(),nt(),ze.current=!1,me(!1),He();const r=()=>i===Ee.current;Ve.current=window.setTimeout(()=>{Ve.current=void 0,!(!r()||be.current)&&$e("Connection did not succeed within 15 seconds",i)},Bn);try{const o=new WebSocket(_r(t,c));if(D.current=o,await new Promise((y,d)=>{let I=!1;const P=window.setTimeout(()=>d(new Error("WebSocket connection timeout")),3e4),S=Y=>{I||(I=!0,window.clearTimeout(P),Y())};o.onopen=()=>{!r()||D.current!==o||S(y)},o.onerror=Y=>{!r()||D.current!==o||(k("WebSocket error: "+Y),S(()=>d(new Error("WebSocket connection failed"))))},o.onclose=()=>{!r()||D.current!==o||(k("WebSocket closed"),S(()=>d(new Error("WebSocket closed before connection was established"))))}}),!r()||D.current!==o)return;o.onerror=y=>{!r()||D.current!==o||k("WebSocket error: "+y)},o.onclose=()=>{!r()||D.current!==o||k("WebSocket closed")};const m=new Promise((y,d)=>{const I=Y=>{window.clearTimeout(P),o.removeEventListener("message",S),Y()},P=window.setTimeout(()=>I(()=>d(new Error("RTCConfiguration timeout"))),3e4),S=Y=>{if(!(!r()||D.current!==o))try{const q=JSON.parse(Y.data);if(q.type==="signalingError"&&q.requestType==="requestRtcConfiguration"&&q.sessionId===ee){const Q=typeof q.error=="string"?q.error:"Unable to load streaming configuration";I(()=>d(new Error(Q)))}else q.type==="rtcConfiguration"&&I(()=>y({rtcConfiguration:q.rtcConfiguration,cameraSupported:q.cameraSupported===!0}))}catch(q){console.error("Error handling RTC configuration:",q),I(()=>d(q))}};o.addEventListener("message",S),o.send(JSON.stringify({type:"requestRtcConfiguration",sessionId:ee}))}),{rtcConfiguration:l,cameraSupported:h}=await m;if(!r()||D.current!==o)return;const u=new RTCPeerConnection(l);H.current=u,u.addTransceiver("audio",{direction:"recvonly"});const g=u.addTransceiver("video",{direction:"recvonly"}),_=ie==="ios"||h,b=_?u.addTransceiver("video",{direction:"sendonly"}):null;pe.current=b?.sender??null;const v=_?u.addTransceiver("audio",{direction:"sendonly"}):null;if(Je.current=v?.sender??null,RTCRtpReceiver.getCapabilities){const y=RTCRtpReceiver.getCapabilities("video");if(y&&y.codecs){const I=y.codecs.sort((P,S)=>{const Y=q=>{const Q=q.mimeType.toLowerCase();return Q.includes("vp9")?1:Q.includes("h265")||Q.includes("hevc")?2:Q.includes("h264")||Q.includes("avc")?3:4};return Y(P)-Y(S)});g.setCodecPreferences(I),W("Set codec preferences:",I.map(P=>P.mimeType).join(", "))}}if(b&&RTCRtpSender.getCapabilities){const y=RTCRtpSender.getCapabilities("video");if(y&&y.codecs){const d=new Set(["42e01f","42e028","640c1f"]),I=S=>{const Y=S.mimeType.toLowerCase(),Q=(S.sdpFmtpLine||"").toLowerCase().match(/profile-level-id=([0-9a-f]{6})/),fn=Q?Q[1]:"";return Y==="video/h265"||Y==="video/hevc"?1:Y==="video/h264"&&d.has(fn)?2:Y==="video/h264"?3:Y==="video/vp9"?4:Y==="video/vp8"?5:Y==="video/rtx"||Y==="video/red"||Y==="video/ulpfec"?0:6},P=[...y.codecs].sort((S,Y)=>I(S)-I(Y));try{b.setCodecPreferences(P),W("Outbound camera codec preferences:",P.map(S=>`${S.mimeType}${S.sdpFmtpLine?`[${S.sdpFmtpLine}]`:""}`).join(", "))}catch(S){R("Failed to set outbound camera codec preferences:",S)}}}const w=u.createDataChannel("control",{ordered:!0,negotiated:!0,id:1});if(L.current=w,w.onopen=()=>{if(!r()||L.current!==w||D.current!==o)return;be.current=!0,yt(),k("Control channel opened"),j.current||(j.current=new Sn({platform:ie,baseIntervalMs:de,maxBackoffMs:ct,androidElementTreeOptions:Re,send:d=>{if(!D.current||D.current.readyState!==WebSocket.OPEN)return!1;try{return D.current.send(JSON.stringify(d)),!0}catch{return!1}},onSnapshot:d=>{zt(I=>$t(I,d)?I:d),queueMicrotask(()=>{x("onAxSnapshotChange",kt.current,d)})},onStatusChange:(d,I)=>{x("onAxStatusChange",tr.current,d,I)}}),(Ke.current===!0||Ke.current==="hover-only")&&j.current.start());const y=()=>{!r()||ze.current||L.current!==w||D.current!==o||o.readyState!==WebSocket.OPEN||o.send(JSON.stringify({type:"requestFrame",sessionId:ee}))};if(y(),nt(),st.current=window.setInterval(()=>{if(!r()||ze.current||L.current!==w||D.current!==o||o.readyState!==WebSocket.OPEN){nt();return}y()},250),E){try{const d=decodeURIComponent(E);k("Opening URL"),o.send(JSON.stringify({type:"openUrl",url:d,sessionId:ee}))}catch(d){console.error({error:d},"Error decoding URL, falling back to the original URL"),o.send(JSON.stringify({type:"openUrl",url:E,sessionId:ee}))}j.current?.bumpActivity()}},w.onclose=()=>{!r()||L.current!==w||k("Control channel closed")},w.onerror=y=>{!r()||L.current!==w||(console.error("Control channel error:",y),k("Control channel error: "+y))},u.onconnectionstatechange=()=>{!r()||H.current!==u||(k("Connection state: "+u.connectionState),u.connectionState==="failed"&&$e("WebRTC connection entered failed state",i))},u.oniceconnectionstatechange=()=>{if(!r()||H.current!==u)return;const y=u.iceConnectionState;if(k("ICE state: "+y),y==="connected"||y==="completed"){ot(),Ut()&&!oe.current&&xt();return}if(y==="failed"){ot(),$e("ICE connection entered failed state",i);return}y==="disconnected"&&Mt.current&&Me.current===void 0&&(Me.current=window.setTimeout(()=>{Me.current=void 0,!(!r()||H.current!==u)&&u.iceConnectionState==="disconnected"&&$e("ICE stayed disconnected past grace period",i)},qn))},u.ontrack=y=>{!r()||H.current!==u||(k("Received remote track: "+y.track.kind),y.track.kind==="video"&&F.current&&(W(`[${new Date().toISOString()}] Video track received:`,y.track),F.current.srcObject=y.streams[0]))},u.onicecandidate=y=>{if(!(!r()||H.current!==u||D.current!==o))if(y.candidate&&o.readyState===WebSocket.OPEN){const d={type:"candidate",candidate:y.candidate.candidate,sdpMid:y.candidate.sdpMid,sdpMLineIndex:y.candidate.sdpMLineIndex,sessionId:ee};o.send(JSON.stringify(d)),k("Sent ICE candidate")}else k("ICE candidate gathering completed")},o.onmessage=async y=>{if(!r()||D.current!==o)return;let d;try{d=JSON.parse(y.data)}catch(I){R("Error parsing message:",I);return}if(!j.current?.handleMessage(d))switch(k("Received: "+d.type),d.type){case"signalingError":{if(d.requestType!=="offer"||d.sessionId!==ee)break;const S=typeof d.error=="string"?d.error:"Unable to establish the streaming connection";k("Error: "+S),$e(S,i);break}case"answer":if(!H.current||H.current!==u){k("No peer connection, skipping answer");break}if(await u.setRemoteDescription(new RTCSessionDescription({type:"answer",sdp:d.sdp})),!r()||H.current!==u)return;k("Set remote description");break;case"candidate":if(!H.current||H.current!==u){k("No peer connection, skipping candidate");break}if(await u.addIceCandidate(new RTCIceCandidate({candidate:d.candidate,sdpMid:d.sdpMid,sdpMLineIndex:d.sdpMLineIndex})),!r()||H.current!==u)return;k("Added ICE candidate");break;case"screenshot":case"screenshotResult":{if(typeof d.id!="string"){R("Received invalid screenshot success message:",d);break}const S=Qn(d);if(S){const Q=se.current.get(d.id);if(!Q){R(`Received screenshot error for unknown or handled id: ${d.id}`);break}R(`Received screenshot error for id ${d.id}: ${S}`),Q(new Error(S)),ue.current.delete(d.id),se.current.delete(d.id);break}const Y=Zn(d);if(!Y){R("Received screenshot message without image data:",d);break}const q=ue.current.get(d.id);if(!q){R(`Received screenshot data for unknown or handled id: ${d.id}`);break}W(`Received screenshot data for id ${d.id}`),q(Y),ue.current.delete(d.id),se.current.delete(d.id);break}case"screenshotError":if(typeof d.id!="string"||typeof d.message!="string"){R("Received invalid screenshot error message:",d);break}const I=se.current.get(d.id);if(!I){R(`Received screenshot error for unknown or handled id: ${d.id}`);break}R(`Received screenshot error for id ${d.id}: ${d.message}`),I(new Error(d.message)),ue.current.delete(d.id),se.current.delete(d.id);break;case"cameraRequest":{const S=pe.current;if(!S)break;const Y=bn(d);if(!Y){R("Received invalid cameraRequest:",d);break}console.info("[RemoteControl] cameraRequest received:",Y.active,Y.facingMode),await nn(Y,o,S,r);break}case"microphoneRequest":{if(!Je.current)break;const S=d.active===!0;if(It.current=S,console.info("[RemoteControl] microphoneRequest received, active=",S),!S){Nt.current||await Ot();break}if(oe.current){hr(o);break}await xt();break}case"microphoneStartResult":{if(Ne.current===null||d.id!==Ne.current)break;Ne.current=null,typeof d.error=="string"&&(console.warn("[RemoteControl] microphoneStart rejected by host:",d.error),Ot(d.error));break}case"terminateAppResult":if(typeof d.id!="string"){R("Received invalid terminateApp result message:",d);break}if(typeof d.error=="string"){const S=ye.current.get(d.id);if(!S){R(`Received terminateApp error for unknown or handled id: ${d.id}`);break}R(`Received terminateApp error for id ${d.id}: ${d.error}`),S(new Error(d.error)),Ce.current.delete(d.id),ye.current.delete(d.id);break}const P=Ce.current.get(d.id);if(!P){R(`Received terminateApp result for unknown or handled id: ${d.id}`);break}W(`Received terminateApp success for id ${d.id}`),P(),Ce.current.delete(d.id),ye.current.delete(d.id);break;default:R(`Received unhandled message type: ${d.type}`,d);break}},H.current===u){const y=await u.createOffer({offerToReceiveVideo:!0,offerToReceiveAudio:!1});if(!r()||H.current!==u||(await u.setLocalDescription(y),!r()||H.current!==u))return;r()&&D.current===o&&o.readyState===WebSocket.OPEN&&o.send(JSON.stringify({type:"offer",sdp:y.sdp,sessionId:ee})),k("Sent offer")}}catch(o){if(!r())return;const m=o instanceof Error?o.message:String(o);k("Error: "+m),$e(m,i)}},Ft=()=>{Ae.current=!1,qt(!1),gr(0)},an=()=>{Ee.current+=1,at.current=0,be.current=!1,Ct(),ot(),He(),k("Stopped")},un=e=>{e.stopPropagation(),Ft()};f.useEffect(()=>{Ut()?xt():Ot()},[J]),f.useEffect(()=>(me(!1),Ft(),document.hidden||lr(),document.addEventListener("visibilitychange",pr),()=>{dr(),an(),document.removeEventListener("visibilitychange",pr)}),[t,c,a]);const Cr=()=>{const e=rt();if(!e){Qt(null);return}const i={left:e.videoRect.left-e.containerRect.left+e.offsetX,top:e.videoRect.top-e.containerRect.top+e.offsetY,width:e.actualWidth,height:e.actualHeight};Qt(r=>r&&r.left===i.left&&r.top===i.top&&r.width===i.width&&r.height===i.height?r:i)};f.useEffect(()=>{const e=F.current,i=he.current,r=we.current;if(!e)return;const o=()=>{if(!Et||!i){jt({}),Cr();return}const h=i.clientWidth,u=i.clientHeight;if(h===0||u===0)return;const g=e.videoWidth>e.videoHeight;Yr(g),kr(ie==="android"&&jn(e.videoWidth,e.videoHeight));const _=g?Ue.videoPosition.landscape:Ue.videoPosition.portrait;let b={};_.heightMultiplier?(b.height=`${u*_.heightMultiplier}px`,b.width="auto"):_.widthMultiplier&&(b.width=`${h*_.widthMultiplier}px`,b.height="auto"),b.borderRadius=`${g?u*Ue.videoBorderRadiusMultiplier:h*Ue.videoBorderRadiusMultiplier}px`,jt(b),Cr()},m=new ResizeObserver(()=>{o()});i&&m.observe(i),m.observe(e),r&&m.observe(r),i&&i.addEventListener("load",o),e.addEventListener("loadedmetadata",o),e.addEventListener("resize",o);const l=()=>j.current?.bumpActivity();return e.addEventListener("resize",l),o(),()=>{m.disconnect(),e.removeEventListener("loadedmetadata",o),e.removeEventListener("resize",o),e.removeEventListener("resize",l),i&&i.removeEventListener("load",o)}},[Ue,Et]),f.useEffect(()=>{const e=j.current;et?e?.start():(e?.stop(),zt(null),Ze(null),ht(null),Zt(null),Ye(null),mt.current=null,Z.current!==void 0&&(window.cancelAnimationFrame(Z.current),Z.current=void 0),x("onAxSnapshotChange",kt.current,null))},[et]),f.useEffect(()=>()=>{Z.current!==void 0&&(window.cancelAnimationFrame(Z.current),Z.current=void 0)},[]),f.useEffect(()=>{if(!et)return;const e=i=>{i.key==="Escape"&&(Kt||Yt)&&(ht(null),Ze(null),Ye(null),x("onInspectSelectionChange",Pe.current,null))};return window.addEventListener("keydown",e),()=>window.removeEventListener("keydown",e)},[et,Kt,Yt]);const ln=()=>{F.current&&F.current.focus()};f.useImperativeHandle(fe,()=>({openUrl:e=>{if(!D.current||D.current.readyState!==WebSocket.OPEN){R("WebSocket not open, cannot send open_url command via ref.");return}try{const i=decodeURIComponent(e);k("Opening URL"),D.current.send(JSON.stringify({type:"openUrl",url:i,sessionId:ee}))}catch(i){R("Error decoding or sending URL via ref:",{error:i,url:e}),D.current.send(JSON.stringify({type:"openUrl",url:e,sessionId:ee}))}j.current?.bumpActivity()},sendKeyEvent:e=>{if(!L.current||L.current.readyState!=="open"){R("Data channel not ready for imperative key command:",L.current?.readyState);return}const i=Rr[e.code];if(!i){R(`Unknown event.code for imperative command: ${e.code}`);return}let r=s.META_NONE;e.shiftKey&&(r|=s.META_SHIFT_ON),e.altKey&&(r|=s.META_ALT_ON),e.ctrlKey&&(r|=s.META_CTRL_ON),e.metaKey&&(r|=s.META_META_ON);const o=e.type==="keydown"?s.ACTION_DOWN:s.ACTION_UP;W(`Sending Imperative Key Command: code=${e.code}, keycode=${i}, action=${o}, meta=${r}`);const m=it(o,i,0,r);m&&te(m)},screenshot:()=>new Promise((e,i)=>{if(!D.current||D.current.readyState!==WebSocket.OPEN)return R("WebSocket not open, cannot send screenshot command."),i(new Error("WebSocket is not connected or connection is not open."));const r=`ui-ss-${Date.now()}-${Math.random().toString(36).substring(2,9)}`,o={type:"screenshot",id:r};ue.current.set(r,e),se.current.set(r,i),W("Sending screenshot request:",o);try{D.current.send(JSON.stringify(o))}catch(m){R("Failed to send screenshot request immediately:",m),ue.current.delete(r),se.current.delete(r),i(m);return}setTimeout(()=>{ue.current.has(r)&&(R(`Screenshot request timed out for id ${r}`),se.current.get(r)?.(new Error("Screenshot request timed out")),ue.current.delete(r),se.current.delete(r))},3e4)}),terminateApp:e=>new Promise((i,r)=>{if(!D.current||D.current.readyState!==WebSocket.OPEN)return R("WebSocket not open, cannot send terminateApp command."),r(new Error("WebSocket is not connected or connection is not open."));const o=`ui-term-${Date.now()}-${Math.random().toString(36).substring(2,9)}`,m={type:"terminateApp",id:o,bundleId:e};Ce.current.set(o,i),ye.current.set(o,r),W("Sending terminateApp request:",m);try{D.current.send(JSON.stringify(m))}catch(l){R("Failed to send terminateApp request immediately:",l),Ce.current.delete(o),ye.current.delete(o),r(l);return}j.current?.bumpActivity(),setTimeout(()=>{Ce.current.has(o)&&(R(`terminateApp request timed out for id ${o}`),ye.current.get(o)?.(new Error("terminateApp request timed out")),Ce.current.delete(o),ye.current.delete(o))},3e4)}),reconnect:()=>Ft(),refreshAxTree:async()=>{const e=j.current;if(!e)throw new Error("Inspect mode is not active");return e.refresh()},getAxSnapshot:()=>ke.current,setInspectHighlight:e=>{Ze(e?.id??null)},setInspectSelection:e=>{ht(e?.id??null),Ye(e?mt.current:null);const i=ke.current;e&&i?x("onInspectSelectionChange",Pe.current,{element:e,snapshot:i}):x("onInspectSelectionChange",Pe.current,null)},getAxStatus:()=>j.current?.getStatus()??"idle"}));const dn=Lr&&_e!==null;return A.jsxs("div",{ref:we,className:De("rc-container",n),style:{touchAction:"none"},onMouseDown:le,onMouseMove:le,onMouseUp:le,onMouseLeave:le,onTouchStart:le,onTouchMove:le,onTouchEnd:le,onTouchCancel:le,children:[dn&&_e&&A.jsxs(A.Fragment,{children:[A.jsx("div",{className:"rc-touch-indicator",style:{left:`${_e.containerX}px`,top:`${_e.containerY}px`}}),A.jsx("div",{className:"rc-touch-indicator",style:{left:`${_e.mirrorContainerX}px`,top:`${_e.mirrorContainerY}px`}})]}),Et&&A.jsx("img",{ref:he,src:or,alt:"",className:ie==="ios"?De("rc-phone-frame","rc-phone-frame-ios"):"rc-phone-frame",draggable:!1}),A.jsx("video",{ref:F,className:De("rc-video",!Et&&"rc-video-frameless",!Xe&&"rc-video-loading"),style:{...Pr,...ir?{backgroundImage:`url("${ir}")`,backgroundRepeat:"no-repeat",backgroundPosition:"center",backgroundSize:Ue.loadingLogoSize}:{}},autoPlay:!0,playsInline:!0,muted:!0,tabIndex:0,onKeyDown:ur,onKeyUp:ur,onClick:ln,onLoadedData:Qr,onFocus:()=>{F.current&&(F.current.style.outline="none")},onBlur:()=>{F.current&&(F.current.style.outline="none")}}),et&&A.jsx(Hn,{snapshot:Jt,geometry:xr,highlightedId:Yt,selectedId:Kt,mode:Hr,cursorPosition:Fr,frozenCursorPosition:Wr,onSelectChange:(e,i)=>{ht(e?.id??null),e&&i?Ye(i):e||Ye(null);const r=ke.current;e&&r?x("onInspectSelectionChange",Pe.current,{element:e,snapshot:r}):x("onInspectSelectionChange",Pe.current,null)},onTapElement:(e,i)=>{if(i){sr(i.x,i.y);return}const r=ke.current;r&&$r(e,r)}}),At?A.jsxs("div",{className:"rc-terminated",role:"status",children:[A.jsx("div",{className:"rc-terminated-title",children:"Instance terminated"}),A.jsx("div",{className:"rc-terminated-message",children:"This instance no longer exists and can't be reconnected."})]}):Bt&&A.jsx("button",{type:"button",className:"rc-retry-button",onClick:un,children:"Retry"})]})}),Qn=n=>typeof n.message=="string"?n.message:typeof n.error=="string"?n.error:null,Zn=n=>typeof n.dataUri=="string"?{dataUri:n.dataUri}:typeof n.base64=="string"?n.base64.startsWith("data:")?{dataUri:n.base64}:{dataUri:`data:${n.base64.startsWith("/9j/")?"image/jpeg":"image/png"};base64,${n.base64}`}:null;exports.AX_UNAVAILABLE_ERROR=Dt;exports.RemoteControl=zn;exports.axElementAtPoint=br;exports.axElementSelectorExpression=Nr;exports.axElementSummary=Xt;exports.axElementsEqual=Ht;exports.axSnapshotsEqual=$t;exports.clampAxFrameForScreen=Mr;exports.normalizeAndroidTree=Sr;exports.normalizeIosTree=Ar;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@limrun/ui",
3
- "version": "0.15.1",
3
+ "version": "0.16.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -13,6 +13,11 @@
13
13
  "import": "./dist/index.js",
14
14
  "require": "./dist/index.cjs"
15
15
  },
16
+ "./lite": {
17
+ "types": "./dist/lite.d.ts",
18
+ "import": "./dist/lite.js",
19
+ "require": "./dist/lite.cjs"
20
+ },
16
21
  "./package.json": "./package.json"
17
22
  },
18
23
  "scripts": {
@@ -48,9 +53,12 @@
48
53
  "vite": "^7.1.4",
49
54
  "vite-plugin-dts": "^4.5.4",
50
55
  "vite-plugin-lib-inject-css": "^2.2.2",
51
- "vitest": "^3.2.6"
56
+ "vitest": "^4.1.11"
52
57
  },
53
58
  "dependencies": {
54
59
  "clsx": "^2.1.1"
60
+ },
61
+ "resolutions": {
62
+ "vite": "^7.1.4"
55
63
  }
56
64
  }
@@ -0,0 +1,14 @@
1
+ import { forwardRef } from 'react';
2
+ import {
3
+ RemoteControl as RemoteControlBase,
4
+ type RemoteControlHandle,
5
+ type RemoteControlProps,
6
+ } from './remote-control';
7
+ import { defaultRemoteControlAssets } from '../frame-assets';
8
+
9
+ // The default export: RemoteControl with Limrun's device frames unless the
10
+ // caller supplies its own assets.
11
+ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>((props, ref) => (
12
+ <RemoteControlBase ref={ref} assets={defaultRemoteControlAssets} {...props} />
13
+ ));
14
+ RemoteControl.displayName = 'RemoteControl';
@@ -4,14 +4,6 @@ import './remote-control.css';
4
4
 
5
5
  import { ANDROID_KEYS, AMOTION_EVENT, codeMap } from '../core/constants';
6
6
 
7
- import iphoneFrameImage from '../assets/iphone16pro_black_bg.webp';
8
- import pixelFrameImage from '../assets/pixel9_black.webp';
9
- import pixelFrameImageLandscape from '../assets/pixel9_black_landscape.webp';
10
- import pixelTabletFrameImage from '../assets/pixel_tablet_portrait.webp';
11
- import pixelTabletFrameImageLandscape from '../assets/pixel_tablet_landscape.webp';
12
- import iphoneFrameImageLandscape from '../assets/iphone16pro_black_landscape_bg.webp';
13
- import appleLogoSvg from '../assets/Apple_logo_white.svg';
14
- import androidBootImage from '../assets/android_boot.webp';
15
7
  import {
16
8
  createTouchControlMessage,
17
9
  createInjectKeycodeMessage,
@@ -82,6 +74,15 @@ export interface RemoteControlProps {
82
74
  * etc.). It is not called for ordinary network drops.
83
75
  */
84
76
  onTerminated?: () => void;
77
+ /**
78
+ * Device frame and boot-logo images. The default export bundles Limrun's
79
+ * frames; `@limrun/ui/lite` omits them so a frameless embed stays small.
80
+ */
81
+ assets?: RemoteControlAssets;
82
+ /**
83
+ * Coarse connection lifecycle, for status UI outside the component.
84
+ */
85
+ onConnectionStateChange?: (state: RemoteControlConnectionState) => void;
85
86
 
86
87
  /**
87
88
  * Enable the inspect overlay. When set, the component starts polling the
@@ -388,18 +389,30 @@ export const withAuthenticationToken = (url: string, token: string): string => {
388
389
  return endpoint.toString();
389
390
  };
390
391
 
392
+ export type RemoteControlConnectionState =
393
+ | 'connecting'
394
+ | 'connected'
395
+ | 'reconnecting'
396
+ | 'failed'
397
+ | 'terminated';
398
+
399
+ export type RemoteControlPlatformAssets = {
400
+ frame?: string;
401
+ frameLandscape?: string;
402
+ tabletFrame?: string;
403
+ tabletFrameLandscape?: string;
404
+ loadingLogo?: string;
405
+ };
406
+
407
+ export type RemoteControlAssets = Partial<Record<DevicePlatform, RemoteControlPlatformAssets>>;
408
+
391
409
  type DeviceConfig = {
392
410
  videoBorderRadiusMultiplier: number;
393
- loadingLogo: string;
394
411
  loadingLogoSize: string;
395
412
  videoPosition: {
396
413
  portrait: { heightMultiplier?: number; widthMultiplier?: number };
397
414
  landscape: { heightMultiplier?: number; widthMultiplier?: number };
398
415
  };
399
- frame: {
400
- image: string;
401
- imageLandscape: string;
402
- };
403
416
  };
404
417
 
405
418
  const ANDROID_TABLET_VIDEO_WIDTH = 1920;
@@ -420,12 +433,7 @@ const isAndroidTabletVideo = (width: number, height: number): boolean =>
420
433
  // Video position percentages are relative to the frame image dimensions
421
434
  const deviceConfig: Record<DevicePlatform, DeviceConfig> = {
422
435
  ios: {
423
- frame: {
424
- image: iphoneFrameImage,
425
- imageLandscape: iphoneFrameImageLandscape,
426
- },
427
436
  videoBorderRadiusMultiplier: 0.15,
428
- loadingLogo: appleLogoSvg,
429
437
  loadingLogoSize: '20%',
430
438
  // Video position as percentage of frame dimensions
431
439
  videoPosition: {
@@ -434,12 +442,7 @@ const deviceConfig: Record<DevicePlatform, DeviceConfig> = {
434
442
  },
435
443
  },
436
444
  android: {
437
- frame: {
438
- image: pixelFrameImage,
439
- imageLandscape: pixelFrameImageLandscape,
440
- },
441
445
  videoBorderRadiusMultiplier: 0.13,
442
- loadingLogo: androidBootImage,
443
446
  loadingLogoSize: '40%',
444
447
  // Video position as percentage of frame dimensions
445
448
  videoPosition: {
@@ -490,6 +493,8 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
490
493
  showFrame = true,
491
494
  autoReconnect = false,
492
495
  onTerminated,
496
+ assets,
497
+ onConnectionStateChange,
493
498
  inspectMode,
494
499
  onAxSnapshotChange,
495
500
  onInspectSelectionChange,
@@ -702,6 +707,16 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
702
707
  onAxStatusChangeRef.current = onAxStatusChange;
703
708
  const onTerminatedRef = useRef(onTerminated);
704
709
  onTerminatedRef.current = onTerminated;
710
+ const onConnectionStateChangeRef = useRef(onConnectionStateChange);
711
+ onConnectionStateChangeRef.current = onConnectionStateChange;
712
+ const connectionStateRef = useRef<RemoteControlConnectionState | null>(null);
713
+ // Reports each transition once; callers get status text without tracking
714
+ // the retry machinery below.
715
+ const reportConnectionState = (next: RemoteControlConnectionState) => {
716
+ if (connectionStateRef.current === next) return;
717
+ connectionStateRef.current = next;
718
+ safeInvoke('onConnectionStateChange', onConnectionStateChangeRef.current, next);
719
+ };
705
720
 
706
721
  const inspectActive = inspectMode === true || inspectMode === 'hover-only';
707
722
  const inspectModeResolved: InspectMode = inspectMode === 'hover-only' ? 'hover-only' : 'select';
@@ -715,6 +730,17 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
715
730
 
716
731
  const platform = useMemo(() => detectPlatform(url), [url]);
717
732
  const config = deviceConfig[platform];
733
+ const platformAssets = assets?.[platform];
734
+ const frameImageSrc =
735
+ platform === 'android' && useAndroidTabletFrame ?
736
+ isLandscape ? platformAssets?.tabletFrameLandscape
737
+ : platformAssets?.tabletFrame
738
+ : isLandscape ? platformAssets?.frameLandscape
739
+ : platformAssets?.frame;
740
+ const loadingLogo = platformAssets?.loadingLogo;
741
+ // Without a frame image (the lite entry) the video lays out frameless
742
+ // whatever showFrame says.
743
+ const frameVisible = showFrame && !!frameImageSrc;
718
744
 
719
745
  const updateStatus = (message: string) => {
720
746
  // Use the wrapper for conditional logging
@@ -1809,6 +1835,7 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
1809
1835
  firstFrameShownRef.current = true;
1810
1836
  stopRequestFrameLoop();
1811
1837
  setVideoLoaded(true);
1838
+ reportConnectionState('connected');
1812
1839
  };
1813
1840
 
1814
1841
  // Stop every track on a MediaStream and release the device handle.
@@ -2454,6 +2481,7 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
2454
2481
  clearIceDisconnectedGrace();
2455
2482
  teardownConnection();
2456
2483
  updateStatus('Instance terminated');
2484
+ reportConnectionState('terminated');
2457
2485
  safeInvoke('onTerminated', onTerminatedRef.current);
2458
2486
  };
2459
2487
 
@@ -2489,6 +2517,7 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
2489
2517
  if (!autoReconnectRef.current) {
2490
2518
  updateStatus(`Connection failed after it was established: ${reason}`);
2491
2519
  setRetryExhausted(true);
2520
+ reportConnectionState('failed');
2492
2521
  teardownConnection();
2493
2522
  return;
2494
2523
  }
@@ -2504,11 +2533,13 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
2504
2533
  if (nextAttempt >= MAX_CONNECTION_ATTEMPTS) {
2505
2534
  updateStatus(`Connection failed after ${MAX_CONNECTION_ATTEMPTS} attempts: ${reason}`);
2506
2535
  setRetryExhausted(true);
2536
+ reportConnectionState('failed');
2507
2537
  teardownConnection();
2508
2538
  return;
2509
2539
  }
2510
2540
 
2511
2541
  updateStatus(`Retrying connection (${nextAttempt + 1}/${MAX_CONNECTION_ATTEMPTS})`);
2542
+ reportConnectionState('reconnecting');
2512
2543
  teardownConnection();
2513
2544
  retryTimeoutRef.current = window.setTimeout(() => {
2514
2545
  retryTimeoutRef.current = undefined;
@@ -2528,6 +2559,10 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
2528
2559
  connectionAttemptRef.current = attemptNumber;
2529
2560
  controlChannelOpenedRef.current = false;
2530
2561
  setRetryExhausted(false);
2562
+ // A drop after an established session restarts the attempt budget at 0
2563
+ // while staying 'reconnecting'; only a fresh start reports 'connecting'.
2564
+ if (attemptNumber === 0 && connectionStateRef.current !== 'reconnecting')
2565
+ reportConnectionState('connecting');
2531
2566
  clearScheduledRetry();
2532
2567
  clearConnectionSuccessTimeout();
2533
2568
  stopRequestFrameLoop();
@@ -2610,24 +2645,43 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
2610
2645
  rtcConfiguration: RTCConfiguration;
2611
2646
  cameraSupported: boolean;
2612
2647
  }>((resolve, reject) => {
2613
- const timeoutId = window.setTimeout(() => reject(new Error('RTCConfiguration timeout')), 30000);
2648
+ const settle = (callback: () => void) => {
2649
+ window.clearTimeout(timeoutId);
2650
+ ws.removeEventListener('message', messageHandler);
2651
+ callback();
2652
+ };
2653
+ const timeoutId = window.setTimeout(
2654
+ () => settle(() => reject(new Error('RTCConfiguration timeout'))),
2655
+ 30000,
2656
+ );
2614
2657
 
2615
2658
  const messageHandler = (event: MessageEvent) => {
2659
+ if (!isCurrentAttempt() || wsRef.current !== ws) {
2660
+ return;
2661
+ }
2616
2662
  try {
2617
2663
  const message = JSON.parse(event.data);
2618
- if (message.type === 'rtcConfiguration') {
2619
- window.clearTimeout(timeoutId);
2620
- ws.removeEventListener('message', messageHandler);
2621
- resolve({
2622
- rtcConfiguration: message.rtcConfiguration,
2623
- cameraSupported: message.cameraSupported === true,
2624
- });
2664
+ if (
2665
+ message.type === 'signalingError' &&
2666
+ message.requestType === 'requestRtcConfiguration' &&
2667
+ message.sessionId === sessionId
2668
+ ) {
2669
+ const reason =
2670
+ typeof message.error === 'string' ?
2671
+ message.error
2672
+ : 'Unable to load streaming configuration';
2673
+ settle(() => reject(new Error(reason)));
2674
+ } else if (message.type === 'rtcConfiguration') {
2675
+ settle(() =>
2676
+ resolve({
2677
+ rtcConfiguration: message.rtcConfiguration,
2678
+ cameraSupported: message.cameraSupported === true,
2679
+ }),
2680
+ );
2625
2681
  }
2626
2682
  } catch (e) {
2627
- window.clearTimeout(timeoutId);
2628
- ws.removeEventListener('message', messageHandler);
2629
2683
  console.error('Error handling RTC configuration:', e);
2630
- reject(e);
2684
+ settle(() => reject(e));
2631
2685
  }
2632
2686
  };
2633
2687
 
@@ -2986,6 +3040,18 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
2986
3040
  }
2987
3041
  updateStatus('Received: ' + message.type);
2988
3042
  switch (message.type) {
3043
+ case 'signalingError': {
3044
+ if (message.requestType !== 'offer' || message.sessionId !== sessionId) {
3045
+ break;
3046
+ }
3047
+ const reason =
3048
+ typeof message.error === 'string' ?
3049
+ message.error
3050
+ : 'Unable to establish the streaming connection';
3051
+ updateStatus('Error: ' + reason);
3052
+ scheduleRetry(reason, generation);
3053
+ break;
3054
+ }
2989
3055
  case 'answer':
2990
3056
  if (!peerConnectionRef.current || peerConnectionRef.current !== peerConnection) {
2991
3057
  updateStatus('No peer connection, skipping answer');
@@ -3296,7 +3362,7 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
3296
3362
  const updateVideoPosition = () => {
3297
3363
  // If no frame, just refresh overlay geometry; no inset/letterbox math
3298
3364
  // is needed since the video element is its own size.
3299
- if (!showFrame || !frame) {
3365
+ if (!frameVisible || !frame) {
3300
3366
  setVideoStyle({});
3301
3367
  recomputeOverlayGeometry();
3302
3368
  return;
@@ -3369,7 +3435,7 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
3369
3435
  video.removeEventListener('resize', bumpOnResize);
3370
3436
  if (frame) frame.removeEventListener('load', updateVideoPosition);
3371
3437
  };
3372
- }, [config, showFrame]);
3438
+ }, [config, frameVisible]);
3373
3439
 
3374
3440
  // Start/stop the AX poller and reset inspect state when inspect mode
3375
3441
  // toggles. Connection state is independent: the fetcher gets created on
@@ -3613,12 +3679,6 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
3613
3679
 
3614
3680
  // Show indicators when Alt is held and we have a valid hover point (null when outside)
3615
3681
  const showAltIndicators = isAltHeld && hoverPoint !== null;
3616
- const frameImageSrc =
3617
- platform === 'android' && useAndroidTabletFrame ?
3618
- isLandscape ? pixelTabletFrameImageLandscape
3619
- : pixelTabletFrameImage
3620
- : isLandscape ? config.frame.imageLandscape
3621
- : config.frame.image;
3622
3682
 
3623
3683
  return (
3624
3684
  <div
@@ -3654,7 +3714,7 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
3654
3714
  />
3655
3715
  </>
3656
3716
  )}
3657
- {showFrame && (
3717
+ {frameVisible && (
3658
3718
  <img
3659
3719
  ref={frameRef}
3660
3720
  src={frameImageSrc}
@@ -3665,12 +3725,16 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
3665
3725
  )}
3666
3726
  <video
3667
3727
  ref={videoRef}
3668
- className={clsx('rc-video', !showFrame && 'rc-video-frameless', !videoLoaded && 'rc-video-loading')}
3728
+ className={clsx(
3729
+ 'rc-video',
3730
+ !frameVisible && 'rc-video-frameless',
3731
+ !videoLoaded && 'rc-video-loading',
3732
+ )}
3669
3733
  style={{
3670
3734
  ...videoStyle,
3671
- ...(config.loadingLogo ?
3735
+ ...(loadingLogo ?
3672
3736
  {
3673
- backgroundImage: `url("${config.loadingLogo}")`,
3737
+ backgroundImage: `url("${loadingLogo}")`,
3674
3738
  backgroundRepeat: 'no-repeat',
3675
3739
  backgroundPosition: 'center',
3676
3740
  backgroundSize: config.loadingLogoSize,
package/src/demo.tsx CHANGED
@@ -1,6 +1,7 @@
1
1
  import { useState, useRef } from 'react';
2
2
  import { createRoot } from 'react-dom/client';
3
- import { RemoteControl, RemoteControlHandle } from './components/remote-control';
3
+ import { RemoteControl } from './components/remote-control-framed';
4
+ import type { RemoteControlHandle } from './components/remote-control';
4
5
  import { AxSnapshot } from './core/ax-tree';
5
6
 
6
7
  type InspectChoice = 'off' | 'hover-only' | 'select';
@@ -0,0 +1,22 @@
1
+ // Limrun's bundled device frames and boot logos. Imported only by the default
2
+ // entry so `@limrun/ui/lite` ships without ~600 KB of inlined artwork.
3
+ import type { RemoteControlAssets } from './components/remote-control';
4
+ import iphoneFrame from './assets/iphone16pro_black_bg.webp';
5
+ import iphoneFrameLandscape from './assets/iphone16pro_black_landscape_bg.webp';
6
+ import pixelFrame from './assets/pixel9_black.webp';
7
+ import pixelFrameLandscape from './assets/pixel9_black_landscape.webp';
8
+ import pixelTabletFrame from './assets/pixel_tablet_portrait.webp';
9
+ import pixelTabletFrameLandscape from './assets/pixel_tablet_landscape.webp';
10
+ import appleLogo from './assets/Apple_logo_white.svg';
11
+ import androidBootLogo from './assets/android_boot.webp';
12
+
13
+ export const defaultRemoteControlAssets: RemoteControlAssets = {
14
+ ios: { frame: iphoneFrame, frameLandscape: iphoneFrameLandscape, loadingLogo: appleLogo },
15
+ android: {
16
+ frame: pixelFrame,
17
+ frameLandscape: pixelFrameLandscape,
18
+ tabletFrame: pixelTabletFrame,
19
+ tabletFrameLandscape: pixelTabletFrameLandscape,
20
+ loadingLogo: androidBootLogo,
21
+ },
22
+ };
package/src/index.ts CHANGED
@@ -1,7 +1,11 @@
1
- export { RemoteControl } from './components/remote-control';
1
+ export { RemoteControl } from './components/remote-control-framed';
2
+ export { defaultRemoteControlAssets } from './frame-assets';
2
3
  export type {
3
4
  RemoteControlProps,
4
5
  RemoteControlHandle,
6
+ RemoteControlAssets,
7
+ RemoteControlPlatformAssets,
8
+ RemoteControlConnectionState,
5
9
  MicrophoneState,
6
10
  CameraStreamStats,
7
11
  } from './components/remote-control';
@@ -0,0 +1,30 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+
5
+ // Guards the reason the lite entry exists: an embed that imports it must not
6
+ // pull the bundled device artwork. Runs against dist, so build first.
7
+ const dist = join(__dirname, '..', 'dist');
8
+ const describeDist = existsSync(join(dist, 'lite.js')) ? describe : describe.skip;
9
+
10
+ describeDist('@limrun/ui/lite bundle', () => {
11
+ const chunksOf = (entry: string): string[] => {
12
+ const src = readFileSync(join(dist, entry), 'utf8');
13
+ const chunks = [...src.matchAll(/from "\.\/([^"]+)"/g)].map((m) => m[1]);
14
+ return [src, ...chunks.map((c) => readFileSync(join(dist, c), 'utf8'))];
15
+ };
16
+
17
+ test('lite entry and its chunks carry no inlined images', () => {
18
+ for (const code of chunksOf('lite.js')) {
19
+ expect(code).not.toMatch(/data:image\//);
20
+ }
21
+ });
22
+
23
+ test('default entry still bundles the device frames (positive control)', () => {
24
+ expect(chunksOf('index.js').some((code) => /data:image\/webp/.test(code))).toBe(true);
25
+ });
26
+
27
+ test('one stylesheet keeps its historical name', () => {
28
+ expect(readdirSync(dist).filter((f) => f.endsWith('.css'))).toEqual(['index.css']);
29
+ });
30
+ });
package/src/lite.ts ADDED
@@ -0,0 +1,30 @@
1
+ // Frameless entry: identical API, but no bundled device artwork. Pass `assets`
2
+ // to add frames or boot logos.
3
+ export { RemoteControl } from './components/remote-control';
4
+ export type {
5
+ RemoteControlProps,
6
+ RemoteControlHandle,
7
+ RemoteControlAssets,
8
+ RemoteControlPlatformAssets,
9
+ RemoteControlConnectionState,
10
+ MicrophoneState,
11
+ CameraStreamStats,
12
+ } from './components/remote-control';
13
+ export type { CameraCaptureInfo, CameraFacingMode, CameraRequest, CameraResult } from './core/camera';
14
+
15
+ // Accessibility / inspect-mode types and helpers. Exported so customers can
16
+ // build their own side panels, search UIs, or agent-driven inspectors on top
17
+ // of the snapshots delivered via `onAxSnapshotChange`.
18
+ export type { AxSnapshot, AxElement, AxRect, AxSelectors, AxPlatform } from './core/ax-tree';
19
+ export type { AndroidElementTreeOptions, AxStatus } from './core/ax-fetcher';
20
+ export {
21
+ axElementAtPoint,
22
+ axElementSelectorExpression,
23
+ axElementSummary,
24
+ axElementsEqual,
25
+ axSnapshotsEqual,
26
+ clampAxFrameForScreen,
27
+ normalizeAndroidTree,
28
+ normalizeIosTree,
29
+ AX_UNAVAILABLE_ERROR,
30
+ } from './core/ax-tree';