PageSourceSearch

https://sunra.ai/_next/static/chunks/0-l3rg8_thsed.js?dpl=dpl_J4BzYExe6nabgNNe1zu1S3bLw4TM

js sunra.ai collected 2026-10-05 16:10:13 UTC 502,585 bytes, 6,153 lines download raw bytes

1(globalThis.TURBOPACK||(globalThis.TURBOPACK=[])).push(["object"==typeof document?document.currentScript:void 0,763408,86821,82832,393741,618154,918774,475923,885100,49908,e=>{"use strict";let t=(0,e.i(768834).create)(e=>({selection:null,frameId:null,activeDoc:null,activePageId:null,liveStyle:null,applyStyle:null,previewStyle:null,setAttr:null,deleteSelection:null,selectFont:null,loadFont:null,replaceImageSrc:null,openCrop:null,setImageAlt:null,setFrameBackground:null,setFramePreset:null,reorderComponentInFrame:null,setShapeParams:null,docked:!1,publishSelection:(t,n)=>e({selection:t,frameId:n}),clearSelection:()=>e({selection:null,frameId:null,liveStyle:null}),publishDocument:(t,n)=>e({activeDoc:t,activePageId:n,liveStyle:null}),publishLiveStyle:t=>e({liveStyle:t}),registerWriters:t=>e({applyStyle:t?.applyStyle??null,previewStyle:t?.previewStyle??null,setAttr:t?.setAttr??null,deleteSelection:t?.deleteSelection??null,selectFont:t?.selectFont??null,loadFont:t?.loadFont??null,replaceImageSrc:t?.replaceImageSrc??null,openCrop:t?.openCrop??null,setImageAlt:t?.setImageAlt??null,setFrameBackground:t?.setFrameBackground??null,setFramePreset:t?.setFramePreset??null,reorderComponentInFrame:t?.reorderComponentInFrame??null,setShapeParams:t?.setShapeParams??null}),setDocked:t=>e({docked:t})}));function n(e,t){return e?e.frames.find(e=>e.id===t)??e.frames[0]??null:null}e.s(["componentForCid",0,function(e,t,r){if(!r)return null;let a=n(e,t);return a?.components.find(e=>e.cid===r)??null},"frameForId",0,n,"guardInspectorWriters",0,function(e,t){return{...e,...Object.fromEntries(Object.entries(e).map(([e,n])=>[e,(...e)=>{t()&&Reflect.apply(n,void 0,e)}]))}},"selectionFromPickerElements",0,function(e){let t=e.filter(e=>!!e.meta);return 0===t.length?null:{cids:t.map(e=>(function(e){let t="";for(let n of e.matchAll(/\[data-cid="((?:[^"\\]|\\.)*)"\]/g))t=n[1];return t.replace(/\\(.)/g,"$1")})(e.selector)),selectors:t.map(e=>e.selector),meta:t.map(e=>e.meta),tags:t.map(e=>(e.tag||"").toLowerCase())}},"usePreviewInspector",0,t],763408);var r=e.i(843476),a=e.i(407013),o=e.i(564800),i=e.i(975157);e.s(["AddPageSlot",0,function({onAdd:e,width:t,label:n="Add section",className:a}){return(0,r.jsxs)("button",{type:"button",onClick:e,title:n,"aria-label":n,style:t?{width:t}:void 0,className:(0,i.cn)("group pointer-events-auto flex h-9 items-center justify-center gap-1.5","rounded-[var(--radius-bar-control)] border border-dashed border-border text-muted-foreground","bg-transparent","shadow-[var(--shadow-border)] backdrop-blur-sm transition-surface","hover:border-solid hover:border-input hover:bg-card hover:text-foreground",a),children:[(0,r.jsx)(o.Plus,{size:15}),(0,r.jsx)("span",{className:"text-caption font-medium",children:n})]})}],86821),e.s(["addSlotGapPx",0,function(e){return Math.max(12,Math.round(24*(Number.isFinite(e)&&e>0?e:1)))}],82832);var s=e.i(271645),l=e.i(28157),d=e.i(226189),c=e.i(576048);e.i(716787);var h=e.i(373800),u=e.i(214339),p=e.i(165927),m=e.i(69466),f=e.i(269812);function g(e,t){let n=(0,f.resolvePreset)(e);return Math.round(t*n.height/n.width)}function b(e,t){let n=(0,f.resolvePreset)(e.preset);return Math.round(t*(0,f.frameHeight)(e)/n.width)}function y(e,t={}){let n=t.boardWidth??960,r=t.gap??48,a=Math.max(1,t.columns??3),o=t.addSize??40,i=t.arrange??(0,f.arrangeFor)(e.format),s=e.frames,l=[],d=[];if("single"===i){let e=Math.min(Math.max(0,Math.floor(t.activeIndex??0)),Math.max(0,s.length-1)),r=s[e],a=r?b(r,n):0;return r&&l.push({frameId:r.id,index:e,x:0,y:0,w:n,h:a}),{arrange:i,boards:l,addSlots:d,width:n,height:a}}if("filmstrip"===i){let t=0,a=0;s.forEach((e,o)=>{let i=b(e,n);l.push({frameId:e.id,index:o,x:t,y:0,w:n,h:i}),a=Math.max(a,i),t+=n+r}),d.push({x:t,y:0,w:o,h:a||g(e.format,n),after:s.length-1});let c=a||g(e.format,n);return{arrange:i,boards:l,addSlots:d,width:t+o,height:c}}if("grid"===i){let t=0,o=0,c=0,h=0,u=0;s.forEach((e,i)=>{let s=b(e,n);l.push({frameId:e.id,index:i,x:t,y:o,w:n,h:s}),c=Math.max(c,s),u=Math.max(u,t+n),(h+=1)>=a?(h=0,t=0,o+=c+r,c=0):t+=n+r});let p=g(e.format,n);return d.push({x:t,y:o,w:n,h:p,after:s.length-1}),{arrange:i,boards:l,addSlots:d,width:Math.max(u,n),height:o+Math.max(c,p)}}let c=Math.max(0,t.headerSize??0),h=!0===t.trailingAddOnly,u=0;
1return s.forEach((e,t)=>{u+=c;let a=b(e,n);l.push({frameId:e.id,index:t,x:0,y:u,w:n,h:a}),u+=a,h&&t!==s.length-1||d.push({x:0,y:u+(r-o)/2,w:n,h:o,after:t}),u+=r}),0===s.length&&(d.push({x:0,y:0,w:n,h:o,after:-1}),u=o),{arrange:i,boards:l,addSlots:d,width:n,height:u}}function v({liveKey:e,settledKey:t,hasPrevious:n}){return n&&t!==e}function w(e,t){return"single"===e?"board-single":t}function x(e){return{slot:0,html:e,pending:null}}function E(e,t){return t===e.html?e.pending?{...e,pending:null}:e:e.pending?.html===t?e:{...e,pending:{slot:+(0===e.slot),html:t}}}function k(e){return e.pending?{slot:e.pending.slot,html:e.pending.html,pending:null}:e}function S(e){let t=e.margin??4,{boardTop:n,prevBottom:r,pageWidth:a,rowHeight:o,rowWidth:i}=e;return a<i||(null===r?1/0:n-r-2*t)<o?{top:n,height:0,width:0,mode:"hidden"}:{top:n-t-o,height:o,width:a,mode:"full"}}function T(e){return null===e.prevBottom?e.slotTop:Math.max(e.slotTop,e.prevBottom+24)}function A(e,t,n){return t+e*n}function R(e,t,n){let r=t.width/n;if(!Number.isFinite(r)||r<=0)return null;let a=(e.x-t.left)/r,o=(e.y-t.top)/r;return Number.isFinite(a)&&Number.isFinite(o)?{x:a,y:o}:null}e.s(["computeArrange",0,y],393741),e.s(["LIVE_FRAME_SETTLE_TIMEOUT_MS",0,3e3,"beginSwap",0,E,"boardSlotKey",0,w,"holdsPrevious",0,v,"initialSlots",0,x,"liveFrameSlotKey",0,function(){return"live-board"},"promoteSwap",0,k],618154),e.s(["canvasToScreen",0,A,"framePointFromScreen",0,R,"placeAddTab",0,T,"placePageRow",0,S],918774);var N=e.i(121332),C=e.i(302606),I=e.i(539398);let _="Adding a page will be available once this composition finishes writing";function O({html:e,nativeW:t,nativeH:n,scale:a,label:o}){let i=(0,N.previewSurfaceRung)(t*a),l=(0,s.useMemo)(()=>(0,N.rewritePreviewHtml)(e,{width:i,dpr:1,priority:"low"}),[e,i]),[d,c]=(0,s.useState)(()=>x(l)),h=(0,s.useCallback)(()=>c(k),[]);(0,s.useEffect)(()=>{c(e=>E(e,l))},[l]);let u=d.pending?.html;(0,s.useEffect)(()=>{if(void 0===u)return;let e=setTimeout(h,3e3);return()=>clearTimeout(e)},[u,h]);let p=(0,s.useMemo)(()=>(0,C.pageBitmap)(d.html),[d.html]),f=(0,s.useMemo)(()=>void 0===u?null:(0,C.pageBitmap)(u),[u]);return(0,r.jsx)(r.Fragment,{children:[0,1].map(e=>{let i=e===d.slot,s=d.pending?.slot===e,l=i?p:s?f:p;return l?(0,r.jsx)(m.PageBitmapSlot,{bitmap:l,loaded:i||s,scale:a,opacity:+!!i,fetchPriority:"low",onSettle:s?h:void 0},e):(0,r.jsx)("iframe",{title:o,sandbox:"",srcDoc:i?d.html:s?d.pending?.html:"",onLoad:s?h:void 0,tabIndex:-1,"aria-hidden":!0,className:"pointer-events-none absolute left-0 top-0 origin-top-left border-0",style:{width:t,height:n,transform:`scale(${a})`,opacity:+!!i}},e)})})}function P({label:e,nativeWidth:t,onDeselect:n,onOpenAt:a}){return(0,r.jsx)("div",{"aria-hidden":!0,"data-board-hit":"",title:e,onMouseDown:e=>{0===e.button&&(e.stopPropagation(),n(),a(R({x:e.clientX,y:e.clientY},e.currentTarget.getBoundingClientRect(),t)))},className:"absolute inset-0 cursor-default rounded-[inherit]"})}function D({box:e,label:t,onSelect:n}){let a=function(e,t=6){if(!(t>0)||!Number.isFinite(e.width)||!Number.isFinite(e.height))return null;let n=4*t;if(e.width<n||e.height<n)return null;let{left:r,top:a,width:o,height:i}=e;return[{side:"top",left:r,top:a,width:o,height:t},{side:"bottom",left:r,top:a+i-t,width:o,height:t},{side:"left",left:r,top:a,width:t,height:i},{side:"right",left:r+o-t,top:a,width:t,height:i}]}(e);return a?(0,r.jsx)(r.Fragment,{children:a.map(e=>(0,r.jsx)("div",{"aria-hidden":!0,"data-frame-edge":e.side,title:t,onMouseDown:e=>{0===e.button&&(e.stopPropagation(),e.preventDefault(),n())},className:"pointer-events-auto absolute cursor-default",style:{left:e.left,top:e.top,width:e.width,height:e.height}},e.side))}):null}let L=(0,i.cn)("pointer-events-auto flex items-center justify-center gap-1.5","rounded-[var(--radius-tile-md)] border border-dashed border-border text-muted-foreground","transition-surface enabled:hover:border-solid enabled:hover:border-ring enabled:hover:bg-accent enabled:hover:text-foreground","disabled:cursor-not-allowed disabled:opacity-40");function z({x:e,y:t,w:n,label:i,disabledReason:s,onAdd:l}){return(0,r.jsxs)("button",{type:"button",onClick:l,"aria-label":i,title:s??void 0,disabled:null!==s,style:{position:"absolute",left:e,top:t,width:n,height:30},className:L,children:[(0,r.jsx)(o.Plus,{size:a.GLYPH_PX}),n>=160&&(0,r.jsx)("span",{className:"text-caption font-medium",children:i})]})}function F({left:e,top:t,width:n,height:a,...o}){return(0,r.jsx)("div",{style:{position:"absolute",left:e,top:t,width:n,height:a},className:"pointer-events-auto flex items-center justify-end gap-1",children:(0,r.jsx)(H,{...o})})}let H=(0,s.memo)(function({pageId:e,index:t,total:n,label:o,active:s,ops:u,onSelectFrame:m}){return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)("button",{type:"button",onClick:()=>m(e),className:(0,i.cn)("min-w-0 flex-1 truncate rounded-md px-1.5 py-0.5 text-left text-caption transition-surface",s?"font-medium text-foreground":"text-muted-foreground hover:text-foreground"),children:o}),(0,r.jsx)(h.IconButton,{label:`Move page ${t+1} up`,title:"Move up",disabled:0===t,noHitAreaExtension:!0,className:p.flowIconButtonSm,onClick:()=>u.onMove(e,-1),children:(0,r.jsx)(l.ChevronUp,{size:a.GLYPH_PX_DENSE})}),(0,r.jsx)(h.IconButton,{label:`Move page ${t+1} down`,title:"Move down",disabled:t===n-1,noHitAreaExtension:!0,className:p.flowIconButtonSm,onClick:()=>u.onMove(e,1),children:(0,r.jsx)(l.ChevronDown,{size:a.GLYPH_PX_DENSE})}),(0,r.jsx)(h.IconButton,{label:`Duplicate page ${t+1}`,title:"Duplicate",noHitAreaExtension:!0,className:p.flowIconButtonSm,onClick:()=>
1u.onDuplicate(e),children:(0,r.jsx)(d.Copy,{size:a.GLYPH_PX_DENSE})}),(0,r.jsx)(h.IconButton,{label:`Delete page ${t+1}`,title:u.canDelete?"Delete":"Not available while writing",disabled:!u.canDelete,noHitAreaExtension:!0,className:p.flowIconButtonSm,onClick:()=>u.onDelete(e),children:(0,r.jsx)(c.Trash2,{size:a.GLYPH_PX_DENSE})})]})});e.s(["ADD_UNAVAILABLE_WHILE_WRITING",0,_,"ARRANGE_HEADER_H",0,34,"ComposeArrangeCanvas",0,function({doc:e,pages:t,activePageId:n,selectedPageIds:a,boardWidth:o,view:l,activeIframe:d,activeFrameGrowable:c,onSelectFrame:h,onActivateFrame:p,onDeselect:m,onAddPageAt:g,onAddSection:b,allPages:x=!1,headerSize:E=0,pageOps:k,canAdd:R=!0}){let N=(0,u.useViewerReadOnly)(),C=Math.max(0,e.frames.findIndex(e=>e.id===n)),L=(0,s.useMemo)(()=>y(e,{boardWidth:o,activeIndex:C,...x?{arrange:"stack",headerSize:E,trailingAddOnly:!0}:{arrange:"single"}}),[e,o,C,x,E]),H=d.rebuildKey,[M,j]=(0,s.useState)(null),B=(0,s.useCallback)(()=>j(H),[H]),G=(0,s.useCallback)(()=>{d.onLoad(),B()},[d,B]);(0,s.useEffect)(()=>{if(M===H)return;let e=setTimeout(B,3e3);return()=>clearTimeout(e)},[H,M,B]);let W=(0,f.isGrowableFormat)(e.format),U=(0,s.useMemo)(()=>{let o=new Map(t.map(e=>[e.id,e.html])),s=new Set(a??[]);return L.boards.map(t=>{let a=e.frames[t.index];if(!a)return null;let l=(0,f.resolvePreset)(a.preset),d=t.w/l.width,c=l.width,h=(0,f.frameHeight)(a),u=a.id===n,g=s.has(a.id),b=(0,I.frameTitle)(a,t.index);return(0,r.jsxs)("div",{"data-selected":g?"":void 0,className:(0,i.cn)("absolute overflow-hidden rounded-none bg-[var(--canvas-node)]",g?"shadow-[0_0_0_2px_var(--flow-select)]":u?"dark:shadow-[var(--shadow-tile)] ring-1 ring-foreground/40 light:ring-0":"dark:shadow-[var(--shadow-tile)]"),style:{left:t.x,top:t.y,width:t.w,height:t.h},children:[(0,r.jsx)(O,{html:o.get(a.id)??"",nativeW:c,nativeH:h,scale:d,label:b}),(!u||N)&&(0,r.jsx)(P,{label:b,nativeWidth:c,onDeselect:m,onOpenAt:e=>p(a.id,e??void 0)})]},w(L.arrange,a.id))})},[L,e,t,n,a,p,m,N]),q=L.boards.find(t=>e.frames[t.index]?.id===n),X=q?e.frames[q.index]:void 0,Y=X?(0,f.resolvePreset)(X.preset):void 0;return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)("div",{className:"absolute left-0 top-0 origin-top-left",style:{width:L.width,height:L.height,transform:`translate(${l.panX}px, ${l.panY}px) scale(${l.zoom})`,transition:"none"},children:U}),!N&&q&&X&&Y&&(0,r.jsx)("div",{className:"absolute left-0 top-0 origin-top-left overflow-hidden",style:{width:q.w,height:q.h,transform:`translate(${l.panX+q.x*l.zoom}px, ${l.panY+q.y*l.zoom}px) scale(${l.zoom})`,transition:"none"},children:(0,r.jsx)("iframe",{ref:d.ref,title:d.title,sandbox:"allow-scripts",srcDoc:d.srcDoc,onLoad:G,className:"absolute left-0 top-0 origin-top-left border-0",style:{width:Y.width,height:(0,f.frameHeight)(X),transform:`scale(${q.w/Y.width})`,opacity:+!v({liveKey:H,settledKey:M,hasPrevious:!!t.find(e=>e.id===n)?.html})}},d.rebuildKey)}),(0,r.jsxs)("div",{className:"pointer-events-none absolute inset-0",children:[L.boards.map(t=>{let n=e.frames[t.index];return n?((0,f.resolvePreset)(n.preset),(0,r.jsx)(D,{box:{left:A(t.x,l.panX,l.zoom),top:A(t.y,l.panY,l.zoom),width:t.w*l.zoom,height:t.h*l.zoom},label:(0,I.frameTitle)(n,t.index),onSelect:()=>h(n.id)},`edge-${n.id}`)):null}),k&&L.boards.map((t,a)=>{let o=e.frames[t.index];if(!o)return null;(0,f.resolvePreset)(o.preset);let i=a>0?L.boards[a-1]:null,s=o.id===n,d=S({boardTop:A(t.y,l.panY,l.zoom),prevBottom:i?A(i.y+i.h,l.panY,l.zoom):null,pageWidth:t.w*l.zoom,rowHeight:28,rowWidth:176});return"hidden"===d.mode?null:(0,r.jsx)(F,{pageId:o.id,index:t.index,total:e.frames.length,label:(0,I.frameTitle)(o,t.index),left:A(t.x,l.panX,l.zoom),width:d.width,top:d.top,height:d.height,active:s,ops:k,onSelectFrame:h},`row-${o.id}`)}),L.addSlots.map(e=>{let t=W&&c&&e.after===C,n="stack"===L.arrange?L.boards.find(t=>t.index===e.after)??null:null;return(0,r.jsx)(z,{x:A(e.x,l.panX,l.zoom),y:T({slotTop:A(e.y,l.panY,l.zoom),prevBottom:n?A(n.y+n.h,l.panY,l.zoom):null}),w:e.w*l.zoom,label:t?"Add section":"Add page",disabledReason:R?null:_,onAdd:t?b:()=>g(e.after)},`slot-${e.after}-${Math.round(e.x)}-${Math.round(e.y)}`)})]})]})},"PAGE_ROW_H",0,28,"PAGE_ROW_MIN_W",0,176,"PageControlRow",0,F],475923);var M=e.i(539019);function j(e,t={}){return t.space||t.middleButton||"hand"===e?"pan":"marquee"}function B(e,t={}){return"hand"===e||!0===t.space}let G=["select","hand"],W={select:"V",hand:"H",text:"T",rectangle:"R",ellipse:"O",line:"L"};e.s(["TOOL_SHORTCUT",0,W,"canvasCursorFor",0,function(e,t={},n=!1){return"hand"!==e&&!0!==t.space&&!0!==t.middleButton?"default":n?"grabbing":"grab"},"floorClickIntent",0,function(e,t={}){return"marquee"===j(e,t)?"deselect":"none"},"floorGestureFor",0,j,"panOverlayActive",0,B,"toggleTool",0,function(e){return G.find(t=>t!==e)??e},"toolForKey",0,function(e,t=!1){if(t)return null;let n=e.toUpperCase();for(let e of Object.keys(W))if(W[e]===n)return e;return null}],885100),e.s(["CANVAS_CURSOR_CLASS",0,{default:"cursor-default",grab:"cursor-grab",grabbing:"cursor-grabbing"},"ComposeCanvasGestureLayer",0,function({tool:e,spaceHeld:t,band:n}){let a={space:t},o=null!==n&&"marquee"===j(e,a)?(0,M.marqueeRect)(n.from.x,n.from.y,n.to.x,n.to.y):null;return(0,r.jsxs)(r.Fragment,{children:[B(e,a)&&(0,r.jsx)("div",{className:"absolute inset-0","aria-hidden":"true","data-compose-pan-overlay":""}),o&&(0,r.jsx)("div",{className:"pointer-events-none absolute","data-compose-marquee":"",style:{left:o.left,top:o.top,width:o.right-o.left,height:o.bottom-o.top,boxSh
1adow:"0 0 0 1px var(--flow-select)",background:"color-mix(in oklab, var(--flow-select) 12%, transparent)",transition:"none"}})]})}],49908)},70784,e=>{"use strict";var t=e.i(726497);e.s(["HandGrab",()=>t.IconHand4Finger])},400924,672619,209228,726975,438397,236256,512467,237334,977385,e=>{"use strict";var t=e.i(843476),n=e.i(407013),r=e.i(221313),a=e.i(564800),o=e.i(98558);e.i(716787);var i=e.i(373800),s=e.i(214339),l=e.i(975157),d=e.i(331263),c=e.i(736522),h=e.i(165927),u=e.i(155876),p=e.i(165727),m=e.i(948148),f=e.i(271645),g=e.i(846696),b=e.i(218943),y=e.i(610947),v=e.i(194373),w=e.i(9194),x=e.i(142308),E=e.i(404015),k=e.i(578109),S=e.i(157171),T=e.i(770136),A=e.i(538299),R=e.i(196126),N=e.i(75299),C=e.i(570885),I=e.i(589057),_=e.i(763408),O=e.i(340677),P=e.i(197546),D=e.i(86821),L=e.i(82832),z=e.i(475923),F=e.i(49908),H=e.i(186970),M=e.i(939260),j=e.i(28157),B=e.i(103906),G=e.i(767600),W=e.i(70784),U=e.i(726497),U=U,q=e.i(822086),X=e.i(453198),Y=e.i(448863),$=U,V=e.i(755146);function K(e){if(!e||e.length<2)return!1;let t=0,n=0;for(let r=0;r<e.length;r++){let a=(e[r].text||"").replace(/\s+/g,"");if(a.length>1)return!1;1===a.length&&n++;let o=(e[r].transform||"").trim().toLowerCase();o&&"none"!==o&&/(rotate|translate|matrix|skew|scale|perspective)\s*\(/.test(o)&&t++}return n>=2&&t>=2}function Z(e,t){let n=Array.from(e||""),r=n.length;if(0===r)return"";let a=t.transforms||[],o=a.length,i=a.map(function(e){let t=[];if(!e)return t;let n=/([a-zA-Z]+)\s*\(([^)]*)\)/g,r=n.exec(e);for(;null!==r;){let a=r[2].split(",").map(e=>{let t=e.trim(),n=Number.parseFloat(t),r=t.replace(/^[+-]?[0-9]*\.?[0-9]+(?:[eE][+-]?[0-9]+)?/,"").trim();return{num:Number.isFinite(n)?n:0,unit:r}});t.push({name:r[1],parts:a}),r=n.exec(e)}return t}),s=[];for(let e=0;e<r;e++){let d=r>1?e/(r-1):0,c="";if(1===o)c=a[0];else if(o>1){let e=d*(o-1),t=Math.floor(e),n=Math.min(o-1,t+1),r=e-t;c=(function(e,t,n){return!function(e,t){if(e.length!==t.length)return!1;for(let n=0;n<e.length;n++)if(e[n].name!==t[n].name||e[n].parts.length!==t[n].parts.length)return!1;return!0}(e,t)?n<.5?e:t:e.map((e,r)=>({name:e.name,parts:e.parts.map((e,a)=>({num:e.num+(t[r].parts[a].num-e.num)*n,unit:e.unit||t[r].parts[a].unit}))}))})(i[t],i[n],r).map(e=>`${e.name}(${e.parts.map(e=>{var t;return(Number.isFinite(t=e.num)?String(Math.round(1e3*t)/1e3):"0")+e.unit}).join(", ")})`).join(" ")}s.push(function(e,t,n){let r=e.tag||"span",a=e.styleRest||"";a&&";"!==a.charAt(a.length-1)&&(a+=";"),n&&(a+=`transform:${n}`);let o="";e.className&&(o+=` class="${l(e.className)}"`),a&&(o+=` style="${l(a)}"`);let i=" "===t?"&nbsp;":l(t);return`<${r}${o}>${i}</${r}>`}(t,n[e],c))}return s.join("");function l(e){return e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}}function J(e){let t=e[0]||{tag:"span",className:"",styleRest:""};return{tag:t.tag||"span",className:t.className||"",styleRest:t.styleRest||"",transforms:e.map(e=>e.transform||"")}}function Q(e){if(!e)return{transform:"",rest:""};let t=e.split(";"),n="",r=[];for(let e of t){let t=e.indexOf(":");if(t<0){e.trim()&&r.push(e.trim());continue}let a=e.slice(0,t).trim().toLowerCase(),o=e.slice(t+1).trim();"transform"===a?n=o:a&&r.push(`${a}: ${o}`)}return{transform:n,rest:r.join("; ")}}var ee=e.i(701532);let et=`
2  // ── FAIL-CLOSED IMAGE GUARD ──────────────────────────────────────────────
3  // The renderer already keeps its own reserved slots honest (schema-render.ts
4  // emits alt="" while src is empty) and the patch path below keeps a failed fill
5  // honest. Neither covers every way a broken src reaches this document:
6  //
7  //   • a REBUILD (page switch, undo/redo, reload) seats alt AND src straight from
8  //     the stored document — no patch runs, so a url that fails there is a glyph
9  //     nothing else is watching;
10  //   • an HTML-authored composition is raw model output. Its <img> tags carry no
11  //     data-cid and their srcs are whatever the author typed — a relative
12  //     "logo.png", a plausible-looking CDN that does not exist. This frame is
13  //     sandboxed WITHOUT allow-same-origin, i.e. an OPAQUE origin with no base to
14  //     resolve against, so a relative src cannot resolve even in principle.
15  //
16  // So the guard lives on the DOCUMENT, downstream of every one of those paths,
17  // and keys off the only signal that is never wrong: the image told us it failed.
18  // A picture that loads never fires \`error\`, so this cannot touch legitimate art.
19  function disarmBrokenImage(el) {
20    if (!el || el.tagName !== "IMG") return;
21    // An alt is what the engine draws the torn-page glyph FOR — an empty alt means
22    // decorative, so there is nothing to stand in for and it paints the bare box.
23    if (el.getAttribute("alt")) el.setAttribute("alt", "");
24    // A NON-SLOT image (no data-cid) is not an asset box the parent can ever
25    // patch, so its src will never improve — take it out of the paint entirely.
26    // \`visibility\`, not \`display\`: the box stays, so a layout built around it does
27    // not reflow into a different composition than the one that was authored.
28    if (!el.hasAttribute("data-cid")) el.style.visibility = "hidden";
29  }
30  // Capture: resource \`error\` events do not bubble, but they do capture.
31  document.addEventListener("error", function (e) { disarmBrokenImage(e.target); }, true);
32  // The listener only sees failures that happen AFTER it is installed (this script
33  // is injected at the end of the body, and a cached failure can settle before
34  // that) and a body swap can graft in markup whose images have already failed. So
35  // sweep the settled ones too, wherever the markup changes.
36  function sweepBrokenImages() {
37    var list = document.getElementsByTagName("img");
38    for (var i = 0; i < list.length; i++) {
39      var im = list[i];
40      // \`complete\` with NO natural size is a settled failure; a picture still in
41      // flight is not \`complete\`, so a merely slow image is never disarmed. An
42      // empty src is \`complete\` too, and is the reserved slot — it already carries
43      // alt="" and a data-cid, so disarming it is a no-op by construction.
44      if (im.complete && !im.naturalWidth) disarmBrokenImage(im);
45    }
46  }
47
48  // Warm a url and call back once it is DECODED (fully rasterized), so the swap
49  // never paints a half-drawn plate. Falls back to load/error, and to an immediate
50  // callback when the browser has it cached. Always settles exactly once, and
51  // never blocks.
52  //
53  // IT REPORTS WHETHER THE BYTES ACTUALLY ARRIVED (\`done(ok)\`), and it RETRIES
54  // before saying no. Both are the fix for the defect Andy kept hitting: a deck
55  // whose assets are fully generated (and render fine everywhere else in the app)
56  // showing the broken-image glyph in Compose forever. Two properties of this
57  // surface conspire to make ONE transient fetch failure permanent:
58  //
59  //   • an <img> that has errored NEVER re-requests on its own, and
60  //   • the pane deliberately never rebuilds a live frame (that is the whole point
61  //     of the patch path — a rebuild is the black flash),
62  //
63  // so nothing after the failure ever asks for those bytes again. A deck's slots
64  // fill as a BATCH (one burst once every asset is generated), which is exactly the
65  // moment a frame fires N multi-megabyte requests at once — the likeliest moment
66  // for one of them to be reset. Retry is therefore not politeness; it is the only
67  // re-request this document will ever get.
68  var WARM_RETRY_MS = [600, 1800];
69  function warmAsset(url, done) {
70    var settled = false;
71    var attempt = 0;
72    function finish(ok) { if (settled) return; settled = true; done(!!ok); }
73    function fail() {
74      if (settled) return;
75      if (attempt >= WARM_RETRY_MS.length) { finish(false); return; }
76      setTimeout(attemptWarm, WARM_RETRY_MS[attempt++]);
77    }
78    function attemptWarm() {
79      if (settled) return;
80      var probe = new Image();
81      probe.onload = function () { finish(true); };
82      probe.onerror = fail;
83      try { probe.src = url; } catch (err) { fail(); return; }
84      if (probe.decode) { try { probe.decode().then(function () { finish(true); }, fail); } catch (err) { /* onload covers it */ } }
85      // A cached hit is \`complete\` synchronously. A NATURAL SIZE is what tells a
86      // decoded picture apart from a settled failure (a broken image is also
87      // \`complete\`), so it gates the early success — otherwise a cached 404 would
88      // report \`ok\` and re-seat the alt that paints the glyph.
89      if (probe.complete && probe.naturalWidth > 0) finish(true);
90    }
91    if (!url) { finish(false); return; }
92    attemptWarm();
93  }
94`;var en=e.i(198459);let er=["nw","ne","se","sw"];function ea(e,t,n){if(!(n>0))return null;let r=e.left+e.width,a=e.top+e.height;return"nw"===t?{left:e.left-n,top:e.top-n,width:n,height:n}:"ne"===t?{left:r,top:e.top-n,width:n,height:n}:"se"===t?{left:r,top:a,width:n,height:n}:"sw"===t?{left:e.left-n,top:a,width:n,height:n}:null}function eo(e,t,n,r,a,o,i,s){let l=180*Math.atan2(a-n,r-t)/Math.PI,d=e+(180*Math.atan2(i-n,o-t)/Math.PI-l);return s>0&&(d=Math.round(d/s)*s),0==(d=(d%360+360)%360)?0:d}function ei(e,t){let n=[],r=Math.round((Number.isFinite(e)?e:0)*100)/100;return 0!==r&&n.push(`rotate(${r}deg)`),t&&n.push("scaleX(-1)"),n.join(" ")}function es(e,t,n,r,a){if(!a)return{x:e,y:t};let o=a*Math.PI/180,i=Math.cos(o),s=Math.sin(o),l=e-n,d=t-r;return{x:n+l*i-d*s,y:r+l*s+d*i}}function el(e,t,n,r){return[e,t,n,r].every(e=>Number.isFinite(e))?(180*Math.atan2(-n,r)/Math.PI%360+360)%360:0}var ed=e.i(932287),ec=e.i(850846),eh=e.i(994893);let eu=(0,e.i(768834).create)(e=>({pending:null,setPending:t=>e({pending:t}),clear:()=>e({pending:null})}));e.s(["REF_TEXT_MAX",0,160,"SNIPPET_MAX",0,2e3,"serializeComposeRef",0,function(e){return["```compose-element-ref",JSON.stringify({type:e.type,title:e.title,html:e.html,selector:e.selector,snippet:e.snippet,tag:e.tag,...e.text?{text:e.text.slice(0,160)}:{},...e.alt?{alt:e.alt.slice(0,160)}:{},...e.count&&e.count>1?{count:e.count}:{}}),"```"].join("\n")},"useComposeSelection",0,eu],672619);let ep="ponpon-compose-frame",em="ponpon-compose-host",ef=["rectangle","line","arrow","ellipse","polygon","star"],eg=[{kind:"rectangle",key:"r",shift:!1,label:"R"},{kind:"line",key:"l",shift:!1,label:"L"},{kind:"arrow",key:"l",shift:!0,label:"⇧L"},{kind:"ellipse",key:"o",shift:!1,label:"O"}];function eb(e){if(e.metaKey||e.ctrlKey||e.altKey)return null;var t=e.key.toLowerCase();return"r"===t?e.shiftKey?null:"rectangle":"l"===t?e.shiftKey?"arrow":"line":"o"===t?e.shiftKey?null:"ellipse":null}function ey(e,t){e?.postMessage({source:em,...t},"*")}let ev=new Set(["pan","selection","selection-rect","selection-style","text-commit","style-commit","doc-size","insert-commit","wheel","undo","redo","copy","paste","file-drop","remove","crop-frame","tool-change","exit"]),ew="M6 17 Q6 6 12 6 Q18 6 18 17 M3 13.5 L6 17.5 L9 13.5 M15 13.5 L18 17.5 L21 13.5";function ex(e){let t=`<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24'><g transform='rotate(${e} 12 12)' fill='none' stroke-linecap='round' stroke-linejoin='round'><path d='${ew}' stroke='#ffffff' stroke-width='4.5'/><path d='${ew}' stroke='#000000' stroke-width='2'/></g></svg>`;return`url("data:image/svg+xml,${encodeURIComponent(t)}") 12 12, grabbing`}let eE=["text","rectangle","ellipse","line"];function ek(e){return eE.includes(e)}var eS=e.i(885100);let eT=n.GLYPH_PX,eA=[{kind:"rectangle",label:"Rectangle",Icon:q.Square},{kind:"line",label:"Line",Icon:r.Minus},{kind:"arrow",label:"Arrow",Icon:H.ArrowUpRight},{kind:"ellipse",label:"Ellipse",Icon:B.Circle},{kind:"polygon",label:"Polygon",Icon:$.IconArrowTriangleTop},{kind:"star",label:"Star",Icon:X.Star}];function eR(e){return eg.find(t=>t.kind===e)?.label??""}function eN({checked:e}){return(0,t.jsx)("span",{className:"flex w-3.5 shrink-0 items-center justify-center","aria-hidden":!0,children:e?(0,t.jsx)(M.Check,{size:13}):null})}function eC({children:e}){return e?(0,t.jsx)("span",{className:"shrink-0 text-caption text-muted-foreground",children:e}):null}function eI({label:e,children:n}){return(0,t.jsx)(i.IconButton,{label:e,title:"Create a page first",disabled:!0,noHitAreaExtension:!0,className:h.flowIconButton,children:n})}function e_({onInsert:e,onInsertImage:n,tool:r,onToolChange:o,pagesView:l,hasPage:d=!0,shapeKind:c="rectangle"}){let u=(0,m.useTranslations)("Flow"),p=(0,s.useViewerReadOnly)(),b=(0,f.useRef)(null),y=(ek(r)&&"text"!==r?r:null)??c,v=(eA.find(e=>e.kind===y)??eA[0]).Icon,w=ek(y)?y:null,x=null!==w&&r===w,E=w?eS.TOOL_SHORTCUT[w].toLowerCase():void 0,k=u("hand"===r?"hand":"select"),S=l&&(l.canShowAll||l.total>1)?l:null;return(0,t.jsxs)("div",{className:`pointer-events-auto ${h.flowBottomToolbar}`,children:[d?(0,t.jsx)(i.IconButton,{label:k,title:k,"aria-keyshortcuts":"v h","aria-pressed":"hand"===r,onClick:()=>o((0,eS.toggleTool)(r)),noHitAreaExtension:!0,className:h.flowIconButton,children:"hand"===r?(0,t.jsx)(W.HandGrab,{size:eT}
94):(0,t.jsx)(G.Cursor,{size:eT})}):(0,t.jsx)(eI,{label:k,children:"hand"===r?(0,t.jsx)(W.HandGrab,{size:eT}):(0,t.jsx)(G.Cursor,{size:eT})}),p?null:d?(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(i.IconButton,{label:"Text",title:"Text (T)","aria-keyshortcuts":"t","aria-pressed":"text"===r,onClick:()=>o("text"===r?"select":"text"),noHitAreaExtension:!0,className:h.flowIconButton,children:(0,t.jsx)(Y.TextT,{size:eT})}),(0,t.jsxs)("div",{className:"flex flex-row items-center",children:[(0,t.jsx)(i.IconButton,{label:"Shape",title:`Shape (${eR(y)||"R"})`,"aria-keyshortcuts":E,"aria-pressed":x,noHitAreaExtension:!0,className:h.flowIconButton,onClick:()=>{w?o(x?"select":w):e(y)},children:(0,t.jsx)(v,{size:eT})}),(0,t.jsxs)(V.DropdownMenu,{children:[(0,t.jsx)(V.DropdownMenuTrigger,{render:(0,t.jsx)(i.IconButton,{label:"Shape options",title:"Shape options",noHitAreaExtension:!0,className:h.flowPreviewToolChevron,children:(0,t.jsx)(j.ChevronDown,{size:8})})}),(0,t.jsx)(V.DropdownMenuContent,{align:"center",sideOffset:10,className:"w-56",children:(0,t.jsx)(V.DropdownMenuGroup,{children:eA.map(({kind:n,label:r,Icon:a})=>(0,t.jsxs)(V.DropdownMenuItem,{className:"gap-2","data-shape-row":n,"data-current":n===c?"true":void 0,onClick:()=>e(n),children:[(0,t.jsx)(eN,{checked:n===c}),(0,t.jsx)(a,{size:15}),(0,t.jsx)("span",{className:"flex-1",children:r}),(0,t.jsx)(eC,{children:eR(n)})]},n))})})]})]}),(0,t.jsx)(i.IconButton,{label:"Add image",title:"Add image",noHitAreaExtension:!0,className:h.flowIconButton,onClick:()=>b.current?.click(),children:(0,t.jsx)(a.Plus,{size:eT})})]}):(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(eI,{label:"Text",children:(0,t.jsx)(Y.TextT,{size:eT})}),(0,t.jsx)(eI,{label:"Shape",children:(0,t.jsx)(q.Square,{size:eT})}),(0,t.jsx)(eI,{label:"Add image",children:(0,t.jsx)(a.Plus,{size:eT})})]}),S&&(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)("span",{className:"mx-1 h-5 w-px bg-border","aria-hidden":!0}),(0,t.jsxs)("div",{className:"flex flex-row items-center gap-0.5",children:[S?.canShowAll&&(0,t.jsx)(i.IconButton,{label:u("all"===S.view?"pagesViewThumbnails":"pagesViewAll"),title:u("all"===S.view?"pagesViewThumbnails":"pagesViewAll"),"aria-pressed":"thumbnails"===S.view,onClick:S.onToggle,noHitAreaExtension:!0,className:h.flowIconButton,children:(0,t.jsx)(U.IconLayersBehind,{size:eT})}),S&&S.total>1&&(0,t.jsxs)("span",{className:"px-1 text-caption tabular-nums text-foreground","aria-live":"polite",children:[S.index," / ",S.total]})]})]}),(0,t.jsx)("input",{ref:b,type:"file",accept:"image/*",className:"hidden",onChange:e=>{(e=>{if(!e)return;if(!e.type.startsWith("image/"))return(0,g.toast)("Pick an image file");let t=new FileReader;t.onload=()=>{"string"==typeof t.result&&n(t.result)},t.readAsDataURL(e)})(e.target.files?.[0]),e.target.value=""}})]})}var eO=e.i(960197),eP=e.i(688666),eD=U,eL=e.i(952819),ez=e.i(256754),eF=e.i(622070),eH=e.i(673692),eM=U;e.s(["Underline",()=>eM.IconUnderline],209228);var eM=U,ej=e.i(736643),eB=e.i(337822),eG=e.i(197257),eW=e.i(330777),eU=e.i(751921);let eq=[12,14,16,18,20,24,28,32,40,48,56,64,80,96,128];function eX({current:e,onPick:n,side:r}){let[a,o]=(0,f.useState)(!1);return(0,t.jsxs)(eB.Popover,{open:a,onOpenChange:o,children:[(0,t.jsx)(eB.PopoverTrigger,{render:(0,t.jsxs)(ej.Button,{variant:"ghost",size:"sm",className:(0,l.cn)(h.flowBarControl,"min-w-[128px] max-w-[168px] justify-between gap-1.5"),title:"Font",children:[(0,t.jsx)("span",{className:"truncate text-body",children:e||"Font"}),(0,t.jsx)(j.ChevronDown,{size:14,className:"shrink-0 opacity-60"})]})}),(0,t.jsx)(eB.PopoverContent,{align:"start",side:r,sideOffset:6,className:"w-64 gap-0 p-0",children:(0,t.jsx)(eO.FontPickerList,{current:e,onPick:e=>{n(e),o(!1)}})})]})}function eY({value:e,onChange:n,side:r}){let[a,o]=(0,f.useState)(!1),[i,s]=(0,f.useState)(String(e)),d=(0,f.useRef)(null),c=(0,f.useRef)(null),u=(0,f.useMemo)(()=>eq.reduce((t,n)=>Math.abs(n-e)<Math.abs(t-e)?n:t,eq[0]),[e]),p=(0,f.useCallback)(e=>{let t=e?.offsetParent;e&&t&&(t.scrollTop=e.offsetTop-t.clientHeight/2+e.offsetHeight/2)},[]),m=(0,f.useRef)(!1);(0,f.useEffect)(()=>{m.current||s(String(e))},[e]);let g=t=>{let r=Number.parseInt(t,10);if(Number.isFinite(r)&&r>0){let e=Math.min(999,Math.max(1,r));s(String(e)),n(e)}else s(String(e))};return(0,t.jsxs)("div",{className:"flex items-center gap-0.5",children:[(0,t.jsx)("button",{type:"button",title:"Decrease size",onClick:()=>n(Math.max(1,e-1)),className:(0,l.cn)(h.flowBarControl,"w-6 px-0 text-title"),children:"−"}),(0,t.jsxs)("div",{ref:d,className:"relative flex items-center",children:[(0,t.jsx)("input",{ref:c,onClick:()=>o(!0),value:i,onChange:e=>s(e.target.value.replace(/[^\d]/g,"")),onFocus:()=>{m.current=!0},onBlur:()=>{m.current=!1,g(i)},onKeyDown:t=>{"Enter"===t.key?(t.preventDefault(),g(i),t.target.blur()):"Escape"===t.key&&(s(String(e)),t.target.blur())},inputMode:"numeric",title:"Font size",className:"h-8 w-11 rounded-[var(--radius-bar-control)] border border-border bg-card text-center text-body text-foreground outline-none focus:border-ring"}),(0,t.jsx)(eB.Popover,{open:a,onOpenChange:o,children:(0,t.jsx)(eB.PopoverContent,{anchor:d,initialFocus:c,align:"center",side:r,sideOffset:6,className:"w-24 p-0",children:(0,t.jsx)("div",{className:"relative max-h-[240px] overflow-y-auto py-1",children:eq.map(r=>(0,t.jsxs)("button",{ref:r===u?p:void 0,type:"button",onClick:()=>{s(String(r)),n(r),o(!1)},className:`flex w-full items-center justify-between gap-2 px-3 py-2 text-left text-[15px] leading-none transition-colors hover:bg-accent ${r===e?"bg-accent font-medium text-foreground":"text-foreground"}`,children:[(0,t.jsx)("span",{children:r}),r===e&&(0,t.jsx)(M.Check,{size:13,className:"shrink-0 opacity-70"})]},r))})})})]}),(0,t.jsx)("button",{type:"button",title:"Increase size",onClick:()=>n(e+1),className:(0,l.cn)(h.flowBarControl,"w-6 px-0 text-title"),children:"+"})]})}function e$(){return(0,t.jsx)("span",{className:"mx-0.5 h-5 w-px bg-border","aria-hidden":!0})}function eV({selection:e,onApplyStyle:n,onPreviewStyle:r,onFontSelect:a,menuSide:o="bottom",imageTools:s,imageOnly:d=!1}){let c,u=e.style,p=Number.isFinite(c=Number.parseFloat(u.fontSize))?Math.round(c):16,m=Number.parseInt(u.fontWeight,10)>=600,f="italic"===u.fontStyle,g=/underline/.test(u.textDecorationLine),b="start"===u.textAlign?"left":u.textAlign,y=(0,eU.primaryFamilyOf)(u.fontFamily),v=(0,eW.isTransparent)(u.backgroundColor),w=(e,r,a)=>(0,t.jsx)(i.IconButton,{label:a,title:a,noHitAreaExtension:!0,"aria-pressed":b===e,onClick:()=>n({"text-align":e}),className:h.flowIconButton,children:r});return(0,t.jsxs)("div",{className:(0,l.cn)(h.flowNodeFeatureBar,"pointer-events-auto"),children:[!d&&e.text&&(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(eG.ColorSwatch,{label:"Text color",variant:"disc",side:o,color:u.color,onChange:e=>n({color:e}),onPreview:e=>r({color:e})}),(0,t.jsx)(e$,{}),(0,t.jsx)(eX,{current:y,onPick:a,side:o}),(0,t.jsx)(e$,{}),(0,t.jsx)(eY,{value:p,side:o,onChange:e=>n({"font-size":`${e}px`})}),(0,t.jsx)(e$,{}),(0,t.jsx)(i.IconButton,{label:"Bold",title:"Bold",noHitAreaExtension:!0,"aria-pressed":m,onClick:()=>n({"font-weight":m?"400":"700"}),className:h.flowIconButton,children:(0,t.jsx)(eF.Bold,{size:18})}),(0,t.jsx)(i.IconButton,{label:"Italic",title:"Italic",noHitAreaExtension:!0,"aria-pressed":f,onClick:()=>n({"font-style":f?"normal":"italic"}),className:h.flowIconButton,children:(0,t.jsx)(eH.Italic,{size:18})}),(0,t.jsx)(i.IconButton,{label:"Underline",title:"Underline",noHitAreaExtension:!0,"aria-pressed":g,onClick:()=>n({"text-decoration-line":g?"none":"underline"}),className:h.flowIconButton,children:(0,t.jsx)(eM.IconUnderline,{size:18})}),(0,t.jsx)(e$,{}),w("left",(0,t.jsx)(eL.AlignLeft,{size:18}),"Align left"),w("center",(0,t.jsx)(eP.AlignCenter,{size:18}),"Align center"),w("right",(0,t.jsx)(ez.AlignRight,{size:18}),"Align right"),w("justify",(0,t.jsx)(eD.IconAlignmentJustify,{size:18}),"Justify")]}),!d&&e.fill&&(0,t.jsxs)(t.Fragment,{children:[e.text&&(0,t.jsx)(e$,{}),(0,t.jsx)(eG.ColorSwatch,{label:e.text?"Fill color":"Fill",variant:"disc",side:o,color:u.backgroundColor,empty:v,onChange:e=>n({"background-color":e}),onPreview:e=>r({"background-color":e})})]}),e.image&&s&&(0,t.jsxs)(t.Fragment,{children:[!d&&(e.text||e.fill)&&(0,t.jsx)(e$,{}),s]}),!d&&e.count>1&&(0,t.jsxs)("span",{className:"ml-1 mr-0.5 text-caption text-muted-foreground",children:[e.count," selected"]})]})}var eK=e.i(372660);function eZ({mode:e,tools:n,onClose:r}){return"text-edit"===e?(0,t.jsx)(O.TextEditPanel,{state:n.textState,readingHeight:n.readingHeight,onEditedChange:n.setEdited,onCancel:n.revertAll,onApply:n.applyText}):"crop"===e?(0,t.jsx)(eK.CropSidePanel,{natW:n.dims?.w??0,natH:n.dims?.h??0,cropSettings:n.cropSettings,onCropSettingsChange:n.setCropSettings,onApply:e=>{n.actions.handleCrop(e),r()}}):null}var eJ=e.i(445859),eQ=e.i(97507),e0=e.i(373931),e1=e.i(495104),e2=e.i(562617),e5=e.i(847753),e6=e.i(418298),e4=e.i(308985),e3=e.i(624141),e8=e.i(1066),e7=e.i(198218),e9=e.i(381272),te=e.i(284333),tt=e.i(576048),tn=e.i(409237),tr=e.i(616593),ta=e.i(454106),to=e.i(32727);function ti({mode:e,onToggleMode:r,dims:a,onUpscale:o,onRemoveBg:i,onRotate:s,onFlip:l,layerizing:d,layerizeDisabledReason:c,onLayerize:u,busy:p,onSaveToLibrary:f,onDownload:g,downloading:b,onDelete:y}){let v=(0,m.useTranslations)("Flow"),w=(0,m.useTranslations)("SaveToLibrary"),x=(0,m.useTranslations)("Upscale"),E=p?v("layerizing"):void 0,k=eK.EDIT_TOOLBAR_BTN_BASE,S=eK.EDIT_TOOLBAR_BTN_ACTIVE,T=!!c||d||p;return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(to.LayerizePriceTooltip,{dims:a?{width:a.w,height:a.h}:null,children:(0,t.jsxs)("button",{type:"button",disabled:T,"aria-busy":d,onClick:u,className:`${k} disabled:cursor-not-allowed disabled:opacity-40`,title:T?E??c:void 0,children:[d?(0,t.jsx)(e4.Loader2,{size:n.GLYPH_PX,className:"animate-spin"}):(0,t.jsx)(e6.LayersTwo,{size:n.GLYPH_PX}),(0,t.jsx)("span",{children:v(d?"layerizing":"layerize")})]})}),(0,t.jsxs)(V.DropdownMenu,{children:[(0,t.jsxs)(V.DropdownMenuTrigger,{disabled:!a||p,className:`${k} disabled:cursor-not-allowed disabled:opacity-40`,title:E??v("upscale"),children:[(0,t.jsx)(e5.Hd,{size:n.GLYPH_PX})," ",v("upscale")]}),(0,t.jsx)(V.DropdownMenuContent,{align:"center",sideOffset:8,className:"min-w-[120px]",onPointerDown:e=>e.stopPropagation(),children:["2","4"].map(e=>{let n=!!a&&(0,ta.outputMegapixels)(a.w,a.h,Number(e))>ta.MAX_OUTPUT_MP;return(0,t.jsxs)(V.DropdownMenuItem,{disabled:n,onClick:()=>o(e),className:"justify-between gap-3 text-xs",children:[e,"x",n?(0,t.jsx)("span",{className:"ml-auto whitespace-nowrap text-[10px] text-muted-foreground opacity-50",children:x("outputTooLarge",{max:ta.MAX_OUTPUT_MP})}):(0,t.jsxs)("span",{className:"ml-auto flex items-center gap-0.5 text-[10px] text-muted-foreground opacity-50",children:[(0,t.jsx)(tn.Zap,{size:10}),(0,tr.getUpscaleCost)(a?.w??0,a?.h??0,Number(e))??"--"]})]},e)})})]}),(0,t.jsxs)("button",{type:"button",onClick:i,className:k,title:v("removeBgTooltip"),children:[(0,t.jsx)(e8.RemoveBackgroun
94d,{size:n.GLYPH_PX})," ",v("removeBg")]}),(0,t.jsxs)("button",{type:"button",disabled:!a||p,onClick:()=>r("crop"),className:`${"crop"===e?S:k} disabled:cursor-not-allowed disabled:opacity-40`,title:E??v("cropTooltip"),children:[(0,t.jsx)(eJ.Crop,{size:n.GLYPH_PX})," ",v("crop")]}),(0,t.jsxs)(eK.EditToolbarOverflowMenu,{children:[(0,t.jsxs)(V.DropdownMenuItem,{disabled:p,onClick:()=>r("text-edit"),className:"text-edit"===e?"text-foreground":void 0,children:[(0,t.jsx)(te.TextEdit,{size:14})," ",v("editText")]}),(0,t.jsx)(V.DropdownMenuSeparator,{}),(0,t.jsxs)(V.DropdownMenuItem,{disabled:p,onClick:()=>s(90),children:[(0,t.jsx)(e9.RotateCw,{size:14})," ",v("rotateCw")]}),(0,t.jsxs)(V.DropdownMenuItem,{disabled:p,onClick:()=>s(-90),children:[(0,t.jsx)(e7.RotateCcw,{size:14})," ",v("rotateCcw")]}),(0,t.jsxs)(V.DropdownMenuItem,{disabled:p,onClick:()=>l("h"),children:[(0,t.jsx)(e0.FlipHorizontal2,{size:14})," ",v("flipH")]}),(0,t.jsxs)(V.DropdownMenuItem,{disabled:p,onClick:()=>l("v"),children:[(0,t.jsx)(e1.FlipVertical2,{size:14})," ",v("flipV")]})]}),(0,t.jsx)("span",{className:"mx-0.5 h-5 w-px bg-border","aria-hidden":!0}),(0,t.jsx)("button",{type:"button",onClick:f,className:h.flowNodeIconButton,"aria-label":w("action"),title:w("action"),children:(0,t.jsx)(e2.Folder2,{size:n.GLYPH_PX})}),(0,t.jsx)("button",{type:"button",onClick:g,disabled:b,"aria-busy":b,className:`${h.flowNodeIconButton} disabled:cursor-default`,"aria-label":v("download"),title:v("download"),children:b?(0,t.jsx)(e3.LoadingArc,{size:n.GLYPH_PX,className:"animate-spin"}):(0,t.jsx)(eQ.Download,{size:n.GLYPH_PX})}),(0,t.jsx)("button",{type:"button",onClick:y,className:h.flowNodeIconButton,"aria-label":v("delete"),title:v("delete"),children:(0,t.jsx)(tt.Trash2,{size:n.GLYPH_PX})})]})}var ts=e.i(226189),tl=e.i(789235),td=e.i(160649),tc=e.i(347337),th=e.i(945875),tu=e.i(793479),tp=e.i(803550);function tm({size:e=16,className:n,children:r}){return(0,t.jsx)("svg",{width:e,height:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round","aria-hidden":"true",focusable:"false",className:n,children:r})}function tf(e){return(0,t.jsxs)(tm,{...e,children:[(0,t.jsx)("path",{d:"M4.5 4.5h15"}),(0,t.jsx)("path",{d:"M12 20V8.5"}),(0,t.jsx)("path",{d:"M7 13.5 12 8.5l5 5"})]})}function tg(e){return(0,t.jsxs)(tm,{...e,children:[(0,t.jsx)("path",{d:"M4.5 19.5h15"}),(0,t.jsx)("path",{d:"M12 4v11.5"}),(0,t.jsx)("path",{d:"M7 10.5 12 15.5l5-5"})]})}function tb({frame:e,selectedCids:r,disabled:a=!1,unsupported:o=!1,busyCids:s=[],onSelect:d,onMove:c,onAction:u,onRename:p}){let[m,g]=(0,tp.usePersistedState)("ponpon.compose.layers.collapsed",!1),b=(0,f.useRef)(null),[y,v]=(0,f.useState)(!1);(0,f.useLayoutEffect)(()=>{let e=b.current?.parentElement;if(!e||"u"<typeof ResizeObserver)return;let t=()=>v(e.clientWidth>0&&e.clientWidth<640);t();let n=new ResizeObserver(t);return n.observe(e),()=>n.disconnect()},[]);let w=[...e?.components??[]].sort((e,t)=>e.z-t.z).reverse(),x=!a&&0===s.length,E=m||y,k=(0,f.useRef)(null),S=(0,f.useRef)(null),T=(0,f.useRef)(!1),A=(0,f.useRef)(null),[R,N]=(0,f.useState)(null),[C,I]=(0,f.useState)(null),[_,O]=(0,f.useState)(null),P=(0,f.useRef)(null),D=(0,f.useRef)(!1);(0,f.useLayoutEffect)(()=>(S.current?.(),I(null),O(null),()=>S.current?.()),[e,x,E]),(0,f.useLayoutEffect)(()=>()=>{A.current&&clearTimeout(A.current)},[]);let L=_?.cid;(0,f.useLayoutEffect)(()=>{L&&(P.current?.focus(),P.current?.select())},[L]);let z=()=>{_&&!D.current&&(D.current=!0,x&&p?.(_.cid,_.draft.trim()),O(null))};return(0,t.jsx)("div",{ref:b,className:E?"contents":"flex w-60 shrink-0",children:E?(0,t.jsx)(i.IconButton,{label:y?"Widen the window to show layers":"Show layers",title:y?"Widen the window to show layers":"Show layers",disabled:y,noHitAreaExtension:!0,onClick:()=>g(!1),className:(0,l.cn)("absolute left-2 top-2 z-30 bg-card shadow-[var(--shadow-float)]",h.flowIconButton),children:(0,t.jsx)(tc.PanelLeftPane,{size:n.GLYPH_PX})}):(0,t.jsxs)("aside",{"aria-label":"Layers",className:(0,l.cn)("flex min-h-0 w-full flex-col border-r border-border bg-background",n.GLYPH_STROKE),children:[(0,t.jsxs)("div",{className:"flex min-h-11 shrink-0 items-center gap-2 border-b border-border px-3",children:[(0,t.jsx)("span",{className:"text-body font-medium text-foreground",children:"Layers"}),(0,t.jsx)(i.IconButton,{label:"Hide layers",title:"Hide layers",noHitAreaExtension:!0,onClick:()=>g(!0),className:(0,l.cn)("ml-auto",h.flowIconButton),children:(0,t.jsx)(tc.PanelLeftPane,{size:n.GLYPH_PX})})]}),(0,t.jsx)("div",{ref:k,"data-layers-scroll":!0,className:"min-h-0 flex-1 overflow-y-auto overscroll-contain p-2",children:0===w.length?(0,t.jsx)("p",{className:"px-1 py-3 text-caption text-muted-foreground",children:o?"Layer ordering is available for native Design elements. Select an element to edit it.":"No layers yet"}):(0,t.jsx)("ul",{"aria-label":"Design layers",className:"space-y-0.5",children:w.map((o,s)=>{let h=e?.components.findIndex(e=>e.cid===o.cid)??0,m=function(e,t){if(e.name?.trim())return e.name.trim();if("text"===e.type)return e.content.trim()||`Text ${t+1}`;if("image"===e.type)return e.alt?.trim()||`Image ${t+1}`;let n="rect"===e.shape?"Rectangle":e.shape;return`${n[0].toUpperCase()}${n.slice(1)} ${t+1}`}(o,h),f="image"===o.type?tl.ImageIcon:"text"===o.type?Y.TextT:q.Square,g=R?.gap===s||s===w.length-1&&R?.gap===w.length;return(0,t.jsxs)("li",{className:"group/layer relative",children:[g&&(0,t.jsx)("div",{"aria-hidden":"true","data-layer-insertion":R?.gap,className:(0,l.cn)("pointer-events-none absolute inset-x-0 z-10 h-0.5 bg-[var(--flow-select)]",R?.gap===w.length?"-bottom-px":"-top-px")}),_?.cid===o.cid?(0,t.jsx)("div",{className:"flex h-10 items-center px-2",children:(0,t.jsx)(tu.Input,{ref:P,"aria-label":"Layer name",value:_.draft,maxLength:120,onChange:e=>O({..._,draft:e.target.value}),onBlur:z,onKeyDown:e=>{e.stopPropagation(),"Enter"!==e.key||e.nativeEvent.isComposing||(e.preventDefault(),z()),"Escape"===e.key&&(e.preventDefault(),D.current=!0,O(null))},className:"h-7 min-w-0 px-1 text-body"})}):(0,t.jsxs)(t.Fragment,{children:[(0,t.jsxs)(ej.Button,{variant:"ghost",size:"sm","data-layer-cid":o.cid,"aria-pressed":r.includes(o.cid),"aria-keyshortcuts":"Alt+ArrowUp Alt+ArrowDown",title:m,disabled:a,onPointerDown:e=>((e,t)=>{if(0!==e.button||!x||w.length<2||_)return;S.current?.(),T.current=!1;let n=k.current;if(!n)return;let r=e.pointerId,a=e.currentTarget,o=e.clientX,i=e.clientY,s=w.findIndex(e=>e.cid===t),l=o,d=i,h=!1,u=null,p=0,m=()=>{let e=n.getBoundingClientRect();if(l<e.left||l>e.right||d<e.top||d>e.bottom)u=null;else{let e=Array.from(n.querySelectorAll("[data-layer-cid]")),t=e.findIndex(e=>{let t=e.getBoundingClientRect();return d<t.top+t.height/2});u=t<0?e.length:t}N({cid:t,gap:u})},f=()=>{if(!h)return;let e=n.getBoundingClientRect();if(l>=e.left&&l<=e.right&&d>=e.top&&d<=e.bottom){let t=Math.min(32,e.height/3),r=d<e.top+t?-8:8*(d>e.bottom-t);r&&(n.scrollTop+=r,m())}p=requestAnimationFrame(f)},g=()=>{cancelAnimationFrame(p),window.removeEventListener("pointermove",b),window.removeEventListener("pointerup",y),window.removeEventListener("pointercancel",v),window.removeEventListener("keydown",E,!0),window.removeEventListener("blur",v),a.removeEventListener("lostpointercapture",v),a.hasPointerCapture?.(r)&&a.releasePointerCapture(r),h&&(T.current=!0,A.current&&clearTimeout(A.current),A.current=setTimeout(()=>{T.current=!1},0)),S.current=null,N(null)},b=e=>{e.pointerId!==r||(l=e.clientX,d=e.clientY,!h&&5>Math.hypot(l-o,d-i)||(h||(h=!0,a.setPointerCapture?.(r),p=requestAnimationFrame(f)),e.preventDefault(),m()))},y=e=>{if(e.pointerId!==r)return;l=e.clientX,d=e.clientY,h&&m();let n=null===u?null:u>s?u-1:u,a=h&&null!==n&&n!==s;g(),a&&c(t,n)},v=()=>g(),E=e=>{"Escape"===e.key&&(e.preventDefault(),e.stopPropagation(),g())};S.current=g,window.addEventListener("pointermove",b,{passive:!1}),window.addEventListener("pointerup",y),window.addEventListener("pointercancel",v),window.addEventListener("keydown",E,!0),window.addEventListener("blur",v),a.addEventListener("lostpointercapture",v)})(e,o.cid),onDragStart:e=>e.preventDefault(),onKeyDown:e=>{if(!e.altKey||"ArrowUp"!==e.key&&"ArrowDown"!==e.key)return;e.preventDefault(),e.stopPropagation();let t=s+("ArrowUp"===e.key?-1:1);x&&t>=0&&t<w.length&&c(o.cid,t)},onClick:e=>{if(T.current){T.current=!1,e.preventDefault();return}d(o.cid)},className:(0,l.cn)("w-full select-none justify-start gap-2 rounded-[var(--radius-inspector-control)] py-0 pl-2 pr-8 text-body font-normal aria-pressed:bg-muted","h-10",x&&"touch-none cursor-grab",R?.cid===o.cid&&"cursor-grabbing opacity-40"),children:["image"===o.type&&o.src?(0,t.jsx)("img",{src:o.src,alt:"",loading:"lazy",decoding:"async",draggable:!1,
94className:"size-6 shrink-0 rounded-sm bg-muted object-cover"}):(0,t.jsx)(f,{size:n.GLYPH_PX,className:"shrink-0 text-muted-foreground"}),(0,t.jsx)("span",{className:"min-w-0 truncate",children:m})]}),(0,t.jsxs)(V.DropdownMenu,{open:C===o.cid,onOpenChange:e=>{S.current?.(),I(e&&!a?o.cid:null),e&&!a&&d(o.cid)},children:[(0,t.jsx)(V.DropdownMenuTrigger,{render:(0,t.jsx)(i.IconButton,{label:`Options for ${m}`,size:"icon-xs",noHitAreaExtension:!0,disabled:a,"data-layer-options":o.cid,className:(0,l.cn)("absolute right-1 top-2 size-6 text-muted-foreground hover:bg-transparent hover:text-foreground focus-visible:text-foreground [&_svg]:size-4.5",C===o.cid?"opacity-100":"opacity-0 group-hover/layer:opacity-100 group-focus-within/layer:opacity-100"),children:(0,t.jsx)(td.MoreHorizontal,{size:n.GLYPH_PX})})}),(0,t.jsxs)(V.DropdownMenuContent,{align:"start",side:"right",finalFocus:!_&&void 0,children:[(0,t.jsxs)(V.DropdownMenuItem,{disabled:a||!u,onClick:()=>u?.(o.cid,"copy"),children:[(0,t.jsx)(ts.Copy,{}),"Copy"]}),(0,t.jsxs)(V.DropdownMenuItem,{disabled:!x||0===s||!u,onClick:()=>u?.(o.cid,"front"),children:[(0,t.jsx)(tf,{}),"Bring to front"]}),(0,t.jsxs)(V.DropdownMenuItem,{disabled:!x||s===w.length-1||!u,onClick:()=>u?.(o.cid,"back"),children:[(0,t.jsx)(tg,{}),"Send to back"]}),(0,t.jsxs)(V.DropdownMenuItem,{disabled:!x||!p,onClick:()=>{D.current=!1,O({cid:o.cid,draft:m})},children:[(0,t.jsx)(th.Pencil,{}),"Rename"]}),(0,t.jsx)(V.DropdownMenuSeparator,{}),(0,t.jsxs)(V.DropdownMenuItem,{variant:"destructive",disabled:!x||!u,onClick:()=>u?.(o.cid,"delete"),children:[(0,t.jsx)(tt.Trash2,{}),"Delete"]})]})]})]})]},o.cid)})})})]})})}e.s(["LayerBackward",0,function(e){return(0,t.jsxs)(tm,{...e,children:[(0,t.jsx)("path",{d:"M12 4v15"}),(0,t.jsx)("path",{d:"M6.5 13.5 12 19l5.5-5.5"})]})},"LayerForward",0,function(e){return(0,t.jsxs)(tm,{...e,children:[(0,t.jsx)("path",{d:"M12 20V5"}),(0,t.jsx)("path",{d:"M6.5 10.5 12 5l5.5 5.5"})]})},"LayerToBack",0,tg,"LayerToFront",0,tf],726975);let ty={active:!1,title:null,phase:"idle",isEdit:!1,startedAt:0,lastGrowthAt:0};function tv(e){return"string"==typeof e&&e.trim()?e.trim():null}function tw(e,t){switch(t.type){case"start":return{active:!0,title:tv(t.title),phase:t.isEdit?"streaming":"thinking",isEdit:t.isEdit,startedAt:t.now,lastGrowthAt:t.now};case"grow":if(!e.active)return e;return{...e,phase:"streaming",title:tv(t.title)??e.title,lastGrowthAt:t.now};case"alive":if(!e.active)return e;return{...e,title:tv(t.title)??e.title,lastGrowthAt:t.now};case"tick":if(!e.active||"stalled"===e.phase)return e;if(t.now-e.lastGrowthAt>=2e4)return{...e,phase:"stalled"};return e;case"finish":return ty}}function tx(e){if(!e.active)return{kind:"none"};switch(e.phase){case"thinking":return{kind:"thinking",text:"Thinking through the design…"};case"stalled":return{kind:"stalled",text:"Still working…"};case"streaming":return{kind:"streaming"};default:return{kind:"none"}}}function tE({composing:e}){let n=(0,m.useTranslations)("Flow"),r=tx(e),a=e.title,o=a?n("composeComposing",{title:a}):n("composeAssembling"),i="thinking"===r.kind||"stalled"===r.kind?r.text:"";return(0,t.jsxs)("div",{className:"absolute inset-0 flex flex-col items-center justify-center bg-muted px-8",children:[(0,t.jsxs)("div",{className:"relative w-[min(78%,24rem)] aspect-[4/5] overflow-hidden rounded-none border border-border bg-card dark:shadow-[var(--shadow-tile)]",children:[(0,t.jsx)("div",{className:"absolute inset-0 shimmer","aria-hidden":"true"}),(0,t.jsxs)("div",{className:"absolute inset-0 flex flex-col gap-3 p-[8%]","aria-hidden":"true",children:[(0,t.jsx)("div",{className:"h-2.5 w-1/3 rounded-full bg-foreground
94/5"}),(0,t.jsx)("div",{className:"mt-auto h-7 w-3/4 rounded-md bg-foreground/5"}),(0,t.jsx)("div",{className:"h-7 w-1/2 rounded-md bg-foreground/5"}),(0,t.jsx)("div",{className:"mt-1 h-2.5 w-2/5 rounded-full bg-foreground/5"})]})]}),(0,t.jsx)("p",{className:"mt-5 max-w-[28rem] text-center text-body font-medium text-foreground",children:o}),(0,t.jsx)("p",{className:"mt-1 text-caption text-muted-foreground","aria-live":"polite","data-compose-phase":e.phase,children:i})]})}e.s(["composingCaption",0,tx,"idleComposingState",0,ty,"reduceComposing",0,tw],438397);var tk=e.i(393741);function tS(e,t,n){return e<t?t:e>n?n:e}function tT(e,t){let{panX:n,panY:r,zoom:a,boardX:o=0,boardY:i=0,boardScale:s=1}=t;return{left:n+(o+e.left*s)*a,top:r+(i+e.top*s)*a,width:e.width*s*a,height:e.height*s*a}}function tA(e){return e<0?0:e>100?100:e}function tR(e,t){return{x:(e.x-t.panX)/t.zoom,y:(e.y-t.panY)/t.zoom}}var tN=e.i(269812);let tC="doc-web";var tI=e.i(588006),t_=e.i(618154),tO=e.i(499714);function tP(e,t){return{identity:t.identity,page:t.page,tick:t.tick,srcDoc:t.wanted,shown:t.wanted,nonce:e.nonce+1,patches:null,body:null}}function tD(e,t,n){return{...e,page:t.page,tick:t.tick,shown:"none"===n.kind?e.shown:t.wanted,patches:"patch"===n.kind?n.patches:null,body:"body"===n.kind?{seq:(e.body?.seq??0)+1,html:n.html}:null}}var tL=e.i(502325);function tz(e){let t;if(!e.trim()||"u"<typeof DOMParser)return null;try{t=new DOMParser().parseFromString(e,"text/html")}catch{return null}return e=>{let n;try{n=t.querySelector(e)}catch{return null}return n?{outer:n.outerHTML,inner:n.innerHTML}:null}}var tF=e.i(154752),tH=e.i(641599),tM=e.i(539019),tj=e.i(918774);function tB(e){if(!e)return null;let t=(0,tF.parseDesignClipboardHtml)(e.getData?.("text/html")??"");if("components"===t.kind)return t;if("invalid"===t.kind)return null;let n=[],r=new Set,a=e=>{e&&"string"==typeof e.type&&e.type.startsWith("image/")&&!r.has(e)&&(r.add(e),n.push(e))};if(e.files)for(let t of Array.from(e.files))a(t);if(e.items)for(let t of Array.from(e.items))"file"===t.kind&&a(t.getAsFile());if(n.length>0)return{kind:"images",files:n};let o=(e.getData?.("text/plain")??"").replace(/\r\n?/g,"\n").trim();return o?{kind:"text",text:o.length>tF.COMPOSE_PASTE_TEXT_MAX?o.slice(0,tF.COMPOSE_PASTE_TEXT_MAX):o}:null}var tG=e.i(550585),tW=e.i(666697),tU=e.i(232246),tq=e.i(964858);function tX(e){let t;try{t=JSON.parse(e)}catch{return null}let n=(0,tN.parseFramesEnvelope)(t);return n&&n.frames.length>0?n:null}function tY(e){let t=(0,tq.parseCompositionPages)(e);return 0===t.pages.length?null:t.pages.map(e=>({id:e.id,title:e.title,html:e.html}))}function t$(e,t){return JSON.stringify({kind:"pages",pages:e.map(e=>({id:e.id,title:e.title,html:e.html})),activePageId:t})}function tV(e,t){if(!e)return"refuse";let n=function(e){if((0,tN.looksLikeFramesEnvelope)(e.html)){let t=tX(e.html);return t?t.frames.map(e=>e.id):[]}return(0,tq.parseCompositionPages)(e).pages.map(e=>e.id)}(e);return 0!==n.length&&n.includes(t)?n.length>1?"page":"empty":"refuse"}var tK=e.i(818136),tZ=e.i(594476),tJ=e.i(121332),tQ=e.i(565214);let t0=[{presetId:"deck-16x9",name:"Presentation",ratio:"16:9"},{presetId:"poster-2x3",name:"Poster",ratio:"2:3"},{presetId:"social-9x16",name:"Story",ratio:"9:16"},{presetId:"social-1x1",name:"Square",ratio:"1:1"},{presetId:"case-3x2",name:"Landscape",ratio:"3:2"},{presetId:"doc-a4",name:"Document",ratio:"A4"},{presetId:"case-3x1",name:"Banner",ratio:"3:1"}];function t1(e,t=40){let n=tN.FRAME_PRESETS[e];return n.width>=n.height?{width:t,height:Math.round(t*n.height/n.width)}:{width:Math.round(t*n.width/n.height),height:t}}function t2({onPick:e,size:n="sm",className:r}){let[a,o]=(0,f.useState)(!1),i="lg"===n;return a?(0,t.jsx)("div",{"data-first-page-formats":"all",className:(0,l.cn)("flex max-h-80 flex-col overflow-y-auto",r),children:tN.FRAME_PRESET_IDS.map(n=>{let r=tN.FRAME_PRESETS[n];return(0,t.jsxs)("button",{type:"button","data-format-preset":n,onClick:()=>e(n),className:(0,l.cn)("flex h-8 w-full shrink-0 cursor-pointer items-center justify-between gap-3 rounded-lg px-2.5 text-left","text-[13px] text-muted-foreground transition-surface hover:bg-accent hover:text-foreground"),children:[(0,t.jsx)("span",{className:"truncate",children:r.label}),(0,t.jsxs)("span",{className:"shrink-0 text-[11px] tabular-nums text-muted-foreground",children:[r.width," × ",r.height]})]},n)})}):(0,t.jsxs)("div",{"data-first-page-formats":"grid",className:(0,l.cn)("grid grid-cols-3",i?"gap-2":"gap-1",r),children:[t0.map(n=>{var r;return(0,t.jsxs)("button",{type:"button","data-format-preset":n.presetId,onClick:()=>e(n.presetId),className:(0,l.cn)("flex cursor-pointer flex-col items-center gap-1.5 rounded-[var(--radius-tile-sm)] text-center","transition-surface hover:bg-hover-surface","focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",i?"px-2 py-3":"px-1 py-2"),children:[(0,t.jsx)("span",{"aria-hidden":!0,
94className:"flex size-10 shrink-0 items-center justify-center",children:(0,t.jsx)("span",{"data-format-tile-box":"",className:"block border border-border",style:{...t1(n.presetId),borderRadius:Math.max(2,Math.min(4,Math.round(Math.min((r=t1(n.presetId)).width,r.height)/10)))}})}),(0,t.jsx)("span",{className:"min-w-0 max-w-full truncate text-[13px] leading-none text-foreground",children:n.name}),(0,t.jsx)("span",{className:"text-[11px] leading-none tabular-nums text-muted-foreground",children:n.ratio})]},n.presetId)}),(0,t.jsxs)("button",{type:"button","data-format-more":"",onClick:()=>o(!0),className:(0,l.cn)("flex cursor-pointer flex-col items-center gap-1.5 rounded-[var(--radius-tile-sm)] text-center","transition-surface hover:bg-hover-surface","focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",i?"px-2 py-3":"px-1 py-2"),children:[(0,t.jsx)("span",{"aria-hidden":!0,className:"flex size-10 shrink-0 items-center justify-center text-muted-foreground",children:(0,t.jsx)(td.MoreHorizontal,{size:18})}),(0,t.jsx)("span",{className:"min-w-0 max-w-full truncate text-[13px] leading-none text-foreground",children:"More…"}),(0,t.jsx)("span",{"aria-hidden":!0,className:"text-[11px] leading-none",children:" "})]})]})}var t5=e.i(516320);function t6(e){let t=Math.min(112,72*e);return{width:t,height:t/e}}function t4({page:e,index:r,active:a,eager:o,onSelect:i,actions:s}){let d=(0,f.useRef)(null),[c,h]=(0,f.useState)(o),[u,p]=(0,f.useState)(!1),[m,g]=(0,f.useState)(!1),b=(0,tK.decodeHtmlEntities)(e.title),[y,v]=(0,f.useState)(b||`Page ${r+1}`);(0,f.useEffect)(()=>{let e=d.current;if(!e||c)return;let t=new IntersectionObserver(([e])=>{e.isIntersecting&&(h(!0),t.disconnect())},{rootMargin:"200px"});return t.observe(e),()=>t.disconnect()},[c]);let w=(0,f.useMemo)(()=>(0,t5.pageSheetBox)(e.html),[e.html]),x=w?.width??1280,E=w?.height??800,k=(0,t5.thumbBoxAspect)(w,1.6),S=t6(k),T=S.width,A=Math.min(T/x,S.height/E),R=(0,tJ.previewSurfaceRung)(T),N=function(){let[e,t]=(0,f.useReducer)(e=>e+1,0);return(0,f.useEffect)(()=>(0,tJ.subscribeLayerImageVerdicts)(t),[]),e}(),C=(0,f.useMemo)(()=>{var t,n;let r,a,o;return n=t=(0,tJ.rewritePreviewHtml)(e.html,{width:R,dpr:1,priority:"low"}),a=(r=(0,tU.readPageShell)(n))&&!r.sheetColor&&r.bodyColor?`${r.selector}{background:${r.bodyColor}}`:"",o=`<style>html,body{background:transparent !important}${a}</style>`,/<\/head>/i.test(t)?t.replace(/<\/head>/i,`${o}</head>`):o+t},[e.html,R,N]);(0,f.useEffect)(()=>{c&&(0,tJ.warmPreviewHtml)(C,{width:R,dpr:1})},[c,C,R]);let I=()=>{g(!1);let t=y.trim();t&&t!==b&&s?.onRename(e.id,t)};return(0,t.jsxs)("div",{className:"group relative w-28 shrink-0",children:[(0,t.jsxs)("button",{type:"button",onClick:i,"data-page-thumb":e.id,"aria-current":a?"true":void 0,"aria-label":`Page ${r+1}${b?` — ${b}`:""}`,title:b||`Page ${r+1}`,className:(0,l.cn)("flex w-full flex-col items-center gap-1 rounded-[var(--radius-tile-sm)] text-center outline-none transition-surface","focus-visible:ring-[3px] focus-visible:ring-ring/50"),children:[(0,t.jsx)("div",{className:"flex h-18 w-full items-center justify-center",children:(0,t.jsx)("div",{ref:d,"data-page-preview":"",className:(0,l.cn)("relative w-full overflow-hidden rounded-none bg-muted transition-shadow",a?"shadow-[0_0_0_2px_var(--flow-select)]":"shadow-[0_0_0_0.5px_var(--border)] group-hover:shadow-[0_0_0_0.5px_var(--input)]"),style:{...S,aspectRatio:String(k)},children:c?(0,t.jsx)("div",{className:"absolute inset-0 flex items-center justify-center",children:(0,t.jsx)("div",{className:"relative shrink-0",style:{width:x*A,he
94ight:E*A},children:(0,t.jsx)(tQ.BufferedThumbnail,{html:C,title:b||`Page ${r+1}`,logicalWidth:x,logicalHeight:E,scale:A,fetchPriority:"low"})})}):(0,t.jsx)("div",{className:"absolute inset-0 bg-muted"})})}),m?(0,t.jsx)("input",{autoFocus:!0,value:y,onChange:e=>v(e.target.value),onClick:e=>e.stopPropagation(),onKeyDown:e=>{e.stopPropagation(),"Enter"===e.key?I():"Escape"===e.key&&g(!1)},onBlur:I,className:"w-full rounded-[var(--radius-sm)] border border-ring bg-card px-1 py-0.5 text-center text-caption leading-none text-foreground outline-none"}):(0,t.jsxs)("span",{className:(0,l.cn)("max-w-full truncate text-[length:var(--text-caption)] font-medium leading-none",a?"text-foreground":"text-muted-foreground"),children:[r+1,b?` \xb7 ${b}`:""]})]}),s&&!m&&(0,t.jsxs)(V.DropdownMenu,{open:u,onOpenChange:p,children:[(0,t.jsx)(V.DropdownMenuTrigger,{render:(0,t.jsx)("button",{type:"button","aria-label":`Page ${r+1} options`,onClick:e=>e.stopPropagation(),className:(0,l.cn)("absolute right-1 top-1 flex size-7 items-center justify-center rounded-[var(--radius-sm)] text-muted-foreground shadow-border outline-none backdrop-blur transition-surface",n.GLYPH_STROKE,"bg-[color-mix(in_oklab,var(--card)_90%,transparent)]","hover:bg-card hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50",u?"opacity-100":"opacity-0 group-hover:opacity-100 group-focus-within:opacity-100"),children:(0,t.jsx)(td.MoreHorizontal,{size:n.GLYPH_PX})})}),(0,t.jsxs)(V.DropdownMenuContent,{side:"top",align:"end",sideOffset:8,className:"w-44",children:[(0,t.jsxs)(V.DropdownMenuItem,{onClick:()=>{p(!1),v(b||`Page ${r+1}`),g(!0)},children:[(0,t.jsx)(th.Pencil,{size:15}),"Rename"]}),(0,t.jsxs)(V.DropdownMenuItem,{onClick:()=>s.onDuplicate(e.id),children:[(0,t.jsx)(ts.Copy,{size:15}),"Duplicate"]}),(0,t.jsxs)(V.DropdownMenuItem,{onClick:()=>s.onDownload(e.id),children:[(0,t.jsx)(eQ.Download,{size:15}),"Download PNG"]}),(0,t.jsx)(V.DropdownMenuSeparator,{}),(0,t.jsxs)(V.DropdownMenuItem,{variant:"destructive",disabled:!s.canDelete,title:s.canDelete?void 0:"A composition keeps at least one page",onClick:()=>s.canDelete&&s.onDelete(e.id),children:[(0,t.jsx)(tt.Trash2,{size:15}),"Delete"]})]})]})]})}function t3({pages:e,activePageId:n,onSelect:r,onAddPage:a,onAddFirstPageAtFormat:o,addDisabledReason:i=null,actions:s}){let l=(0,f.useRef)(null),d=(0,f.useMemo)(()=>{let t=e.length>0?e[e.length-1]:void 0;return(0,t5.thumbBoxAspect)(t?(0,t5.pageSheetBox)(t.html):null,1.6)},[e]);return(0,f.useEffect)(()=>{let e=l.current;if(!e||!n)return;let t=Array.from(e.querySelectorAll("[data-page-thumb]")).find(e=>e.dataset.pageThumb===n);if(!t)return;let r=e.ownerDocument.activeElement;r&&r!==t&&e.contains(r)&&t.focus({preventScroll:!0});let a=e.getBoundingClientRect(),o=t.getBoundingClientRect(),i=function(e,t,n,r){if(n<e)return n;let a=n+r;return a>e+t?Math.min(n,a-t):e}(e.scrollLeft,e.clientWidth,o.left-a.left+e.scrollLeft,o.width);i!==e.scrollLeft&&(e.scrollLeft=i)},[n]),(0,t.jsx)("nav",{"aria-label":"Pages",className:"h-31 min-w-0 shrink-0 overflow-hidden border-t border-border bg-background",children:(0,t.jsx)("div",{ref:l,className:"scrollbar-hover h-full overflow-x-auto overflow-y-hidden",children:(0,t.jsxs)("div",{className:"mx-auto flex h-full w-max min-w-full items-center justify-center gap-4 px-4 py-3",children:[e.map((e,a)=>(0,t.jsx)(t4,{page:e,index:a,active:e.id===n,eager:a<8,onSelect:()=>r(e.id),actions:s},e.id)),a&&(0,t.jsx)(t8,{onAdd:a,onPickFormat:o,disabledReason:i,aspect:d})]})})})}function t8({onAdd:e,onPickFormat:n,disabledReason:r,aspect:o}){let[i,s]=(0,f.useState)(!1),d=null!==r,c=(0,t.jsxs)("button",{type:"button",onClick:n?void 0:e,title:r??"Add page","aria-label":"Add page",disabled:d,className:(0,l.cn)("group flex w-28 shrink-0 flex-col items-center gap-1 rounded-[var(--radius-tile-sm)] text-center outline-none transition-surface","focus-visible:ring-[3px] focus-visible:ring-ring/50","disabled:cursor-not-allowed disabled:opacity-40"),children:[(0,t.jsx)("div",{className:"flex h-18 w-full items-center justify-center",children:(0,t.jsx)("div",{"data-page-preview":"",className:(0,l.cn)("flex items-center justify-center rounded-[var(--radius-tile-sm)] border border-dashed border-border text-muted-foreground transition-surface",!d&&"group-hover:border-foreground
94/25 group-hover:bg-hover-surface group-hover:text-foreground"),style:{...t6(o),aspectRatio:String(o)},children:(0,t.jsx)(a.Plus,{size:18})})}),(0,t.jsx)("span",{className:"text-caption font-medium leading-none text-muted-foreground",children:"Add page"})]});return n?(0,t.jsxs)(eB.Popover,{open:i,onOpenChange:s,children:[(0,t.jsx)(eB.PopoverTrigger,{render:c}),(0,t.jsx)(eB.PopoverContent,{side:"top",align:"start",sideOffset:8,className:"w-72 gap-0 p-2",children:(0,t.jsx)(t2,{size:"sm",onPick:e=>{s(!1),n(e)}})})]}):c}async function t7(e){if("function"==typeof createImageBitmap)try{let t=await createImageBitmap(e),n={w:t.width,h:t.height};if(t.close?.(),n.w>0&&n.h>0)return n}catch{}if("u"<typeof URL||"u"<typeof Image)return null;let t=URL.createObjectURL(e);try{return await new Promise(e=>{let n=new Image;n.onload=()=>e({w:n.naturalWidth,h:n.naturalHeight}),n.onerror=()=>e(null),n.src=t})}finally{URL.revokeObjectURL(t)}}async function t9(e,t){let n=[];for(let r of e){let e=await t.measure(r).catch(()=>null),a=t.insert(e);a&&(t.setUploading(a,!0),n.push({cid:a,file:r}))}await Promise.all(n.map(e=>ne(e,t)))}async function ne(e,t){try{let n=await t.upload(e.file);t.swap(e.cid,n)}catch(n){t.rollback(e.cid),t.onUploadFailed(n)}finally{t.setUploading(e.cid,!1)}}var nt=e.i(832772);function nn(e,t){let n=e.match(RegExp(`${t}\\s*=\\s*"([^"]*)"`,"i"));return n?n[1]:null}var nr=e.i(539398);let na={desktop:960,mobile:390};var no=e.i(594690),ni=e.i(116040),ns=e.i(390841),nl=e.i(64720),nd=e.i(263019),nc=e.i(751149),nh=e.i(602095),nu=e.i(78002),np=e.i(730142);let nm={x:0,y:0};var nf=e.i(133139);async function ng(e){try{let t=await fetch("/api/blitz-generate/extract-text",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({imageUrl:e.imageUrl}),signal:e.signal});if(!t.ok){let e=await t.json().catch(()=>null);return 429===t.status&&(0,nf.reportGenerationConcurrencyError)(e),{texts:[],error:"string"==typeof e?.error?e.error:"Could not read the image"}}let n=await t.json();return{texts:Array.isArray(n.texts)?n.texts.filter(e=>"string"==typeof e):[]}}catch(e){if(e instanceof DOMException&&"AbortError"===e.name)return{texts:[]};return{texts:[],error:e instanceof Error?e.message:"Could not read the image"}}}var nb=e.i(535146),ny=e.i(279761);e.i(247167);var nv=e.i(313686),nw=e.i(918707),nx=e.i(979952);let nE=new Map,nk=new Map;function nS(e){let t=nE.get(e);return t?{...t}:null}function nT(e){return"preview"===e?"page":"assets"===e?"assets":"canvas"===e?"canvas":e}e.s(["adoptDraftTab",0,function(e,t,n){let r=nE.get(e);return!!r&&((0,nv.notifyDraftMaterialized)({draftId:e,docId:t,docKind:nT(r.kind),version:0}),(0,nv.forgetDraftBody)(e),nE.delete(e),n({type:"rekey",fromId:e,toId:t,kind:r.kind}),!0)},"forgetDraftTab",0,function(e){nE.delete(e),(0,nv.forgetDraftBody)(e)},"newDraftTabId",0,function(){let e="u">typeof crypto&&"function"==typeof crypto.randomUUID?crypto.randomUUID():`${Date.now().toString(16)}-${Math.random().toString(16).slice(2)}`;return`${nx.DRAFT_TAB_PREFIX}${e}`},"readDraftTabSpec",0,nS,"registerDraftTab",0,function(e,t){nE.set(e,t)},"setDraftTabTitle",0,function(e,t){let n=nE.get(e);n&&nE.set(e,{...n,title:t})},"useDraftTabMaterializer",0,function(e,t){let n=(0,f.useRef)(t);n.current=t,(0,f.useEffect)(()=>{var t;if(!e)return;let r=(t=e=>n.current(e),function(n){let r=nk.get(n);if(r)return r;let a=nE.get(n);if(!a)return Promise.resolve(null);let o=(0,ny.buildCompositionCreateBody)(a.kind,a.format);if(!o)return Promise.resolve(null);let i=(async()=>{let r=(0,nv.readDraftBody)(n),i={...o,...r?.html!==void 0?{html:r.html}:{},...r?.sceneJson!==void 0?{sceneJson:r.sceneJson}:{},...r?.assetRefs?{assetRefs:[...r.assetRefs]}:{}},s=a.title?.trim()||r?.title?.trim();s&&(i.title=s);let l=await (0,ny.postComposition)(e,i);return(0,nb.forgetComposition)(`/api/projects/${encodeURIComponent(e)}/compositions/${encodeURIComponent(l.id)}`),(0,nv.notifyDraftMaterialized)({draftId:n,docId:l.id,docKind:nT(a.kind),version:l.version}),(0,nv.forgetDraftBody)(n),nE.delete(n),t({type:"rekey",fromId:n,toId:l.id,kind:a.kind}),(0,nw.refreshProjectDocumentMetadata)(e),l.id})().catch(e=>(console.warn("[drafts] could not materialize a draft tab",e),null)).finally(()=>{nk.get(n)===i&&nk.delete(n)});return nk.set(n,i),i});return(0,nv.setDraftMaterializer)(r,e),()=>(0,nv.clearDraftMaterializer)(r)},[e])}],236256);var nA=e.i(514219);let nR=new Set(["script","style","textarea","title"]);function nN(e,t){let n=e.length,r=t+1,a=!1;"/"===e[r]&&(a=!0,r++);let o=r;for(;r<n&&/[a-zA-Z0-9:-]/.test(e[r]);)r++;let i=e.slice(o,r).toLowerCase(),s=null;for(;r<n;){let t=e[r];if(s){t===s&&(s=null),r++;continue}if('"'===t||"'"===t){s=t,r++;continue}if(">"===t)return{end:r+1,name:i,isEnd:a,selfClosing:"/"===e[r-1]};r++}return null}let nC=/^&[a-zA-Z0-9#]*$/,nI="ponpon.compose.v0";function n_(){try{let e=window.localStorage.getItem(nI);if(!e)return{compositions:[],activeId:null};let t=JSON.parse(e);return{compositions:Array.isArray(t.compositions)?t.compositions:[],activeId:"string"==typeof t.activeId?t.activeId:null}}catch{return{compositions:[],activeId:null}}}let nO=["page","poster","deck","film","draw"];function nP(e){return nO.includes(e)?e:"page"}let nD=/^(?:https?:)?\/\//i,nL=new Set(["src","url","poster","dataURL"]);
94function nz(e,t=[]){let n=new Set,r=e=>{"string"==typeof e&&nD.test(e.trim())&&n.add(e.trim())},a=e=>e.replace(/&(#x[\da-f]+|#\d+|amp|quot|apos|lt|gt);/gi,(e,t)=>{if(!t.startsWith("#"))return({amp:"&",quot:'"',apos:"'",lt:"<",gt:">"})[t.toLowerCase()]??e;let n="x"===t[1].toLowerCase()?Number.parseInt(t.slice(2),16):Number(t.slice(1));return n>0&&n<=1114111?String.fromCodePoint(n):e}),o=e=>{let t=e.replace(/<!--[\s\S]*?-->|<script\b[^>]*>[\s\S]*?<\/script\s*>/gi,""),n=e=>{for(let t of e.replace(/\/\*[\s\S]*?\*\//g,"").matchAll(/url\(\s*(?:"([^"]*)"|'([^']*)'|([^\s'"()][^)]*?))\s*\)/gi))r(t[1]??t[2]??t[3])};for(let e of t.matchAll(/<([a-z][\w:-]*)\b(?:[^>"']|"[^"]*"|'[^']*')*>/gi)){let t=["img","video","audio","source","track","image"].includes(e[1].toLowerCase());for(let o of e[0].matchAll(/\s([^\s=/>]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g)){let e=o[1].toLowerCase(),i=a(o[2]??o[3]??o[4]??"");if("style"===e){n(i);continue}if(t&&["src","poster","srcset","href","xlink:href"].includes(e))if("srcset"===e){let e=i;for(;e;){let t=/^[\s,]*(\S+)([\s\S]*)$/.exec(e);if(!t)break;let n=t[1];r(n.replace(/,+$/,""));let a=t[2];if(e=n.endsWith(",")?a:a.slice(a.indexOf(",")+1),!n.endsWith(",")&&!a.includes(","))break}}else r(i)}}for(let e of t.matchAll(/<style\b[^>]*>([\s\S]*?)<\/style\s*>/gi))n(e[1])},i=e.trimStart();if(i.startsWith("{")||i.startsWith("[")){let n;try{n=JSON.parse(e)}catch{return[...new Set(t)]}if(!n||"object"!=typeof n||!["frames","pages","timeline","draw"].includes(n.kind))return[...new Set(t)];let a=[n];for(;a.length;){let e=a.pop();if(e&&"object"==typeof e){for(let[t,n]of Object.entries(e))nL.has(t)?r(n):"html"===t&&"string"==typeof n&&o(n);for(let t of Object.values(e).reverse())t&&"object"==typeof t&&a.push(t)}}}else o(e);let s=new Set(t.filter(e=>!nD.test(e.trim())||n.has(e.trim())));for(let e of n)s.add(e);return[...s]}function nF(e,t){return t?`/api/projects/${encodeURIComponent(e)}/compositions/${encodeURIComponent(t)}`:`/api/projects/${encodeURIComponent(e)}/composition`}async function nH(e,t){let n=await (0,nb.readComposition)(nF(e,t));if("found"!==n.kind)return{kind:n.kind};try{var r,a;let e,{composition:o,version:i}=(r=n.body,e=Date.parse(r.updatedAt),{composition:{...a={id:r.id,title:r.title,kind:nP(r.kind),html:r.html,assetRefs:Array.isArray(r.assetRefs)?r.assetRefs.filter(e=>"string"==typeof e):[],updatedAt:Number.isFinite(e)?e:Date.now()},assetRefs:nz(a.html,a.assetRefs)},version:r.version});if(t&&o.id!==t)return{kind:"unavailable"};return{kind:"found",composition:o,version:i}}catch{return{kind:"unavailable"}}}async function nM(e,t,n,r){let a=nF(e,r);(0,nb.forgetComposition)(a);try{let o=await fetch(a,{method:"PUT",credentials:"same-origin",headers:{"Content-Type":"application/json"},body:JSON.stringify({kind:t.kind,title:t.title,html:t.html,assetRefs:t.assetRefs,version:n})});if(409===o.status){let t=await nH(e,r);if(console.warn("[compose] save conflict (409) — refetched the server composition."),"found"===t.kind)return{kind:"conflict",composition:t.composition,version:t.version};return{kind:"unavailable"}}if(o.ok){let t=await o.json().catch(()=>null);return(0,nw.refreshProjectDocumentMetadata)(e),{kind:"saved",version:"number"==typeof t?.version?t.version:n+1}}return 413===o.status?console.warn("[compose] save rejected (413) — composition html exceeds the 2MB cap."):console.warn(`[compose] save failed (${o.status}) — composition NOT persisted. Content is in-memory only.`),{kind:"unavailable"}}catch{return console.warn("[compose] save failed (network) — composition NOT persisted this attempt."),{kind:"unavailable"}}}async function nj(e,t){try{(await fetch(nF(e,t),{method:"DELETE",credentials:"same-origin"})).ok&&(0,nw.refreshProjectDocumentMetadata)(e)}catch{}}function nB(e){return!!e.hydrated&&!e.composing.active&&(0===e.compositions.length||(0,tq.isEmptyFramesDocument)(e.active??null))}e.s(["isComposeEmptyBoard",0,nB,"useCompositions",0,function(e,t){let n=(0,f.useRef)(null),r=(0,f.useRef)({projectId:e,docId:t,epoch:0});(r.current.projectId!==e||r.current.docId!==t)&&(r.current={projectId:e,docId:t,epoch:r.current.epoch+1});let a=r.current.epoch,o=(0,f.useRef)(a);t&&n.current===t&&(o.current=a);let i=(0,f.useRef)(!0),[s,l]=(0,f.useState)(null),[d,c]=(0,f.useState)(!1),[h,u]=(0,f.useState)("server"),[p,m]=(0,f.useState)(0),[g,b]=(0,f.useState)(0),[y,v]=(0,f.useState)(null),w=(0,f.useRef)(null);w.current=y;let x=(0,f.useRef)(nA.initialComposeRunState),[E,k]=(0,f.useState)(ty),S=(0,f.useCallback)(e=>{k(t=>tw(t,e))},[]),T=(0,f.useRef)(""),A=(0,f.useRef)(null),R=(0,f.useRef)(0),N=(0,f.useRef)(e),C=(0,f.useRef)(t);C.current=t;let I=(0,f.useRef)(e),_=(0,f.useRef)(t),O=(0,f.useRef)(0),P=(0,f.useRef)(0),D=(0,f.useRef)(!1),L=(0,f.useRef)(null),z=(0,f.useRef)(!1),F=(0,f.useRef)(!1),H=(0,f.useRef)(null),M=(0,f.useRef)(!1),j=(0,f.useRef)(0),B=(0,f.useRef)(!1),[G,W]=(0,f.useState)(0),U=(0,f.useRef)({past:[],future:[]}
94),q=(0,f.useRef)(new Map),[X,Y]=(0,f.useState)(!1),[$,V]=(0,f.useState)(!1),K=(0,f.useRef)(null),Z=(0,f.useCallback)(()=>i.current&&null!==t&&r.current.epoch===a&&o.current===a&&I.current===e&&_.current===t,[e,t,a]),J=(0,f.useCallback)(e=>{Z()&&(K.current=e)},[Z]),Q=(0,f.useCallback)(()=>{Y(U.current.past.length>0),V(U.current.future.length>0)},[]),ee=(0,f.useCallback)(()=>{U.current={past:[],future:[]},Y(!1),V(!1)},[]);A.current=s,N.current=e;let et=(0,f.useCallback)(async()=>{if(F.current){M.current=!0;return}F.current=!0;try{for(;;){M.current=!1,L.current&&(clearTimeout(L.current),L.current=null);let e=I.current,t=_.current,n=o.current,a=A.current;if(e&&(0,nx.isDraftTabId)(t)){if(z.current){z.current=!1;let n=await (0,nv.materializeDraft)(t);I.current===e&&_.current!==t?(j.current=0,u("server"),(0,nw.refreshProjectDocumentMetadata)(e)):I.current!==e||n||(z.current=!0,m(e=>e+1),u("local-only"))}}else if(e&&a&&D.current&&z.current){z.current=!1;let s=nM(e,a,R.current,t);H.current={projectId:e,docId:t,promise:s};let d=await s;if(H.current?.promise===s&&(H.current=null),i.current&&o.current===n&&r.current.epoch===n&&I.current===e&&_.current===t)if("saved"===d.kind)R.current=d.version,j.current=0,u("server");else if("conflict"===d.kind){let e=z.current;if(e&&j.current<1)j.current+=1,R.current=d.version,u("server"),M.current=!0;else{R.current=d.version,A.current=d.composition,l(d.composition),W(e=>e+1),u("server");let t=e||j.current>0;j.current=0,z.current=!1,t&&b(e=>e+1)}}else z.current=!0,m(e=>e+1),u("local-only")}if(!M.current||!z.current)return}}finally{F.current=!1}},[]),en=(0,f.useCallback)(()=>{if(z.current=!0,D.current){if((0,nx.isDraftTabId)(_.current)&&!B.current){B.current=!0,L.current&&(clearTimeout(L.current),L.current=null),et();return}L.current&&clearTimeout(L.current),L.current=setTimeout(()=>{et()},800)}},[et]);(0,f.useEffect)(()=>(0,nv.subscribeDraftMaterialized)(({draftId:e,docId:t,docKind:r,version:a})=>{if("page"!==r||_.current!==e)return;_.current=t,C.current=t,R.current=a,D.current=!0;let o=A.current;if(o){let e={...o,id:t};A.current=e,l(e)}n.current=t}),[]),(0,f.useEffect)(()=>{if(t&&n.current===t)return n.current=null,I.current=e,_.current=t,o.current=a,i.current=!0,c(!0),()=>{i.current=!1,et()};L.current&&(clearTimeout(L.current),L.current=null),z.current=!1,D.current=!1,R.current=0,A.current=null,j.current=0,M.current=!1,B.current=!1,I.current=e,_.current=t,o.current=a,i.current=!0,K.current=null;let r=++O.current;if(l(null),w.current=null,T.current="",x.current=nA.initialComposeRunState,v(null),k(ty),ee(),u("server"),m(0),b(0),!e||null===t)return void c(!0);if((0,nx.isDraftTabId)(t)){let e,n;D.current=!0,R.current=0;let r=(e=nS(t),n=(0,ny.buildCompositionCreateBody)("preview",e?.format??""),{id:t,title:e?.title?.trim()||String(n?.title??"Design"),kind:nP(String(n?.kind??"page")),html:String(n?.html??""),assetRefs:[],updatedAt:Date.now()});A.current=r,l(r),c(!0);let a=(0,nv.registerDraftBody)(t,()=>{let e=A.current??r;return{html:e.html,title:e.title,assetRefs:e.assetRefs}});return()=>{a(),et(),i.current=!1}}let s=!1;return c(!1),nH(e,t).then(async n=>{if(s||O.current!==r)return;if("found"===n.kind){if(R.current=n.version,D.current=!0,z.current)et();else{l(n.composition),A.current=n.composition;let r=q.current.get(nF(e,t));r?.html===n.composition.html&&(U.current=r.history,Q())}c(!0);return}if("empty"===n.kind){if(t){D.current=!1,c(!0);return}if(D.current=!0,R.current=0,z.current){et(),c(!0);return}let n=n_().compositions[0]??null;if(n){let r;l(n),A.current=n,F.current=!0;let a=nM(e,n,0,t);H.current={projectId:e,docId:t,promise:a};try{r=await a}finally{H.current?.promise===a&&(H.current=null),F.current=!1}if(s||"saved"!==r.kind)s||"conflict"!==r.kind?s||(m(e=>
94e+1),u("local-only")):(R.current=r.version,l(r.composition),A.current=r.composition);else{R.current=r.version;try{window.localStorage.removeItem(nI)}catch{}}!s&&(M.current||z.current)&&et()}if(s||O.current!==r)return;c(!0);return}D.current=!1,R.current=0,u("local-only");let a=t?null:n_();l(a?.compositions[0]??null),c(!0)}),()=>{s=!0,e&&A.current&&q.current.set(nF(e,t),{html:A.current.html,history:U.current});let n=A.current;if(e&&n&&D.current&&z.current&&F.current){let r=H.current,a=R.current;z.current=!1,(async()=>{let o=r?.projectId===e&&r.docId===t?await r.promise:null,i=o&&"unavailable"!==o.kind?o.version:a;"saved"!==(await nM(e,n,i,t)).kind&&console.warn("[compose] leaving document's queued edit did not persist; review that document before closing the session.")})()}else et();i.current=!1}},[e,t,a,et,ee,Q]);let er=i.current&&r.current.epoch===o.current&&I.current===e&&_.current===t,ea=er?y??s:null,eo=(0,f.useMemo)(()=>ea?[ea]:[],[ea]),ei=(0,f.useCallback)(e=>{},[]),es=(0,f.useCallback)(e=>{if(!Z())return;let{next:t,clearPreview:n}=(0,nA.beginComposeRun)();x.current=t,n&&(w.current=null,T.current="",v(null)),S({type:"start",title:e,isEdit:null!==A.current,now:Date.now()})},[S,Z]),el=(0,f.useCallback)(()=>{Z()&&(w.current=null,v(null),x.current=(0,nA.decideComposeApply)(x.current,"final",!0).next,k(ty))},[Z]),ed=(0,f.useCallback)(e=>{if(w.current)return ee(),!1;let t=A.current;if(!t||t.html===e)return t?.html===e;let n=(0,tG.planScopedApplyFromHtml)(t.html,e);if(n){let e=n.frames.filter(e=>K.current?.(e.ops.redo,e.frameId,t.id)===!0);if(0===n.frames.length||1===e.length){let n=e[0],r={html:t.html};return n&&(r.ops=n.ops,r.page=n.frameId),U.current={past:[...U.current.past,r].slice(-100),future:[]},Q(),!0}}return ee(),!1},[ee,Q]),ec=(0,f.useCallback)(async(e,t=0)=>{let n=I.current;if(!Z()||!n||_.current!==e||z.current||F.current)return!1;let r=O.current,a=++P.current;(0,nb.forgetComposition)(nF(n,e));let o=await nH(n,e);if(!Z()||P.current!==a||O.current!==r||I.current!==n||_.current!==e||C.current!==e||z.current||F.current||"found"!==o.kind||o.composition.id!==e||o.version<Math.max(t,R.current))return!1;O.current+=1;let i=ed(o.composition.html);return R.current=o.version,A.current=o.composition,l(o.composition),D.current=!0,w.current=null,v(null),x.current=(0,nA.decideComposeApply)(x.current,"final",!0).next,k(ty),c(!0),u("server"),i||W(e=>e+1),!0},[Z,ed]),eh=(0,f.useCallback)((e,t)=>{if(!Z()||t?.docId!==void 0&&t.docId!==(_.current??A.current?.id))return null;let n=t?.partial?"partial":t?.immediate?"final":"edit",r=(0,nA.decideComposeApply)(x.current,n,!!t?.immediate);if(x.current=r.next,"partial"===n&&!r.updatePreview)return w.current??A.current;if(r.updatePreview){var a,o;let n=window.location.origin,r=function(e,t="",n){let r={html:"",atomic:!1,floor:t};if(!e||!e.trim())return r;if(e.trimStart().startsWith("{")){try{let r=JSON.parse(e);if(r&&"object"==typeof r&&!Array.isArray(r)&&(0,tq.rendersAnyPage)(e,n))return{html:e,atomic:!0,floor:t}}catch{}return r}let a=function(e){var t,n,r;let a;if(!e)return"";if(e.trimStart().startsWith("{"))return e;if(!e.trim())return"";let o=e.length,i=0,s=0,l=0,d=null,c=0;for(;i<o;){if(d){let t=function(e,t,n){let r=`</${n}`,a=e.toLowerCase(),o=a.indexOf(r,t);for(;-1!==o;){let t=e[o+r.length];if(void 0===t)break;if(">"===t||"/"===t||/\s/.test(t))return o;o=a.indexOf(r,o+r.length)}return -1}(e,i,d);if(-1===t){if("style"===d){let t=e.lastIndexOf("}");if(t>=c)return`${e.slice(0,t+1)}</style>`}break}let n=nN(e,t);if(!n)break;d=null,s=i=n.end;continue}if("<"!==e[i]){l=++i;continue}if(e.startsWith("<!--",i)){let t=e.indexOf("-->",i+4);if(-1===t)break;s=i=t+3;continue}if("!"===e[i+1]||"?"===e[i+1]){let t=e.indexOf(">",i+1);if(-1===t)break;s=i=t+1;continue}if(!(void 0!==(t=e[i+1])&&/[a-zA-Z/]/.test(t))){i++;continue}let n=nN(e,i);if(!n)break;if(!n.isEnd&&!n.selfClosing&&nR.has(n.name)){d=n.name,c=n.end,i=n.end;continue}s=i=n.end}return 0===s?"":e.slice(0,Math.max(s,(n=s,r=l,(a=e.lastIndexOf("&",r-1))<n?r:nC.test(e.slice(a,r))?a:r)))}(e),o=a.length>=t.length?a:t;return{html:o,atomic:!1,floor:o}}(e.html,T.current,n),i=r.html;if(T.current=r.floor,!i)return S({type:"alive",title:e.title,now:Date.now()}),w.current??A.current;let s=(a=A.current,o=t?.page,r.atomic||!o?.pageId?r.html:(0,tq.slotPartialPage)(a,r.html,o,n)??r.html);
94if(w.current&&w.current.html===s)return w.current;let l=A.current,d=l?{...l,title:e.title,html:s,updatedAt:Date.now()}:{id:_.current??w.current?.id??"cmp_composing",title:e.title,kind:"page",html:s,assetRefs:[],updatedAt:Date.now()};return w.current=d,v(d),W(e=>e+1),S({type:"grow",title:e.title,now:Date.now()}),d}let i=r.persist,s=i&&ed(e.html);if(!i&&t?.amend){let t=U.current,n=t.past[t.past.length-1];n&&n.html===e.html&&(t.past=t.past.slice(0,-1),Q())}else if(!i){let n=A.current?.html;if("string"==typeof n&&n!==e.html){let e=U.current,r={html:n};t?.inverse&&(r.ops=t.inverse),t?.editPage&&(r.page=t.editPage),e.past=[...e.past,r].slice(-100),e.future=[],Q()}}let d=function(e,t,n=Date.now()){let r=nz(t.html,t.assetRefs??e?.assetRefs);return e?{...e,kind:t.kind??e.kind,title:t.title,html:t.html,assetRefs:r,updatedAt:n}:{id:`cmp_${n.toString(36)}`,title:t.title,kind:t.kind??"page",html:t.html,assetRefs:r,updatedAt:n}}(A.current??w.current,e);return _.current&&(d.id=_.current),l(d),A.current=d,r.clearPreview&&(w.current=null,v(null)),r.persist?(z.current=!0,et(),s||W(e=>e+1),S({type:"finish"})):en(),d},[en,et,ed,Q,S,Z]);(0,f.useEffect)(()=>{if(!E.active)return;let e=setInterval(()=>{S({type:"tick",now:Date.now()})},2e3);return()=>clearInterval(e)},[E.active,S]);let eu=(0,f.useCallback)(e=>{var t,n;let r;if(!Z()||w.current)return;let a=A.current;if(!a)return;let o=function(e,t,n){if("undo"===t){if(0===e.past.length)return null;let t=e.past[e.past.length-1];return{entry:t,stacks:{past:e.past.slice(0,-1),future:[{html:n,ops:t.ops,page:t.page},...e.future].slice(0,100)}}}if(0===e.future.length)return null;let r=e.future[0];return{entry:r,stacks:{past:[...e.past,{html:n,ops:r.ops,page:r.page}].slice(-100),future:e.future.slice(1)}}}(U.current,e,a.html);if(!o)return;U.current=o.stacks,Q();let i={...a,html:o.entry.html,updatedAt:Date.now()};A.current=i,l(i),z.current=!0,"patch"==(t=o.entry,n=e=>K.current?.(e,o.entry.page,a.id)===!0,(r=t.ops?.[e])&&0!==r.length&&n(r)?"patch":"rebuild")?en():(W(e=>e+1),et())},[et,en,Q,Z]),ep=(0,f.useCallback)(()=>eu("undo"),[eu]),em=(0,f.useCallback)(()=>eu("redo"),[eu]),ef=(0,f.useCallback)(e=>{if(!Z()||e!==A.current?.id)return;let t=I.current;t&&q.current.delete(nF(t,_.current)),L.current&&(clearTimeout(L.current),L.current=null),z.current=!1,M.current=!1,j.current=0,l(null),A.current=null,w.current=null,T.current="",x.current=nA.initialComposeRunState,v(null),k(ty),R.current=0,ee(),t&&D.current&&nj(t,C.current)},[ee,Z]);return{compositions:eo,active:ea,activeId:ea?.id??null,transient:er&&null!==y,startRun:es,composing:er?E:ty,setActiveId:ei,applyIncoming:eh,refreshSaved:ec,finishRun:el,remove:ef,hydrated:er&&d,externalRev:G,undo:ep,redo:em,canUndo:er&&X,canRedo:er&&$,registerPreviewOpSink:J,persistence:h,failedSaves:p,discardedEdits:g}}],512467);let nG="bg-[var(--flow-gallery-canvas)]";function nW(e){return e.map(e=>e.selector)}function nU(e,t,n){if(!e.trim())return"";if(!(0,tL.isDocumentHtml)(e))return console.error("[compose] refused to render a non-document (envelope/JSON) as srcDoc — upstream page resolution produced a `{`-leading string, not an HTML document."),"";let r=getComputedStyle(document.documentElement),a=r?.getPropertyValue("--muted").trim(),o=r?.getPropertyValue("--foreground").trim();return function(e,t,n){let r=e.toLowerCase(),a=r.lastIndexOf("</body>");if(-1!==a)return e.slice(0,a)+t+e.slice(a);let o=r.lastIndexOf("</html>");return -1!==o?e.slice(0,o)+t+e.slice(o):n.partial||!function(e){if(e.lastIndexOf("<")>e.lastIndexOf(">"))return!1;let t=e.toLowerCase();for(let e of["script","style"]){let n=t.lastIndexOf(`<${e}`);if(-1!==n&&t.lastIndexOf(`</${e}`)<n)return!1}return!0}(e)?e:e+t}(e,function(e,t={muted:"oklch(0.269 0 0)",foreground:"oklch(0.985 0 0)"}){let n=JSON.stringify({out:ep,host:em});return`
95<style>
96  /* CONSTANT-SCREEN-SIZE CHROME (⑪): the artboard's CSS scale lives on an ancestor
97     OUTSIDE this frame, so a plain 1px stroke here renders at (1 \xd7 zoom) screen px —
98     blurry/thin at 42%, fat at 150%. We compensate with --ppn-s = 1/scale (posted
99     by the parent on every zoom change), so every stroke/handle is authored in
100     SCREEN px and divided back into frame px → tight, crisp, device-pixel lines at
101     any zoom. Falls back to 1 before the first set-scale (native 100%). */
102  :root { --ppn-s: 1; }
103  /* HOVER HIGHLIGHT (Andy, 2026-08-01): an OVERLAY rect, for the same reason the
104     selection ring below is one. This used to be a CSS \`outline\` on the hovered
105     element, and a CSS outline can only ever trace the BORDER BOX — so on a
106     centred heading in a full-width authored box, hover drew the whole slide
107     width and the selection that followed drew just the word. ("whys that
108     hovering and actually selecting the text box have different dimensions?")
109     Positioned at chromeRect() now, the same one rect selection uses, so what
110     the highlight promises is what the click delivers. \`.__ppn_hover\` stays as
111     the state marker clearHover() tracks, but no longer paints — exactly the
112     arrangement \`.__ppn_selected\` has had since the ring moved out. */
113  .__ppn_hoverbox { position: fixed; z-index: 2147483643; pointer-events: none; box-sizing: border-box; border: calc(1.25px * var(--ppn-s, 1)) solid ${e}; }
114  /* FILE-DRAG LANDING (SUNRA-1220) — the hover ring, said about the PAGE.
115     A file dragged in from the OS is hovering the whole page, not an element in
116     it, so the target is the document's own box: \`fixed\` + \`inset:0\`. Same
117     accent, same overlay construction and the same zoom compensation as the
118     hover box above and the marquee below — the selection-chrome family already
119     means "this is what you are about to act on", and a file drag is one more
120     sentence in it, not a new vocabulary. The stroke weight and the 8% wash are
121     the PARENT's drop ring's (see ComposePane's \`dropHighlight\`), so the two
122     halves of the same surface promise a landing in one voice. */
123  .__ppn_dropring { position: fixed; inset: 0; z-index: 2147483646; pointer-events: none; box-sizing: border-box; border: calc(3px * var(--ppn-s, 1)) solid ${e}; background: color-mix(in srgb, ${e} 8%, transparent); }
124  /* SELECTION RING (Andy, 2026-07-29): an OVERLAY rect, not a CSS \`outline\` on the
125     element. A CSS outline can only ever trace the element's BORDER BOX — so a
126     cast-list line whose authored height/width undershoot its font painted its
127     glyphs OUTSIDE the blue box. The ring is positioned instead at \`chromeRect()\`,
128     which MEASURES the text that actually renders (see compose-text-metrics.ts), so
129     the stroke and the 8 handles now frame the same measured box. \`.__ppn_selected\` stays
130     as the state marker applyOutlines() tracks/clears, but no longer paints. */
131  .__ppn_ring { position: fixed; z-index: 2147483644; pointer-events: none; box-sizing: border-box; border: calc(1.5px * var(--ppn-s, 1)) solid ${e}; }
132  /* GROUP BOX (Andy, 2026-08-08): the single union rect over a MULTI-selection —
133     "one bigger bounding box around all of them". Same accent and the same
134     hairline weight as the member rings, because it is the same kind of
135     statement about the same selection; what sets it apart is that it encloses
136     them. SAME z as the rings — not a step above: the ladder's next rung is
137     already the alignment guides', and where the union's edge does coincide with
138     a member's the two strokes are identical anyway, so stacking order is not a
139     question the eye can ask. Chrome, like every overlay here: pointer-events:
140     none, and isChrome() matches it. */
141  .__ppn_groupbox { position: fixed; z-index: 2147483644; pointer-events: none; box-sizing: border-box; border: calc(1.5px * var(--ppn-s, 1)) solid ${e}; }
142  /* Edit frame (⑫): a SOLID accent border (Canva convention — a dashed border reads
143     as "disabled/not editable"), set apart from a plain selection by a faint inner
144     ring rather than a dash. */
145  .__ppn_editing { outline: calc(1.5px * var(--ppn-s, 1)) solid ${e} !important; outline-offset: calc(-1.5px * var(--ppn-s, 1)) !important; box-shadow: inset 0 0 0 calc(1px * var(--ppn-s, 1)) color-mix(in srgb, ${e} 45%, transparent) !important; }
146  body.__ppn_edit, body.__ppn_edit * { cursor: default !important; }
147  /* NO NATIVE TEXT SELECTION ON THE EDIT LAYER. The gesture path is POINTER
148     events (see the capture note above beginDrag), and \`preventDefault()\` on a
149     \`pointerdown\` does NOT suppress the mouse defaults the old \`mousedown\` one
150     did — so without this, press-and-drag on a headline paints a blue text
151     selection across the slide instead of moving the block. This is a design
152     surface: dragging arranges, it does not select prose. The element actually
153     being INLINE-EDITED opts straight back in — that one surface IS a text
154     document, and its caret / select-all depend on it. */
155  body.__ppn_edit, body.__ppn_edit * { -webkit-user-select: none !important; user-select: none !important; }
156  body.__ppn_edit .__ppn_editing, body.__ppn_edit .__ppn_editing * { -webkit-user-select: text !important; user-select: text !important; }
157  /* HAND TOOL (⑫) — one cursor for the whole surface, including over content and
158     over the selection chrome, so the tool reads unambiguously. Ranked after the
159     edit-layer default and before the per-handle resize cursors below is not
160     enough on its own (the handle rules are more specific), so the hand rules
161     repeat the handle selectors — a hand press pans, it never resizes. */
162  body.__ppn_hand, body.__ppn_hand *, body.__ppn_hand .__ppn_handle { cursor: grab !important; }
163  body.__ppn_hand.__ppn_panning, body.__ppn_hand.__ppn_panning *,
164  body.__ppn_hand.__ppn_panning .__ppn_handle { cursor: grabbing !important; user-select: none !important; }
165  /* PLACEMENT TOOL ARMED (ENG-11306) — one crosshair over the whole page, the
166     hand rule's exact shape and for its exact reason: the tool has to be
167     unambiguous, and a page whose content still showed text/pointer cursors
168     would be promising a select that the next press is not going to do. Placed
169     AFTER the \`body.__ppn_edit *\` default rule (equal specificity, later wins)
170     and after the handle cursors, so nothing under the pointer can claim the
171     press it is about to take. */
172  body.__ppn_place, body.__ppn_place *, body.__ppn_place .__ppn_handle { cursor: crosshair !important; }
173  /* THE PENDING TEXT CARET (ENG-11545) — what the T tool leaves at the click
174     point instead of an empty box. \`position: fixed\` because the click arrives
175     in the frame's VIEWPORT coordinates and the frame's viewport IS the
176     artboard, which is how every other overlay here is placed.
177
178     WIDTH is scale-compensated (\`--ppn-s\`) and HEIGHT is not, and that asymmetry
179     is the whole design: a caret is a hairline at every zoom (so the stroke
180     stays one screen px, like the selection rings and handles), but its height
181     is the line box of the type it is about to become (so it grows with the
182     page when you zoom in). Set inline from PLACED_TEXT.
183
184     STEP TIMING, not a fade: a caret that eases in and out reads as a rendering
185     artefact rather than a cursor. ~1s is the native cadence on both platforms
186     and in Figma. */
187  .__ppn_caret { position: fixed; z-index: 2147483646; pointer-events: none; margin: 0; padding: 0; width: calc(1px * var(--ppn-s, 1)); animation: __ppn_caret_blink 1s step-end infinite; }
188  @keyframes __ppn_caret_blink { 0%, 50% { opacity: 1; } 50.01%, 100% { opacity: 0; } }
189  @media (prefers-reduced-motion: reduce) {
190    /* Steady, not gone: the caret is the ONLY thing telling the user where the
191       next keystroke lands, so "less motion" must not become "no cursor". */
192    .__ppn_caret { animation: none; opacity: 1; }
193  }
194  /* The caret's keyboard anchor — an invisible focused contenteditable at the
195     same point. It exists because IME is not reachable any other way: a
196     composition can only start on a focused editable, so a bare window keydown
197     listener would take Latin typing and silently drop every 中文 / かな / 한글
198     first character. It is 1px wide, paints nothing, and hides its OWN caret
199     (\`caret-color: transparent\`) so the only cursor on screen is the bar above;
200     it does carry the placed type's metrics AND the page's own ink (set inline),
201     so an IME candidate window opens at the right place and the text being
202     COMPOSED is visible where it will land — a composition is several keystrokes
203     long and must not be typed blind. */
204  .__ppn_caretin { position: fixed; z-index: 2147483645; margin: 0; padding: 0; border: 0; outline: 0; background: transparent; caret-color: transparent; white-space: pre; min-width: 1px; }
205  .__ppn_editing, .__ppn_editing * { cursor: text !important; }
206  .__ppn_marquee { position: fixed; z-index: 2147483647; pointer-events: none; border: calc(1px * var(--ppn-s, 1)) solid ${e}; background: color-mix(in srgb, ${e} 14%, transparent); }
207  .__ppn_handle { position: fixed; z-index: 2147483646; width: calc(9px * var(--ppn-s, 1)); height: calc(9px * var(--ppn-s, 1)); box-sizing: border-box; margin: 0; padding: 0; background: #fff; border: calc(1.5px * var(--ppn-s, 1)) solid ${e}; border-radius: calc(2px * var(--ppn-s, 1)); transform: translate(-50%, -50%); pointer-events: auto; }
208  /* Per-direction resize cursors on the handles. These MUST outrank the
209     body.__ppn_edit * { cursor: default !important } rule above (an !important on
210     a universal rule beats an inline style.cursor), so we match with higher
211     specificity + !important — otherwise the handles show the plain arrow. */
212  body.__ppn_edit .__ppn_handle[data-dir="nw"], body.__ppn_edit .__ppn_handle[data-dir="se"] { cursor: nwse-resize !important; }
213  body.__ppn_edit .__ppn_handle[data-dir="ne"], body.__ppn_edit .__ppn_handle[data-dir="sw"] { cursor: nesw-resize !important; }
214  body.__ppn_edit .__ppn_handle[data-dir="n"], body.__ppn_edit .__ppn_handle[data-dir="s"] { cursor: ns-resize !important; }
215  body.__ppn_edit .__ppn_handle[data-dir="e"], body.__ppn_edit .__ppn_handle[data-dir="w"] { cursor: ew-resize !important; }
216  body.__ppn_dragging, body.__ppn_dragging * { cursor: inherit !important; user-select: none !important; }
217  /* ── EDGE HIT BANDS (Andy, 2026-08-25) ────────────────────────────────────
218     "the edges on the side should be able to be draggable too, not just the
219     white nodes, this is common ux practice too."
220
221     INVISIBLE ON PURPOSE. The visible affordance is already there — the midpoint
222     handle — and drawing a second thing along the whole side would put a stripe
223     over the artwork to advertise a tolerance nobody needs told about. What the
224     band changes is the SIZE of the target, not the look of it: the cursor
225     turning at the edge is the feedback, exactly as it is in every tool that does
226     this. Sized and placed entirely from script (see edgeBandRect) because a band
227     spans BETWEEN the corner zones, which is a measurement, not a constant.
228
229     Ranked BELOW the handles so a corner square still wins where they meet, and
230     the geometry cuts the corners out on top of that — belt and braces, because
231     the two rules answer to different failure modes (stacking vs hit-testing). */
232  .__ppn_edge { position: fixed; z-index: 2147483642; margin: 0; padding: 0; background: transparent; pointer-events: auto; }
233  body.__ppn_edit .__ppn_edge[data-dir="n"], body.__ppn_edit .__ppn_edge[data-dir="s"] { cursor: ns-resize !important; }
234  body.__ppn_edit .__ppn_edge[data-dir="e"], body.__ppn_edit .__ppn_edge[data-dir="w"] { cursor: ew-resize !important; }
235  /* ── ROTATE ZONES (Andy, 2026-09-07 — ENG-11582) ──────────────────────────
236     "hovering just OUTSIDE any of the four corners of the selection box shows a
237     curved-arrow rotate cursor, and pressing there and dragging rotates the
238     element around its centre."
239
240     INVISIBLE, exactly like the edge bands and for the same reason: the
241     affordance IS the cursor. Drawing four grey squares around a selection would
242     put chrome over the artwork to advertise a tolerance the cursor already
243     announces the moment you reach for it.
244
245     RANKED BELOW EVERYTHING. z one under the edge bands, three under the handle
246     squares: where a zone's inner corner overlaps the handle centred on that
247     corner, the handle takes the press and the gesture is a resize. That is the
248     whole of the "the zone does not steal the handle's hit area" rule — geometry
249     puts the zone strictly outside the BOX (see rotateZoneRect), stacking keeps
250     it off the HANDLE, and the two answer to different failure modes. */
251  .__ppn_rotzone { position: fixed; z-index: 2147483641; margin: 0; padding: 0; background: transparent; pointer-events: auto; }
252  /* The drawn glyph, one turn per corner so the arrows face the box (see
253     rotateCursorValue). !important + the extra specificity for the same reason
254     the handle cursors carry them: body.__ppn_edit * { cursor: default } is an
255     !important universal rule and beats anything weaker. */
256  body.__ppn_edit .__ppn_rotzone[data-dir="nw"] { cursor: ${ex(315)} !important; }
257  body.__ppn_edit .__ppn_rotzone[data-dir="ne"] { cursor: ${ex(45)} !important; }
258  body.__ppn_edit .__ppn_rotzone[data-dir="se"] { cursor: ${ex(135)} !important; }
259  body.__ppn_edit .__ppn_rotzone[data-dir="sw"] { cursor: ${ex(225)} !important; }
260  /* THE STATE MARKER, and a cursor floor under it. \`__ppn_rotate\` on the body is
261     what says "the pointer is in a rotate zone" — the one fact a test, a
262     screenshot and the drag itself can all read, and the reason the class exists
263     rather than the zone rules alone. It carries the generic glyph too, so the
264     surface can never fall back to the universal default arrow at the exact
265     pixel where a zone's edge meets whatever is under it. The per-corner rules
266     above are more specific and keep their own turn. */
267  body.__ppn_edit.__ppn_rotate, body.__ppn_edit.__ppn_rotate * { cursor: ${ex(315)} !important; }
268  /* ── CROP MODE (Andy, 2026-08-25) ─────────────────────────────────────────
269     Double-click an image and the editor stops talking about the box and starts
270     talking about the PICTURE: the whole asset appears, extending past the frame
271     it is placed in, and the frame becomes a window over it.
272
273     ── THE CHROME, RULED TWICE ─────────────────────────────────────────────
274     FIRST TAKE (superseded): chunky white CIRCLES tucked inside the four
275     corners. The reasoning was that a mode needs to be legible at a glance, and
276     that giving crop a different HANDLE SHAPE from the selection squares is the
277     cheapest way to say so. The shape was the wrong lever.
278
279     SECOND RULING (Andy, 2026-08-25, with a Figma reference): "for the cropping
280     resize, we need the ui to be like in Figma, not the white dots that we have
281     right now." So: solid ACCENT tabs — chunky squares on the four corners and
282     chunky bars on the four edge midpoints, all STRADDLING the boundary rather
283     than tucked inside it — over a fine DASHED outline.
284
285     The differentiation argument survives, it just runs on different evidence.
286     Crop mode is unmistakable because the whole surround is DIMMED and the
287     window is dashed; against a small white selection square, a fat solid blue
288     tab on a dashed edge is not a thing anyone confuses. Matching the tool
289     people already know beats inventing a private vocabulary to make a point the
290     dimming was already making.
291
292     The midpoint tabs are the same system as the edge bands above: the TAB is
293     the visible affordance, the BAND is the generous hit zone behind it. One
294     idea, two elements, so neither has to compromise.
295
296     The ghost is the real asset at full opacity — not a faded copy — and the DIM
297     is applied by four bands around the window instead. Two reasons that way
298     round: the part you are choosing renders at true brightness (you are judging
299     a photograph, so it must not be judged through a veil), and the bands are
300     plain rects, so there is no clip-path/even-odd support question to answer on
301     a surface whose whole point is that it renders everywhere.
302
303     All of it is chrome: isChrome() matches the __ppn_crop prefix, so none of
304     it hovers, selects, marquees or commits. The ghost, the tabs and the hit
305     bands are the only pieces that take pointer events — the dim bands and the
306     window outline must not swallow the press that pans the picture. */
307  .__ppn_cropghost { position: fixed; z-index: 2147483643; margin: 0; padding: 0; border: 0; max-width: none; max-height: none; object-fit: fill; pointer-events: auto; cursor: move; }
308  .__ppn_cropband { position: fixed; z-index: 2147483644; pointer-events: none; background: color-mix(in srgb, #000 62%, transparent); }
309  /* DASHED, per the Figma reference. A dashed hairline reads as "this boundary is
310     being negotiated" where a solid one reads as "this object is selected" — the
311     same distinction a marquee makes, and the reason every crop tool draws it
312     this way. The drop shadow stays: a hairline over a photograph needs a dark
313     side to survive a light frame. */
314  .__ppn_cropwin { position: fixed; z-index: 2147483645; pointer-events: none; box-sizing: border-box; border: calc(1px * var(--ppn-s, 1)) dashed ${e}; box-shadow: 0 0 0 calc(1px * var(--ppn-s, 1)) color-mix(in srgb, #000 45%, transparent); }
315  /* The tabs. SOLID accent, no inner fill — the selection handle is a white chip
316     with a thin accent ring, so filling these completely is the single strongest
317     way to tell the two apart without changing shape vocabulary. Centred on the
318     boundary via translate(-50%, -50%), which is what "straddling" means and is
319     also what makes a tab grabbable from either side of the line. */
320  .__ppn_crophandle { position: fixed; z-index: 2147483646; box-sizing: border-box; margin: 0; padding: 0; background: ${e}; border-radius: calc(1.5px * var(--ppn-s, 1)); pointer-events: auto; transform: translate(-50%, -50%); box-shadow: 0 0 0 calc(1px * var(--ppn-s, 1)) color-mix(in srgb, #000 35%, transparent); }
321  body.__ppn_edit .__ppn_crophandle[data-dir="nw"], body.__ppn_edit .__ppn_crophandle[data-dir="se"] { cursor: nwse-resize !important; }
322  body.__ppn_edit .__ppn_crophandle[data-dir="ne"], body.__ppn_edit .__ppn_crophandle[data-dir="sw"] { cursor: nesw-resize !important; }
323  body.__ppn_edit .__ppn_crophandle[data-dir="n"], body.__ppn_edit .__ppn_crophandle[data-dir="s"] { cursor: ns-resize !important; }
324  body.__ppn_edit .__ppn_crophandle[data-dir="e"], body.__ppn_edit .__ppn_crophandle[data-dir="w"] { cursor: ew-resize !important; }
325  /* The crop window's own edge bands — the hit zone behind the midpoint tabs.
326     Below the tabs and the corner squares, cut back from the corners by the same
327     geometry the selection bands use, and above the ghost so a press on the edge
328     resizes the window instead of panning the picture. */
329  .__ppn_cropedge { position: fixed; z-index: 2147483645; margin: 0; padding: 0; background: transparent; pointer-events: auto; }
330  body.__ppn_edit .__ppn_cropedge[data-dir="n"], body.__ppn_edit .__ppn_cropedge[data-dir="s"] { cursor: ns-resize !important; }
331  body.__ppn_edit .__ppn_cropedge[data-dir="e"], body.__ppn_edit .__ppn_cropedge[data-dir="w"] { cursor: ew-resize !important; }
332  body.__ppn_edit .__ppn_cropghost { cursor: move !important; }
333  /* The size badge (Andy's reference) — the window's px dimensions, pinned under
334     it, so a crop is a measured edit rather than an eyeballed one. Same accent
335     chip the rest of the tool uses; never a hit target. */
336  .__ppn_cropsize { position: fixed; z-index: 2147483646; pointer-events: none; white-space: nowrap; background: ${e}; color: #fff; border-radius: calc(3px * var(--ppn-s, 1)); padding: calc(2px * var(--ppn-s, 1)) calc(5px * var(--ppn-s, 1)); font: 500 calc(10px * var(--ppn-s, 1))/1.4 ui-sans-serif, system-ui, -apple-system, sans-serif; transform: translate(-50%, 0); }
337  /* ── THE CROP FRAME (ENG-10647) — the CANVAS's crop overlay, drawn in here ──
338     A different tool from the framing crop above, and it must LOOK different:
339     that one negotiates which part of the asset fills a box the page keeps
340     (dashed accent window, accent tabs); this one picks the rect a re-encode
341     will keep, and the picture that comes back IS the rect. So this is
342     \`CropOverlay\`'s chrome, matched deliberately rather than approximately —
343     Andy: "align Preview to Flow's UX" — a WHITE hairline box, a white
344     rule-of-thirds grid at 30%, eight white grips, a 50% dim outside, and the
345     live source-px readout under the box.
346
347     WHITE, not accent, and that is the substantive half of the match: accent is
348     this surface's language for a SELECTION (rings, handles, guides, marquee). A
349     crop rect is not a selection — it is a statement about the picture — and it
350     is read against photography, where a neutral rule is the convention every
351     crop tool in the category uses.
352
353     Every class starts \`__ppn_crop\`, which is what puts them inside isChrome()'s
354     existing one-prefix rule: no crop chrome can be selected, dragged as a page
355     element, or serialized into the stored document. */
356  .__ppn_cropplate { position: fixed; z-index: 2147483643; margin: 0; padding: 0; border: 0; max-width: none; max-height: none;
356 object-fit: fill; pointer-events: none; }
357  .__ppn_cropdim { position: fixed; z-index: 2147483644; pointer-events: none; background: color-mix(in srgb, #000 50%, transparent); }
358  /* The box itself is the MOVE target — the whole interior drags the rect, as it
359     does on the canvas, so the gesture does not depend on finding a grip. */
360  .__ppn_cropframe { position: fixed; z-index: 2147483645; box-sizing: border-box; border: calc(1px * var(--ppn-s, 1)) solid #fff; pointer-events: auto; }
361  body.__ppn_edit .__ppn_cropframe { cursor: move !important; }
362  .__ppn_cropgrid { position: fixed; z-index: 2147483645; pointer-events: none; background: color-mix(in srgb, #fff 30%, transparent); }
363  /* The grips: white chips with a dark hairline, straddling the boundary via
364     translate(-50%, -50%) — the same shape vocabulary the canvas overlay uses
365     (\`bg-foreground\` + \`border-background\`, squared off at rounded-sm). Sized in
366     SCREEN px so they stay the same size to the hand at 91% and at 113%. */
367  .__ppn_cropdot { position: fixed; z-index: 2147483646; box-sizing: border-box; margin: 0; padding: 0; background: #fff; border: calc(1px * var(--ppn-s, 1)) solid color-mix(in srgb, #000 55%, transparent); border-radius: calc(2px * var(--ppn-s, 1)); pointer-events: auto; transform: translate(-50%, -50%); }
368  body.__ppn_edit .__ppn_cropdot[data-dir="nw"], body.__ppn_edit .__ppn_cropdot[data-dir="se"] { cursor: nwse-resize !important; }
369  body.__ppn_edit .__ppn_cropdot[data-dir="ne"], body.__ppn_edit .__ppn_cropdot[data-dir="sw"] { cursor: nesw-resize !important; }
370  body.__ppn_edit .__ppn_cropdot[data-dir="n"], body.__ppn_edit .__ppn_cropdot[data-dir="s"] { cursor: ns-resize !important; }
371  body.__ppn_edit .__ppn_cropdot[data-dir="e"], body.__ppn_edit .__ppn_cropdot[data-dir="w"] { cursor: ew-resize !important; }
372  /* The readout, under the box, in SOURCE px — the same number the panel's
373     Dimensions fields hold, which is the point: it is the crop's size, not the
374     frame's on-screen size. No chip: it sits over the dimmed picture, where a
375     shadowed white label is quieter and still legible. */
376  .__ppn_cropdims { position: fixed; z-index: 2147483646; pointer-events: none; white-space: nowrap; color: #fff; text-shadow: 0 calc(1px * var(--ppn-s, 1)) calc(3px * var(--ppn-s, 1)) color-mix(in srgb, #000 70%, transparent); font: 500 calc(11px * var(--ppn-s, 1))/1.4 ui-sans-serif, system-ui, -apple-system, sans-serif; font-variant-numeric: tabular-nums; transform: translate(-50%, 0); }
377  /* Smart-alignment guides (⑩) — hairline accent rules the drag paints while an
378     edge/center is near/snapped to an artboard or peer alignment line. Chrome:
379     never selectable / committable (isChrome matches them), pointer-events off, and
380     stacked just BELOW the resize handles so a handle stays grabbable over a guide. */
381  .__ppn_guide { position: fixed; z-index: 2147483645; pointer-events: none; background: ${e}; }
382  /* ── BUSY: the op is happening HERE ────────────────────────────────────────
383     A dim scrim to say "this is not yours to touch right now", and one soft
384     band sweeping down it to say "still working". The Canva reference's SPIRIT
385     — progress on the element itself — without its literal rainbow: this
386     surface is a design document, and a saturated animated gradient over the
387     art would be the loudest thing on the page.
388
389     \`pointer-events: none\` on the overlay: it must not become the hit target.
390     Refusing interaction is done in the hit test (see \`isBusy\`), because an
391     overlay that swallowed the click would also swallow the CLICK-AWAY that
392     deselects, and the user would be stuck.
393
394     Sized and placed off \`chromeRect\`, like every other overlay here, so it
395     cannot drift from the ring it replaces. */
396  .__ppn_busy { position: fixed; z-index: 2147483645; pointer-events: none; overflow: hidden; box-sizing: border-box; background: color-mix(in srgb, #000 34%, transparent); outline: calc(1.5px * var(--ppn-s, 1)) solid color-mix(in srgb, ${e} 70%, transparent); outline-offset: calc(-1.5px * var(--ppn-s, 1)); }
397  .__ppn_busy::after { content: ""; position: absolute; top: 0; bottom: 0; left: 0; width: 45%; background: linear-gradient(to right, transparent, color-mix(in srgb, #fff 22%, transparent), transparent); animation: __ppn_sweep 1.6s cubic-bezier(0.4, 0, 0.2, 1) infinite; }
398  /* Empty generation and upload slots share the dark loading surface and sweep.
399     Status tags match Flow's GeneratingPill card variant, including text shimmer.
400     These overlays sit below the busy scrim and never block selection or edits. */
401  .__ppn_uploading, .__ppn_pending { position: fixed; z-index: 2147483644; pointer-events: none; overflow: hidden; box-sizing: border-box; background: color-mix(in oklab, ${t.muted} 96%, ${t.foreground} 4%); outline: calc(1.5px * var(--ppn-s, 1)) solid color-mix(in srgb, ${e} 45%, transparent); outline-offset: calc(-1.5px * var(--ppn-s, 1)); }
402  .__ppn_uploading::after, .__ppn_pending::after { content: ""; position: absolute; top: 0; right: 0; bottom: 0; left: 0; background-image: linear-gradient(100deg, transparent 30%, color-mix(in oklab, ${t.foreground} 12%, transparent) 50%, transparent 70%); background-size: 220% 100%; animation: __ppn_pendsweep 1.6s ease-in-out infinite; }
403  @keyframes __ppn_pendsweep { 0% { background-position: -50% 0; } 100% { background-position: 150% 0; } }
404  /* Flow's bottom-center GeneratingPill, scaled to stay readable while zooming. */
405  .__ppn_loading_pill { position: absolute; bottom: calc(12px * var(--ppn-s, 1)); left: 50%; transform: translateX(-50%); z-index: 1; max-width: calc(100% - 16px * var(--ppn-s, 1)); box-sizing: border-box; overflow: hidden; border-radius: 9999px; border: calc(1px * var(--ppn-s, 1)) solid ${t.border??"oklch(1 0 0 / 7%)"}; background: ${t.card??"oklch(0.205 0 0)"}; padding: calc(6px * var(--ppn-s, 1)) calc(14px * var(--ppn-s, 1)); pointer-events: none; user-select: none; }
406  .__ppn_loading_text { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: ${t.foreground}; font: 500 calc(11px * var(--ppn-s, 1))/1 ui-sans-serif, system-ui, -apple-system, sans-serif; }
407  @supports ((-webkit-background-clip: text) or (background-clip: text)) {
408    .__ppn_loading_text { background: linear-gradient(90deg, color-mix(in oklab, ${t.foreground} 35%, transparent) 0%, color-mix(in oklab, ${t.foreground} 95%, transparent) 50%, color-mix(in oklab, ${t.foreground} 35%, transparent) 100%); background-size: 200% auto; -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; color: transparent; animation: __ppn_textglow 2.4s linear infinite reverse; }
409  }
410  @keyframes __ppn_textglow { 0% { background-position: -200% center; } 100% { background-position: 200% center; } }
411  /* LEFT → RIGHT (Andy 2026-08-08). A scan reads as a scan when it travels the
412     way the eye already travels the page; downward it read as a loading bar
413     lying on its side. */
414  @keyframes __ppn_sweep { 0% { transform: translateX(-100%); } 100% { transform: translateX(322%); } }
415  @media (prefers-reduced-motion: reduce) {
416    /* The scrim alone still says "busy"; a sweeping band is the part that is
417       motion for motion's sake to someone who asked for less of it. */
418    .__ppn_busy::after { animation: none; opacity: 0.35; }
419    .__ppn_loading_text { animation: none; background-image: none; color: ${t.foreground}; -webkit-text-fill-color: currentColor; }
420    /* Keep the loading surface visible without movement. */
421    .__ppn_uploading::after, .__ppn_pending::after { animation: none; opacity: 0.35; }
422  }
423</style>
424<script>
425(function () {
426  var CFG = ${n};
427  // ── the SVG shapes' markup (ENG-11305) ──────────────────────────────────
428  // Generated ONCE by schema-shape-path.ts at its default parameters and frozen
429  // into this runtime, so the element an insert puts on screen is the exact
430  // string the deterministic renderer will emit for the component it creates.
431  // The runtime only paints and positions it; it never derives a vertex.
432  var PPN_SHAPE_MARKUP = ${JSON.stringify(eh.INSERT_SHAPE_MARKUP)};
433  // The R / L / ⇧L / O mapping, shared verbatim with the pane's window listener
434  // (self-contained by contract — see shapeShortcutFor).
435  var shapeShortcutFor = ${eb.toString()};
436  // ── arched per-letter text helpers ──────────────────────────────────────
437  // Injected from compose-arch.ts so the sandboxed runtime and the node unit
438  // tests share ONE implementation of detection + arc-preserving re-split.
439  // Each is self-contained (no module-scope refs) so its .toString() runs here.
440  var isArchLetters = ${K.toString()};
441  var redistributeArchLetters = ${Z.toString()};
442  var buildArchTemplate = ${J.toString()};
443  var splitTransformFromStyle = ${Q.toString()};
444  // Smart-alignment snap math (⑩) — injected from compose-snap.ts so the runtime
445  // and the node unit tests share ONE pure implementation of guide
445-candidate
446  // computation + snap/break hysteresis. Self-contained (no module refs) so its
447  // .toString() runs here against the live document.
448  var computeSnap = ${ed.computeSnap.toString()};
449  // Resize geometry — the box a handle drag defines, derived from a LATCHED
450  // anchor. Same self-contained contract as computeSnap above, so the injected
451  // form and the node-tested form cannot diverge.
452  var resizeBox = ${en.resizeBox.toString()};
453  // The invisible hit strip that makes a SIDE draggable, not just its midpoint
454  // square. Same self-contained contract; the corner cut-out that gives a corner
455  // drag priority is geometry, so it is tested in node like the rest.
456  var edgeBandRect = ${en.edgeBandRect.toString()};
457  // ── GROUP RESIZE (SUNRA-1213) ────────────────────────────────────────────
458  // The union box's own arithmetic, under the identical self-contained contract:
459  // one anchor + two scale factors, the map every member of a multi-selection is
460  // carried by, and the floor that stops the smallest of them collapsing. Tested
461  // in node beside resizeBox, injected here so there is one implementation of
462  // "the group scaled by this much".
463  var groupResizeScale = ${en.groupResizeScale.toString()};
464  var scaleRectAbout = ${en.scaleRectAbout.toString()};
465  var groupScaleFloor = ${en.groupScaleFloor.toString()};
466  // ── ROTATE (ENG-11582) ───────────────────────────────────────────────────
467  // The corner-outside rotate gesture's arithmetic, injected from
468  // compose-rotate.ts under the identical self-contained contract: ONE
469  // implementation of where the zone is, what angle a drag means and how that
470  // angle is spelled, shared by this runtime and the node tests. The constants
471  // ride along because the calls below name them.
472  var ROTATE_DIRS = ${JSON.stringify(er)};
473  var ROTATE_REACH = 14;
474  var ROTATE_SNAP_DEG = 15;
475  var rotateZoneRect = ${ea.toString()};
476  var rotateAngleFor = ${eo.toString()};
477  var rotateTransformCss = ${ei.toString()};
478  var rotatePoint = ${es.toString()};
479  var angleFromMatrix = ${el.toString()};
480  // Rendered-text metrics (Andy, 2026-07-29) — injected from compose-text-metrics.ts
481  // so the runtime and the node unit tests share ONE implementation of "how big is
482  // the text really" (selection ring + handles). Self-contained for .toString(),
483  // same contract as computeSnap above.
484  //
485  // TOMBSTONE — there is no \`resizeFontSize\` here any more (ENG-11576). The
486  // 2026-07-29 ask ① ("resizing a text box must scale the text", corner handles,
487  // width ratio) was OVERTURNED on 2026-09-07: dragging any handle of a text
488  // element changes the BOX only, and the font size is the inspector's alone
489  // (Framer's behaviour). Nothing in this runtime may derive a font size from a
490  // box size again — see compose-text-box.dom.test.ts's eight-handle tombstone.
491  var chromeRectFor = ${ec.chromeRectFor.toString()};
492  var textInkRect = ${ec.textInkRect.toString()};
493  var inkPinBox = ${ec.inkPinBox.toString()};
494  var alignPinBox = ${ec.alignPinBox.toString()};
495  var snapOutward = ${ec.snapOutward.toString()};
496  // The AUTO ⇄ FIXED marker (ENG-11546). One mapping, shared by this runtime, the
497  // schema renderer and the style write-back, so all three read the same fact off
498  // the same declaration — see verticalAlignCss for why it is \`align-content\`.
499  var verticalAlignCss = ${ec.verticalAlignCss.toString()};
500  var verticalAlignFromCss = ${ec.verticalAlignFromCss.toString()};
501  // Crop geometry — injected from compose-crop.ts under the identical contract:
502  // ONE pure implementation of "which part of the picture shows", shared by this
503  // runtime and the node tests, so the gesture and the renderer cannot drift
504  // about what a crop means. The two module constants ride along because the
505  // functions close over them by name.
506  var CROP_EPS = ${ee.CROP_EPS};
507  var FULL_CROP = ${JSON.stringify(ee.FULL_CROP)};
508  var isFullCrop = ${ee.isFullCrop.toString()};
509  var clampCrop = ${ee.clampCrop.toString()};
510  var coverCropRect = ${ee.coverCropRect.toString()};
511  var effectiveCrop = ${ee.effectiveCrop.toString()};
512  var cropToViewBox = ${ee.cropToViewBox.toString()};
513  var parseViewBox = ${ee.parseViewBox.toString()};
514  var imageRectForCrop = ${ee.imageRectForCrop.toString()};
515  var cropForWindow = ${ee.cropForWindow.toString()};
516  var clampWindowInsideImage = ${ee.clampWindowInsideImage.toString()};
517  var clampImageAroundWindow = ${ee.clampImageAroundWindow.toString()};
518  var cropAfterFrameResize = ${ee.cropAfterFrameResize.toString()};
519  // The CROP FRAME's arithmetic (ENG-10647) — injected from crop-rect.ts, which
520  // the CANVAS overlay imports normally. One implementation of the drag and of
521  // the natural-px ↔ display-px mapping, so "the same crop" is a promise about
522  // numbers rather than about two files being kept in step by hand.
523  var cropDragRect = ${A.cropDragRect.toString()};
524  var cropRectToBox = ${A.cropRectToBox.toString()};
525  var cropBoxToRect = ${A.cropBoxToRect.toString()};
526  // Does this engine implement the property the crop is expressed in? Chrome 104+,
527  // Edge 104+, Safari 18+. Where it is missing the document still renders — the
528  // declaration is ignored and the picture cover-fits exactly as it did before
529  // crops existed — so the honest response is to not OFFER the gesture rather than
530  // to let a user author a crop they cannot see. Read once: it cannot change.
531  var CROP_SUPPORTED = (function () {
532    try { return !!(window.CSS && CSS.supports && CSS.supports("object-view-box", "inset(0%)")); }
533    catch (err) { return false; }
534  })();
535  var INLINE = { SPAN:1, A:1, B:1, STRONG:1, I:1, EM:1, U:1, S:1, SMALL:1, CODE:1, MARK:1, SUB:1, SUP:1, BR:1, LABEL:1, ABBR:1, CITE:1, Q:1, TIME:1, KBD:1, SAMP:1, VAR:1, WBR:1, BDI:1, BDO:1, DFN:1, INS:1, DEL:1 };
536  var SKIP = { HTML:1, BODY:1, HEAD:1, SCRIPT:1, STYLE:1 };
537  // ── THE TWO HALVES OF A PAGE (ENG-11665) ────────────────────────────────
538  // CONTAINER: the block boxes a page is BUILT from. They hold other things and
539  // say nothing themselves, and on a real page they cover every pixel — which is
540  // why a press had to stop meaning "grab this" for a marquee to be reachable at
541  // all (see isContainerHit).
542  var CONTAINER = {
543    DIV:1, SECTION:1, MAIN:1, HEADER:1, FOOTER:1, ARTICLE:1, NAV:1, ASIDE:1,
544    UL:1, OL:1, DL:1, FIGURE:1, FORM:1, TABLE:1, THEAD:1, TBODY:1, TFOOT:1,
545    TR:1, FIELDSET:1, BLOCKQUOTE:1,
546  };
547  // ATOMIC: elements whose INSIDES are not page elements. A marquee must never
548  // reach into an <svg> and hand back three <path>s, and the old hit test only
549  // avoided that by accident (an enclosed parent suppressed its children).
550  var ATOMIC = {
551    IMG:1, SVG:1, PICTURE:1, VIDEO:1, CANVAS:1, IFRAME:1, INPUT:1, TEXTAREA:1,
552    SELECT:1, HR:1,
553  };
554  var active = false;        // is the edit layer live?
555  var hovered = null;        // element the hover highlight is tracking
556  var hoverBox = null;       // its overlay rect (drawn at chromeRect), or null
557  var selectedSet = [];      // array of selected elements (multi-select)
558  var editingEl = null;      // element being inline-edited
559  // The box a PLACEMENT TOOL just put down and has not committed yet
560  // (ENG-11306). It is on screen and being typed into, but the document has
561  // never heard of it — see placePrimitive / stopEditing's fresh branch.
562  var freshEl = null;
563  // THE PENDING TEXT CARET (ENG-11545) — a click point armed by the T tool that
564  // has not become anything yet: { bar, anchor, x, y, ink }. Deliberately NOT an
565  // element and NOT a selection; it is one overlay hairline plus the invisible
566  // editable that catches the first keystroke, and dropping it removes both and
567  // leaves the document, the selection and the history untouched.
568  var caretPend = null;
569  var editingOriginal = "";  // pre-edit innerHTML (for Escape revert)
570  var archTemplate = null;   // when editing an arched word: its per-letter template
571  var reverting = false;     // suppress commit on the blur that follows a revert
572  var down = null;           // mousedown info { x, y, shift } (for marquee)
573  var marqueeEl = null;      // the marquee rectangle overlay while dragging
574  var suppressClick = false; // eat the click that ends a marquee drag
575  var drag = null;           // active drag/resize gesture (see beginDrag) or null
576  var nudge = null;          // active arrow-key move burst (see nudgeBy) or null
577  var handles = [];          // the 8 resize-handle overlays for a single selection
578  var rings = [];            // selection-ring overlays, one per selected element
579  var groupBox = null;       // the UNION rect over a multi-selection, or null
580  var ghandles = [];
580         // the union box's 4 corner handles (SUNRA-1213)
581  var gres = null;           // active GROUP RESIZE gesture (see beginGroupResize)
582  var guides = [];           // active alignment-guide overlay lines (⑩, drag only)
583  // Magnetic snap radius in SCREEN px — converted to frame px per drag frame with
584  // the live artboard scale, so the magnet feels the same at 40% and at 250%.
585  var SCREEN_SNAP_PX = 7;
586  // Active tool (⑫) — "select" is the normal edit layer; "hand" turns the whole
587  // surface into a pan handle. Mirrors the canvas V / H model; the parent owns the
588  // toggle + shortcuts and posts every change.
589  var curTool = "select";
590  // The tool vocabulary, as tables so an unknown tool falls back to select and a
591  // placement tool is one lookup rather than a growing || chain. Mirrors
592  // PlaceTool / PickerTool in compose-picker.ts (compose-place-tool.test.ts pins
593  // the two lists together — this runtime is a STRING and cannot import them).
594  var PLACE_TOOLS = { text: 1, rectangle: 1, ellipse: 1, line: 1 };
595  var TOOLS = { select: 1, hand: 1, text: 1, rectangle: 1, ellipse: 1, line: 1 };
596  // The click that PLACED something is spent: it already created the element and
597  // opened its editor, so the click event that follows the placing pointerdown
598  // must not also commit it (an empty fresh box would be dropped) or deselect it.
599  var placeClick = false;
600  var pan = null;            // { x, y } last pointer position while hand-panning
601  var curScale = 1;          // on-screen scale of the artboard (⑪ crisp chrome)
602
603  function post(msg) { msg.source = CFG.out; parent.postMessage(msg, "*"); }
604
605  // ── DELETE SPACERS (ENG-11643) ─────────────────────────────────────────
606  // The invisible box a deleted element leaves behind so its neighbours do not
607  // move (we have no auto-layout; a removed node in normal flow lets every
608  // following sibling slide up). NOT chrome: a spacer is REAL markup, it is
609  // saved with the document and it must keep counting in 'selectorFor''s
610  // nth-of-type index — which is exactly what 'isChrome' excludes nodes from.
611  // So it is its own predicate, and it is consulted only where the question is
612  // "may the user select this?": the hit test and the marquee.
613  var SPACER_ATTR = "data-ppn-spacer";
614  function isSpacer(node) {
615    return !!(node && node.nodeType === 1 && node.hasAttribute && node.hasAttribute(SPACER_ATTR));
616  }
617
618  // Any overlay NODE we injected — the resize handles and the marquee rectangle.
619  // These are our OWN DOM nodes and must be invisible to hover/pick/marquee/sample
620  // logic so the layer never selects itself or poisons design-system sampling.
621  //
622  // Match ONLY those overlay classes — NOT the transient STATE classes we stamp
623  // onto real composition elements (__ppn_selected / __ppn_hover / __ppn_editing).
624  // A selected element is a real element we've merely outlined; it must stay fully
625  // interactive. Testing the broad "__ppn_" substring here misclassified a SELECTED
626  // element as chrome, so onDown's move branch (!isChrome) rejected it and the
627  // press fell through to the marquee branch — i.e. you could not drag an element
628  // once it was selected.
629  function isChrome(node) {
630    if (!node || typeof node.className !== "string") return false;
631    return (
632      node.className.indexOf("__ppn_handle") >= 0 ||
633      // The invisible side bands. Chrome like everything else here: they must
634      // never hover, select, marquee or be committed — onDown reads them BEFORE
635      // the pick, which is the only place they are allowed to mean anything.
636      node.className.indexOf("__ppn_edge") >= 0 ||
637      // The invisible corner-outside ROTATE zones (ENG-11582). Chrome under the
638      // same rule as the bands: onDown reads them BEFORE the pick, and that is
639      // the only place they are allowed to mean anything. Without this a marquee
640      // would enclose them and a hover would light one up as content.
641      node.className.indexOf("__ppn_rotzone") >= 0 ||
642      // ONE prefix covers the whole crop overlay — ghost, bands, window outline,
643      // the eight tabs, the edge hit bands and the size badge. They are all our
644      // nodes and all named __ppn_crop…; the ghost in particular is an <img> on
645      // top of the composition, and a hit test that mistook it for content would
646      // let a crop session select or drag its own chrome.
647      node.className.indexOf("__ppn_crop") >= 0 ||
648      node.className.indexOf("__ppn_marquee") >= 0 ||
649      node.className.indexOf("__ppn_ring") >= 0 ||
650      node.className.indexOf("__ppn_groupbox") >= 0 ||
651      node.className.indexOf("__ppn_hoverbox") >= 0 ||
652      node.className.indexOf("__ppn_guide") >= 0 ||
653      node.className.indexOf("__ppn_busy") >= 0 ||
654      // The pending-slot shimmer (ENG-12012). Chrome for the same reason the
655      // busy scrim is: it sits ON the composition, and a marquee that enclosed
656      // it — or a selector index that counted it — would be reading our own
657      // overlay as the user's content.
658      node.className.indexOf("__ppn_pending") >= 0 ||
659      // The upload label and its two inner nodes (ENG-12629) — one prefix covers
660      // all three. Chrome for the same reason the pending shimmer is: it sits ON
661      // the composition, and a marquee that enclosed it (or a selector index that
662      // counted it) would be reading our own overlay as the user's content.
663      node.className.indexOf("__ppn_uploading") >= 0 ||
664      node.className.indexOf("__ppn_loading_") >= 0 ||
665      // The pending caret + its keyboard anchor (ENG-11545). Chrome for the same
666      // reason the handles are: they sit ON the composition and must never be
667      // hovered, picked, marqueed, measured into a selector index or committed.
668      node.className.indexOf("__ppn_caret") >= 0
669    );
670  }
671
672  function cidOf(node) {
673    return node && node.getAttribute ? node.getAttribute("data-cid") : null;
674  }
675
676  // Stable selector, most-stable signal first:
677  //   1. data-cid  → the generation-side stable id ([data-cid="…"]). Present on
678  //      agent-authored elements; survives reflow / reorder / class churn and
679  //      resolves identically in the live (runtime-injected) and stored docs.
680  //   2. id        → an element id.
681  //   3. DOM path  → LEGACY fallback for compositions with no data-cid: a
682  //      child-combinator path of tag + stable class names, with :nth-of-type
683  //      pinning the position at EVERY level that has same-tag siblings.
684  //
685  // WHY nth-of-type EVEN WITH CLASSES (Andy 2026-08-25, "resized the bars,
686  // ⌘Z did nothing"): sibling elements routinely SHARE a class — five
687  // div.bar's in an agent-authored chart — and a class-only segment then
688  // matches all of them. Every consumer of this selector demands exactly one
689  // resolution: the stored-html patchers ABORT the whole commit on an ambiguous
690  // selector (patchCompositionStyles' need-1 rule), which silently dropped the
691  // resize from the persisted doc AND from the undo history — the live frame
692  // showed a state the document never had, and ⌘Z had nothing to pop. The
693  // inverse-op lookups (compose-inverse) resolve querySelector-first, so
694  // ambiguity there patches the WRONG sibling on undo. Pinning the position
695  // makes the path unique BY CONSTRUCTION along the child-combinator chain.
696  //
697  // The nth index counts NON-CHROME same-tag siblings only: every overlay this
698  // runtime injects (rings, handles, ghosts, busy scrims — the __ppn_ set) is
699  // appended to document.body, but counting through isChrome() means the index
700  // is identical in the live and stored documents even if chrome ever lands
701  // beside content. A trailing injected <style>/<script> can't shift per-tag
702  // counts either, so the path stays stable across the two docs.
703  function selectorFor(el) {
704    var cid = cidOf(el);
705    if (cid) return '[data-cid="' + CSS.escape(cid) + '"]';
706    if (el.id) return "#" + CSS.escape(el.id);
707    var parts = [];
708    var node = el;
709    while (node && node.nodeType === 1 && node !== document.body && node !== document.documentElement) {
710      var ncid = cidOf(node);
711      if (ncid) { parts.unshift('[data-cid="' + CSS.escape(ncid) + '"]'); break; }
712      if (node.id) { parts.unshift("#" + CSS.escape(node.id)); break; }
713      var seg = node.tagName.toLowerCase();
714      var classes = [];
715      var cl = node.classList ? Array.from(node.classList) : [];
716      for (var i = 0; i < cl.length; i++) {
717        if (cl[i].indexOf("__ppn_") === 0) continue;
718        if (/^[A-Za-z][A-Za-z0-9_-]*$/.test(cl[i])) classes.push(cl[i]);
719      }
720      if (classes.length) {
721        seg += "." + classes.map(function (c) { return CSS.escape(c); }).join(".");
722      }
723      var parent = node.parentNode;
724      if (parent) {
725        var same = [];
726        var kids = parent.children;
727        for (var j = 0; j < kids.length; j++) {
728          if (kids[j].tagName !== node.tagName) continue;
729          if (isChrome(kids[j])) continue; // overlay nodes don't exist in the stored doc
730          same.push(kids[j]);
731        }
732        if (same.length > 1) seg += ":nth-of-type(" + (same.indexOf(node) + 1) + ")";
733      }
734      parts.unshift(seg);
735      node = node.parentElement;
736    }
737    return parts.join(" > ");
738  }
739
740  // A text-bearing element = non-empty text whose element children are all
741  // inline/text-level — i.e. a leaf headline / paragraph / link, NOT a block
742  // container (which holds other blocks). Only these are inline-editable.
743  function isTextBearing(el) {
744    if (!el || el.nodeType !== 1 || SKIP[el.tagName]) return false;
745    if (!el.textContent || !el.textContent.trim()) return false;
746    var kids = el.children;
747    for (var i = 0; i < kids.length; i++) {
748      if (!INLINE[kids[i].tagName]) return false;
749    }
750    return true;
751  }
752
753  // Does this element PAINT its whole box? A fill, a background image, or a real
754  // border means the box itself is visible content (a text chip, a callout), so the
755  // chrome must frame the BOX — shrinking to the glyphs would cut through paint the
756  // user can see. Only an unpainted text leaf is "just glyphs".
757  function paintsBox(el, cs) {
758    if (hasFill(el, cs)) return true;
759    var bg = cs.backgroundImage;
760    if (bg && bg !== "none") return true;
761    return (
762      (parseFloat(cs.borderTopWidth) || 0) > 0 || (parseFloat(cs.borderRightWidth) || 0) > 0 ||
763      (parseFloat(cs.borderBottomWidth) || 0) > 0 || (parseFloat(cs.borderLeftWidth) || 0) > 0
764    );
765  }
766  // ── AUTO vs FIXED (ENG-11546) ────────────────────────────────────────────
767  // A FIXED text box is one the user has sized on purpose: they dragged a handle
768  // or set a vertical alignment, and from then on the rectangle is theirs. The
769  // marker is the \`align-content\` declaration that does the block-axis alignment
770  // — read off the INLINE style, like \`transform\` in styleSnapshot and for the
771  // same reason: it is the value the document STATES, not one a cascade or a UA
772  // default resolved to, so nothing an author's stylesheet says can accidentally
773  // promote a paragraph to fixed.
774  function textVAlignOf(el) {
775    if (!el || !el.style) return null;
776    return verticalAlignFromCss(el.style.getPropertyValue("align-content"));
777  }
778  function isFixedTextBox(el) {
779    return isTextBearing(el) && !!textVAlignOf(el);
780  }
781  // Is this element's geometry driven by its INK rather than its authored box?
782  // True only for an unpainted text leaf that is still AUTO — the case ask ③ is
783  // about. A fixed box is a box: its chrome frames it, its hit test IS it, and
784  // its height is the user's number rather than a measurement (ENG-11546).
785  function usesInkChrome(el) {
786    if (!isTextBearing(el)) return false;
787    if (textVAlignOf(el)) return false;
788    return !paintsBox(el, getComputedStyle(el));
789  }
790  // The box the SELECTION CHROME frames — the one rect the ring, the 8 handles and
791  // the hit-test all read (Andy, 2026-07-29). Both originally trusted
792  // getBoundingClientRect(), i.e. the AUTHORED border box, which broke twice: a box
793  // too SMALL let glyphs paint outside their own blue ring, and an agent-authored
794  // box too LARGE (the full-width "\xa9 2007 Universal Pictures" credit) painted chrome
795  // across the whole artboard for one short centered line.
796  //
797  // So for an unpainted text leaf the measured INK is the geometry — it is right in
798  // BOTH directions, where the earlier box ∪ ink could only ever grow. A PAINTED
799  // text box keeps box ∪ ink (its fill is content; overflow still shows). Non-text
800  // elements keep the plain border box — see the shapes note in compose-text-metrics.
801  // ONE function, called by the hover highlight, the selection ring, the 8
802  // handles and the hit test — see chromeRectFor for why the rule lives in
803  // compose-text-metrics and what happened when hover could not obey it. The
804  // guards below stay here because they need the DOM; they express themselves to
805  // the rule as "there is no ink", and skipping the Range measure is also what
806  // keeps a drag frame cheap.
807  function chromeRect(el) {
808    var r = el.getBoundingClientRect();
809    var box = { left: r.left, top: r.top, width: r.width, height: r.height };
810    if (!isTextBearing(el)) return chromeRectFor(box, null, false);
811    // MID-RESIZE THE CHROME FRAMES THE BOX, NOT THE INK (Andy: "the anchor for
812    // resizing a selection box is not steady yet, it still moves around a bit").
813    //
814    // At rest, a text leaf's chrome is its INK — that is the whole "select the
815    // text really, not the sloppy authored box" behaviour and it stays. But DURING
816    // a resize the ink is a moving target: the words reflow as the box narrows
817    // (FAUVISM → FAUVIS / M), so the measured glyph bounds change
818    // non-monotonically. (Until ENG-11576 the type scaled with the drag too,
819    // which made it worse; the reflow alone is still enough.) The box is anchored
820    // correctly — it is the CHROME drawn from the ink that drifts, which is
821    // exactly what the user sees wobbling. While the gesture owns the box, the
822    // chrome shows the box the gesture is defining; on release the commit
823    // re-seats it onto the settled ink (anchorWrappedRect) and the ink-driven
824    // chrome comes back.
825    if (drag && drag.moved && drag.kind !== "move" && drag.el === el) {
826      return chromeRectFor(box, null, false);
827    }
828    // THE SAME RULE FOR A GROUP RESIZE (SUNRA-1213): while the gesture owns the
829    // boxes, the chrome shows the boxes the gesture is defining. Without this the
830    // union box and its four corners would chase the reflowing ink of every text
831    // member — the exact wobble the guard above exists to stop, multiplied by N.
832    if (gres && gresHolds(el)) return chromeRectFor(box, null, false);
833    // A FIXED TEXT BOX FRAMES ITS BOX (ENG-11546). The ink rule below is the
834    // AUTO answer — "select the text really, not the sloppy authored box" — and
835    // applying it to a box the user sized on purpose contradicts the whole
836    // feature: they widened the selection, and the ring snapped back onto the
837    // words. Caught in the running preview, where the inspector read W 323 / H 99
838    // while the ring on the artboard hugged a 102\xd731 word.
839    //
840    // The BOX exactly, not box ∪ ink: overflowing content is not clipped (Figma),
841    // and a ring grown out to cover it would put the handles somewhere other than
842    // the edges they resize.
843    if (textVAlignOf(el)) return chromeRectFor(box, null, false);
844    // Ink may be unmeasurable (pre-paint / empty) — the rule reads that as "no
845    // ink" and the authored box stands.
846    return chromeRectFor(box, textInkRect(el, document), paintsBox(el, getComputedStyle(el)));
847  }
848  // The element's padding + border, so an ink rect can be converted to the border
849  // box that wraps it exactly (pinAbsolute writes box-sizing: border-box).
850  function insetsOf(el) {
851    var cs = getComputedStyle(el);
852    return {
853      left: (parseFloat(cs.paddingLeft) || 0) + (parseFloat(cs.borderLeftWidth) || 0),
854      right: (parseFloat(cs.paddingRight) || 0) + (parseFloat(cs.borderRightWidth) || 0),
855      top: (parseFloat(cs.paddingTop) || 0) + (parseFloat(cs.borderTopWidth) || 0),
856      bottom: (parseFloat(cs.paddingBottom) || 0) + (parseFloat(cs.borderBottomWidth) || 0),
857    };
858  }
859  // ── THE BLOCK AXIS IS LINE BOXES, AND ONLY A LAYOUT CAN SAY SO ────────────
860  // What the element's own vertical layout does once its authored height is
861  // RELEASED: the natural border-box height, and how far below the border-box top
862  // the first line's ink starts (the top inset plus the half-leading). Both are
863  // read in the same released state, so they describe one consistent layout.
864  //
865  // Measured, not computed: a font's ascent + descent is not derivable from
866  // font-size, so the half-leading — the whole quantity textInkRect cannot see —
867  // has no closed form. See inkPinBox for the defect that taught us that.
868  //
869  // \`height: auto\` rather than removeProperty: an authored height can come from a
870  // stylesheet rule as easily as from the style attribute, and an inline
871  // declaration beats both. The previous value goes back before this returns, and
872  // no paint happens between synchronous style writes in one task, so nothing is
873  // observable — the cost is one extra layout flush on the element being measured.
874  function blockMetricsOf(el) {
875    var s = el.style;
876    var prev = s.getPropertyValue("height");
877    var prio = s.getPropertyPriority("height");
878    s.setProperty("height", "auto");
879    var r = el.getBoundingClientRect();
880    var ink = textInkRect(el, document);
881    if (prev) s.setProperty("height", prev, prio); else s.removeProperty("height");
882    if (!(r.height > 0)) return null;
883    // No ink → no lead to know. 0 is never used as an answer: inkPinBox refuses a
884    // null ink outright, and the resize height path reads only \`height\`.
885    return { height: r.height, lead: ink ? ink.top - r.top : 0 };
886  }
887  // The box to PIN an ink-driven text leaf to: the INK across the inline axis
888  // (grown back out by padding + border, rounded outward so the glyphs keep their
889  // exact size and line breaks) over the element's OWN LINE-BOX layout down the
890  // block axis, placed so the glyphs do not move at all. Returns null when the
891  // element isn't ink-driven or can't be measured — the caller then keeps the
892  // authored box (never mutate on a guess).
893  //
894  // ORDER MATTERS: the ink is measured FIRST, in the element's current state, so
895  // the pin re-seats the box around where the glyphs are RIGHT NOW; blockMetricsOf
896  // then reports what the layout would do, which is a relative quantity and
897  // therefore immune to its own temporary reflow.
898  function inkPinRect(el) {
899    if (!usesInkChrome(el)) return null;
900    return inkPinBox(textInkRect(el, document), insetsOf(el), blockMetricsOf(el));
901  }
902  // The box an ALIGNMENT click freezes to — the ink's INLINE extent over the
903  // element's OWN block extent, and null once the box already fits its text. NOT
904  // inkPinRect: that one derives all four edges from the ink, whose top is the
905  // glyph fragment's rather than the line box's, so re-deriving it on every click
906  // walked the element up the page half a leading at a time. See alignPinBox.
907  function alignPinRect(el) {
908    if (!usesInkChrome(el)) return null;
909    var r = el.getBoundingClientRect();
910    return alignPinBox(
911      { left: r.left, top: r.top, width: r.width, height: r.height },
912      snapOutward(textInkRect(el, document)),
913      insetsOf(el),
914    );
915  }
916
917  // ── content-fitting hit test (ask ③) ────────────────────────────────────
918  // The browser hit-tests the BOX, so a full-width credit line used to swallow
919  // clicks across the whole artboard. The user only sees glyphs, so only the glyphs
920  // (plus a few px of slop) should be clickable — and a miss must SEE THROUGH to
921  // whatever is stacked underneath, or the oversized box would still block it.
922  // Slop is authored in SCREEN px like every other chrome dimension, then divided
923  // back into frame px by the artboard scale (⑪).
924  var HIT_SLOP = 4;
925  function hitsChrome(el, x, y) {
926    var r = chromeRect(el);
927    var s = HIT_SLOP / (curScale > 0 ? curScale : 1);
928    return (
929      x >= r.left - s && x <= r.left + r.width + s &&
930      y >= r.top - s && y <= r.top + r.height + s
931    );
932  }
933  // The element a pointer at (x, y) should act on. Walks the hit stack top-down and
934  // skips any ink-driven text leaf the point misses. Falls back to the browser's own
935  // target when elementsFromPoint is unavailable or the stack yields no candidate,
936  // so this can only ever refine the native answer, never lose it.
937  // An SVG shape (ENG-11305) is one COMPONENT drawn as an <svg> wrapping a
938  // <path>, and the hit test lands on the path — which carries no data-cid, so
939  // selecting it would mint a DOM-path selector the schema cannot address. The
940  // component is the atom here exactly as an arched word's wrapper is: climb to
941  // the owning <svg> root and pick that. (ownerSVGElement is null ON the root,
942  // so this is a no-op for the shape itself and for every HTML element.)
943  function svgHost(n) {
944    return n && n.ownerSVGElement ? n.ownerSVGElement : n;
945  }
946  function pickAt(x, y, fallback) {
947    var stack = null;
948    try {
949      if (document.elementsFromPoint) stack = document.elementsFromPoint(x, y);
950    } catch (err) { stack = null; }
951    if (!stack || !stack.length) { var f0 = svgHost(fallback); return isSpacer(f0) ? null : f0; }
952    for (var i = 0; i < stack.length; i++) {
953      var n = svgHost(stack[i]);
954      if (!n || n.nodeType !== 1) continue;
955      if (SKIP[n.tagName] || isChrome(n)) continue;
956      // A DELETE SPACER IS NOT PICKABLE (ENG-11643). It already carries
957      // 'pointer-events:none', so a real browser's elementsFromPoint rarely
958      // returns it — but 'fallback' (e.target) and a jsdom stack both can, and
959      // "the user selected the hole where their element used to be" is not a
960      // state this editor should be able to reach.
961      if (isSpacer(n)) continue;
962      if (usesInkChrome(n) && !hitsChrome(n, x, y)) continue; // empty part → see through
963      return n;
964    }
965    var fb = svgHost(fallback);
966    return isSpacer(fb) ? null : fb;
967  }
968
969  // ── arched per-letter word grouping ─────────────────────────────────────
970  // A skill-pack poster builds a curved title as a WRAPPER whose children are one
971  // <span> PER LETTER, each with its own transform (the arc). We treat that group
972  // as ONE word: select / drag / style / edit the wrapper, never a lone letter.
973  //
974  // Extract each letter child's authoring data, or null when el is NOT such a
975  // wrapper (per-letter single-char inline leaves carrying transforms).
976  function archLetterData(el) {
977    if (!el || el.nodeType !== 1 || SKIP[el.tagName]) return null;
978    var kids = el.children;
979    if (!kids || kids.length < 2) return null;
980    var data = [];
981    var signals = [];
982    for (var i = 0; i < kids.length; i++) {
983      var k = kids[i];
984      if (isChrome(k)) return null;
985      if (!INLINE[k.tagName]) return null;              // a block child → not letters
986      if (k.children && k.children.length) return null; // letters are leaves
987      var styleAttr = k.getAttribute("style") || "";
988      var split = splitTransformFromStyle(styleAttr);
989      var cls = "";
990      if (k.className && typeof k.className === "string") {
991        cls = k.className.split(/\\s+/).filter(function (c) {
992          return c && c.indexOf("__ppn_") !== 0;
993        }).join(" ");
994      }
995      data.push({
996        tag: k.tagName.toLowerCase(),
997        className: cls,
998        styleRest: split.rest,
999        transform: split.transform,
1000      });
1001      signals.push({ tag: k.tagName.toLowerCase(), text: k.textContent || "", transform: split.transform });
1002    }
1003    if (!isArchLetters(signals)) return null;
1004    return data;
1005  }
1006  // The arched-word WRAPPER for el: el itself if it wraps letters, else its
1007  // parent if el IS one of the letters; null when neither.
1008  function archGroupOf(el) {
1009    if (!el || el.nodeType !== 1) return null;
1010    if (archLetterData(el)) return el;
1011    var p = el.parentElement;
1012    if (p && archLetterData(p)) return p;
1013    return null;
1014  }
1015  // Non-transform text props cascade to the letters (a lone <span> that overrides
1016  // one would otherwise block the wrapper's inherited value); geometry / transform
1017  // stay on the wrapper alone.
1018  var ARCH_CASCADE_SKIP = {
1019    transform: 1, position: 1, left: 1, top: 1, right: 1, bottom: 1,
1020    width: 1, height: 1, margin: 1,
1021  };
1022  // Push the applied text props onto any letter that sets them inline (so an
1023  // authored per-letter override doesn't leave that letter behind). Returns true
1024  // when a letter actually changed — the caller then persists the wrapper's new
1025  // innerHTML via a text-commit. When no letter overrides, the wrapper's own
1026  // style change cascades by inheritance and no innerHTML persist is needed.
1027  function cascadeArchStyle(wrapper, styles) {
1028    var props = Object.keys(styles || {});
1029    var kids = wrapper.children;
1030    var changed = false;
1031    for (var i = 0; i < kids.length; i++) {
1032      var k = kids[i];
1033      if (!k.style) continue;
1034      for (var p = 0; p < props.length; p++) {
1035        var prop = props[p];
1036        if (ARCH_CASCADE_SKIP[prop]) continue;
1037        if (k.style.getPropertyValue(prop) === "") continue; // no override → inherits
1038        var v = styles[prop];
1039        if (v === "" || v == null) k.style.removeProperty(prop);
1040        else k.style.setProperty(prop, v);
1041        changed = true;
1042      }
1043    }
1044    return changed;
1045  }
1046
1047  function clearHover() {
1048    if (hoverBox) {
1049      if (hoverBox.parentNode) hoverBox.parentNode.removeChild(hoverBox);
1050      hoverBox = null;
1051    }
1052    if (hovered) {
1053      hovered.classList.remove("__ppn_hover");
1054      hovered = null;
1055    }
1056  }
1057  // Draw the hover highlight on the rect the click would actually hand over.
1058  // Same chromeRect() the ring and the handles read, so the two cannot disagree
1059  // for any element kind — that identity is the fix, not a coincidence to keep
1060  // an eye on.
1061  function positionHoverBox() {
1062    if (!hovered) return;
1063    if (!hoverBox) {
1064      hoverBox = document.createElement("div");
1065      hoverBox.className = "__ppn_hoverbox";
1066      document.body.appendChild(hoverBox);
1067    }
1068    // THE HOVER OUTLINE TURNS TOO (ENG-11582). It is the same promise the ring
1069    // makes one click later — "this is the box you are about to be holding" — so
1070    // it has to be the same box. Left on chromeRect it would draw the upright
1071    // hull over a rotated element and then be replaced, on the click, by a ring
1072    // that visibly does not match it.
1073    var q = chromeQuad(hovered);
1074    hoverBox.style.left = q.left + "px";
1075    hoverBox.style.top = q.top + "px";
1076    hoverBox.style.width = q.width + "px";
1077    hoverBox.style.height = q.height + "px";
1078    turnChrome(hoverBox, q);
1079  }
1080
1081  // ── classification + style snapshot (for the contextual format bar) ─────
1082  function isRootSheet(el) {
1083    // The outermost body child = the page sheet. Never drag / resize it, but its
1084    // fill IS editable (page background).
1085    return el && el.parentElement === document.body;
1086  }
1087  function hasFill(el, cs) {
1088    var bg = cs.backgroundColor;
1089    return !!bg && bg !== "transparent" && bg.indexOf("rgba(0, 0, 0, 0)") < 0;
1090  }
1091  function styleSnapshot(el) {
1092    var cs = getComputedStyle(el);
1093    return {
1094      fontFamily: cs.fontFamily || "",
1095      fontSize: cs.fontSize || "",
1096      fontWeight: cs.fontWeight || "",
1097      fontStyle: cs.fontStyle || "",
1098      textDecorationLine: (cs.textDecorationLine || cs.textDecoration || ""),
1099      textAlign: cs.textAlign || "",
1100      lineHeight: cs.lineHeight || "",
1101      letterSpacing: cs.letterSpacing || "",
1102      color: cs.color || "",
1103      backgroundColor: cs.backgroundColor || "",
1104      // THE INSPECTOR'S SEVEN (ENG-11278) — see PickerElementStyle for why the
1105      // transform is read off the INLINE style and everything else off the
1106      // computed one.
1107      opacity: cs.opacity || "",
1108      transform: el.style.transform || "",
1109      alignContent: el.style.getPropertyValue("align-content") || "",
1110      zIndex: cs.zIndex || "",
1111      borderRadius: cs.borderRadius || "",
1112      objectFit: cs.objectFit || "",
1113      imageGeometry: el.tagName === "IMG" ? {
1114        width: parseFloat(cs.width) || el.clientWidth,
1115        height: parseFloat(cs.height) || el.clientHeight,
1116        naturalWidth: el.naturalWidth || 0,
1117        naturalHeight: el.naturalHeight || 0,
1118        fit: cs.objectFit || "fill",
1119        position: cs.objectPosition || "50% 50%",
1120        crop: parseViewBox(cs.getPropertyValue("object-view-box")),
1121      } : undefined,
1122      borderWidth: cs.borderWidth || "",
1123      borderColor: cs.borderColor || "",
1124    };
1125  }
1126  function metaOf(el) {
1127    var cs = getComputedStyle(el);
1128    var isImg = el.tagName === "IMG";
1129    var text = isTextBearing(el);
1130    // Fill applies to a rule, an image-less block/shape, or anything already
1131    // carrying a background — i.e. NOT a plain text leaf (unless it has a bg).
1132    var fill = !isImg && (el.tagName === "HR" || !text || hasFill(el, cs));
1133    return {
1134      text: text,
1135      fill: fill,
1136      image: isImg,
1137      root: isRootSheet(el),
1138      src: isImg ? (el.currentSrc || el.src || "") : undefined,
1139      shape: el.getAttribute("data-shape") || undefined,
1140      style: styleSnapshot(el),
1141    };
1142  }
1143
1144  // ── multi-select set ──────────────────────────────────────────────────
1145  function elemInfo(el) {
1146    // TEXT AND ALT RIDE ALONG (ENG-11666) — the chip in the composer and the one
1147    // in the transcript are both built from this, and without the element's own
1148    // words the only human-facing fact either had was the CSS selector.
1149    // Trimmed, whitespace-collapsed and capped here rather than at render time:
1150    // it crosses a postMessage and then the wire, and a chip shows ~24 chars of it.
1151    var txt = (el.textContent || "").replace(/\\s+/g, " ").trim().slice(0, 160);
1152    var alt = el.tagName === "IMG" ? (el.getAttribute("alt") || "").trim().slice(0, 160) : "";
1153    return {
1154      selector: selectorFor(el),
1155      snippet: (el.outerHTML || "").slice(0, 2000),
1156      tag: el.tagName.toLowerCase(),
1157      text: txt || undefined,
1158      alt: alt || undefined,
1159      meta: metaOf(el),
1160    };
1161  }
1162  function applyOutlines() {
1163    var marked = document.querySelectorAll(".__ppn_selected");
1164    for (var i = 0; i < marked.length; i++) marked[i].classList.remove("__ppn_selected");
1165    for (var j = 0; j < selectedSet.length; j++) selectedSet[j].classList.add("__ppn_selected");
1166    syncHandles();
1167  }
1168  function postSelection() {
1169    var els = [];
1170    for (var i = 0; i < selectedSet.length; i++) els.push(elemInfo(selectedSet[i]));
1171    post({ type: "selection", elements: els });
1172    postSelectionRect();
1173  }
1174  // The selection's chrome box, for the parent's floating format bar. The UNION
1175  // over a multi-select, so a bar over three selected elements hovers above the
1176  // group rather than above whichever one happened to be clicked first.
1177  //
1178  // chromeRect() and nothing else: it is the exact box the ring and the handles
1179  // are drawn at, so the bar is anchored to what the user can SEE selected — not
1180  // to the authored border box, which for a text leaf is routinely nothing like it.
1181  function selectionChromeRect() {
1182    if (!selectedSet.length) return null;
1183    var l = Infinity, t = Infinity, r = -Infinity, b = -Infinity;
1184    for (var i = 0; i < selectedSet.length; i++) {
1185      var c = chromeRect(selectedSet[i]);
1186      if (!c) continue;
1187      if (c.left < l) l = c.left;
1188      if (c.top < t) t = c.top;
1189      if (c.left + c.width > r) r = c.left + c.width;
1190      if (c.top + c.height > b) b = c.top + c.height;
1191    }
1192    if (l === Infinity) return null;
1193    return { left: l, top: t, width: r - l, height: b - t };
1194  }
1195  function postSelectionRect() {
1196    // \`gesture\` hides the parent's floating format bar. A crop session counts:
1197    // the bar would sit over the picture being framed, and every control on it
1198    // is about a selection that is, for the duration, not what the pointer means.
1199    // A GROUP RESIZE and a LIVE BAND count as gestures too (SUNRA-1213): the bar
1200    // would sit over a box that is changing under it every frame, and over a band
1201    // it would be a panel of controls for a selection the user is still drawing.
1202    post({ type: "selection-rect", rect: selectionChromeRect(), loading: selectionIsLoading(), gesture: !!drag || !!gres || !!marqueeEl || !!cropSess || !!(cframe && cframe.g) });
1203  }
1204  function clearSelection(notify) {
1205    selectedSet = [];
1206    applyOutlines();
1207    if (notify) postSelection();
1208  }
1209  function toggleInSet(el) {
1210    var idx = selectedSet.indexOf(el);
1211    if (idx >= 0) selectedSet.splice(idx, 1);
1212    else selectedSet.push(el);
1213  }
1214
1215  // ── delete selection ────────────────────────────────────────────────────
1216  // Remove every currently-selected element from the LIVE document, then post
1217  // ONE "remove" (the full selector set) so the parent replays the SAME removal
1218  // into the stored html/schema through the self-edit commit path — a single undo
1219  // step restores a multi-delete. Selectors are captured BEFORE any detach (a
1220  // legacy DOM-path selector reflects the original tree); the root sheet / body /
1221  // html are never removable (deleting the page is not a Delete-key action).
1222  //
1223  // NOTHING ELSE MOVES (ENG-11643). An element in normal flow is not detached but
1224  // REPLACED by an invisible same-box spacer, so its neighbours stay where they
1225  // are — we have no auto-layout, and a delete that reflows the page is the one
1226  // outcome the ruling forbids. See buildSpacerFor for what a spacer is and which
1227  // elements do not get one.
1228  function removeSelection() {
1229    if (!selectedSet.length) return;
1230    var targets = [];
1231    for (var i = 0; i < selectedSet.length; i++) {
1232      var el = selectedSet[i];
1233      if (!el || el.nodeType !== 1) continue;
1234      if (isRootSheet(el) || el === document.body || el === document.documentElement) continue;
1235      targets.push(el);
1236    }
1237    if (!targets.length) return;
1238    var selectors = [];
1239    for (var s = 0; s < targets.length; s++) selectors.push(selectorFor(targets[s]));
1240    // MEASURE FIRST, DETACH SECOND. Every spacer's box is read while the whole
1241    // selection is still in the document: measuring inside the removal loop
1242    // would size the second spacer against a page the first removal had already
1243    // reflowed.
1244    var spacers = [];
1245    for (var m = 0; m < targets.length; m++) spacers.push(buildSpacerFor(targets[m]));
1246    var payload = [];
1247    for (var d = 0; d < targets.length; d++) {
1248      var tEl = targets[d];
1249      var sp = spacers[d];
1250      if (!tEl.parentNode) { payload.push(null); continue; }
1251      if (sp) {
1252        tEl.parentNode.replaceChild(sp, tEl);
1253        payload.push({ selector: "#" + CSS.escape(sp.id), html: sp.outerHTML });
1254      } else {
1255        tEl.parentNode.removeChild(tEl);
1256        payload.push(null);
1257      }
1258    }
1259    selectedSet = [];
1260    clearHover();
1261    applyOutlines(); // clears selection outlines + drops the resize handles
1262    post({ type: "remove", selectors: selectors, spacers: payload });
1263    postSelection(); // empty set → parent drops the chip + hides the format bar
1264    postDocSize();   // removal may shrink the board
1265  }
1266
1267  // ── THE SPACER A DELETE LEAVES BEHIND (ENG-11643) ───────────────────────
1268  //
1269  // Andy, 2026-09-07: "我们还没做 auto-layout,删除后其它东西必须留在原地."
1270  // A director-authored one-pager is normal document flow, so removing a node
1271  // slides every following sibling up the page. Until there IS an auto-layout
1272  // model, the only honest answer is that the box stays: the element is replaced
1273  // by an invisible, empty, non-interactive element reproducing its box exactly.
1274  //
1275  // SAME TAG, deliberately, not the '<span>' a first reading suggests.
1276  // 'selectorFor' addresses a legacy element by a 'tag[.class]:nth-of-type(n)'
1277  // path, and nth-of-type counts by TAG — so swapping an <h2> for a <span> would
1278  // silently renumber every later <h2> on the page and break the stored
1279  // selectors of elements nobody touched. Keeping the tag also keeps the
1280  // author's own 'h2 + p' / ':nth-child' rules matching, which is more of the
1281  // layout held still for free. Nothing of the original comes with it: no
1282  // data-cid (it is not a component), no classes, no content.
1283  //
1284  // NO SPACER FOR AN ELEMENT THAT WAS NOT IN FLOW. Anything already dragged is
1285  // 'position:absolute' (see pinAbsolute / pinForGesture) and holds no space at
1286  // all; a spacer there would be an invisible box the layout never had. Same for
1287  // 'fixed', and for an element with no box to hold (display:none, zero rect).
1288  function buildSpacerFor(el) {
1289    var cs = null;
1290    try { cs = getComputedStyle(el); } catch (err) { cs = null; }
1291    if (!cs) return null;
1292    var pos = cs.position;
1293    if (pos === "absolute" || pos === "fixed") return null;
1294    var disp = cs.display;
1295    if (!disp || disp === "none" || disp === "contents") return null;
1296    // The LAYOUT box, which is what the neighbours were placed against. For a
1297    // transformed element getBoundingClientRect is the rotated hull — a shadow
1298    // the flow never saw — so the un-transformed border box is read instead
1299    // (the same distinction localRectOf draws for the resize anchor).
1300    var rect = el.getBoundingClientRect();
1301    var w = rect.width, h = rect.height;
1302    var xf = cs.transform;
1303    if (xf && xf !== "none") { w = el.offsetWidth; h = el.offsetHeight; }
1304    if (!(w > 0) && !(h > 0)) return null;
1305    var sp = document.createElement(el.tagName);
1306    sp.setAttribute(SPACER_ATTR, "1");
1307    // An id, because the undo has to ADDRESS this node to swap the original back
1308    // into its slot — and a class/nth-of-type path would be re-derived against a
1309    // tree the restore is about to change.
1310    sp.id = "ppn-sp-" + Date.now().toString(36) + Math.floor(Math.random() * 1e6).toString(36);
1311    var style =
1312      "display:" + (disp === "inline" ? "inline-block" : disp) + ";" +
1313      "box-sizing:border-box;" +
1314      "width:" + w + "px;height:" + h + "px;";
1315    // MARGINS ARE PART OF THE HOLE. getBoundingClientRect stops at the border
1316    // box, but the gap the neighbours sit at includes the removed element's own
1317    // margins (and their collapsing with the siblings'), so they are carried
1318    // over verbatim rather than zeroed.
1319    var m = [cs.marginTop, cs.marginRight, cs.marginBottom, cs.marginLeft];
1320    style += "margin:" + m[0] + " " + m[1] + " " + m[2] + " " + m[3] + ";";
1321    // A float takes its element out of normal flow sideways; a non-floated
1322    // spacer in its place would push the text that used to wrap around it.
1323    if (cs.cssFloat && cs.cssFloat !== "none") style += "float:" + cs.cssFloat + ";";
1324    style += "visibility:hidden;pointer-events:none;";
1325    sp.setAttribute("style", style);
1326    return sp;
1327  }
1328
1329  // ── drag (reposition) + resize (⑧) ─────────────────────────────────────
1330  // A single non-root selection gets 8 resize handles and can be dragged to
1331  // reposition. The gesture is handled ENTIRELY inside the frame: pointer deltas
1332  // here are in the frame's OWN coordinate space, which is UNSCALED — the
1333  // artboard's CSS scale lives on an ANCESTOR of the iframe, and the browser maps
1334  // screen coords back through that transform into the frame, so a delta already
1335  // respects zoom (no divide-by-scale; the mirror of the wheel-forward note).
1336  var DIRS = ["nw", "n", "ne", "e", "se", "s", "sw", "w"];
1337  // The four sides, in the order the bands are built. A SUBSET of DIRS on
1338  // purpose: a band only exists for a direction the element actually offers, so
1339  // the invisible tolerance can never promise a gesture the visible chrome does
1340  // not (a contain image shows corners only, and gets no side bands either).
1341  var EDGE_DIRS = ["n", "e", "s", "w"];
1342  // Band thickness in SCREEN px, straddling the edge (half out, half in), and
1343  // the amount kept clear at each corner so a corner drag always wins there.
1344  // Both are screen measurements divided back into frame px by chromeUnit, the
1345  // same way --ppn-s does it for every other piece of chrome.
1346  var EDGE_BAND = 7;
1347  var EDGE_CORNER = 11;
1348  // One screen pixel, in the frame's own units. curScale is the artboard's
1349  // on-screen scale; the stylesheet gets 1/curScale as --ppn-s, and script that
1350  // has to MEASURE rather than merely declare needs the same number.
1351  function chromeUnit() { return curScale > 0 ? 1 / curScale : 1; }
1352  // ── THE SELECTION IS A QUAD, NOT A BOX (ENG-11582) ───────────────────────
1353  //
1354  // Andy's screenshot, 2026-09-07: a rotated headline wearing an upright blue
1355  // rectangle that touches none of its corners. That ring is chromeRect(), i.e.
1356  // getBoundingClientRect(), i.e. the AXIS-ALIGNED HULL of the rotated box — the
1357  // same shadow localRectOf() exists to avoid for the resize anchor. It was
1358  // survivable while the ring was only decoration; it is not survivable now,
1359  // because a rotate zone has to hang off a REAL corner and the hull has none.
1360  //
1361  // So the chrome is placed from three numbers instead of four: the box's CENTRE
1362  // (viewport px), its own UN-ROTATED size, and its ANGLE. The centre is read off
1363  // the hull and that is exact rather than approximate — rotate() and scaleX()
1364  // both act about \`transform-origin\`, which schema-render never sets and which
1365  // therefore defaults to the box's centre, and a linear map about the centre
1366  // leaves the centre where it was. The size is offsetWidth/Height, the border
1367  // box BEFORE the transform, which is the space an absolute left/top is written
1368  // in (the same pair beginDrag pins a transformed element to).
1369  //
1370  // An AXIS-ALIGNED element takes chromeRect() unchanged — the ink-measured box
1371  // for a text leaf, the plain border box otherwise — so nothing about the
1372  // overwhelmingly common case moves by a pixel. The two branches are not
1373  // interchangeable and that asymmetry is deliberate: a rotated element's ink
1374  // rect is measured off Range rects, which are as rotated as the element is, so
1375  // there is no ink measurement to have (see beginDrag's note on the same point).
1376  function angleOfEl(el) {
1377    var xf = null;
1378    try { xf = transformOf(el, localRectOf(el)); } catch (err) { return 0; }
1379    if (!xf) return 0;
1380    return angleFromMatrix(xf.a, xf.b, xf.c, xf.d);
1381  }
1382  // Is the element MIRRORED? Off the determinant of its own linear map, not off
1383  // the transform string: \`scaleX(-1)\` flips the handedness of the plane, so
1384  // \`a\xb7d − b\xb7c\` is negative for a mirrored element and positive for any pure
1385  // rotation, whatever spelling or whitespace the engine hands back. The rotate
1386  // drag has to carry the mirror through untouched (transformCss writes ONE
1387  // declaration and dropping half of it silently un-mirrors the element), and a
1388  // string match is exactly the kind of test that survives review and then fails
1389  // on the one browser that normalizes the value differently.
1390  function flipOfEl(el) {
1391    var xf = null;
1392    try { xf = transformOf(el, localRectOf(el)); } catch (err) { return false; }
1393    if (!xf) return false;
1394    return (xf.a * xf.d - xf.b * xf.c) < 0;
1395  }
1396  function chromeQuad(el) {
1397    var ang = angleOfEl(el);
1398    if (!ang) {
1399      var r = chromeRect(el);
1400      return {
1401        left: r.left, top: r.top, width: r.width, height: r.height,
1402        cx: r.left + r.width / 2, cy: r.top + r.height / 2, angle: 0,
1403      };
1404    }
1405    var h = el.getBoundingClientRect();
1406    var cx = h.left + h.width / 2, cy = h.top + h.height / 2;
1407    var w = el.offsetWidth, ht = el.offsetHeight;
1408    return { left: cx - w / 2, top: cy - ht / 2, width: w, height: ht, cx: cx, cy: cy, angle: ang };
1409  }
1410  // Turn one chrome node with the element. The node is laid out AS IF upright
1411  // (its left/top/width/height are the un-rotated rect) and then rotated about
1412  // the SELECTION's centre — not its own — which is what keeps four separate
1413  // rects rigid with respect to each other. Expressed as a transform-origin in
1414  // the node's own coordinates, because that is the only way CSS lets a box turn
1415  // about a point outside itself.
1416  function turnChrome(node, q) {
1417    var st = node.style;
1418    if (!q.angle) { st.transformOrigin = ""; st.transform = ""; return; }
1419    st.transformOrigin = (q.cx - parseFloat(st.left || "0")) + "px " + (q.cy - parseFloat(st.top || "0")) + "px";
1420    st.transform = "rotate(" + q.angle + "deg)";
1421  }
1422  // AN IMAGE GETS CORNERS ONLY. Its resize is ratio-locked (see beginDrag), and
1423  // an n/s/e/w handle on a ratio-locked box is a lie told by the chrome: it
1424  // offers a one-axis change and then moves both. Removing them makes the
1425  // affordance and the behaviour say the same thing — every handle an image
1426  // shows resizes it proportionally, which is also the convention every design
1427  // tool uses for placed art.
1428  var IMG_DIRS = ["nw", "ne", "se", "sw"];
1429  // ── A TEXT LEAF GETS ALL EIGHT (ENG-11546) ───────────────────────────────
1430  // THE RULING THIS REVISES: an ink-driven text leaf showed six handles, no n/s,
1431  // because "its height is MEASURED off the ink — a top/bottom handle has nothing
1432  // it can change". That was a true statement about the AUTO model and it is the
1433  // reason Andy could not drag the top or bottom edge of a text box (追加 1, bug ①).
1434  //
1435  // Under the fixed-size model the premise is gone: a drag on ANY handle converts
1436  // the element to a fixed box, so the height IS a free variable the moment the
1437  // gesture starts, and n/s deliver exactly the change they promise. The chrome
1438  // and the behaviour still say the same thing — which was the whole content of
1439  // the old rule — they now say a bigger one.
1440  /** The element's resolved object-fit, or "" for anything that is not an image. */
1441  function imageFitOf(el) {
1442    if (!el || el.tagName !== "IMG") return "";
1443    try { return getComputedStyle(el).objectFit || ""; } catch (err) { return ""; }
1444  }
1445  /**
1446   * Is this image a FRAME — a window its picture fills — rather than a picture
1447   * that happens to have a box?
1448   *
1449   * THE RULING THIS REVISES (Andy, 2026-08-01): "a photograph has ONE correct
1450   * shape", so an image resized proportionally and showed corners only. That was
1451   * right about the DEFECT — a free-form drag on a fill image squashes the
1452   * photograph — and wrong about the remedy, because it also removed the only
1453   * gesture that can fix a badly-placed picture. An agent lays a saxophone into a
1454   * poster, a thin slice of it shows, and with a ratio-locked box there is
1455   * nothing the user can do about it but regenerate the image.
1456   *
1457   * object-fit: cover already forbids the distortion the lock was defending
1458   * against: whatever shape the box takes, the picture fills it without
1459   * stretching, and the box's shape decides WHICH PART shows. So for a cover
1460   * image the lock buys nothing and costs the whole feature, and it goes — the
1461   * eight handles come back and a drag reveals or hides picture.
1462   *
1463   * contain keeps the lock, because there the old ruling still holds verbatim:
1464   * contain means "show the whole picture", its one correct box shape IS the
1465   * picture's, and a free drag can only letterbox it. fill (the UA default,
1466   * and what an image with no fit at all resolves to) is the distorting case and
1467   * is normalized to cover the moment a resize begins — see beginDrag.
1468   */
1469  function isFramedImage(el) {
1470    var f = imageFitOf(el);
1471    return f === "cover" || f === "fill" || f === "scale-down" || f === "none";
1472  }
1473  function dirsFor(el) {
1474    if (el && el.tagName === "IMG") return isFramedImage(el) ? DIRS : IMG_DIRS;
1475    return DIRS;
1476  }
1477  function cursorFor(dir) {
1478    if (dir === "nw" || dir === "se") return "nwse-resize";
1479    if (dir === "ne" || dir === "sw") return "nesw-resize";
1480    if (dir === "n" || dir === "s") return "ns-resize";
1481    return "ew-resize";
1482  }
1483  // Do the live handles already spell the wanted SET? "Some handles exist" is not
1484  // the same question, and answering that one instead is how selecting an image
1485  // right after a text block hands the image the stale 8-handle chrome back.
1486  function handlesMatch(dirs) {
1487    if (handles.length !== dirs.length) return false;
1488    for (var i = 0; i < handles.length; i++) {
1489      if (handles[i].getAttribute("data-dir") !== dirs[i]) return false;
1490    }
1491    return true;
1492  }
1493  function ensureHandles(el) {
1494    var dirs = dirsFor(el);
1495    if (handlesMatch(dirs)) return;
1496    removeHandles(); // the set CHANGED — rebuild rather than patch
1497    for (var i = 0; i < dirs.length; i++) {
1498      var h = document.createElement("div");
1499      h.className = "__ppn_handle";
1500      h.setAttribute("data-dir", dirs[i]);
1501      h.style.cursor = cursorFor(dirs[i]);
1502      document.body.appendChild(h);
1503      handles.push(h);
1504    }
1505  }
1506  function removeHandles() {
1507    for (var i = 0; i < handles.length; i++) {
1508      if (handles[i].parentNode) handles[i].parentNode.removeChild(handles[i]);
1509    }
1510    handles = [];
1511    // The bands belong to the handles and are torn down with them, from this ONE
1512    // place. They are a second set of nodes tracking the same box, which is
1513    // exactly the shape a sync gap takes: handles gone, invisible strips left
1514    // behind, presses landing on a selection that is no longer there.
1515    removeEdges();
1516    // The rotate zones belong to the handles too, and are torn down from this
1517    // ONE place for the identical reason (ENG-11582). Every state that stands the
1518    // handles down — a crop session, a crop frame, inline text editing, a
1519    // multi-selection, a busy element — therefore stands the rotate gesture down
1520    // with them, without any of those call sites having to know it exists.
1521    removeRotZones();
1522  }
1523  // ── EDGE HIT BANDS ───────────────────────────────────────────────────────
1524  // The sides of the selection, made draggable. See the .__ppn_edge CSS note for
1525  // why they are invisible and the edgeBandRect docblock for the corner rule.
1526  var edges = [];
1527  // Only the sides this element really offers — dirsFor is the single source of
1528  // truth for that, so the bands cannot drift from the handles.
1529  function edgeDirsFor(el) {
1530    var dirs = dirsFor(el), out = [];
1531    for (var i = 0; i < EDGE_DIRS.length; i++) {
1532      if (dirs.indexOf(EDGE_DIRS[i]) >= 0) out.push(EDGE_DIRS[i]);
1533    }
1534    return out;
1535  }
1536  function edgesMatch(dirs) {
1537    if (edges.length !== dirs.length) return false;
1538    for (var i = 0; i < edges.length; i++) {
1539      if (edges[i].getAttribute("data-dir") !== dirs[i]) return false;
1540    }
1541    return true;
1542  }
1543  function ensureEdges(el) {
1544    var dirs = edgeDirsFor(el);
1545    if (edgesMatch(dirs)) return;
1546    for (var i = 0; i < edges.length; i++) {
1547      if (edges[i].parentNode) edges[i].parentNode.removeChild(edges[i]);
1548    }
1549    edges = [];
1550    for (var j = 0; j < dirs.length; j++) {
1551      var b = document.createElement("div");
1552      b.className = "__ppn_edge";
1553      b.setAttribute("data-dir", dirs[j]);
1554      document.body.appendChild(b);
1555      edges.push(b);
1556    }
1557  }
1558  function removeEdges() {
1559    for (var i = 0; i < edges.length; i++) {
1560      if (edges[i].parentNode) edges[i].parentNode.removeChild(edges[i]);
1561    }
1562    edges = [];
1563  }
1564  // Place every band off the SAME chromeRect the ring and the handles use, so
1565  // what the cursor promises at the edge is the edge the drag will move.
1566  function positionEdges(el) {
1567    if (!edges.length) return;
1568    var q = chromeQuad(el), u = chromeUnit();
1569    var t = EDGE_BAND * u, c = EDGE_CORNER * u;
1570    for (var i = 0; i < edges.length; i++) {
1571      var b = edges[i];
1572      var band = edgeBandRect(q, b.getAttribute("data-dir"), t, c);
1573      // A box barely bigger than its own corner zones is ALL corner. Hidden
1574      // rather than clamped: a zero-width strip is invisible either way, and a
1575      // clamped one would creep over the corners it exists to stay out of.
1576      if (!band) { b.style.display = "none"; continue; }
1577      b.style.display = "block";
1578      b.style.left = band.left + "px";
1579      b.style.top = band.top + "px";
1580      b.style.width = band.width + "px";
1581      b.style.height = band.height + "px";
1582      // Turned with the element, off the SELECTION's centre (ENG-11582): a band
1583      // is derived from the un-rotated box, so on a rotated element it would
1584      // otherwise lie along an edge the box does not have — the tolerance
1585      // promising a drag somewhere the element is not.
1586      turnChrome(b, q);
1587    }
1588  }
1589  // ── ROTATE ZONES (ENG-11582) ─────────────────────────────────────────────
1590  // Four invisible squares, one just outside each corner. Nodes rather than a
1591  // hit test on pointermove, for the same three reasons the edge bands are
1592  // nodes: the CURSOR comes from a stylesheet rule instead of from script, the
1593  // press resolves by \`e.target\` in onDown like every other chrome gesture, and
1594  // an idle canvas pays nothing per mouse move.
1595  var rotzones = [];
1596  function ensureRotZones() {
1597    if (rotzones.length === ROTATE_DIRS.length && rotzones[0].isConnected) return;
1598    removeRotZones();
1599    for (var i = 0; i < ROTATE_DIRS.length; i++) {
1600      var z = document.createElement("div");
1601      z.className = "__ppn_rotzone";
1602      z.setAttribute("data-dir", ROTATE_DIRS[i]);
1603      document.body.appendChild(z);
1604      rotzones.push(z);
1605    }
1606  }
1607  function removeRotZones() {
1608    for (var i = 0; i < rotzones.length; i++) {
1609      if (rotzones[i].parentNode) rotzones[i].parentNode.removeChild(rotzones[i]);
1610    }
1611    rotzones = [];
1612    // The cursor is a hover STATE, and the thing it was hovering just went away.
1613    // Left set, it would paint a rotate cursor over a selection that no longer
1614    // has zones — the exact shape of the stuck-marquee class of bug.
1615    document.body.classList.remove("__ppn_rotate");
1616  }
1617  function positionRotZones(q) {
1618    if (!rotzones.length) return;
1619    var reach = ROTATE_REACH * chromeUnit();
1620    for (var i = 0; i < rotzones.length; i++) {
1621      var z = rotzones[i];
1622      var r = rotateZoneRect(q, z.getAttribute("data-dir"), reach);
1623      if (!r) { z.style.display = "none"; continue; }
1624      z.style.display = "block";
1625      z.style.left = r.left + "px";
1626      z.style.top = r.top + "px";
1627      z.style.width = r.width + "px";
1628      z.style.height = r.height + "px";
1629      turnChrome(z, q);
1630    }
1631  }
1632  // The one element eligible for drag/resize right now, or null. Never the root
1633  // sheet (page background is repositionable-into but not itself movable).
1634  // The single element that gets RESIZE HANDLES — and, because it is the same
1635  // question, the one a handle press resizes.
1636  //
1637  // BUSY ELEMENTS ARE EXCLUDED, and only from here (Andy 2026-08-08: "even in
1638  // this state i should be able to move the image around, just not interact with
1639  // it in any other way"). MOVE does not come through this function — it comes
1640  // off \`pickAt\` in onDown — so dropping out here takes away exactly the
1641  // resize affordance and leaves select + drag intact. Resizing is refused
1642  // because the op is about to replace the picture at a size it chose; a width
1643  // the user set mid-flight would be silently discarded a second later.
1644  function draggableSel() {
1645    // A crop session replaces the selection chrome wholesale, so nothing here is
1646    // draggable or resizable while one is open — its own handles are.
1647    if (!active || editingEl || marqueeEl || cropSess) return null;
1648    if (selectedSet.length !== 1) return null;
1649    var el = selectedSet[0];
1650    if (!el || el.nodeType !== 1) return null;
1651    if (isRootSheet(el) || el === document.body || el === document.documentElement) return null;
1652    if (isBusy(el)) return null;
1653    return el;
1654  }
1655  // ── selection ring overlays (Andy, 2026-07-29) ──────────────────────────
1656  // One fixed-position rect per selected element, painted at chromeRect() so the
1657  // stroke fits the RENDERED text (see the .__ppn_ring CSS note). They are chrome:
1658  // isChrome() matches them, so they never hover / select / marquee / commit.
1659  function removeRings() {
1660    for (var i = 0; i < rings.length; i++) {
1661      if (rings[i].parentNode) rings[i].parentNode.removeChild(rings[i]);
1662    }
1663    rings = [];
1664  }
1665  // THE RING TURNS WITH THE ELEMENT (ENG-11582). See chromeQuad: it is laid out
1666  // as the element's own un-rotated box and then turned about that box's centre,
1667  // so a 12\xb0 headline wears a 12\xb0 ring whose four corners are the four corners
1668  // the user sees — which is what the rotate zones hang off and what made the
1669  // upright hull in Andy's screenshot untenable.
1670  function positionRings() {
1671    for (var i = 0; i < rings.length && i < selectedSet.length; i++) {
1672      var q = chromeQuad(selectedSet[i]);
1673      var st = rings[i].style;
1674      st.left = q.left + "px";
1675      st.top = q.top + "px";
1676      st.width = q.width + "px";
1677      st.height = q.height + "px";
1678      turnChrome(rings[i], q);
1679    }
1680  }
1681  // ── THE GROUP BOX — one union rect over a multi-selection ────────────────
1682  //
1683  // Andy 2026-08-08: several elements picked read as N unrelated rectangles.
1684  // What a multi-selection IS, is one thing you are about to act on, and every
1685  // canvas tool says so with a single box enclosing the lot.
1686  //
1687  // It does NOT replace the per-element rings. The union box answers "what am I
1688  // holding"; the rings answer "what is in it" — and over a sparse selection
1689  // (two corners of a page) the box alone would claim everything between them.
1690  // Figma draws both for the same reason.
1691  //
1692  // GEOMETRY IS NOT NEW: \`selectionChromeRect()\` has computed this exact union
1693  // since the format bar needed somewhere to hover. Drawing it here means the
1694  // box and the bar above it are the same rect by construction, not two
1695  // measurements that agree until one of them drifts.
1696  //
1697  // Single selection draws NO group box: the element's own ring is already that
1698  // box, and a second stroke on top of it is just a thicker stroke.
1699  function syncGroupBox() {
1700    // \`groupMembers()\`, not \`selectedSet.length\`: the box and its four corners
1701    // exist to offer the group gesture, so they stand down when fewer than two
1702    // elements can actually take it (see \`groupChromeRect\`).
1703    var members = groupMembers();
1704    var r = members.length > 1 ? groupChromeRect(members) : null;
1705    if (!r) {
1706      if (groupBox && groupBox.parentNode) groupBox.parentNode.removeChild(groupBox);
1707      groupBox = null;
1708      // The four corner handles belong to the box and go with it (SUNRA-1213),
1709      // from this ONE place — the same discipline removeHandles follows for the
1710      // bands and the rotate zones, and for the same reason: two node sets
1711      // tracking one rect is exactly the shape a sync gap takes.
1712      removeGroupHandles();
1713      return;
1714    }
1715    // \`isConnected\`, not just null: the overlays are BODY CHILDREN, so an html
1716    // re-graft (\`document.body.innerHTML = html\` on an asset swap / insert /
1717    // undo) destroys them while this variable still points at the detached node.
1718    // Styling that would paint nothing, silently.
1719    if (!groupBox || !groupBox.isConnected) {
1720      groupBox = document.createElement("div");
1721      groupBox.className = "__ppn_groupbox";
1722      document.body.appendChild(groupBox);
1723    }
1724    var st = groupBox.style;
1725    st.left = r.left + "px";
1726    st.top = r.top + "px";
1727    st.width = r.width + "px";
1728    st.height = r.height + "px";
1729    // The corners ride the SAME single entry point the box does, so they cannot
1730    // develop a sync gap the box does not have — a reflow, a pan, a zoom, a drag
1731    // frame and an html re-graft all land here (SUNRA-1213).
1732    syncGroupHandles(r);
1733  }
1734  // ── THE UNION BOX'S FOUR CORNERS (SUNRA-1213) ────────────────────────────
1735  //
1736  // Andy 2026-09-20, with Figma on screen: "多选的联合选区四角要有方块把手,拖动可
1737  // 等比/整体缩放全部选中对象". The same square, the same stroke, the same
1738  // scale-compensated size as the single selection's — it IS the \`.__ppn_handle\`
1739  // class, because a second look for the same kind of grip is a second thing to
1740  // learn. \`__ppn_ghandle\` rides alongside purely as the IDENTITY onDown reads:
1741  // the node also matches \`__ppn_handle\`, so a branch that saw it as an ordinary
1742  // handle would ask draggableSel() (null for a multi-selection) and fall through
1743  // to a pick on whatever the square happens to cover.
1744  //
1745  // FOUR, NOT EIGHT. An edge handle on a group promises a one-axis squash of
1746  // every member — a different operation from scaling the lot, and not the one
1747  // that was asked for. Andy: "Edge handles are not required."
1748  //
1749  // THE BOX ITSELF STAYS pointer-events: none. A press in its empty middle has to
1750  // reach what is under it, and the group DRAG decides by geometry instead (see
1751  // onDown's union-box branch) — making the rect a hit target would have blocked
1752  // every click on an unselected element standing between two selected ones.
1753  var GROUP_DIRS = ["nw", "ne", "se", "sw"];
1754  // The members of a multi-selection a gesture may actually act on. The nudge's
1755  // refusals, in one place, so the corners, the group drag and the group resize
1756  // cannot disagree about what "the selection" is: never the page itself, never a
1757  // detached node, and never an element an op is already rewriting (isBusy — a
1758  // gesture there would be an edit racing a write that is about to replace it).
1759  function groupMembers() {
1760    var out = [];
1761    for (var i = 0; i < selectedSet.length; i++) {
1762      var el = selectedSet[i];
1763      if (!el || el.nodeType !== 1 || !el.isConnected) continue;
1764      if (isRootSheet(el) || el === document.body || el === document.documentElement) continue;
1765      if (isBusy(el)) continue;
1766      out.push(el);
1767    }
1768    return out;
1769  }
1770  /** The hull the GROUP chrome promises AND the group gesture fills.
1771   *
1772   *  Over \`groupMembers()\`, never \`selectedSet\`: the two differ whenever a
1773   *  selected element is detached, is the root sheet, or is BUSY (an agent op
1774   *  rewriting it). Those are excluded from every group write, so a box drawn
1775   *  around them promises a rect the resize cannot keep — the scale would be
1776   *  computed against a union containing a member that never moves, and the
1777   *  per-frame \`syncGroupBox()\` re-measure would then disagree with
1778   *  \`scaleRectAbout(base, g)\` and the handle would visibly lag the pointer.
1779   *  \`selectionChromeRect()\` stays as it is: the FORMAT BAR anchors to what the
1780   *  user sees selected, which is a different question. */
1781  function groupChromeRect(els) {
1782    var list = els || groupMembers();
1783    var l = Infinity, t = Infinity, r = -Infinity, b = -Infinity;
1784    for (var i = 0; i < list.length; i++) {
1785      var c = chromeRect(list[i]);
1786      if (!c) continue;
1787      if (c.left < l) l = c.left;
1788      if (c.top < t) t = c.top;
1789      if (c.left + c.width > r) r = c.left + c.width;
1790      if (c.top + c.height > b) b = c.top + c.height;
1791    }
1792    if (l === Infinity) return null;
1793    return { left: l, top: t, width: r - l, height: b - t };
1794  }
1795  function removeGroupHandles() {
1796    for (var i = 0; i < ghandles.length; i++) {
1797      if (ghandles[i].parentNode) ghandles[i].parentNode.removeChild(ghandles[i]);
1798    }
1799    ghandles = [];
1800  }
1801  function syncGroupHandles(box) {
1802    // Withheld in every state that stands the ordinary handles down, for that same
1803    // reason: a mode drawing its own chrome must not carry a second set of squares
1804    // offering a gesture the runtime will not perform. A LIVE BAND is one of those
1805    // states — the selection under it is still being drawn.
1806    var ok = !!box && box.width > 0 && box.height > 0 &&
1807      selectedSet.length > 1 && !editingEl && !marqueeEl && !cropSess && !cframe &&
1808      groupMembers().length > 1;
1809    if (!ok) { removeGroupHandles(); return; }
1810    // \`isConnected\`, not just the count: the overlays are BODY CHILDREN, so an
1811    // html re-graft destroys them while this array still points at detached no
1811des
1812    // — the same defect syncRings and syncGroupBox each carry a term for.
1813    if (ghandles.length !== GROUP_DIRS.length || !ghandles[0].isConnected) {
1814      removeGroupHandles();
1815      for (var i = 0; i < GROUP_DIRS.length; i++) {
1816        var h = document.createElement("div");
1817        h.className = "__ppn_handle __ppn_ghandle";
1818        h.setAttribute("data-dir", GROUP_DIRS[i]);
1819        h.style.cursor = cursorFor(GROUP_DIRS[i]);
1820        document.body.appendChild(h);
1821        ghandles.push(h);
1822      }
1823    }
1824    // The SAME handlePoints the single selection's squares are placed from, so a
1825    // group corner and an element corner are the same point of the same rect. The
1826    // union box never turns (it is the axis-aligned hull of the selection), so
1827    // there is no angle to carry and the stylesheet's translate(-50%, -50%) is the
1828    // whole of the centring.
1829    var pts = handlePoints({ left: box.left, top: box.top, width: box.width, height: box.height });
1830    for (var j = 0; j < ghandles.length; j++) {
1831      var p = pts[ghandles[j].getAttribute("data-dir")];
1832      ghandles[j].style.left = p[0] + "px";
1833      ghandles[j].style.top = p[1] + "px";
1834    }
1835  }
1836  // ── CAN AN OVERLAY OWN THIS BOX? (ENG-12634) ────────────────────────────
1837  //
1838  // The page itself is never it. Andy's report, 2026-09-16: "a black screen
1839  // overlay is triggered when i click on the pages" — the busy scrim covering a
1840  // whole Design page edge to edge, over a text element that was still selected
1841  // and still had its handles. An IMAGE op cannot legitimately lock a page: the
1842  // thing it is about to replace is one \`<img>\`, so a set that resolves to the
1843  // sheet / body / documentElement is a resolution accident, never an intent.
1844  // Refusing it here is the hard floor under both overlay sets — below the
1845  // host's bookkeeping, below the selector, below whatever a path happened to
1846  // match — because a whole-page scrim also REFUSES INTERACTION with everything
1847  // it contains (\`isBusy\` walks ancestors), i.e. it is a black screen you cannot
1848  // click through.
1849  function overlayTarget(el) {
1850    return !!el && el.nodeType === 1 &&
1851      !isRootSheet(el) && el !== document.body && el !== document.documentElement;
1852  }
1853  function setLoadingLabel(box, label) {
1854    var pill = box.firstChild;
1855    if (!pill) {
1856      pill = document.createElement("div");
1857      pill.className = "__ppn_loading_pill";
1858      pill.setAttribute("role", "status");
1859      var text = document.createElement("span");
1860      text.className = "__ppn_loading_text";
1861      pill.appendChild(text);
1862      box.appendChild(pill);
1863    }
1864    if (pill.firstChild.textContent !== label) pill.firstChild.textContent = label;
1865  }
1866  // Only withhold the selected item's toolbar; other items remain editable.
1867  function selectionIsLoading() {
1868    if (!selectedSet.length) return false;
1869    var els = busyElements().concat(pendingElements());
1870    var uploads = uploadingPairs();
1871    for (var u = 0; u < uploads.length; u++) els.push(uploads[u].el);
1872    for (var i = 0; i < selectedSet.length; i++) {
1873      for (var j = 0; j < els.length; j++) {
1874        if (selectedSet[i] === els[j] || selectedSet[i].contains(els[j]) || els[j].contains(selectedSet[i])) return true;
1875      }
1876    }
1877    return false;
1878  }
1879  // ── BUSY OVERLAYS — one per element an op is running on ─────────────────
1880  //
1881  // The host owns the SET (see the \`set-busy\` message); this owns the drawing.
1882  // Rebuilt on a set change and re-placed on every chrome sync, which is the
1883  // same discipline the rings follow and the reason a busy element's overlay
1884  // survives a reflow, a pan, a zoom and an html re-graft.
1885  //
1886  // Kept OFF \`selectedSet\` on purpose: an op outlives its selection. The user
1887  // fires Upscale, clicks somewhere else while it runs, and the shimmer has to
1888  // stay on the picture that is actually being worked on — not follow the
1889  // pointer to whatever is selected now.
1890  //
1891  // ── ELEMENTS, NOT SELECTORS (ENG-12634) ─────────────────────────────────
1892  // \`setBusy\` resolves each selector ONCE and the set is element references
1893  // from then on. It used to hold the strings and re-run \`querySelector\` on
1894  // every sync, which is a different question asked of a different document
1895  // every time the page underneath changes: an element with no \`data-cid\`
1896  // (agent-authored html) is addressed by a DOM PATH, and neither a \`set-body\`
1897  // page swap nor an outer \`set-html\` re-graft clears the set — so the same
1898  // path re-resolved against the NEW markup and the scrim landed on whatever
1899  // happened to sit at that position, up to and including the page root.
1900  // An element reference cannot make that mistake: when the markup it lived in
1901  // is replaced it is simply DETACHED, and a detached element is dropped rather
1902  // than re-found. The host re-sends \`set-busy\` after a write it knows the set
1903  // survived (ComposePane), which is the only way a still-running op gets its
1904  // scrim back — and it gets it back on the element the host still means.
1905  var busyEls = [];
1906  var busyBoxes = [];
1907  var busyLabel = "Generating...";
1908  /** The live members of the busy set: the resolved elements that are still in
1909   *  the document. Prunes in place, so a page swap costs one pass and never a
1910   *  re-query. */
1911  function busyElements() {
1912    var out = [];
1913    for (var i = 0; i < busyEls.length; i++) {
1914      if (busyEls[i] && busyEls[i].isConnected) out.push(busyEls[i]);
1915    }
1916    if (out.length !== busyEls.length) busyEls = out;
1917    return out;
1918  }
1919  /** Is \`node\` (or an ancestor) currently under an op? Drives the interaction
1920   *  refusal — a busy element cannot be selected, hovered, dragged or resized,
1921   *  because every one of those would be an edit racing a write that is already
1922   *  in flight and about to replace the thing being edited. */
1923  function isBusy(node) {
1924    if (!busyEls.length || !node) return false;
1925    var els = busyElements();
1926    for (var i = 0; i < els.length; i++) {
1927      if (els[i] === node || (els[i].contains && els[i].contains(node))) return true;
1928    }
1929    return false;
1930  }
1931  function syncBusy() {
1932    var els = busyElements();
1933    if (busyBoxes.length !== els.length || (busyBoxes.length > 0 && !busyBoxes[0].isConnected)) {
1934      for (var i = 0; i < busyBoxes.length; i++) {
1935        if (busyBoxes[i].parentNode) busyBoxes[i].parentNode.removeChild(busyBoxes[i]);
1936      }
1937      busyBoxes = [];
1938      for (var j = 0; j < els.length; j++) {
1939        var d = document.createElement("div");
1940        d.className = "__ppn_busy";
1941        document.body.appendChild(d);
1942        busyBoxes.push(d);
1943      }
1944    }
1945    for (var k = 0; k < busyBoxes.length && k < els.length; k++) {
1946      setLoadingLabel(busyBoxes[k], busyLabel);
1947      var r = chromeRect(els[k]);
1948      var st = busyBoxes[k].style;
1949      st.left = r.left + "px";
1950      st.top = r.top + "px";
1951      st.width = r.width + "px";
1952      st.height = r.height + "px";
1953    }
1954  }
1955  function setBusy(selectors, label) {
1956    busyLabel = typeof label === "string" && label ? label : "Generating...";
1957    // THE ONE RESOLUTION. Everything downstream works on the elements this loop
1958    // found; nothing asks the document again (see the note above the set).
1959    busyEls = [];
1960    for (var i = 0; i < selectors.length; i++) {
1961      if (typeof selectors[i] !== "string" || !selectors[i]) continue;
1962      var found = null;
1963      try { found = document.querySelector(selectors[i]); } catch (e) { found = null; }
1964      if (!overlayTarget(found)) continue; // the page itself is never under an op
1965      if (busyEls.indexOf(found) < 0) busyEls.push(found);
1966    }
1967    // SELECTION SURVIVES. An earlier cut dropped busy elements out of the
1968    // selection so their handles could not invite a doomed resize; the handles
1969    // are now withheld by \`draggableSel\` instead, which is the narrower fix —
1970    // deselecting also took away the move Andy wants to keep. The host hides
1971    // the action bar while the selected item is loading.
1972    syncBusy();
1973    syncHandles();
1974    postSelectionRect();
1975  }
1976
1977  // ── PENDING OVERLAYS — one per empty slot a generation is cooking for ────
1978  //
1979  // Structurally the busy machinery's twin (same host-owns-the-set protocol,
1980  // same chromeRect placement, same rebuild-on-change discipline) and
1981  // behaviourally its opposite: NOTHING here feeds the hit test. A pending slot
1982  // is still the user's to select, move and resize, because the wait is tens of
1983  // seconds long and the fill lands in whatever box it finds at the end
1984  // (\`slot-fill-reconcile.ts\` keeps the local box, so laying out while you wait
1985  // is never work the delivery throws away).
1986  //
1987  // Kept as its own set rather than folded into the busy set for exactly that
1988  // reason — one set with two meanings would have to be read differently by
1989  // every guard that consults it, and the first one that forgot would take the
1990  // resize away again.
1991  var pendingSelectors = [];
1992  var pendingBoxes = [];
1993  //
1994  // It KEEPS the re-query the busy set gave up (ENG-12634), and that is not an
1995  // oversight: a pending selector is always a \`[data-cid]\` — a stable identity
1996  // the parent mints, not a DOM path — so re-resolving it can only ever find the
1997  // same slot or nothing, never a different element. The re-query is what lets a
1998  // shimmer survive a page swap and come back when the user returns to the page
1999  // the generation was ordered on, with no re-send to arrange. What it does take
2000  // from the busy fix is the HARD FLOOR: a selector that resolves to the sheet
2001  // may not shimmer the whole page either.
2002  function pendingElements() {
2003    var out = [];
2004    for (var i = 0; i < pendingSelectors.length; i++) {
2005      var el = null;
2006      try { el = document.querySelector(pendingSelectors[i]); } catch (e) { el = null; }
2007      if (overlayTarget(el)) out.push(el);
2008    }
2009    return out;
2010  }
2011  function syncPending() {
2012    var els = pendingElements();
2013    // Same connectivity term as syncBusy / syncRings, and a REAL defect without
2014    // it: an html re-graft wipes these body children, and an equal count would
2015    // otherwise satisfy the check and leave us positioning detached no
2015des.
2016    if (pendingBoxes.length !== els.length || (pendingBoxes.length > 0 && !pendingBoxes[0].isConnected)) {
2017      for (var i = 0; i < pendingBoxes.length; i++) {
2018        if (pendingBoxes[i].parentNode) pendingBoxes[i].parentNode.removeChild(pendingBoxes[i]);
2019      }
2020      pendingBoxes = [];
2021      for (var j = 0; j < els.length; j++) {
2022        var d = document.createElement("div");
2023        d.className = "__ppn_pending";
2024        document.body.appendChild(d);
2025        pendingBoxes.push(d);
2026      }
2027    }
2028    for (var k = 0; k < pendingBoxes.length && k < els.length; k++) {
2029      setLoadingLabel(pendingBoxes[k], "Generating...");
2030      var r = chromeRect(els[k]);
2031      var st = pendingBoxes[k].style;
2032      st.left = r.left + "px";
2033      st.top = r.top + "px";
2034      st.width = r.width + "px";
2035      st.height = r.height + "px";
2036    }
2037  }
2038  function setPending(selectors) {
2039    pendingSelectors = [];
2040    for (var i = 0; i < selectors.length; i++) {
2041      if (typeof selectors[i] === "string" && selectors[i]) pendingSelectors.push(selectors[i]);
2042    }
2043    // No selection side effects and no handle refresh: unlike setBusy this
2044    // changes nothing about what may be done to the element, so there is nothing
2045    // for the handles to re-decide.
2046    syncPending();
2047    postSelectionRect();
2048  }
2049
2050  // ── UPLOADING — one shimmer per slot whose bytes are still in the air ─────
2051  //
2052  // ENG-12629. A ⌘V commits an EMPTY image component and uploads behind it; this
2053  // is the shimmer drawn on the slot the picture will land in.
2054  //
2055  // TWO PARALLEL ARRAYS rather than a cid→label object: the keys come from the
2056  // parent, and \`{}\` already answers to "toString" / "constructor". An indexOf
2057  // over a list that is at most four long costs nothing and cannot be confused
2058  // by an inherited property.
2059  //
2060  // ADDRESSED ONE AT A TIME (see PickerHostMsg "set-uploading"): unlike the busy
2061  // and pending sets, which the parent re-derives wholesale, each upload starts
2062  // and settles in its own closure, so a replace-the-set protocol would make
2063  // every settle responsible for restating the labels it knows nothing about.
2064  var uploadingCids = [];
2065  var uploadingText = [];
2066  var uploadingBoxes = [];
2067  function uploadingPairs() {
2068    var out = [];
2069    for (var i = 0; i < uploadingCids.length; i++) {
2070      var sel = '[data-cid="' + String(uploadingCids[i]).replace(/["\\\\]/g, "\\\\$&") + '"]';
2071      var el = null;
2072      try { el = document.querySelector(sel); } catch (e) { el = null; }
2073      // A slot the user deleted (or an undone paste) simply stops being drawn;
2074      // the parent's clear still arrives and removes it from the set.
2075      if (el) out.push({ el: el, label: uploadingText[i] });
2076    }
2077    return out;
2078  }
2079  function syncUploading() {
2080    var pairs = uploadingPairs();
2081    // Same connectivity term as syncBusy / syncPending / syncRings, and a REAL
2082    // defect without it: an html re-graft wipes these body children, and an equal
2083    // count would otherwise satisfy the check and leave us positioning detached
2084    // nodes.
2085    if (uploadingBoxes.length !== pairs.length || (uploadingBoxes.length > 0 && !uploadingBoxes[0].isConnected)) {
2086      for (var i = 0; i < uploadingBoxes.length; i++) {
2087        if (uploadingBoxes[i].parentNode) uploadingBoxes[i].parentNode.removeChild(uploadingBoxes[i]);
2088      }
2089      uploadingBoxes = [];
2090      for (var j = 0; j < pairs.length; j++) {
2091        var d = document.createElement("div");
2092        d.className = "__ppn_uploading";
2093        document.body.appendChild(d);
2094        uploadingBoxes.push(d);
2095      }
2096    }
2097    for (var k = 0; k < uploadingBoxes.length && k < pairs.length; k++) {
2098      var r = chromeRect(pairs[k].el);
2099      var st = uploadingBoxes[k].style;
2100      st.left = r.left + "px";
2101      st.top = r.top + "px";
2102      st.width = r.width + "px";
2103      st.height = r.height + "px";
2104      setLoadingLabel(uploadingBoxes[k], pairs[k].label);
2105    }
2106  }
2107  function setUploadingLabel(cid, label) {
2108    if (typeof cid !== "string" || !cid) return;
2109    var at = uploadingCids.indexOf(cid);
2110    if (typeof label === "string" && label) {
2111      if (at < 0) { uploadingCids.push(cid); uploadingText.push(label); }
2112      else uploadingText[at] = label;
2113    } else if (at >= 0) {
2114      uploadingCids.splice(at, 1);
2115      uploadingText.splice(at, 1);
2116    } else {
2117      return; // a clear for a cid we never labelled — nothing to rebuild
2118    }
2119    syncUploading();
2120    postSelectionRect();
2121  }
2122
2123  // Rebuild to match the current selection (count changes are rare — a plain
2124  // rebuild beats diffing), then place each one.
2125  function syncRings() {
2126    // The connectivity term is a REAL defect, not belt-and-braces: an html
2127    // re-graft wipes these body children, and if the same number of elements
2128    // survived (the common case) the count check alone was satisfied — so the
2129    // rings were re-positioned while detached and the selection lost its stroke
2130    // until the count happened to change. Same hazard, same fix as 
2130syncGroupBox.
2131    if (rings.length !== selectedSet.length || (rings.length > 0 && !rings[0].isConnected)) {
2132      removeRings();
2133      for (var i = 0; i < selectedSet.length; i++) {
2134        var d = document.createElement("div");
2135        d.className = "__ppn_ring";
2136        document.body.appendChild(d);
2137        rings.push(d);
2138      }
2139    }
2140    positionRings();
2141  }
2142  // The eight points, in the element's OWN axes, then turned with it. A handle's
2143  // \`data-dir\` therefore keeps meaning what it always meant — "e" is the box's
2144  // own right edge, wherever that edge is pointing on screen — which is exactly
2145  // the frame resizeBox already works in (drag.xf carries the same map), so the
2146  // chrome and the arithmetic describe one box rather than two.
2147  function handlePoints(q) {
2148    var rx = q.left + q.width;
2149    var by = q.top + q.height;
2150    return {
2151      nw: [q.left, q.top], n: [q.left + q.width / 2, q.top], ne: [rx, q.top],
2152      e: [rx, q.top + q.height / 2], se: [rx, by], s: [q.left + q.width / 2, by],
2153      sw: [q.left, by], w: [q.left, q.top + q.height / 2],
2154    };
2155  }
2156  function positionHandles(el) {
2157    var q = chromeQuad(el);
2158    var pts = handlePoints(q);
2159    for (var i = 0; i < handles.length; i++) {
2160      var p = pts[handles[i].getAttribute("data-dir")];
2161      var t = rotatePoint(p[0], p[1], q.cx, q.cy, q.angle);
2162      handles[i].style.left = t.x + "px";
2163      handles[i].style.top = t.y + "px";
2164      // The square is CENTRED on its point by a translate the stylesheet owns, so
2165      // the element's angle is appended to it rather than replacing it — a bare
2166      // rotate() here would drop the centring and hang every handle off its own
2167      // top-left corner.
2168      handles[i].style.transform = q.angle
2169        ? "translate(-50%, -50%) rotate(" + q.angle + "deg)"
2170        : "translate(-50%, -50%)";
2171    }
2172    positionRotZones(q);
2173  }
2174  // Rings track the WHOLE selection (multi-select included, root sheet included);
2175  // handles only the single draggable one. Syncing both here means every existing
2176  // syncHandles() call site keeps the ring in step for free — no new sync gaps.
2177  function syncHandles() {
2178    // CROP MODE draws its own chrome and nothing else draws over it. A selection
2179    // ring and eight squares on the same box would be two modes making the same
2180    // claim, and the dimmed surround plus the dashed window is what tells you at
2181    // a glance which one you are in. Same single entry point, so the crop overlay
2182    // cannot develop a sync gap the ring does not have.
2183    if (cropSess) { removeRings(); removeHandles(); removeGroupHandles(); positionCropChrome(); return; }
2184    // The crop FRAME stands down the same chrome for the same reason, and rides
2185    // the same single entry point so it cannot develop a sync gap the rings do
2186    // not have — a reflow, a pan, a zoom and an html re-graft all land here.
2187    if (cframe) { removeRings(); removeHandles(); removeGroupHandles(); positionCropFrame(); return; }
2188    syncRings();
2189    // Busy overlays are measured chrome too — same entry point, same guarantee
2190    // that they cannot develop a sync gap the rings do not have.
2191    syncBusy();
2192    // …and the pending-slot shimmer, which rides the same single entry point for
2193    // the same reason: a reflow, a pan, a zoom and an html re-graft all land here.
2194    syncPending();
2195    // …and the upload labels (ENG-12629), for that same reason: a pasted slot is
2196    // the user's to drag and resize while its bytes fly, and the label has to go
2197    // with it.
2198    syncUploading();
2199    // The union box rides the same single entry point the rings do, so it cannot
2200    // develop a sync gap the rings do not have.
2201    syncGroupBox();
2202    // The hover highlight is measured chrome too, so it is re-asserted wherever
2203    // the ring is — a live patch (asset swap, text undo, insert) reflows the
2204    // element under a stationary pointer, and hoverAt's "already lit" early-out
2205    // would otherwise leave the highlight on the old rect until the mouse moved.
2206    positionHoverBox();
2207    var el = draggableSel();
2208    if (!el) { removeHandles(); return; }
2209    ensureHandles(el); // the SET depends on the element (an image gets corners only)
2210    // FOUR, ALWAYS — unlike the handles and the bands, whose set depends on what
2211    // the element offers. Every box has four corners and every element can turn,
2212    // including a ratio-locked contain image that shows no side handles at all.
2213    ensureRotZones();
2214    positionHandles(el); // …which places the zones off the same quad (ENG-11582)
2215    // The bands ride the same single entry point, for the same reason everything
2216    // else here does: one call site, no gap to develop.
2217    ensureEdges(el);
2218    positionEdges(el);
2219  }
2220  // Pin an element to an explicit absolute box reproducing its CURRENT visual
2221  // rect, so drag/resize write unambiguous position + size inline styles (the
2222  // deck's designed-placement idiom). A flowing block is converted in place:
2223  // its box is captured FIRST, then it's taken out of flow at the same spot.
2224  function pinAbsolute(el, rect) {
2225    el.style.setProperty("box-sizing", "border-box");
2226    el.style.setProperty("margin", "0");
2227    el.style.setProperty("position", "absolute");
2228    el.style.setProperty("width", rect.width + "px");
2229    el.style.setProperty("height", rect.height + "px");
2230    var op = el.offsetParent;
2231    var pr = op ? op.getBoundingClientRect() : { left: 0, top: 0 };
2232    var bl = op ? op.clientLeft : 0; // subtract the offset parent's border box
2233    var bt = op ? op.clientTop : 0;
2234    var left = rect.left - pr.left - bl;
2235    var top = rect.top - pr.top - bt;
2236    return pinLocal(el, { left: left, top: top, width: rect.width, height: rect.height });
2237  }
2238  // The same pin, for a box that is ALREADY in the element's offset-parent space
2239  // (no viewport round-trip). pinAbsolute ends here too, so there is exactly one
2240  // place that writes a pinned box.
2241  function pinLocal(el, box) {
2242    el.style.setProperty("box-sizing", "border-box");
2243    el.style.setProperty("margin", "0");
2244    el.style.setProperty("position", "absolute");
2245    // A PIN IS THE LAST WORD ON THE BOX, so nothing may be left able to overrule
2246    // it. An authored max-width/max-height still applies to an absolutely
2247    // positioned element and silently caps the width/height written on the next
2248    // two lines — which is exactly how an inserted <img> (buildInsert ships it
2249    // with \`max-width: 100%\`) came out squashed the first time it was dragged
2250    // wider than its container: the width hit the cap, the ratio-locked height
2251    // went on obeying the gesture, and the "locked" aspect was anything but.
2252    // Neutralized HERE because this is the one place a pinned box is written —
2253    // clearing it at any single call site would leave the others still capped.
2254    el.style.setProperty("max-width", "none");
2255    el.style.setProperty("max-height", "none");
2256    el.style.setProperty("width", box.width + "px");
2257    el.style.setProperty("height", box.height + "px");
2258    el.style.setProperty("left", box.left + "px");
2259    el.style.setProperty("top", box.top + "px");
2260    return { left: box.left, top: box.top, width: box.width, height: box.height };
2261  }
2262  // ── the element's OWN box, for a transformed element (Andy: a corner drag
2263  // "still wiggles"; "it should anchor in one corner") ──────────────────────
2264  //
2265  // A component may carry a CSS rotate() — schema-render's boxCss writes one for
2266  // any non-zero "rotation" on the component. For such an element
2267  // getBoundingClientRect() reports the AXIS-ALIGNED HULL of the rotated box, not
2268  // the box: a 1152\xd7130 text line at 12\xb0 measures 1154\xd7366. Pinning to that hull
2269  // replaces the element's box with its own shadow — it inflates and jumps the
2270  // instant a handle is pressed — and then every frame recomputes a hull whose
2271  // corners move as the box's centre moves, so the "anchored" corner walks
2272  // (measured: ~0.33px per frame, drifting up and left for a whole "se" drag).
2273  //
2274  // offsetLeft/Top/Width/Height are the border box BEFORE the transform, already
2275  // in the space an absolute left/top is written in (offsets are measured after
2276  // margins, which is what pinLocal makes explicit anyway). They are integer
2277  // rounded — half a pixel, once, at mousedown, against a gesture that was losing
2278  // hundreds. Read this BEFORE pinning: pinLocal zeroes the margin.
2279  function localRectOf(el) {
2280    return {
2281      left: el.offsetLeft, top: el.offsetTop,
2282      width: el.offsetWidth, height: el.offsetHeight,
2283    };
2284  }
2285  // That transform in compose-resize's latched form, or null when the element
2286  // carries none (the normal case) / it is unreadable — null means "take the
2287  // plain path", so nothing changes for the elements that were already steady.
2288  //
2289  // Only the LINEAR part is carried: a translation does not depend on the box's
2290  // size, so it cancels out of the anchor equation. The origin becomes a FRACTION
2291  // of the box because CSS re-resolves a percentage origin against every new
2292  // size — that coupling is exactly why pinning left/top alone cannot hold a
2293  // rotated corner still.
2294  function transformOf(el, box) {
2295    var cs = getComputedStyle(el);
2296    var t = cs.transform;
2297    if (!t || t === "none") return null;
2298    var open = t.indexOf("(");
2299    if (open < 0) return null;
2300    var n = t.slice(open + 1, t.lastIndexOf(")")).split(",");
2301    // matrix(a,b,c,d,e,f) vs matrix3d(m11,m12,m13,m14,m21,…): take the 2-D linear
2302    // part from either, so a 3-D transform degrades to its flattened map rather
2303    // than to NaN.
2304    var m3 = t.indexOf("matrix3d") === 0;
2305    var a = parseFloat(n[0]), b = parseFloat(n[1]);
2306    var c = parseFloat(m3 ? n[4] : n[2]), d = parseFloat(m3 ? n[5] : n[3]);
2307    if (!isFinite(a) || !isFinite(b) || !isFinite(c) || !isFinite(d)) return null;
2308    // A transform that does NOT bend the box (a pure translate) is still
2309    // reported: the PIN has to know the rendered rect is displaced, or it writes
2310    // the displacement into left/top and the element jumps by it a second time.
2311    // resizeBox sees an identity linear part and takes its plain arithmetic, so
2312    // the numbers such an element resizes by are unchanged.
2313    var o = (cs.transformOrigin || "").split(" ");
2314    var ox = parseFloat(o[0]), oy = parseFloat(o[1]);
2315    return {
2316      a: a, b: b, c: c, d: d,
2317      ox: box.width > 0 && isFinite(ox) ? ox / box.width : 0.5,
2318      oy: box.height > 0 && isFinite(oy) ? oy / box.height : 0.5,
2319    };
2320  }
2321  // Pin an element for a gesture. Transformed → its own box; otherwise → its
2322  // rendered rect, which is the same thing and the long-standing behaviour.
2323  function pinForGesture(el, rect) {
2324    var local = localRectOf(el);
2325    if (transformOf(el, local)) return pinLocal(el, local);
2326    return pinAbsolute(el, rect || el.getBoundingClientRect());
2327  }
2328  // ── smart-alignment guides (⑩, drag only) ──────────────────────────────
2329  // A box in the frame's viewport px (the space getBoundingClientRect + our
2330  // position:fixed guides share — the artboard scale lives OUTSIDE the frame).
2331  function snapRectOf(node) {
2332    var r = node.getBoundingClientRect();
2333    return { left: r.left, top: r.top, width: r.width, height: r.height };
2334  }
2335  // The alignment sources for a drag: the artboard sheet + every OTHER visible
2336  // composition element (never chrome, never the dragged element or its
2337  // ancestors/descendants — snapping to your own parent/child is noise).
2338  function collectSnapTargets(el) {
2339    var boardEl = document.body.firstElementChild || document.body;
2340    var rects = [];
2341    var all = document.body.getElementsByTagName("*");
2342    for (var i = 0; i < all.length; i++) {
2343      var n = all[i];
2344      if (n === el || n === boardEl) continue;
2345      if (SKIP[n.tagName] || isChrome(n)) continue;
2346      if (n.contains(el) || el.contains(n)) continue;
2347      var r = n.getBoundingClientRect();
2348      if (r.width === 0 || r.height === 0) continue;
2349      rects.push({ left: r.left, top: r.top, width: r.width, height: r.height });
2350    }
2351    return { board: snapRectOf(boardEl), rects: rects };
2352  }
2353  function clearGuides() {
2354    for (var i = 0; i < guides.length; i++) {
2355      if (guides[i].parentNode) guides[i].parentNode.removeChild(guides[i]);
2356    }
2357    guides = [];
2358  }
2359  // Draw the current frame's guide lines. Full board-span hairlines (vertical for
2360  // an x-line, horizontal for a y-line) — the board never scrolls internally, so
2361  // fixed positioning in viewport px lines up with the snap math's coordinates.
2362  function renderGuides(vLines, hLines) {
2363    clearGuides();
2364    var de = document.documentElement;
2365    var fullH = Math.max(de.scrollHeight, document.body ? document.body.scrollHeight : 0, window.innerHeight || 0);
2366    var fullW = Math.max(de.scrollWidth, document.body ? document.body.scrollWidth : 0, window.innerWidth || 0);
2367    for (var i = 0; i < vLines.length; i++) {
2368      var v = document.createElement("div");
2369      v.className = "__ppn_guide";
2370      v.style.left = vLines[i] + "px";
2371      v.style.top = "0px";
2372      v.style.width = "calc(1px * var(--ppn-s, 1))"; // crisp 1 screen-px at any zoom
2373      v.style.height = fullH + "px";
2374      document.body.appendChild(v);
2375      guides.push(v);
2376    }
2377    for (var j = 0; j < hLines.length; j++) {
2378      var h = document.createElement("div");
2379      h.className = "__ppn_guide";
2380      h.style.left = "0px";
2381      h.style.top = hLines[j] + "px";
2382      h.style.height = "calc(1px * var(--ppn-s, 1))"; // crisp 1 screen-px at any zoom
2383      h.style.width = fullW + "px";
2384      document.body.appendChild(h);
2385      guides.push(h);
2386    }
2387  }
2388
2389  // Apply the primary element's RESOLVED delta (post-snap) to every follower in a
2390  // group move. Taking the resolved delta rather than the raw pointer delta is what
2391  // keeps the group rigid: when the primary snaps to a guide the whole set shifts
2392  // with it, so relative spacing is preserved to the pixel.
2393  function moveFollowers(mvX, mvY) {
2394    if (!drag || !drag.followers) return;
2395    for (var i = 0; i < drag.followers.length; i++) {
2396      var f = drag.followers[i];
2397      f.el.style.setProperty("left", (f.base.left + mvX) + "px");
2398      f.el.style.setProperty("top", (f.base.top + mvY) + "px");
2399    }
2400  }
2401
2402  // ── POINTER CAPTURE — the fix for "let go outside the preview and it keeps
2403  // resizing" (Andy, 2026-08-01) ─────────────────────────────────────────────
2404  //
2405  // Every listener this gesture depends on lives on THIS document, and this
2406  // document is inside a sandboxed, opaque-origin iframe. The moment the pointer
2407  // crosses the iframe's edge the frame stops seeing it: the \`mouseup\` that ends
2408  // the drag is delivered to the PARENT page instead, so \`endDrag()\` never ran,
2409  // \`drag\` stayed armed forever, and the next move back inside the frame resumed
2410  // a gesture the user had finished — the element kept resizing under a button
2411  // nobody was holding.
2412  //
2413  // \`setPointerCapture\` is the root fix rather than a wider net of listeners:
2414  // capturing the pointer id re-targets EVERY subsequent event for that pointer
2415  // back to the capturing node, viewport boundary included, so \`pointerup\` is
2416  // delivered where the gesture actually lives. That is also why the whole
2417  // down/move/up path is pointer events now and not a mouse/pointer hybrid — a
2418  // pointerId only exists on a pointer event, and a gesture that begins on one
2419  // event family and ends on another is exactly how a release goes missing.
2420  //
2421  // Capture is best-effort: a browser may refuse (no such pointer, node
2422  // detached), and every refusal is survivable because \`onMove\`'s \`buttons === 0\`
2423  // floor and the window-blur teardown below both end a stranded gesture anyway.
2424  function capturePointer(g, e) {
2425    if (!g || !e || typeof e.pointerId !== "number") return;
2426    var node = e.target && e.target.setPointerCapture ? e.target : document.body;
2427    try { node.setPointerCapture(e.pointerId); } catch (err) { return; }
2428    g.ptrId = e.pointerId;
2429    g.ptrEl = node;
2430  }
2431  function releasePointer(g) {
2432    if (!g || !g.ptrEl || typeof g.ptrId !== "number") return;
2433    try {
2434      if (g.ptrEl.hasPointerCapture && g.ptrEl.hasPointerCapture(g.ptrId)) {
2435        g.ptrEl.releasePointerCapture(g.ptrId);
2436      }
2437    } catch (err) { /* the node went away with its capture — nothing to release */ }
2438    g.ptrId = null;
2439    g.ptrEl = null;
2440  }
2441
2442  function beginDrag(el, kind, e) {
2443    drag = {
2444      el: el, kind: kind, startX: e.clientX, startY: e.clientY, moved: false, base: null,
2445      // The captured pointer id + the node holding it (see capturePointer).
2446      ptrId: null, ptrEl: null,
2447      // Latch every image's ratio; contain locks by default and Shift also
2448      // constrains cover images. Read the modifier on each move.
2449      lockAspect: false, aspect: 0,
2450      // The crop as it stood at mousedown, or null for an uncropped image — the
2451      // fixed picture a frame resize reveals more or less of. See beginDrag.
2452      cropBase: null,
2453      // Smart-alignment (move only) — populated on first real move (after pin).
2454      startRect: null, snapTargets: null, snapState: { x: null, y: null },
2455      // ── A RESIZE NEVER TOUCHES THE FONT SIZE (Andy, 2026-09-07 — ENG-11576)
2456      // THE RULING THIS OVERTURNS: "TYPE SCALING (Andy, 2026-07-29) — a resize of
2457      // a text leaf scales its font with the box" (corner handles, width ratio).
2458      // Framer is the reference: drag a text box bigger and the type is exactly
2459      // the size it was. So there is no \`baseFont\` latch here any more, and no
2460      // \`archReflow\` either — the only thing that ever cascaded a per-letter
2461      // font-size onto an arched word mid-drag was that scaling. Font size now
2462      // changes through ONE gesture, the inspector's size field.
2463      // \`textish\` stays: it is what the AUTO → FIXED conversion below keys off
2464      // (ENG-11546), which is about the BOX and outlives the type-scaling rule.
2465      textish: false, inkPinned: false,
2466      // The element's own transform, latched here so resizeBox can hold the
2467      // rendered corner still (see localRectOf). null for an axis-aligned element.
2468      xf: null,
2469      // ── A RESIZE PRESS THAT NEVER MOVED LEAVES NOTHING BEHIND (ENG-11546 追加 2)
2470      // A resize sets \`moved\` at pointerdown (the pin below IS a size change), so
2471      // without these two the mere PRESS commits — and a double-click is two of
2472      // them before the dblclick arrives. On a fixed box that spent a stray \xb11px
2473      // of ink-pin rounding; on an AUTO box it silently converted the element to
2474      // fixed, so "double-click a handle on an auto box does nothing" could not be
2475      // true no matter what the dblclick handler did. \`styleBefore\` is the whole
2476      // style attribute as it stood BEFORE the pin, latched below for resizes
2477      // only; \`resized\` is set by the first frame that actually writes a box.
2478      resized: false, styleBefore: null,
2479      // The rotate gesture's latch (ENG-11582): the centre it turns about, the
2480      // angle it started from and the mirror it has to preserve. Null for every
2481      // other kind. Latched here under the same rule as the resize anchor —
2482      // re-deriving the centre each frame from an element that is moving under
2483      // the pointer is how a gesture starts chasing its own output.
2484      rot: null,
2485    };
2486    capturePointer(drag, e);
2487    // HIDE THE FLOATING FORMAT BAR FOR THE DURATION. The box is about to become
2488    // a moving target and the bar would sit under the pointer doing the moving;
2489    // it returns over the settled box when the gesture lands (see endDrag's own
2490    // postSelectionRect). Cheap: one message per gesture, not one per frame.
2491    postSelectionRect();
2492    // ── ROTATE WRITES ONE DECLARATION AND TOUCHES NOTHING ELSE (ENG-11582) ──
2493    // No pin, no AUTO → FIXED conversion, no aspect latch, no crop re-derivation:
2494    // turning a box changes neither its size nor its position, so every piece of
2495    // machinery below would be writing values the gesture does not mean. A text
2496    // leaf in particular must NOT be converted to a fixed box by being turned —
2497    // that conversion is the resize's ruling (ENG-11546) and it is about the box.
2498    //
2499    // \`moved\` is set for the same reason a resize sets it (endDrag's commit
2500    // branch is gated on it), and \`resized\` is deliberately left false so a
2501    // press-and-release inside a zone restores \`styleBefore\` and commits nothing
2502    // — the ENG-11546 追加 2 rule, reused verbatim.
2503    if (kind === "rotate") {
2504      drag.styleBefore = el.getAttribute("style");
2505      var rq = chromeQuad(el);
2506      // \`sel\` is latched here with the centre and the angle, under the same rule
2507      // and for one extra reason: the live \`selection-style\` post (ENG-11582
2508      // 追加 3) needs an ADDRESS every frame, and selectorFor walks the tree to
2509      // build one. Computing it once per gesture instead of once per frame keeps
2510      // the rAF write to a style mutation and a postMessage, and it guarantees the
2511      // live messages and the settled style-commit name the same element even if
2512      // the tree moved under the gesture.
2513      drag.rot = { cx: rq.cx, cy: rq.cy, start: rq.angle, flip: flipOfEl(el), sel: selectorFor(el) };
2514      drag.moved = true;
2515      clearHover();
2516      document.body.classList.add("__ppn_dragging");
2517      return;
2518    }
2519    if (kind !== "move") {
2520      // Resize commits a size change immediately — pin now. For an ink-driven text
2521      // leaf we pin the INLINE axis to the INK, not to the authored box (ask ③):
2522      // the handles the user grabbed sit on the glyphs, so the gesture must be
2523      // measured from the glyphs — and a resize is exactly where a sloppy
2524      // full-width agent box gets normalized.
2525      //
2526      // The BLOCK axis comes from the element's own line-box layout instead, and
2527      // is placed so the glyphs do not move (inkPinBox). \`drag.moved\` is set true
2528      // three lines below — a resize commits from the pointerdown, before any
2529      // pointer movement — so any error this pin makes is a jump the user gets for
2530      // merely TOUCHING a handle. It used to make one: 3–4px of half-leading, up
2531      // for tight display type and down for loose body copy.
2532      //
2533      // A TRANSFORMED element takes its own box instead, and skips the ink pin:
2534      // textInkRect measures Range rects, which are as rotated as the element is,
2535      // so an ink pin there would hand the gesture the glyphs' hull — the same
2536      // wrong rect localRectOf exists to avoid. Its normalization is given up for
2537      // the drag; its anchor is not.
2538      // Latched BEFORE the first mutation of this gesture, so endDrag can put the
2539      // element back exactly as it found it when no frame ever wrote a box.
2540      drag.styleBefore = el.getAttribute("style");
2541      var local = localRectOf(el);
2542      drag.xf = transformOf(el, local);
2543      var pin = drag.xf ? null : inkPinRect(el);
2544      drag.inkPinned = !!pin;
2545      drag.base = drag.xf ? pinLocal(el, local) : pinAbsolute(el, pin || el.getBoundingClientRect());
2546      drag.textish = isTextBearing(el);
2547      // ── THE DRAG IS WHAT MAKES A TEXT BOX FIXED (ENG-11546) ──────────────
2548      // Andy: "用户拖过任一手柄后,元素记录显式 width/height … 文字在框内按 textAlign
2549      // 与 verticalAlign 排布". So the conversion happens HERE, on the pointerdown
2550      // that starts the gesture, not on release: every frame after this one is
2551      // already operating on a fixed box, which is what lets the pointer own the
2552      // height (an e/w drag then holds it, an n/s drag moves it) instead of the
2553      // document overwriting it with a measurement.
2554      //
2555      // The box it converts AT is the ink pin above — the shrink-wrap that used
2556      // to happen on commit. That is deliberate and it keeps ask ③'s
2557      // normalization intact: touch a handle on a sloppy full-width agent box and
2558      // it becomes the tight box around its own words, without a rendered pixel
2559      // moving; the drag then grows THAT. An already-fixed box keeps the vertical
2560      // alignment it has (top for a fresh conversion, which is where the text
2561      // already sits, so the conversion is invisible until the box changes size).
2562      if (drag.textish && !drag.xf && !textVAlignOf(el)) {
2563        el.style.setProperty("align-content", verticalAlignCss("top"));
2564      }
2565      // ASPECT LOCK FOR AN IMAGE (Andy, 2026-08-01). A photograph has ONE correct
2566      // shape: a free-form corner drag squashes it, and the "resize" the user
2567      // thought they asked for is silently a distortion they now have to undo by
2568      // hand. So the ratio is latched HERE, off the same \`drag.base\` the anchor is
2569      // derived from — measured once, never re-read mid-gesture — and resizeBox
2570      // couples the two axes from it. The chrome agrees rather than contradicting:
2571      // syncHandles gives an <img> CORNERS ONLY, because an n/s/e/w handle on a
2572      // ratio-locked box promises a one-axis change it cannot deliver.
2573      // ── AN IMAGE RESIZES ITS FRAME, NOT ITS PICTURE (Andy, 2026-08-25) ────
2574      // See isFramedImage for why the 2026-08-01 aspect lock is now conditional
2575      // rather than unconditional. Three cases, decided once, here:
2576      //
2577      //   \xb7 fill (or no fit at all, which resolves to fill) — the one case that
2578      //     really would squash the photograph. NORMALIZED to cover, right now,
2579      //     before the first frame of the drag: the property is written inline so
2580      //     it rides the same style commit width/height do, and the schema's
2581      //     fit follows it (applyImageStyle). This is the same shape as the
2582      //     text path's shrink-wrap — the user's first resize is where a sloppy
2583      //     authored value gets fixed, without moving a rendered pixel.
2584      //   \xb7 cover — free-form. The box may take any shape; the picture fills it
2585      //     either way and the shape decides what shows.
2586      //   \xb7 contain — still locked. Its correct box shape is the picture's.
2587      //
2588      // The CROP comes along for the ride. An image that has already been cropped
2589      // is being shown at a chosen scale and offset, and a frame resize must not
2590      // silently re-fit it — so the crop as it stands at mousedown is latched here
2591      // (never re-read mid-gesture, for the same reason the anchor is not) and
2592      // every frame re-derives the crop that keeps the picture still. Null for an
2593      // uncropped image, which needs nothing: cover re-derives its own source rect
2594      // from the new box, which is already the wanted behaviour.
2595      if (el.tagName === "IMG") {
2596        var fit = imageFitOf(el);
2597        if (fit !== "cover" && fit !== "contain") {
2598          el.style.setProperty("object-fit", "cover");
2599          fit = "cover";
2600        }
2601        drag.lockAspect = fit === "contain";
2602        drag.cropBase = fit === "cover" ? parseViewBox(el.style.getPropertyValue("object-view-box")) : null;
2603      }
2604      drag.aspect = el.tagName === "IMG" && drag.base.height > 0 ? drag.base.width / drag.base.height : 0;
2605      drag.moved = true;
2606      clearHover();
2607      document.body.classList.add("__ppn_dragging");
2608    }
2609  }
2610  function applyDragMove(e) {
2611    if (!drag) return;
2612    var dx = e.clientX - drag.startX;
2613    var dy = e.clientY - drag.startY;
2614    if (!drag.moved) {
2615      if (Math.abs(dx) < 4 && Math.abs(dy) < 4) return; // below threshold — still a click
2616      drag.moved = true;
2617      var rect0 = drag.el.getBoundingClientRect();
2618      drag.base = pinForGesture(drag.el, rect0);
2619      // Freeze the dragged box (viewport px) + gather alignment targets AFTER the
2620      // pin (so peer rects reflect the reflow from taking the element out of flow).
2621      drag.startRect = { left: rect0.left, top: rect0.top, width: rect0.width, height: rect0.height };
2622      drag.snapTargets = collectSnapTargets(drag.el);
2623      drag.snapState = { x: null, y: null };
2624      // GROUP MOVE — a multi-selection drags as ONE object. Every OTHER selected
2625      // element is pinned the same way and follows the primary by the identical
2626      // resolved delta, so the arrangement the user built is rigid under the
2627      // gesture (drag one of three aligned cards and the other two keep their
2628      // spacing). Only for "move": a resize still acts on the grabbed element.
2629      drag.followers = [];
2630      if (drag.kind === "move" && selectedSet.length > 1) {
2631        for (var gi = 0; gi < selectedSet.length; gi++) {
2632          var gEl = selectedSet[gi];
2633          if (gEl === drag.el || !gEl || !gEl.isConnected) continue;
2634          drag.followers.push({ el: gEl, base: pinForGesture(gEl) });
2635        }
2636      }
2637      clearHover();
2638      document.body.classList.add("__ppn_dragging");
2639    }
2640    var b = drag.base, s = drag.el.style, k = drag.kind;
2641    // ── ROTATE (ENG-11582) ───────────────────────────────────────────────────
2642    // The angle is a pure function of the LATCHED centre, the LATCHED start
2643    // rotation and the two pointer positions — never of the element's current
2644    // transform and never of the previous frame. Same discipline as the resize
2645    // anchor, and the same payoff: a drag out and back returns the exact angle it
2646    // started at instead of accumulating a frame's worth of rounding each pass.
2647    //
2648    // ⇧ SNAPS TO 15\xb0 absolutely (Figma), so the modifier is read per frame rather
2649    // than latched: pressing shift MID-DRAG must snap what is already on screen,
2650    // and releasing it must hand the angle straight back to the pointer.
2651    if (k === "rotate") {
2652      var R = drag.rot;
2653      var deg = rotateAngleFor(
2654        R.start, R.cx, R.cy, drag.startX, drag.startY, e.clientX, e.clientY,
2655        e.shiftKey ? ROTATE_SNAP_DEG : 0,
2656      );
2657      // A frame is about to write, so the press is a rotate rather than a touch
2658      // (see beginDrag's \`resized\`, which endDrag reads to undo a bare press).
2659      drag.resized = true;
2660      // ONE declaration, spelled exactly as the inspector spells it, with the
2661      // mirror carried through — see rotateTransformCss. An angle of 0 REMOVES
2662      // the property rather than writing an empty one: \`transform: \` is not the
2663      // same as no transform, and the schema reads an absent rotate() as 0.
2664      var xfCss = rotateTransformCss(deg, R.flip);
2665      if (xfCss) s.setProperty("transform", xfCss);
2666      else s.removeProperty("transform");
2667      // ── THE PANEL FOLLOWS THE ARTBOARD (ENG-11582 追加 3) ──────────────────
2668      // Inside the rAF write, so this is ONE message per PAINTED frame — the same
2669      // coalescing the transform above rides, not one per pointermove. It is a
2670      // live readout and nothing else: no commit, no history, no persist (see
2671      // PickerSelectionStyleMsg). The angle posted is the one just WRITTEN, so
2672      // the field and the glyphs cannot disagree by a frame or by a rounding.
2673      post({ type: "selection-style", selector: R.sel, style: { transform: xfCss }, rotation: deg });
2674    } else if (k === "move") {
2675      // Magnetic smart-alignment: resolve the raw (pointer-proposed) box against the
2676      // artboard + peer lines, then write the SNAPPED left/top back in the element's
2677      // offset-parent space (b is css; startRect is viewport — the delta bridges them,
2678      // and equals dx/dy exactly when nothing snapped). Guides paint only while near.
2679      if (drag.startRect && drag.snapTargets) {
2680        var raw = {
2681          left: drag.startRect.left + dx, top: drag.startRect.top + dy,
2682          width: drag.startRect.width, height: drag.startRect.height,
2683        };
2684        // THRESHOLD IS IN SCREEN PX, not frame px (Andy: 6–8 screen px). The
2685        // artboard's zoom lives OUTSIDE this frame, so a fixed frame-px threshold
2686        // changes how the magnet FEELS at every zoom level: at 40% a 6-frame-px
2687        // radius is 2.4 screen px (the magnet barely engages — you have to land it
2688        // by hand), and at 250% it is 15 screen px (it grabs lines you were not
2689        // aiming at). Dividing by the live scale — the same --ppn-s compensation
2690        // the 1px guide and the handles already use — keeps the pull identical to
2691        // the pointer at every zoom. Break-away tracks it so hysteresis scales too.
2692        var snapPx = SCREEN_SNAP_PX / (curScale > 0 ? curScale : 1);
2693        var snapped = computeSnap(raw, drag.snapTargets, drag.snapState, {
2694          snap: snapPx,
2695          break: snapPx * 2,
2696        });
2697        drag.snapState = snapped.state;
2698        var mvX = snapped.left - drag.startRect.left;
2699        var mvY = snapped.top - drag.startRect.top;
2700        s.setProperty("left", (b.left + mvX) + "px");
2701        s.setProperty("top", (b.top + mvY) + "px");
2702        moveFollowers(mvX, mvY);
2703        renderGuides(snapped.vLines, snapped.hLines);
2704      } else {
2705        s.setProperty("left", (b.left + dx) + "px");
2706        s.setProperty("top", (b.top + dy) + "px");
2707        moveFollowers(dx, dy);
2708      }
2709    } else {
2710      // ANCHORED RESIZE. The box is a pure function of the LATCHED base, the
2711      // LATCHED transform and the cursor delta — never of the element's current
2712      // rect, and never of the previous frame. That is what keeps the opposite
2713      // corner/edge bit-stable from mousedown to release instead of walking a
2714      // fraction per frame. drag.xf is null for an axis-aligned element, which is
2715      // the same arithmetic as before it existed.
2716      // THE GESTURE IS REAL FROM HERE ON: a frame is about to write a box, so the
2717      // press is a resize rather than a touch (see beginDrag's \`resized\`).
2718      drag.resized = true;
2719      var aspect = drag.lockAspect || e.shiftKey ? drag.aspect : 0;
2720      var rb = resizeBox(b, k, dx, dy, 8, drag.xf, aspect);
2721      var L = rb.left, T = rb.top, W = rb.width, H = rb.height;
2722      s.setProperty("width", W + "px");
2723      s.setProperty("height", H + "px");
2724      s.setProperty("left", L + "px");
2725      s.setProperty("top", T + "px");
2726      // ── THE PICTURE HOLDS STILL, THE WINDOW MOVES ────────────────────────
2727      // A cropped image is showing a chosen part of itself at a chosen scale;
2728      // resizing its frame must reveal or hide picture, not re-fit it. The crop
2729      // is re-derived from the LATCHED one and the box the drag started at (never
2730      // from the previous frame, same rule as the anchor), so the picture is
2731      // bit-stable under the gesture and a drag out and back returns the crop it
2732      // started with. Dragged past the asset's own edge it clamps to the whole
2733      // picture and object-fit: cover takes over — the frame stays full, which
2734      // is what "keep the offset and scale AS MUCH AS VALID" has to mean.
2735      if (drag.cropBase) {
2736        // A proportional resize scales the chosen picture, not its crop window.
2737        // Derive both modes from the original crop so toggling Shift is reversible.
2738        var vb = cropToViewBox(aspect ? drag.cropBase : cropAfterFrameResize(drag.cropBase, b, { left: L, top: T, width: W, height: H }));
2739        if (vb) s.setProperty("object-view-box", vb);
2740        else s.removeProperty("object-view-box");
2741      }
2742      // ── THE BOX ONLY. NO FONT SIZE IS WRITTEN HERE (ENG-11576) ───────────
2743      // THE RULING THIS OVERTURNS: "SCALE THE TYPE WITH THE BOX (Andy,
2744      // 2026-07-29)" — a corner drag multiplied the font size by the width ratio
2745      // (resizeFontSize) and wrote it inline, so it rode this same style-commit
2746      // into the persisted doc.
2747      //
2748      // Andy, 2026-09-07, with Framer on screen: dragging a text box b
2748igger
2749      // leaves the type size untouched. EVERY handle of a text element's
2750      // selection box — the four corners included — is now the same pure-geometry
2751      // gesture the n/s/e/w handles always were, and the ONLY way a font size
2752      // changes is the inspector's size field. So this frame writes width /
2753      // height / left / top and stops; the four lines above are the whole of what
2754      // a resize says about a text element.
2755      //
2756      // Nothing to replace it with: the helper is gone from compose-text-metrics
2757      // rather than left unwired, so re-adding this behaviour means re-deriving a
2758      // font size from a box size — which is the thing that was overturned.
2759      // ── THE HEIGHT IS THE USER'S, NOT A MEASUREMENT (ENG-11546) ───────────
2760      // THE RULING THIS REVISES: "a text leaf's height is not a free variable —
2761      // given the wrap width and the font size it is determined", so every frame
2762      // overwrote the pointer's H with the measured line-box height and
2763      // re-anchored the top. That was the right answer for an AUTO box and it is
2764      // the mechanism behind 追加 1's bug ②: an e/w drag changes neither the font
2765      // nor the pointer's height, yet the box still grew taller, because the
2766      // measured line-box height (half-leading included, ~20px on a 26px/1.5
2767      // headline) replaced the ink height the gesture had been pinned to.
2768      //
2769      // A fixed box has no such constraint: width, height and font size are three
2770      // things the user states, overflow is not clipped (Figma), and the
2771      // arithmetic in resizeBox already guarantees the axis rule Andy asked for —
2772      // e/w touch width only, n/s touch height only, corners both. So the height
2773      // written above stands, and the block that used to overwrite it is gone.
2774      // blockMetricsOf is still what SIZES the conversion (inkPinRect on the
2775      // pointerdown above); it just no longer runs once per frame.
2776    }
2777    positionHandles(drag.el);
2778    positionRings();
2779    // …and the busy overlay, which is why this is not just \`positionHandles\`:
2780    // a busy element can still be MOVED, so its scrim has to travel with it
2781    // frame by frame rather than snapping back into place on drop.
2782    syncBusy();
2783    // The pending shimmer has the same obligation and a stronger claim on it: a
2784    // pending slot can be moved AND resized, so its overlay must track the box
2785    // every frame or it would be left behind by the very gesture it invites.
2786    syncPending();
2787    // …and the upload label, under the same obligation: a just-pasted slot is
2788    // auto-selected, so the very first thing the user can do with it is drag it
2789    // while the bytes are still flying (ENG-12629).
2790    syncUploading();
2791    // A group MOVE drags every member, so the union travels with them — re-derive
2792    // it per frame rather than leaving the box parked over where the group was.
2793    syncGroupBox();
2794  }
2795  function endDrag() {
2796    if (dragRaf) { cancelAnimationFrame(dragRaf); dragRaf = 0; }
2797    // FLUSH THE LAST MOVE INSTEAD OF DROPPING IT. Moves are rAF-coalesced, so a
2798    // gesture short enough to fit inside one frame — a flick, or any drag on a
2799    // page whose next frame is late — has its ONLY move still sitting here when
2800    // the button comes up. Discarding it used to be survivable because a resize
2801    // committed its pointerdown pin regardless; now that a resize which never
2802    // wrote a box leaves nothing behind (see below), dropping this event would
2803    // drop the whole gesture. The user released at this position: use it.
2804    if (drag && pendingDragEvent) applyDragMove(pendingDragEvent);
2805    pendingDragEvent = null;
2806    clearGuides(); // guides are a live drag affordance — gone on release
2807    if (!drag) return;
2808    releasePointer(drag); // hand the pointer back — the gesture is over
2809    var el = drag.el, moved = drag.moved;
2810    var kind = drag.kind, resized = drag.resized, styleBefore = drag.styleBefore;
2811    // Every element the gesture actually moved — the primary plus a group move's
2812    // followers. They were pinned + written live, so they must ALL ride the same
2813    // style commit or the ones that only moved on screen snap back on the next
2814    // re-render (their new left/top never reached the stored html).
2815    var movedEls = [el];
2816    if (drag.followers) {
2817      for (var fi = 0; fi < drag.followers.length; fi++) movedEls.push(drag.followers[fi].el);
2818    }
2819    // ── A TOUCH IS NOT A RESIZE (ENG-11546 追加 2) ──────────────────────────
2820    // A resize declares itself \`moved\` at pointerdown, because the pin it writes
2821    // there IS a size change (see beginDrag). That is right for a gesture that
2822    // goes on to move and wrong for one that does not: pressing a handle and
2823    // letting go re-pinned the element and persisted it, which on a text leaf
2824    // means the ink pin's outward rounding (\xb11px of width) and, on an AUTO box,
2825    // the conversion to fixed. A double-click is exactly two such presses, so the
2826    // "double-click a handle" gesture below could never be the no-op it has to be
2827    // on an auto box until the presses stopped writing.
2828    //
2829    // So: no frame ever wrote a box → put the style attribute back as it was and
2830    // commit nothing. \`suppressClick\` still stands, because the trailing click is
2831    // on a handle and must not be read as a pick.
2832    var untouched = kind !== "move" && !resized;
2833    document.body.classList.remove("__ppn_dragging");
2834    drag = null;
2835    if (untouched) {
2836      if (styleBefore == null) el.removeAttribute("style");
2837      else el.setAttribute("style", styleBefore);
2838      suppressClick = true;
2839      syncHandles();
2840      postSelectionRect();
2841      return;
2842    }
2843    if (moved) {
2844      // ── NO SHRINK-WRAP ON COMMIT (ENG-11546) ──────────────────────────────
2845      // THE RULING THIS REVISES: a text resize used to re-measure the settled ink
2846      // on release and pin the box onto it (\`anchorWrappedRect(rect, inkPinRect,
2847      // kind)\`), which is what normalized a sloppy agent-authored box. That
2848      // normalization has MOVED, not gone: beginDrag now converts the element to
2849      // a fixed box AT the ink pin, so the shrink-wrap happens on the pointerdown
2850      // and the drag grows the tight box from there.
2851      //
2852      // Doing it here as well would undo the whole feature — it would snap the
2853      // box the user just dragged back down onto the words inside it, which is
2854      // precisely the "选框被文字本身撑死" Andy is asking us to stop doing. (It
2855      // would also be a no-op by construction: \`inkPinRect\` refuses a fixed box.)
2856      postStyleCommit(movedEls); // persist the new position/size into the stored html
2857      // No text-commit rides along any more (ENG-11576): the only thing a drag
2858      // ever changed inside a text element's markup was the per-letter font-size
2859      // cascade on an arched word, and that existed solely to keep a type-SCALED
2860      // word scaling together. A drag now changes the box, so the markup is
2861      // untouched and the style patch is the whole story. (applyStyleToSelection
2862      // still cascades — an inspector font-size change is a real style change.)
2863      suppressClick = true;  // the trailing click must not re-select / clear
2864      postSelection();
2865      // syncHandles (not a bare positionHandles): a drag ends by RE-ASSERTING the
2866      // chrome invariant. positionHandles only moves handles that already exist,
2867      // so any teardown that raced the gesture would leave the element selected
2868      // and handle-less until the next click.
2869      syncHandles();
2870      postDocSize();         // out-of-flow conversion may change board height
2871    }
2872    // THE BAR COMES BACK, over wherever the box actually landed. Unconditional and
2873    // last: the moved branch above has already re-seated the chrome onto the
2874    // settled ink, and a gesture that never moved still has to clear the hide flag
2875    // it set in beginDrag — otherwise a press-without-drag would leave the bar
2876    // hidden until the next click.
2877    postSelectionRect();
2878  }
2879
2880  // ══ ARROW-KEY NUDGE (Andy, 2026-09-07 — ENG-11620 追加 2) ═══════════════════
2881  //
2882  // "选中组件后按 ↑←↓→ 应该移动(1px;Shift 10px),现在完全不动." One press moves
2883  // the selection one pixel; holding the key moves it continuously; releasing it
2884  // spends ONE style-commit, so a five-second hold is one undo step and not
2885  // ninety.
2886  //
2887  // THE SAME GESTURE AS A POINTER DRAG, WITH A DIFFERENT INPUT. Everything below
2888  // is applyDragMove's move branch with dx/dy coming from a keyboard instead of a
2889  // pointer, and endNudge is endDrag's committing half:
2890  //
2891  //   \xb7 the box is LATCHED ONCE, on the first key of the burst (pinForGesture, the
2892  //     very call the drag's first real frame makes), and every step writes
2893  //     base + accumulated delta. Never base + 1 read back off the element — that
2894  //     is the "gesture chasing its own output" failure the resize anchor and the
2895  //     rotate centre are both written to avoid, and over a 200-step key repeat it
2896  //     would compound sub-pixel rounding into a visible drift.
2897  //   \xb7 EVERY selected element moves by the identical delta, so a multi-selection
2898  //     stays rigid — the group-move rule, with no primary/follower split because
2899  //     a nudge has no grabbed element to be the primary.
2900  //   \xb7 a ROTATED element translates in SCREEN axes, not along its own rotation:
2901  //     pinForGesture hands a transformed element pinLocal(localRectOf(el)), i.e.
2902  //     left/top in the offset parent's UN-transformed space, and a CSS rotate()
2903  //     is applied after that placement. Pressing → on a 12\xb0 headline moves it
2904  //     right, which is what the key says.
2905  //   \xb7 the commit is postStyleCommit over the same element list, so the inspector's
2906  //     X / Y read the settled document exactly as they do after a drag.
2907  //
2908  // WHAT IT DELIBERATELY DOES NOT DO: no magnetic alignment and no guides. The
2909  // pointer gets a magnet because it cannot land on an exact pixel; the arrow key
2910  // is the gesture you reach for BECAUSE you want the exact pixel, and a snap that
2911  // silently eats or adds a few px would make it the one gesture that cannot.
2912  //
2913  // ENG-11546 追加 2 ("a touch is not a resize") has no analogue here and needs
2914  // none: that rule exists because a RESIZE declares itself moved at pointerdown
2915  // and so a press that never moved still committed the pin. A nudge has no press
2916  // — every arrow key that reaches this function writes a real \xb11/\xb110px — so
2917  // there is no untouched case to undo. The pin it takes is a MOVE's pin, the same
2918  // one applyDragMove takes on its first real frame; a move has always pinned the
2919  // box it is about to place, and an auto text leaf is never converted to a fixed
2920  // BOX in the ENG-11546 sense (no align-content, no ink shrink-wrap — those are
2921  // the resize path's, and this is not it).
2922  var NUDGE_DIR = { ArrowLeft: [-1, 0], ArrowRight: [1, 0], ArrowUp: [0, -1], ArrowDown: [0, 1] };
2923  var NUDGE_STEP = 1;
2924  var NUDGE_STEP_SHIFT = 10;
2925  // A field owns its arrow keys, always. The runtime's own inline editor is
2926  // \`editingEl\`, but the frame can also hold a focused <input>/<textarea> (a
2927  // rendered form control in the user's own composition), and there the arrows
2928  // mean "move the caret". Read off the EVENT TARGET rather than the selection,
2929  // because that is what the browser would be typing into.
2930  function isEditableTarget(t) {
2931    if (!t || t.nodeType !== 1) return false;
2932    var tag = t.tagName;
2933    if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
2934    if (t.isContentEditable) return true;
2935    // WALK THE ANCESTORS TOO, and honour an explicit "false" as the stop. Editability
2936    // INHERITS: a keydown inside an editable box targets whatever leaf the caret sits
2937    // in, which is routinely a child that carries no attribute of its own. The
2938    // isContentEditable property above says exactly this in a browser — the walk is
2939    // what makes the answer independent of a DOM that does not implement it (jsdom
2940    // does not), so the test and the browser agree on the same rule.
2941    for (var n = t; n; n = n.parentElement) {
2942      var v = n.getAttribute ? n.getAttribute("contenteditable") : null;
2943      if (v === "" || v === "true" || v === "plaintext-only") return true;
2944      if (v === "false") return false;
2945    }
2946    return false;
2947  }
2948  function nudgeBy(dx, dy) {
2949    if (!nudge) {
2950      var items = [];
2951      for (var i = 0; i < selectedSet.length; i++) {
2952        var el = selectedSet[i];
2953        if (!el || el.nodeType !== 1 || !el.isConnected) continue;
2954        if (isRootSheet(el) || el === document.body || el === document.documentElement) continue;
2955        if (isBusy(el)) continue; // an op is rewriting it — same refusal a resize makes
2956        items.push({ el: el, base: pinForGesture(el) });
2957      }
2958      if (!items.length) return false;
2959      nudge = { items: items, dx: 0, dy: 0 };
2960      clearHover(); // the pointer has not moved, but the element under it has
2961    }
2962    nudge.dx += dx;
2963    nudge.dy += dy;
2964    for (var j = 0; j < nudge.items.length; j++) {
2965      var it = nudge.items[j];
2966      it.el.style.setProperty("left", (it.base.left + nudge.dx) + "px");
2967      it.el.style.setProperty("top", (it.base.top + nudge.dy) + "px");
2968    }
2969    // The chrome follows every step, exactly as it does per drag frame: ring,
2970    // handles + rotate zones, the busy scrim (a busy element can still be moved)
2971    // and the multi-selection union box.
2972    var sel = draggableSel();
2973    if (sel) positionHandles(sel);
2974    positionRings();
2975    syncBusy();
2976    syncPending();
2977    syncUploading();
2978    syncGroupBox();
2979    postSelectionRect(); // the floating format bar rides the box it is anchored to
2980    return true;
2981  }
2982  // The burst is over — persist it as ONE history step. Called from the keyup, and
2983  // from every other way a burst can end without one (the frame losing focus, a
2984  // pointer gesture starting on top of it).
2985  function endNudge() {
2986    if (!nudge) return;
2987    var els = [];
2988    for (var i = 0; i < nudge.items.length; i++) {
2989      var el = nudge.items[i].el;
2990      // A live patch (an op landing, an html re-graft, a Delete) can detach an
2991      // element mid-burst. Its selector would no longer resolve in the parent's
2992      // stored html, so it is dropped rather than committed into a patch that
2993      // cannot apply.
2994      if (el && el.isConnected) els.push(el);
2995    }
2996    nudge = null;
2997    if (!els.length) return;
2998    postStyleCommit(els);
2999    postSelection();
3000    syncHandles();  // re-assert the whole chrome invariant, as endDrag does
3001    postDocSize();  // the out-of-flow pin can change the board's height
3002    postSelectionRect();
3003  }
3004  // ══ GROUP RESIZE — the union box's four corners (SUNRA-1213) ══════════════
3005  //
3006  // Andy 2026-09-20: "四角把手缩放整组,对象位置/尺寸按比例映射,一次 undo 还原."
3007  //
3008  // ITS OWN GESTURE, NOT A \`drag\` WITH FOLLOWERS. \`drag\` is one element plus a
3009  // kind, and every branch of applyDragMove / endDrag is written around that
3010  // element: the snap targets, the ink pin, the crop re-derivation, the rotate
3011  // centre, the aspect lock. A group resize has no grabbed element at all — the
3012  // thing being dragged is a rect nothing owns — so bolting it on would mean
3013  // teaching each of those branches to mean something else when a flag is set.
3014  //
3015  // WHAT IT SHARES is everything that makes a gesture here trustworthy, and each
3016  // for the reason the original gesture had it:
3017  //   \xb7 the base is LATCHED at pointerdown (the union box AND every member's box),
3018  //     never re-read mid-gesture, so a drag out and back returns the boxes it
3019  //     started with instead of accumulating a frame of rounding per pass;
3020  //   \xb7 the pointer is CAPTURED, so a release outside the frame still ends it;
3021  //   \xb7 the writes are rAF-coalesced — one pass over N members per painted frame;
3022  //   \xb7 the chrome re-syncs per frame (rings, busy/pending/upload overlays, the
3023  //     union box and its own corners), so nothing is left parked;
3024  //   \xb7 a press that never moved is REVERTED, not committed (ENG-11546 追加 2's
3025  //     rule, and N times more important here: arming the gesture pins EVERY
3026  //     member, so a bare touch would otherwise persist N boxes nobody asked for);
3027  //   \xb7 the release spends exactly ONE postStyleCommit over every member — one
3028  //     patch message, one parent commitSelfEdit, ONE undo step for the group.
3029  //
3030  // ONE SPACE FOR THE GEOMETRY, and it is the frame's VIEWPORT: the union box, the
3031  // handles and every chromeRect already live there, and there is NO SCALE between
3032  // it and a member's offset-parent space (the artboard's zoom lives on an ancestor
3033  // outside this frame — the same fact the crop session relies on). So the scaled
3034  // viewport rect becomes the box we write by a per-member TRANSLATION measured at
3035  // pointerdown, which is why each item carries both its \`vp\` (what the union box
3036  // was built from) and its \`base\` (what the pin wrote).
3037  var gresRaf = 0;
3038  var pendingGresEvent = null;
3039  /** Is this element part of the live group resize? Read by chromeRect, which
3040   *  freezes a member's chrome on its BOX for the duration — the same rule the
3041   *  single resize has, and for the same wobble (a text member's ink reflows as
3042   *  the box scales, and the union box would chase it). */
3043  function gresHolds(el) {
3044    if (!gres) return false;
3045    for (var i = 0; i < gres.items.length; i++) if (gres.items[i].el === el) return true;
3046    return false;
3047  }
3048  function beginGroupResize(dir, e) {
3049    var els = groupMembers();
3050    if (els.length < 2) return false;
3051    // The base is the MEMBERS' hull, the same rect \`syncGroupBox\` draws — not
3052    // \`selectionChromeRect()\`, whose union also covers selected elements this
3053    // gesture will not write (see \`groupChromeRect\`).
3054    var box = groupChromeRect(els);
3055    if (!box || box.width <= 0 || box.height <= 0) return false;
3056    // MEASURE EVERYTHING FIRST, PIN AFTERWARDS. A pin takes an element out of
3057    // flow, which reflows its siblings — so a base read after the previous member
3058    // was pinned would be the box that element moved TO, not the box the user is
3059    // looking at. Every rect below is read before a single style is written, and
3060    // each member is then pinned to ITS OWN pre-pin rect, which is what keeps the
3061    // group visually still as the gesture arms.
3062    var items = [];
3063    var vps = [];
3064    for (var i = 0; i < els.length; i++) {
3065      var el = els[i];
3066      var local = localRectOf(el);
3067      var r = el.getBoundingClientRect();
3068      var vp = chromeRect(el);
3069      vps.push(vp);
3070      items.push({
3071        el: el,
3072        before: el.getAttribute("style"),
3073        vp: vp,
3074        xf: transformOf(el, local),
3075        local: local,
3076        rect: { left: r.left, top: r.top, width: r.width, height: r.height },
3077        ink: inkPinRect(el),
3078        textish: isTextBearing(el),
3079        // A cropped image is showing a chosen part of itself at a chosen scale, and
3080        // a frame resize must reveal or hide picture rather than silently re-fit it
3081        // — the single resize's rule, latched here for the same reason (never
3082        // re-read mid-gesture).
3083        cropBase: el.tagName === "IMG" && imageFitOf(el) === "cover"
3084          ? parseViewBox(el.style.getPropertyValue("object-view-box")) : null,
3085        base: null,
3086      });
3087    }
3088    for (var j = 0; j < items.length; j++) {
3089      var it = items[j];
3090      // The single resize's pin, member by member: a transformed element takes its
3091      // OWN box (its hull is a shadow — see localRectOf), an auto text leaf takes
3092      // its INK (ask ③'s shrink-wrap, which is where a sloppy agent-authored box
3093      // gets normalized), anything else takes its rendered rect.
3094      it.base = it.xf ? pinLocal(it.el, it.local) : pinAbsolute(it.el, it.ink || it.rect);
3095      // …and the same conversion to a FIXED text box (ENG-11546): a scaled height
3096      // is only the user's if the document states it, or the next measurement
3097      // overwrites it and the member snaps back to its words.
3098      if (it.textish && !it.xf && !textVAlignOf(it.el)) {
3099        it.el.style.setProperty("align-content", verticalAlignCss("top"));
3100      }
3101    }
3102    gres = {
3103      dir: dir, startX: e.clientX, startY: e.clientY, base: box, items: items,
3104      // No member may collapse below a pixel (Andy) — the SMALLEST one sets the
3105      // floor, which is a thing only a scale can say (see groupScaleFloor). It is
3106      // also what forbids a flip: the scale can never reach zero or go negative.
3107      floor: groupScaleFloor(vps, 1),
3108      wrote: false, ptrId: null, ptrEl: null,
3109    };
3110    capturePointer(gres, e);
3111    clearHover();
3112    clearGuides();
3113    document.body.classList.add("__ppn_dragging");
3114    syncHandles();       // the chrome now frames the pinned boxes
3115    postSelectionRect(); // …and the bar stands down for the gesture
3116    return true;
3117  }
3118  function applyGroupResize(e) {
3119    if (!gres) return;
3120    var dx = e.clientX - gres.startX;
3121    var dy = e.clientY - gres.startY;
3122    // ⇧ CONSTRAINS THE RATIO, read PER FRAME rather than latched — the discipline
3123    // the rotate snap already follows: pressing shift mid-drag must constrain what
3124    // is already on screen, and releasing it must hand the box straight back to
3125    // the pointer.
3126    var g = groupResizeScale(gres.base, gres.dir, dx, dy, gres.floor.x, gres.floor.y, !!e.shiftKey);
3127    for (var i = 0; i < gres.items.length; i++) {
3128      var it = gres.items[i];
3129      if (!it.el || !it.el.isConnected) continue;
3130      // THE SAME MAP for the union box and for every member (scaleRectAbout), so
3131      // the rect the chrome promises is the rect the members fill.
3132      var nv = scaleRectAbout(it.vp, g);
3133      var nb = {
3134        // POSITION by the translation measured at pointerdown (vp → base), SIZE by
3135        // the scale applied to the member's OWN box: for a rotated member the
3136        // viewport hull is not the box a width/height declaration talks about.
3137        left: it.base.left + (nv.left - it.vp.left),
3138        top: it.base.top + (nv.top - it.vp.top),
3139        width: it.base.width * g.sx,
3140        height: it.base.height * g.sy,
3141      };
3142      var st = it.el.style;
3143      st.setProperty("left", nb.left + "px");
3144      st.setProperty("top", nb.top + "px");
3145      st.setProperty("width", nb.width + "px");
3146      st.setProperty("height", nb.height + "px");
3147      if (it.cropBase) {
3148        var vb = cropToViewBox(cropAfterFrameResize(it.cropBase, it.base, nb));
3149        if (vb) st.setProperty("object-view-box", vb);
3150        else st.removeProperty("object-view-box");
3151      }
3152    }
3153    // A frame has written boxes, so the press is a resize rather than a touch (see
3154    // endGroupResize, which reverts a gesture that never got here).
3155    gres.wrote = true;
3156    positionRings();
3157    // The measured overlays travel with the boxes they mark, frame by frame, for
3158    // the same reasons the drag frame lists: a busy element can still be moved, a
3159    // pending slot can be resized, a pasted slot is draggable while its bytes fly.
3160    syncBusy();
3161    syncPending();
3162    syncUploading();
3163    // …and the union box, which re-places its own four corners (SUNRA-1213).
3164    syncGroupBox();
3165  }
3166  function flushGroupResize() {
3167    gresRaf = 0;
3168    if (gres && pendingGresEvent) applyGroupResize(pendingGresEvent);
3169  }
3170  function endGroupResize() {
3171    if (gresRaf) { cancelAnimationFrame(gresRaf); gresRaf = 0; }
3172    // FLUSH THE LAST FRAME INSTEAD OF DROPPING IT — endDrag's rule: a gesture
3173    // short enough to fit inside one frame has its ONLY move still sitting here
3174    // when the button comes up, and the user released at that position.
3175    if (gres && pendingGresEvent) applyGroupResize(pendingGresEvent);
3176    pendingGresEvent = null;
3177    if (!gres) return;
3178    releasePointer(gres); // the gesture is over — hand the pointer back
3179    var items = gres.items, wrote = gres.wrote;
3180    gres = null;
3181    document.body.classList.remove("__ppn_dragging");
3182    suppressClick = true; // the trailing click is on a handle, never a pick
3183    if (!wrote) {
3184      // A TOUCH IS NOT A RESIZE (ENG-11546 追加 2). Put every style attribute back
3185      // exactly as it was found and commit nothing: the pin this gesture took is
3186      // a real size change on N elements, and a press-and-release must leave the
3187      // document — and the history — untouched.
3188      for (var i = 0; i < items.length; i++) {
3189        if (items[i].before == null) items[i].el.removeAttribute("style");
3190        else items[i].el.setAttribute("style", items[i].before);
3191      }
3192      syncHandles();
3193      postSelectionRect();
3194      return;
3195    }
3196    var els = [];
3197    for (var j = 0; j < items.length; j++) {
3198      // A live patch (an op landing, an html re-graft, a Delete) can detach a
3199      // member mid-gesture. Its selector would no longer resolve in the parent's
3200      // stored html, so it is dropped rather than committed into a patch that
3201      // cannot apply — the nudge's rule.
3202      if (items[j].el && items[j].el.isConnected) els.push(items[j].el);
3203    }
3204    if (els.length) postStyleCommit(els); // ONE message, N patches → ONE undo step
3205    postSelection();
3206    syncHandles();  // re-assert the whole chrome invariant, as endDrag does
3207    postDocSize();  // the out-of-flow pins can change the board's height
3208    postSelectionRect();
3209  }
3210
3211  // ══ CROP MODE (Andy, 2026-08-25) ═══════════════════════════════════════════
3212  //
3213  // The second of the two image gestures, and the one that needs a mode: a
3214  // double-click stops the editor talking about the BOX and starts it talking
3215  // about the PICTURE. The whole asset appears — extending past the frame it is
3216  // placed in — the frame becomes a lit window over it, and two drags are live at
3217  // once: a corner or edge tab moves the WINDOW over a fixed picture, and a
3218  // press on the picture itself moves the PICTURE under a fixed window.
3219  //
3220  // NON-DESTRUCTIVE, and that is a property of the storage shape rather than of
3221  // this code: the only thing a commit writes is a source rect
3222  // (\`object-view-box\`) and the window's own geometry. The asset is never
3223  // touched, no new image is minted, and re-entering shows the whole picture
3224  // again because the picture was always all still there.
3225  //
3226  // WHY THE SESSION HOLDS ITS OWN GEOMETRY rather than re-reading the element:
3227  // the window and the picture are two rectangles that move independently, and
3228  // only one of them is an element. Keeping both in the session — in VIEWPORT px,
3229  // the space every overlay here already lives in — means one addition converts
3230  // to the element's offset-parent space (there is no SCALE between them; the
3231  // artboard's transform is on an ancestor outside this frame) and the arithmetic
3232  // never has to round-trip through the DOM mid-gesture.
3233  var cropSess = null;
3234  var cropRaf = 0;
3235  var pendingCropEvent = null;
3236
3237  function cropNode(cls, tag) {
3238    var n = document.createElement(tag || "div");
3239    n.className = cls;
3240    document.body.appendChild(n);
3241    cropSess.nodes.push(n);
3242    return n;
3243  }
3244  function placeCropNode(n, r) {
3245    n.style.left = r.left + "px";
3246    n.style.top = r.top + "px";
3247    n.style.width = (r.width > 0 ? r.width : 0) + "px";
3248    n.style.height = (r.height > 0 ? r.height : 0) + "px";
3249  }
3250  /**
3251   * Open a crop session on an image. Returns false — changing NOTHING — for every
3252   * case where the gesture cannot honestly be offered:
3253   *
3254   *   \xb7 the engine has no \`object-view-box\`, so a crop would be authored and then
3255   *     not rendered (see CROP_SUPPORTED);
3256   *   \xb7 the asset has not decoded, so there is no natural size to measure a
3257   *     source rect against and no picture to draw the ghost from;
3258   *   \xb7 an op is running on it — the same refusal resize makes, for the same
3259   *     reason: the picture is about to be replaced.
3260   *
3261   * A refusal leaves the double-click meaning what it meant before, which for an
3262   * image is nothing.
3263   */
3264  function enterCrop(el) {
3265    if (!CROP_SUPPORTED || !el || el.tagName !== "IMG" || isBusy(el)) return false;
3266    var natW = el.naturalWidth || 0, natH = el.naturalHeight || 0;
3267    if (!(natW > 0) || !(natH > 0)) return false;
3268    var src = el.currentSrc || el.getAttribute("src") || "";
3269    if (!src) return false;
3270    // The style attribute EXACTLY as it stands, so Escape restores the document
3271    // the user had rather than a reconstruction of it.
3272    var style0 = el.getAttribute("style");
3273    // Pin first (the window needs explicit geometry to move), THEN measure — the
3274    // pin takes a flowing element out of flow and its rect is only stable after.
3275    var localBox = pinForGesture(el, el.getBoundingClientRect());
3276    // A crop is a statement about which part FILLS the window, so the session is
3277    // cover for its duration and after. Written before the rect is read: it can
3278    // change what the element paints, never how big it is.
3279    el.style.setProperty("object-fit", "cover");
3280    var vr = el.getBoundingClientRect();
3281    var win = { left: vr.left, top: vr.top, width: vr.width, height: vr.height };
3282    cropSess = {
3283      el: el, style0: style0, natW: natW, natH: natH,
3284      // The constant translation between viewport space and the element's own,
3285      // latched once for the session.
3286      off: { x: localBox.left - vr.left, y: localBox.top - vr.top },
3287      win: win, image: null, g: null, moved: false, nodes: [],
3288    };
3289    // START FROM WHAT THE USER IS LOOKING AT. Not from the stored crop — that is
3290    // an upper bound on what shows, and cover may be trimming it — and not from
3291    // the whole asset. effectiveCrop folds the trim in, so the first frame of the
3292    // session is pixel-identical to the last frame before it and the ghost lands
3293    // exactly over the picture already on screen.
3294    var crop0 = effectiveCrop(
3295      parseViewBox(el.style.getPropertyValue("object-view-box")), natW, natH, win.width, win.height,
3296    );
3297    cropSess.image = imageRectForCrop(win, crop0);
3298    var ghost = document.createElement("img");
3299    ghost.className = "__ppn_cropghost";
3300    ghost.setAttribute("alt", "");
3301    ghost.draggable = false;
3302    document.body.appendChild(ghost);
3303    cropSess.nodes.push(ghost);
3304    ghost.src = src; // after the append: one decode, already in cache
3305    cropSess.ghost = ghost;
3306    cropSess.bands = [cropNode("__ppn_cropband"), cropNode("__ppn_cropband"), cropNode("__ppn_cropband"), cropNode("__ppn_cropband")];
3307    cropSess.winEl = cropNode("__ppn_cropwin");
3308    // EIGHT TABS, per the Figma reference: four corners and four edge midpoints.
3309    // The midpoints are not decoration — they are the visible half of the same
3310    // edge-drag system the selection bands provide, so crop mode offers every
3311    // gesture a selection does rather than a corners-only subset of it.
3312    cropSess.chandles = [];
3313    var cdirs = ["nw", "n", "ne", "e", "se", "s", "sw", "w"];
3314    for (var i = 0; i < cdirs.length; i++) {
3315      var h = cropNode("__ppn_crophandle");
3316      h.setAttribute("data-dir", cdirs[i]);
3317      cropSess.chandles.push(h);
3318    }
3319    // …and the generous hit zone behind the midpoint tabs, cut back from the
3320    // corners by the same rule the selection bands use.
3321    cropSess.cedges = [];
3322    for (var e = 0; e < EDGE_DIRS.length; e++) {
3323      var cb = cropNode("__ppn_cropedge");
3324      cb.setAttribute("data-dir", EDGE_DIRS[e]);
3325      cropSess.cedges.push(cb);
3326    }
3327    cropSess.badge = cropNode("__ppn_cropsize");
3328    clearHover();
3329    document.body.classList.add("__ppn_cropping");
3330    syncHandles();       // the selection ring + 8 squares stand down
3331    postSelectionRect(); // and the floating format bar goes with them
3332    return true;
3333  }
3334  function positionCropChrome() {
3335    if (!cropSess) return;
3336    var im = cropSess.image, w = cropSess.win;
3337    placeCropNode(cropSess.ghost, im);
3338    // The dim is FOUR BANDS = the image rect minus the window. Laid out as two
3339    // full-width bands above and below and two between them, so no two overlap:
3340    // a seam where one 62% scrim stacks on another reads as a bug in the picture
3341    // rather than as chrome.
3342    var botY = w.top + w.height, rightX = w.left + w.width;
3343    placeCropNode(cropSess.bands[0], { left: im.left, top: im.top, width: im.width, height: w.top - im.top });
3344    placeCropNode(cropSess.bands[1], { left: im.left, top: botY, width: im.width, height: im.top + im.height - botY });
3345    placeCropNode(cropSess.bands[2], { left: im.left, top: w.top, width: w.left - im.left, height: w.height });
3346    placeCropNode(cropSess.bands[3], { left: rightX, top: w.top, width: im.left + im.width - rightX, height: w.height });
3347    placeCropNode(cropSess.winEl, w);
3348    var midX = w.left + w.width / 2, midY = w.top + w.height / 2;
3349    var pts = {
3350      nw: [w.left, w.top], n: [midX, w.top], ne: [rightX, w.top], e: [rightX, midY],
3351      se: [rightX, botY], s: [midX, botY], sw: [w.left, botY], w: [w.left, midY],
3352    };
3353    // The tabs are sized here rather than in CSS because a corner square and an
3354    // edge bar are two shapes off one rule: the bar is long along the edge it
3355    // sits on and short across it. Screen px throughout (chromeUnit), so a tab
3356    // stays the same size to the hand at 42% zoom and at 150%.
3357    var u = chromeUnit();
3358    var corner = 12 * u, barLong = 24 * u, barShort = 6 * u;
3359    for (var i = 0; i < cropSess.chandles.length; i++) {
3360      var h = cropSess.chandles[i], d = h.getAttribute("data-dir"), p = pts[d];
3361      var horiz = d === "n" || d === "s";
3362      var vert = d === "e" || d === "w";
3363      var tw = horiz ? barLong : vert ? barShort : corner;
3364      var th = horiz ? barShort : vert ? barLong : corner;
3365      // A midpoint tab longer than the side it sits on would poke out past both
3366      // corners and read as a frame, not a grip — clamp it to what fits between
3367      // the corner tabs.
3368      if (horiz && tw > w.width - corner) tw = Math.max(barShort, w.width - corner);
3369      if (vert && th > w.height - corner) th = Math.max(barShort, w.height - corner);
3370      h.style.width = tw + "px";
3371      h.style.height = th + "px";
3372      h.style.left = p[0] + "px";
3373      h.style.top = p[1] + "px";
3374    }
3375    var bt = EDGE_BAND * u, bc = EDGE_CORNER * u;
3376    for (var b2 = 0; b2 < cropSess.cedges.length; b2++) {
3377      var eb = cropSess.cedges[b2];
3378      var q = edgeBandRect(w, eb.getAttribute("data-dir"), bt, bc);
3379      if (!q) { eb.style.display = "none"; continue; }
3380      eb.style.display = "block";
3381      eb.style.left = q.left + "px";
3382      eb.style.top = q.top + "px";
3383      eb.style.width = q.width + "px";
3384      eb.style.height = q.height + "px";
3385    }
3386    // The live size badge — the WINDOW's dimensions, which is the number the
3387    // gesture is about (the picture's size is not changing).
3388    cropSess.badge.textContent = Math.round(w.width) + " \xd7 " + Math.round(w.height);
3389    cropSess.badge.style.left = (w.left + w.width / 2) + "px";
3390    cropSess.badge.style.top = (botY + 8) + "px";
3391  }
3392  /** Write the session's two rectangles onto the element: the window becomes its
3393   *  box, and their relationship becomes its source rect. The ONLY place the
3394   *  document is touched during a session, so a cancel has exactly one thing to
3395   *  undo (the style attribute) and a commit has nothing left to compute. */
3396  function writeCrop(sess) {
3397    var s = sess.el.style, w = sess.win;
3398    s.setProperty("left", (w.left + sess.off.x) + "px");
3399    s.setProperty("top", (w.top + sess.off.y) + "px");
3400    s.setProperty("width", w.width + "px");
3401    s.setProperty("height", w.height + "px");
3402    var vb = cropToViewBox(cropForWindow(sess.image, w));
3403    if (vb) s.setProperty("object-view-box", vb);
3404    else s.removeProperty("object-view-box");
3405  }
3406  function beginCropGesture(kind, e) {
3407    cropSess.g = {
3408      kind: kind, startX: e.clientX, startY: e.clientY, ptrId: null, ptrEl: null,
3409      // Both rectangles latched at mousedown, for the reason stated at length in
3410      // compose-resize: a frame derived from the previous frame inherits its
3411      // rounding and walks.
3412      win0: { left: cropSess.win.left, top: cropSess.win.top, width: cropSess.win.width, height: cropSess.win.height },
3413      image0: { left: cropSess.image.left, top: cropSess.image.top, width: cropSess.image.width, height: cropSess.image.height },
3414    };
3415    capturePointer(cropSess.g, e);
3416    document.body.classList.add("__ppn_dragging");
3417  }
3418  function applyCropMove(e) {
3419    var g = cropSess && cropSess.g;
3420    if (!g) return;
3421    var dx = e.clientX - g.startX, dy = e.clientY - g.startY;
3422    if (g.kind === "pan") {
3423      // THE PICTURE MOVES, THE WINDOW IS FIXED. Clamped to keep the window
3424      // covered: sliding a photograph until blank ground shows inside its own
3425      // frame is not an edit anyone means to make.
3426      cropSess.image = clampImageAroundWindow(
3427        { left: g.image0.left + dx, top: g.image0.top + dy, width: g.image0.width, height: g.image0.height },
3428        cropSess.win,
3429      );
3430    } else {
3431      // THE WINDOW MOVES, THE PICTURE IS FIXED — and it moves through the SAME
3432      // latched-anchor arithmetic a selection handle uses, so the two modes
3433      // cannot disagree about which corner holds still. No transform and no
3434      // aspect lock: a crop window is free by definition, and a rotated image is
3435      // pinned to its own box before the session opens.
3436      var rb = resizeBox(g.win0, g.kind, dx, dy, 8, null, 0);
3437      cropSess.win = clampWindowInsideImage(
3438        { left: rb.left, top: rb.top, width: rb.width, height: rb.height }, cropSess.image,
3439      );
3440    }
3441    cropSess.moved = true;
3442    writeCrop(cropSess);
3443    positionCropChrome();
3444  }
3445  function flushCrop() {
3446    cropRaf = 0;
3447    var e = pendingCropEvent;
3448    pendingCropEvent = null;
3449    if (e) applyCropMove(e);
3450  }
3451  function endCropGesture() {
3452    if (!cropSess || !cropSess.g) return;
3453    releasePointer(cropSess.g);
3454    cropSess.g = null;
3455    document.body.classList.remove("__ppn_dragging");
3456  }
3457  /**
3458   * Close the session. \`commit\` lands the crop through the ordinary style-commit
3459   * path — the same one a drag and a format edit use, so a crop is persisted,
3460   * undone and re-rendered by machinery that needed no changes to learn about it.
3461   *
3462   * A session that never MOVED restores instead of committing, whichever way it
3463   * is closed. Entering crop mode normalizes an element (pins it absolute, makes
3464   * its fit cover) and those are real edits to the stored document; making them
3465   * on a double-click the user immediately clicked away from would mean idly
3466   * inspecting a picture dirtied the page and cost an undo step.
3467   */
3468  function exitCrop(commit) {
3469    if (!cropSess) return;
3470    var sess = cropSess;
3471    if (sess.g) { releasePointer(sess.g); document.body.classList.remove("__ppn_dragging"); }
3472    // Cleared BEFORE the syncs below, so they take the ordinary path and put the
3473    // selection chrome back rather than re-drawing the crop overlay.
3474    cropSess = null;
3475    if (cropRaf) { cancelAnimationFrame(cropRaf); cropRaf = 0; }
3476    pendingCropEvent = null;
3477    for (var i = 0; i < sess.nodes.length; i++) {
3478      if (sess.nodes[i].parentNode) sess.nodes[i].parentNode.removeChild(sess.nodes[i]);
3479    }
3480    document.body.classList.remove("__ppn_cropping");
3481    if (commit && sess.moved) {
3482      writeCrop(sess);
3483      postStyleCommit([sess.el]);
3484      postDocSize(); // the window's box may have changed the board's height
3485    } else if (sess.style0 == null) {
3486      sess.el.removeAttribute("style");
3487    } else {
3488      sess.el.setAttribute("style", sess.style0);
3489    }
3490    postSelection();
3491    syncHandles();
3492  }
3493
3494  // ── THE CROP FRAME (ENG-10647) ──────────────────────────────────────────
3495  // The DESTRUCTIVE crop's gesture: the rect a re-encode will keep, dragged over
3496  // the whole picture, paired with the parent's aspect panel. Distinct from the
3497  // framing session above in every way that matters — it touches the document
3498  // NOT AT ALL (no pin, no style write, no commit), because the edit it feeds
3499  // does not happen here: Apply runs the canvas's \`handleCrop\`, which uploads a
3500  // genuinely smaller image and re-points the page at it.
3501  //
3502  // WHY THE SESSION LIVES IN HERE rather than in the parent, where the panel is:
3503  // the board is a sandboxed iframe. The parent document receives no pointer
3504  // events over it and \`elementFromPoint\` there returns the IFRAME, so a crop
3505  // frame drawn parent-side would be a picture of a gesture. Drawn here it rides
3506  // the same fixed-position, \`--ppn-s\`-sized chrome machinery as the selection
3507  // ring — which is what makes it track a reflow, a pan and a zoom for free.
3508  //
3509  // TWO-WAY: the panel posts \`set-crop-frame\` down (an aspect press recenters
3510  // the rect, a Dimensions edit resizes it) and a drag posts \`crop-frame\` up. A
3511  // rect arriving DOWN mid-drag is ignored — see setCropFrame.
3512  var cframe = null;
3513  var cframeRaf = 0;
3514  var pendingCframeEvent = null;
3515  /** The minimum rect, in SCREEN px — the same floor the canvas overlay uses. */
3516  var CROP_FRAME_MIN = 24;
3517
3518  /**
3519   * Where the FULL picture would sit, in viewport px — the space the crop rect
3520   * is drawn in and the thing the ghost plate is laid over.
3521   *
3522   * It is derived per \`object-fit\`, because the fit decides how much of the
3523   * asset the window is already showing: \`cover\` trims (so the visible source
3524   * rect is \`effectiveCrop\`'s, folded through any authored \`object-view-box\`),
3525   * \`contain\` letterboxes it inside the window, and \`fill\` stretches it across
3526   * the whole window. Inverting whichever one it is gives the same answer to the
3527   * same question — "if none of the picture were hidden, where would it be?"
3528   */
3529  function cropFramePicture(el) {
3530    var vr = el.getBoundingClientRect();
3531    var win = { left: vr.left, top: vr.top, width: vr.width, height: vr.height };
3532    var natW = el.naturalWidth || 0, natH = el.naturalHeight || 0;
3533    var vb = parseViewBox(el.style.getPropertyValue("object-view-box"));
3534    var fit = "";
3535    try { fit = (window.getComputedStyle(el).objectFit || "").toLowerCase(); } catch (err) { fit = ""; }
3536    if (fit === "cover") return imageRectForCrop(win, effectiveCrop(vb, natW, natH, win.width, win.height));
3537    var k = vb || { x: 0, y: 0, w: 1, h: 1 };
3538    if (fit === "contain" && natW > 0 && natH > 0) {
3539      var srcW = k.w * natW, srcH = k.h * natH;
3540      var sc = Math.min(win.width / srcW, win.height / srcH);
3541      var dw = srcW * sc, dh = srcH * sc;
3542      return imageRectForCrop(
3543        { left: win.left + (win.width - dw) / 2, top: win.top + (win.height - dh) / 2, width: dw, height: dh },
3544        k,
3545      );
3546    }
3547    return imageRectForCrop(win, k);
3548  }
3549
3550  function cframeNode(cls) {
3551    var n = document.createElement("div");
3552    n.className = cls;
3553    document.body.appendChild(n);
3554    cframe.nodes.push(n);
3555    return n;
3556  }
3557
3558  function openCropFrame(el, rect, ratio) {
3559    if (!el || el.tagName !== "IMG") return false;
3560    var natW = el.naturalWidth || 0, natH = el.naturalHeight || 0;
3561    if (!(natW > 0) || !(natH > 0)) return false;
3562    var src = el.currentSrc || el.getAttribute("src") || "";
3563    if (!src) return false;
3564    closeCropFrame();
3565    cframe = { el: el, natW: natW, natH: natH, ratio: ratio, rect: rect, image: null, nodes: [], g: null };
3566    cframe.image = cropFramePicture(el);
3567    // The plate: the WHOLE asset, drawn where the crop rect is measured against.
3568    // A composition image is routinely cover-fitted into a box that shows a slice
3569    // of it, and cropping a slice you cannot see is not a gesture — so the
3570    // session reveals the picture for its duration, exactly as the canvas node
3571    // does when it squares its card for crop.
3572    var ghost = document.createElement("img");
3573    ghost.className = "__ppn_cropplate";
3574    ghost.setAttribute("alt", "");
3575    ghost.draggable = false;
3576    document.body.appendChild(ghost);
3577    cframe.nodes.push(ghost);
3578    ghost.src = src; // after the append: one decode, already in cache
3579    cframe.ghost = ghost;
3580    cframe.dim = [cframeNode("__ppn_cropdim"), cframeNode("__ppn_cropdim"), cframeNode("__ppn_cropdim"), cframeNode("__ppn_cropdim")];
3581    cframe.box = cframeNode("__ppn_cropframe");
3582    cframe.grid = [cframeNode("__ppn_cropgrid"), cframeNode("__ppn_cropgrid"), cframeNode("__ppn_cropgrid"), cframeNode("__ppn_cropgrid")];
3583    cframe.dots = [];
3584    var ddirs = ["nw", "n", "ne", "e", "se", "s", "sw", "w"];
3585    for (var i = 0; i < ddirs.length; i++) {
3586      var dot = cframeNode("__ppn_cropdot");
3587      dot.setAttribute("data-dir", ddirs[i]);
3588      cframe.dots.push(dot);
3589    }
3590    cframe.badge = cframeNode("__ppn_cropdims");
3591    clearHover();
3592    syncHandles();
3593    return true;
3594  }
3595
3596  function positionCropFrame() {
3597    if (!cframe) return;
3598    // The element went away (a delivery re-grafted the document, the image was
3599    // replaced by an op) — there is nothing left to crop.
3600    if (!cframe.el.isConnected) { closeCropFrame(); return; }
3601    // An html re-graft wipes body children. Same hazard the rings have, same fix:
3602    // re-attach rather than re-position detached nodes into nowhere.
3603    for (var n = 0; n < cframe.nodes.length; n++) {
3604      if (!cframe.nodes[n].isConnected) document.body.appendChild(cframe.nodes[n]);
3605    }
3606    cframe.image = cropFramePicture(cframe.el);
3607    var im = cframe.image;
3608    var b = cropRectToBox(cframe.rect, im, cframe.natW, cframe.natH);
3609    placeCropNode(cframe.ghost, im);
3610    // The dim is FOUR BANDS = the picture minus the rect, laid out so no two
3611    // overlap (a seam where one 50% scrim stacks on another reads as a bug in the
3612    // photograph rather than as chrome). Same arrangement the framing session
3613    // uses, for the same reason.
3614    var botY = b.top + b.height, rightX = b.left + b.width;
3615    placeCropNode(cframe.dim[0], { left: im.left, top: im.top, width: im.width, height: b.top - im.top });
3616    placeCropNode(cframe.dim[1], { left: im.left, top: botY, width: im.width, height: im.top + im.height - botY });
3617    placeCropNode(cframe.dim[2], { left: im.left, top: b.top, width: b.left - im.left, height: b.height });
3618    placeCropNode(cframe.dim[3], { left: rightX, top: b.top, width: im.left + im.width - rightX, height: b.height });
3619    placeCropNode(cframe.box, b);
3620    var u = chromeUnit();
3621    var hair = u; // one SCREEN px, whatever the board is scaled to
3622    placeCropNode(cframe.grid[0], { left: b.left, top: b.top + b.height / 3, width: b.width, height: hair });
3623    placeCropNode(cframe.grid[1], { left: b.left, top: b.top + (b.height * 2) / 3, width: b.width, height: hair });
3624    placeCropNode(cframe.grid[2], { left: b.left + b.width / 3, top: b.top, width: hair, height: b.height });
3625    placeCropNode(cframe.grid[3], { left: b.left + (b.width * 2) / 3, top: b.top, width: hair, height: b.height });
3626    var midX = b.left + b.width / 2, midY = b.top + b.height / 2;
3627    var pts = {
3628      nw: [b.left, b.top], n: [midX, b.top], ne: [rightX, b.top], e: [rightX, midY],
3629      se: [rightX, botY], s: [midX, botY], sw: [b.left, botY], w: [b.left, midY],
3630    };
3631    var grip = 11 * u;
3632    for (var i = 0; i < cframe.dots.length; i++) {
3633      var dot = cframe.dots[i], p = pts[dot.getAttribute("data-dir")];
3634      // A LOCKED aspect hides the edge grips, exactly as the canvas overlay does:
3635      // an n/s/e/w handle on a ratio-locked rect is a lie told by the chrome —
3636      // one axis cannot move without the other.
3637      if (cframe.ratio != null && dot.getAttribute("data-dir").length === 1) { dot.style.display = "none"; continue; }
3638      dot.style.display = "block";
3639      dot.style.width = grip + "px";
3640      dot.style.height = grip + "px";
3641      dot.style.left = p[0] + "px";
3642      dot.style.top = p[1] + "px";
3643    }
3644    cframe.badge.textContent = Math.round(cframe.rect.w) + " \xd7 " + Math.round(cframe.rect.h);
3645    cframe.badge.style.left = midX + "px";
3646    cframe.badge.style.top = (botY + 8 * u) + "px";
3647  }
3648
3649  /** Is this event target the crop frame's business — its own chrome, or the
3650   *  picture it is drawn over? The one test both the press and the click use, so
3651   *  the two cannot disagree about what the session swallows. */
3652  function cropFrameOwnsPoint(target) {
3653    if (!cframe) return false;
3654    var cls = target && typeof target.className === "string" ? target.className : "";
3655    return cls.indexOf("__ppn_crop") >= 0 || target === cframe.el;
3656  }
3657
3658  function postCropFrame() {
3659    if (!cframe) return;
3660    post({ type: "crop-frame", rect: { x: cframe.rect.x, y: cframe.rect.y, w: cframe.rect.w, h: cframe.rect.h } });
3661  }
3662
3663  function beginCropFrameGesture(dir, e) {
3664    cframe.g = {
3665      dir: dir, startX: e.clientX, startY: e.clientY, ptrId: null, ptrEl: null,
3666      // Both the rect and the picture latched at pointerdown, for the reason
3667      // stated at length in compose-resize: a frame derived from the previous
3668      // frame inherits its rounding and walks.
3669      box0: cropRectToBox(cframe.rect, cframe.image, cframe.natW, cframe.natH),
3670      image0: { left: cframe.image.left, top: cframe.image.top, width: cframe.image.width, height: cframe.image.height },
3671    };
3672    capturePointer(cframe.g, e);
3673    document.body.classList.add("__ppn_dragging");
3674    postSelectionRect(); // the floating bar stands down for the gesture
3675  }
3676
3677  function applyCropFrameMove(e) {
3678    var g = cframe && cframe.g;
3679    if (!g) return;
3680    var im = g.image0;
3681    // Into the picture's own space (its top-left as origin), which is the space
3682    // cropDragRect works in. NO ZOOM DIVISION: a sandboxed iframe's pointer
3683    // coordinates are already in its unscaled layout px — the board's transform
3684    // lives on an ancestor OUTSIDE this document. (The canvas overlay, whose card
3685    // IS inside the transformed tree, divides by the React Flow zoom instead.)
3686    var start = { x: g.box0.left - im.left, y: g.box0.top - im.top, w: g.box0.width, h: g.box0.height };
3687    var min = Math.min(CROP_FRAME_MIN * chromeUnit(), Math.min(im.width, im.height));
3688    var next = cropDragRect(
3689      g.dir, start, e.clientX - g.startX, e.clientY - g.startY, im.width, im.height, cframe.ratio, min,
3690    );
3691    cframe.rect = cropBoxToRect(
3692      { left: im.left + next.x, top: im.top + next.y, width: next.w, height: next.h },
3693      im, cframe.natW, cframe.natH,
3694    );
3695    positionCropFrame();
3696    postCropFrame();
3697  }
3698
3699  function flushCropFrame() {
3700    cframeRaf = 0;
3701    var e = pendingCframeEvent;
3702    pendingCframeEvent = null;
3703    if (e) applyCropFrameMove(e);
3704  }
3705
3706  function endCropFrameGesture() {
3707    if (!cframe || !cframe.g) return;
3708    releasePointer(cframe.g);
3709    cframe.g = null;
3710    document.body.classList.remove("__ppn_dragging");
3711    if (cframeRaf) { cancelAnimationFrame(cframeRaf); cframeRaf = 0; }
3712    pendingCframeEvent = null;
3713    postCropFrame();
3714    postSelectionRect(); // and the bar comes back over the settled box
3715  }
3716
3717  function closeCropFrame() {
3718    if (!cframe) return;
3719    var sess = cframe;
3720    // Cleared BEFORE the sync, so it takes the ordinary path and puts the
3721    // selection chrome back rather than re-drawing the crop frame.
3722    cframe = null;
3723    if (sess.g) { releasePointer(sess.g); document.body.classList.remove("__ppn_dragging"); }
3724    if (cframeRaf) { cancelAnimationFrame(cframeRaf); cframeRaf = 0; }
3725    pendingCframeEvent = null;
3726    for (var i = 0; i < sess.nodes.length; i++) {
3727      if (sess.nodes[i].parentNode) sess.nodes[i].parentNode.removeChild(sess.nodes[i]);
3728    }
3729    syncHandles();
3730    postSelectionRect();
3731  }
3732
3733  /** The host's half of the two-way binding. */
3734  function setCropFrame(d) {
3735    if (!d || !d.rect) { closeCropFrame(); return; }
3736    var ratio = (typeof d.ratio === "number" && isFinite(d.ratio) && d.ratio > 0) ? d.ratio : null;
3737    var el = null;
3738    if (d.selector) { try { el = document.querySelector(d.selector); } catch (err) { el = null; } }
3739    if (!el && selectedSet.length === 1) el = selectedSet[0];
3740    if (!el || el.tagName !== "IMG") { closeCropFrame(); return; }
3741    var rect = { x: +d.rect.x || 0, y: +d.rect.y || 0, w: +d.rect.w || 0, h: +d.rect.h || 0 };
3742    if (!(rect.w > 0) || !(rect.h > 0)) { closeCropFrame(); return; }
3743    if (cframe && cframe.el === el) {
3744      cframe.ratio = ratio;
3745      // A DRAG OWNS THE RECT WHILE IT LASTS. Every move posts up, the parent
3746      // stores it and the effect posts it straight back down; applying that echo
3747      // mid-gesture would re-enter the arithmetic with a rounded copy of its own
3748      // last answer. The ratio still lands — an aspect press is the host's to make.
3749      if (!cframe.g) cframe.rect = rect;
3750      positionCropFrame();
3751      return;
3752    }
3753    openCropFrame(el, rect, ratio);
3754  }
3755
3756  // Live-set an attribute (image mini-bar src swap); parent persists the same.
3757  function setAttr(selector, name, value) {
3758    var el;
3759    try { el = document.querySelector(selector); } catch (err) { el = null; }
3760    if (!el) return;
3761    el.setAttribute(name, value);
3762    postSelection();
3763    syncHandles();
3764    postDocSize();
3765  }
3766
3767  // Live-write markup for an UNDO/REDO, or for a DIRECTOR delivery the parent
3768  // absorbed as component ops (parent replays ops instead of rebuilding the
3769  // srcDoc). outer=true swaps the element itself — empty html removes it, and an
3770  // unresolved selector RESTORES the markup into the frame sheet (undo of a
3771  // delete; the parent only sends that for a frames doc, whose components are
3772  // absolutely positioned with an explicit z-index, so order cannot shift the
3773  // paint). The inner form (text undo) leaves the element attached and keeps the
3774  // selection untouched.
3775  //
3776  // AN OUTER WRITE DETACHES nodes the selection may hold, so the selection is
3777  // RE-RESOLVED rather than dropped (ENG-11297): the selected elements are
3778  // addressed by SELECTOR, exactly as setBody already does for a streaming body
3779  // swap, so whatever still exists after the write stays selected and whatever
3780  // the write removed simply falls out. Dropping it wholesale was fine while this
3781  // channel only carried ⌘Z; once the DIRECTOR writes over it, a person editing
3782  // one element lost their selection every time Rae touched a different one.
3783  // Either way the parent is told (reselect posts the new selection), so the chip
3784  // / format bar / inspector can never describe an element that is gone.
3785  function setHtml(selector, html, outer) {
3786    var el;
3787    try { el = document.querySelector(selector); } catch (err) { el = null; }
3788    if (!outer) {
3789      if (!el) return;
3790      el.innerHTML = html;
3791      syncHandles();
3792      postDocSize();
3793      return;
3794    }
3795    if (!el && !html) return;       // already gone — nothing to remove
3796    var sheet = el ? null : document.querySelector("[data-frame]");
3797    if (!el && !sheet) return;      // nowhere to restore it into
3798    var keep = [];
3799    for (var i = 0; i < selectedSet.length; i++) {
3800      var sel = selectorFor(selectedSet[i]);
3801      if (sel) keep.push(sel);
3802    }
3803    // Drop live chrome + any in-flight gesture BEFORE the markup goes away, so no
3804    // handler is left holding a detached node.
3805    if (drag) endDrag();
3806    clearGuides();
3807    removeHandles();
3808    selectedSet = [];
3809    if (el) {
3810      if (html) el.outerHTML = html;
3811      else if (el.parentNode) el.parentNode.removeChild(el);
3812    } else {
3813      sheet.insertAdjacentHTML("beforeend", html);
3814    }
3815    clearHover();
3816    reselect(keep); // the selection outlives an outer write; what is gone drops out
3817    syncHandles();
3818    postDocSize();
3819  }
3820
3821${et}
3822
3823  // ── In-place ASSET catch-up (no document rebuild) ────────────────────────
3824  // The url-warm + the fail-closed image rule are injected above, from
3825  // compose-image-guard.ts (where they are unit-tested against a fake DOM):
3826  // warmAsset(url, done(ok)) / disarmBrokenImage(el) / sweepBrokenImages().
3827  // Apply the parent's asset patches to the LIVE document. Each entry either
3828  // re-sources an <img data-cid> or swaps the frame sheet's background layer.
3829  // Idempotent: an entry whose url already matches is skipped, so a re-flush
3830  // (e.g. after a reload) costs nothing.
3831  function patchAssets(list) {
3832    if (!list || !list.length) return;
3833    for (var i = 0; i < list.length; i++) applyAssetPatch(list[i]);
3834  }
3835  function applyAssetPatch(p) {
3836    if (!p) return;
3837    if (p.cid) {
3838      var sel = '[data-cid="' + String(p.cid).replace(/["\\\\]/g, "\\\\$&") + '"]';
3839      var el;
3840      try { el = document.querySelector(sel); } catch (err) { el = null; }
3841      if (!el || el.tagName !== "IMG") return;
3842      var wantSrc = typeof p.src === "string" ? p.src : null;
3843      var wantAlt = typeof p.alt === "string" ? p.alt : null;
3844      // Apply BOTH attributes in one shot, and — when a url is arriving — only
3845      // inside warmAsset's callback, i.e. after the bytes are decoded. Setting alt
3846      // first would restore a real description onto an element whose src is still
3847      // empty, which is the exact combination that paints the broken-image glyph:
3848      // we would flash the defect we are removing, for one frame, on every fill.
3849      //
3850      // THE ALT FOLLOWS THE ASSET — and an asset that did not load is not there.
3851      // \`ok\` is warmAsset's verdict AFTER its retries. When it is false the url
3852      // still gets seated (a later rebuild / reload can resolve it, and the parent's
3853      // stored document says the slot is filled) but the alt stays EMPTY, so the
3854      // engine paints the styled box rather than the torn-page glyph plus a
3855      // description of artwork nobody can see. Same rule as the reserved slot in
3856      // schema-render.ts, extended from "no url yet" to "url that did not load" —
3857      // the two look identical on screen and must therefore behave identically.
3858      function applyAttrs(ok) {
3859        if (wantAlt !== null) el.setAttribute("alt", ok ? wantAlt : "");
3860        if (wantSrc !== null) el.setAttribute("src", wantSrc);
3861        syncHandles();
3862        postDocSize();
3863      }
3864      if (wantSrc !== null && el.getAttribute("src") !== wantSrc) { warmAsset(wantSrc, applyAttrs); return; }
3865      if (wantAlt !== null && el.getAttribute("alt") !== wantAlt) applyAttrs(true);
3866      return;
3867    }
3868    // The frame sheet's background layer — url only, no alt to speak of.
3869    if (typeof p.src !== "string") return;
3870    var sheet = document.querySelector("[data-frame]");
3871    if (!sheet) return;
3872    var next = "url('" + String(p.src).replace(/['\\\\]/g, "\\\\$&") + "')";
3873    if (sheet.style.backgroundImage === next) return;
3874    warmAsset(p.src, function () {
3875      sheet.style.backgroundImage = next;
3876      postDocSize();
3877    });
3878  }
3879
3880  // Hover resolution, shared by the mouseover boundary event and the in-box
3881  // mousemove refresh below. Uses pickAt so the highlight lands on the CONTENT
3882  // under the pointer, not on whatever oversized box covers the point (ask ③).
3883  function hoverAt(x, y, fallback) {
3884    if (!active || editingEl || marqueeEl || drag) return;
3885    var el = pickAt(x, y, fallback);
3886    if (!el || el.nodeType !== 1 || SKIP[el.tagName] || isChrome(el)) { clearHover(); return; }
3887    // Hovering a letter of an arched word highlights the WHOLE word (the unit the
3888    // click will select).
3889    var hg = archGroupOf(el);
3890    if (hg) el = hg;
3891    if (selectedSet.indexOf(el) >= 0) { clearHover(); return; }
3892    if (hovered === el) return; // already lit — don't churn classes each frame
3893    clearHover();
3894    hovered = el;
3895    el.classList.add("__ppn_hover");
3896    positionHoverBox();
3897  }
3898  // ── THE ROTATE CURSOR IS A HOVER STATE (ENG-11582) ───────────────────────
3899  // \`__ppn_rotate\` on the body says "the pointer is in a rotate zone". Driven off
3900  // \`mouseover\`, which fires on every crossing INTO a node — the zone when you
3901  // reach past a corner, and whatever is beneath it the moment you leave — so one
3902  // handler covers both edges of the state and there is nothing to poll.
3903  //
3904  // A body class rather than the zone's own cursor rule alone, because the class
3905  // is the one fact a test, a screenshot and the drag can all read; the zone
3906  // rules ride on top of it with the per-corner turn (see the stylesheet).
3907  //
3908  // Withheld for the hand and the placement tools, exactly as the hover highlight
3909  // is: with either up, a press in the zone is a pan or a create, and a rotate
3910  // cursor would be the chrome offering a gesture the runtime will not perform.
3911  function syncRotateCursor(target) {
3912    var on =
3913      !!target && typeof target.className === "string" &&
3914      target.className.indexOf("__ppn_rotzone") >= 0 &&
3915      curTool === "select" && !editingEl && !cropSess && !cframe;
3916    document.body.classList.toggle("__ppn_rotate", on);
3917  }
3918  function onOver(e) {
3919    if (curTool === "hand") { syncRotateCursor(null); return; } // the hand tool never highlights pickable content
3920    // Nor does an armed placement tool: the press is going to CREATE, so lighting
3921    // up what it would have selected is an offer the tool will not honour.
3922    if (PLACE_TOOLS[curTool]) { syncRotateCursor(null); return; }
3923    syncRotateCursor(e.target);
3924    hoverAt(e.clientX, e.clientY, e.target);
3925  }
3926  function onOut() {
3927    syncRotateCursor(null);
3928    if (active && !editingEl) clearHover();
3929  }
3930
3931  // ── WHAT A BAND CAN CATCH (ENG-11665) ───────────────────────────────────
3932  // Direct text ink: does this element say anything ITSELF, as opposed to
3933  // through the things it contains? One walk over its own child TEXT nodes — no
3934  // layout read, so it is free to call on every press.
3935  function hasDirectTextInk(el) {
3936    var n = el.firstChild;
3937    while (n) {
3938      if (n.nodeType === 3 && n.nodeValue && n.nodeValue.trim()) return true;
3939      n = n.nextSibling;
3940    }
3941    return false;
3942  }
3943  // Is this element inside something whose insides are not page elements — an
3944  // <svg>'s paths, a <picture>'s sources? Those are never selectable units.
3945  function atomicAncestor(el) {
3946    var p = el.parentElement;
3947    while (p && p !== document.body && p !== document.documentElement) {
3948      if (ATOMIC[p.tagName]) return p;
3949      p = p.parentElement;
3950    }
3951    return null;
3952  }
3953  // A LEAF is a unit of content: a headline, a paragraph, a button, a picture, a
3954  // shape. Not a box that holds those. This is the whole hit rule for a marquee
3955  // (see marqueeSelect) — a band across a section must hand back its heading, its
3956  // paragraph and its button, which is what "change these two green" is asking about.
3957  function isMarqueeLeaf(el) {
3958    if (ATOMIC[el.tagName]) return true;
3959    // A headline / paragraph / button label: text whose element children are all
3960    // inline. Its <b> and <span> are parts of it, not siblings of it.
3961    if (isTextBearing(el)) return true;
3962    // An arched word is ONE thing, exactly as it is to a click.
3963    if (archGroupOf(el) === el) return true;
3964    var kids = el.children;
3965    for (var i = 0; i < kids.length; i++) {
3966      if (isChrome(kids[i]) || SKIP[kids[i].tagName]) continue;
3967      return false; // it contains content → it is a container, not a unit
3968    }
3969    return true; // an empty shape, a rule, a spacer with a fill
3970  }
3971  // ── marquee: drag a rectangle to select every LEAF entirely within it.
3972  //
3973  // IT USED TO KEEP "the outermost fully-enclosed unit", i.e. cards as cards
3974  // (and, on a page whose section fitted inside the band, the SECTION). That was
3975  // the right rule while the marquee could only ever start on the page
3976  // background, where a band was a coarse "grab that region" gesture. Now that a
3977  // press on any container starts one (see isContainerHit), the band is the
3978  // ordinary way to pick several things, and the things people mean are the ones
3979  // they can see: the heading, the paragraph, the button. Handing back their
3980  // shared parent instead would answer "make these two green" by painting the
3981  // whole section green — one element, the wrong one, and no way to say the
3982  // other thing at all.
3983  //
3984  // Shift adds to the current set; a plain marquee replaces it.
3985  //
3986  // ── THE CANDIDATES ARE MEASURED ONCE (SUNRA-1213) ────────────────────────
3987  // The walk and the one chromeRect per leaf are split out of marqueeSelect
3988  // because the band now selects LIVE, i.e. the hit test runs on every
3989  // pointermove. What it measures cannot move while the band is drawn — the band
3990  // moves, the page does not — so latching the list when the band is BORN makes
3991  // every later frame pure arithmetic instead of N forced reflows. Same
3992  // discipline as every other gesture here (the resize anchor, the rotate centre,
3993  // the drag's snap targets): measure at the start, never mid-gesture.
3994  function marqueeCandidates() {
3995    var all = document.body.getElementsByTagName("*");
3996    var out = [];
3997    for (var i = 0; i < all.length; i++) {
3998      var el = all[i];
3999      if (SKIP[el.tagName]) continue;
4000      if (isChrome(el)) continue; // skip our overlay nodes, NOT selected/hovered real elements
4001      if (isSpacer(el)) continue; // the hole a delete left is not a selectable unit (ENG-11643)
4002      if (isRootSheet(el)) continue; // the page itself is never a marquee hit
4003      if (atomicAncestor(el)) continue; // an <svg>'s own guts are not page elements
4004      // The letters of an arched word are the word's, not the band's.
4005      var g = archGroupOf(el);
4006      if (g && g !== el) continue;
4007      if (!isMarqueeLeaf(el)) continue;
4008      // Measured geometry (ask ③), so a marquee catches what the user SEES: an
4009      // oversized text box is caught once its glyphs are, not once its stale
4010      // full-width box is.
4011      var r = chromeRect(el);
4012      if (r.width === 0 || r.height === 0) continue;
4013      out.push({ el: el, left: r.left, top: r.top, right: r.left + r.width, bottom: r.top + r.height });
4014    }
4015    return out;
4016  }
4017  // ── A BAND SELECTS ON TOUCH, NOT ON CONTAINMENT (SUNRA-1213) ─────────────
4018  //
4019  // THE RULING THIS OVERTURNS: "drag a rectangle to select every LEAF entirely
4020  // within it". Andy 2026-09-20, Figma on screen: "选框一碰到某个组件就应立刻选中它
4021  // (相交即选,不是包含才选)" — a band overlapping the corner of a dark card left
4022  // it unselected.
4023  //
4024  // INTERSECTION IS THE CONVENTION and containment was the outlier: Figma,
4025  // Illustrator, Sketch and every OS file manager select what the rubber band
4026  // TOUCHES. Containment also fails hardest on the things a deck is made of — a
4027  // full-bleed hero, a headline wider than the artboard is tall — because no band
4028  // the user can draw ever encloses them, so the gesture silently does nothing.
4029  //
4030  // NOTHING ABOUT WHAT COUNTS AS A UNIT MOVES. The container scoping is exactly
4031  // as it was (leaves only, never a box that holds them, never an <svg>'s guts,
4032  // never a spacer, never the page — see marqueeCandidates). Touching changes
4033  // WHICH leaves answer, not WHAT a leaf is.
4034  function marqueeSelect(rect, cands) {
4035    var list = cands || marqueeCandidates();
4036    var kept = [];
4037    for (var i = 0; i < list.length; i++) {
4038      var c = list[i];
4039      // Strict comparisons, so a band that merely GRAZES an edge counts as a hit —
4040      // "碰到" is the whole ruling, and a >= here would make the one-pixel overlap
4041      // the screenshot showed go on missing.
4042      if (c.right < rect.left || c.left > rect.right) continue;
4043      if (c.bottom < rect.top || c.top > rect.bottom) continue;
4044      kept.push(c.el);
4045    }
4046    return kept;
4047  }
4048  // The band's rect as it stands right now, read off the overlay's own style —
4049  // ONE reader for the live update and for the release, so "what was highlighted"
4050  // and "what got selected" can never come from two different numbers.
4051  function marqueeRectNow() {
4052    if (!marqueeEl) return null;
4053    var l = parseFloat(marqueeEl.style.left), t = parseFloat(marqueeEl.style.top);
4054    return {
4055      left: l, top: t,
4056      right: l + parseFloat(marqueeEl.style.width),
4057      bottom: t + parseFloat(marqueeEl.style.height),
4058    };
4059  }
4060  function sameSelection(a, b) {
4061    if (a.length !== b.length) return false;
4062    for (var i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
4063    return true;
4064  }
4065  // ── THE BAND'S SELECTION IS LIVE (SUNRA-1213) ───────────────────────────
4066  // Andy: selected "live as the box grows/shrinks". It used to land only on
4067  // release, so the user drew a rectangle and found out afterwards what they had
4068  // caught.
4069  //
4070  // APPLIED ONLY WHEN IT CHANGED. A band crosses an element once and fires
4071  // pointermove dozens of times; re-stamping the classes and posting a selection
4072  // on every one of them would churn the DOM and flood the parent (each message
4073  // carries a snippet per element). The additive case re-derives from the set
4074  // latched at the band's start rather than accumulating, so shrinking the band
4075  // back releases what it no longer touches instead of keeping it for good.
4076  function applyMarqueeSelection(rect) {
4077    if (!down || !rect) return false;
4078    var hits = marqueeSelect(rect, down.cands);
4079    var next = hits;
4080    if (down.shift) {
4081      next = (down.base || []).slice();
4082      for (var i = 0; i < hits.length; i++) if (next.indexOf(hits[i]) < 0) next.push(hits[i]);
4083    }
4084    if (sameSelection(next, selectedSet)) return false;
4085    selectedSet = next;
4086    applyOutlines();
4087    postSelection();
4088    return true;
4089  }
4090  // ── A PRESS ON A CONTAINER IS A MARQUEE (ENG-11665) ─────────────────────
4091  //
4092  // THE BUG, exactly: onDown's move branch claimed every press on any non-root
4093  // element, and on a real authored page there is no such thing as a press that
4094  // lands on nothing — a <section> or a wrapper <div> covers every pixel between
4095  // the words. So the marquee branch below it was reachable only with a modifier
4096  // held, and the plain drag-a-box gesture every canvas tool has did not exist.
4097  //
4098  // The rule is Lovable's and Figma's: press on a CONTAINER (a box that holds
4099  // things and says nothing itself) and you are drawing a band; press on a LEAF
4100  // (words, a picture, a button) and you are holding that thing. Which means the
4101  // question this answers is not "is it draggable" — everything is — but "is
4102  // this press about the thing under the pointer, or about a region".
4103  //
4104  // THE ONE EXCEPTION IS THE SELECTION ITSELF: pressing a container that is
4105  // already selected still drags it (and, per the canvas rule below, the whole
4106  // group with it). Losing a group you had just banded, because your next press
4107  // landed on one of its members, would make the band useless.
4108  function isContainerHit(el) {
4109    if (!el || el.nodeType !== 1) return false;
4110    if (!CONTAINER[el.tagName]) return false;   // a leaf, or a tag we do not know
4111    if (SKIP[el.tagName] || isChrome(el)) return false;
4112    if (el === document.body || el === document.documentElement) return false;
4113    if (hasDirectTextInk(el)) return false;     // it says something of its own
4114    if (isTextBearing(el)) return false;        // …and the same fact from the other side
4115    if (archGroupOf(el)) return false;          // an arched word is one unit, never a region
4116    if (selectedSet.indexOf(el) >= 0) return false; // press on the selection = drag it
4117    return true;
4118  }
4119  function onDown(e) {
4120    if (!active || editingEl) return;
4121    // ── THE FRAME MUST HOLD FOCUS FOR A KEY TO REACH IT (ENG-11620 追加 2) ──
4122    // Every keyboard gesture this runtime owns — the arrow nudge, Delete, Escape,
4123    // the R/L/O shape letters, the forwarded undo — arrives at THIS document's
4124    // keydown, and the browser routes a keystroke only to the focused frame.
4125    // Pressing inside an iframe normally focuses it; this handler calls
4126    // preventDefault() on the pointerdown (to suppress native text selection and
4127    // the <img> drag ghost) and that suppresses the focus transfer along with it.
4128    // Measured on the real page before this line existed: after clicking an
4129    // element the parent's document.activeElement was still <body>, the frame's
4130    // document.hasFocus() was false, and no arrow key ever reached the board.
4131    //
4132    // NOTHING IS STOLEN. It is taken from INSIDE, on a press the frame already
4133    // owns: a click on an inspector field is a press in the PARENT and never
4134    // reaches this handler, and neither does a parent-driven \`reselect\` (which is
4135    // how the inspector selects something while its own input keeps the caret).
4136    // The hasFocus() gate makes it a no-op for every press after the first, so an
4137    // open inline editor / caret anchor is never refocused out from under itself.
4138    try { if (!document.hasFocus || !document.hasFocus()) window.focus(); } catch (err) { /* best-effort */ }
4139    // A NUDGE BURST ENDS WHERE A POINTER GESTURE BEGINS. beginDrag is about to
4140    // latch a fresh base off the element's CURRENT box; if the arrow move that
4141    // put it there had not been committed first, the drag's own commit would
4142    // carry it silently into the same history entry — and a burst abandoned by
4143    // switching to the mouse would leave no undo step of its own.
4144    endNudge();
4145    // A PENDING CARET (ENG-11545) is cancelled by a press ANYWHERE ELSE, and the
4146    // press then means whatever the current tool says it means. Dropped at the
4147    // top, above every branch, so the gesture that cancels the caret is never
4148    // also the gesture that starts something on top of it.
4149    if (caretPend) dropTextCaret();
4150    // HAND TOOL (⑫) — the press is a PAN, wherever it landed. Swallowed before any
4151    // handle / pick / marquee branch so the tool is unambiguous: with the hand up,
4152    // nothing in the preview is selectable, resizable or draggable.
4153    if (curTool === "hand") {
4154      if (e.button !== 0) return;
4155      // Captured for the same reason a drag is: a pan released outside the frame
4156      // used to leave the board panning on the next move back in.
4157      pan = { x: e.clientX, y: e.clientY, ptrId: null, ptrEl: null };
4158      capturePointer(pan, e);
4159      document.body.classList.add("__ppn_panning");
4160      e.preventDefault();
4161      e.stopPropagation();
4162      return;
4163    }
4164    // PLACEMENT TOOL (ENG-11306) — the press CREATES, wherever it landed.
4165    // Swallowed here, beside the hand branch and for the same reason: with a
4166    // placement tool armed nothing in the preview is selectable, resizable or
4167    // draggable, so no handle / pick / marquee branch may see this press.
4168    if (PLACE_TOOLS[curTool]) {
4169      if (e.button !== 0) return;
4170      e.preventDefault();
4171      e.stopPropagation();
4172      placeClick = true;
4173      // TEXT ARMS A CARET, the shapes still build (ENG-11545). The T tool's press
4174      // no longer creates anything: it marks the spot and waits for a character.
4175      if (curTool === "text") armTextCaret(e.clientX, e.clientY, e.target);
4176      else placePrimitive(curTool, e.clientX, e.clientY);
4177      return;
4178    }
4179    // THE CROP FRAME OWNS THE PRESS while the panel holds it open — ranked with
4180    // (and above) the framing session for the same reason: a mode that can be
4181    // half-escaped by pressing the wrong pixel is not a mode. It differs in what
4182    // a press ELSEWHERE means: the framing crop commits on click-away, this one
4183    // does not, because its commit is the panel's Apply button and a stray click
4184    // must never spend the user's credits.
4185    if (cframe) {
4186      var ft = e.target;
4187      var fcls = ft && typeof ft.className === "string" ? ft.className : "";
4188      if (fcls.indexOf("__ppn_cropdot") >= 0) {
4189        beginCropFrameGesture(ft.getAttribute("data-dir") || "se", e);
4190        e.preventDefault();
4191        e.stopPropagation();
4192        return;
4193      }
4194      // The box's interior slides the whole rect, as the canvas overlay's does.
4195      if (fcls.indexOf("__ppn_cropframe") >= 0) {
4196        beginCropFrameGesture("move", e);
4197        e.preventDefault();
4198        e.stopPropagation();
4199        return;
4200      }
4201      // A press on the plate or the dim — the photograph, in other words — is
4202      // swallowed. It is not a pick (the picture under the session is not a page
4203      // element right now) and it is not a drag; letting it fall through would
4204      // select whatever the plate happens to be covering.
4205      if (cropFrameOwnsPoint(ft)) {
4206        e.preventDefault();
4207        e.stopPropagation();
4208        return;
4209      }
4210      // Anywhere else falls through: the press means what it would have meant,
4211      // and selecting a different element is what closes the panel (and with it
4212      // this session) parent-side.
4213    }
4214    // CROP MODE OWNS THE PRESS while it is open — ranked above every branch
4215    // below because a mode that could be half-escaped by pressing the wrong
4216    // pixel is not a mode.
4217    if (cropSess) {
4218      var ct = e.target;
4219      var ccls = ct && typeof ct.className === "string" ? ct.className : "";
4220      // A tab or the band behind it — both resolve to a direction and both mean
4221      // the same gesture. Ranked above the ghost so an edge press resizes the
4222      // window instead of panning the picture underneath it.
4223      if (ccls.indexOf("__ppn_crophandle") >= 0 || ccls.indexOf("__ppn_cropedge") >= 0) {
4224        beginCropGesture(ct.getAttribute("data-dir") || "se", e);
4225        e.preventDefault();
4226        e.stopPropagation();
4227        return;
4228      }
4229      // The ghost spans the whole picture, in and out of the window, so one test
4230      // covers "pressed the photograph" wherever it landed. The element itself is
4231      // included for the frames where the ghost has not painted yet.
4232      if (ccls.indexOf("__ppn_cropghost") >= 0 || ct === cropSess.el) {
4233        beginCropGesture("pan", e);
4234        e.preventDefault();
4235        e.stopPropagation();
4236        return;
4237      }
4238      // Anywhere else IS the commit — and then the press goes on to mean what it
4239      // would have meant anyway, so clicking away both lands the crop and selects
4240      // what was clicked. One press, both meanings, which is what makes
4241      // click-away feel like leaving rather than like a dialog dism
4241issal.
4242      exitCrop(true);
4243    }
4244    var t = e.target;
4245    // 0) a ROTATE ZONE → turn the single selection about its own centre
4246    // (ENG-11582). Read before the handle branch, though the two can never both
4247    // match: a node carries one className, and the zones are z-ranked under the
4248    // squares so a press that lands on a corner handle never reaches one. The
4249    // ordering is about READING — "outside the corner" is the outermost thing a
4250    // press can hit, so it is answered first.
4251    if (t && typeof t.className === "string" && t.className.indexOf("__ppn_rotzone") >= 0) {
4252      var rsel = draggableSel();
4253      if (rsel) {
4254        beginDrag(rsel, "rotate", e);
4255        e.preventDefault();
4256        e.stopPropagation();
4257        return;
4258      }
4259    }
4260    // 0.5) a GROUP CORNER (SUNRA-1213) → scale the whole multi-selection about the
4261    // opposite corner. Ranked ABOVE the handle branch below and read off
4262    // \`e.target\` like every other chrome gesture, because the node carries
4263    // \`__ppn_handle\` too (it is the same square): the branch below would ask
4264    // draggableSel(), get null for a multi-selection, fall through, and let the
4265    // press become a pick on whatever the square happens to cover.
4266    if (t && typeof t.className === "string" && t.className.indexOf("__ppn_ghandle") >= 0) {
4267      if (beginGroupResize(t.getAttribute("data-dir") || "se", e)) {
4268        e.preventDefault();
4269        e.stopPropagation();
4270        return;
4271      }
4272    }
4273    // 1) a resize handle → resize the single selection.
4274    if (
4275      t && typeof t.className === "string" &&
4276      (t.className.indexOf("__ppn_handle") >= 0 || t.className.indexOf("__ppn_edge") >= 0)
4277    ) {
4278      // A band resolves to a direction exactly like the square it stands behind,
4279      // so the gesture below is the SAME gesture — same beginDrag, same latched
4280      // anchor, same aspect-lock rules per element type. An edge drag on a
4281      // ratio-locked image therefore behaves as its midpoint handle would,
4282      // because it IS that code path with the same argument.
4283      var sel = draggableSel();
4284      if (sel) {
4285        beginDrag(sel, t.getAttribute("data-dir") || "se", e);
4286        e.preventDefault();
4287        e.stopPropagation();
4288        return;
4289      }
4290    }
4291    // Past the handles, the press acts on the CONTENT under the pointer — an
4292    // oversized text box no longer claims presses across its empty width (ask ③).
4293    t = pickAt(e.clientX, e.clientY, t);
4294    // 2) mousedown on a movable LEAF (not root sheet / background / chrome / a
4295    // container) → select it (plain click) and start a MOVE. Deep target matches
4296    // click-select; additive (shift/⌘) falls through to marquee so multi-select
4297    // still works, and so does a press on a container (ENG-11665).
4298    var additive = e.shiftKey || e.metaKey || e.ctrlKey;
4299    // An arched word letter → select / drag the WHOLE group, not the lone letter.
4300    if (!additive) {
4301      var g = archGroupOf(t);
4302      if (g) t = g;
4303    }
4304    if (
4305      !additive && t && t.nodeType === 1 && !isChrome(t) && !SKIP[t.tagName] &&
4306      !isRootSheet(t) && t !== document.body && t !== document.documentElement &&
4307      // …and the press is about THIS THING, not about a region (ENG-11665).
4308      // A container falls through to branch 3 and draws a band instead.
4309      !isContainerHit(t)
4310    ) {
4311      // Canvas rule: pressing an element that is ALREADY part of the selection
4312      // keeps that selection and drags the whole group. Collapsing to [t] here is
4313      // what made group move impossible — the set was reduced to one before the
4314      // drag even began, so a marquee of three could only ever move one.
4315      if (selectedSet.indexOf(t) < 0) {
4316        selectedSet = [t];
4317        clearHover();
4318        applyOutlines();
4319        postSelection();
4320      }
4321      beginDrag(t, "move", e);
4322      e.preventDefault();
4323      return;
4324    }
4325    // 2.5) INSIDE THE UNION BOX → DRAG THE WHOLE GROUP (SUNRA-1213).
4326    //
4327    // Andy 2026-09-20: "多个对象被选中后,在选区框内的空白处按下并拖动,应移动整组;
4328    // 现在只有按在实际对象上才能拖."
4329    //
4330    // Branch 2 above already drags the group when the press lands ON a member (the
4331    // canvas rule: pressing part of a selection keeps it and moves all of it). What
4332    // was missing is the EMPTY SPACE BETWEEN the members, which on a real authored
4333    // page is a <section>, a wrapper or the sheet — i.e. precisely the presses
4334    // branch 2 declines and hands to the band. So this sits BETWEEN the two and
4335    // takes only what branch 2 left, and only inside the box:
4336    //
4337    //   \xb7 a press on an UNSELECTED LEAF inside the box still selects that leaf
4338    //     (branch 2), so a group cannot swallow the ability to pick a third card
4339    //     standing between two selected ones;
4340    //   \xb7 an ADDITIVE press still falls through — that is how ⌘/⇧-clicking a
4341    //     member TOGGLES it (onClick's toggleInSet), which Andy asked to keep;
4342    //   \xb7 NOTHING IS SELECTED OR DESELECTED HERE. A press is not a click: with no
4343    //     movement this commits nothing (endDrag's \`moved\` gate) and never sets
4344    //     suppressClick, so the trailing click decides exactly as it does for a
4345    //     single selection — a release on the bare sheet clears, a container
4346    //     selects itself.
4347    //
4348    // GEOMETRY, NOT A HIT TARGET. The union box overlay stays pointer-events: none
4349    // (making it hittable would block every click on what lies under it), so the
4350    // test is a point against selectionChromeRect() — the same rect the box is
4351    // drawn at, so what the user sees enclosed is what answers.
4352    if (!additive && selectedSet.length > 1) {
4353      var ubox = selectionChromeRect();
4354      if (
4355        ubox &&
4356        e.clientX >= ubox.left && e.clientX <= ubox.left + ubox.width &&
4357        e.clientY >= ubox.top && e.clientY <= ubox.top + ubox.height
4358      ) {
4359        var gmem = groupMembers();
4360        if (gmem.length > 1) {
4361          // The PRIMARY is a member, not the thing under the pointer — there may be
4362          // nothing there at all. Every other member follows it by the RESOLVED
4363          // (post-snap) delta, which is the group move applyDragMove already does.
4364          beginDrag(gmem[0], "move", e);
4365          e.preventDefault();
4366          return;
4367        }
4368      }
4369    }
4370    // 3) background / container / additive → marquee. CAPTURED like pan and drag (the
4371    // 2026-08-01 rule, which this branch was the last gesture to miss): without
4372    // capture, a band whose release lands outside the frame's edge never gets
4373    // its pointerup — the blue rectangle stayed painted at wherever the drag
4374    // ended, surviving scrolling, until some later in-frame move hit the
4375    // buttons === 0 floor (Andy 2026-08-17, stuck marquee over the preview).
4376    // With capture the up always arrives here: the selection applies and the
4377    // band clears, and mid-drag the band keeps tracking past the frame edge.
4378    down = { x: e.clientX, y: e.clientY, shift: additive, ptrId: null, ptrEl: null };
4379    capturePointer(down, e);
4380  }
4381  // rAF-coalesced drag: a mousemove only records the latest pointer and schedules
4382  // one DOM write per frame, so a burst of moves never thrashes layout (③/④
4383  // smoothness). The dragged element is position:absolute (out of flow), so the
4384  // write reflows nothing but itself; commit happens on settle (endDrag) only.
4385  var dragRaf = 0;
4386  var pendingDragEvent = null;
4387  var hoverRaf = 0;
4388  var pendingHoverEvent = null;
4389  function flushHover() {
4390    hoverRaf = 0;
4391    var e = pendingHoverEvent;
4392    pendingHoverEvent = null;
4393    if (e) hoverAt(e.clientX, e.clientY, e.target);
4394  }
4395  function flushDrag() {
4396    dragRaf = 0;
4397    if (drag && pendingDragEvent) applyDragMove(pendingDragEvent);
4398  }
4399  // THE FLOOR UNDER POINTER CAPTURE. Capture is the fix; this is what makes the
4400  // fix unfalsifiable. If a release ever escapes the frame anyway — capture
4401  // refused, a pointer type the frame never owned, a browser that drops capture
4402  // when the node reflows — then the very next move arrives with NO button held,
4403  // and a gesture that outlived its own button is over by definition. Ending it
4404  // here (rather than applying the move first) commits the box the user actually
4405  // released at, which is what the missing pointerup would have done.
4406  function endStrandedGesture() {
4407    // A held arrow key whose keyup will never arrive (the window blurred with the
4408    // key down) is exactly as stranded as a drag whose pointerup went to the
4409    // parent — and it strands the same way: the move stays on screen and never
4410    // reaches the stored html. Land it.
4411    endNudge();
4412    if (pan) {
4413      releasePointer(pan);
4414      pan = null;
4415      document.body.classList.remove("__ppn_panning");
4416    }
4417    if (drag) endDrag(); // releases its own capture + commits the settled box
4418    // A crop DRAG is ended; the crop SESSION is not. Losing the pointer (a
4419    // pointercancel, the window blurring) says the gesture is over, not that the
4420    // user is done cropping — and tearing the mode down on a blur would close it
4421    // every time they reached for a different app and came back.
4422    endCropGesture();
4423    // Same ruling for the crop FRAME: the drag ends, the session stays. The
4424    // panel opened it and only the panel (or a selection change) closes it.
4425    endCropFrameGesture();
4426    // THE BAND IS DROPPED; WHAT IT ALREADY SELECTED STANDS (SUNRA-1213 revises
4427    // this). It used to be "a marquee is ABANDONED rather than applied", on the
4428    // grounds that its last rectangle was sized wherever the pointer was when the
4429    // frame lost it — "not a selection the user ever drew". Under a LIVE band that
4430    // premise is gone: every rect the band ever had was applied and highlighted as
4431    // it was drawn, so the set standing when the pointer went missing is exactly
4432    // what was on screen. Losing a pointer is not an undo.
4433    if (marqueeEl) { marqueeEl.remove(); marqueeEl = null; }
4434    if (gres) endGroupResize(); // …which releases its own capture + commits
4435    if (down) releasePointer(down);
4436    down = null;
4437  }
4438  function onMove(e) {
4439    // WHERE THE POINTER IS, kept for ⌘V (ENG-12620) — see PickerPasteMsg.at.
4440    // First statement in the handler, BEFORE every gesture early-return: a paste
4441    // taken mid-drag still wants the pointer's real position, and the cheapest
4442    // honest tracker is the one that runs unconditionally. Two number writes.
4443    lastPt = { x: e.clientX, y: e.clientY };
4444    if ((drag || pan || down || gres || (cropSess && cropSess.g) || (cframe && cframe.g)) && e.buttons === 0) { endStrandedGesture(); return; }
4445    // A live crop drag, rAF-coalesced exactly as the element drag below is: one
4446    // pass over the session's ten overlay nodes per frame, never one per move.
4447    if (cropSess && cropSess.g) {
4448      pendingCropEvent = e;
4449      e.preventDefault();
4450      if (!cropRaf) cropRaf = requestAnimationFrame(flushCrop);
4451      return;
4452    }
4453    // The crop FRAME's drag, coalesced the same way — and for one more reason
4454    // than the others: every uncoalesced move would also be a postMessage into
4455    // the parent's panel state.
4456    if (cframe && cframe.g) {
4457      pendingCframeEvent = e;
4458      e.preventDefault();
4459      if (!cframeRaf) cframeRaf = requestAnimationFrame(flushCropFrame);
4460      return;
4461    }
4462    if (pan) {
4463      // Screen-px deltas, forwarded per move — the parent adds them to the artboard
4464      // pan exactly as its own empty-floor drag does, so the two gestures agree at
4465      // every zoom. No rAF coalescing: one message per move is what the parent's
4466      // own mousemove pan already costs.
4467      post({ type: "pan", dx: e.clientX - pan.x, dy: e.clientY - pan.y });
4468      // MUTATE, never re-create: the captured pointer id rides on this object and
4469      // a fresh literal would drop it (and with it the release).
4470      pan.x = e.clientX;
4471      pan.y = e.clientY;
4472      e.preventDefault();
4473      return;
4474    }
4475    if (drag) {
4476      pendingDragEvent = e;
4477      e.preventDefault();
4478      if (!dragRaf) dragRaf = requestAnimationFrame(flushDrag);
4479      return;
4480    }
4481    // A GROUP RESIZE, coalesced exactly as the element drag above is: one pass
4482    // over N members' boxes plus the chrome per PAINTED frame, never one per move.
4483    if (gres) {
4484      pendingGresEvent = e;
4485      e.preventDefault();
4486      if (!gresRaf) gresRaf = requestAnimationFrame(flushGroupResize);
4487      return;
4488    }
4489    if (!down) {
4490      // Crossing the glyph boundary INSIDE an ink-driven text box changes what is
4491      // hoverable but fires no mouseover (it is all one element), so refresh here —
4492      // rAF-coalesced, and gated on that case alone so an idle canvas pays only an
4493      // isTextBearing() check per move, never a layout read (ask ③).
4494      var raw = e.target;
4495      if (raw && raw.nodeType === 1 && !isChrome(raw) && usesInkChrome(raw)) {
4496        pendingHoverEvent = e;
4497        if (!hoverRaf) hoverRaf = requestAnimationFrame(flushHover);
4498      }
4499      return;
4500    }
4501    var dx = e.clientX - down.x, dy = e.clientY - down.y;
4502    if (!marqueeEl && (Math.abs(dx) > 4 || Math.abs(dy) > 4)) {
4503      clearHover();
4504      marqueeEl = document.createElement("div");
4505      marqueeEl.className = "__ppn_marquee";
4506      document.body.appendChild(marqueeEl);
4507      // Latched at the band's BIRTH, both of them (SUNRA-1213): the leaves it can
4508      // catch (with their rects — see marqueeCandidates) and the selection it
4509      // started from, which is what an additive band adds to.
4510      down.cands = marqueeCandidates();
4511      down.base = selectedSet.slice();
4512      // A BAND IS A CHROME STATE, so the chrome re-decides the moment one exists
4513      // (SUNRA-1213). draggableSel() and syncGroupHandles both already refuse while
4514      // \`marqueeEl\` is up, but nothing was re-asserting them — so the standing
4515      // selection's eight squares, and now its four group corners, sat over the band
4516      // offering gestures the runtime would not perform until the set happened to
4517      // change under it.
4518      syncHandles();
4519    }
4520    if (marqueeEl) {
4521      marqueeEl.style.left = Math.min(down.x, e.clientX) + "px";
4522      marqueeEl.style.top = Math.min(down.y, e.clientY) + "px";
4523      marqueeEl.style.width = Math.abs(dx) + "px";
4524      marqueeEl.style.height = Math.abs(dy) + "px";
4525      // LIVE: what the band touches is selected as it grows and shrinks, off the
4526      // rect just written so the highlight and the stroke describe the same box.
4527      applyMarqueeSelection(marqueeRectNow());
4528      e.preventDefault();
4529    }
4530  }
4531  function onUp() {
4532    // A crop drag settles; the mode stays open, which is the whole difference
4533    // between a mode and a gesture. The commit is Enter or a click away.
4534    if (cropSess && cropSess.g) { endCropGesture(); return; }
4535    // Same for the crop frame — the drag settles, the panel keeps the session.
4536    if (cframe && cframe.g) { endCropFrameGesture(); return; }
4537    if (pan) {
4538      releasePointer(pan);
4539      pan = null;
4540      document.body.classList.remove("__ppn_panning");
4541      suppressClick = true; // the trailing click must not select anything
4542      return;
4543    }
4544    if (drag) { endDrag(); return; }
4545    if (gres) { endGroupResize(); return; }
4546    if (!down) return;
4547    if (marqueeEl) {
4548      // The set is already what the band last showed (applyMarqueeSelection ran on
4549      // every move), so the release only has to make sure the FINAL rect landed —
4550      // a band released in the same frame as its last move would otherwise commit
4551      // the frame before it. Same rect reader as the live path.
4552      applyMarqueeSelection(marqueeRectNow());
4553      marqueeEl.remove(); marqueeEl = null;
4554      // Re-assert the chrome now that the band is gone: the union box's corner
4555      // handles are withheld WHILE one is drawn (see syncGroupHandles), so this is
4556      // where a multi-selection gets its grips.
4557      applyOutlines();
4558      postSelection();
4559      suppressClick = true;
4560    }
4561    releasePointer(down);
4562    down = null;
4563  }
4564
4565  function placeCaret(e) {
4566    try {
4567      var range = null;
4568      if (document.caretRangeFromPoint) range = document.caretRangeFromPoint(e.clientX, e.clientY);
4569      else if (document.caretPositionFromPoint) {
4570        var p = document.caretPositionFromPoint(e.clientX, e.clientY);
4571        if (p) { range = document.createRange(); range.setStart(p.offsetNode, p.offset); range.collapse(true); }
4572      }
4573      if (range) { var s = getSelection(); s.removeAllRanges(); s.addRange(range); }
4574    } catch (err) { /* caret placement is best-effort */ }
4575  }
4576
4577  function selectAllContent(el) {
4578    try {
4579      var range = document.createRange();
4580      range.selectNodeContents(el);
4581      var sel = getSelection();
4582      sel.removeAllRanges();
4583      sel.addRange(range);
4584    } catch (err) { /* best-effort */ }
4585  }
4586
4587  function startEditing(el, e) {
4588    clearHover();
4589    removeHandles(); // no drag/resize chrome while typing
4590    removeRings();   // the .__ppn_editing frame is the only stroke while editing
4591    // Same rule for the union box: entering text edit collapses the selection to
4592    // the one element being typed into, so a box enclosing the old group would
4593    // outlive the group it described.
4594    if (groupBox && groupBox.parentNode) groupBox.parentNode.removeChild(groupBox);
4595    groupBox = null;
4596    removeGroupHandles(); // …and its corners, which are the box's (SUNRA-1213)
4597    editingEl = el;
4598    editingOriginal = el.innerHTML;
4599    reverting = false;
4600    archTemplate = null;
4601    // Arched word → capture the per-letter template, then FLATTEN to one editable
4602    // line (the whole word on a single surface) so typing edits the word, not a
4603    // lone letter. The arc is rebuilt from the template on commit.
4604    var data = archLetterData(el);
4605    if (data) {
4606      archTemplate = buildArchTemplate(data);
4607      el.textContent = (el.textContent || "").replace(/\\u00a0/g, " ");
4608    }
4609    el.classList.add("__ppn_editing");
4610    el.setAttribute("contenteditable", "true");
4611    el.focus();
4612    // Canva convention (⑫): entering text edit SELECTS ALL so the first keystroke
4613    // replaces the box; a subsequent click inside then places the caret natively
4614    // (onClick returns early once the target IS the editing element). Arch words
4615    // already preselected; regular text now mirrors it, so the dbl-click point (e)
4616    // no longer seeds an initial caret.
4617    selectAllContent(el);
4618  }
4619
4620  function stopEditing(commit) {
4621    var el = editingEl;
4622    if (!el) return;
4623    var tpl = archTemplate;
4624    editingEl = null;
4625    archTemplate = null;
4626    el.removeAttribute("contenteditable");
4627    el.classList.remove("__ppn_editing");
4628    // Exit edit FULLY: drop focus off the text node and clear any caret/selection
4629    // range the edit left behind. Removing the attribute alone leaves a collapsed
4630    // Selection anchored in the text, which renders as a stray blinking caret on a
4631    // merely-SELECTED element. Text editing lives ONLY in dbl-click edit mode.
4632    if (el.blur) el.blur();
4633    try {
4634      var _sel = document.getSelection ? document.getSelection() : null;
4635      if (_sel) _sel.removeAllRanges();
4636    } catch (err) {
4637      /* selection clearing is best-effort */
4638    }
4639    // A FRESH PLACEMENT (ENG-11306) ends differently from an edit, because what
4640    // is ending is the INSERT. The document has never seen this element: there is
4641    // no text-commit to send (its selector resolves to nothing parent-side, and
4642    // the patch would abort), and nothing to undo if it is dropped.
4643    if (el === freshEl) {
4644      freshEl = null;
4645      // \`trim\` and not a regex: it strips U+00A0 too (a contenteditable that was
4646      // clicked into and left alone can hold one), and a regex written in this
4647      // template literal would need double-escaping to survive to the browser.
4648      var typed = (el.textContent || "").trim();
4649      if (!commit || !typed) {
4650        // Empty on blur, or Escape at any point → the box never existed. Removed
4651        // outright, leaving no history entry behind for an undo to find.
4652        if (el.parentNode) el.parentNode.removeChild(el);
4653        selectedSet = [];
4654        removeHandles();
4655        removeRings();
4656        clearHover();
4657        postSelection();
4658        postDocSize();
4659        return;
4660      }
4661      // Freeze the auto-width box at what the user is looking at, so the schema
4662      // component lands at the size on screen instead of a default one. Guarded:
4663      // a zero measurement (no layout yet) must not write a zero-size box.
4664      if (el.offsetWidth > 0) el.style.setProperty("width", el.offsetWidth + "px");
4665      if (el.offsetHeight > 0) el.style.setProperty("height", el.offsetHeight + "px");
4666      // The same message the creation bar's inserts post — one apply → commit,
4667      // one undo entry, and the frames-doc mapping (schema-insert) already knows
4668      // how to turn this markup into a text component at these coordinates.
4669      post({ type: "insert-commit", html: el.outerHTML, selector: selectorFor(el) });
4670      applyOutlines();
4671      postSelection();
4672      postDocSize();
4673      return;
4674    }
4675    if (commit) {
4676      // Arched word → re-split the freshly typed text into per-letter spans that
4677      // preserve the arc (interpolated across the new letter count) BEFORE we read
4678      // innerHTML for the commit.
4679      if (tpl) el.innerHTML = redistributeArchLetters(el.textContent || "", tpl);
4680      post({ type: "text-commit", selector: selectorFor(el), html: el.innerHTML });
4681    } else {
4682      el.innerHTML = editingOriginal;
4683    }
4684    syncHandles(); // selection persists — restore drag/resize chrome
4685  }
4686
4687  function onClick(e) {
4688    if (!active) return;
4689    // The click that FOLLOWS a placement press. It must die here, above the
4690    // editing branch: by now the fresh box is the editing element and this
4691    // click's target is whatever was under the cursor, so the branch below would
4692    // read it as "clicked outside the editor", commit an empty box, and delete
4693    // the thing the user just placed before they could type into it.
4694    if (placeClick) { placeClick = false; e.preventDefault(); e.stopPropagation(); return; }
4695    if (curTool === "hand") { e.preventDefault(); e.stopPropagation(); return; }
4696    // A press swallowed by the crop frame (onDown) still produces a click, and a
4697    // click that reached the plate must not re-pick underneath it. Presses that
4698    // FELL THROUGH the session — on the page outside the picture — are let by, so
4699    // clicking another element still moves the selection and closes the panel.
4700    if (cframe && cropFrameOwnsPoint(e.target)) { e.preventDefault(); e.stopPropagation(); return; }
4701    // While inline-editing, let clicks act natively (caret placement) inside the
4702    // edited element; a click OUTSIDE it commits (handled by blur).
4703    if (editingEl) {
4704      if (e.target === editingEl || (editingEl.contains && editingEl.contains(e.target))) return;
4705      stopEditing(true);
4706      return;
4707    }
4708    // Never let a link / button inside the composition act on an edit-layer click.
4709    e.preventDefault();
4710    e.stopPropagation();
4711    // The click that terminates a marquee drag must not re-select.
4712    if (suppressClick) { suppressClick = false; return; }
4713    // Content-fitting hit target (ask ③) — a click in the empty part of an
4714    // oversized box falls through to what is really under it (often the sheet,
4715    // which correctly deselects).
4716    var el = pickAt(e.clientX, e.clientY, e.target);
4717    if (!el || el.nodeType !== 1) return;
4718    var additive = e.shiftKey || e.metaKey || e.ctrlKey; // Shift matches the canvas
4719    // An arched word letter → select the WHOLE group so the format bar / drag act
4720    // on every letter at once.
4721    if (!additive) {
4722      var grp = archGroupOf(el);
4723      if (grp) el = grp;
4724    }
4725    // A click on empty background clears the selection — the body / html, AND the
4726    // root sheet (the outermost body child = the page background). Selecting the
4727    // ROOT sheet by click is almost never intended (it's how you end up with
4728    // whole-page handles), so a bare-background click falls through to deselect
4729    // instead. The root remains reachable by nothing here — it's excluded from
4730    // Delete already, and click-select now too. (Additive still marquees/toggles.)
4731    if (
4732      !additive &&
4733      (isRootSheet(el) || el === document.body || el === document.documentElement)
4734    ) {
4735      clearHover();
4736      clearSelection(true);
4737      return;
4738    }
4739    if (additive) {
4740      toggleInSet(el);
4741    } else {
4742      // Plain click always selects just this element. Re-clicking the sole
4743      // selection KEEPS it (Canva-style) — so a follow-up double-click can edit
4744      // it; deselect is background-click or Escape.
4745      selectedSet = [el];
4746    }
4747    clearHover();
4748    applyOutlines();
4749    postSelection();
4750  }
4751
4752  // Double-click a text-bearing leaf → inline edit it (contentEditable); an image
4753  // → crop it. ONE double-click, two destinations, and they cannot contend: an
4754  // <img> is a replaced element and bears no text, a text leaf is not an image.
4755  // Both are the same promise — a double-click opens the thing you pointed at for
4756  // direct editing — and this is also the touch story, since a double-TAP is what
4757  // the engine reports here.
4758  function onDblClick(e) {
4759    if (!active) return;
4760    e.preventDefault();
4761    e.stopPropagation();
4762    // Never open the FRAMING crop from inside the crop FRAME. Two crop tools on
4763    // one picture at once is not a state that means anything, and the double-tap
4764    // that produced it was aimed at a grip.
4765    if (cframe) return;
4766    // ── DOUBLE-CLICK A HANDLE OR AN EDGE = FIT THE BOX TO ITS TEXT ──────────
4767    // Andy 2026-09-07 (ENG-11546 追加 2): "固定尺寸文字框上,双击任一边或手柄把框收回
4768    // 到刚好包住文字(= 回到 auto,等价 inspector 的「Resize to fit」)". Figma's
4769    // gesture, and it is the SAME operation the panel's button performs — so it is
4770    // literally the same call, \`align-content: ""\`, and there is still exactly one
4771    // writer of "this box is auto again" (fitBoxToContent + the removal, one
4772    // commit, one undo step).
4773    //
4774    // Ranked ABOVE the pick below, unconditionally for a chrome target: a
4775    // double-click on a handle is never a request to edit text, so it must not
4776    // fall through even when there is nothing to fit. The bands answer here for
4777    // the same reason they answer in onDown — the edge IS the handle's side, and
4778    // a gesture the square offers has to work on the strip behind it too.
4779    var dcls = e.target && typeof e.target.className === "string" ? e.target.className : "";
4780    if (dcls.indexOf("__ppn_handle") >= 0 || dcls.indexOf("__ppn_edge") >= 0) {
4781      fitSelectionToContent();
4782      return;
4783    }
4784    // Same content-fitting target as click (ask ③): double-clicking the empty part
4785    // of an oversized box must not drop a caret into text that isn't there.
4786    var el = pickAt(e.clientX, e.clientY, e.target);
4787    if (!el || el.nodeType !== 1) return;
4788    // Busy → no inline editing. Moving it is fine (nothing about a position is
4789    // in the op's answer); typing into it is not, because the op is about to
4790    // write over the very content being typed.
4791    if (isBusy(el)) return;
4792    // AN IMAGE → CROP. Re-entering on the element already being cropped is a
4793    // no-op rather than a restart, so a stray second double-click cannot throw
4794    // away the adjustment in progress.
4795    if (el.tagName === "IMG") {
4796      if (cropSess && cropSess.el === el) return;
4797      if (cropSess) exitCrop(true);
4798      if (editingEl) stopEditing(true);
4799      enterCrop(el);
4800      return;
4801    }
4802    // An arched word → edit the WHOLE group (double-clicking a lone letter still
4803    // opens the word), never a single letter.
4804    var group = archGroupOf(el);
4805    if (group) el = group;
4806    if (editingEl && el === editingEl) return;
4807    if (editingEl) stopEditing(true);
4808    if (group || isTextBearing(el)) startEditing(el, e);
4809  }
4810
4811  function onCopy(e) {
4812    if (!active || editingEl || caretPend || cropSess || cframe || isEditableTarget(e.target) || !selectedSet.length) return;
4813    var textSelection = window.getSelection();
4814    if (textSelection && !textSelection.isCollapsed && textSelection.toString()) return;
4815    // Legacy HTML has no native component schema; keep browser copy there.
4816    if (selectedSet.some(function (el) { return !el.getAttribute("data-cid"); })) return;
4817    e.preventDefault();
4818    e.stopPropagation();
4819    post({ type: "copy", selectors: selectedSet.map(selectorFor) });
4820  }
4821  document.addEventListener("copy", onCopy, true);
4822
4823  function onKey(e) {
4824    // ── CROP MODE OWNS THE KEYBOARD ────────────────────────────────────────
4825    // Enter commits, Escape cancels and restores — the pair every modal gesture
4826    // in this editor already spells that way (inline text edit is the other).
4827    // Delete is SWALLOWED rather than passed through: while a mode is open the
4828    // key that means "remove this element" is the most destructive possible
4829    // reading of an ambiguous press, and the element is the very thing being
4830    // adjusted. Ranked above undo/redo for the same reason the pointer branch is
4831    // ranked above the handles — a mode is not a mode if a keystroke can leave it
4832    // half-open.
4833    if (cropSess) {
4834      if (e.key === "Enter" || e.key === "Escape") {
4835        e.preventDefault();
4836        e.stopPropagation();
4837        exitCrop(e.key === "Enter");
4838        return;
4839      }
4840      if (e.key === "Delete" || e.key === "Backspace") {
4841        e.preventDefault();
4842        e.stopPropagation();
4843        return;
4844      }
4845    }
4846    // THE CROP FRAME owns the keyboard on the same terms, with one difference:
4847    // there is no Enter. Its commit is Apply, which SPENDS — an edit that mints a
4848    // new picture is not something a stray Return should trigger. Escape drops
4849    // the session and tells the parent to leave the tool, which closes the panel.
4850    if (cframe) {
4851      if (e.key === "Escape") {
4852        e.preventDefault();
4853        e.stopPropagation();
4854        closeCropFrame();
4855        post({ type: "exit" });
4856        return;
4857      }
4858      if (e.key === "Delete" || e.key === "Backspace") {
4859        e.preventDefault();
4860        e.stopPropagation();
4861        return;
4862      }
4863    }
4864    // Undo / redo — forwarded to the parent so the ONE history pipeline owns it,
4865    // BUT only when we're not inline-editing. While a contentEditable field is
4866    // focused, ⌘Z/⇧⌘Z/⌘Y must run the browser's NATIVE per-character undo (the
4867    // whole edit becomes one atomic history step on blur), so we let it through.
4868    var mod = e.metaKey || e.ctrlKey;
4869    if (mod && e.key.toLowerCase() === "c" && !e.altKey && !e.shiftKey) {
4870      onCopy(e);
4871      return;
4872    }
4873    if (mod && (e.key === "z" || e.key === "Z" || e.key === "y" || e.key === "Y")) {
4874      if (editingEl || caretPend) return; // native contentEditable undo
4875      var redo = e.key === "y" || e.key === "Y" || e.shiftKey;
4876      e.preventDefault();
4877      e.stopPropagation();
4878      post({ type: redo ? "redo" : "undo" });
4879      return;
4880    }
4881    // Delete / Backspace with a selection → remove the selected element(s). NOT
4882    // while inline-editing text (contentEditable is focused → the keys must edit
4883    // characters natively) and only when something is actually selected (else a
4884    // stray keypress is a no-op, never a page-level delete).
4885    if ((e.key === "Delete" || e.key === "Backspace") && !editingEl && selectedSet.length) {
4886      e.preventDefault();
4887      e.stopPropagation();
4888      removeSelection();
4889      return;
4890    }
4891    // ── ARROW KEYS MOVE THE SELECTION (ENG-11620 追加 2) ────────────────────
4892    // 1px, or 10px with Shift. See nudgeBy for the whole gesture; this is only
4893    // the gate, and every clause in it is a place the arrows already mean
4894    // something else:
4895    //
4896    //   \xb7 an EDITABLE target (the runtime's own inline editor, or a focused
4897    //     input/textarea inside the user's composition) — they move the caret;
4898    //   \xb7 a pending CARET (ENG-11545) — the user is about to type;
4899    //   \xb7 CROP MODE / the crop FRAME — those own the keyboard outright, which is
4900    //     the same reason they swallow Delete above;
4901    //   \xb7 ⌘/⌃/⌥ held — those belong to the browser and the OS (⌥→ is a word jump,
4902    //     ⌘→ a line end), and a nudge that fired under them would be stealing a
4903    //     shortcut. ⇧ is the ONE modifier this gesture claims, as the big step.
4904    //
4905    // preventDefault so the frame does not also SCROLL: the board document is
4906    // taller than its viewport, and an arrow key is a scroll key by default —
4907    // which would move the whole page under the element the user just moved 1px.
4908    if (
4909      !editingEl && !caretPend && !cropSess && !cframe &&
4910      !mod && !e.altKey && selectedSet.length &&
4911      NUDGE_DIR[e.key] && !isEditableTarget(e.target)
4912    ) {
4913      var dir = NUDGE_DIR[e.key];
4914      var step = e.shiftKey ? NUDGE_STEP_SHIFT : NUDGE_STEP;
4915      // Claim the key even if there turns out to be nothing movable under the
4916      // selection (a busy element, the root sheet): the page must not scroll
4917      // either way, and a half-answered key is worse than a no-op.
4918      e.preventDefault();
4919      e.stopPropagation();
4920      nudgeBy(dir[0] * step, dir[1] * step);
4921      return;
4922    }
4923    // ── SHAPE SHORTCUTS — R / L / ⇧L / O (ENG-11305) ────────────────────────
4924    // The in-frame half of the pair; ComposePane runs the identical mapping on
4925    // the window for when focus is OUTSIDE the iframe. Both land on the same
4926    // insertPrimitive, so a shortcut and the bar's menu row are one code path.
4927    //
4928    // RANKED BELOW every mode and every editing state above it, and gated on
4929    // them again here: a plain letter means "type this" whenever a text field
4930    // owns the keyboard, and a crop session owns it outright (same argument as
4931    // the swallowed Delete). Escape below is unaffected — it is not a letter.
4932    // …and not while a caret is pending: R / O / L are letters the user is about
4933    // to TYPE. A tool shortcut that fires into a live text cursor is the same
4934    // defect as one firing into a focused field, which is why this gate already
4935    // reads editingEl.
4936    if (!editingEl && !caretPend && !cropSess && !cframe) {
4937      var shapeKind = shapeShortcutFor(e);
4938      if (shapeKind) {
4939        e.preventDefault();
4940        e.stopPropagation();
4941        // THE IN-FRAME HALF OF THE R / O / L SPLIT (merge, 2026-09-05). The
4942        // parent's copy of this branch carries the full argument; the short
4943        // version is that ENG-11306's placement tools own the three letters that
4944        // have one, and ENG-11305's centred insert keeps only ⇧L (the arrow),
4945        // which has no tool. Running both put down two shapes per press.
4946        //
4947        // Arming here does the same two things the parent's set-tool does when
4948        // the press lands outside: flip the local tool (crosshair, gestures
4949        // stood down) and TELL THE PARENT, so the bar's Shape glyph lights blue
4950        // for a key pressed with focus inside the iframe.
4951        if (PLACE_TOOLS[shapeKind]) {
4952          setTool(shapeKind);
4953          post({ type: "tool-change", tool: shapeKind });
4954          return;
4955        }
4956        insertPrimitive(shapeKind, "");
4957        return;
4958      }
4959    }
4960    if (e.key !== "Escape") return;
4961    // Escape drops a pending caret and leaves nothing (ENG-11545) — ranked above
4962    // the selection/tool branches because the caret is the innermost open state,
4963    // and one Escape should close exactly one thing.
4964    if (caretPend) {
4965      e.preventDefault();
4966      e.stopPropagation();
4967      dropTextCaret();
4968      return;
4969    }
4970    if (editingEl) {
4971      // Escape reverts the in-progress edit (blur that follows must NOT commit).
4972      reverting = true;
4973      stopEditing(false);
4974      return;
4975    }
4976    // Escape clears the selection (and the planted chip)...
4977    if (selectedSet.length) { clearSelection(true); }
4978    // ...and puts the tool down. The parent owns the tool state, and its own
4979    // window keydown never fires while focus is INSIDE this iframe — which is
4980    // exactly where focus sits after a hand-drag — so tell it. Idempotent: the
4981    // parent flipping to Select while already on Select changes nothing.
4982    if (curTool !== "select") post({ type: "exit" });
4983  }
4984
4985  // Blur of the edited element commits (unless a revert is in flight).
4986  function onBlur(e) {
4987    if (editingEl && e.target === editingEl) {
4988      if (reverting) { reverting = false; return; }
4989      stopEditing(true);
4990    }
4991  }
4992
4993  // ── contextual style application (format bar / drag / resize) ───────────
4994  // Apply a set of CSS props to every selected element as INLINE styles (live,
4995  // no reload), then echo a style-commit so the parent persists the same patch
4996  // into the stored html. Free-form: whatever prop/value the bar sends is set,
4997  // no clamping.
4998  // ── alignment = a WITHIN-THE-BOX gesture ────────────────────────────────
4999  // Bug: "align left throws the glyphs out to the left of the selection rect".
5000  // An unpainted text leaf's chrome is its INK (ask ③), but text-align aligns
5001  // inside the AUTHORED box — which for agent-authored type is routinely much
5002  // wider than the words. So left-aligning slammed the glyphs to the far edge of
5003  // an invisible box while the ring stayed on the old ink: the text visibly
5004  // escaped its own outline.
5005  //
5006  // Alignment must move the text INSIDE a box that stays put. So before applying
5007  // it, freeze the box's WIDTH to exactly what the user is looking at. After the
5008  // pin, box == the selection rect across the inline axis, so:
5009  //   • the box (and its ring) does not move,
5010  //   • every alignment keeps the glyphs inside it — a line can never be wider
5011  //     than the widest line the box was wrapped to,
5012  //   • multi-line type aligns visibly (short lines move within the block),
5013  //   • single-line type stores the alignment and shows it the moment the box is
5014  //     widened with a handle — Figma's auto-width vs fixed-width behaviour.
5015  // The pin rides the SAME style-commit as the alignment, so the schema gets the
5016  // box and the alignment in one patch (one undo step, no divergence).
5017  //
5018  // WIDTH, AND ONLY WHEN THERE IS SLACK — see alignPinBox for the defect that
5019  // taught us both halves (Andy, 2026-08-08: aligning "moved the text box more
5020  // and more upward" instead of aligning). This used to freeze with the RESIZE
5021  // pin, which re-derives the top and the height from the ink too; the ink's top
5022  // is the glyph fragment's, not the line box's, so every click re-seated the
5023  // element half a leading higher and the type crawled up the page. A resize
5024  // survives that because it re-anchors the edge it held; an alignment has no
5025  // held edge, and no business touching the block axis at all.
5026  function freezeBoxForAlign() {
5027    for (var i = 0; i < selectedSet.length; i++) {
5028      var pin = alignPinRect(selectedSet[i]);
5029      if (pin) pinAbsolute(selectedSet[i], pin);
5030    }
5031  }
5032  // ── BACK TO AUTO = FIT THE BOX TO THE CONTENT (ENG-11546) ────────────────
5033  // The inspector's resize-to-fit sends \`align-content: ""\`, which by the marker
5034  // contract means "this text box is auto again". Dropping the declaration alone
5035  // would only change what the CHROME frames — the element would keep whatever
5036  // width and height the fixed box was dragged to, so the user would press "fit"
5037  // and watch the box stay exactly the size it was.
5038  //
5039  // So the SHRINK-WRAP retired from endDrag lives here instead, where it is the
5040  // gesture rather than a side effect of every resize: re-pin the element onto its
5041  // own ink (inline axis) over its own line-box layout (block axis), which is the
5042  // box that fits its words without moving a rendered glyph. It rides the SAME
5043  // style commit as the removal, so the document gets one patch and one undo step.
5044  function fitBoxToContent() {
5045    for (var i = 0; i < selectedSet.length; i++) {
5046      var el = selectedSet[i];
5047      if (!isTextBearing(el)) continue;
5048      // Measured with the marker already gone, so inkPinRect sees an AUTO leaf.
5049      el.style.removeProperty("align-content");
5050      var pin = inkPinRect(el);
5051      if (pin) pinAbsolute(el, pin);
5052    }
5053  }
5054  // The handle/edge double-click (ENG-11546 追加 2), routed through the inspector's
5055  // own message so the two gestures cannot diverge: one function decides what
5056  // "back to auto" means and one commit carries it.
5057  //
5058  // AN AUTO BOX IS LEFT ALONE — no style write, no commit, no undo entry. It
5059  // already fits its text, so the only thing a fit could add there is an explicit
5060  // pinned box the user never asked for (and the ink pin's outward rounding with
5061  // it). Nothing to fit is not a fit that happens to change nothing.
5062  function fitSelectionToContent() {
5063    for (var i = 0; i < selectedSet.length; i++) {
5064      if (isFixedTextBox(selectedSet[i])) {
5065        applyStyleToSelection({ "align-content": "" });
5066        return;
5067      }
5068    }
5069  }
5070  // ── NO FONT FLASH (Andy 2026-09-07: "switching to a different font would
5071  // always have the font change to something else first then flash to the new
5072  // font"). The parent posts load-font (a <link>) and apply-style back to
5073  // back; setting font-family before the file has arrived paints the fallback
5074  // for a few hundred ms, then the webfont. So a font-family apply WAITS: for
5075  // the stylesheet links still loading, then for document.fonts.load() of that
5076  // family — and only then writes the property, on the elements that were
5077  // selected when the user chose it. Capped at 2.5 s so a font that never loads
5078  // still applies (the fallback is then honest). jsdom has no document.fonts,
5079  // so tests and hosts without the API take the synchronous path unchanged.
5080  var pendingFontLinks = [];
5081  // Bumped by every style-preview; a deferred font-family preview only writes if
5082  // it is still the newest one. See previewStyleOnSelection.
5083  var previewSeq = 0;
5084  function fontFamilyName(value) {
5085    var m = /^"?([^",]+)"?/.exec(String(value || "").trim());
5086    return m ? m[1].trim() : "";
5087  }
5088  function fontReady(family) {
5089    var fonts = document.fonts;
5090    if (!fonts || typeof fonts.load !== "function" || !family) return null;
5091    try {
5092      if (typeof fonts.check === "function" && fonts.check('16px "' + family + '"') && !pendingFontLinks.length) return null;
5093    } catch (e) {}
5094    var links = pendingFontLinks.slice();
5095    var loaded = Promise.all(links).then(function () {
5096      return fonts.load('16px "' + family + '"');
5097    });
5098    var capped = new Promise(function (resolve) { setTimeout(resolve, 2500); });
5099    return Promise.race([loaded, capped]).catch(function () {});
5100  }
5101  function applyStyleToSelection(styles) {
5102    if (!selectedSet.length) return;
5103    var family = styles && styles["font-family"] ? fontFamilyName(styles["font-family"]) : "";
5104    var wait = family ? fontReady(family) : null;
5105    if (wait) {
5106      var targets = selectedSet.slice();
5107      wait.then(function () { applyStyleToTargets(styles, targets); });
5108      return;
5109    }
5110    applyStyleToTargets(styles, selectedSet);
5111  }
5112  function applyStyleToTargets(styles, targets) {
5113    if (!targets.length) return;
5114    if (styles && styles["align-content"] === "") fitBoxToContent();
5115    if (styles && styles["text-align"]) freezeBoxForAlign();
5116    var props = Object.keys(styles || {});
5117    var archReflow = []; // arch groups whose LETTERS changed → persist innerHTML
5118    for (var i = 0; i < targets.length; i++) {
5119      var el = targets[i];
5120      for (var p = 0; p < props.length; p++) {
5121        var v = styles[props[p]];
5122        if (v === "" || v == null) el.style.removeProperty(props[p]);
5123        else el.style.setProperty(props[p], v);
5124      }
5125      // An arched word: the wrapper's own change cascades to letters by
5126      // inheritance, but a letter that OVERRIDES a prop inline must be updated too
5127      // (else it lags the rest of the word). When that happens, persist the new
5128      // per-letter innerHTML alongside the wrapper's style.
5129      if (archLetterData(el) && cascadeArchStyle(el, styles)) archReflow.push(el);
5130    }
5131    postStyleCommit(targets);
5132    for (var a = 0; a < archReflow.length; a++) {
5133      post({ type: "text-commit", selector: selectorFor(archReflow[a]), html: archReflow[a].innerHTML });
5134    }
5135    postSelection(); // refresh the bar's reflected values
5136    // RE-MEASURE THE CHROME. Every style here can move the rendered ink — a font,
5137    // a size, a weight, and above all an alignment. Without this the ring and the
5138    // 8 handles kept framing the PRE-edit measurement, which is what let the glyphs
5139    // sit outside their own selection rect (the align bug's visible half). The
5140    // outline follows the type on every commit, so it always contains the glyphs.
5141    syncHandles();
5142  }
5143  function postStyleCommit(els) {
5144    var patches = [];
5145    for (var i = 0; i < els.length; i++) {
5146      patches.push({ selector: selectorFor(els[i]), style: els[i].getAttribute("style") || "" });
5147    }
5148    post({ type: "style-commit", patches: patches });
5149  }
5150  // LIVE PREVIEW: set inline props on the selection with NO commit / NO history /
5151  // NO postSelection. This is the cheap per-pointermove path for continuous
5152  // controls (color drag) — just a direct style mutation, so a burst of moves
5153  // costs nothing beyond the paint. Arch letters that OVERRIDE a prop are cascaded
5154  // live too (so an arched word previews correctly), but nothing is persisted; the
5155  // settled apply-style that follows handles the real commit + arch text-commit.
5156  function previewStyleOnSelection(styles) {
5157    if (!selectedSet.length) return;
5158    // SAME NO-FLASH RULE AS THE COMMIT (ENG-11571 then ENG-11575). The font list
5159    // previews the family you are HOVERING, so this path now carries font-family
5160    // too, and writing it before the file has arrived paints the fallback for a
5161    // few hundred ms exactly as the commit used to. So a preview waits for the
5162    // face as well.
5163    //
5164    // THE SEQUENCE NUMBER IS WHAT THE COMMIT PATH DOES NOT NEED. Previews arrive
5165    // in a stream (hover A, hover B, leave) and the restore at the end is
5166    // SYNCHRONOUS as soon as the hovered font's stylesheet has settled: the
5167    // original face is loaded, so the check in fontReady passes and it returns
5168    // null. Without a guard, A's deferred write would land after that restore and
5169    // leave the wrong face on screen. Only the newest preview may write.
5170    //
5171    // KNOWN, AND BOUNDED: leaving the list while the hovered stylesheet is STILL
5172    // in flight makes the restore wait for it too, because fontReady deliberately
5173    // distrusts fonts.check while any link is pending (check answers TRUE for a
5174    // family with no font-face rule yet, reporting that the SYSTEM fallback is
5175    // loaded, which is the trap ENG-11571 hit). That wait is the remainder of a
5176    // load which had already run for the 100ms hover debounce, and the right face
5177    // still lands; the alternative is trusting check and reintroducing the very
5178    // flash this path exists to prevent.
5179    previewSeq++;
5180    var seq = previewSeq;
5181    var family = styles && styles["font-family"] ? fontFamilyName(styles["font-family"]) : "";
5182    var wait = family ? fontReady(family) : null;
5183    if (wait) {
5184      var targets = selectedSet.slice();
5185      wait.then(function () {
5186        if (seq === previewSeq) previewStyleOnTargets(styles, targets);
5187      });
5188      return;
5189    }
5190    previewStyleOnTargets(styles, selectedSet);
5191  }
5192  function previewStyleOnTargets(styles, targets) {
5193    var props = Object.keys(styles || {});
5194    for (var i = 0; i < targets.length; i++) {
5195      var el = targets[i];
5196      for (var p = 0; p < props.length; p++) {
5197        var v = styles[props[p]];
5198        if (v === "" || v == null) el.style.removeProperty(props[p]);
5199        else el.style.setProperty(props[p], v);
5200      }
5201      if (archLetterData(el)) cascadeArchStyle(el, styles);
5202    }
5203  }
5204
5205  // Append a webfont <link> to the LIVE document head (idempotent per href) so a
5206  // font choice renders immediately; the parent persists the same link.
5207  function loadFont(href) {
5208    if (!href) return;
5209    var links = document.head.getElementsByTagName("link");
5210    for (var i = 0; i < links.length; i++) if (links[i].href === href) return;
5211    var link = document.createElement("link");
5212    link.rel = "stylesheet";
5213    link.href = href;
5214    // Tracked until it settles, so a font-family apply that follows this link
5215    // can wait for the stylesheet before asking document.fonts for the face.
5216    var settled = new Promise(function (resolve) {
5217      link.onload = resolve;
5218      link.onerror = resolve;
5219      setTimeout(resolve, 2500);
5220    }).then(function () {
5221      var at = pendingFontLinks.indexOf(settled);
5222      if (at >= 0) pendingFontLinks.splice(at, 1);
5223    });
5224    pendingFontLinks.push(settled);
5225    document.head.appendChild(link);
5226  }
5227
5228  // ── ON-LOOK inserts (⑦ creation bar) ───────────────────────────────────
5229  // A user-created element must arrive looking like it belongs — same font
5230  // family / scale / palette the page already uses. The runtime is the only side
5231  // that can read the live computed styles (the parent is cross-origin), so the
5232  // sampling happens here: we clone the relevant tokens off existing peers.
5233  function freshCid() {
5234    return "u" + Date.now().toString(36) + Math.floor(Math.random() * 1e4).toString(36);
5235  }
5236  function visibleTextEls() {
5237    var out = [];
5238    var all = document.body.getElementsByTagName("*");
5239    for (var i = 0; i < all.length; i++) {
5240      var el = all[i];
5241      if (!isTextBearing(el)) continue;
5242      // Skip only genuinely hidden peers (display:none) — NOT zero-rect ones,
5243      // which can happen before first paint; computed font-size is valid either
5244      // way, so sampling still works and inserts arrive on-scale.
5245      if (getComputedStyle(el).display === "none") continue;
5246      out.push(el);
5247    }
5248    out.sort(function (a, b) {
5249      return parseFloat(getComputedStyle(b).fontSize) - parseFloat(getComputedStyle(a).fontSize);
5250    });
5251    return out;
5252  }
5253  // role: "heading" | "subheading" | "body" | "caption" → a peer to copy from.
5254  function sampleTextPeer(role) {
5255    var els = visibleTextEls();
5256    if (!els.length) return null;
5257    if (role === "heading") return els[0];
5258    if (role === "subheading") return els[1] || els[0];
5259    if (role === "caption") return els[els.length - 1];
5260    // body → a mid paragraph (prefer an actual <p>)
5261    for (var i = 0; i < els.length; i++) if (els[i].tagName === "P") return els[i];
5262    return els[Math.floor(els.length / 2)] || els[0];
5263  }
5264  function copyTextTokens(fromEl, toEl) {
5265    if (!fromEl) return;
5266    var cs = getComputedStyle(fromEl);
5267    toEl.style.setProperty("font-family", cs.fontFamily);
5268    toEl.style.setProperty("font-size", cs.fontSize);
5269    toEl.style.setProperty("font-weight", cs.fontWeight);
5270    toEl.style.setProperty("line-height", cs.lineHeight);
5271    toEl.style.setProperty("letter-spacing", cs.letterSpacing);
5272    toEl.style.setProperty("color", cs.color);
5273    toEl.style.setProperty("text-transform", cs.textTransform);
5274  }
5275  // A fill color the page already uses (a colored block / accent), else the text
5276  // color — so a shape lands in-palette rather than a foreign default.
5277  function samplePageFill() {
5278    var root = document.body.firstElementChild;
5279    var all = document.body.getElementsByTagName("*");
5280    for (var i = 0; i < all.length; i++) {
5281      var el = all[i];
5282      if (el === root || isChrome(el)) continue;
5283      var cs = getComputedStyle(el);
5284      if (hasFill(el, cs)) return cs.backgroundColor;
5285    }
5286    return root ? getComputedStyle(root).color : "rgb(51, 51, 51)";
5287  }
5288  function samplePageRadius() {
5289    var all = document.body.getElementsByTagName("*");
5290    for (var i = 0; i < all.length; i++) {
5291      if (isChrome(all[i])) continue;
5292      var br = parseFloat(getComputedStyle(all[i]).borderTopLeftRadius);
5293      if (br > 0) return br + "px";
5294    }
5295    return "8px";
5296  }
5297  // ── contrast-aware insert defaults ──────────────────────────────────────
5298  // A fresh shape/text must be VISIBLE against the page, not blend into it (a
5299  // black shape on dark poster art = invisible). Sample the page background
5300  // luminance and pick a light OR dark "ink" accordingly.
5301  // BUG, found while wiring the caret (ENG-11545): this regex shipped with its
5302  // parentheses UNESCAPED — \`/rgba?(([^)]+))/\` — so it read them as two capture
5303  // groups instead of the literal brackets of \`rgb(…)\`. It still MATCHED, which
5304  // is why nothing looked broken: it just captured "(10, 10, 12" and handed
5305  // \`parseFloat("(10")\` → NaN to every caller. \`NaN < 0.5\` is false, so
5306  // pageIsDark() answered "light" for every page in existence and insertInk()
5307  // has been returning the near-black ink onto dark posters — the exact
5308  // invisible-insert defect its own comment says it exists to prevent. (The
5309  // escapes are doubled because this is a template literal: \`\\(\` is what emits
5310  // \`\\(\` into the runtime string.)
5311  function __parseRgb(str) {
5312    var m = /rgba?\\(([^)]+)\\)/i.exec(str || "");
5313    if (!m) return null;
5314    var p = m[1].split(",");
5315    if (p.length < 3) return null;
5316    return { r: parseFloat(p[0]), g: parseFloat(p[1]), b: parseFloat(p[2]), a: p.length > 3 ? parseFloat(p[3]) : 1 };
5317  }
5318  function __relLum(c) {
5319    function f(v) { v /= 255; return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); }
5320    return 0.2126 * f(c.r) + 0.7152 * f(c.g) + 0.0722 * f(c.b);
5321  }
5322  // The page's effective background: the first opaque background walking from the
5323  // root sheet outward (posters paint their art/bg on the sheet), else the body.
5324  function pageBgRgb() {
5325    var el = document.body.firstElementChild || document.body;
5326    while (el && el !== document.documentElement) {
5327      var c = __parseRgb(getComputedStyle(el).backgroundColor);
5328      if (c && c.a > 0.01) return c;
5329      el = el.parentElement;
5330    }
5331    var b = __parseRgb(getComputedStyle(document.body).backgroundColor);
5332    return b && b.a > 0.01 ? b : { r: 255, g: 255, b: 255, a: 1 };
5333  }
5334  function pageIsDark() { return __relLum(pageBgRgb()) < 0.5; }
5335  // A neutral ink that reads on the page — near-white on dark, near-black on light.
5336  function insertInk() { return pageIsDark() ? "#f5f5f5" : "#141414"; }
5337  // The text tool's type defaults. MIRRORED from PLACED_TEXT_STYLE in
5338  // compose-place-tool.ts, which is where they are stated for the parent and the
5339  // tests; this runtime is an injected STRING and cannot import it, so
5340  // compose-place-tool.test.ts asserts the emitted script carries these exact
5341  // values rather than trusting two lists to stay in step.
5342  var PLACED_TEXT = { family: "Inter, system-ui, sans-serif", size: "26px", weight: "400", lineHeight: "1.5", minWidth: "48px" };
5343  // The page's current max z-index (excluding our overlay chrome) so an insert can
5344  // stack ABOVE existing content rather than behind full-bleed art.
5345  function maxPageZ() {
5346    var all = document.body.getElementsByTagName("*");
5347    var max = 0;
5348    for (var i = 0; i < all.length; i++) {
5349      if (isChrome(all[i])) continue;
5350      var z = parseInt(getComputedStyle(all[i]).zIndex, 10);
5351      if (!isNaN(z) && z > max) max = z;
5352    }
5353    return max;
5354  }
5355
5356  function buildInsert(kind, text) {
5357    var el;
5358    if (kind === "text") {
5359      // THE TEXT TOOL'S BOX (ENG-11306) — deliberately NOT sampled off a peer the
5360      // way the four presets below are. "Insert a Heading" wants to look like the
5361      // page's headings; "put a text box here" wants ONE neutral, predictable
5362      // default you then restyle, which is what Figma's T lands and what made the
5363      // Heading/Subheading/Body/Caption menu redundant. Auto width (no width set)
5364      // with a min so an empty box is still a target, and the page's own ink so it
5365      // is legible on a dark poster and a white page alike.
5366      el = document.createElement("p");
5367      el.textContent = text || "";
5368      el.style.setProperty("font-family", PLACED_TEXT.family);
5369      el.style.setProperty("font-size", PLACED_TEXT.size);
5370      el.style.setProperty("font-weight", PLACED_TEXT.weight);
5371      el.style.setProperty("line-height", PLACED_TEXT.lineHeight);
5372      el.style.setProperty("min-width", PLACED_TEXT.minWidth);
5373      el.style.setProperty("white-space", "pre-wrap");
5374      el.style.setProperty("color", insertInk());
5375      el.style.setProperty("margin", "0");
5376      return el;
5377    }
5378    if (kind === "heading" || kind === "subheading" || kind === "body" || kind === "caption") {
5379      var tag = kind === "heading" ? "h2" : kind === "subheading" ? "h3" : "p";
5380      el = document.createElement(tag);
5381      el.textContent = text || (kind === "heading" ? "Heading" : kind === "subheading" ? "Subheading" : kind === "caption" ? "Caption" : "Body text");
5382      copyTextTokens(sampleTextPeer(kind), el);
5383      // Force a CONTRAST color over the sampled peer's — a fresh text block must be
5384      // legible on the page, never inherit a dim/near-invisible caption color.
5385      el.style.setProperty("color", insertInk());
5386      el.style.setProperty("margin", "16px 0 0");
5387    } else if (kind === "rectangle" || kind === "ellipse" || kind === "line") {
5388      el = document.createElement("div");
5389      // Contrast ink (not a sampled page fill, which on a dark poster is dark → the
5390      // invisible-shape bug); the user recolors freely via the format bar after.
5391      var fill = insertInk();
5392      el.style.setProperty("background", fill);
5393      el.style.setProperty("margin", "16px 0 0");
5394      if (kind === "rectangle") {
5395        el.style.setProperty("width", "240px");
5396        el.style.setProperty("height", "140px");
5397        el.style.setProperty("border-radius", samplePageRadius());
5398      } else if (kind === "ellipse") {
5399        el.style.setProperty("width", "180px");
5400        el.style.setProperty("height", "180px");
5401        el.style.setProperty("border-radius", "50%");
5402      } else {
5403        el.style.setProperty("width", "200px");
5404        el.style.setProperty("height", "3px");
5405      }
5406    } else if (kind === "polygon" || kind === "star" || kind === "arrow") {
5407      // ── THE SVG SHAPES (ENG-11305) ────────────────────────────────────
5408      // The markup is NOT built here: it is INSERT_SHAPE_MARKUP, generated by
5409      // schema-shape-path.ts at its default parameters and interpolated into
5410      // this runtime. So the element the user sees the instant they insert is
5411      // byte-identical to the one renderFrame will emit for the component the
5412      // insert creates — one vertex formula, never two.
5413      var holder = document.createElement("div");
5414      holder.innerHTML = PPN_SHAPE_MARKUP[kind];
5415      el = holder.firstElementChild;
5416      if (!el) return null;
5417      var ink = insertInk();
5418      el.style.setProperty("margin", "16px 0 0");
5419      if (kind === "arrow") {
5420        // Stroke-only — the head is a stroked chevron, so there is one colour.
5421        el.style.setProperty("fill", "none");
5422        el.style.setProperty("stroke", ink);
5423        el.style.setProperty("stroke-width", "3px");
5424        el.style.setProperty("stroke-linecap", "round");
5425        el.style.setProperty("stroke-linejoin", "round");
5426        el.style.setProperty("width", "220px");
5427        el.style.setProperty("height", "80px");
5428      } else {
5429        el.style.setProperty("fill", ink);
5430        el.style.setProperty("width", "180px");
5431        el.style.setProperty("height", "180px");
5432      }
5433      el.style.setProperty("overflow", "visible");
5434    } else if (kind === "image") {
5435      el = document.createElement("img");
5436      el.setAttribute("src", text || "");
5437      el.setAttribute("alt", "");
5438      el.style.setProperty("display", "block");
5439      el.style.setProperty("max-width", "100%");
5440      el.style.setProperty("margin", "16px 0 0");
5441    }
5442    return el || null;
5443  }
5444
5445  // Build an on-look primitive, append it into the root sheet (so it also inherits
5446  // the cascade), select it, and post an insert-commit so the parent appends the
5447  // SAME element to the stored html + persists.
5448  function insertPrimitive(kind, text) {
5449    if (editingEl) stopEditing(true);
5450    var root = document.body.firstElementChild || document.body;
5451    var el = buildInsert(kind, text);
5452    if (!el) return;
5453    el.setAttribute("data-cid", freshCid());
5454    root.appendChild(el);
5455    // ── VISIBLE placement (never a top-left, bottom-layer pile-up behind art) ──
5456    // Lift out of flow, stack ABOVE the page's max z, and center the element in the
5457    // current viewport. Measured at (0,0) absolute so a text block reports its true
5458    // shrink-wrapped size (in flow it would span the full sheet width).
5459    el.style.setProperty("position", "absolute");
5460    el.style.setProperty("margin", "0");
5461    el.style.setProperty("z-index", String(maxPageZ() + 1));
5462    el.style.setProperty("left", "0px");
5463    el.style.setProperty("top", "0px");
5464    var op = el.offsetParent || document.body;
5465    var pr = op.getBoundingClientRect();
5466    var bl = op.clientLeft || 0, bt = op.clientTop || 0;
5467    var r = el.getBoundingClientRect();
5468    var vw = document.documentElement.clientWidth || window.innerWidth || 0;
5469    var vh = document.documentElement.clientHeight || window.innerHeight || 0;
5470    var cx = (window.scrollX || 0) + vw / 2;
5471    var cy = (window.scrollY || 0) + vh / 2;
5472    el.style.setProperty("left", (cx - r.width / 2 - pr.left - bl) + "px");
5473    el.style.setProperty("top", (cy - r.height / 2 - pr.top - bt) + "px");
5474    // Auto-select so the resize handles show immediately (unmissable) and the
5475    // format bar / chip drive off the fresh element.
5476    selectedSet = [el];
5477    clearHover();
5478    applyOutlines();
5479    post({ type: "insert-commit", html: el.outerHTML, selector: selectorFor(el) });
5480    postSelection();
5481    postDocSize();
5482  }
5483
5484  // ── PLACEMENT (ENG-11306) — the creation-bar insert, aimed ──────────────
5485  // Same build → append → commit path as insertPrimitive above; the ONE thing it
5486  // changes is where the element lands (the click point, in frame px) and, for
5487  // text, WHEN the commit happens.
5488  //
5489  // A TEXT BOX IS COMMITTED ON THE WAY OUT, NOT ON THE WAY IN. The box arrives
5490  // empty and goes straight into inline editing, so committing at placement
5491  // would put an empty paragraph into the document and then need a SECOND commit
5492  // to delete it when the user clicks away without typing — two undo entries for
5493  // one gesture that produced nothing. Holding the commit until the edit ends
5494  // means the document only ever hears about text that has content, in exactly
5495  // one entry, through the same apply → commit path the inspector's writers use.
5496  //
5497  // THE TOOL IS SPENT BY ONE USE. Figma's T places one box and hands you back
5498  // the pointer; leaving it armed would make the click that dismisses the editor
5499  // place a second box. The parent owns tool state, so it is told.
5500  // Lift an element out of flow and put its top-left ON a viewport point. Shared
5501  // by the shape placement below and the caret's text build (ENG-11545) so the
5502  // two land identically — the caret's promise is "the text appears HERE", and a
5503  // second copy of this arithmetic is exactly how that promise gets broken.
5504  function aimAt(el, clientX, clientY) {
5505    el.style.setProperty("position", "absolute");
5506    el.style.setProperty("margin", "0");
5507    el.style.setProperty("z-index", String(maxPageZ() + 1));
5508    // The click is in VIEWPORT px and \`left\`/\`top\` are relative to the offset
5509    // parent's padding box, whose viewport origin is (rect + border) — so the
5510    // difference is the element's own box, with no scroll term to get wrong.
5511    var op = el.offsetParent || document.body;
5512    var pr = op.getBoundingClientRect();
5513    var bl = op.clientLeft || 0, bt = op.clientTop || 0;
5514    el.style.setProperty("left", (clientX - pr.left - bl) + "px");
5515    el.style.setProperty("top", (clientY - pr.top - bt) + "px");
5516  }
5517
5518  function placePrimitive(kind, clientX, clientY) {
5519    if (editingEl) stopEditing(true);
5520    var root = document.body.firstElementChild || document.body;
5521    var el = buildInsert(kind, "");
5522    if (!el) return;
5523    el.setAttribute("data-cid", freshCid());
5524    root.appendChild(el);
5525    aimAt(el, clientX, clientY);
5526    clearHover();
5527    selectedSet = [el];
5528    applyOutlines();
5529    post({ type: "insert-commit", html: el.outerHTML, selector: selectorFor(el) });
5530    postSelection();
5531    postDocSize();
5532    setTool("select");
5533    post({ type: "tool-change", tool: "select" });
5534  }
5535
5536  // ── THE PENDING CARET (ENG-11545) ───────────────────────────────────────
5537  // Andy: "点了工具栏的 T 再点到页面上… 应该像 Figma 一样,落点处出现一根闪烁的
5538  // 光标,什么框都没有;用户一开始打字,才变成现在这种带选框的文字元素."
5539  //
5540  // This REPLACES text's branch of placePrimitive above, and the difference is
5541  // not cosmetic. The old gesture put a real, empty, SELECTED <p> on the page:
5542  // a blue box announcing an element that the document had never heard of and
5543  // that would be deleted again the moment the user clicked away. What it looked
5544  // like (a committed thing) and what it was (an uncommitted maybe) disagreed.
5545  // A caret is the honest render of that state — it says "the next character
5546  // lands here" and claims nothing else.
5547  //
5548  // THE INK IS SAMPLED, NOT GUESSED. A hairline has no glyph shapes to soften,
5549  // so \`insertInk()\`'s near-black/near-white pair is not enough: the caret goes
5550  // to pure #fff / #000, chosen off the luminance of what it actually landed on
5551  // (the hit element's own paint, else the nearest painted ancestor, else the
5552  // frame). Mirrors caretInkFor / resolveCaretBackground in compose-place-tool.ts.
5553  //
5554  // THE ANCHOR EXISTS FOR IME. A window keydown listener would take Latin
5555  // characters and silently swallow the first 中文 / かな / 한글 one, because a
5556  // composition can only begin on a focused editable. So the caret ships with an
5557  // invisible focused contenteditable at the same point: plain typing, paste and
5558  // composition all arrive through it natively, and the FIRST of them converts.
5559
5560  // The background the caret is standing on. An <img> (or anything else we
5561  // cannot read a paint off) answers with the frame's background rather than
5562  // with a pixel readback — the caret only needs to know dark-or-light, and a
5563  // canvas round-trip per click to refine a binary answer is not worth it.
5564  function caretBgUnder(target) {
5565    var node = target;
5566    if (node && node.tagName === "IMG") node = node.parentElement;
5567    while (node && node.nodeType === 1 && node !== document.documentElement) {
5568      if (!isChrome(node)) {
5569        var c = __parseRgb(getComputedStyle(node).backgroundColor);
5570        if (c && c.a > 0.01) return c;
5571      }
5572      node = node.parentElement;
5573    }
5574    return pageBgRgb();
5575  }
5576  // MIRRORS caretInkFor in compose-place-tool.ts (same 0.5 luminance split, same
5577  // two inks);
5577 compose-place-tool.test.ts asserts this script carries them.
5578  function caretInk(rgb) { return __relLum(rgb) < 0.5 ? "#fff" : "#000"; }
5579
5580  // Drop the caret and leave NOTHING — no element, no selection, no history
5581  // entry, nothing for an undo to find. Called by Escape, by a press anywhere
5582  // else, by a tool change, by the parent clearing the selection, and by the
5583  // streaming body swap (which would otherwise wipe the nodes and strand the
5584  // state pointing at two detached elements).
5585  function dropTextCaret() {
5586    if (!caretPend) return;
5587    var p = caretPend;
5588    caretPend = null;
5589    if (p.bar && p.bar.parentNode) p.bar.parentNode.removeChild(p.bar);
5590    if (p.anchor && p.anchor.parentNode) p.anchor.parentNode.removeChild(p.anchor);
5591  }
5592
5593  function armTextCaret(clientX, clientY, target) {
5594    dropTextCaret();
5595    if (editingEl) stopEditing(true);
5596    // THE TOOL IS SPENT BY ONE USE, and it is spent HERE rather than after the
5597    // text exists — otherwise the press that cancels the caret would be read as
5598    // "place another one". Done BEFORE the caret is built so setTool's own
5599    // dropTextCaret cannot eat the caret this call is about to arm.
5600    setTool("select");
5601    post({ type: "tool-change", tool: "select" });
5602    clearHover();
5603    removeHandles();
5604    removeRings();
5605    // A pending caret is not a selection: the inspector must show the page, not
5606    // an element, and nothing may keep resize handles while the caret blinks.
5607    if (selectedSet.length) clearSelection(true);
5608    var ink = caretInk(caretBgUnder(target));
5609    var h = parseFloat(PLACED_TEXT.size) * parseFloat(PLACED_TEXT.lineHeight);
5610    var bar = document.createElement("div");
5611    bar.className = "__ppn_caret";
5612    bar.setAttribute("data-text-caret", "");
5613    bar.style.setProperty("left", clientX + "px");
5614    bar.style.setProperty("top", clientY + "px");
5615    bar.style.setProperty("height", h + "px");
5616    bar.style.setProperty("background-color", ink);
5617    document.body.appendChild(bar);
5618    var anchor = document.createElement("span");
5619    anchor.className = "__ppn_caretin";
5620    anchor.setAttribute("contenteditable", "true");
5621    anchor.style.setProperty("left", clientX + "px");
5622    anchor.style.setProperty("top", clientY + "px");
5623    anchor.style.setProperty("font-family", PLACED_TEXT.family);
5624    anchor.style.setProperty("font-size", PLACED_TEXT.size);
5625    anchor.style.setProperty("font-weight", PLACED_TEXT.weight);
5626    anchor.style.setProperty("line-height", PLACED_TEXT.lineHeight);
5627    anchor.style.setProperty("color", insertInk());
5628    anchor.addEventListener("input", onCaretInput);
5629    anchor.addEventListener("compositionend", onCaretComposed);
5630    document.body.appendChild(anchor);
5631    caretPend = { bar: bar, anchor: anchor, x: clientX, y: clientY, ink: ink };
5632    try { anchor.focus(); } catch (err) { /* focus is best-effort */ }
5633    // The press we are inside is a pointerdown; the mousedown that follows it
5634    // moves focus to whatever the browser thinks was clicked, which would undo
5635    // the line above. Re-assert once the gesture has settled — guarded on the
5636    // caret still being THIS one, so a caret dropped in between is not revived.
5637    setTimeout(function () {
5638      if (caretPend && caretPend.anchor === anchor) {
5639        try { anchor.focus(); } catch (err) { /* best-effort */ }
5640      }
5641    }, 0);
5642  }
5643
5644  // The first thing typed converts the caret into the real text element. \`input\`
5645  // covers a keystroke AND a paste (one event, whole clipboard); a composition
5646  // is skipped while it runs (isComposing) and lands on compositionend instead,
5647  // so an IME's candidate list can be navigated without the box appearing
5648  // half-way through.
5649  function onCaretInput(e) {
5650    if (e && e.isComposing) return;
5651    commitTextCaret();
5652  }
5653  function onCaretComposed() { commitTextCaret(); }
5654
5655  function commitTextCaret() {
5656    if (!caretPend) return;
5657    var p = caretPend;
5658    var seed = p.anchor ? (p.anchor.textContent || "") : "";
5659    if (!seed) return; // a deletion / no-op input is not "the user started typing"
5660    // Teardown BEFORE the element takes focus, so the anchor's blur finds no
5661    // pending caret to cancel and the bar cannot outlive the thing it pointed at.
5662    dropTextCaret();
5663    var root = document.body.firstElementChild || document.body;
5664    var el = buildInsert("text", seed);
5665    if (!el) return;
5666    el.setAttribute("data-cid", freshCid());
5667    root.appendChild(el);
5668    aimAt(el, p.x, p.y);
5669    clearHover();
5670    selectedSet = [el];
5671    // Uncommitted exactly as before (\xa7 THE EMPTY BOX): the document hears about
5672    // this element when the edit ENDS with content in it, in one entry.
5673    freshEl = el;
5674    startEditing(el, null);
5675    // startEditing SELECTS ALL, which is right for a box that already has text
5676    // (the first keystroke replaces it) and wrong here: the seed IS the user's
5677    // first keystroke, so the caret belongs after it — otherwise the second
5678    // character deletes the first.
5679    caretToEnd(el);
5680    postDocSize();
5681  }
5682
5683  function caretToEnd(el) {
5684    try {
5685      var range = document.createRange();
5686      range.selectNodeContents(el);
5687      range.collapse(false);
5688      var sel = getSelection();
5689      sel.removeAllRanges();
5690      sel.addRange(range);
5691    } catch (err) { /* best-effort */ }
5692  }
5693
5694  // ── in-place body swap (streaming ticks) ────────────────────────────────
5695  // The parent decided this tick changes only the BODY (see planPreviewSwap), so
5696  // the document is updated WITHOUT a reload — that reload is the black flash.
5697  //
5698  // Everything we own inside the body is chrome (selection rings, the 8 handles,
5699  // guides, the marquee) and is appended to the body, so the assignment wipes it.
5700  // Re-arming honestly: the elements the user had selected are addressed by
5701  // SELECTOR, so re-resolve them against the new markup and keep whatever still
5702  // exists; anything the tick removed simply drops out of the selection. The
5703  // parent is told either way, so its chip / format bar can never describe an
5704  // element that is no longer on the page.
5705  function setBody(html) {
5706    if (!document.body) return;
5707    var keep = [];
5708    for (var i = 0; i < selectedSet.length; i++) {
5709      var sel = selectorFor(selectedSet[i]);
5710      if (sel) keep.push(sel);
5711    }
5712    // Drop live chrome + any in-flight gesture BEFORE the markup goes away, so no
5713    // handler is left holding a detached node.
5714    if (drag) endDrag();
5715    dropTextCaret(); // the assignment below wipes its nodes; don't strand the state
5716    clearGuides();
5717    clearHover();
5718    removeHandles();
5719    selectedSet = [];
5720    editingEl = null;
5721    document.body.innerHTML = html;
5722    reselect(keep); // re-resolves what survived, and posts the new selection
5723    // Grafted markup can carry images that are already settled failures (a cached
5724    // 404 never fires \`error\` again) — sweep them before they paint.
5725    sweepBrokenImages();
5726    postDocSize();
5727  }
5728
5729  function reselect(selectors) {
5730    var found = [];
5731    for (var i = 0; i < selectors.length; i++) {
5732      try {
5733        var el = document.querySelector(selectors[i]);
5734        if (el) found.push(el);
5735      } catch (e) { /* invalid selector — skip */ }
5736    }
5737    selectedSet = found;
5738    clearHover();
5739    applyOutlines();
5740    postSelection();
5741  }
5742
5743  // ── SELECT WHAT IS AT A POINT (ENG-12621) ──────────────────────────────
5744  // The parent saw a press this frame never did. Every board on the arrange
5745  // canvas except the live one is a STATIC PICTURE with no picker in it, so a
5746  // press that opens a page has to be re-asked here once the page IS this frame:
5747  // the parent converts it to this frame's own px and posts it in.
5748  //
5749  // THE SAME DECISION A CLICK AT THAT POINT MAKES, reusing the same pieces —
5750  // pickAt (so an oversized box's empty part still sees through), the arch-word
5751  // grouping, and the root-sheet branch that makes a press on bare sheet a
5752  // DESELECT rather than a whole-page selection. What it deliberately does NOT
5753  // do is PLACE: with the text/rect/ellipse/line tool armed a click builds a
5754  // primitive, and a select-at is the tail of a gesture aimed at a different
5755  // board — the user asked to open that page, not to stamp a box into it at the
5756  // point they opened it from. Hand is the one tool that answers with nothing,
5757  // exactly as its click branch does.
5758  function selectAt(x, y) {
5759    if (!active) return;
5760    if (curTool === "hand") return;
5761    // No event, so no e.target to fall back on — ask the document for one.
5762    var fb = null;
5763    try { if (document.elementFromPoint) fb = document.elementFromPoint(x, y); } catch (e) { fb = null; }
5764    var el = pickAt(x, y, fb);
5765    if (
5766      !el || el.nodeType !== 1 ||
5767      isRootSheet(el) || el === document.body || el === document.documentElement
5768    ) {
5769      clearHover();
5770      clearSelection(true);
5771      return;
5772    }
5773    var grp = archGroupOf(el);
5774    if (grp) el = grp;
5775    selectedSet = [el];
5776    clearHover();
5777    applyOutlines();
5778    postSelection();
5779  }
5780
5781  // ── document size (fixed-artboard sizing for zoom/pan) ──────────────────
5782  var lastDocW = 0, lastDocH = 0;
5783  function postDocSize() {
5784    if (drag && drag.moved) return; // don't refit the board mid drag/resize
5785    var de = document.documentElement;
5786    var w = Math.max(de.scrollWidth, document.body ? document.body.scrollWidth : 0);
5787    var h = Math.max(de.scrollHeight, document.body ? document.body.scrollHeight : 0);
5788    if (w === lastDocW && h === lastDocH) return;
5789    lastDocW = w; lastDocH = h;
5790    post({ type: "doc-size", width: w, height: h });
5791  }
5792
5793  // Record the artboard's on-screen scale and expose its INVERSE as --ppn-s so the
5794  // scale-compensated chrome CSS (selection, edit frame, marquee, handles, guides)
5795  // renders at a constant SCREEN size regardless of zoom (⑪). Posted by the parent
5796  // on every zoom change + on load.
5797  // Switch tools. Leaving "hand" tears down any in-flight pan and its cursor so a
5798  // tool change mid-gesture can never strand the grabbing cursor or the pan state.
5799  function setTool(tool) {
5800    var next = TOOLS[tool] ? tool : "select";
5801    if (next === curTool) return;
5802    // Switching tools abandons a pending caret (ENG-11545). armTextCaret spends
5803    // the T tool BEFORE it builds the caret precisely so this line cannot eat the
5804    // caret that same gesture is arming.
5805    dropTextCaret();
5806    curTool = next;
5807    // The rotate cursor is a SELECT-tool state (ENG-11582). Dropped on the switch
5808    // rather than on the next mouse move: a tool change is a statement about what
5809    // the next press means, and leaving the glyph up until the pointer happens to
5810    // cross a boundary would advertise a gesture the new tool does not perform.
5811    if (next !== "select") document.body.classList.remove("__ppn_rotate");
5812    document.body.classList.toggle("__ppn_hand", next === "hand");
5813    // An armed placement tool wears the crosshair and, like the hand, stops
5814    // hovering / picking / marqueeing (see onOver and onDown).
5815    document.body.classList.toggle("__ppn_place", !!PLACE_TOOLS[next]);
5816    if (PLACE_TOOLS[next]) {
5817      // Arming a placement tool ends the gestures it outranks, exactly as the
5818      // hand does below: the next press belongs to the tool, so a drag / crop
5819      // session left open would be stranded behind a branch that no longer
5820      // routes to it.
5821      clearHover();
5822      if (drag) endDrag();
5823      if (cropSess) exitCrop(true);
5824      closeCropFrame();
5825      down = null;
5826      return;
5827    }
5828    if (next === "hand") {
5829      clearHover();
5830      if (drag) endDrag();
5831      // Picking up the hand ends crop mode, and ends it the way endDrag ends a
5832      // drag: by LANDING the work. onDown ranks the hand branch above the crop
5833      // branch, so leaving the session open would strand its overlay on a surface
5834      // that no longer routes presses to it.
5835      if (cropSess) exitCrop(true);
5836      // The crop frame goes for the same reason — onDown ranks the hand above it,
5837      // so an open session would strand its overlay on a surface that no longer
5838      // routes presses to it. Nothing is landed: Apply is the panel's, not the
5839      // hand tool's.
5840      closeCropFrame();
5841      down = null;
5842    } else {
5843      pan = null;
5844      document.body.classList.remove("__ppn_panning");
5845    }
5846  }
5847
5848  function setScale(scale) {
5849    curScale = typeof scale === "number" && scale > 0 ? scale : 1;
5850    document.documentElement.style.setProperty("--ppn-s", String(1 / curScale));
5851  }
5852
5853  // Any truthy mode ("edit", or the legacy "inspect"/"text" aliases) activates
5854  // the one unified edit layer; null / falsy deactivates it.
5855  //
5856  // IDEMPOTENT RE-ASSERTION (bug: "the resize handles vanish after I move a
5857  // shape"). The parent re-posts set-mode "edit" whenever its active composition
5858  // object changes — and a drag/resize commit produces a new composition object,
5859  // so EVERY move re-entered this function. It used to tear the live chrome down
5860  // unconditionally: removeHandles() dropped the 8 handles while selectedSet (and
5861  // therefore the ring) survived, leaving a selected element wearing a ring and no
5862  // handles until the user re-clicked it. Same teardown killed an in-flight drag
5863  // and the hover.
5864  //
5865  // A re-assert of the mode already in force must therefore change NOTHING. Only a
5866  // real transition tears down, and an activation ENDS by re-asserting the chrome
5867  // invariant (syncHandles: a single selected element always has its 8 handles).
5868  function setMode(next) {
5869    var want = !!next;
5870    if (want === active) return; // same mode — nothing to assert, nothing to tear down
5871    if (editingEl) stopEditing(true); // deactivating commits an open edit
5872    // …and so does an open CROP, for the identical reason and by the identical
5873    // precedent: an adjustment the user made is work, and a mode switch is not a
5874    // place to silently throw work away. (A marquee, below, IS abandoned — its
5875    // rectangle was never an edit, only a proposal.)
5876    if (cropSess) exitCrop(true);
5877    // A pending caret is ABANDONED, not landed (ENG-11545) — like the marquee
5878    // below, it is a proposal and not an edit: nothing has been typed, so there
5879    // is nothing a mode switch could be throwing away.
5880    dropTextCaret();
5881    // The crop FRAME is torn down instead of landed: it has authored nothing —
5882    // its commit is the panel's Apply, and the panel goes away with the mode.
5883    closeCropFrame();
5884    if (marqueeEl) { marqueeEl.remove(); marqueeEl = null; }
5885    if (drag) { releasePointer(drag); document.body.classList.remove("__ppn_dragging"); drag = null; }
5886    if (pan) { releasePointer(pan); pan = null; document.body.classList.remove("__ppn_panning"); }
5887    down = null;
5888    removeHandles();
5889    clearGuides();
5890    clearHover();
5891    active = want;
5892    if (!active) clearSelection(false);
5893    document.body.classList.toggle("__ppn_edit", active);
5894    // Activating with a selection already in hand (a re-activate after a reload
5895    // replays reselect) must show its handles immediately.
5896    if (active) syncHandles();
5897  }
5898
5899  // Wheel → forward to the parent for artboard zoom/pan. The page is a fixed
5900  // canvas (never reflows / scrolls internally), so we always preventDefault and
5901  // let the parent transform the board. zoom = pinch (trackpad sets ctrlKey) or
5902  // an explicit ctrl/cmd+wheel; otherwise it is a pan.
5903  //
5904  // NOT GATED ON \`active\`, and that guard is what this comment always claimed we
5905  // did not have. DEFECT (Andy): open /flow/<id> straight into Preview and pinch
5906  // immediately — the WHOLE BROWSER PAGE zoomed instead of the artboard.
5907  // MECHANISM: this script runs at parse time, so the listener is installed the
5908  // instant the document is read; but \`active\` only flips when the parent posts
5909  // \`set-mode\`, and the parent's reliable assertion of that rides the iframe's
5910  // \`load\` — which, for a srcdoc frame, waits for every SUBRESOURCE to settle
5911  // (see ComposeArrangeCanvas's note on the same property). A branding long-scroll
5912  // full of CDN images holds that for seconds. In the gap the page is on screen and
5913  // looks ready, this handler returns early, nothing calls preventDefault, and
5914  // ctrl/⌘+wheel falls through to the browser's own page zoom. It then "starts
5915  // working" once the images land, which is what made it read as interaction-gated.
5916  // The parent's own \`wheel\` branch is ungated for exactly this reason, so
5917  // forwarding early is safe — and zoom/pan is a VIEWPORT concern that was never
5918  // the edit layer's to authorize.
5919  function onWheel(e) {
5920    e.preventDefault();
5921    post({
5922      type: "wheel",
5923      deltaX: e.deltaX,
5924      deltaY: e.deltaY,
5925      zoom: !!(e.ctrlKey || e.metaKey),
5926      x: e.clientX,
5927      y: e.clientY,
5928      // Raw event facts the parent's zoom law needs; see PickerWheelMsg.
5929      deltaMode: e.deltaMode,
5930      ctrlKey: !!e.ctrlKey,
5931    });
5932  }
5933  document.addEventListener("wheel", onWheel, { passive: false, capture: true });
5934
5935  document.addEventListener("mouseover", onOver, true);
5936  document.addEventListener("mouseout", onOut, true);
5937  // THE GESTURE TRIO IS POINTER EVENTS, all three of them. A drag that begins on
5938  // a mousedown and ends on a pointerup (or the reverse) is how a release goes
5939  // missing; setPointerCapture — the thing that keeps a drag alive past the
5940  // iframe's edge and, crucially, delivers its END — needs a pointerId, and only
5941  // this family carries one. Hover stays on mouseover/mouseout: that is a
5942  // boundary notification, not a gesture, and nothing about it can be stranded.
5943  document.addEventListener("pointerdown", onDown, true);
5944  document.addEventListener("pointermove", onMove, true);
5945  document.addEventListener("pointerup", onUp, true);
5946  // A cancelled pointer (the OS took it: a gesture, a context menu, the node
5947  // holding the capture leaving the document) is a release the frame WILL be
5948  // told about — take it as one instead of waiting for an up that is not coming.
5949  document.addEventListener("pointercancel", function () { endStrandedGesture(); }, true);
5950  // Native HTML5 drag is the one default a pointerdown \`preventDefault()\` no
5951  // longer suppresses (it hangs off the mouse defaults). An <img> is draggable by
5952  // default, so without this, pressing an image to MOVE it tears off a ghost copy
5953  // instead — and the ghost swallows the release.
5954  document.addEventListener("dragstart", function (e) { if (active) e.preventDefault(); }, true);
5955  document.addEventListener("click", onClick, true);
5956  document.addEventListener("dblclick", onDblClick, true);
5957  document.addEventListener("keydown", onKey, true);
5958  // ⌘V into the page (ENG-12511) — relayed to the parent, which owns uploads and
5959  // the schema. Never while a field or the inline editor owns the paste.
5960  //
5961  // THE POINTER RIDES ALONG (ENG-12620). \`lastPt\` is written by every
5962  // \`pointermove\` and cleared the moment the pointer leaves the document, so
5963  // "null" means exactly "the mouse is not over this page" — the case where a
5964  // pasted image must fall back to the centre of the active page rather than to
5965  // whatever stale corner the pointer last crossed on its way out.
5966  // NOT a capturing listener on \`document\`: \`mouseleave\` does not bubble, so a
5967  // capture-phase handler up there would fire for EVERY element the pointer
5968  // leaves and blank the position on the way across the page. Bound to the root
5969  // element in the bubble phase, it fires once — when the pointer leaves it.
5970  var lastPt = null;
5971  document.documentElement.addEventListener("mouseleave", function () { lastPt = null; });
5972  function onPaste(e) {
5973    if (!active || editingEl || caretPend || isEditableTarget(e.target)) return;
5974    var dt = e.clipboardData;
5975    if (!dt) return;
5976    var files = [];
5977    var i;
5978    if (dt.files) {
5979      for (i = 0; i < dt.files.length; i++) {
5980        if (dt.files[i] && (dt.files[i].type || "").indexOf("image/") === 0) files.push(dt.files[i]);
5981      }
5982    }
5983    if (!files.length && dt.items) {
5984      for (i = 0; i < dt.items.length; i++) {
5985        var it = dt.items[i];
5986        if (it.kind !== "file") continue;
5987        var f = it.getAsFile();
5988        if (f && (f.type || "").indexOf("image/") === 0) files.push(f);
5989      }
5990    }
5991    var text = files.length ? "" : (dt.getData("text/plain") || "");
5992    var html = dt.getData("text/html") || "";
5993    if (!files.length && !text.trim() && html.indexOf("data-sunra-design-layer-v1") < 0) return;
5994    e.preventDefault();
5995    e.stopPropagation();
5996    post({ type: "paste", files: files, text: text, html: html, at: lastPt });
5997  }
5998  document.addEventListener("paste", onPaste, true);
5999
6000  // ── AN OS FILE DROPPED ON THE PAGE (SUNRA-1220) ──────────────────────────
6001  // THE DEFAULT HERE IS NAVIGATION — see PickerFileDropMsg for the whole story.
6002  // Two rules follow from that, and they are why this is not gated the way
6003  // \`onPaste\` is:
6004  //
6005  //   1. The REFUSAL is unconditional. \`preventDefault()\` runs before anything
6006  //      is decided and whether or not the edit layer is live, because a viewer
6007  //      looking at a read-only page must not have the page navigated out from
6008  //      under them either. Only the RELAY is gated on \`active\`.
6009  //   2. \`dragover\` must be prevented as well as \`drop\`. A dragover left alone
6010  //      means "this element does not take drops", and the browser then refuses
6011  //      to deliver the drop at all — so the one listener the eye skips is the
6012  //      one that makes the other possible.
6013  //
6014  // The point rides along the way the paste's does, except it is READ OFF THE
6015  // EVENT rather than remembered: a drop always knows where it happened, which is
6016  // why PickerFileDropMsg.at is required where PickerPasteMsg.at is nullable.
6017  var dropRing = null;
6018  function dragHasFiles(dt) {
6019    // The only question the frame asks. WHAT each file is belongs to the parent
6020    // (lib/drop-contract.ts owns this surface's mask, and the refusal has to name
6021    // the file it turned away) — and mid-drag \`files\` is empty anyway, so
6022    // \`types\` is the whole of what the browser will say here.
6023    if (!dt) return false;
6024    var t = dt.types;
6025    if (!t) return false;
6026    for (var i = 0; i < t.length; i++) if (t[i] === "Files") return true;
6027    return false;
6028  }
6029  function showDropRing() {
6030    if (!active || dropRing) return;
6031    dropRing = document.createElement("div");
6032    dropRing.className = "__ppn_dropring";
6033    document.body.appendChild(dropRing);
6034  }
6035  function clearDropRing() {
6036    if (dropRing && dropRing.parentNode) dropRing.parentNode.removeChild(dropRing);
6037    dropRing = null;
6038  }
6039  function onFileDragOver(e) {
6040    if (!dragHasFiles(e.dataTransfer)) return;
6041    e.preventDefault();
6042    e.stopPropagation();
6043    // \`dropEffect\` is what turns the cursor from the no-entry sign into a copy
6044    // badge. In a try/catch because it is read-only on some engines while the
6045    // drag is in protected mode, and a throw here would abandon the ring.
6046    try { e.dataTransfer.dropEffect = "copy"; } catch (err) { /* cursor stays generic */ }
6047    showDropRing();
6048  }
6049  function onFileDragLeave(e) {
6050    // A \`relatedTarget\` INSIDE this document is the pointer crossing from one
6051    // element to the next, not leaving the page — the same distinction the
6052    // parent's overlay makes with its own dragleave. A null one is the real exit
6053    // (out of the board, or out of the window entirely).
6054    if (e.relatedTarget) return;
6055    clearDropRing();
6056  }
6057  function onFileDrop(e) {
6058    if (!dragHasFiles(e.dataTransfer)) return;
6059    e.preventDefault();
6060    e.stopPropagation();
6061    clearDropRing();
6062    if (!active) return;
6063    var dt = e.dataTransfer;
6064    var files = [];
6065    // IN THE ORDER DROPPED, every one of them: the parent lays a slot per file in
6066    // this order, so a reordering here would scramble a multi-file drop.
6067    if (dt.files) for (var i = 0; i < dt.files.length; i++) { if (dt.files[i]) files.push(dt.files[i]); }
6068    if (!files.length) return;
6069    post({ type: "file-drop", files: files, at: { x: e.clientX, y: e.clientY } });
6070  }
6071  document.addEventListener("dragenter", onFileDragOver, true);
6072  document.addEventListener("dragover", onFileDragOver, true);
6073  document.addEventListener("dragleave", onFileDragLeave, true);
6074  document.addEventListener("drop", onFileDrop, true);
6075
6076  // THE RELEASE IS THE COMMIT. A held arrow key is one gesture however many
6077  // keydowns the OS repeat generates, so the burst is persisted here — once —
6078  // and the parent gets ONE history entry for it. Ending on ANY arrow release
6079  // (not just the one that started the burst) is deliberate: pressing → then ↓
6080  // and letting go of → is one continuous move to the user, and the second key's
6081  // own release would otherwise commit a burst that had already been committed.
6082  document.addEventListener("keyup", function (e) { if (NUDGE_DIR[e.key]) endNudge(); }, true);
6083  document.addEventListener("blur", onBlur, true);
6084  // The frame losing focus entirely (the user let go over the parent app and
6085  // clicked something there) is the other way a press ends without an up. Note
6086  // this is the WINDOW's blur — element blurs target descendants of \`document\`
6087  // and are handled by onBlur above, so the two never collide.
6088  window.addEventListener("blur", function () { endStrandedGesture(); });
6089
6090  window.addEventListener("message", function (e) {
6091    var d = e.data;
6092    if (!d || d.source !== CFG.host) return;
6093    if (d.type === "set-mode") setMode(d.mode || null);
6094    else if (d.type === "set-scale") setScale(d.scale);
6095    else if (d.type === "set-tool") setTool(d.tool);
6096    else if (d.type === "clear-selection") {
6097      // A press on the PARENT's floor — the grey around the page — never reaches
6098      // onDown, so the in-frame "anywhere else is the commit" rule above cannot
6099      // see it; the parent's deselect arrives here instead. It means the same
6100      // thing: land the crop, then clear (Andy 2026-09-08: "clicking outside of
6101      // the image when it's in the cropped mode should perform the crop on the
6102      // image and exit the crop mode").
6103      if (cropSess) exitCrop(true);
6104      dropTextCaret(); clearHover(); clearSelection(false);
6105    }
6106    else if (d.type === "apply-style") applyStyleToSelection(d.styles || {});
6107    else if (d.type === "style-preview") previewStyleOnSelection(d.styles || {});
6108    else if (d.type === "load-font") loadFont(d.href);
6109    else if (d.type === "set-attr") setAttr(d.selector, d.name, d.value);
6110    else if (d.type === "set-html") setHtml(d.selector, typeof d.html === "string" ? d.html : "", !!d.outer);
6111    else if (d.type === "patch-assets") patchAssets(d.patches || []);
6112    else if (d.type === "set-body") setBody(typeof d.html === "string" ? d.html : "");
6113    else if (d.type === "reselect") reselect(d.selectors || []);
6114    // The press that opened this page, replayed in this page's own px — see
6115    // selectAt. Guarded on real numbers: a dropped/garbled coordinate would
6116    // otherwise hit-test NaN and clear the selection for no reason.
6117    else if (d.type === "select-at") { if (typeof d.x === "number" && typeof d.y === "
6117number" && isFinite(d.x) && isFinite(d.y)) selectAt(d.x, d.y); }
6118    else if (d.type === "insert") insertPrimitive(d.kind, d.text || "");
6119    // Parent-forwarded Delete/Backspace (focus wandered out of the iframe). Same
6120    // effect as the in-frame onKey path — remove the selection if any.
6121    else if (d.type === "delete-selection") { if (selectedSet.length) removeSelection(); }
6122    else if (d.type === "set-busy") setBusy(d.selectors || [], d.label);
6123    else if (d.type === "set-pending") setPending(d.selectors || []);
6124    else if (d.type === "set-uploading") setUploadingLabel(d.cid, typeof d.label === "string" ? d.label : null);
6125    // The crop frame's half of the two-way binding with the side panel.
6126    else if (d.type === "set-crop-frame") setCropFrame(d);
6127  });
6128
6129  // Report the document's natural size once laid out, and whenever it changes
6130  // (webfont swap, insert, image load) so the parent's fixed artboard tracks it.
6131  function reportSize() { sweepBrokenImages(); postDocSize(); }
6132  sweepBrokenImages();
6133  if (document.readyState === "complete") reportSize();
6134  else window.addEventListener("load", reportSize);
6135  window.addEventListener("resize", reportSize);
6136  try {
6137    if (window.ResizeObserver) {
6138      var ro = new ResizeObserver(function () { postDocSize(); });
6139      if (document.body) ro.observe(document.body);
6140    }
6141  } catch (e) { /* ResizeObserver unavailable — load/resize covers it */ }
6142  // Fonts / images settle after first paint — re-measure shortly after init.
6143  // Guarded: the runtime is also installed into jsdom by the compose dom tests,
6144  // and a timer outliving its test fires into a torn-down document (vitest
6145  // counts the ReferenceError as an unhandled error, CI 2026-09-11). In a
6146  // browser the guard is always true.
6147  function reportSizeLater() { if (typeof document !== "undefined" && document.body) reportSize(); }
6148  setTimeout(reportSizeLater, 60);
6149  setTimeout(reportSizeLater, 400);
6150})();
6151</script>`}(t,a&&o?{muted:a,foreground:o,card:r?.getPropertyValue("--card").trim()||void 0,border:r?.getPropertyValue("--border").trim()||void 0}:void 0),{partial:n})}let nq=[];function nX(e){let t="u">typeof CSS&&CSS.escape?CSS.escape(e):e.replace(/"/g,'\\"');return`[data-cid="${t}"]`}function nY({compose:n,pages:r,zoom:o,store:i,hidden:d=!1}){let c=(0,s.useViewerReadOnly)(),h=(0,s.useViewerDisplayOnly)(),u=(0,f.useRef)(h);u.current=h;let{active:p,compositions:H,applyIncoming:M,hydrated:j,transient:B,externalRev:G,undo:W,redo:U,registerPreviewOpSink:q,composing:X}=n.compositions,Y=eu(e=>e.setPending),$=eu(e=>e.clear),V=eu(e=>e.pending),K=nB({hydrated:j,compositions:H,composing:X,active:p}),Z=K&&null!==(0,tq.emptyFramesDocOf)(p),J=K?null:p,Q=(0,f.useRef)(K);Q.current=K;let[ee,et]=(0,f.useState)({w:0,h:0}),en=(0,f.useMemo)(()=>(0,tq.emptyFramesDocOf)(p)?.format??tC,[p]),er=(0,f.useMemo)(()=>(function(e,t,n=tC){let r=(0,tN.resolvePreset)(n),a=r.height/r.width,o=Math.max(240,Math.round(Math.min(e>0?Math.max(1,e-96):620,(t>0?Math.max(1,t-96):1/0)/a,620)));return{width:o,height:Math.round(o*a)}})(ee.w,ee.h,en),[ee,en]),ea=(0,m.useTranslations)("Flow"),eo=(0,m.useTranslations)("Common"),ei=(0,f.useRef)(null),es=(0,f.useMemo)(()=>getComputedStyle(document.documentElement).getPropertyValue("--flow-select").trim()||"currentColor",[]),[el,ed]=(0,f.useState)(null),[ec,eh]=(0,f.useState)(null),[em,eg]=(0,f.useState)(!1),ew=(0,f.useRef)([]),ex=(0,f.useRef)(p);ex.current=p;let eE=(0,f.useRef)(r.activePageId);eE.current=r.activePageId,(0,f.useEffect)(()=>((0,k.noteComposePageHint)(r.activePageId,r.activeIndex,p?.id),()=>(0,k.noteComposePageHint)(null,null,p?.id)),[r.activePageId,r.activeIndex,p?.id]);let eT=(0,f.useRef)(null),[eA,eR]=(0,f.useState)(null),eN=(0,f.useCallback)(e=>{eT.current=e,eR(e)},[]),eC=(0,f.useRef)(null),[eI,eO]=(0,f.useState)(0),eP=(0,f.useRef)(eI);eP.current=eI;let[eD,eL]=(0,f.useState)(0),[ez,eF]=(0,f.useState)({zoom:1,panX:0,panY:0}),eH=(0,f.useRef)(ez),eM=(0,f.useRef)(null),ej=(0,f.useCallback)(e=>{let t="function"==typeof e?e(eH.current):e;t===eH.current||(eH.current=t,null===eM.current&&(eM.current=requestAnimationFrame(()=>{eM.current=null,eF(eH.current)})))},[]),eB=(0,f.useCallback)(e=>{let t="function"==typeof e?e(eH.current):e;
6151eH.current=t,null!==eM.current&&(cancelAnimationFrame(eM.current),eM.current=null),eF(t)},[]);(0,f.useEffect)(()=>()=>{null!==eM.current&&cancelAnimationFrame(eM.current)},[]);let eG=na[n.device],eW=r.doc,eq="frames"===r.source&&!!eW,eX=(0,f.useMemo)(()=>eq||eD<=eG+1?eG:Math.min(eD,3*eG),[eq,eG,eD]),eY=(0,f.useRef)(eX);eY.current=eX;let e$=eq||Z,eK="all"===r.view&&e$,eJ=!eK&&(r.pages.length>0||Z),eQ=r.activePageId,e0=(0,f.useMemo)(()=>eK?{arrange:"stack",headerSize:z.ARRANGE_HEADER_H,trailingAddOnly:!0}:{arrange:"single"},[eK]),e1=(0,f.useMemo)(()=>eW?Math.max(0,eW.frames.findIndex(e=>e.id===r.activePageId)):0,[eW,r.activePageId]),e2=(0,f.useMemo)(()=>eq&&eW?(0,tk.computeArrange)(eW,{boardWidth:eG,activeIndex:e1,...e0}):null,[eq,eW,eG,e1,e0]),e5=(0,f.useMemo)(()=>{if(!e2||!eW)return null;let e=e2.boards.find(e=>eW.frames[e.index]?.id===r.activePageId),t=e?eW.frames[e.index]:null;return e&&t?{x:e.x,y:e.y,scale:e.w/(0,tN.resolvePreset)(t.preset).width}:null},[e2,eW,r.activePageId]),e6=(0,f.useRef)(null);e6.current=e5;let[e4,e3]=(0,f.useState)({width:0,height:0}),e8=(0,f.useCallback)(e=>{if(!e||"u"<typeof ResizeObserver){e||e3({width:0,height:0});return}let t=()=>e3({width:e.offsetWidth,height:e.offsetHeight});t();let n=new ResizeObserver(t);n.observe(e),e7.current?.disconnect(),e7.current=n},[]),e7=(0,f.useRef)(null);(0,f.useEffect)(()=>()=>e7.current?.disconnect(),[]);let e9=e4.width>0&&e4.height>0,te=(0,f.useMemo)(()=>ec?function({selection:e,bar:t,pane:n,gap:r=12,edgePad:a=8}){let o=e.top-r-t.height,i=e.top+e.height+r,s=o>=a,l=i+t.height<=n.height-a,d=s||!l?"above":"below";return{left:tS(e.left+e.width/2-t.width/2,a,Math.max(a,n.width-t.width-a)),top:tS("above"===d?o:i,a,Math.max(a,n.height-t.height-a)),side:d}}({selection:tT(ec,{panX:ez.panX,panY:ez.panY,zoom:ez.zoom,boardX:e5?.x??0,boardY:e5?.y??0,boardScale:e5?.scale??1}),bar:e4,pane:{width:ee.w,height:ee.h}}):{left:Math.max(8,(ee.w-e4.width)/2),top:12,side:"above"},[ec,ez,e5,e4,ee]),[tt,tn]=(0,f.useState)(0),tr=(0,f.useRef)(null),ta=(0,f.useCallback)(e=>{if(!e||"u"<typeof ResizeObserver){e||tn(0);return}let t=()=>tn(e.offsetHeight);t();let n=new ResizeObserver(t);n.observe(e),tr.current?.disconnect(),tr.current=n},[]);(0,f.useEffect)(()=>()=>tr.current?.disconnect(),[]);let to=(0,f.useMemo)(()=>ec?function({selection:e,panel:t,pane:n,gap:r=12,edgePad:a=8}){let o,i=e.left+e.width+r,s=e.left-r-t.width,l=i+t.width<=n.width-a,d=s>=a;return{left:tS("right"==(o=l?"right":d?"left":n.width-a-(e.left+e.width+r)>=e.left-r-a?"right":"left")?i:s,a,Math.max(a,n.width-t.width-a)),top:tS(e.top,a,Math.max(a,n.height-t.height-a)),side:o}}({selection:tT(ec,{panX:ez.panX,panY:ez.panY,zoom:ez.zoom,boardX:e5?.x??0,boardY:e5?.y??0,boardScale:e5?.scale??1}),panel:{width:O.TEXT_EDIT_PANEL_WIDTH,height:tt},pane:{width:ee.w,height:ee.h}}):{left:Math.max(8,ee.w-O.TEXT_EDIT_PANEL_WIDTH-8),top:8,side:"right"},[ec,ez,e5,tt,ee]),ts=r.pages,tl=r.activePageId;(0,f.useEffect)(()=>{if(ts.length<2)return;let e=ts.findIndex(e=>e.id===tl);if(e<0)return;let t=window.requestIdleCallback,n=window.cancelIdleCallback,r=()=>(0,tJ.warmNeighborPages)(ts,e,{width:(0,tJ.previewSurfaceRung)(eG),dpr:1,priority:"low"});if(t&&n){let e=t.call(window,r);return()=>n.call(window,e)}let a=window.setTimeout(r,300);return()=>window.clearTimeout(a)},[ts,tl,eG]);let td=(0,f.useRef)(e2);td.current=e2;let tc=(0,f.useRef)(eW);tc.current=eW;let[th,tu]=(0,f.useState)(!1),tp=(0,b.useDropRejectedLabel)(),[tm,tf]=(0,f.useState)("none"),[tg,ty]=(0,f.useState)(null),tv=(0,f.useRef)(null),tw=(0,f.useRef)(null),tx=(0,f.useRef)(!1),tQ=(0,f.useRef)(""),t0=r.activeHtml,t1=function(e,t,n){let[r,a]=(0,f.useState)(0),o=(0,f.useRef)(0),i=(0,f.useRef)(null),s=(0,f.useRef)(e);return(0,f.useEffect)(()=>{if(s.current===e)return;if(s.current=e,!t){i.current&&(clearTimeout(i.current),i.current=null),o.current=Date.now(),a(e=>e+1);return}let r=Date.now()-o.current;r>=n?(o.current=Date.now(),a(e=>e+1)):i.current||(i.current=setTimeout(()=>{i.current=null,o.current=Date.now(),a(e=>e+1)},n-r))},[e,t,n]),(0,f.useEffect)(()=>()=>{i.current&&clearTimeout(i.current)},[]),r}(G,B,tO.PREVIEW_BUILD_THROTTLE_MS),t2=(0,f.useRef)(B);t2.current=B;let t5=(0,f.useRef)(t0);t5.current=t0;let[t6,t4]=(0,f.useState)(0),t8=(0,t_.liveFrameSlotKey)(),ne=`${p?.id??""}:${t6}:${t8}`,nf=r.activePageId??"",nb=(0,f.useMemo)(()=>{let e=t5.current;return ex.current&&e.trim()?c?e:nU(e,es,t2.current):""},[ne,nf,t1,es,c]),[ny,nv]=(0,f.useState)(()=>{var e;return{identity:(e={identity:ne,page:nf,tick:t1,wanted:nb}).identity,page:e.page,tick:e.tick,srcDoc:e.wanted,shown:e.wanted,nonce:0,patches:null,body:null}}),[nw,nx]=(0,f.useState)(null),nE=!c&&nw===ny.nonce,nk=(0,f.useRef)(!1);nk.current=nE;let nS=function(e,t){if(e.identity!==t.identity)return tP(e,t);if(e.page!==t.page){let n=(0,tO.planPreviewSwap)(e.shown,t.wanted);return"rebuild"!==n.kind&&("none"===n.kind||t.frameReady)?tD(e,t,n):tP(e,t)}if(e.tick!==t.tick){let n=(0,tO.planPreviewSwap)(e.shown,t.wanted);return"none"===n.kind||t.frameReady?"rebuild"===n.kind?tP(e,t):tD(e,t,n):tP(e,t)}return e}(ny,{identity:ne,page:nf,tick:t1,wanted:nb,frameReady:nE});nS!==ny&&nv(nS);let nT=ny.srcDoc,nA=String(ny.nonce);(0,f.useEffect)(()=>{eL(0)},[nA,nf]);let nR=(0,f.useRef)(null),nN=(0,f.useRef)(ny.nonce);nN.current=ny.nonce;let nC=(0,f.useRef)({pageId:null,selectors:[]}),[nI,n_]=(0,f.useState)([]),nO=(0,f.useRef)(!0),nP=(0,f.useCallback)(()=>{let e=nC.current;0===e.selectors.length||e.pageId===(eE.current??null)&&ey(ei.current?.contentWindow,{type:"set-busy",selectors:e.selectors,label:e.label})},[]),nD=ny.body;(0,f.useEffect)(()=>{nD&&(ey(ei.current?.contentWindow,{type:"set-body",html:nD.html}),nP())},[nD,nP]);let nL=(0,f.useCallback)(()=>{let e=tw.current;if(e){if(e.frameId!==(eE.current??"")){tw.current=null;return}tw.current=null,ey(ei.current?.contentWindow,{type:"select-at",x:e.x,y:e.y})}},[]);(0,f.useEffect)(()=>{nE&&nL()},[nE,nD,nf,nL]);let nz=ny.patches;(0,f.useEffect)(()=>{nz&&0!==nz.length&&(nR.current={nonce:nN.current,patches:nz},ey(ei.current?.contentWindow,{type:"patch-assets",patches:nz}))},[nz]);let nF=(0,f.useCallback)(()=>{let e=nR.current;e&&e.nonce===nN.current&&ey(ei.current?.contentWindow,{type:"patch-assets",patches:e.patches})},[]),nH=(0,f.useRef)(!1),nM=(0,f.useCallback)((e,t,n)=>{let r=eE.current??null;nH.current=!0,M({title:t,html:e},{editPage:r,...n?{inverse:n}:{}})},[M]),nj=window.location.origin,n$=(0,f.useCallback)(e=>{let t=ex.current;if(!t)return"";let n=(0,tq.parseCompositionPages)({...t,html:e},nj);return(0,tq.resolveActivePage)(n,eE.current).page?.html??""},[nj]);(0,f.useEffect)(()=>
6151(q((e,t,n)=>{if(n&&n!==ex.current?.id)return!1;let r=ei.current?.contentWindow;if(!r||!nk.current||0===e.length||t&&eE.current&&t!==eE.current)return!1;for(let t of e)ey(r,{type:"set-html",selector:t.selector,html:t.html,outer:t.outer});return e.some(e=>e.outer)&&nP(),nH.current=!0,!0}),()=>q(null)),[q,nP]),(0,f.useEffect)(()=>{if(!nH.current)return;nH.current=!1;let e=nU(t0,es,t2.current);e&&nv(t=>t.shown===e?t:{...t,shown:e})},[t0,es]);let nV=(0,f.useCallback)((e,t,n)=>{var r;let a,o,i=t5.current,s=n$(e);return i&&s?(a=tz((r={before:i,after:s,selectors:t,scope:n,canRestore:(0,tN.looksLikeFramesEnvelope)(e)}).before),o=tz(r.after),a&&o?function(e,t,n,r,a){if(0===n.length)return null;let o=[],i=[];for(let s of n){if(!s.trim())return null;let n=e(s),l=t(s);if(!n&&!l)return null;if("text"===r){if(!n||!l)return null;o.push({selector:s,html:n.inner}),i.push({selector:s,html:l.inner});continue}if((!n||!l)&&!a)return null;o.push({selector:s,html:n?n.outer:"",outer:!0}),i.push({selector:s,html:l?l.outer:"",outer:!0})}return{undo:o,redo:i}}(a,o,r.selectors,r.scope,r.canRestore):null):null},[n$]),nK=(0,f.useCallback)((e,t)=>{var n;let r,a=t5.current;return a?(r=tz((n={before:a,selectors:e,spacers:t}).before))?function(e,t,n){if(0===t.length)return null;let r=[],a=[];for(let o=0;o<t.length;o+=1){let i=t[o];if(!i.trim())return null;let s=e(i);if(!s)return null;let l=n[o];if(!l||!l.selector.trim()||!l.html.trim())return null;r.push({selector:l.selector,html:s.outer,outer:!0}),a.push({selector:i,html:l.html,outer:!0})}return{undo:r,redo:a}}(r,n.selectors,n.spacers):null:null},[]),nZ=(0,f.useCallback)(e=>{ey(ei.current?.contentWindow,{type:"apply-style",styles:e})},[]),nJ=(0,f.useCallback)(e=>{ey(ei.current?.contentWindow,{type:"style-preview",styles:e})},[]),nQ=(0,f.useCallback)((e,t,n)=>{ey(ei.current?.contentWindow,{type:"set-attr",selector:e,name:t,value:n})},[]),n0=(0,f.useCallback)(()=>{ey(ei.current?.contentWindow,{type:"delete-selection"})},[]),n1=(0,_.usePreviewInspector)(e=>e.registerWriters),n2=(0,_.usePreviewInspector)(e=>e.publishSelection),n5=(0,_.usePreviewInspector)(e=>e.clearSelection),n6=(0,_.usePreviewInspector)(e=>e.publishDocument),n4=(0,_.usePreviewInspector)(e=>e.publishLiveStyle),n3=(0,_.usePreviewInspector)(e=>e.docked),n8=(0,_.usePreviewInspector)(e=>e.selection),n7=(0,_.usePreviewInspector)(e=>e.frameId),n9=r.doc??null,re=r.activePageId,rt=`${n.projectId??""}:${p?.id??""}:${r.activePageId??""}`,rn=(0,f.useRef)({scope:rt});rn.current.scope!==rt&&(rn.current={scope:rt});let rr=rn.current;(0,f.useLayoutEffect)(()=>{n5()},[rr,n5]),(0,f.useLayoutEffect)(()=>{n6(n9,re)},[n6,n9,re]);let ra=(0,f.useMemo)(()=>(function(e){if(!e?.image||1!==e.count||1!==e.selectors.length)return null;let t=e.selectors[0],n=(e.src??"").trim();return t&&n?{selector:t,src:n,...e.style.imageGeometry?{geometry:e.style.imageGeometry}:{}}:null})(el),[el]),ro=!!p&&(0,tN.looksLikeFramesEnvelope)(p.html),ri=(0,f.useCallback)((e,t,n)=>{let r=ex.current,a=t??ra?.selector;if(!r||!a||0===e.length)return;if(!(0,tN.looksLikeFramesEnvelope)(r.html)&&!n)throw Error("Couldn't read this image's layout; reopen the Design and try again");let o=`lyr-${Date.now()}`,i=(0,tN.looksLikeFramesEnvelope)(r.html)?(0,nt.patchFramesEnvelope)(r.html,t=>(0,nt.replaceImageComponentWithLayers)(t,eE.current,a,e,o,n?{width:n.naturalWidth,height:n.naturalHeight}:void 0)):(0,tL.applyPageAwarePatch)(r.html,eE.current,t=>(0,tH.replaceImageWithLayerStack)(t,a,e,{cidPrefix:o,sourceGeometry:n}));i?(nM(i,r.title,nV(i,[a],"outer")),ey(ei.current?.contentWindow,{type:"clear-selection"}),ed(null),eh(null),t4(e=>e+1)):(0,g.toast)("Couldn't place the layers in this page")},[ra?.selector,nM,nV]),rs=(0,f.useCallback)((e,t)=>{let n=ex.current,r=t??ra?.selector;if(!n||!r||!e)return;let a=ro?(0,nt.patchFramesEnvelope)(n.html,t=>(0,nt.applyImageSrcToDoc)(t,eE.current,r,e)):(0,tL.applyPageAwarePatch)(n.html,eE.current,t=>(0,tL.patchCompositionAttr)(t,r,"src",e));a?(nM(a,n.title,nV(a,[r],"outer")),ey(ei.current?.contentWindow,{type:"set-attr",selector:r,name:"src",value:e})):(0,g.toast)("Couldn't update this image")},[ra?.selector,ro,nM,nV]),rl=(0,f.useCallback)((e,t)=>{let n=ex.current,r=t??ra?.selector;if(!n||!r)return;let a=ro?(0,nt.patchFramesEnvelope)(n.html,t=>(0,nt.applyImageAltToDoc)(t,eE.current,r,e)):(0,tL.applyPageAwarePatch)(n.html,eE.current,t=>(0,tL.patchCompositionAttr)(t,r,"alt",e));a?(nM(a,n.title,nV(a,[r],"outer")),ey(ei.current?.contentWindow,{type:"set-attr",selector:r,name:"alt",value:e})):(0,g.toast)("Couldn't update this image")},[ra?.selector,ro,nM,nV]),rd=(0,f.useCallback)(e=>{let t=ex.current;if(!t||!ro)return;let n=(0,w.normalizeToHex)(e);if(!n)return;let r=eE.current,a=(0,nt.patchFramesEnvelope)(t.html,e=>
6151(0,nt.applyFrameBackgroundToDoc)(e,r,n));if(!a)return void(0,g.toast)("Couldn't update the page background");nM(a,t.title);let o=r?n9?.frames.find(e=>e.id===r):n9?.frames[0];o&&ey(ei.current?.contentWindow,{type:"set-attr",selector:"[data-frame]",name:"style",value:(0,nr.frameSheetStyle)({...o,background:{type:"color",color:n}})})},[ro,n9,nM]),rc=(0,f.useCallback)(e=>{let t=ex.current;if(!t||!ro)return;let n=eE.current,r=(0,nt.patchFramesEnvelope)(t.html,t=>(0,nt.applyFramePresetToDoc)(t,n,e));if(!r)return;nM(r,t.title);let a=n?n9?.frames.find(e=>e.id===n):n9?.frames[0];a&&(0,tN.isFramePresetId)(e)&&ey(ei.current?.contentWindow,{type:"set-attr",selector:"[data-frame]",name:"style",value:(0,nr.frameSheetStyle)({...a,preset:e})})},[ro,n9,nM]),rh=(0,f.useCallback)((e,t)=>{let n=ex.current;if(!n||!e||!ro||nO.current)return;let r=(0,nt.patchFramesEnvelope)(n.html,n=>(0,nt.reorderComponentInFrame)(n,eE.current,e,t));if(!r)return;let a=(0,tG.planScopedApplyFromHtml)(n.html,r),o=a&&1===a.frames.length?a.frames[0]:null;if(o){for(let e of(nM(r,n.title,o.ops),o.ops.redo))ey(ei.current?.contentWindow,{type:"set-html",selector:e.selector,html:e.html,outer:e.outer});o.ops.redo.some(e=>e.outer)&&nP()}},[ro,nM,nP]),{isDownloading:ru,download:rp}=(0,E.useAssetDownload)(),rm=(0,f.useCallback)((e,t)=>{nC.current={pageId:eE.current??null,selectors:e,label:t},n_(e),ey(ei.current?.contentWindow,{type:"set-busy",selectors:e,label:t})},[]),rf=function({projectId:e,documentId:t,pageId:n,store:r,target:a,layerizeHtml:o,onImageSrc:i,onLayers:s,onBusyChange:l}){var d;let c=(0,m.useTranslations)("Flow"),h=a?.src??null,u=(0,f.useRef)(o);u.current=o;let p=(0,f.useMemo)(()=>({projectId:e,documentId:t,pageId:n,store:r}),[e,t,n,r]),b=(0,f.useRef)(p);b.current=p;let y=(0,f.useRef)(!0);(0,f.useEffect)(()=>(y.current=!0,()=>{y.current=!1}),[]);let v=(0,f.useCallback)(()=>y.current&&!!p.documentId&&b.current===p,[p]),[w,x]=(0,f.useState)(!1),{refreshWallet:E}=(0,no.useAuth)(),k=(0,f.useRef)(null),S=(0,f.useRef)(new Set),T=(0,f.useRef)(new Set),A=(0,f.useRef)(!1);(0,f.useEffect)(()=>{S.current=new Set,T.current=new Set,A.current=!1},[p]);let R=(0,f.useCallback)(()=>{k.current={owner:p,selector:a?.selector??null},A.current=!0},[p,a?.selector]),[N,C]=(0,f.useState)(null),I=(0,f.useRef)(null);(0,f.useEffect)(()=>{if(I.current=null,C(null),!h||"u"<typeof Image)return;let e=!1,t=new Image;return t.onload=()=>{!e&&v()&&C({w:t.naturalWidth,h:t.naturalHeight})},t.src=h,()=>{e=!0}},[h,v]);let _=(0,f.useMemo)(()=>({src:h??void 0,width:N?.w,height:N?.h,dimsSrc:N?h??void 0:void 0}),[h,N]),O=(0,f.useCallback)(()=>{v()&&x(!0)},[v]),P=(0,f.useRef)(null),D=(0,f.useCallback)(()=>{var e;if(!h)return null;let t=P.current;if(t&&t.src===h&&t.store===r)return{nodeId:t.nodeId,position:t.position};let n=r.getState().nodes.find(e=>e.data.src===h);if(n){let e=n.position??{x:0,y:0};return P.current={src:h,store:r,nodeId:n.id,position:e},{nodeId:n.id,position:e}}let a=`adopted-${Date.now()}-${Math.random().toString(36).slice(2,6)}`,o=function(e){let t=-1/0,n=1/0;for(let r of e){let e=r.position?.x,a=r.position?.y;"number"==typeof e&&Number.isFinite(e)&&(t=Math.max(t,e)),"number"==typeof a&&Number.isFinite(a)&&(n=Math.min(n,a))}return Number.isFinite(t)?{x:t+120,y:Number.isFinite(n)?n:0}:{...nm}}(r.getState().nodes);return r.getState().addNode({id:(e={id:a,url:h,position:o,width:N?.w,height:N?.h}).id,type:"image",position:e.position,data:{modality:"image",label:e.label||"Page image",isUploaded:!0,...e.width?{width:e.width}:{},...e.height?{height:e.height}:{},src:e.url,.../^https?:\/\//.test(e.url)?{out_image:e.url}:{}}}),P.current={src:h,store:r,nodeId:a,position:o},{nodeId:a,position:o}},[h,r,N]),L=(0,f.useCallback)(e=>{v()&&i(e,a?.selector??null)},[i,v,a?.selector]),z=(0,np.useImageOpActions)(_,r,{onResult:L,openPaywall:O,resolveSourceNode:D,silentOp:!0}),[F,H]=(0,f.useState)(null),[M,j]=(0,f.useState)(null),[B,G]=(0,f.useState)([]);(0,f.useEffect)(()=>{H(null),j(null),G([])},[h,a?.selector,p]);let W=(0,f.useCallback)(e=>H(t=>t===e?null:e),[]),U=(0,f.useCallback)(()=>H(null),[]),[q,X]=(0,f.useState)({aspect:"free",rect:{x:0,y:0,w:0,h:0}});(0,f.useEffect)(()=>{"crop"!==F||!N||N.w<=0||N.h<=0||X({aspect:"free",rect:{x:0,y:0,w:N.w,h:N.h}})},[F,N]);let Y=(0,f.useCallback)(e=>X(t=>({...t,rect:e})),[]);(0,f.useEffect)(()=>
6151{if("text-edit"!==F||!h||null!==M)return;let e=!1;return(async()=>{try{let t=I.current?.src===h?I.current.url:await (0,nh.ensureNodeOssUrl)(h);if(e||!v())return;I.current={src:h,url:t};let n=(0,nl.getCachedExtraction)(t);if(n){e||(j(n),G([...n]));return}let r=await ng({imageUrl:(0,ns.getOssTextExtractUrl)(t)});if(e||!v())return;if(r.error){(0,g.toast)(`Couldn't read the image — ${r.error}`),j([]);return}(0,nl.setCachedExtraction)(t,r.texts),j(r.texts),G([...r.texts])}catch(t){!e&&v()&&((0,g.toast)(t instanceof Error?`Couldn't read the image — ${t.message}`:"Couldn't read the image"),j([]))}})(),()=>{e=!0}},[F,h,M,v]);let $=(0,f.useCallback)(e=>{0!==e.length&&(H(null),R(),z.handleTextEdit(e))},[z,R]),[V,K]=(0,f.useState)(!1),Z=(0,f.useRef)(null);(0,f.useEffect)(()=>(K(!1),()=>Z.current?.abort()),[p]);let J=(0,f.useCallback)(()=>{if(!e||!h||V||!v())return;K(!0);let t=new AbortController,n=(0,ni.getAuthIdentitySignal)(),i=AbortSignal.any([t.signal,n]);Z.current?.abort(),Z.current=t,(async()=>{let l=null;try{k.current={owner:p,selector:a?.selector??null};let t=D();if(!t?.nodeId)return;let d=t.nodeId,c=I.current?.src===h?I.current.url:await (0,nh.ensureNodeOssUrl)(h);if(i.aborted||!v())return;I.current={src:h,url:c},r.getState().updateNodeData(d,{src:c,out_image:c});let m=r.getState().nodes.find(e=>e.id===d),f="string"==typeof m?.data.label?m.data.label:"Page image",b=await (0,nc.runLayerizeTask)({store:r,projectId:e,sourceNodeId:d,sourceLabel:f,sourceUrl:c,anchor:()=>{let e=r.getState().nodes.find(e=>e.id===d)?.position??t.position??{x:0,y:0};return{x:e.x+nd.NODE_EXTEND_DX,y:e.y}},signal:i,onTaskSubmitted:e=>{l=e,n.aborted||E()}});if(i.aborted||!v())return;if(!b.ok)return void("insufficient-credits"===b.failure.kind?O():"error"===b.failure.kind&&(0,g.toast)(`Couldn't layerize — ${b.failure.message}`));if(u.current!==o)throw Error("The Design changed while separating layers; the original image was kept");s(b.layers.map(e=>({name:e.name,url:e.url,boundingBox:e.boundingBox})),a?.selector??null,a?.geometry),(0,g.toast)(`Separated ${b.layers.length} layers`)}catch(e){if(i.aborted||!v())return;(0,g.toast)(e instanceof Error?`Couldn't layerize — ${e.message}`:"Couldn't layerize")}finally{l&&!n.aborted&&(E(),i.aborted&&(0,nu.refreshWalletAfterLayerizeSettles)({taskId:l,identity:n,refreshWallet:E})),!t.signal.aborted&&Z.current===t&&v()&&K(!1)}})()},[e,h,V,r,s,o,a?.geometry,D,O,E,a?.selector,p,v]),Q=r(e=>{let t="";for(let n of e.nodes)!0===n.data.silentOp&&!0===n.data.generating&&(t+=`${n.id}|`);return t}),ee=(0,f.useMemo)(()=>{let e=Q?Q.split("|").filter(Boolean):[],t=S.current,n=T.current,r=!1;for(let a of e)!n.has(a)&&(n.add(a),A.current&&(A.current=!1,t.add(a))),t.has(a)&&(r=!0);for(let n of Array.from(t))e.includes(n)||t.delete(n);for(let t of Array.from(n))e.includes(t)||n.delete(t);return r},[Q]),et=(0,f.useMemo)(()=>(ee||V)&&k.current?.owner===p?[k.current.selector].filter(e=>!!e):[],[ee,V,p]),en=V?c("layerizing"):r.getState().nodes.find(e=>S.current.has(e.id)&&e.data.generating)?.data.label;(0,f.useEffect)(()=>
6151{l(et,en)},[et,en,l]);let er=(0,f.useCallback)(e=>(...t)=>(R(),e(...t)),[R]),ea=(0,f.useMemo)(()=>({...z,handleVary:er(z.handleVary),handleRemoveBg:er(z.handleRemoveBg),handleUpscale:er(z.handleUpscale),handleTextEdit:er(z.handleTextEdit),handleExpand:er(z.handleExpand),handleRotate:er(z.handleRotate),handleFlip:er(z.handleFlip),handleCrop:er(z.handleCrop)}),[z,er]),eo=h?e?(0,nc.canLayerize)({src:h,generating:V,readOnly:!1,projectId:e})?void 0:"This image can't be separated right now":"This project isn't connected to a canvas yet":"Select an image to separate into layers";return{actions:ea,dims:N,selectionBusy:!!a?.selector&&et.includes(a.selector),mode:F,toggleMode:W,closeMode:U,cropSettings:q,setCropSettings:X,setCropRect:Y,layerizing:V,layerizeDisabledReason:eo,runLayerize:J,textState:null===M?null:{texts:M,edited:B,extracting:!1},setEdited:G,revertAll:(0,f.useCallback)(()=>G([...M??[]]),[M]),applyText:$,readingHeight:(d=N)&&!(d.w<=0)&&!(d.h<=0)?Math.max(200,Math.min(460,Math.round(228*(d.h/d.w)))):200,paywall:w,closePaywall:(0,f.useCallback)(()=>x(!1),[])}}({projectId:n.projectId,documentId:p?.id??null,pageId:r.activePageId,store:i,target:ra,layerizeHtml:ro?void 0:p?.html,onLayers:ri,onImageSrc:rs,onBusyChange:rm}),rg=rf.toggleMode,rb=(0,f.useMemo)(()=>{let e=ra?.src??"";try{return new URL(e,"https://x.invalid").pathname.split("/").pop()||"image.png"}catch{return"image.png"}},[ra?.src]),ry=(0,f.useCallback)(()=>{let e=ra?.src;e&&(0,v.openSaveToLibrary)([{type:"image",url:e,name:rb,...rf.dims?{width:rf.dims.w,height:rf.dims.h}:{}}])},[ra?.src,rb,rf.dims]),rv=(0,f.useCallback)(()=>{let e=ra?.src;e&&rp(e,rb,"image")},[ra?.src,rb,rp]),rw=rf.cropSettings.rect,rx=rf.cropSettings.aspect,rE=ra?.selector??null,rk=rf.dims,rS="crop"===rf.mode;(0,f.useEffect)(()=>{let e=ei.current?.contentWindow;if(e){if(!rS||!rE||!rk||!(rw.w>0)||!(rw.h>0))return void ey(e,{type:"set-crop-frame",rect:null});ey(e,{type:"set-crop-frame",selector:rE,rect:rw,ratio:(0,A.cropRatioOf)(rx,rk.w,rk.h)})}},[rS,rE,rk,rw,rx]);let rT=(0,f.useRef)({setRect:rf.setCropRect,close:rf.closeMode});rT.current={setRect:rf.setCropRect,close:rf.closeMode};let rA=(0,f.useCallback)(()=>{let e=ei.current;if(!e?.contentWindow)return;let t=e.clientWidth||e.offsetWidth;if(t<=0)return;let n=e.getBoundingClientRect().width/t;n>0&&Number.isFinite(n)&&ey(e.contentWindow,{type:"set-scale",scale:n})},[]),[rR,rN]=(0,f.useState)("select"),rC=(0,f.useRef)(rR);rC.current=rR;let rI=(0,f.useCallback)(e=>{ey(ei.current?.contentWindow,{type:"set-tool",tool:e})},[]);(0,f.useEffect)(()=>{rI(rR)},[rR,rI]);let[r_,rO]=(0,f.useState)([]),rP=(0,f.useRef)(r_);rP.current=r_;let[rD,rL]=(0,f.useState)(null),rz=(0,f.useRef)(null),[rF,rH]=(0,f.useState)(!1),[rM,rj]=(0,f.useState)(!1),rB=(0,f.useRef)(rM);rB.current=rM,(0,f.useEffect)(()=>{if(d||h)return void rj(!1);let e=e=>{let t;("Space"===e.code||" "===e.key)&&(t=e.target,t?.closest("input, textarea, select, [contenteditable='true']")||(e.preventDefault(),rj(!0)))},t=e=>{("Space"===e.code||" "===e.key)&&rj(!1)},n=()=>rj(!1);return window.addEventListener("keydown",e),window.addEventListener("keyup",t),window.addEventListener("blur",n),document.addEventListener("visibilitychange",n),()=>{window.removeEventListener("keydown",e),window.removeEventListener("keyup",t),window.removeEventListener("blur",n),document.removeEventListener("visibilitychange",n)}},[d,h]),(0,f.useEffect)(()=>{rz.current=null,rL(null)},[rR]),(0,f.useEffect)(()=>{if(d||h)return;let e=e=>{if(e.metaKey||e.ctrlKey||e.altKey)return;let t=e.target;if(t?.closest("input, textarea, select, [contenteditable='true']"))return;let n=(0,eS.toolForKey)(e.key,e.shiftKey);n?rN(n):"Escape"===e.key&&(rN("select"),rO([]),rz.current=null,rL(null),$(),ed(null),ey(ei.current?.contentWindow,{type:"clear-selection"}))};return window.addEventListener("keydown",e),()=>window.removeEventListener("keydown",e)},[$,d,h]);let rG=(0,f.useMemo)(()=>r.pages.map(e=>e.id),[r.pages]),rW=r.activePageId,rU=r.setActivePageId;(0,f.useEffect)(()=>{if(!eJ||d)return;let e=e=>{!function(e,t){var n,r,a,o;let i,s,l,d,c,h="ArrowDown"===(n=e.key)||"ArrowRight"===n?1:"ArrowUp"===n||"ArrowLeft"===n?-1:null;if(null===h)return;let u=e.target??null,p=t.paneEl?.ownerDocument??u?.ownerDocument??null;if(!((o={...(r=t.paneEl,s=(i=[u,a=p?.activeElement??null].filter(e=>!!e&&"function"==typeof e.closest)).some(e=>!!e.closest('input, textarea, select, [contenteditable="true"]')),l=i.some(e=>!!e.closest('[role="dialog"], [role="alertdialog"], [role="menu"], [role="menubar"], [role="listbox"], [aria-modal="true"]')),d=!!r&&i.some(e=>r.contains(e)),c=a?.ownerDocument??r?.ownerDocument??null,{editable:s,overlay:l,insidePane:d,unscoped:null===a||a===c?.body||a===c?.documentElement}),railVisible:t.railVisible,hasModifier:e.metaKey||e.ctrlKey||e.altKey||e.shiftKey}).railVisible&&!o.hasModifier&&!o.editable&&!o.overlay&&(o.insidePane||o.unscoped)))return;let m=function(e,t,n){if(0===e.length)return null;let r=null===t?-1:e.indexOf(t);if(r<0)return 1===n?e[0]:e[e.length-1];let a=r+n;return a<0||a>=e.length?null:e[a]}(t.pageIds,t.activePageId,h);null!==m&&m!==t.activePageId&&(e.preventDefault(),t.onSelect(m))}(e,{paneEl:eC.current,pageIds:rG,activePageId:rW,onSelect:rU,railVisible:!0})};return window.addEventListener("keydown",e),()=>window.removeEventListener("keydown",e)},[eJ,d,rG,rW,rU]);let rq=(0,f.useCallback)(e=>{let t=window.location.origin;ey(ei.current?.contentWindow,{type:"load-font",href:(0,eU.fontCss2Url)(e,t)})},[]),rX=(0,f.useCallback)(e=>{ew.current=[(0,eU.fontCss2Url)(e)],rq(e),ey(ei.current?.contentWindow,{type:"apply-style",styles:{"font-family":(0,eU.fontFamilyValue)(e)}})},[rq]),rY=(0,f.useCallback)((e,t)=>rs(t,e),[rs]),r$=(0,f.useCallback)((e,t)=>rl(t,e),[rl]),rV=(0,f.useCallback)(()=>rg("crop"),[rg]),rK=(0,f.useCallback)((e,t)=>{let n=ex.current;if(!n||0===e.length)return;let r=(0,nt.patchFramesEnvelope)(n.html,n=>
6151(0,nt.setShapeParamsInDoc)(n,eE.current,e,t));r&&(tv.current=e.map(e=>nX(e)),nM(r,n.title),t4(e=>e+1))},[nM]);(0,f.useLayoutEffect)(()=>{let e=(0,_.guardInspectorWriters)({applyStyle:nZ,previewStyle:nJ,setAttr:nQ,deleteSelection:n0,selectFont:rX,loadFont:rq,replaceImageSrc:rY,openCrop:rV,setImageAlt:r$,setFrameBackground:rd,setFramePreset:rc,reorderComponentInFrame:rh,setShapeParams:rK},()=>rn.current===rr);return n1(e),()=>{_.usePreviewInspector.getState().applyStyle===e.applyStyle&&n1(null)}},[rr,n1,nZ,nJ,nQ,n0,rX,rq,rY,rd,rc,rV,r$,rh,rK]);let rZ=(0,f.useCallback)(e=>{let t=eT.current;if(!t||t.clientWidth<=0||t.clientHeight<=0)return!1;if(e2&&eK){let e=eW?e2.boards.find(e=>eW.frames[e.index]?.id===r.activePageId):void 0;return eB((0,tW.fitToWidth)(e2.width,t.clientWidth,{focusY:e?e.y-z.ARRANGE_HEADER_H:0})),!0}if(e2)return eB((0,tW.fitToView)(e2.width,e2.height,t.clientWidth,t.clientHeight)),!0;let n=e??eI;return!(n<=0)&&(eB((0,tW.fitToView)(eX,n,t.clientWidth,t.clientHeight)),!0)},[eX,eI,e2,eK,eW,r.activePageId,eB]);(0,f.useEffect)(()=>{tx.current||rZ()},[eX]);let rJ=(0,f.useCallback)((e,t,n,r,a,o)=>{tx.current=!0,ej(i=>{if(n){let e=i.panX+r*i.zoom,n=i.panY+a*i.zoom;return(0,tW.zoomToward)(i,(0,tW.wheelZoomFactor)({deltaY:t,...o}),e,n)}return{...i,panX:i.panX-e,panY:i.panY-t}})},[ej]),rQ=(0,f.useCallback)(e=>{let t=eT.current;if(!t)return;tx.current=!0;let n=t.clientWidth/2,r=t.clientHeight/2;eB(t=>(0,tW.zoomToward)(t,e,n,r))},[eB]),r0=(0,f.useCallback)(e=>{let t=eT.current;if(!t)return;tx.current=!0;let n=t.clientWidth/2,r=t.clientHeight/2;eB(t=>(0,tW.zoomToward)(t,e/t.zoom,n,r))},[eB]),r1=(0,f.useCallback)(()=>{tx.current=!1,rZ()},[rZ]);(0,f.useEffect)(()=>{o?.publishZoom(ez.zoom)},[ez.zoom,o]),(0,f.useEffect)(()=>{if(o)return o.controls.current={zoomIn:()=>rQ(1.2),zoomOut:()=>rQ(1/1.2),zoomTo:r0,reset:r1},()=>{o.controls.current=null}},[o,rQ,r0,r1]);let r2=(0,f.useRef)(null),r5=(0,f.useCallback)(()=>{null!==r2.current&&(window.clearTimeout(r2.current),r2.current=null);let e=ei.current;e&&(e.style.pointerEvents="")},[]),r6=(0,f.useCallback)(()=>{let e=ei.current;e&&(e.style.pointerEvents="none"),null!==r2.current&&window.clearTimeout(r2.current),r2.current=window.setTimeout(r5,tW.WHEEL_GESTURE_IDLE_MS)},[r5]);(0,f.useEffect)(()=>{let e=()=>{null!==r2.current&&r5()};return window.addEventListener("mousemove",e),()=>{window.removeEventListener("mousemove",e),r5()}},[r5]);let r4=(0,f.useCallback)(e=>{let t=eT.current;if(!t||Q.current)return;e.preventDefault(),r6();let n=t.getBoundingClientRect(),r=e.clientX-n.left,a=e.clientY-n.top;if(tx.current=!0,e.ctrlKey||e.metaKey){let t=(0,tW.wheelZoomFactor)({deltaY:e.deltaY,...(0,tW.wheelKind)(e)});ej(e=>(0,tW.zoomToward)(e,t,r,a))}else ej(t=>({...t,panX:t.panX-e.deltaX,panY:t.panY-e.deltaY}))},[ej,r6]);(0,f.useEffect)(()=>{if(!eA||h)return;let e=e=>r4(e);return eA.addEventListener("wheel",e,{passive:!1}),()=>eA.removeEventListener("wheel",e)},[eA,r4,h]);let r3=(0,f.useRef)(null),r8=(0,f.useCallback)(()=>{rO([]),$(),ed(null),ey(ei.current?.contentWindow,{type:"clear-selection"})},[$]),r7=(0,f.useCallback)(e=>{r8(),r.setActivePageId(e),rO([e])},[r8,r.setActivePageId]),r9=(0,f.useCallback)((e,t)=>{tw.current=t?{frameId:e,x:t.x,y:t.y}:null,r.setActivePageId(e)},[r.setActivePageId]),ae=(0,f.useCallback)(e=>{if(!u.current)return e;let t=eT.current;if(!t)return e;let n=td.current,r=n?{width:n.width,height:n.height}:{width:eY.current,height:eP.current},a=(0,tW.clampPanToViewport)(e,r,{width:t.clientWidth,height:t.clientHeight}),o=eH.current;return a.panX===o.panX&&a.panY===o.panY&&a.zoom===o.zoom?o:a},[]),at=(0,f.useCallback)((e,t="mouse")=>{tx.current=!0;let n={x:e.clientX,y:e.clientY,panX:eH.current.panX,panY:eH.current.panY};r3.current=n,rH(!0);let r=!1,a="pointer"===t?"pointermove":"mousemove",o="pointer"===t?"pointerup":"mouseup",i=e=>{!r&&(Math.abs(e.clientX-n.x)>3||Math.abs(e.clientY-n.y)>3)&&(r=!0),ej(t=>ae({...t,panX:n.panX+(e.clientX-n.x),panY:n.panY+(e.clientY-n.y)}))},s=()=>{r3.current=null,rH(!1),window.removeEventListener(a,i),window.removeEventListener(o,s),"pointer"===t&&window.removeEventListener("pointercancel",s),document.body.style.cursor=""};window.addEventListener(a,i),window.addEventListener(o,s),"pointer"===t&&window.addEventListener("pointercancel",s),document.body.style.cursor="grabbing"},[ej,ae]),an=(0,f.useCallback)(e=>{let t=eT.current;if(!t)return;let n=t.getBoundingClientRect(),r=e=>({x:e.clientX-n.left,y:e.clientY-n.top}),a=r(e),o={from:a,to:a};rz.current=o,rL(o);let i=e=>{let t={from:a,to:r(e)};rz.current=t,rL(t)},s=e=>{window.removeEventListener("mousemove",i),window.removeEventListener("mouseup",s),rz.current=null,rL(null);let t=function(e){var t;let n;if(!(0,tM.isDrag)(e.to.x-e.from.x,e.to.y-e.from.y))return{kind:"deselect"};let r=function(e,t,n){if(!Number.isFinite(n.zoom)||n.zoom<=0)return null;let r=(0,tM.marqueeRect)(e.x,e.y,t.x,t.y);return{left:(r.left-n.panX)/n.zoom,top:(r.top-n.panY)/n.zoom,right:(r.right-n.panX)/n.zoom,bottom:(r.bottom-n.panY)/n.zoom}}(e.from,e.to,e.view);return r?{kind:"select",frameIds:(t=e.boards,n=(0,tM.marqueeSelection)(r,t.map(e=>({key:e.frameId,rect:{left:e.x,top:e.y,right:e.x+e.w,bottom:e.y+e.h}}))),t.slice().sort((e,t)=>
6151e.index-t.index).filter(e=>n.has(e.frameId)).map(e=>e.frameId))}:{kind:"deselect"}}({from:a,to:r(e),view:eH.current,boards:td.current?.boards??[]});if("deselect"===t.kind){"deselect"===(0,eS.floorClickIntent)(rC.current,{space:rB.current})&&r8();return}rO(t.frameIds)};window.addEventListener("mousemove",i),window.addEventListener("mouseup",s)},[r8]),ar=(0,f.useCallback)(e=>{let t=1===e.button;if(0!==e.button&&!t||Q.current)return;let n=e.target;n?.closest("button, a, input, textarea, select, [role='button'], iframe")||(e.preventDefault(),"pan"===(0,eS.floorGestureFor)(rC.current,{space:rB.current,middleButton:t})?at(e):an(e))},[at,an]),aa=(0,f.useCallback)(e=>{if(0!==e.button&&"mouse"===e.pointerType||Q.current)return;let t=e.target;if(!t?.closest("button, a, input, textarea, select, [role='button']")&&"hand"===rC.current){e.preventDefault();try{e.currentTarget.setPointerCapture?.(e.pointerId)}catch{}at(e,"pointer")}},[at]),[ao,ai]=(0,f.useState)("rectangle"),as=(0,f.useCallback)((e,t)=>{ef.includes(e)&&ai(e),ey(ei.current?.contentWindow,{type:"insert",kind:e,text:t})},[]),al=(0,f.useCallback)(e=>{ey(ei.current?.contentWindow,{type:"insert",kind:"image",text:e})},[]),ad=(0,f.useCallback)(()=>{let e=tv.current;e&&0!==e.length&&(tv.current=null,ey(ei.current?.contentWindow,{type:"reselect",selectors:e}))},[]),ac=(0,f.useCallback)((e,t)=>{let n=eT.current;if(!n)return null;let r=n.getBoundingClientRect(),a={x:e-r.left,y:t-r.top},o=eH.current,i=td.current;if(i)return function(e,t,n){let r=tR(e,n);for(let e=t.length-1;e>=0;e--){let a=t[e];if(r.x>=a.x&&r.x<=a.x+a.w&&r.y>=a.y&&r.y<=a.y+a.h)return{frameId:a.frameId,index:a.index,xPct:tA((r.x-a.x)/a.w*100),yPct:tA((r.y-a.y)/a.h*100),screen:{x:n.panX+a.x*n.zoom,y:n.panY+a.y*n.zoom,w:a.w*n.zoom,h:a.h*n.zoom}}}return null}(a,i.boards,o);let s=eE.current??ex.current?.id??"legacy";return function(e,t,n,r,a){if(r<=0||n<=0)return null;let o=tR(e,t);return o.x<0||o.x>n||o.y<0||o.y>r?null:{frameId:a,index:0,xPct:tA(o.x/n*100),yPct:tA(o.y/r*100),screen:{x:t.panX,y:t.panY,w:n*t.zoom,h:r*t.zoom}}}(a,o,eY.current,eP.current,s)},[]),ah=(0,f.useRef)(null),au=(0,f.useCallback)(e=>{ah.current={x:e.clientX,y:e.clientY}},[]),ap=(0,f.useCallback)(()=>{ah.current=null},[]),am=(0,f.useCallback)(()=>{let e=ah.current;if(!e)return null;let t=ac(e.x,e.y);return t&&t.frameId!==eE.current?t:null},[ac]),af=(0,f.useCallback)(e=>{if(!e)return null;let t=tc.current,n=eE.current??t?.frames[0]?.id??null,r=n?t?.frames.find(e=>e.id===n):null;if(!r||!n)return null;let a=function(e,t,n){if(!e||!(t>0)||!(n>0)||!Number.isFinite(e.x)||!Number.isFinite(e.y))return null;let r=e.x/t*100,a=e.y/n*100;return r<0||r>100||a<0||a>100?null:{xPct:r,yPct:a}}(e,(0,tN.resolvePreset)(r.preset).width,(0,tN.frameHeight)(r));return a?{frameId:n,...a}:null},[]),ag=(0,f.useCallback)((e,t)=>{let n=ex.current;if(!n||!(0,tN.looksLikeFramesEnvelope)(n.html))return null;let{url:a}=e,o=tc.current,i=(t&&o?.frames.some(e=>e.id===t.frameId)?t.frameId:null)??eE.current??o?.frames[0]?.id??null,s=o?.frames.find(e=>e.id===i);if(!s||!i)return null;let l=function(e,t,n,r,a,o,i=.4){let s=100*i,l=r>0?s/100*n/(a&&o&&a>0&&o>0?a/o:4/3)/r*100:3/4*s;return{x:e-s/2,y:t-l/2,w:s,h:l}}(t&&t.frameId===i?t.xPct:50,t&&t.frameId===i?t.yPct:50,(0,tN.resolvePreset)(s.preset).width,(0,tN.frameHeight)(s),e.width,e.height),d=(0,tN.freshCid)("u"),c=(0,nt.patchFramesEnvelope)(n.html,e=>(0,nt.appendComponentToDoc)(e,i,{type:"image",src:a,fit:"contain",cid:d,box:l}));return c?(r.setActivePageId(i),tv.current=[nX(d)],nM(c,n.title),t4(e=>e+1),{cid:d,frameId:i}):null},[nM,r.setActivePageId]),ab=(0,f.useCallback)(async(e,t)=>{let n=ex.current;if(!n)return;if(!(0,tN.looksLikeFramesEnvelope)(n.html))return void al(e.url);let{width:r,height:a}=e;if(null==r||null==a){var o;let t=await (o=e.url,new Promise(e=>{let t=new Image;t.onload=()=>e({w:t.naturalWidth,h:t.naturalHeight}),t.onerror=()=>e(null),t.src=o}));r=t?.w,a=t?.h}ag({url:e.url,width:r,height:a},t)},[ag,al]);(0,f.useEffect)(()=>{let e=e=>{let t=e.detail;if(t&&"preview"===t.view)for(let e of t.assets)"image"===e.type&&e.url&&ab(e,null)};return window.addEventListener(P.TAB_ASSET_LANDING_EVENT,e),()=>window.removeEventListener(P.TAB_ASSET_LANDING_EVENT,e)},[ab]);let ay=(0,f.useRef)(new Map),av=(0,f.useRef)(new Map),[aw,ax]=(0,f.useState)([]),aE=(0,f.useCallback)((e,t)=>{t?av.current.set(e,t):av.current.delete(e),ax([...av.current.keys()]),ey(ei.current?.contentWindow,{type:"set-uploading",cid:e,label:t})},[]),ak=(0,f.useCallback)(()=>{for(let[e,t]of av.current)ey(ei.current?.contentWindow,{type:"set-uploading",cid:e,label:t})},[]);(0,f.useEffect)(()=>()=>{for(let e of av.current.keys())ey(ei.current?.contentWindow,{type:"set-uploading",cid:e,label:null});av.current.clear()},[]);let aS=(0,f.useCallback)((e,t)=>{let n=ay.current.get(e)??null;ay.current.delete(e);let r=ex.current;if(!r)return;let a=nX(e),o=(0,nt.patchFramesEnvelope)(r.html,e=>null===t?(0,nt.removeComponentsFromDoc)(e,n,[a]):(0,nt.applyImageSrcToDoc)(e,n,a,t));if(!o)return;if(M({title:r.title,html:o},{amend:!0}),nH.current=!0,null===t)return void t4(e=>e+1);let i={cid:e,src:t},s=nR.current;nR.current={nonce:nN.current,patches:s&&s.nonce===nN.current?[...s.patches,i]:[i]},ey(ei.current?.contentWindow,{type:"patch-assets",patches:[i]})},[M]),aT=(0,f.useCallback)(t=>({measure:t7,upload:async t=>{let{uploadFileToBlitz:n}=await e.A(569286);return n(t)},insert:e=>{let n=ag({url:"",width:e?.w,height:e?.h},t);return n?(ay.current.set(n.cid,n.frameId),n.cid):null},setUploading:(e,t)=>
6151aE(e,t?eo("dropUploading"):null),swap:(e,t)=>aS(e,t),rollback:e=>aS(e,null),onUploadFailed:e=>{e?.code!=="AUTH_REQUIRED"&&(0,g.toast)("Couldn't upload that image")}}),[ag,aE,aS,eo]),aA=(0,f.useCallback)(async(t,n)=>{if("components"===t.kind){let e=ex.current;if(!e||d||c||t2.current||!nk.current)return;let a=n?.frameId??eE.current;if(!a)return;let o=[],i=(0,nt.patchFramesEnvelope)(e.html,e=>{let r=(0,nt.pasteComponentsIntoFrame)(e,a,t.components,t.sourceSize,n);return r?(o=r.cids,r.doc):null});return i?(r.setActivePageId(a),tv.current=o.map(nX),nM(i,e.title),void t4(e=>e+1)):void g.toast.error("Couldn't paste these layers onto this page.")}if("text"===t.kind)return void as("text",t.text);let a=(0,x.sortDroppedFiles)("preview",t.files);for(let e of a.rejected)g.toast.error(tp(e.file.name,e.reason));let o=a.accepted.slice(0,4).map(e=>e.file);if(0!==o.length){if(!(0,tN.looksLikeFramesEnvelope)(ex.current?.html??"")){for(let t of o)try{let{uploadFileToBlitz:r}=await e.A(569286),a=await r(t);await ab({url:a},n)}catch(e){if(e?.code==="AUTH_REQUIRED")return;(0,g.toast)("Couldn't upload that image")}return}await t9(o,aT(n))}},[ab,as,aT,tp,d,c,r.setActivePageId,nM]),aR=(0,f.useCallback)((e,t)=>{if(0===e.length)return;let n=ex.current;n&&(!(0,tN.looksLikeFramesEnvelope)(n.html)||(tc.current?.frames.length??0)>0)?aA({kind:"images",files:[...e]},t):(0,g.toast)("Add a page first, then drop the image onto it")},[aA]),aN=(0,f.useCallback)((e,t)=>{let n=e.find(e=>e.url),a=ex.current;if(!n||!a)return;let o="page"===t.landing.kind?t.landing:null,i=(0,tZ.insertGeneratedImage)(a.html,n.url,{frameId:o?.frameId??eE.current??void 0,slot:o?.slot,fileId:n.id});i?(M({title:a.title,html:i.html,assetRefs:(0,tZ.withAssetRefs)(a.assetRefs,i.assetRefs)},{editPage:i.placement.frameId}),r.setActivePageId(i.placement.frameId),t4(e=>e+1),tv.current=[nX(i.placement.slot)]):al(n.url)},[M,al,r.setActivePageId]),aC=(0,f.useCallback)(e=>"page"===e.landing.kind&&e.landing.docId===ex.current?.id,[]);(0,C.useFileTrackLanding)({projectId:n.projectId??null,accepts:aC,onComplete:aN,onEmpty:()=>g.toast.error(ea("generateHereNoOutput")),onError:e=>g.toast.error(e)});let aI=(0,f.useSyncExternalStore)(N.subscribePendingGenerations,N.pendingGenerations,()=>nq),a_=(0,f.useMemo)(()=>(function(e,t,n){if(!t||!n)return[];let r=[],a=new Set;for(let o of e){if("running"!==o.status||"page"!==o.landing.kind||o.landing.docId!==t)continue;let e=o.landing.slot;!(!e||a.has(e))&&function(e,t,n){for(let r of e.frames)if(void 0===n||r.id===n){for(let e of r.components)if(e.cid===t)return"image"===e.type&&""===e.src}return!1}(n,e,o.landing.frameId)&&(a.add(e),r.push(e))}return r})(aI,p?.id??null,r.doc??null).map(nX),[aI,p?.id,r.doc]),aO=a_.join("|"),aP=n9?.frames.find(e=>e.id===re)??null,aD=new Set(aP?.components.map(e=>e.cid)),aL=[...a_.map(nt.cidFromSelector),...nC.current.pageId===re?nI.map(nt.cidFromSelector):[],...aw].filter(e=>!!e&&aD.has(e)),az=d||c||B||!nE,aF=(0,f.useRef)(az);aF.current=az,nO.current=az||aL.length>0;let aH=(0,f.useCallback)((e,t,n=!1)=>{let r=ex.current;if(!r||!e||e===r.html)return;let a=(0,tG.planScopedApplyFromHtml)(r.html,e),o=a?.frames.find(e=>e.frameId===eE.current);if(nM(e,r.title,o?.ops),a){for(let e of o?.ops.redo??[])ey(ei.current?.contentWindow,{type:"set-html",...e});n||ey(ei.current?.contentWindow,{type:"reselect",selectors:[nX(t)]})}else tv.current=n?null:[nX(t)],t4(e=>e+1);n&&n5()},[nM,n5]),aM=(0,f.useCallback)((e,t)=>{let n=ex.current;n&&!nO.current&&aH((0,nt.patchFramesEnvelope)(n.html,n=>(0,nt.renameComponentInFrame)(n,eE.current,e,t)),e)},[aH]),aj=(0,f.useCallback)((e,t)=>{let n=ex.current;if(!n||aF.current||!Array.isArray(e)||!e.length||e.length>128||e.some(e=>"string"!=typeof e)||new Set(e).size!==e.length)return!1;try{let r=(0,tN.parseFramesEnvelope)(JSON.parse(n.html)),a=r?.frames.find(e=>e.id===eE.current);if(!a)return!1;let o=a.components.filter(t=>e.includes(t.cid));if(o.length!==e.length)return!1;let i={width:(0,tN.resolvePreset)(a.preset).width,height:(0,tN.frameHeight)(a)};if(t){let e=(0,tF.serializeDesignComponents)(o,i);return t.setData("text/html",e.html),t.setData("text/plain",e.text),!0}return(0,tF.copyDesignComponents)(o,i).catch(()=>g.toast.error("Couldn't copy these layers. Allow clipboard access and try again.")),!0}catch{return!1}},[]),aB=(0,f.useCallback)(async(e,t)=>{let n,r=ex.current;if(!r||aF.current||"copy"!==t&&nO.current)return;let a=eE.current;try{n=(0,tN.parseFramesEnvelope)(JSON.parse(r.html))}catch{return}if(n?.frames.find(e=>e.id===a)?.components.find(t=>t.cid===e)){if("front"===t||"back"===t)return void rh(e,"front"===t?"top":"bottom");if("delete"===t)return void aH((0,nt.patchFramesEnvelope)(r.html,t=>(0,nt.removeComponentsFromDoc)(t,a,[nX(e)])),e,!0);if("copy"===t)return void aj([e])}},[rh,aH,aj]),aG=(0,f.useRef)([]);aG.current=a_;let aW=(0,f.useCallback)(()=>{ey(ei.current?.contentWindow,{type:"set-pending",selectors:aG.current})},[]);(0,f.useEffect)(()=>{aW()},[aO,aW]),(0,f.useEffect)(()=>()=>{ey(ei.current?.contentWindow,{type:"set-pending",selectors:[]})},[]);let aU=(0,f.useCallback)(e=>{e.preventDefault(),e.stopPropagation(),tu(!1),ty(null),tf("none");let t=e.dataTransfer,n=ac(e.clientX,e.clientY),r=(0,R.readPonponFilePayload)(t);if(r)return(0,R.isImagePayload)(r)?void ab({url:r.url,width:r.width,height:r.height},n):void(0,g.toast)("Only images can be placed in a frame",{description:`"${r.filename||"That file"}" is ${(0,R.fileModality)(r)??"not an image"} — drop it on the canvas instead.`});let a=t.getData(I.DRAG_ASSET_MIME);if(a){let e=(0,S.parseLibraryAssetDrop)(a).find(e=>"image"===e.type);return e?void ab(e,n):void(0,g.toast)("Only images can be placed in a frame")}aR(Array.from(t.files||[]),n)},[ac,ab,aR]),aq=(0,f.useCallback)(e=>{let t=e.dataTransfer;if(!(0,R.dragHasFilePayload)(t)&&!t.types.includes(I.DRAG_ASSET_MIME))return;e.preventDefault();let n=(0,x.dragCarriesFiles)(t.types)?(0,x.dragVerdict)("preview",(0,x.draggedKinds)(t.items)):"none";tf(n),t.dropEffect="reject"===n?"none":"copy",tu(!0);let r=ac(e.clientX,e.clientY);ty(r&&"reject"!==n?r.screen:null)},[ac]);(0,f.useEffect)(()=>
6151{if(d||c)return;let e=e=>{(function(e){if(!e)return!1;for(let t of Array.from(e))if(t===R.FILE_DRAG_MIME||"Files"===t||t===I.DRAG_ASSET_MIME)return!0;return!1})(e.dataTransfer?.types)&&tu(!0)},t=()=>{tu(!1),ty(null),tf("none")};return window.addEventListener("dragstart",e),window.addEventListener("dragenter",e),window.addEventListener("dragend",t),window.addEventListener("drop",t),()=>{window.removeEventListener("dragstart",e),window.removeEventListener("dragenter",e),window.removeEventListener("dragend",t),window.removeEventListener("drop",t)}},[d,c]);let aX=(0,f.useCallback)(e=>{e.defaultPrevented||(0,x.dragCarriesFiles)(e.dataTransfer?.types)&&(e.preventDefault(),e.dataTransfer.dropEffect="copy")},[]),aY=(0,f.useCallback)(e=>{e.defaultPrevented||!(0,x.dragCarriesFiles)(e.dataTransfer?.types)||(e.preventDefault(),c||aR(Array.from(e.dataTransfer.files||[]),null))},[c,aR]),a$=(0,f.useCallback)(e=>{let t=e.relatedTarget;t&&e.currentTarget.contains(t)||(tu(!1),ty(null),tf("none"))},[]),aV=r.setActivePageId,aK=(0,tU.canAddPage)(p),aZ=(0,f.useCallback)(()=>{let e=ex.current;if(!e)return;let t=(0,tU.addPageAfterActive)(e,eE.current);t?(nM(t.html,e.title),aV(t.newPageId)):(0,g.toast)("Add a first page from chat",{description:"Ask Sunra to author the composition, then add pages here."})},[nM,aV]),aJ=(0,f.useCallback)(e=>{if(!Q.current)return;let t=ex.current,{html:n,newPageId:r,mode:a}=e?(0,tU.firstPageAtFormat)(t,e):(0,tU.firstPageWrite)(t,tC);"self-edit"===a&&t?nM(n,t.title):M({title:"Untitled",html:n},{immediate:!0}),aV(r)},[M,nM,aV]),aQ=(0,f.useCallback)(()=>aJ(),[aJ]),a0=(0,f.useCallback)(e=>{let t=ex.current;if(!t)return;let n=(0,tU.addPageAfterIndex)(t,e);n&&(nM(n.html,t.title),aV(n.newPageId))},[nM,aV]),a1=(0,f.useCallback)((e,t)=>{let n=ex.current;if(!n)return;let r=function(e,t,n){let r=n.trim();if(!r)return null;if((0,tN.looksLikeFramesEnvelope)(e.html)){let n=tX(e.html);if(!n)return null;let a=n.frames.findIndex(e=>e.id===t);if(-1===a)return null;let o=n.frames.slice();return o[a]={...o[a],title:r},{html:(0,tN.serializeFramesEnvelope)({...n,frames:o}),activePageId:t}}let a=tY(e);if(!a)return null;let o=a.findIndex(e=>e.id===t);if(-1===o)return null;let i=a.slice();return i[o]={...i[o],title:r},{html:t$(i,t),activePageId:t}}(n,e,t);r&&nM(r.html,n.title)},[nM]),a2=(0,f.useCallback)(e=>{let t=rP.current;return t.includes(e)?t:[e]},[]),a5=(0,f.useCallback)(e=>{let t=ex.current;if(!t)return;let n=t.html,r=null;for(let a of a2(e)){let e=function(e,t,n=Date.now()){if((0,tN.looksLikeFramesEnvelope)(e.html)){var r;let n,a=tX(e.html);if(!a)return null;let o=a.frames.findIndex(e=>e.id===t);if(-1===o)return null;let i=(n=(r=a.frames[o]).components.map(e=>({...e,cid:(0,tN.freshCid)(),box:{...e.box}})),{...r,id:(0,tN.freshCid)("f"),background:"image"===r.background.type?{type:"image",src:r.background.src,fit:r.background.fit}:{type:"color",color:r.background.color},components:n}),s=a.frames.slice();return s.splice(o+1,0,i),{html:(0,tN.serializeFramesEnvelope)({...a,frames:s}),activePageId:i.id}}let a=tY(e);if(!a)return null;let o=a.findIndex(e=>e.id===t);if(-1===o)return null;let i=function(e,t){let n=`page-${t}`,r=2;for(;e.has(n);)n=`page-${t}-${r++}`;return n}(new Set(a.map(e=>e.id)),n.toString(36)),s={id:i,title:a[o].title,html:a[o].html},l=a.slice();return l.splice(o+1,0,s),{html:t$(l,i),activePageId:i}}({...t,html:n},a);e&&(n=e.html,r=e.activePageId)}null!==r&&(nM(n,t.title),aV(r),rO([]),t4(e=>e+1))},[nM,aV,a2]),a6=(0,f.useCallback)(e=>{let t=ex.current;if(!t)return;let n=a2(e);if(1===n.length&&"empty"===tV(t,n[0]??e)){let e=function(e,t){if((0,tN.looksLikeFramesEnvelope)(e.html)){let t=tX(e.html);return t?{html:(0,tN.serializeFramesEnvelope)({...t,frames:[]}),activePageId:null}:null}return 0===(0,tq.parseCompositionPages)(e).pages.length?null:{html:(0,tN.serializeFramesEnvelope)({format:t,frames:[]}),activePageId:null}}(t,tC);if(!e)return;nM(e.html,t.title),aV(null),rO([]),t4(e=>e+1);return}let r=t.html,a=null;for(let e of n){let n=function(e,t){if((0,tN.looksLikeFramesEnvelope)(e.html)){let n=tX(e.html);if(!n||n.frames.length<=1)return null;let r=n.frames.findIndex(e=>e.id===t);if(-1===r)return null;let a=n.frames.slice();a.splice(r,1);let o=a[Math.max(0,r-1)].id;return{html:(0,tN.serializeFramesEnvelope)({...n,frames:a}),activePageId:o}}let n=tY(e);
6151if(!n||n.length<=1)return null;let r=n.findIndex(e=>e.id===t);if(-1===r)return null;let a=n.slice();a.splice(r,1);let o=a[Math.max(0,r-1)].id;return{html:t$(a,o),activePageId:o}}({...t,html:r},e);n&&(r=n.html,a=n.activePageId)}null!==a&&(nM(r,t.title),aV(a),rO([]),t4(e=>e+1))},[nM,aV,a2]),a4=(0,f.useCallback)((e,t)=>{let n=ex.current;if(!n)return;let r=function(e,t,n){let r=Math.trunc(n);if(0===r)return null;if((0,tN.looksLikeFramesEnvelope)(e.html)){let n=tX(e.html);if(!n)return null;let a=n.frames.findIndex(e=>e.id===t);if(-1===a)return null;let o=a+r;if(o<0||o>=n.frames.length)return null;let i=n.frames.slice(),[s]=i.splice(a,1);return i.splice(o,0,s),{html:(0,tN.serializeFramesEnvelope)({...n,frames:i}),activePageId:s.id}}let a=tY(e);if(!a)return null;let o=a.findIndex(e=>e.id===t);if(-1===o)return null;let i=o+r;if(i<0||i>=a.length)return null;let s=a.slice(),[l]=s.splice(o,1);return s.splice(i,0,l),{html:t$(s,l.id),activePageId:l.id}}(n,e,t);r&&(nM(r.html,n.title),aV(r.activePageId),t4(e=>e+1))},[nM,aV]),a3=(0,f.useCallback)(e=>{(0,tI.dispatchComposeDownloadPage)({pageId:e})},[]),a8="refuse"!==tV(p,r.activePageId??""),a7=(0,f.useMemo)(()=>({onRename:a1,onDuplicate:a5,onDownload:a3,onDelete:a6,canDelete:a8}),[a1,a5,a3,a6,a8]),a9=(0,f.useMemo)(()=>({onMove:a4,onDuplicate:a5,onDelete:a6,canDelete:a8}),[a4,a5,a6,a8]),oe=r.pages.findIndex(e=>e.id===eQ),ot=oe>=0?r.pages[oe]:null,on=(0,tj.placePageRow)({boardTop:ez.panY,prevBottom:0,pageWidth:eX*ez.zoom,rowHeight:z.PAGE_ROW_H,rowWidth:z.PAGE_ROW_MIN_W}),or=(0,f.useCallback)(()=>{let e=ex.current;if(!e)return;let t=(0,tU.addSectionToActive)(e,eE.current);t&&(nM(t.html,e.title),t4(e=>e+1))},[nM]),oa=(0,f.useMemo)(()=>{let e=r.doc;if(!e)return!1;let t=e.frames.find(e=>e.id===r.activePageId)??e.frames[0];return!!t&&(0,tN.isGrowableFormat)(t.preset)},[r.doc,r.activePageId]);(0,f.useEffect)(()=>{let e=e=>{let t=e.detail?.format;p&&("pdf"===t?t0.trim()&&function(e){try{let t=document.createElement("iframe");t.setAttribute("sandbox","allow-same-origin allow-modals"),t.style.position="fixed",t.style.right="0",t.style.bottom="0",t.style.width="0",t.style.height="0",t.style.border="0",t.style.opacity="0",t.srcdoc=e,t.onload=()=>{try{t.contentWindow?.focus(),t.contentWindow?.print()}catch{(0,g.toast)("PDF export needs a print-capable window",{description:"Server-side PDF rendering isn't wired up yet."})}window.setTimeout(()=>t.remove(),1e3)},document.body.appendChild(t)}catch{(0,g.toast)("Couldn't open the print dialog")}}(t0):"png"===t&&(0,g.toast)("PNG export queued",{description:"Rasterizing to PNG runs server-side — not wired up yet."}))};return window.addEventListener(tI.COMPOSE_EXPORT_EVENT,e),()=>window.removeEventListener(tI.COMPOSE_EXPORT_EVENT,e)},[p,t0]),(0,f.useEffect)(()=>{let e=e=>{var t;if(ei.current&&e.source!==ei.current.contentWindow||!((t=e.data)&&"object"==typeof t&&t.source===ep&&"string"==typeof t.type&&ev.has(t.type)))return;let n=e.data;if("copy"!==n.type&&"paste"!==n.type||ei.current&&e.source===ei.current.contentWindow&&!d&&!c&&!aF.current){if("selection-rect"===n.type){eg(n.gesture||!!n.loading),n.rect&&eh(n.rect);return}if("selection-style"===n.type)return void n4({selector:n.selector,style:n.style,rotation:n.rotation});if("selection"===n.type){if(!p)return;let e=n.elements;if(rO(e=>e.length?[]:e),0===e.length){$(),ed(null),eh(null),n5();return}ed(function(e){let t=e[0];if(!t?.meta)return null;let n=e.length>1;return{count:e.length,text:e.every(e=>!!e.meta?.text),fill:n?e.every(e=>!!(e.meta?.fill||e.meta?.text)):!!t.meta.fill,image:!n&&!!t.meta.image,root:!!t.meta.root,src:t.meta.src,selectors:e.map(e=>e.selector),style:t.meta.style}}(e)),n2((0,_.selectionFromPickerElements)(e),r.activePageId);let t=Math.max(200,Math.floor(2e3/e.length)),a=e.map((e,n)=>`<!-- selection ${n+1}: ${e.selector} -->
6152${e.snippet.slice(0,t)}`).join("\n\n").slice(0,2e3);Y({type:"compose_element_ref",title:p.title,html:p.html,selector:e.map(e=>e.selector).join(", "),snippet:a,tag:e[0].tag,text:e[0].text,alt:e[0].alt,count:e.length})}else if("text-commit"===n.type){if(!p)return;if((0,tN.looksLikeFramesEnvelope)(p.html)){let e=(0,nt.patchFramesEnvelope)(p.html,e=>(0,nt.applyTextCommitToDoc)(e,r.activePageId,n.selector,n.html));e&&nM(e,p.title,nV(e,[n.selector],"text"));return}let e=(0,tL.applyPageAwarePatch)(p.html,r.activePageId,e=>(function(e,t,n){let r,a=new DOMParser().parseFromString(e,"text/html");try{r=a.querySelectorAll(t)}catch{return console.warn("[compose] text edit: invalid selector, aborting —",t),null}return 1!==r.length?(console.warn(`[compose] text edit: selector matched ${r.length} elements (need exactly 1), aborting —`,t),null):(r[0].innerHTML=function(e){let t=document.createElement("template");for(let n of(t.innerHTML=e,Array.from(t.content.querySelectorAll("script"))))n.remove();for(let e of Array.from(t.content.querySelectorAll("*")))for(let t of Array.from(e.attributes))/^on/i.test(t.name)?e.removeAttribute(t.name):("href"===t.name||"src"===t.name)&&/^\s*javascript:/i.test(t.value)&&e.removeAttribute(t.name);return t.content.firstChild?t.innerHTML:e}(n),`<!doctype html>
6153${a.documentElement.outerHTML}`)})(e,n.selector,n.html));e&&nM(e,p.title,nV(e,[n.selector],"text"))}else if("style-commit"===n.type){if(!p)return;if((0,tN.looksLikeFramesEnvelope)(p.html)){let e=(0,nt.patchFramesEnvelope)(p.html,e=>(0,nt.applyStyleCommitToDoc)(e,r.activePageId,n.patches));ew.current=[],e&&nM(e,p.title,nV(e,nW(n.patches),"outer"));return}let e=ew.current,t=(0,tL.applyPageAwarePatch)(p.html,r.activePageId,t=>{let r=(0,tL.patchCompositionStyles)(t,n.patches);if(null==r)return null;for(let t of e)r=(0,tL.ensureFontLink)(r,t);return r});if(null==t)return;ew.current=[],nM(t,p.title,nV(t,nW(n.patches),"outer"))}else if("insert-commit"===n.type){if(!p)return;if((0,tN.looksLikeFramesEnvelope)(p.html)){let e=function(e){let t=e.match(/^\s*<([a-zA-Z0-9]+)/);if(!t)return null;let n=t[1].toLowerCase(),r=nn(e,"data-cid")??void 0,a=(0,nt.parseStyleStr
6153ing)(nn(e,"style")??""),o=function(e){let t={};for(let n of["left","top","width","height"]){let r=e[n];r&&/^-?[\d.]+(px)?$/.test(r.trim())&&(t[n]=r.trim())}return Object.keys(t).length>0?t:void 0}(a),i=o?{[nt.INSERT_PX_STYLE]:o}:{};if("svg"===n){let t=nn(e,"data-shape");if(!t)return null;let n=t=>{let n=nn(e,t);if(null==n)return;let r=Number.parseFloat(n);return Number.isFinite(r)?r:void 0},o=Number.parseFloat(a["stroke-width"]??""),s=a.fill&&"none"!==a.fill?a.fill:void 0;return{type:"shape",shape:t,cid:r,...s?{fill:s}:{},...a.stroke&&"none"!==a.stroke?{borderColor:a.stroke,borderWidth:Number.isFinite(o)?o:0}:{},...null!=n("data-sides")?{sides:n("data-sides")}:{},...null!=n("data-points")?{points:n("data-points")}:{},...null!=n("data-inner-ratio")?{innerRatio:n("data-inner-ratio")}:{},...i}}if("img"===n)return{type:"image",cid:r,src:nn(e,"src")??"",fit:"contain",...i};if("div"===n){let e=a.background??a["background-color"]??"#000000",t=a["border-radius"]??"";if(t.includes("50%"))return{type:"shape",shape:"ellipse",cid:r,fill:e,...i};let n=Number.parseFloat(a.height??"0");if(Number.isFinite(n)&&n>0&&n<=6)return{type:"shape",shape:"line",cid:r,fill:e,borderColor:e,...i};let o=Number.parseFloat(t);return{type:"shape",shape:"rect",cid:r,fill:e,radius:Number.isFinite(o)?o:0,...i}}let s=e.replace(/^\s*<[^>]+>/,"").replace(/<\/[^>]+>\s*$/,""),l={type:"text",cid:r,content:(0,nt.innerHtmlToText)(s),...i},d=a["font-family"];d&&(l.family=d.split(",")[0].trim().replace(/^['"]|['"]$/g,""));let c=Number.parseFloat(a["font-size"]??"");Number.isFinite(c)&&(l.size=c);let h=Number.parseInt(a["font-weight"]??"",10);return Number.isFinite(h)&&(l.weight=h),a.color&&(l.color=a.color),l}(n.html);if(e){let t=(0,nt.patchFramesEnvelope)(p.html,t=>(0,nt.appendComponentToDoc)(t,r.activePageId,e));t&&nM(t,p.title,nV(t,[n.selector],"outer"))}return}let e=(0,tL.applyPageAwarePatch)(p.html,r.activePageId,e=>(0,tL.appendToRootSheet)(e,n.html));e&&nM(e,p.title,nV(e,[n.selector],"outer"))}else if("remove"===n.type){if(!p)return;if((0,tN.looksLikeFramesEnvelope)(p.html)){let e=(0,nt.patchFramesEnvelope)(p.html,e=>(0,nt.removeComponentsFromDoc)(e,r.activePageId,n.selectors));e&&nM(e,p.title,nV(e,n.selectors,"outer"));return}let e=n.spacers??[],t=(0,tL.applyPageAwarePatch)(p.html,r.activePageId,t=>(0,tL.removeCompositionElements)(t,n.selectors,n.selectors.map((t,n)=>e[n]?.html??null)));t&&nM(t,p.title,nK(n.selectors,e))}else if("copy"===n.type){if(Array.isArray(n.selectors)&&n.selectors.every(e=>"string"==typeof e)){let e=n.selectors.map(nt.cidFromSelector);e.every(e=>null!==e)&&aj(e)}}else if("paste"===n.type){let e=tB({files:n.files,getData:e=>"text/html"===e?n.html??"":n.text});e&&aA(e,af(n.at))}else if("file-drop"===n.type)tu(!1),ty(null),tf("none"),aR(n.files,af(n.at));else if("undo"===n.type)W();else if("redo"===n.type)U();else if("doc-size"===n.type)nx(nN.current),eO(n.height),eL(n.width),tx.current||rZ(n.height);else if("pan"===n.type)tx.current=!0,ej(e=>({...e,panX:e.panX+n.dx,panY:e.panY+n.dy}));else if("wheel"===n.type){let e=(0,tW.wheelKind)(n);r6();let t=e6.current;if(t){let r=t.x+n.x*t.scale,a=t.y+n.y*t.scale;tx.current=!0,ej(t=>{if(n.zoom){let o=(0,tW.wheelZoomFactor)({deltaY:n.deltaY,...e});return(0,tW.zoomToward)(t,o,t.panX+r*t.zoom,t.panY+a*t.zoom)}return{...t,panX:t.panX-n.deltaX,panY:t.panY-n.deltaY}})}else rJ(n.deltaX,n.deltaY,n.zoom,n.x,n.y,e)}else"crop-frame"===n.type?rT.current.setRect(n.rect):"tool-change"===n.type?rN(n.tool):"exit"===n.type&&(rN("select"),rT.current.close(),n5())}};return window.addEventListener("message",e),()=>window.removeEventListener("message",e)},[p,d,c,aj,aA,aR,af,r.activePageId,Y,$,nM,nV,nK,rZ,rJ,ej,r6,W,U,n2,n5,n4]),(0,f.useEffect)(()=>{el||n5()},[el,n5]),(0,f.useEffect)(()=>{if(!p||d||c)return;let e=e=>!!e&&(e.isContentEditable||/^(INPUT|TEXTAREA|SELECT)$/.test(e.tagName)),t=(t,n)=>{let r=document.activeElement;if(t.defaultPrevented||e(r)||e(t.target)||r&&r!==document.body&&!eC.current?.contains(r))return!1;let a=_.usePreviewInspector.getState();if(a.frameId!==eE.current)return!1;let o=r?.closest("[data-layer-cid]"),i=!!o&&(a.selection?.cids??[]).includes(o.dataset.layerCid??""),s=window.getSelection();return!(!i&&s&&!s.isCollapsed&&s.toString())&&aj(a.selection?.cids??[],n)},n=e=>{t(e,e.clipboardData)&&e.preventDefault()},r=n=>{let r=document.activeElement??null,a=n.target,o=e(r)||e(a);if(n.metaKey||n.ctrlKey){let e=n.key.toLowerCase();if("c"===e){!n.altKey&&!n.shiftKey&&t(n)&&n.preventDefault();return}if("z"!==e&&"y"!==e||o)return;let r="y"===e||"z"===e&&n.shiftKey;n.preventDefault(),r?U():W();return}if(("Delete"===n.key||"Backspace"===n.key)&&V&&!o){n.preventDefault(),ey(ei.current?.contentWindow,{type:"delete-selection"});return}if(!o){let e=eb(n);e&&!ek(e)&&(n.preventDefault(),as(e))}};return window.addEventListener("keydown",r),window.addEventListener("copy",n),()=>{window.removeEventListener("keydown",r),window.removeEventListener("copy",n)}},[p,d,W,U,V,c,as,aj]),(0,f.useEffect)(()=>
6153{if(!p||d||c)return;let e=e=>{if(e.defaultPrevented||!(0,T.canvasPasteAllowedForTarget)(e.target,document.activeElement))return;let t=document.activeElement;if(t&&t!==document.body&&!eC.current?.contains(t))return;let n=tB(e.clipboardData);n&&(e.preventDefault(),aA(n,am()))};return window.addEventListener("paste",e),()=>window.removeEventListener("paste",e)},[p,d,c,aA,am]);let oo=p?.id,oi=r.activePageId;(0,f.useLayoutEffect)(()=>{let e=`${r.view}:${(0,tW.composeFitKey)({arrange:eq,compositionId:oo,activePageId:oi,device:n.device,shape:e2?.boards[0]})}`;tQ.current===e||(tx.current=!1,rZ()&&(tQ.current=e))},[oo,oi,n.device,r.view,rZ,eq,e2]),(0,f.useEffect)(()=>{if(!eA)return;let e=()=>et({w:eA.clientWidth,h:eA.clientHeight});if(e(),"u"<typeof ResizeObserver)return;let t=new ResizeObserver(()=>{e(),tx.current||rZ()});return t.observe(eA),()=>t.disconnect()},[eA,rZ]);let os=!!p;(0,f.useEffect)(()=>{ey(ei.current?.contentWindow,{type:"set-mode",mode:os?"edit":null})},[os]);let ol=p?(0,tK.decodeHtmlEntities)(p.title):"",od=(0,f.useCallback)(()=>{if(nx(nN.current),c){let e=ei.current?.contentDocument,t=e?Math.max(e.documentElement?.scrollHeight??0,e.body?.scrollHeight??0,Math.ceil(e.body?.getBoundingClientRect().height??0)):0;t>0&&(eO(t),eL(Math.max(e?.documentElement?.scrollWidth??0,e?.body?.scrollWidth??0)),tx.current||rZ(t));return}ey(ei.current?.contentWindow,{type:"set-mode",mode:"edit"}),rA(),rI(rC.current),ad(),nL(),nF(),aW(),ak(),nP()},[c,rZ,rA,rI,ad,nL,nF,aW,ak,nP]),oc=(0,f.useMemo)(()=>({ref:ei,srcDoc:nT,rebuildKey:nA,title:ol,onLoad:od}),[nT,nA,ol,od]);(0,f.useEffect)(()=>{let e=requestAnimationFrame(rA);return()=>cancelAnimationFrame(e)},[ez.zoom,e2,rA]),(0,f.useEffect)(()=>{V||ey(ei.current?.contentWindow,{type:"clear-selection"})},[V]);let oh=p?.id;(0,f.useEffect)(()=>{$(),ed(null),rO([])},[oh,$]);let ou=r.view;return((0,f.useEffect)(()=>{rO([])},[ou]),j&&0===H.length&&X.active)?(0,t.jsx)(tE,{composing:X}):(0,t.jsxs)("div",{ref:eC,className:"absolute inset-0 flex bg-muted",onDragOver:aX,onDrop:aY,children:[!c&&(0,t.jsx)(tb,{frame:aP,unsupported:!!p&&!ro,selectedCids:n7===re?n8?.cids??[]:[],disabled:az,busyCids:aL,onSelect:e=>{!az&&rn.current===rr&&aD.has(e)&&ey(ei.current?.contentWindow,{type:"reselect",selectors:[nX(e)]})},onMove:(e,t)=>{rn.current===rr&&rh(e,{index:t})},onAction:(e,t)=>{rn.current===rr&&aB(e,t)},onRename:(e,t)=>{rn.current===rr&&aM(e,t)}}),(0,t.jsxs)("div",{className:"flex min-h-0 min-w-0 flex-1 flex-col",children:[(0,t.jsxs)("div",{className:"relative min-h-0 min-w-0 flex-1",onDragEnter:c?void 0:aq,onDragOver:c?void 0:aq,onDrop:c?void 0:aU,children:[!c&&el&&(n3?!!ra:el.text||el.fill||!!ra)&&!em&&!rf.selectionBusy&&(0,t.jsx)("div",{className:"pointer-events-none absolute left-0 top-0 z-30",style:{transform:`translate(${te.left}px, ${te.top}px)`,visibility:e9?"visible":"hidden"},children:(0,t.jsx)("div",{ref:e8,className:"w-fit",children:(0,t.jsx)(eV,{selection:el,onApplyStyle:nZ,onPreviewStyle:nJ,onFontSelect:rX,imageOnly:n3,menuSide:"above"===te.side?"bottom":"top",imageTools:ra?(0,t.jsx)(ti,{mode:rf.mode,onToggleMode:rf.toggleMode,dims:rf.dims,onUpscale:rf.actions.handleUpscale,onRemoveBg:rf.actions.handleRemoveBg,onRotate:rf.actions.handleRotate,onFlip:rf.actions.handleFlip,layerizing:rf.layerizing,layerizeDisabledReason:rf.layerizeDisabledReason,onLayerize:rf.runLayerize,busy:rf.selectionBusy,onSaveToLibrary:ry,onDownload:rv,downloading:!!ra?.src&&ru(ra.src),onDelete:n0}):void 0})})}),ra&&rf.mode&&(0,t.jsx)("div",{className:"absolute top-0 left-0 z-30",style:{transform:`translate(${to.left}px, ${to.top}px)`},children:(0,t.jsx)("div",{ref:ta,className:"w-fit",children:(0,t.jsx)(eZ,{mode:rf.mode,tools:rf,onClose:rf.closeMode})})}),(0,t.jsx)(y.default,{show:rf.paywall,onClose:rf.closePaywall}),(0,t.jsxs)("div",{ref:eN,onMouseDown:h?void 0:ar,onPointerDown:h?aa:void 0,onPointerMove:au,onPointerLeave:ap,className:(0,l.cn)("absolute inset-0 overflow-hidden",nG,F.CANVAS_CURSOR_CLASS[(0,eS.canvasCursorFor)(rR,{space:rM},rF)],h&&"hand"===rR&&"select-none"),style:h&&"hand"===rR?{touchAction:"none"}:void 0,children:[J&&eq&&eW?(0,t.jsx)(z.ComposeArrangeCanvas,{doc:eW,pages:r.pages,activePageId:r.activePageId,selectedPageIds:r_,boardWidth:eG,view:ez,activeIframe:oc,activeFrameGrowable:oa,allPages:eK,headerSize:z.ARRANGE_HEADER_H,pageOps:c?void 0:a9,canAdd:!c&&aK,onSelectFrame:r7,onActivateFrame:r9,onDeselect:r8,onAddPageAt:a0,onAddSection:or}
6153):(0,t.jsxs)(t.Fragment,{children:[K&&!c&&(0,t.jsx)("div",{className:"pointer-events-none absolute inset-0 flex items-center justify-center overflow-hidden",children:(0,t.jsxs)("button",{type:"button","data-compose-empty-board":"",onClick:aQ,style:{width:er.width,height:er.height},className:(0,l.cn)("pointer-events-auto flex cursor-pointer select-none flex-col items-center justify-center gap-2 px-8 text-center","rounded-lg border border-dashed border-border","bg-hover-surface transition-surface hover:border-muted-foreground hover:bg-muted","focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"),children:[(0,t.jsx)(a.Plus,{size:18,className:"text-muted-foreground"}),(0,t.jsx)("p",{className:"text-title font-normal text-foreground",children:ea("composeEmptyCreate")}),(0,t.jsx)("p",{className:"text-caption text-muted-foreground",children:ea("composeEmptyOrAsk")})]})}),J&&(0,t.jsx)("div",{className:"absolute left-0 top-0 origin-top-left bg-card dark:shadow-[var(--shadow-elevated)]",style:{width:eX,height:eI||800,transform:`translate(${ez.panX}px, ${ez.panY}px) scale(${ez.zoom})`,transition:"none"},children:(0,t.jsx)("iframe",{ref:ei,title:(0,tK.decodeHtmlEntities)(J.title),sandbox:c?"allow-same-origin":"allow-scripts",srcDoc:nT,onLoad:od,className:"block h-full w-full border-0"},nA)}),J&&!c&&ot&&"hidden"!==on.mode&&(0,t.jsx)("div",{className:"pointer-events-none absolute inset-0",children:(0,t.jsx)(z.PageControlRow,{pageId:ot.id,index:oe,total:r.pages.length,label:(0,tK.decodeHtmlEntities)(ot.title),left:ez.panX,width:on.width,top:on.top,height:on.height,active:!0,ops:a9,onSelectFrame:r7})}),J&&!c&&eI>0&&(0,t.jsx)("div",{className:"pointer-events-none absolute left-0 top-0",style:{transform:`translate(${ez.panX}px, ${ez.panY+(eI||800)*ez.zoom+(0,L.addSlotGapPx)(ez.zoom)}px)`,width:eX*ez.zoom},children:(0,t.jsx)("div",{className:"flex justify-center",children:(0,t.jsx)(D.AddPageSlot,{onAdd:oa?or:aZ,label:oa?"Add section":"New page",width:eX*ez.zoom})})})]}),(0,t.jsx)(F.ComposeCanvasGestureLayer,{tool:rR,spaceHeld:rM,band:rD})]}),J&&!c&&th&&(0,t.jsxs)("div",{className:"absolute inset-0 z-40",onDragOver:aq,onDragLeave:a$,onDrop:aU,children:[tg&&(0,t.jsx)("div",{className:"pointer-events-none absolute",style:{left:tg.x,top:tg.y,width:tg.w,height:tg.h,boxShadow:"0 0 0 3px var(--flow-select)",background:"color-mix(in oklab, var(--flow-select) 8%, transparent)",transition:"none"}}),(0,t.jsx)(b.FileDropOverlay,{surface:"preview",verdict:tm})]}),(p||K)&&(0,t.jsx)("div",{className:"pointer-events-none absolute inset-x-0 bottom-3 z-30 flex justify-center px-3",children:(0,t.jsx)(e_,{onInsert:as,onInsertImage:al,shapeKind:ao,hasPage:!!J,tool:rR,onToolChange:rN,pagesView:J?{view:r.view,onToggle:r.toggleView,index:r.activeIndex,total:r.pages.length,canShowAll:e$}:void 0})})]}),eJ&&(0,t.jsx)(t3,{pages:r.pages,activePageId:r.activePageId,onSelect:r.setActivePageId,onAddPage:c?void 0:Z?aQ:aZ,onAddFirstPageAtFormat:c||!Z?void 0:aJ,addDisabledReason:c||Z||aK?null:z.ADD_UNAVAILABLE_WHILE_WRITING,actions:c?void 0:a7})]})]})}var n$=e.i(905455),nV=e.i(967489),nK=e.i(699375),nZ=e.i(103496),nJ=e.i(411983);function nQ(e,t){let n=t>>>0;for(let t=0;t<e.length;t++)n^=e.charCodeAt(t),n=Math.imul(n,0x1000193);return n>>>0}function n0(e){return(e>>>0).toString(16).padStart(8,"0")}var n1=e.i(871315),n2=e.i(981308),n5=e.i(88116);function n6(e){return e.startsWith("video/")?"video":e.startsWith("audio/")?"audio":e.startsWith("image/")?"image":"file"}function n4(e){return("string"==typeof e.filename?e.filename.trim():"")||function(e){try{let t=new URL(e,"https://cdn.ponpon.ai").pathname;return decodeURIComponent(t.split("/").filter(Boolean).pop()??"")||"export"}catch{return"export"}}(e.url)}function n3(e,t){let n=URL.createObjectURL(e),r=document.createElement("a");r.href=n,r.download=t,r.rel="noopener",document.body.appendChild(r),r.click(),r.remove(),setTimeout(()=>URL.revokeObjectURL(n),0)}async function n8(e,t){return{blob:await (0,n2.blobToJpeg)(e),filename:(0,n2.jpegFilename)(t)}}function n7(e){try{window.open(e,"_blank","noopener,noreferrer")}catch{}}async function n9(e,t,n){var r,a;if(t)return t;let o=n.fetchImpl??(e=>fetch(e)),i=[(r=e.url,/^https?:\/\//i.test(r)?(0,ns.getCorsSafeOssUrl)((0,ns.stripOssSignature)(r)):r)],s=(a=e.url,/^https?:\/\//i.test(a)?`/api/proxy-image?url=${encodeURIComponent(a)}`:null);for(let e of(s&&i.push(s),i))try{let t=await o(e);if(!t.ok)throw Error(`export fetch ${t.status}`);return await t.blob()}catch{}return null}async function re(e,t,n){if(!n.transform)return{blob:e,filename:t};try{return await n.transform(e,t)}catch{return{blob:e,filename:t}}}async function rt(e,t,n={}){let r=n4(e);(0,n5.trackDownload)(e.url,n6(e.mime),"compose-export");let a=await n9(e,t,n);if(!a)return(n.openFallback??n7)(e.url),null;let o=await re(a,r,n);return(n.save??n3)(o.blob,o.filename),a}async function rn(e,t={}){if(0===e.length)return[];if(1===e.length){let n=e[0],r=await rt(n.file,n.cached,t);return r?[{file:n.file,blob:r}]:[]}let n=await Promise.all(e.map(async({file:e,cached:n})=>{(0,n5.trackDownload)(e.url,n6(e.mime),"compose-export");let r=await n9(e,n,t);if(!r)return null;let a=await re(r,n4(e),t);return{file:e,server:r,saved:a}}));e.forEach((e,r)=>{n[r]||(t.openFallback??n7)(e.file.url)});let r=n.filter(e=>null!==e);if(0===r.length)return[];let a=new n1.default,o=new Set;for(let{saved:e}of r){let t=e.filename;if(o.has(t)){let e=t.lastIndexOf("."),n=e>0?t.slice(0,e):t,r=e>0?t.slice(e):"",a=2;for(;o.has(`${n}-${a}
6153${r}`);)a++;t=`${n}-${a}${r}`}o.add(t),a.file(t,await e.blob.arrayBuffer())}let i=await a.generateAsync({type:"blob",compression:"STORE"});return(t.save??n3)(i,function(e){if(0===e.length)return"export.zip";let t=e.map(e=>e.replace(/\.[^./\\]+$/,"")),n=t[0]??"";for(let e of t.slice(1)){let t=0;for(;t<n.length&&t<e.length&&n[t]===e[t];)t++;n=n.slice(0,t)}return n=n.replace(/[-_\s\d]+$/,"").trim(),`${n||"export"}.zip`}(r.map(e=>e.saved.filename))),r.map(({file:e,server:t})=>({file:e,blob:t}))}function rr({total:e,completed:t,phase:n}){return"finalizing"===n?{key:"composeExportFinalizing"}:e<=1?{key:"composeExportPending"}:null===t?{key:"composeExportPendingPages",values:{count:e}}:{key:"composeExportPendingCount",values:{completed:Math.min(Math.max(Math.floor(t),0),e),total:e}}}function ra(e){return{key:"composeExportSuccess",values:{count:e}}}function ro(e){let t=Math.round(Number(e));return!Number.isFinite(t)||t<1?1:t>4?4:t}function ri(e){return"pdf"===e?"pdf":"jpeg"===e?"jpeg":"png"}function rs({label:e,children:n}){return(0,t.jsxs)("div",{className:"flex flex-col gap-2",children:[(0,t.jsx)("span",{className:"text-[11px] font-semibold text-muted-foreground",children:e}),n]})}function rl({compose:e,pages:n,projectId:r}){let a=(0,m.useTranslations)("Flow"),[o,i]=(0,f.useState)(!1),[s,l]=(0,f.useState)(!1),[d,c]=(0,tp.usePersistedState)("ponpon.compose.export.filetype","png",ri),[h,u]=(0,tp.usePersistedState)("ponpon.compose.export.scale",1,ro),[p,b]=(0,f.useState)("all"),[y,v]=(0,f.useState)(!1),w=(0,f.useId)(),x=!!e.compositions.active,E=n.activePageId,k=!n.doc||"stack"===(0,tN.arrangeFor)(n.doc.format),S="pdf"!==d,T="jpeg"===d,A=S&&k&&y,R=n.doc?(0,tN.resolvePreset)(n.doc.format):null,N=R&&"stack"!==R.arrange?R:null,C=(0,f.useRef)(n.pages.length);C.current=n.pages.length;let I=(0,f.useRef)(e.compositions.active);I.current=e.compositions.active;let _=(0,f.useRef)(e.compositions.transient);_.current=e.compositions.transient;let O=(0,f.useRef)(null),P=(0,f.useCallback)(async(e,t,n=!1)=>{var s,l,d;let c,u,p,m,f,b,y,v=n?{transform:n8}:void 0;if(!r)return void g.toast.error(a("composeExportNoProject"));if(o)return;O.current||(O.current=function(e={}){let t=Math.max(1,e.maxEntries??3),n=Math.max(0,e.maxBytes??0xc000000),r=new Map,a=0,o=()=>{let e=r.keys().next();if(e.done)return!1;let t=r.get(e.value);return r.delete(e.value),(a-=t?.bytes??0)<0&&(a=0),!0};return{get(e){let t=r.get(e);if(t)return r.delete(e),r.set(e,t),t},set(e,i){if(0===i.length)return!1;let s=function(e){let t=0;for(let n of e)t+=n.blob.size||0;return t}(i);if(s>n)return!1;let l=r.get(e);for(l&&(r.delete(e),a-=l.bytes),r.set(e,{files:i,bytes:s}),a+=s;(r.size>t||a>n)&&!(r.size<=1)&&o(););return!0},keys:()=>[...r.keys()],size:()=>r.size,clear(){r.clear(),a=0}}}());let w=O.current,x=I.current,E=x&&!_.current?(l={projectId:r,compositionId:x.id,title:x.title,kind:x.kind,html:x.html},m="pdf"===(d={format:e,scale:h,frameIds:t}).format?0:Math.round(Number(d.scale)||0),f=d.frameIds&&d.frameIds.length>0?d.frameIds.join(","):"all",b=(u=nQ(c=[l.title,l.kind,l.html].join("\0"),0x811c9dc5),p=nQ(`${c.length}\u0000${c}`,0x9dc5811c),`${n0(u)}${n0(p)}`),["v1",l.projectId,l.compositionId,d.format,`s${m}`,`f${f}`,b].join("|")):null,k=E?w.get(E):void 0;if(k){i(!0);try{await rn(k.files.map(e=>({file:e.file,cached:e.blob})),v);let e=ra(k.files.length);g.toast.success(a(e.key,e.values))}finally{i(!1)}return}i(!0);let S={total:(s=C.current,y=Number.isFinite(s)&&s>0?Math.floor(s):0,t&&t.length>0?Math.min(t.length,y||t.length):y),completed:null,phase:"rendering"},T=rr(S),A=g.toast.loading(a(T.key,T.values)),R=()=>{let e=rr(S);g.toast.loading(a(e.key,e.values),{id:A})};try{let n="pdf"!==e,o=await fetch(`/api/projects/${encodeURIComponent(r)}/composition/export?stream=1`,{method:"POST",headers:{"Content-Type":"application/json",Accept:"application/x-ndjson"},body:JSON.stringify({format:e,...t&&t.length>0?{frameIds:t}:{},...n?{scale:h}:{},...x&&!_.current?{docId:x.id}:{}})});if(404===o.status){let e=await o.json().catch(()=>null);g.toast.dismiss(A),e&&"EXPORT_NOT_LIVE"!==e.code&&"NOT_FOUND"!==e.code?g.toast.error(a("composeExportNothingYet")):(0,g.toast)(a("composeExportNotLive"));return}if(!o.ok){let e=await o.json().catch(()=>null);g.toast.dismiss(A),g.toast.error(e?.error||a("composeExportFailed"));return}let i={files:null,failure:null};if((o.headers.get("content-type")||"").includes("x-ndjson")&&o.body)try{await (0,nZ.readCompositionExportEvents)(o.body,t=>{"page"===t.type?(S=function(e,t){if("page"!==t.type)return e;let n=Number.isFinite(t.total)&&t.total>0?Math.floor(t.total):e.total,r=Number.isFinite(t.index)?Math.floor(t.index):0,a=Math.min(Math.max(e.completed??0,r,0),Math.max(n,0));return{total:n,completed:a,phase:n>0&&a>=n?"finalizing":"rendering"}}(S,t),R()):"done"===t.type?i.files=t.files.map(t=>({...t,format:e})):i.failure=t})}catch{}else i.files=((await o.json()).files??[]).map(t=>({...t,format:e}));if(i.failure){g.toast.dismiss(A),"composition_not_found"===i.failure.error?g.toast.error(a("composeExportNothingYet")):g.toast.error(i.failure.detail||a("composeExportFailed"));return}if(!i.files){g.toast.dismiss(A),g.toast.error(a("composeExportInterrupted"));return}let s=i.files;if(0===s.length){g.toast.dismiss(A),(0,g.toast)(a("composeExportNothing"));return}S={...S,phase:"finalizing"},R();let l=await rn(s.map(e=>({file:e})),v);for(let e of(g.toast.dismiss(A),(0,nJ.dispatchFileCreated)(s),s))(0,nJ.dispatchComposeExportArtifact)(e);E&&l.length===s.length&&w.set(E,l);let d=ra(s.length);g.toast.success(a(d.key,d.values))}catch{g.toast.dismiss(A),g.toast.error(a("composeExportFailed"))}finally{i(!1)}},[r,o,h,a]);(0,f.useEffect)(()=>{let e=e=>{let t=e.detail;t?.pageId&&P("png-frames",[t.pageId])};return window.addEventListener(tI.COMPOSE_DOWNLOAD_PAGE_EVENT,e),()=>window.removeEventListener(tI.COMPOSE_DOWNLOAD_PAGE_EVENT,e)},[P]);let D=(0,f.useCallback)(()=>{let e=A?"png-strip":"pdf"===d?"pdf":"png-frames",t=!A&&"current"===p&&E?[E]:void 0;l(!1),P(e,t,T)},[A,d,p,E,T,P]);return x?(0,t.jsxs)(eB.Popover,{open:s,onOpenChange:l,children:[(0,t.jsx)(eB.PopoverTrigger,{render:(0,t.jsxs)(ej.Button,{variant:"secondary",size:"sm",className:"h-7 gap-1.5 !rounded-[var(--radius-bar-control)] border-transparent bg-foreground px-2.5 text-background hover:bg-foreground
6153/90 hover:text-background",disabled:o,"aria-busy":o,children:[o&&(0,t.jsx)(e4.Loader2,{size:15,className:"animate-spin"}),(0,t.jsx)("span",{children:a("gridExport")})]})}),(0,t.jsxs)(eB.PopoverContent,{align:"end",sideOffset:8,className:"w-[292px]",children:[(0,t.jsx)("span",{className:"font-heading text-[15px] font-medium text-foreground",children:a("composeExportTitle")}),(0,t.jsx)(rs,{label:a("composeExportFileType"),children:(0,t.jsxs)(nV.Select,{value:d,onValueChange:e=>e&&c(ri(e)),items:{png:"PNG",jpeg:"JPEG",pdf:"PDF"},children:[(0,t.jsxs)(nV.SelectTrigger,{className:"h-10 w-full rounded-[var(--radius-bar-control)] px-3","aria-label":a("composeExportFileType"),children:["pdf"===d?(0,t.jsx)(j.FilePdf,{size:16}):(0,t.jsx)(tl.ImageIcon,{size:16}),(0,t.jsx)(nV.SelectValue,{})]}),(0,t.jsxs)(nV.SelectContent,{alignItemWithTrigger:!1,sideOffset:6,children:[(0,t.jsxs)(nV.SelectItem,{value:"png",children:[(0,t.jsx)(tl.ImageIcon,{size:16}),"PNG"]}),(0,t.jsxs)(nV.SelectItem,{value:"jpeg",children:[(0,t.jsx)(tl.ImageIcon,{size:16}),"JPEG"]}),(0,t.jsxs)(nV.SelectItem,{value:"pdf",children:[(0,t.jsx)(j.FilePdf,{size:16}),"PDF"]})]})]})}),S&&(0,t.jsxs)(rs,{label:a("genSize"),children:[(0,t.jsx)(n$.SegmentedControl,{fullWidth:!0,size:"md",shape:"squircle",corner:"bar",value:`${h}`,onValueChange:e=>u(ro(e)),options:["1","2","3","4"].map(e=>({value:e,label:`${e}\xd7`}))}),N&&(0,t.jsxs)("span",{className:"text-[11px] tabular-nums text-muted-foreground",children:[N.width*h," × ",N.height*h," px"]})]}),(0,t.jsx)(rs,{label:a("composeExportSelectPages"),children:(0,t.jsx)(n$.SegmentedControl,{fullWidth:!0,size:"md",shape:"squircle",corner:"bar",value:A?"all":p,onValueChange:b,options:[{value:"all",label:a("composeExportPagesAll"),disabled:A},{value:"current",label:a("composeExportPagesCurrent"),disabled:A||!E,tooltip:A?a("composeExportStackedHint"):void 0}]})}),S&&k&&(0,t.jsxs)("div",{className:"-my-1 flex items-center justify-between gap-3",children:[(0,t.jsx)("span",{id:w,className:"text-[13px] text-foreground",children:a("composeExportStacked")}),(0,t.jsx)(nK.Switch,{checked:y,onCheckedChange:v,"aria-labelledby":w})]}),(0,t.jsxs)(ej.Button,{variant:"secondary",size:"lg",className:"w-full !rounded-[var(--radius-bar-control)] border-transparent bg-foreground text-background hover:bg-foreground
6153/90 hover:text-background",onClick:D,disabled:o,children:[o?(0,t.jsx)(e4.Loader2,{size:16,className:"animate-spin"}):(0,t.jsx)(eQ.Download,{size:16}),a("download")]})]})]}):null}let rd=()=>()=>{},rc=()=>1;function rh(e){let t=e?.subscribe??rd,n=e?()=>e.getZoom():rc;return(0,f.useSyncExternalStore)(t,n,n)}e.s(["useComposeZoom",0,function(){let e=(0,f.useRef)(null),t=(0,f.useRef)(null);null===t.current&&(t.current={value:1,listeners:new Set});let n=t.current;return(0,f.useMemo)(()=>({getZoom:()=>n.value,subscribe:e=>(n.listeners.add(e),()=>{n.listeners.delete(e)}),publishZoom:e=>{if(e!==n.value)for(let t of(n.value=e,n.listeners))t()},controls:e}),[n])},"useComposeZoomLevel",0,rh],237334);let ru=c.PANE_CARD_GUTTER_PX,rp="Create a page first";e.s(["ComposeBarControls",0,function({compose:e,pages:n,projectId:o,zoom:l}){let d=rh(l),c=(0,s.useViewerReadOnly)(),u=!!e.compositions.active,m=u||nB(e.compositions),f=!u;return(0,t.jsxs)(t.Fragment,{children:[m&&l&&(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(i.IconButton,{label:"Zoom out",title:f?rp:"Zoom out",noHitAreaExtension:!0,disabled:f||d<=tW.MIN_ZOOM+.001,onClick:()=>l.controls.current?.zoomOut(),className:h.flowBarIconButton,children:(0,t.jsx)(r.Minus,{size:18})}),(0,t.jsx)(p.ZoomMenu,{zoom:f?1:d,onZoomTo:e=>l.controls.current?.zoomTo(e),onZoomToFit:()=>l.controls.current?.reset(),disabled:f,disabledReason:rp}),(0,t.jsx)(i.IconButton,{label:"Zoom in",title:f?rp:"Zoom in",noHitAreaExtension:!0,disabled:f||d>=tW.MAX_ZOOM-.001,onClick:()=>l.controls.current?.zoomIn(),className:h.flowBarIconButton,children:(0,t.jsx)(a.Plus,{size:18})})]}),c?null:(0,t.jsx)(rl,{compose:e,pages:n,projectId:o})]})},"ComposeIsland",0,function({compose:e,left:n,rightPx:r,bottomPx:a=ru,topPx:i=u.WORKSPACE_SURFACE_TOP_PX,paneInsetRightPx:s=0,pages:h,zoom:p,store:m,hidden:f=!1}){return(0,t.jsx)("div",{style:{left:n,right:r,top:i,bottom:a,visibility:f?"hidden":void 0},[d.PANE_FOLLOWER_ATTR]:"left",className:(0,l.cn)("absolute z-20 flex flex-col",f&&"pane-gate-hidden pointer-events-none"),children:(0,t.jsxs)("div",{[o.PANEL_EDGE_ATTR]:"",className:(0,l.cn)("relative min-h-0 flex-1 overflow-hidden bg-muted",(0,c.paneCardCorners)(),c.PANE_CARD_SHADOW_CLASSNAME),children:[(0,t.jsx)("div",{"aria-hidden":"true",className:(0,l.cn)("pointer-events-none absolute inset-y-0 right-0",nG),style:{width:s}}),(0,t.jsx)("div",{className:"absolute inset-0",style:{right:s},children:(0,t.jsx)(nY,{compose:e,pages:h,zoom:p,store:m,hidden:f})})]})})}],400924),e.s(["useComposePages",0,function(e,t,n,r=!1){let a=n??t??"none",[o,i]=(0,tp.usePersistedState)(`ponpon.compose.pages.view.${a}`,"all",e=>"thumbnails"===e?"thumbnails":"all"),[s,l]=(0,tp.usePersistedState)(`ponpon.compose.rail.page.${a}`,null),d=window.location.origin,c=(0,f.useMemo)(()=>(0,tq.parseCompositionPages)(e,d),[e,d]),h=(0,f.useRef)(null);"empty"===c.source||!r&&(0,tq.isEmptyFramesDocument)(e)?h.current=null:c.pages.length>0&&(h.current=c);let u="pending"===c.source&&h.current?h.current:c,{pages:p}=u,{activePageId:m,activeHtml:g}=(0,f.useMemo)(()=>{let{activePageId:e,page:t}=(0,tq.resolveActivePage)(u,s);return{activePageId:e,activeHtml:t?t.html:""}},[u,s]),b=(0,f.useCallback)(e=>l(e),[l]);return{pages:p,activePageId:m,activeHtml:g,setActivePageId:b,view:o,setView:(0,f.useCallback)(e=>i(e),[i]),toggleView:(0,f.useCallback)(()=>i(e=>"all"===e?"thumbnails":"all"),[i]),multiPage:p.length>1,activeIndex:m?p.findIndex(e=>e.id===m)+1:0,source:u.source,doc:u.doc}}],977385)}]);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.