@limrun/ui 0.17.0-rc.1 → 0.17.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -3
- package/dist/{GLTFLoader-DW6wpvyR.mjs → GLTFLoader-Cami_vza.mjs} +1 -1
- package/dist/{GLTFLoader-BKnsBFsj.js → GLTFLoader-DeCLxW_q.js} +1 -1
- package/dist/components/duo-fold-motion.d.ts +1 -1
- package/dist/components/duo-loading.d.ts +8 -0
- package/dist/components/duo-view.d.ts +6 -1
- package/dist/{duo-frame-BzyYljix.mjs → duo-frame-Cmw4p2uN.mjs} +414 -414
- package/dist/{duo-frame-DjfA4gG-.js → duo-frame-iMtrvx_a.js} +18 -18
- package/dist/duo-view-BAh-w2Kz.js +1 -0
- package/dist/duo-view-D9Ae00rj.mjs +512 -0
- package/dist/index.cjs +1 -1
- package/dist/index.css +1 -1
- package/dist/index.js +2 -2
- package/dist/lite.cjs +1 -1
- package/dist/lite.js +1 -1
- package/dist/remote-control-B2jZpf3c.js +1 -0
- package/dist/remote-control-DiddxS8l.mjs +2883 -0
- package/package.json +5 -1
- package/dist/demo.d.ts +0 -1
- package/dist/duo-view-COZg8ieG.js +0 -1
- package/dist/duo-view-o1yxIzxu.mjs +0 -654
- package/dist/remote-control-CQjSEVDF.mjs +0 -2673
- package/dist/remote-control-DVRXfCR2.js +0 -1
- package/index.html +0 -199
- package/src/assets/Apple_logo_white.svg +0 -4
- package/src/assets/android_boot.gif +0 -0
- package/src/assets/android_boot.webp +0 -0
- package/src/assets/iphone16pro_black.webp +0 -0
- package/src/assets/iphone16pro_black_bg.webp +0 -0
- package/src/assets/iphone16pro_black_landscape.webp +0 -0
- package/src/assets/iphone16pro_black_landscape_bg.webp +0 -0
- package/src/assets/pixel9_black.webp +0 -0
- package/src/assets/pixel9_black_landscape.webp +0 -0
- package/src/assets/pixel_tablet_landscape.webp +0 -0
- package/src/assets/pixel_tablet_portrait.webp +0 -0
- package/src/components/device-install/index.ts +0 -1
- package/src/components/duo-button-hinge.test.tsx +0 -138
- package/src/components/duo-button-hinge.ts +0 -75
- package/src/components/duo-controls.tsx +0 -142
- package/src/components/duo-flat-frame.tsx +0 -145
- package/src/components/duo-flat-geometry.test.ts +0 -63
- package/src/components/duo-flat-geometry.ts +0 -97
- package/src/components/duo-fold-motion.test.tsx +0 -245
- package/src/components/duo-fold-motion.tsx +0 -192
- package/src/components/duo-fold-renderer.test.ts +0 -29
- package/src/components/duo-fold-renderer.ts +0 -397
- package/src/components/duo-fold-timing.test.ts +0 -44
- package/src/components/duo-fold-timing.ts +0 -9
- package/src/components/duo-frame.tsx +0 -579
- package/src/components/duo-hardware.test.ts +0 -61
- package/src/components/duo-hardware.ts +0 -61
- package/src/components/duo-model.test.ts +0 -243
- package/src/components/duo-model.ts +0 -607
- package/src/components/duo-screen-handoff.ts +0 -52
- package/src/components/duo-view.test.tsx +0 -104
- package/src/components/duo-view.tsx +0 -315
- package/src/components/inspect-overlay.css +0 -223
- package/src/components/inspect-overlay.tsx +0 -437
- package/src/components/remote-control-framed.tsx +0 -14
- package/src/components/remote-control.css +0 -441
- package/src/components/remote-control.test.ts +0 -34
- package/src/components/remote-control.tsx +0 -3989
- package/src/core/ax-fetcher.test.ts +0 -418
- package/src/core/ax-fetcher.ts +0 -387
- package/src/core/ax-tree.test.ts +0 -491
- package/src/core/ax-tree.ts +0 -416
- package/src/core/camera.test.ts +0 -155
- package/src/core/camera.ts +0 -133
- package/src/core/constants.ts +0 -344
- package/src/core/duo.test.ts +0 -142
- package/src/core/duo.ts +0 -156
- package/src/core/webrtc-messages.ts +0 -148
- package/src/demo.tsx +0 -283
- package/src/frame-assets.ts +0 -22
- package/src/hooks/index.ts +0 -1
- package/src/index.ts +0 -29
- package/src/lite.test.ts +0 -30
- package/src/lite.ts +0 -30
- package/src/vite-env.d.ts +0 -1
- package/tsconfig.json +0 -26
- package/tsconfig.node.json +0 -26
- package/vite.config.ts +0 -35
- package/vitest.config.ts +0 -24
|
@@ -1 +0,0 @@
|
|
|
1
|
-
"use strict";require('./index.css');const w=require("react/jsx-runtime"),l=require("react"),An=require("react-dom");function Pr(r){var t,s,a="";if(typeof r=="string"||typeof r=="number")a+=r;else if(typeof r=="object")if(Array.isArray(r)){var g=r.length;for(t=0;t<g;t++)r[t]&&(s=Pr(r[t]))&&(a&&(a+=" "),a+=s)}else for(s in r)r[s]&&(a&&(a+=" "),a+=s);return a}function Se(){for(var r,t,s=0,a="",g=arguments.length;s<g;s++)(r=arguments[s])&&(t=Pr(r))&&(a&&(a+=" "),a+=t);return a}const Mn={width:164.6,height:117.8,depth:5.2};function bn(r,t,s){return r=Math.max(0,Math.min(1,r)),t=Math.max(0,Math.min(1,t)),s===1?{x:t,y:1-r}:s===2?{x:1-r,y:1-t}:s===3?{x:1-t,y:r}:{x:r,y:t}}function Nn(r){return Number.isFinite(r)&&r>=0&&r<=180}function In(r,t){return{x:Math.max(0,Math.min(1,r)),y:Math.max(0,Math.min(1,1-t))}}function Yn(r,t,s){return t&&(r||!s)?"touch":r?"ignore":"orbit"}function Kn(r,t,s,a){if(!Number.isInteger(r)||r<0||r>3||!Number.isInteger(t)||t<1||t>255||!Number.isFinite(s)||!Number.isFinite(a)||s<0||s>1||a<0||a>1)throw new RangeError("Invalid display touch coordinates");const g=new ArrayBuffer(12),E=new DataView(g);return E.setUint8(0,19),E.setUint8(1,r),E.setUint8(2,t),E.setFloat32(4,s,!0),E.setFloat32(8,a,!0),g}class kn{constructor(t,s){this.send=t,this.failed=s}send;failed;latest;running=!1;stopped=!1;set(t){this.stopped||!Nn(t)||(this.latest=t,this.flush())}stop(){this.stopped=!0,this.latest=void 0}async flush(){if(!(this.running||this.stopped)){this.running=!0;try{for(;this.latest!==void 0&&!this.stopped;){const t=this.latest;this.latest=void 0,await this.send(t)}}catch(t){this.latest=void 0,this.stopped||this.failed(t)}finally{this.running=!1}}}}const Pn={side:"Sleep/Wake",volumeUp:"Volume Up",volumeDown:"Volume Down"};class Un{constructor(t,s){this.send=t,this.failed=s}send;failed;held;tail=Promise.resolve();timeout;press(t){this.held||(this.held=t,this.enqueue(t,!0),this.timeout=setTimeout(()=>this.release(),1e4))}release(){if(!this.held)return;clearTimeout(this.timeout);const t=this.held;this.held=void 0,this.enqueue(t,!1)}enqueue(t,s){this.tail=this.tail.then(()=>this.send(t,s)).catch(this.failed)}}const Yt={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},c={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},Ur={KeyA:c.KEYCODE_A,KeyB:c.KEYCODE_B,KeyC:c.KEYCODE_C,KeyD:c.KEYCODE_D,KeyE:c.KEYCODE_E,KeyF:c.KEYCODE_F,KeyG:c.KEYCODE_G,KeyH:c.KEYCODE_H,KeyI:c.KEYCODE_I,KeyJ:c.KEYCODE_J,KeyK:c.KEYCODE_K,KeyL:c.KEYCODE_L,KeyM:c.KEYCODE_M,KeyN:c.KEYCODE_N,KeyO:c.KEYCODE_O,KeyP:c.KEYCODE_P,KeyQ:c.KEYCODE_Q,KeyR:c.KEYCODE_R,KeyS:c.KEYCODE_S,KeyT:c.KEYCODE_T,KeyU:c.KEYCODE_U,KeyV:c.KEYCODE_V,KeyW:c.KEYCODE_W,KeyX:c.KEYCODE_X,KeyY:c.KEYCODE_Y,KeyZ:c.KEYCODE_Z,Digit0:c.KEYCODE_0,Digit1:c.KEYCODE_1,Digit2:c.KEYCODE_2,Digit3:c.KEYCODE_3,Digit4:c.KEYCODE_4,Digit5:c.KEYCODE_5,Digit6:c.KEYCODE_6,Digit7:c.KEYCODE_7,Digit8:c.KEYCODE_8,Digit9:c.KEYCODE_9,Backquote:c.KEYCODE_GRAVE,Minus:c.KEYCODE_MINUS,Equal:c.KEYCODE_EQUALS,BracketLeft:c.KEYCODE_LEFT_BRACKET,BracketRight:c.KEYCODE_RIGHT_BRACKET,Backslash:c.KEYCODE_BACKSLASH,Semicolon:c.KEYCODE_SEMICOLON,Quote:c.KEYCODE_APOSTROPHE,Comma:c.KEYCODE_COMMA,Period:c.KEYCODE_PERIOD,Slash:c.KEYCODE_SLASH,Space:c.KEYCODE_SPACE,Tab:c.KEYCODE_TAB,Escape:c.KEYCODE_ESCAPE,ArrowUp:c.DPAD_UP,ArrowDown:c.DPAD_DOWN,ArrowLeft:c.DPAD_LEFT,ArrowRight:c.DPAD_RIGHT,Enter:c.ENTER,Backspace:c.DEL,Delete:c.FORWARD_DEL,Home:c.KEYCODE_MOVE_HOME,End:c.KEYCODE_MOVE_END,PageUp:c.KEYCODE_PAGE_UP,PageDown:c.KEYCODE_PAGE_DOWN,Insert:c.KEYCODE_INSERT,F1:c.KEYCODE_F1,F2:c.KEYCODE_F2,F3:c.KEYCODE_F3,F4:c.KEYCODE_F4,F5:c.KEYCODE_F5,F6:c.KEYCODE_F6,F7:c.KEYCODE_F7,F8:c.KEYCODE_F8,F9:c.KEYCODE_F9,F10:c.KEYCODE_F10,F11:c.KEYCODE_F11,F12:c.KEYCODE_F12,ShiftLeft:c.KEYCODE_SHIFT_LEFT,ShiftRight:c.KEYCODE_SHIFT_RIGHT,ControlLeft:c.KEYCODE_CTRL_LEFT,ControlRight:c.KEYCODE_CTRL_RIGHT,AltLeft:c.KEYCODE_ALT_LEFT,AltRight:c.KEYCODE_ALT_RIGHT,MetaLeft:c.KEYCODE_META_LEFT,MetaRight:c.KEYCODE_META_RIGHT,ContextMenu:c.MENU,Numpad0:c.KEYCODE_NUMPAD_0,Numpad1:c.KEYCODE_NUMPAD_1,Numpad2:c.KEYCODE_NUMPAD_2,Numpad3:c.KEYCODE_NUMPAD_3,Numpad4:c.KEYCODE_NUMPAD_4,Numpad5:c.KEYCODE_NUMPAD_5,Numpad6:c.KEYCODE_NUMPAD_6,Numpad7:c.KEYCODE_NUMPAD_7,Numpad8:c.KEYCODE_NUMPAD_8,Numpad9:c.KEYCODE_NUMPAD_9,NumpadDivide:c.KEYCODE_NUMPAD_DIVIDE,NumpadMultiply:c.KEYCODE_NUMPAD_MULTIPLY,NumpadSubtract:c.KEYCODE_NUMPAD_SUBTRACT,NumpadAdd:c.KEYCODE_NUMPAD_ADD,NumpadDecimal:c.KEYCODE_NUMPAD_DOT,NumpadComma:c.KEYCODE_NUMPAD_COMMA,NumpadEnter:c.KEYCODE_NUMPAD_ENTER,NumpadEqual:c.KEYCODE_NUMPAD_EQUALS};function Mt(r,t,s,a,g,E,y=1,O=0,b=0){const D=new ArrayBuffer(32),N=new DataView(D);let A=0;return N.setUint8(A,Yt.INJECT_TOUCH_EVENT),A+=1,N.setUint8(A,r),A+=1,N.setBigInt64(A,BigInt(t),!0),A+=8,N.setInt32(A,Math.round(g),!0),A+=4,N.setInt32(A,Math.round(E),!0),A+=4,N.setUint16(A,s,!0),A+=2,N.setUint16(A,a,!0),A+=2,N.setInt16(A,Math.round(y*65535),!0),A+=2,N.setInt32(A,O,!0),A+=4,N.setInt32(A,b,!0),D}function Ln(r,t,s,a,g,E,y){const O=new ArrayBuffer(24),b=new DataView(O);let D=0;return b.setUint8(D,Yt.INJECT_TWO_FINGER_TOUCH_EVENT),D+=1,b.setUint8(D,r),D+=1,b.setInt32(D,Math.round(a),!0),D+=4,b.setInt32(D,Math.round(g),!0),D+=4,b.setInt32(D,Math.round(E),!0),D+=4,b.setInt32(D,Math.round(y),!0),D+=4,b.setUint16(D,t,!0),D+=2,b.setUint16(D,s,!0),D+=2,b.setUint16(D,0,!0),O}function xn(r,t=!0){const a=new TextEncoder().encode(r),g=new ArrayBuffer(14+a.length),E=new DataView(g);let y=0;return E.setUint8(y,Yt.SET_CLIPBOARD),y+=1,E.setBigInt64(y,BigInt(0),!1),y+=8,E.setUint8(y,t?1:0),y+=1,E.setUint32(y,a.length,!1),y+=4,new Uint8Array(g,y).set(a),g}function lt(r,t,s=0,a=0){const g=new ArrayBuffer(14),E=new DataView(g);let y=0;return E.setUint8(y,Yt.INJECT_KEYCODE),y+=1,E.setUint8(y,r),y+=1,E.setInt32(y,t,!0),y+=4,E.setInt32(y,s,!0),y+=4,E.setInt32(y,a,!0),g}const Nt="Accessibility unavailable on this device.",It=500,Lr=(r,t)=>Math.abs(r.x-t.x)<.5&&Math.abs(r.y-t.y)<.5&&Math.abs(r.width-t.width)<.5&&Math.abs(r.height-t.height)<.5,Fn=r=>Math.max(0,r.width)*Math.max(0,r.height),Wn=r=>{const t=r.find(s=>s.frame&&s.frame.width>0&&s.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}},Hn=r=>{const t={};return typeof r.AXUniqueId=="string"&&r.AXUniqueId.length>0&&(t.AXUniqueId=r.AXUniqueId),typeof r.AXLabel=="string"&&r.AXLabel.length>0&&(t.AXLabel=r.AXLabel),typeof r.type=="string"&&r.type.length>0&&(t.className=r.type),t},$n=r=>{const t={};for(const[s,a]of Object.entries(r))s!=="children"&&(t[s]=a);return t};function xr(r){const t=Array.isArray(r)?r:[r],s=Wn(t),a=[],g=(E,y)=>{if(a.length>=It)return;const O=E.frame;if(O&&O.width>0&&O.height>0&&!Lr(O,s)){const D=E.role_description||E.role||"",N=(typeof E.AXLabel=="string"?E.AXLabel:"")||(typeof E.title=="string"?E.title:"")||"";a.push({id:typeof E.AXUniqueId=="string"&&E.AXUniqueId.length>0?E.AXUniqueId:y,path:y,label:N,value:typeof E.AXValue=="string"?E.AXValue:"",role:D,type:typeof E.type=="string"?E.type:"",enabled:E.enabled!==!1,focused:E.focused===!0,frame:{x:O.x,y:O.y,width:O.width,height:O.height},selectors:Hn(E),raw:$n(E)})}const b=Array.isArray(E.children)?E.children:[];for(let D=0;D<b.length&&a.length<It;D++)g(b[D],`${y}.${D}`)};for(let E=0;E<t.length&&a.length<It;E++)g(t[E],String(E));return{platform:"ios",screen:{width:s.width,height:s.height},elements:a,capturedAt:Date.now()}}const Xn=r=>{let t={x:0,y:0,width:1,height:1},s=0;for(const a of r){const g=a.parsedBounds;if(!g)continue;const E=g.right-g.left,y=g.bottom-g.top,O=Math.max(0,E)*Math.max(0,y);O>s&&(s=O,t={x:0,y:0,width:E,height:y})}return t},Bn=r=>{const t={};return typeof r.resourceId=="string"&&r.resourceId.length>0&&(t.resourceId=r.resourceId),typeof r.contentDesc=="string"&&r.contentDesc.length>0&&(t.contentDesc=r.contentDesc),typeof r.text=="string"&&r.text.length>0&&(t.text=r.text),typeof r.className=="string"&&r.className.length>0&&(t.className=r.className),t};function Fr(r){const t=Xn(r),s=[];for(let a=0;a<r.length&&s.length<It;a++){const g=r[a],E=g.parsedBounds;if(!E)continue;const y=Math.max(0,E.right-E.left),O=Math.max(0,E.bottom-E.top);if(y<=0||O<=0)continue;const b={x:E.left,y:E.top,width:y,height:O};if(Lr(b,{x:0,y:0,width:t.width,height:t.height}))continue;const D=g.contentDesc||g.text||"",N=g.className||"";s.push({id:g.resourceId&&g.resourceId.length>0?g.resourceId:g.contentDesc&&g.contentDesc.length>0?`cd:${g.contentDesc}`:String(a),path:String(a),label:D,value:typeof g.text=="string"?g.text:"",role:N,type:N,enabled:g.enabled!==!1,focused:g.focused===!0,frame:b,selectors:Bn(g),raw:{...g}})}return{platform:"android",screen:{width:t.width,height:t.height},elements:s,capturedAt:Date.now()}}function Wr(r,t){const s=Math.max(0,r.x),a=Math.max(0,r.y),g=Math.min(t.width,r.x+r.width),E=Math.min(t.height,r.y+r.height),y=Math.max(0,g-s),O=Math.max(0,E-a);return y>0&&O>0?{x:s,y:a,width:y,height:O}:null}function Jt(r,t){if(r===t)return!0;if(r.id!==t.id||r.path!==t.path||r.label!==t.label||r.value!==t.value||r.role!==t.role||r.type!==t.type||r.enabled!==t.enabled||r.focused!==t.focused)return!1;const s=r.frame,a=t.frame;return s.x===a.x&&s.y===a.y&&s.width===a.width&&s.height===a.height}function zt(r,t){if(r===t)return!0;if(!r||!t||r.platform!==t.platform||r.screen.width!==t.screen.width||r.screen.height!==t.screen.height||r.elements.length!==t.elements.length)return!1;for(let s=0;s<r.elements.length;s++)if(!Jt(r.elements[s],t.elements[s]))return!1;return!0}function Hr(r,t,s){let a=null,g=1/0;for(const E of r.elements){const y=E.frame;if(t<y.x||s<y.y||t>y.x+y.width||s>y.y+y.height)continue;const O=Fn(y);O<g&&(g=O,a=E)}return a}function $r(r,t){if(t==="ios"){if(r.selectors.AXUniqueId)return`client.tapElement({ AXUniqueId: ${JSON.stringify(r.selectors.AXUniqueId)} })`;if(r.selectors.AXLabel){const s=r.selectors.className?`, type: ${JSON.stringify(r.selectors.className)}`:"";return`client.tapElement({ AXLabel: ${JSON.stringify(r.selectors.AXLabel)}${s} })`}return null}return r.selectors.resourceId?`client.tap({ selector: { resourceId: ${JSON.stringify(r.selectors.resourceId)} } })`:r.selectors.contentDesc?`client.tap({ selector: { contentDesc: ${JSON.stringify(r.selectors.contentDesc)} } })`:r.selectors.text?`client.tap({ selector: { text: ${JSON.stringify(r.selectors.text)} } })`:null}function Xr(r){const t=r.role||r.type||"";if(!t)return"element";let s=t.replace(/^AX/,"");return s.includes(".")&&(s=s.split(".").pop()),s==="GenericElement"?"Element":(s=s.replace(/([a-z])([A-Z])/g,"$1 $2"),s)}const Nr=80;function Qt(r){const t=Xr(r),s=r.label||r.value||"";if(!s)return t;const a=s.length>Nr?s.slice(0,Nr).trimEnd()+"…":s;return`${t} · ${a}`}const qn=500,Vn=2e3,Ir=5e3,jn=8e3,Gn=1200,Jn=250,zn=()=>`ax-rc-${Date.now()}-${Math.random().toString(36).slice(2,8)}`;class Qn{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??qn,this.maxBackoffMs=t.maxBackoffMs??Vn,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,s){if(this.status!==t&&(this.status=t,!!this.onStatusChange))try{this.onStatusChange(t,s)}catch(a){console.error("[AxFetcher] onStatusChange threw:",a)}}bumpActivity(){if(!this.running)return;const t=Date.now();this.boostUntilMs=t+Gn;const s=Math.max(150,Math.floor(this.baseIntervalMs/2));if(t-this.lastBumpAtMs<s){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 s=t.id;if(!this.pending.get(s))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(s,new Error(O)),!0;const b=typeof t.json=="string"?t.json:"";try{const D=JSON.parse(b),N=xr(D);this.settleResolve(s,N)}catch(D){this.settleReject(s,D instanceof Error?D:new Error(String(D)))}return!0}if(t.type!=="getElementTreeResult")return!1;const g=t.error;if(g&&typeof g=="object"){const O=typeof g.message=="string"?g.message:"getElementTree failed";return this.settleReject(s,new Error(O)),!0}const E=t.payload,y=Array.isArray(E?.nodes)?E.nodes:[];try{const O=Fr(y);this.settleResolve(s,O)}catch(O){this.settleReject(s,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=zn();return new Promise((s,a)=>{const g=window.setTimeout(()=>{this.pending.delete(t),a(new Error("elementTree request timed out"))},jn+(this.androidElementTreeOptions?.waitForIdleTimeoutMs??0));this.pending.set(t,{resolve:s,reject:a,timer:g}),this.send(this.buildRequest(t))||(window.clearTimeout(g),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 s=t instanceof Error?t.message:String(t),a=/unavailable|not (yet )?running|failed|timeout/i.test(s);this.currentInterval=a?Ir:Math.min(this.currentInterval*2,this.maxBackoffMs),this.setStatus(a?"unavailable":"error",s)}finally{this.inflight=!1,this.scheduleNext()}}}deliver(t){const s=this.lastSnapshot;if(zt(s,t)){this.currentInterval=Math.min(this.currentInterval*2,this.maxBackoffMs),this.lastSnapshot=t,t.errors?.includes(Nt)||this.setStatus("ready");return}this.lastSnapshot=t,this.currentInterval=this.baseIntervalMs,t.errors?.includes(Nt)?(this.currentInterval=Ir,this.setStatus("unavailable",Nt)):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(Jn,this.currentInterval):this.currentInterval;this.timer=window.setTimeout(()=>{this.timer=void 0,this.runOnce()},t)}settleResolve(t,s){const a=this.pending.get(t);a&&(window.clearTimeout(a.timer),this.pending.delete(t),a.resolve(s))}settleReject(t,s){const a=this.pending.get(t);a&&(window.clearTimeout(a.timer),this.pending.delete(t),a.reject(s))}failAllPending(t){for(const[s,a]of this.pending.entries())window.clearTimeout(a.timer),a.reject(new Error(t)),this.pending.delete(s)}}class Zn{generation=0;tail=Promise.resolve();get currentGeneration(){return this.generation}enqueue(t){const s=++this.generation;return this.append(s,t)}enqueueCurrent(t){return this.append(this.generation,t)}append(t,s){const a={generation:t,isCurrent:()=>t===this.generation},g=this.tail.then(()=>s(a));return this.tail=g.catch(()=>{}),g}invalidate(){++this.generation}}function ei(r){if(!r||typeof r!="object")return null;const t=r;if(t.type!=="cameraRequest"||typeof t.active!="boolean")return null;const s={active:t.active};return(t.facingMode==="user"||t.facingMode==="environment")&&(s.facingMode=t.facingMode),s}function ti(r){const t={frameRate:{ideal:30,max:30}};return r?.facingMode&&(t.facingMode={ideal:r.facingMode}),t}function ri(r,t){return!r||r.facingMode!==t.facingMode}function ni(r){const t=r.getSettings();return{width:t.width,height:t.height,frameRate:t.frameRate,deviceId:t.deviceId,label:r.label||void 0,facingMode:t.facingMode}}function ii(r){return{type:"cameraResult",granted:!0,camera:ni(r)}}const oi=async r=>{if(!r)return!1;try{if(typeof navigator<"u"&&navigator.clipboard?.writeText)return await navigator.clipboard.writeText(r),!0}catch{}try{const t=document.createElement("textarea");t.value=r,t.style.position="fixed",t.style.opacity="0",document.body.appendChild(t),t.select();const s=document.execCommand("copy");return document.body.removeChild(t),s}catch{return!1}},si=l.memo(function({element:t,screen:s,highlighted:a,selected:g,selectable:E,onClick:y}){const O=Wr(t.frame,s);if(!O)return null;const b=Qt(t);return w.jsx("button",{type:"button",className:Se("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":g?"true":"false","aria-label":b,onClick:D=>{E&&(D.preventDefault(),D.stopPropagation(),y(t,{x:D.clientX,y:D.clientY}))},style:{left:`${O.x/s.width*100}%`,top:`${O.y/s.height*100}%`,width:`${O.width/s.width*100}%`,height:`${O.height/s.height*100}%`}})},(r,t)=>r.highlighted===t.highlighted&&r.selected===t.selected&&r.selectable===t.selectable&&r.screen.width===t.screen.width&&r.screen.height===t.screen.height&&r.onClick===t.onClick&&Jt(r.element,t.element)),ci=260,ai=220,ui=6,li=8,di=l.memo(function({element:t,platform:s,anchor:a,cursorAnchored:g,showActions:E,onTap:y}){const[O,b]=l.useState(null);l.useEffect(()=>{if(!O)return;const X=window.setTimeout(()=>b(null),1100);return()=>window.clearTimeout(X)},[O]);const D=l.useMemo(()=>{const X=ci,ue=ai,re=ui,H=li,le=typeof window<"u"?window.innerWidth:1024,dt=typeof window<"u"?window.innerHeight:768,Me=a.x+re+X<=le-H,de=a.y-re-ue>=H;let F=Me?a.x+re:a.x-re,be=de?a.y-re:a.y+re;const ie=`translate(${Me?"0":"-100%"}, ${de?"-100%":"0"})`;return F=Math.max(H+(Me?0:X),Math.min(le-H-(Me?X:0),F)),be=Math.max(H+(de?ue:0),Math.min(dt-H-(de?0:ue),be)),{left:`${F}px`,top:`${be}px`,transform:ie}},[a.x,a.y]),N=l.useMemo(()=>$r(t,s),[t,s]),A=s==="ios"?t.selectors.AXUniqueId:t.selectors.resourceId,Q=s==="ios"?"AXUniqueId":"resourceId",me=l.useCallback(async(X,ue)=>{if(!X)return;await oi(X)&&b(ue)},[]),pe=Xr(t),Ge=t.label||t.value||pe,Ae=Qt(t);return An.createPortal(w.jsxs("div",{className:"rc-inspect-card","data-cursor-anchored":g?"true":"false",style:D,onClick:X=>X.stopPropagation(),onMouseDown:X=>X.stopPropagation(),onMouseMove:X=>X.stopPropagation(),children:[w.jsxs("div",{className:"rc-inspect-card-header",children:[w.jsx("span",{className:"rc-inspect-card-role",children:pe}),!t.enabled&&w.jsx("span",{className:"rc-inspect-card-tag",children:"disabled"}),t.focused&&w.jsx("span",{className:"rc-inspect-card-tag rc-inspect-card-tag-blue",children:"focused"})]}),w.jsx("p",{className:"rc-inspect-card-title",title:Ae,children:Ge}),A&&w.jsxs("div",{className:"rc-inspect-card-row",children:[w.jsxs("span",{className:"rc-inspect-card-row-label",children:[Q,":"]}),w.jsx("span",{className:"rc-inspect-card-row-value",children:A})]}),t.value&&t.value!==t.label&&w.jsxs("div",{className:"rc-inspect-card-row",children:[w.jsx("span",{className:"rc-inspect-card-row-label",children:"value:"}),w.jsx("span",{className:"rc-inspect-card-row-value",children:t.value})]}),w.jsxs("div",{className:"rc-inspect-card-row",children:[w.jsx("span",{className:"rc-inspect-card-row-label",children:"frame:"}),w.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)]})]}),E&&w.jsxs("div",{className:"rc-inspect-card-actions",children:[w.jsx("button",{type:"button",className:"rc-inspect-card-btn rc-inspect-card-btn-primary",onClick:()=>y(t,a),children:"Tap"}),w.jsx("button",{type:"button",className:Se("rc-inspect-card-btn",O==="selector"&&"rc-inspect-card-btn-copied"),disabled:!N,title:N??"No usable selector for this element",onClick:()=>me(N??void 0,"selector"),children:O==="selector"?"Copied!":"Copy selector"}),w.jsx("button",{type:"button",className:Se("rc-inspect-card-btn",O==="id"&&"rc-inspect-card-btn-copied"),disabled:!A,title:A??`No ${Q}`,onClick:()=>me(A,"id"),children:O==="id"?"Copied!":`Copy ${Q}`})]})]}),document.body)}),fi=l.memo(function({snapshot:t,geometry:s,highlightedId:a,selectedId:g,mode:E,cursorPosition:y,frozenCursorPosition:O,onSelectChange:b,onTapElement:D}){const N=E==="select",A=N?a!==null&&a!==g:a!==null,Q=N?a??g:a,me=l.useMemo(()=>{const H=new Map;if(!t)return H;for(const le of t.elements)H.set(le.id,le);return H},[t]),pe=l.useMemo(()=>Q?me.get(Q)??null:null,[Q,me]);if(!(!!t&&!!s&&t.screen.width>0&&t.screen.height>0&&t.elements.length>0))return null;const Ae=A?y:O??y,X=A,ue=N&&!A&&g!==null,re=H=>{N&&H.target===H.currentTarget&&b(null,null)};return w.jsxs(w.Fragment,{children:[w.jsx("div",{className:Se("rc-inspect-overlay",N&&"rc-inspect-overlay-select"),style:{left:`${s.left}px`,top:`${s.top}px`,width:`${s.width}px`,height:`${s.height}px`},onClick:re,children:t.elements.map(H=>w.jsx(si,{element:H,screen:t.screen,highlighted:H.id===a,selected:H.id===g,selectable:N,onClick:b},H.id))}),pe&&Ae&&w.jsx(di,{element:pe,platform:t.platform,anchor:Ae,cursorAnchored:X,showActions:ue,onTap:D})]})}),hi=l.lazy(()=>Promise.resolve().then(()=>require("./duo-view-COZg8ieG.js")).then(r=>r.duoView)),x=(...r)=>{window.debugRemoteControl&&console.log(...r)},R=(...r)=>{window.debugRemoteControl&&console.warn(...r)},L=(r,t,...s)=>{if(t)try{t(...s)}catch(a){console.error(`[RemoteControl] customer callback "${r}" threw:`,a)}},bt=r=>Object.entries(U).find(([s,a])=>s.startsWith("ACTION_")&&a===r)?.[0]??String(r),mi=r=>r.includes("/android_")?"android":"ios",Yr=(r,t)=>{const s=new URL(r);return s.searchParams.set("token",t),s.toString()},Kr=1920,kr=1200,Gt=3,pi=1e3,Ei=15e3,gi=3e3,Ci=4e3,yi=(r,t)=>r===Kr&&t===kr||r===kr&&t===Kr,vi={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 Oi(r){const t=r.code,s=Ur[t];if(!s)return R(`Unknown event.code: ${t}, key: ${r.key}`),null;let a=c.META_NONE;const g=t>="KeyA"&&t<="KeyZ",E=r.getModifierState("CapsLock"),y=r.shiftKey;let O=y;return g&&(O=y!==E),O&&(a|=c.META_SHIFT_ON),r.ctrlKey&&(a|=c.META_CTRL_ON),r.altKey&&(a|=c.META_ALT_ON),r.metaKey&&(a|=c.META_META_ON),{keycode:s,metaState:a}}const _i=l.forwardRef(({className:r,url:t,token:s,sessionId:a,openUrl:g,showFrame:E=!0,deviceModel:y,duoModelUrl:O,autoReconnect:b=!1,onTerminated:D,assets:N,onConnectionStateChange:A,inspectMode:Q,onAxSnapshotChange:me,onInspectSelectionChange:pe,onAxStatusChange:Ge,axPollIntervalMs:Ae,axMaxBackoffMs:X,androidElementTreeOptions:ue,onCameraDemandChange:re,onCameraStats:H,microphoneEnabled:le=!1,onMicrophoneStateChange:dt},Me)=>{const de=l.useRef(null),F=l.useRef(null),be=l.useRef(null),[Zt,er]=l.useState(!1),ie=y==="iphone-duo"||Zt,Je=l.useRef(null),[tr,rr]=l.useState(null),[nr,ir]=l.useState(),Ne=l.useRef(new Map),Kt=(e,o)=>new Promise((i,n)=>{const d=T.current;if(!d||d.readyState!==WebSocket.OPEN){n(new Error("Simulator disconnected"));return}const u=crypto.randomUUID(),h=setTimeout(()=>{Ne.current.delete(u),n(new Error("Device control timed out"))},5e3);Ne.current.set(u,{resolve:i,reject:n,timer:h}),d.send(JSON.stringify({type:e,id:u,...o}))}),[Br,kt]=l.useState(!1),[qr,ft]=l.useState(!1),[Vr,or]=l.useState(!1),Ie=l.useRef(!1),Pt=l.useRef(!1),[sr,jr]=l.useState(!1),[Gr,Jr]=l.useState(!1),[zr,cr]=l.useState({}),T=l.useRef(null),W=l.useRef(null),P=l.useRef(null),Ye=l.useRef(void 0),ze=l.useRef(void 0),Qe=l.useRef(void 0),ht=l.useRef(void 0),Ke=l.useRef(void 0),Ce=l.useRef(0),mt=l.useRef(0),ke=l.useRef(!1),Ut=l.useRef(b);Ut.current=b;const ye=l.useRef(null),Z=l.useRef(null),ve=l.useRef(void 0),Lt=l.useMemo(()=>new Zn,[]),fe=l.useRef(re);fe.current=re;const pt=l.useRef(H);pt.current=H;const Ze=l.useRef(null),Et=l.useRef(null),xt=l.useRef(le);xt.current=le;const Ft=l.useRef(!1),et=l.useRef(dt);et.current=dt;const tt=l.useRef(null),ce=l.useRef(null),gt=l.useRef(0),ar=l.useRef(Promise.resolve()),Pe=l.useRef(null),rt=l.useRef(!1),Ee=l.useRef(new Map),he=l.useRef(new Map),Oe=l.useRef(new Map),_e=l.useRef(new Map),j=l.useRef(new Map),De=l.useRef(!1),[Qr,Zr]=l.useState(!1),B=l.useRef(null),Ue=l.useRef(null),nt=l.useRef({handle:()=>{},end:()=>{}}),[Te,Ct]=l.useState(null),G=l.useRef(null),[ur,lr]=l.useState(null),[Wt,it]=l.useState(null),[Ht,yt]=l.useState(null),[en,dr]=l.useState(null),[tn,fr]=l.useState(null),vt=l.useRef(null),ne=l.useRef(void 0),hr=e=>{vt.current=e,ne.current===void 0&&(ne.current=window.requestAnimationFrame(()=>{ne.current=void 0,fr(vt.current)}))},[rn,Le]=l.useState(null),xe=l.useRef(Q);xe.current=Q;const Fe=l.useRef(null);Fe.current=ur;const $t=l.useRef(me);$t.current=me;const We=l.useRef(pe);We.current=pe;const mr=l.useRef(Ge);mr.current=Ge;const pr=l.useRef(D);pr.current=D;const Er=l.useRef(A);Er.current=A;const Xt=l.useRef(null),He=e=>{Xt.current!==e&&(Xt.current=e,L("onConnectionStateChange",Er.current,e))},ot=Q===!0||Q==="hover-only",nn=Q==="hover-only"?"hover-only":"select",oe=l.useMemo(()=>a||Math.random().toString(36).substring(2,15)+Math.random().toString(36).substring(2,15),[a]),ae=l.useMemo(()=>mi(t),[t]),$e=vi[ae],st=N?.[ae],gr=ae==="android"&&Gr?sr?st?.tabletFrameLandscape:st?.tabletFrame:sr?st?.frameLandscape:st?.frame,Cr=st?.loadingLogo,Ot=!ie&&E&&!!gr,K=e=>{x(e)},se=e=>{!P.current||P.current.readyState!=="open"||(P.current.send(e),G.current?.bumpActivity())},yr=-10,vr=(e,o)=>{const i=ct();if(!i)return;const n=Be(i,e,o);if(!n)return;const{videoX:d,videoY:u,videoWidth:h,videoHeight:p}=n,C=Mt(U.ACTION_DOWN,yr,h,p,d,u,1,U.BUTTON_PRIMARY,U.BUTTON_PRIMARY);C&&se(C),window.setTimeout(()=>{const m=Mt(U.ACTION_UP,yr,h,p,d,u,0,U.BUTTON_PRIMARY,U.BUTTON_PRIMARY);m&&se(m)},60)},on=(e,o)=>{const i=ct();if(!i||o.screen.width<=0||o.screen.height<=0)return;const n=e.frame.x+e.frame.width/2,d=e.frame.y+e.frame.height/2,u=n/o.screen.width*i.actualWidth,h=d/o.screen.height*i.actualHeight,p=i.videoRect.left+i.offsetX+u,C=i.videoRect.top+i.offsetY+h;vr(p,C)},_t=-1,Dt=-2,Re=-3,Xe=(e,o,i,n,d,u)=>{const h=Mt(e,o,i,n,d,u,1,U.BUTTON_PRIMARY,U.BUTTON_PRIMARY);h&&(x("[rc-touch] sendSingleTouch",{action:e,actionName:bt(e),pointerId:o,x:d,y:u,video:{width:i,height:n}}),se(h))},we=(e,o,i)=>{if(!i)return;const{videoX:n,videoY:d,videoWidth:u,videoHeight:h}=i;let p=null,C=null,m=1;const M=U.BUTTON_PRIMARY;switch(o){case"down":e!==Re&&nt.current.end(),p=j.current.size===0?U.ACTION_DOWN:U.ACTION_POINTER_DOWN,C={x:n,y:d},j.current.set(e,C),e===-1&&F.current?.focus();break;case"move":j.current.has(e)&&(p=U.ACTION_MOVE,C={x:n,y:d},j.current.set(e,C));break;case"up":case"cancel":j.current.has(e)&&(C=j.current.get(e),j.current.delete(e),o==="cancel"?p=U.ACTION_CANCEL:p=j.current.size===0?U.ACTION_UP:U.ACTION_POINTER_UP);break}if(p!==null&&C!==null){x("[rc-touch][mouse->touch] sending",{pointerId:e,eventType:o,action:p,actionName:bt(p),positionToSend:C,video:{width:u,height:h},altHeld:De.current,activePointersAfter:Array.from(j.current.entries()).map(([I,$])=>({id:I,x:$.x,y:$.y}))});const _=Mt(p,e,u,h,C.x,C.y,m,M,M);_&&(x("[rc-touch][mouse->touch] buffer",{pointerId:e,actionName:bt(p),byteLength:_.byteLength}),se(_))}else(o==="up"||o==="cancel")&&j.current.delete(e)},Bt=e=>{if(De.current!==e&&(De.current=e,Zr(e),e||Ct(null),x("[rc-touch][alt] updateAltHeld",{nextHeld:e,activePointerIds:Array.from(j.current.keys())}),ae==="ios"&&P.current&&P.current.readyState==="open")){const o=e?c.ACTION_DOWN:c.ACTION_UP,i=lt(o,c.KEYCODE_ALT_LEFT,0,c.META_NONE);x("[rc-touch][alt] sending Indigo modifier keycode",{action:o,keycode:c.KEYCODE_ALT_LEFT}),i&&se(i)}},ct=()=>{const e=F.current,o=de.current;if(!e||!o)return null;const i=e.videoWidth,n=e.videoHeight;if(!i||!n)return null;const d=e.getBoundingClientRect(),u=o.getBoundingClientRect(),h=d.width,p=d.height,C=i/n,m=h/p;let M=h,_=p;C>m?_=h/C:M=p*C;const I=(h-M)/2,$=(p-_)/2;return{videoWidth:i,videoHeight:n,videoRect:d,containerRect:u,actualWidth:M,actualHeight:_,offsetX:I,offsetY:$}},Be=(e,o,i)=>{const n=o-e.videoRect.left-e.offsetX,d=i-e.videoRect.top-e.offsetY,u=Math.max(0,Math.min(e.actualWidth,n)),h=Math.max(0,Math.min(e.actualHeight,d)),p=Math.max(0,Math.min(e.videoWidth,u/e.actualWidth*e.videoWidth)),C=Math.max(0,Math.min(e.videoHeight,h/e.actualHeight*e.videoHeight));return{videoX:p,videoY:C,videoWidth:e.videoWidth,videoHeight:e.videoHeight}},sn=(e,o,i)=>{const n=o-e.videoRect.left-e.offsetX,d=i-e.videoRect.top-e.offsetY,u=Math.max(0,Math.min(e.actualWidth,n)),h=Math.max(0,Math.min(e.actualHeight,d)),p=Math.max(0,Math.min(e.videoWidth,u/e.actualWidth*e.videoWidth)),C=Math.max(0,Math.min(e.videoHeight,h/e.actualHeight*e.videoHeight)),m=e.videoWidth-p,M=e.videoHeight-C,_=e.videoRect.left+e.offsetX,I=e.videoRect.top+e.offsetY,$=_-e.containerRect.left+u,ee=I-e.containerRect.top+h,V=_-e.containerRect.left+(e.actualWidth-u),v=I-e.containerRect.top+(e.actualHeight-h);return{containerX:$,containerY:ee,mirrorContainerX:V,mirrorContainerY:v,videoX:p,videoY:C,mirrorVideoX:m,mirrorVideoY:M,videoWidth:e.videoWidth,videoHeight:e.videoHeight}},cn=(e,o,i,n,d,u,h)=>{const p=Ln(e,o,i,n,d,u,h);x("[rc-touch2] sendTwoFingerMessage (iOS)",{actionName:bt(e),video:{width:o,height:i},p0:{x:n,y:d},p1:{x:u,y:h},byteLength:p.byteLength}),se(p)},qe=(e,o,i,n,d,u,h,p,C)=>{if(x("[rc-touch2] applyTwoFingerEvent",{platform:ae,eventType:e,video:{width:o,height:i},p0:{x:n,y:d,id:p},p1:{x:u,y:h,id:C}}),e==="down"&&nt.current.end(),ae==="ios"){const m=e==="down"?U.ACTION_DOWN:e==="move"?U.ACTION_MOVE:U.ACTION_UP;cn(m,o,i,n,d,u,h)}else e==="down"?(Xe(U.ACTION_DOWN,p,o,i,n,d),Xe(U.ACTION_POINTER_DOWN,C,o,i,u,h)):e==="move"?(Xe(U.ACTION_MOVE,p,o,i,n,d),Xe(U.ACTION_MOVE,C,o,i,u,h)):(Xe(U.ACTION_POINTER_UP,C,o,i,u,h),Xe(U.ACTION_UP,p,o,i,n,d))},an=(e,o,i)=>{if(!De.current){Te!==null&&Ct(null);return}const n=sn(e,o,i);Ct(n)},un=(e,o,i)=>{const n=Fe.current;if(!n||n.screen.width<=0||n.screen.height<=0)return null;const d=o-e.videoRect.left-e.offsetX,u=i-e.videoRect.top-e.offsetY;if(d<0||u<0||d>e.actualWidth||u>e.actualHeight)return null;const h=d/e.actualWidth*n.screen.width,p=u/e.actualHeight*n.screen.height;return Hr(n,h,p)},ge=e=>{if(ie)return;e.preventDefault(),e.stopPropagation();const o=ct();if((xe.current===!0||xe.current==="hover-only")&&!("touches"in e))if(e.type==="mousemove"){if(hr({x:e.clientX,y:e.clientY}),o){const n=un(o,e.clientX,e.clientY);it(n?.id??null)}}else e.type==="mouseleave"&&(hr(null),it(null));if(xe.current!==!0&&(!("touches"in e)&&o&&(e.type==="mousemove"?an(o,e.clientX,e.clientY):e.type==="mouseleave"&&Ct(null)),!(!P.current||P.current.readyState!=="open"||!F.current||!o)))if("touches"in e){const n=e.touches,d=e.changedTouches;let u;switch(e.type){case"touchstart":u="down";break;case"touchmove":u="move";break;case"touchend":u="up";break;case"touchcancel":u="cancel";break;default:return}if(n.length===2){const h=n[0],p=n[1],C=Be(o,h.clientX,h.clientY),m=Be(o,p.clientX,p.clientY);if(!C||!m)return;B.current?B.current.source==="real-touch"&&(B.current.finger0={x:C.videoX,y:C.videoY},B.current.finger1={x:m.videoX,y:m.videoY},qe("move",C.videoWidth,C.videoHeight,C.videoX,C.videoY,m.videoX,m.videoY,B.current.pointerId0,B.current.pointerId1)):(B.current={finger0:{x:C.videoX,y:C.videoY},finger1:{x:m.videoX,y:m.videoY},videoSize:{width:C.videoWidth,height:C.videoHeight},source:"real-touch",pointerId0:h.identifier,pointerId1:p.identifier},qe("down",C.videoWidth,C.videoHeight,C.videoX,C.videoY,m.videoX,m.videoY,h.identifier,p.identifier))}else if(n.length<2&&B.current?.source==="real-touch"){const h=B.current;qe("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(n.length>2)return;{const h=d[0];if(h){const p=Be(o,h.clientX,h.clientY);we(h.identifier,u,p)}}}}else{let d=null;const u=B.current?.source==="alt-mouse";switch(e.type){case"mousedown":e.button===0&&(d="down");break;case"mousemove":(u||j.current.has(-1))&&(d="move");break;case"mouseup":e.button===0&&(d="up");break;case"mouseleave":(u||j.current.has(-1))&&(d="up");break}if(d){const h=Be(o,e.clientX,e.clientY);if(!h)return;x("[rc-touch][mouse] event",{domType:e.type,eventType:d,button:e.button,buttons:e.buttons,client:{x:e.clientX,y:e.clientY},altHeldRef:De.current,inTwoFingerMode:u,geometry:{videoX:h.videoX,videoY:h.videoY,videoWidth:h.videoWidth,videoHeight:h.videoHeight},activePointerIds:Array.from(j.current.keys())}),De.current||u?ln(d,h):we(-1,d,h)}}},ln=(e,o)=>{const{videoX:i,videoY:n,videoWidth:d,videoHeight:u}=o,h=d-i,p=u-n;if(e==="down"){B.current={finger0:{x:i,y:n},finger1:{x:h,y:p},videoSize:{width:d,height:u},source:"alt-mouse",pointerId0:_t,pointerId1:Dt},F.current?.focus(),qe("down",d,u,i,n,h,p,_t,Dt);return}if(e==="move"){B.current?.source==="alt-mouse"&&(B.current.finger0={x:i,y:n},B.current.finger1={x:h,y:p},qe("move",d,u,i,n,h,p,_t,Dt));return}if(e==="up"||e==="cancel"){const C=B.current;if(C?.source==="alt-mouse"){const{finger0:m,finger1:M,videoSize:_}=C;qe("up",_.width,_.height,m.x,m.y,M.x,M.y,_t,Dt),B.current=null}return}},dn=150,Or=()=>{const e=Ue.current;e&&(window.clearTimeout(e.endTimer),we(Re,"up",e.pos),Ue.current=null)},fn=e=>{if(e.ctrlKey||De.current||B.current)return;const o=j.current.size;if(o>1||o===1&&!j.current.has(Re))return;const i=ct();if(!i)return;const n=e.clientX-i.videoRect.left-i.offsetX,d=e.clientY-i.videoRect.top-i.offsetY;if(n<0||n>i.actualWidth||d<0||d>i.actualHeight)return;const u=Be(i,e.clientX,e.clientY);if(!u)return;e.preventDefault();const h=(e.deltaMode===1?16:e.deltaMode===2?i.actualWidth:1)*(i.videoWidth/i.actualWidth),p=(e.deltaMode===1?16:e.deltaMode===2?i.actualHeight:1)*(i.videoHeight/i.actualHeight),C=-e.deltaX*h,m=-e.deltaY*p,M=v=>{window.clearTimeout(v.endTimer),v.endTimer=window.setTimeout(Or,dn)};if(C===0&&m===0){Ue.current&&M(Ue.current);return}const _=()=>({videoWidth:u.videoWidth,videoHeight:u.videoHeight,videoX:Math.abs(C)>Math.abs(m)?Math.min(Math.max(u.videoX,u.videoWidth*.2),u.videoWidth*.8):u.videoX,videoY:Math.abs(m)>=Math.abs(C)?Math.min(Math.max(u.videoY,u.videoHeight*.2),u.videoHeight*.8):u.videoY}),I=v=>({...v,videoX:Math.min(Math.max(v.videoX+C,0),v.videoWidth),videoY:Math.min(Math.max(v.videoY+m,0),v.videoHeight)});let $=Ue.current;if($)window.clearTimeout($.endTimer);else{const v=_();we(Re,"down",v),$={pos:v,endTimer:0},Ue.current=$}let ee=$.pos,V=I(ee);V.videoX===ee.videoX&&V.videoY===ee.videoY&&(we(Re,"up",ee),ee=_(),we(Re,"down",ee),V=I(ee)),we(Re,"move",V),$.pos=V,M($)};l.useEffect(()=>{nt.current={handle:fn,end:Or}}),l.useEffect(()=>{const e=de.current;if(!e)return;const o=i=>nt.current.handle(i);return e.addEventListener("wheel",o,{passive:!1}),()=>{nt.current.end(),e.removeEventListener("wheel",o)}},[]),l.useEffect(()=>{const e=n=>{n.key==="Alt"&&Bt(!0)},o=n=>{n.key==="Alt"&&Bt(!1)},i=()=>{Bt(!1)};return window.addEventListener("keydown",e,!0),window.addEventListener("keyup",o,!0),window.addEventListener("blur",i),()=>{window.removeEventListener("keydown",e,!0),window.removeEventListener("keyup",o,!0),window.removeEventListener("blur",i)}},[]);const Tt=e=>{e.preventDefault(),e.stopPropagation();const o=document.activeElement===F.current||ie&&document.activeElement===e.currentTarget;if(x("Keyboard event:",{type:e.type,key:e.key,keyCode:e.keyCode,code:e.code,target:e.target.tagName,focused:o}),!o){R("Simulator input not focused, skipping keyboard event");return}if(!P.current||P.current.readyState!=="open"){R("Data channel not ready for keyboard event:",P.current?.readyState);return}if(e.type==="keydown"){if(e.key.toLowerCase()==="v"&&(e.metaKey||e.ctrlKey)){x("Paste shortcut detected"),navigator.clipboard.readText().then(n=>{if(n){x("Pasting text via SET_CLIPBOARD:",n.substring(0,20)+(n.length>20?"...":""));const d=xn(n,!0);se(d)}}).catch(n=>{console.error("Failed to read clipboard contents: ",n)});return}if(e.key.toLowerCase()==="m"&&(e.metaKey||e.ctrlKey)){x("Menu shortcut detected");const n=lt(c.ACTION_DOWN,c.MENU,0,c.META_NONE);se(n);const d=lt(c.ACTION_UP,c.MENU,0,c.META_NONE);se(d);return}}const i=Oi(e);if(i){const{keycode:n,metaState:d}=i,u=e.type==="keydown"?c.ACTION_DOWN:c.ACTION_UP;x(`Sending Keycode: key=${e.key}, code=${n}, action=${u}, meta=${d}`);const h=lt(u,n,0,d);se(h)}else x(`Ignoring unhandled key event: type=${e.type}, key=${e.key}`)},hn=()=>{T.current&&T.current.readyState===WebSocket.OPEN&&T.current.send(JSON.stringify({type:"keepAlive",sessionId:oe}))},_r=()=>{Ye.current&&window.clearInterval(Ye.current),Ye.current=window.setInterval(hn,1e4)},Dr=()=>{Ye.current&&(window.clearInterval(Ye.current),Ye.current=void 0)},Rt=()=>{ze.current&&(window.clearTimeout(ze.current),ze.current=void 0)},wt=()=>{Qe.current&&(window.clearTimeout(Qe.current),Qe.current=void 0)},at=()=>{ht.current&&(window.clearInterval(ht.current),ht.current=void 0)},ut=()=>{Ke.current!==void 0&&(window.clearTimeout(Ke.current),Ke.current=void 0)},mn=()=>{rt.current||(rt.current=!0,at(),kt(!0),He("connected"))},J=e=>{for(const o of e.getTracks())try{o.stop()}catch(i){R("track.stop() failed:",i)}},pn=()=>{const e=Z.current;e&&(Z.current=null,J(e))},Tr=e=>{const o=ar.current.then(e).catch(i=>{R("microphone operation failed:",i)});return ar.current=o,o},qt=()=>xt.current||Ft.current,Rr=e=>{const o=`ui-mic-${Date.now()}-${Math.random().toString(36).substring(2,9)}`;Pe.current=o,e.send(JSON.stringify({type:"microphoneStart",id:o}))},Vt=()=>Tr(()=>En()),En=async()=>{const e=gt.current,o=()=>e===gt.current&&qt(),i=p=>{L("onMicrophoneStateChange",et.current,{active:!1,error:p})};if(!o())return;const n=T.current,d=tt.current;if(!n||n.readyState!==WebSocket.OPEN||!d||ce.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)."),i("Microphone capture requires a secure context (https)");return}let u=null;try{u=await navigator.mediaDevices.getUserMedia({audio:!0,video:!1})}catch(p){console.warn("[RemoteControl] microphone getUserMedia denied/failed:",p)}if(!o()){u&&J(u);return}if(!u){i("Microphone permission denied or no device available");return}const h=u.getAudioTracks()[0]??null;if(!h){J(u),i("Captured stream has no audio track");return}try{await d.replaceTrack(h)}catch(p){R("replaceTrack(audioTrack) failed:",p),J(u),i("Failed to attach the microphone track to the connection");return}if(!o()){try{await d.replaceTrack(null)}catch(p){R("replaceTrack(null) on stale mic attach failed:",p)}J(u);return}ce.current=u,h.onended=()=>{ce.current===u&&St("microphone capture ended by the browser")},Rr(n),L("onMicrophoneStateChange",et.current,{active:!0,label:h.label||void 0})},St=e=>(++gt.current,Pe.current=null,Tr(async()=>{const o=tt.current,i=ce.current;if(ce.current=null,o&&i)try{await o.replaceTrack(null)}catch(d){R("replaceTrack(null) on mic detach failed:",d)}i&&J(i);const n=T.current;if(i&&n&&n.readyState===WebSocket.OPEN){const d=`ui-mic-${Date.now()}-${Math.random().toString(36).substring(2,9)}`;n.send(JSON.stringify({type:"microphoneStop",id:d}))}(i||e)&&L("onMicrophoneStateChange",et.current,{active:!1,...e!==void 0&&{error:e}})})),wr=(e,o)=>{let i;try{i=ii(o),console.info(`[RemoteControl] camera capture: ${i.camera?.width}x${i.camera?.height} @ ${i.camera?.frameRate??"?"}fps`+(i.camera?.label?` — ${i.camera.label}`:""))}catch(n){R("getSettings() on outbound camera track failed:",n),i={type:"cameraResult",granted:!0,camera:{label:o.label||void 0}}}e.send(JSON.stringify(i)),L("onCameraDemandChange",fe.current,!0,!0,i.camera)},gn=e=>{if(!e)return;const o=e.indexOf("/"),n=(o>=0?e.slice(o+1):e).toUpperCase();return n==="HEVC"?"H265":n},Cn=async()=>{const e=ye.current,o=pt.current;if(!e||!o)return;let i;try{i=await e.getStats()}catch{return}let n,d,u;if(i.forEach(I=>{I.type==="outbound-rtp"&&I.kind==="video"?n=I:I.type==="remote-inbound-rtp"&&I.kind==="video"&&(u=I)}),n?.codecId){const I=i.get(n.codecId);I&&(d=I.mimeType)}if(!n)return;const h=n.timestamp??Date.now(),p=Et.current;let C,m,M;if(p&&h>p.timestamp){const I=(h-p.timestamp)/1e3;if(typeof n.framesEncoded=="number"&&typeof p.framesEncoded=="number"&&(C=Math.max(0,(n.framesEncoded-p.framesEncoded)/I)),typeof n.bytesSent=="number"&&typeof p.bytesSent=="number"&&(m=Math.max(0,(n.bytesSent-p.bytesSent)*8/I)),typeof n.packetsSent=="number"&&typeof p.packetsSent=="number"&&u&&typeof u.packetsLost=="number"&&typeof p.packetsLost=="number"){const $=n.packetsSent-p.packetsSent,ee=u.packetsLost-p.packetsLost;$>0&&(M=Math.max(0,Math.min(100,ee/$*100)))}}Et.current={timestamp:h,framesEncoded:n.framesEncoded,bytesSent:n.bytesSent,packetsSent:n.packetsSent,packetsLost:u?.packetsLost};const _={codec:gn(d),encoderImplementation:n.encoderImplementation,hardwareAccelerated:typeof n.powerEfficientEncoder=="boolean"?n.powerEfficientEncoder:void 0,framesPerSecond:C,framesEncoded:n.framesEncoded,bitrateBps:m,width:n.frameWidth,height:n.frameHeight,qualityLimitationReason:n.qualityLimitationReason,rttMs:typeof u?.roundTripTime=="number"?u.roundTripTime*1e3:void 0,packetsLostPct:M};L("onCameraStats",pt.current,_)},Sr=()=>{Ze.current===null&&(Et.current=null,Ze.current=setInterval(()=>{Cn()},1e3))},At=()=>{Ze.current!==null&&(clearInterval(Ze.current),Ze.current=null),Et.current=null,L("onCameraStats",pt.current,null)},yn=(e,o,i,n)=>Lt.enqueue(async({isCurrent:d})=>{const u=()=>d()&&n()&&T.current===o&&ye.current===i;if(!u())return;if(!e.active){const _=Z.current;try{await i.replaceTrack(null)}catch(I){R("replaceTrack(null) on camera detach failed:",I)}if(Z.current===_&&(Z.current=null,ve.current=void 0,_&&J(_)),At(),!u())return;L("onCameraDemandChange",fe.current,!1);return}L("onCameraDemandChange",fe.current,!0);const h=Z.current,p=h?.getVideoTracks()[0],C=ve.current;if(p?.readyState==="live"&&C&&!ri(C,e)){ve.current=e,wr(o,p),Sr();return}if(h){try{await i.replaceTrack(null)}catch(_){R("replaceTrack(null) before camera switch failed:",_)}if(Z.current===h&&(Z.current=null,ve.current=void 0,J(h)),At(),!u())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."),!u())return;o.send(JSON.stringify({type:"cameraResult",granted:!1})),L("onCameraDemandChange",fe.current,!0,!1);return}let m=null;try{m=await navigator.mediaDevices.getUserMedia({video:ti(e),audio:!1})}catch(_){console.warn("[RemoteControl] getUserMedia denied/failed:",_)}if(!u()){m&&J(m);return}if(!m){o.send(JSON.stringify({type:"cameraResult",granted:!1})),L("onCameraDemandChange",fe.current,!0,!1);return}const M=m.getVideoTracks()[0]??null;if(!M){if(J(m),!u())return;o.send(JSON.stringify({type:"cameraResult",granted:!1})),L("onCameraDemandChange",fe.current,!0,!1);return}try{M.contentHint="motion"}catch{}try{await i.replaceTrack(M)}catch(_){if(R("replaceTrack(videoTrack) failed:",_),J(m),!u())return;o.send(JSON.stringify({type:"cameraResult",granted:!1})),L("onCameraDemandChange",fe.current,!0,!1);return}if(!u()){try{await i.replaceTrack(null)}catch(_){R("replaceTrack(null) for stale camera attach failed:",_)}J(m);return}try{const _=i.getParameters();(!_.encodings||_.encodings.length===0)&&(_.encodings=[{}]),_.encodings[0].maxBitrate=8e6,_.encodings[0].maxFramerate=30,_.degradationPreference="maintain-framerate",await i.setParameters(_)}catch(_){R("setParameters on outbound camera failed:",_)}if(!u()){try{await i.replaceTrack(null)}catch(_){R("replaceTrack(null) for stale camera finalize failed:",_)}J(m);return}Z.current=m,ve.current=e,M.onended=()=>{Z.current===m&&Lt.enqueueCurrent(async({isCurrent:_})=>{if(!(!_()||Z.current!==m||!n()||T.current!==o||ye.current!==i)){try{await i.replaceTrack(null)}catch(I){R("replaceTrack(null) after camera ended failed:",I)}!_()||Z.current!==m||!n()||T.current!==o||ye.current!==i||(Z.current=null,ve.current=void 0,J(m),At(),o.send(JSON.stringify({type:"cameraResult",granted:!1})),L("onCameraDemandChange",fe.current,!0,!1))}}).catch(_=>R("camera ended cleanup failed:",_))},u()&&(wr(o,M),Sr())}),Ve=()=>{for(const e of Ne.current.values())clearTimeout(e.timer),e.reject(new Error("Simulator disconnected"));Ne.current.clear(),rr(null),Je.current&&(Je.current.srcObject=null),wt(),ut(),at(),G.current&&(G.current.stop(),G.current=null),Lt.invalidate(),At(),pn(),ve.current=void 0,ye.current=null,++gt.current,Ft.current=!1,Pe.current=null,ce.current&&(J(ce.current),ce.current=null,L("onMicrophoneStateChange",et.current,{active:!1})),tt.current=null,ne.current!==void 0&&(window.cancelAnimationFrame(ne.current),ne.current=void 0),T.current&&(T.current.onopen=null,T.current.onmessage=null,T.current.onerror=null,T.current.onclose=null,T.current.close(),T.current=null),W.current&&(W.current.onconnectionstatechange=null,W.current.oniceconnectionstatechange=null,W.current.ontrack=null,W.current.onicecandidate=null,W.current.close(),W.current=null),F.current&&(F.current.srcObject=null),P.current&&(P.current.onopen=null,P.current.onclose=null,P.current.onerror=null,P.current.close(),P.current=null)},Ar=()=>{document.hidden?Dr():_r()},vn=async()=>{if(typeof navigator<"u"&&navigator.onLine===!1)return!1;let e;try{const n=new URL(t);n.protocol=n.protocol==="wss:"?"https:":n.protocol==="ws:"?"http:":n.protocol,e=Yr(n.toString(),s)}catch{return!1}const o=new AbortController,i=window.setTimeout(()=>o.abort(),Ci);try{return(await fetch(e,{method:"GET",cache:"no-store",signal:o.signal})).status===404}catch{return!1}finally{window.clearTimeout(i)}},On=()=>{Ie.current||(Ie.current=!0,or(!0),ft(!1),Ce.current+=1,Rt(),wt(),ut(),Ve(),K("Instance terminated"),He("terminated"),L("onTerminated",pr.current))},_n=()=>{Ie.current||Pt.current||(Pt.current=!0,vn().then(e=>{Pt.current=!1,e&&On()}))},je=(e,o)=>{if(Ie.current||o!==Ce.current)return;if(_n(),ke.current){if(!Ut.current){K(`Connection failed after it was established: ${e}`),ft(!0),He("failed"),Ve();return}K(`Reconnecting after established session dropped: ${e}`),ke.current=!1,mt.current=-1}Rt();const i=mt.current+1;if(i>=Gt){K(`Connection failed after ${Gt} attempts: ${e}`),ft(!0),He("failed"),Ve();return}K(`Retrying connection (${i+1}/${Gt})`),He("reconnecting"),Ve(),ze.current=window.setTimeout(()=>{ze.current=void 0,o===Ce.current&&Mr(i)},pi)},Mr=async(e=0)=>{if(Ie.current)return;const o=Ce.current+1;Ce.current=o,mt.current=e,ke.current=!1,ft(!1),e===0&&Xt.current!=="reconnecting"&&He("connecting"),Rt(),wt(),at(),rt.current=!1,kt(!1),Ve();const i=()=>o===Ce.current;Qe.current=window.setTimeout(()=>{Qe.current=void 0,!(!i()||ke.current)&&je("Connection did not succeed within 15 seconds",o)},Ei);try{const n=new WebSocket(Yr(t,s));if(T.current=n,await new Promise((v,f)=>{let k=!1;const z=window.setTimeout(()=>f(new Error("WebSocket connection timeout")),3e4),S=Y=>{k||(k=!0,window.clearTimeout(z),Y())};n.onopen=()=>{!i()||T.current!==n||S(v)},n.onerror=Y=>{!i()||T.current!==n||(K("WebSocket error: "+Y),S(()=>f(new Error("WebSocket connection failed"))))},n.onclose=()=>{!i()||T.current!==n||(K("WebSocket closed"),S(()=>f(new Error("WebSocket closed before connection was established"))))}}),!i()||T.current!==n)return;n.onerror=v=>{!i()||T.current!==n||K("WebSocket error: "+v)},n.onclose=()=>{!i()||T.current!==n||K("WebSocket closed")};const d=new Promise((v,f)=>{const k=Y=>{window.clearTimeout(z),n.removeEventListener("message",S),Y()},z=window.setTimeout(()=>k(()=>f(new Error("RTCConfiguration timeout"))),3e4),S=Y=>{if(!(!i()||T.current!==n))try{const q=JSON.parse(Y.data);if(q.type==="signalingError"&&q.requestType==="requestRtcConfiguration"&&q.sessionId===oe){const te=typeof q.error=="string"?q.error:"Unable to load streaming configuration";k(()=>f(new Error(te)))}else q.type==="rtcConfiguration"&&k(()=>v({rtcConfiguration:q.rtcConfiguration,cameraSupported:q.cameraSupported===!0,foldSupported:q.foldSupported===!0}))}catch(q){console.error("Error handling RTC configuration:",q),k(()=>f(q))}};n.addEventListener("message",S),n.send(JSON.stringify({type:"requestRtcConfiguration",sessionId:oe}))}),{rtcConfiguration:u,cameraSupported:h,foldSupported:p}=await d;if(!i()||T.current!==n)return;const C=y==="iphone-duo"||p;er(C);const m=new RTCPeerConnection(u);W.current=m,m.addTransceiver("audio",{direction:"recvonly"});const M=m.addTransceiver("video",{direction:"recvonly"}),_=C?m.addTransceiver("video",{direction:"recvonly"}):null,I=ae==="ios"||h,$=I?m.addTransceiver("video",{direction:"sendonly"}):null;ye.current=$?.sender??null;const ee=I?m.addTransceiver("audio",{direction:"sendonly"}):null;if(tt.current=ee?.sender??null,RTCRtpReceiver.getCapabilities){const v=RTCRtpReceiver.getCapabilities("video");if(v&&v.codecs){const k=v.codecs.sort((z,S)=>{const Y=q=>{const te=q.mimeType.toLowerCase();return te.includes("vp9")?1:te.includes("h265")||te.includes("hevc")?2:te.includes("h264")||te.includes("avc")?3:4};return Y(z)-Y(S)});M.setCodecPreferences(k),_?.setCodecPreferences(k),x("Set codec preferences:",k.map(z=>z.mimeType).join(", "))}}if($&&RTCRtpSender.getCapabilities){const v=RTCRtpSender.getCapabilities("video");if(v&&v.codecs){const f=new Set(["42e01f","42e028","640c1f"]),k=S=>{const Y=S.mimeType.toLowerCase(),te=(S.sdpFmtpLine||"").toLowerCase().match(/profile-level-id=([0-9a-f]{6})/),Sn=te?te[1]:"";return Y==="video/h265"||Y==="video/hevc"?1:Y==="video/h264"&&f.has(Sn)?2:Y==="video/h264"?3:Y==="video/vp9"?4:Y==="video/vp8"?5:Y==="video/rtx"||Y==="video/red"||Y==="video/ulpfec"?0:6},z=[...v.codecs].sort((S,Y)=>k(S)-k(Y));try{$.setCodecPreferences(z),x("Outbound camera codec preferences:",z.map(S=>`${S.mimeType}${S.sdpFmtpLine?`[${S.sdpFmtpLine}]`:""}`).join(", "))}catch(S){R("Failed to set outbound camera codec preferences:",S)}}}const V=m.createDataChannel("control",{ordered:!0,negotiated:!0,id:1});if(P.current=V,V.onopen=()=>{if(!i()||P.current!==V||T.current!==n)return;ke.current=!0,wt(),K("Control channel opened"),C&&n.send(JSON.stringify({type:"getFoldState",id:"duo-capabilities"})),G.current||(G.current=new Qn({platform:ae,baseIntervalMs:Ae,maxBackoffMs:X,androidElementTreeOptions:ue,send:f=>{if(!T.current||T.current.readyState!==WebSocket.OPEN)return!1;try{return T.current.send(JSON.stringify(f)),!0}catch{return!1}},onSnapshot:f=>{lr(k=>zt(k,f)?k:f),queueMicrotask(()=>{L("onAxSnapshotChange",$t.current,f)})},onStatusChange:(f,k)=>{L("onAxStatusChange",mr.current,f,k)}}),(xe.current===!0||xe.current==="hover-only")&&G.current.start());const v=()=>{!i()||rt.current||P.current!==V||T.current!==n||n.readyState!==WebSocket.OPEN||n.send(JSON.stringify({type:"requestFrame",sessionId:oe}))};if(v(),at(),ht.current=window.setInterval(()=>{if(!i()||rt.current||P.current!==V||T.current!==n||n.readyState!==WebSocket.OPEN){at();return}v()},250),g){try{const f=decodeURIComponent(g);K("Opening URL"),n.send(JSON.stringify({type:"openUrl",url:f,sessionId:oe}))}catch(f){console.error({error:f},"Error decoding URL, falling back to the original URL"),n.send(JSON.stringify({type:"openUrl",url:g,sessionId:oe}))}G.current?.bumpActivity()}},V.onclose=()=>{!i()||P.current!==V||K("Control channel closed")},V.onerror=v=>{!i()||P.current!==V||(console.error("Control channel error:",v),K("Control channel error: "+v))},m.onconnectionstatechange=()=>{!i()||W.current!==m||(K("Connection state: "+m.connectionState),m.connectionState==="failed"&&je("WebRTC connection entered failed state",o))},m.oniceconnectionstatechange=()=>{if(!i()||W.current!==m)return;const v=m.iceConnectionState;if(K("ICE state: "+v),v==="connected"||v==="completed"){ut(),qt()&&!ce.current&&Vt();return}if(v==="failed"){ut(),je("ICE connection entered failed state",o);return}v==="disconnected"&&Ut.current&&Ke.current===void 0&&(Ke.current=window.setTimeout(()=>{Ke.current=void 0,!(!i()||W.current!==m)&&m.iceConnectionState==="disconnected"&&je("ICE stayed disconnected past grace period",o)},gi))},m.ontrack=v=>{if(!(!i()||W.current!==m)&&(K("Received remote track: "+v.track.kind),v.track.kind==="video"&&F.current)){x(`[${new Date().toISOString()}] Video track received:`,v.track);const k=v.track.id==="duo-inner"||v.streams.some(z=>z.id==="duo-inner")?Je.current:F.current;k&&(k.srcObject=C?new MediaStream([v.track]):v.streams[0]??new MediaStream([v.track]),k.play().catch(()=>{}))}},m.onicecandidate=v=>{if(!(!i()||W.current!==m||T.current!==n))if(v.candidate&&n.readyState===WebSocket.OPEN){const f={type:"candidate",candidate:v.candidate.candidate,sdpMid:v.candidate.sdpMid,sdpMLineIndex:v.candidate.sdpMLineIndex,sessionId:oe};n.send(JSON.stringify(f)),K("Sent ICE candidate")}else K("ICE candidate gathering completed")},n.onmessage=async v=>{if(!i()||T.current!==n)return;let f;try{f=JSON.parse(v.data)}catch(k){R("Error parsing message:",k);return}if(!G.current?.handleMessage(f))switch(K("Received: "+f.type),f.type){case"performActionsResult":case"foldStateResult":case"foldStateChanged":{if(!C)break;f.state?(rr(f.state),ir(void 0)):f.id==="duo-capabilities"&&ir(f.error||"This instance does not support native iPhone Duo displays.");const S=Ne.current.get(f.id);S&&(clearTimeout(S.timer),Ne.current.delete(f.id),f.error?S.reject(new Error(f.error)):S.resolve());break}case"signalingError":{if(f.requestType!=="offer"||f.sessionId!==oe)break;const S=typeof f.error=="string"?f.error:"Unable to establish the streaming connection";K("Error: "+S),je(S,o);break}case"answer":if(!W.current||W.current!==m){K("No peer connection, skipping answer");break}if(await m.setRemoteDescription(new RTCSessionDescription({type:"answer",sdp:f.sdp})),!i()||W.current!==m)return;K("Set remote description");break;case"candidate":if(!W.current||W.current!==m){K("No peer connection, skipping candidate");break}if(await m.addIceCandidate(new RTCIceCandidate({candidate:f.candidate,sdpMid:f.sdpMid,sdpMLineIndex:f.sdpMLineIndex})),!i()||W.current!==m)return;K("Added ICE candidate");break;case"screenshot":case"screenshotResult":{if(typeof f.id!="string"){R("Received invalid screenshot success message:",f);break}const S=Di(f);if(S){const te=he.current.get(f.id);if(!te){R(`Received screenshot error for unknown or handled id: ${f.id}`);break}R(`Received screenshot error for id ${f.id}: ${S}`),te(new Error(S)),Ee.current.delete(f.id),he.current.delete(f.id);break}const Y=Ti(f);if(!Y){R("Received screenshot message without image data:",f);break}const q=Ee.current.get(f.id);if(!q){R(`Received screenshot data for unknown or handled id: ${f.id}`);break}x(`Received screenshot data for id ${f.id}`),q(Y),Ee.current.delete(f.id),he.current.delete(f.id);break}case"screenshotError":if(typeof f.id!="string"||typeof f.message!="string"){R("Received invalid screenshot error message:",f);break}const k=he.current.get(f.id);if(!k){R(`Received screenshot error for unknown or handled id: ${f.id}`);break}R(`Received screenshot error for id ${f.id}: ${f.message}`),k(new Error(f.message)),Ee.current.delete(f.id),he.current.delete(f.id);break;case"cameraRequest":{const S=ye.current;if(!S)break;const Y=ei(f);if(!Y){R("Received invalid cameraRequest:",f);break}console.info("[RemoteControl] cameraRequest received:",Y.active,Y.facingMode),await yn(Y,n,S,i);break}case"microphoneRequest":{if(!tt.current)break;const S=f.active===!0;if(Ft.current=S,console.info("[RemoteControl] microphoneRequest received, active=",S),!S){xt.current||await St();break}if(ce.current){Rr(n);break}await Vt();break}case"microphoneStartResult":{if(Pe.current===null||f.id!==Pe.current)break;Pe.current=null,typeof f.error=="string"&&(console.warn("[RemoteControl] microphoneStart rejected by host:",f.error),St(f.error));break}case"terminateAppResult":if(typeof f.id!="string"){R("Received invalid terminateApp result message:",f);break}if(typeof f.error=="string"){const S=_e.current.get(f.id);if(!S){R(`Received terminateApp error for unknown or handled id: ${f.id}`);break}R(`Received terminateApp error for id ${f.id}: ${f.error}`),S(new Error(f.error)),Oe.current.delete(f.id),_e.current.delete(f.id);break}const z=Oe.current.get(f.id);if(!z){R(`Received terminateApp result for unknown or handled id: ${f.id}`);break}x(`Received terminateApp success for id ${f.id}`),z(),Oe.current.delete(f.id),_e.current.delete(f.id);break;default:R(`Received unhandled message type: ${f.type}`,f);break}},W.current===m){const v=await m.createOffer({offerToReceiveVideo:!0,offerToReceiveAudio:!1});if(!i()||W.current!==m||(await m.setLocalDescription(v),!i()||W.current!==m))return;i()&&T.current===n&&n.readyState===WebSocket.OPEN&&n.send(JSON.stringify({type:"offer",...C?{displayStreams:!0}:{},sdp:v.sdp,sessionId:oe})),K("Sent offer")}}catch(n){if(!i())return;const d=n instanceof Error?n.message:String(n);K("Error: "+d),je(d,o)}},jt=()=>{Ie.current=!1,or(!1),Mr(0)},Dn=()=>{Ce.current+=1,mt.current=0,ke.current=!1,Rt(),ut(),Ve(),K("Stopped")},Tn=e=>{e.stopPropagation(),jt()};l.useEffect(()=>{qt()?Vt():St()},[le]),l.useEffect(()=>(kt(!1),jt(),document.hidden||_r(),document.addEventListener("visibilitychange",Ar),()=>{Dr(),Dn(),document.removeEventListener("visibilitychange",Ar)}),[t,s,a,y]);const br=()=>{const e=ct();if(!e){dr(null);return}const o={left:e.videoRect.left-e.containerRect.left+e.offsetX,top:e.videoRect.top-e.containerRect.top+e.offsetY,width:e.actualWidth,height:e.actualHeight};dr(i=>i&&i.left===o.left&&i.top===o.top&&i.width===o.width&&i.height===o.height?i:o)};l.useEffect(()=>{const e=F.current,o=be.current,i=de.current;if(!e)return;const n=()=>{if(!Ot||!o){cr({}),br();return}const h=o.clientWidth,p=o.clientHeight;if(h===0||p===0)return;const C=e.videoWidth>e.videoHeight;jr(C),Jr(ae==="android"&&yi(e.videoWidth,e.videoHeight));const m=C?$e.videoPosition.landscape:$e.videoPosition.portrait;let M={};m.heightMultiplier?(M.height=`${p*m.heightMultiplier}px`,M.width="auto"):m.widthMultiplier&&(M.width=`${h*m.widthMultiplier}px`,M.height="auto"),M.borderRadius=`${C?p*$e.videoBorderRadiusMultiplier:h*$e.videoBorderRadiusMultiplier}px`,cr(M),br()},d=new ResizeObserver(()=>{n()});o&&d.observe(o),d.observe(e),i&&d.observe(i),o&&o.addEventListener("load",n),e.addEventListener("loadedmetadata",n),e.addEventListener("resize",n);const u=()=>G.current?.bumpActivity();return e.addEventListener("resize",u),n(),()=>{d.disconnect(),e.removeEventListener("loadedmetadata",n),e.removeEventListener("resize",n),e.removeEventListener("resize",u),o&&o.removeEventListener("load",n)}},[$e,Ot]),l.useEffect(()=>{const e=G.current;ot?e?.start():(e?.stop(),lr(null),it(null),yt(null),fr(null),Le(null),vt.current=null,ne.current!==void 0&&(window.cancelAnimationFrame(ne.current),ne.current=void 0),L("onAxSnapshotChange",$t.current,null))},[ot]),l.useEffect(()=>()=>{ne.current!==void 0&&(window.cancelAnimationFrame(ne.current),ne.current=void 0)},[]),l.useEffect(()=>{if(!ot)return;const e=o=>{o.key==="Escape"&&(Ht||Wt)&&(yt(null),it(null),Le(null),L("onInspectSelectionChange",We.current,null))};return window.addEventListener("keydown",e),()=>window.removeEventListener("keydown",e)},[ot,Ht,Wt]);const Rn=()=>{F.current&&F.current.focus()};l.useImperativeHandle(Me,()=>({openUrl:e=>{if(!T.current||T.current.readyState!==WebSocket.OPEN){R("WebSocket not open, cannot send open_url command via ref.");return}try{const o=decodeURIComponent(e);K("Opening URL"),T.current.send(JSON.stringify({type:"openUrl",url:o,sessionId:oe}))}catch(o){R("Error decoding or sending URL via ref:",{error:o,url:e}),T.current.send(JSON.stringify({type:"openUrl",url:e,sessionId:oe}))}G.current?.bumpActivity()},sendKeyEvent:e=>{if(!P.current||P.current.readyState!=="open"){R("Data channel not ready for imperative key command:",P.current?.readyState);return}const o=Ur[e.code];if(!o){R(`Unknown event.code for imperative command: ${e.code}`);return}let i=c.META_NONE;e.shiftKey&&(i|=c.META_SHIFT_ON),e.altKey&&(i|=c.META_ALT_ON),e.ctrlKey&&(i|=c.META_CTRL_ON),e.metaKey&&(i|=c.META_META_ON);const n=e.type==="keydown"?c.ACTION_DOWN:c.ACTION_UP;x(`Sending Imperative Key Command: code=${e.code}, keycode=${o}, action=${n}, meta=${i}`);const d=lt(n,o,0,i);d&&se(d)},screenshot:e=>new Promise((o,i)=>{if(!T.current||T.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 n=`ui-ss-${Date.now()}-${Math.random().toString(36).substring(2,9)}`,d={type:e?"screenshotDisplay":"screenshot",...e?{display:e}:{},id:n};Ee.current.set(n,o),he.current.set(n,i),x("Sending screenshot request:",d);try{T.current.send(JSON.stringify(d))}catch(u){R("Failed to send screenshot request immediately:",u),Ee.current.delete(n),he.current.delete(n),i(u);return}setTimeout(()=>{Ee.current.has(n)&&(R(`Screenshot request timed out for id ${n}`),he.current.get(n)?.(new Error("Screenshot request timed out")),Ee.current.delete(n),he.current.delete(n))},3e4)}),terminateApp:e=>new Promise((o,i)=>{if(!T.current||T.current.readyState!==WebSocket.OPEN)return R("WebSocket not open, cannot send terminateApp command."),i(new Error("WebSocket is not connected or connection is not open."));const n=`ui-term-${Date.now()}-${Math.random().toString(36).substring(2,9)}`,d={type:"terminateApp",id:n,bundleId:e};Oe.current.set(n,o),_e.current.set(n,i),x("Sending terminateApp request:",d);try{T.current.send(JSON.stringify(d))}catch(u){R("Failed to send terminateApp request immediately:",u),Oe.current.delete(n),_e.current.delete(n),i(u);return}G.current?.bumpActivity(),setTimeout(()=>{Oe.current.has(n)&&(R(`terminateApp request timed out for id ${n}`),_e.current.get(n)?.(new Error("terminateApp request timed out")),Oe.current.delete(n),_e.current.delete(n))},3e4)}),reconnect:()=>jt(),refreshAxTree:async()=>{const e=G.current;if(!e)throw new Error("Inspect mode is not active");return e.refresh()},getAxSnapshot:()=>Fe.current,setInspectHighlight:e=>{it(e?.id??null)},setInspectSelection:e=>{yt(e?.id??null),Le(e?vt.current:null);const o=Fe.current;e&&o?L("onInspectSelectionChange",We.current,{element:e,snapshot:o}):L("onInspectSelectionChange",We.current,null)},getAxStatus:()=>G.current?.getStatus()??"idle"}));const wn=Qr&&Te!==null;return w.jsxs("div",{ref:de,className:Se("rc-container",r),style:{touchAction:"none"},onMouseDown:ge,onMouseMove:ge,onMouseUp:ge,onMouseLeave:ge,onTouchStart:ge,onTouchMove:ge,onTouchEnd:ge,onTouchCancel:ge,children:[wn&&Te&&w.jsxs(w.Fragment,{children:[w.jsx("div",{className:"rc-touch-indicator",style:{left:`${Te.containerX}px`,top:`${Te.containerY}px`}}),w.jsx("div",{className:"rc-touch-indicator",style:{left:`${Te.mirrorContainerX}px`,top:`${Te.mirrorContainerY}px`}})]}),Ot&&w.jsx("img",{ref:be,src:gr,alt:"",className:ae==="ios"?Se("rc-phone-frame","rc-phone-frame-ios"):"rc-phone-frame",draggable:!1}),w.jsx("video",{ref:F,className:Se("rc-video",!Ot&&"rc-video-frameless",!Br&&"rc-video-loading"),style:{...zr,...ie?{opacity:0,width:1,height:1,pointerEvents:"none"}:{},...Cr?{backgroundImage:`url("${Cr}")`,backgroundRepeat:"no-repeat",backgroundPosition:"center",backgroundSize:$e.loadingLogoSize}:{}},autoPlay:!0,playsInline:!0,muted:!0,tabIndex:ie?-1:0,onKeyDown:Tt,onKeyUp:Tt,onClick:Rn,onLoadedData:mn,onFocus:()=>{F.current&&(F.current.style.outline="none")},onBlur:()=>{F.current&&(F.current.style.outline="none")}}),ie&&w.jsx("video",{ref:Je,autoPlay:!0,playsInline:!0,muted:!0,style:{position:"absolute",width:1,height:1,opacity:0,pointerEvents:"none"}}),ie&&tr&&w.jsx(l.Suspense,{fallback:w.jsx("div",{children:"Loading iPhone Duo…"}),children:w.jsx(hi,{modelUrl:O,outer:F.current,inner:Je.current,state:tr,setAngle:e=>Kt("setHingeAngle",{angleDegrees:e}),setOrientation:e=>Kt("setDuoOrientation",{orientation:e}),button:(e,o)=>Kt("performActions",{actions:[{type:o?"buttonDown":"buttonUp",button:e}]}),touch:(e,o,i,n)=>{const d=P.current;d?.readyState==="open"&&d.send(Kn(e,o,i,n))},onKeyDown:Tt,onKeyUp:Tt})}),ie&&nr&&w.jsx("div",{role:"alert",className:"rc-duo-error",children:nr}),!ie&&ot&&w.jsx(fi,{snapshot:ur,geometry:en,highlightedId:Wt,selectedId:Ht,mode:nn,cursorPosition:tn,frozenCursorPosition:rn,onSelectChange:(e,o)=>{yt(e?.id??null),e&&o?Le(o):e||Le(null);const i=Fe.current;e&&i?L("onInspectSelectionChange",We.current,{element:e,snapshot:i}):L("onInspectSelectionChange",We.current,null)},onTapElement:(e,o)=>{if(o){vr(o.x,o.y);return}const i=Fe.current;i&&on(e,i)}}),Vr?w.jsxs("div",{className:"rc-terminated",role:"status",children:[w.jsx("div",{className:"rc-terminated-title",children:"Instance terminated"}),w.jsx("div",{className:"rc-terminated-message",children:"This instance no longer exists and can't be reconnected."})]}):qr&&w.jsx("button",{type:"button",className:"rc-retry-button",onClick:Tn,children:"Retry"})]})}),Di=r=>typeof r.message=="string"?r.message:typeof r.error=="string"?r.error:null,Ti=r=>typeof r.dataUri=="string"?{dataUri:r.dataUri}:typeof r.base64=="string"?r.base64.startsWith("data:")?{dataUri:r.base64}:{dataUri:`data:${r.base64.startsWith("/9j/")?"image/jpeg":"image/png"};base64,${r.base64}`}:null;exports.AX_UNAVAILABLE_ERROR=Nt;exports.DUO_BUTTONS=Pn;exports.DUO_DIMENSIONS=Mn;exports.DuoButtonSender=Un;exports.HingeSender=kn;exports.RemoteControl=_i;exports.axElementAtPoint=Hr;exports.axElementSelectorExpression=$r;exports.axElementSummary=Qt;exports.axElementsEqual=Jt;exports.axSnapshotsEqual=zt;exports.clampAxFrameForScreen=Wr;exports.duoPointerMode=Yn;exports.flatDisplayTouch=bn;exports.normalizeAndroidTree=Fr;exports.normalizeIosTree=xr;exports.panelTouch=In;
|
package/index.html
DELETED
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8" />
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
-
<title>RemoteControl Demo - @limrun/ui</title>
|
|
7
|
-
<style>
|
|
8
|
-
* {
|
|
9
|
-
margin: 0;
|
|
10
|
-
padding: 0;
|
|
11
|
-
box-sizing: border-box;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
body {
|
|
15
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
16
|
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
17
|
-
min-height: 100vh;
|
|
18
|
-
padding: 20px;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
#root {
|
|
22
|
-
display: flex;
|
|
23
|
-
flex-direction: column;
|
|
24
|
-
gap: 20px;
|
|
25
|
-
max-width: 1400px;
|
|
26
|
-
margin: 0 auto;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
.header {
|
|
30
|
-
text-align: center;
|
|
31
|
-
color: white;
|
|
32
|
-
padding: 20px;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
.header h1 {
|
|
36
|
-
font-size: 2.5rem;
|
|
37
|
-
margin-bottom: 10px;
|
|
38
|
-
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
.header p {
|
|
42
|
-
font-size: 1.1rem;
|
|
43
|
-
opacity: 0.9;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
.demo-container {
|
|
47
|
-
background: white;
|
|
48
|
-
border-radius: 20px;
|
|
49
|
-
padding: 30px;
|
|
50
|
-
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
.controls {
|
|
54
|
-
display: flex;
|
|
55
|
-
flex-direction: column;
|
|
56
|
-
gap: 15px;
|
|
57
|
-
margin-bottom: 30px;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
.control-group {
|
|
61
|
-
display: flex;
|
|
62
|
-
flex-direction: column;
|
|
63
|
-
gap: 8px;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
.control-group label {
|
|
67
|
-
font-weight: 600;
|
|
68
|
-
font-size: 0.9rem;
|
|
69
|
-
color: #374151;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
.control-group input,
|
|
73
|
-
.control-group select {
|
|
74
|
-
padding: 10px 15px;
|
|
75
|
-
border: 2px solid #e5e7eb;
|
|
76
|
-
border-radius: 8px;
|
|
77
|
-
font-size: 0.95rem;
|
|
78
|
-
transition: border-color 0.2s;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
.control-group input:focus,
|
|
82
|
-
.control-group select:focus {
|
|
83
|
-
outline: none;
|
|
84
|
-
border-color: #667eea;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
.button-group {
|
|
88
|
-
display: flex;
|
|
89
|
-
gap: 10px;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
button {
|
|
93
|
-
padding: 12px 24px;
|
|
94
|
-
border: none;
|
|
95
|
-
border-radius: 8px;
|
|
96
|
-
font-size: 1rem;
|
|
97
|
-
font-weight: 600;
|
|
98
|
-
cursor: pointer;
|
|
99
|
-
transition: all 0.2s;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
button.primary {
|
|
103
|
-
background: #667eea;
|
|
104
|
-
color: white;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
button.primary:hover {
|
|
108
|
-
background: #5568d3;
|
|
109
|
-
transform: translateY(-1px);
|
|
110
|
-
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
button.secondary {
|
|
114
|
-
background: #e5e7eb;
|
|
115
|
-
color: #374151;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
button.secondary:hover {
|
|
119
|
-
background: #d1d5db;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
button:disabled {
|
|
123
|
-
opacity: 0.5;
|
|
124
|
-
cursor: not-allowed;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
.device-preview {
|
|
128
|
-
display: flex;
|
|
129
|
-
gap: 30px;
|
|
130
|
-
justify-content: center;
|
|
131
|
-
flex-wrap: wrap;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
.preview-item {
|
|
135
|
-
flex: 1 1 420px;
|
|
136
|
-
max-width: 520px;
|
|
137
|
-
height: min(75vh, 900px);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
.preview-item h3 {
|
|
141
|
-
text-align: center;
|
|
142
|
-
margin-bottom: 15px;
|
|
143
|
-
font-size: 1.2rem;
|
|
144
|
-
color: #374151;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
.device-wrapper {
|
|
148
|
-
height: 100%;
|
|
149
|
-
width: 100%;
|
|
150
|
-
background: #f9fafb;
|
|
151
|
-
border-radius: 12px;
|
|
152
|
-
overflow: hidden;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
@media (max-width: 640px) {
|
|
156
|
-
body {
|
|
157
|
-
padding: 12px;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
.demo-container {
|
|
161
|
-
padding: 16px;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
.button-group {
|
|
165
|
-
flex-direction: column;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
.preview-item {
|
|
169
|
-
max-width: 100%;
|
|
170
|
-
height: 70vh;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
.info-box {
|
|
175
|
-
background: #fef3c7;
|
|
176
|
-
border: 2px solid #fbbf24;
|
|
177
|
-
border-radius: 8px;
|
|
178
|
-
padding: 15px;
|
|
179
|
-
margin-bottom: 20px;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
.info-box h4 {
|
|
183
|
-
color: #92400e;
|
|
184
|
-
margin-bottom: 8px;
|
|
185
|
-
font-size: 0.95rem;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
.info-box p {
|
|
189
|
-
color: #78350f;
|
|
190
|
-
font-size: 0.9rem;
|
|
191
|
-
line-height: 1.5;
|
|
192
|
-
}
|
|
193
|
-
</style>
|
|
194
|
-
</head>
|
|
195
|
-
<body>
|
|
196
|
-
<div id="root"></div>
|
|
197
|
-
<script type="module" src="/src/demo.tsx"></script>
|
|
198
|
-
</body>
|
|
199
|
-
</html>
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
2
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="842.32007" height="1000.0001">
|
|
3
|
-
<path fill="#fff" d="M824.66636 779.30363c-15.12299 34.93724-33.02368 67.09674-53.7638 96.66374-28.27076 40.3074-51.4182 68.2078-69.25717 83.7012-27.65347 25.4313-57.2822 38.4556-89.00964 39.1963-22.77708 0-50.24539-6.4813-82.21973-19.629-32.07926-13.0861-61.55985-19.5673-88.51583-19.5673-28.27075 0-58.59083 6.4812-91.02193 19.5673-32.48053 13.1477-58.64639 19.9994-78.65196 20.6784-30.42501 1.29623-60.75123-12.0985-91.02193-40.2457-19.32039-16.8514-43.48632-45.7394-72.43607-86.6641-31.060778-43.7024-56.597041-94.37983-76.602609-152.15586C10.740416 658.44309 0 598.01283 0 539.50845c0-67.01648 14.481044-124.8172 43.486336-173.25401C66.28194 327.34823 96.60818 296.6578 134.5638 274.1276c37.95566-22.53016 78.96676-34.01129 123.1321-34.74585 24.16591 0 55.85633 7.47508 95.23784 22.166 39.27042 14.74029 64.48571 22.21538 75.54091 22.21538 8.26518 0 36.27668-8.7405 83.7629-26.16587 44.90607-16.16001 82.80614-22.85118 113.85458-20.21546 84.13326 6.78992 147.34122 39.95559 189.37699 99.70686-75.24463 45.59122-112.46573 109.4473-111.72502 191.36456.67899 63.8067 23.82643 116.90384 69.31888 159.06309 20.61664 19.56727 43.64066 34.69027 69.2571 45.4307-5.55531 16.11062-11.41933 31.54225-17.65372 46.35662zM631.70926 20.0057c0 50.01141-18.27108 96.70693-54.6897 139.92782-43.94932 51.38118-97.10817 81.07162-154.75459 76.38659-.73454-5.99983-1.16045-12.31444-1.16045-18.95003 0-48.01091 20.9006-99.39207 58.01678-141.40314 18.53027-21.27094 42.09746-38.95744 70.67685-53.0663C578.3158 9.00229 605.2903 1.31621 630.65988 0c.74076 6.68575 1.04938 13.37191 1.04938 20.00505z"/>
|
|
4
|
-
</svg>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
import React, { act } from 'react';
|
|
2
|
-
import { createRoot } from 'react-dom/client';
|
|
3
|
-
import { afterEach, beforeEach, expect, it, vi } from 'vitest';
|
|
4
|
-
import { DuoControls } from './duo-controls';
|
|
5
|
-
import { useDuoButtonHinge } from './duo-button-hinge';
|
|
6
|
-
|
|
7
|
-
let now = 0;
|
|
8
|
-
let nextId = 0;
|
|
9
|
-
const callbacks = new Map<number, FrameRequestCallback>();
|
|
10
|
-
const host = document.createElement('div');
|
|
11
|
-
let root: ReturnType<typeof createRoot>;
|
|
12
|
-
let hinge: ReturnType<typeof useDuoButtonHinge>;
|
|
13
|
-
const send = vi.fn<(angle: number) => Promise<void>>();
|
|
14
|
-
const failed = vi.fn();
|
|
15
|
-
function View({ nativeAngle = 0 }: { nativeAngle?: number }) {
|
|
16
|
-
hinge = useDuoButtonHinge(nativeAngle, send, failed);
|
|
17
|
-
return (
|
|
18
|
-
<DuoControls
|
|
19
|
-
angle={hinge.angle}
|
|
20
|
-
changeAngle={hinge.changeAngle}
|
|
21
|
-
interacting={hinge.interacting}
|
|
22
|
-
rotate={() => {}}
|
|
23
|
-
onFold={hinge.fold}
|
|
24
|
-
foldTarget={hinge.target}
|
|
25
|
-
onHingeDrag={hinge.cancelFold}
|
|
26
|
-
/>
|
|
27
|
-
);
|
|
28
|
-
}
|
|
29
|
-
beforeEach(() => {
|
|
30
|
-
now = nextId = 0;
|
|
31
|
-
callbacks.clear();
|
|
32
|
-
send.mockReset().mockResolvedValue(undefined);
|
|
33
|
-
failed.mockReset();
|
|
34
|
-
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
|
35
|
-
vi.spyOn(performance, 'now').mockImplementation(() => now);
|
|
36
|
-
vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) => {
|
|
37
|
-
callbacks.set(++nextId, callback);
|
|
38
|
-
return nextId;
|
|
39
|
-
});
|
|
40
|
-
vi.stubGlobal('cancelAnimationFrame', (id: number) => callbacks.delete(id));
|
|
41
|
-
root = createRoot(host);
|
|
42
|
-
});
|
|
43
|
-
afterEach(async () => {
|
|
44
|
-
await act(async () => root.unmount());
|
|
45
|
-
expect(callbacks.size).toBe(0);
|
|
46
|
-
vi.restoreAllMocks();
|
|
47
|
-
vi.unstubAllGlobals();
|
|
48
|
-
});
|
|
49
|
-
async function advance(duration: number) {
|
|
50
|
-
const until = now + duration;
|
|
51
|
-
while (now < until) {
|
|
52
|
-
now = Math.min(until, now + 1000 / 60);
|
|
53
|
-
const frame = [...callbacks.values()];
|
|
54
|
-
callbacks.clear();
|
|
55
|
-
await act(async () => frame.forEach((callback) => callback(now)));
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
async function clickFold() {
|
|
59
|
-
await act(async () => host.querySelector<HTMLButtonElement>('.rc-duo-fold')!.click());
|
|
60
|
-
}
|
|
61
|
-
for (const from of [0, 180]) {
|
|
62
|
-
it(`sends intermediate native angles from ${from} and the exact endpoint`, async () => {
|
|
63
|
-
await act(async () => root.render(<View nativeAngle={from} />));
|
|
64
|
-
await clickFold();
|
|
65
|
-
expect(send).not.toHaveBeenCalled();
|
|
66
|
-
expect(host.querySelector('.rc-duo-fold')!.textContent).toBe(from === 0 ? 'Fold' : 'Unfold');
|
|
67
|
-
await advance(2700);
|
|
68
|
-
const angles = send.mock.calls.map(([angle]) => angle);
|
|
69
|
-
expect(angles.length).toBeGreaterThan(15);
|
|
70
|
-
expect(angles.length).toBeLessThanOrEqual(77);
|
|
71
|
-
expect(angles[0]).toBeGreaterThan(0);
|
|
72
|
-
expect(angles[0]).toBeLessThan(180);
|
|
73
|
-
expect(angles.at(-1)).toBe(180 - from);
|
|
74
|
-
expect(
|
|
75
|
-
angles.every((angle, i) => i === 0 || (from === 0 ? angle >= angles[i - 1]! : angle <= angles[i - 1]!)),
|
|
76
|
-
).toBe(true);
|
|
77
|
-
expect(hinge.interacting.current).toBe(false);
|
|
78
|
-
expect(hinge.motion.current).toBeUndefined();
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
it('reverses from its current pose and lets a slider change take over', async () => {
|
|
82
|
-
await act(async () => root.render(<View />));
|
|
83
|
-
await clickFold();
|
|
84
|
-
await advance(200);
|
|
85
|
-
const previous = hinge.angle;
|
|
86
|
-
await clickFold();
|
|
87
|
-
expect(hinge.motion.current?.from).toBeCloseTo(previous, 8);
|
|
88
|
-
expect(hinge.target).toBe(0);
|
|
89
|
-
await advance(200);
|
|
90
|
-
expect(hinge.angle).toBeLessThan(previous);
|
|
91
|
-
await act(async () => hinge.changeAngle(110));
|
|
92
|
-
const count = send.mock.calls.length;
|
|
93
|
-
await advance(1500);
|
|
94
|
-
expect(send).toHaveBeenLastCalledWith(110);
|
|
95
|
-
expect(send).toHaveBeenCalledTimes(count);
|
|
96
|
-
});
|
|
97
|
-
it('coalesces a slow native connection without losing the endpoint', async () => {
|
|
98
|
-
let release!: () => void;
|
|
99
|
-
send.mockImplementationOnce(
|
|
100
|
-
() =>
|
|
101
|
-
new Promise<void>((resolve) => {
|
|
102
|
-
release = resolve;
|
|
103
|
-
}),
|
|
104
|
-
);
|
|
105
|
-
await act(async () => root.render(<View />));
|
|
106
|
-
await clickFold();
|
|
107
|
-
await advance(2700);
|
|
108
|
-
expect(send).toHaveBeenCalledTimes(1);
|
|
109
|
-
await act(async () => release());
|
|
110
|
-
expect(send).toHaveBeenCalledTimes(2);
|
|
111
|
-
expect(send).toHaveBeenLastCalledWith(180);
|
|
112
|
-
});
|
|
113
|
-
it('stops animation on a native failure and on unmount', async () => {
|
|
114
|
-
send.mockRejectedValueOnce(new Error('HID failed'));
|
|
115
|
-
await act(async () => root.render(<View />));
|
|
116
|
-
await clickFold();
|
|
117
|
-
await advance(100);
|
|
118
|
-
expect(hinge.motion.current).toBeUndefined();
|
|
119
|
-
expect(hinge.angle).toBe(0);
|
|
120
|
-
expect(failed).toHaveBeenLastCalledWith('Error: HID failed');
|
|
121
|
-
await clickFold();
|
|
122
|
-
await advance(100);
|
|
123
|
-
await act(async () => root.render(null));
|
|
124
|
-
const count = send.mock.calls.length;
|
|
125
|
-
await advance(2700);
|
|
126
|
-
expect(send).toHaveBeenCalledTimes(count);
|
|
127
|
-
});
|
|
128
|
-
it('keeps the default 2D button as one immediate endpoint command', async () => {
|
|
129
|
-
const changeAngle = vi.fn();
|
|
130
|
-
await act(async () =>
|
|
131
|
-
root.render(
|
|
132
|
-
<DuoControls angle={0} changeAngle={changeAngle} interacting={{ current: false }} rotate={() => {}} />,
|
|
133
|
-
),
|
|
134
|
-
);
|
|
135
|
-
await clickFold();
|
|
136
|
-
expect(changeAngle).toHaveBeenCalledExactlyOnceWith(180);
|
|
137
|
-
expect(callbacks.size).toBe(0);
|
|
138
|
-
});
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import { useEffect, useRef, useState } from 'react';
|
|
2
|
-
import { useDuoHinge } from './duo-controls';
|
|
3
|
-
import { duoFoldAngle } from './duo-fold-timing';
|
|
4
|
-
|
|
5
|
-
type Fold = { from: number; target: number; startedAt: number };
|
|
6
|
-
|
|
7
|
-
/** Drive button folds through the slider's native angle queue. */
|
|
8
|
-
export function useDuoButtonHinge(
|
|
9
|
-
nativeAngle: number,
|
|
10
|
-
send: (angle: number) => Promise<void>,
|
|
11
|
-
failed: (error: string | undefined) => void,
|
|
12
|
-
) {
|
|
13
|
-
const motion = useRef<Fold | undefined>(undefined);
|
|
14
|
-
const request = useRef(0);
|
|
15
|
-
const [target, setTarget] = useState<number>();
|
|
16
|
-
const hinge = useDuoHinge(nativeAngle, send, (reason) => {
|
|
17
|
-
if (reason !== undefined) cancelFold();
|
|
18
|
-
failed(reason);
|
|
19
|
-
});
|
|
20
|
-
const latest = useRef(hinge);
|
|
21
|
-
latest.current = hinge;
|
|
22
|
-
const cancelFold = () => {
|
|
23
|
-
if (!motion.current) return;
|
|
24
|
-
cancelAnimationFrame(request.current);
|
|
25
|
-
motion.current = undefined;
|
|
26
|
-
hinge.interacting.current = false;
|
|
27
|
-
setTarget(undefined);
|
|
28
|
-
};
|
|
29
|
-
const fold = () => {
|
|
30
|
-
const now = performance.now();
|
|
31
|
-
const previous = motion.current;
|
|
32
|
-
const from =
|
|
33
|
-
previous ?
|
|
34
|
-
duoFoldAngle(previous.from, previous.target, (now - previous.startedAt) / 1000)
|
|
35
|
-
: hinge.angle;
|
|
36
|
-
const to = (previous?.target ?? hinge.angle) < 90 ? 180 : 0;
|
|
37
|
-
cancelFold();
|
|
38
|
-
const next = { from, target: to, startedAt: now };
|
|
39
|
-
motion.current = next;
|
|
40
|
-
hinge.interacting.current = true;
|
|
41
|
-
setTarget(to);
|
|
42
|
-
let lastSent = -Infinity;
|
|
43
|
-
const tick = (time: number) => {
|
|
44
|
-
if (motion.current !== next) return;
|
|
45
|
-
const angle = duoFoldAngle(from, to, (time - now) / 1000);
|
|
46
|
-
// Native HID updates are bounded; the 3D pose uses the same clock at display refresh rate.
|
|
47
|
-
if (time - lastSent >= 1000 / 30 || angle === to) {
|
|
48
|
-
lastSent = time;
|
|
49
|
-
latest.current.changeAngle(angle);
|
|
50
|
-
}
|
|
51
|
-
if (angle === to) cancelFold();
|
|
52
|
-
else request.current = requestAnimationFrame(tick);
|
|
53
|
-
};
|
|
54
|
-
request.current = requestAnimationFrame(tick);
|
|
55
|
-
};
|
|
56
|
-
useEffect(
|
|
57
|
-
() => () => {
|
|
58
|
-
cancelAnimationFrame(request.current);
|
|
59
|
-
motion.current = undefined;
|
|
60
|
-
latest.current.interacting.current = false;
|
|
61
|
-
},
|
|
62
|
-
[],
|
|
63
|
-
);
|
|
64
|
-
return {
|
|
65
|
-
...hinge,
|
|
66
|
-
motion,
|
|
67
|
-
target,
|
|
68
|
-
fold,
|
|
69
|
-
cancelFold,
|
|
70
|
-
changeAngle: (value: number) => {
|
|
71
|
-
cancelFold();
|
|
72
|
-
hinge.changeAngle(value);
|
|
73
|
-
},
|
|
74
|
-
};
|
|
75
|
-
}
|