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?" ":l(t);return`<${r}${o}>${i}</${r}>`}(t,n[e],c))}return s.join("");function l(e){return e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}}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.