From 83404a0ccf6620763a878a89e554caf46f156f2c Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Mon, 14 Sep 2026 19:09:21 -0700 Subject: [PATCH] Improve: streamline assembled-page review Show the approved composition and captured page in one comparison workspace. Replace component inventory and staged bulk actions with explicit page approval or direct feedback, retaining revision checks and read-only history. AI-assisted implementation with Codex under maintainer direction. --- crates/context/assets/component-review.js | 93 ++++++++++++++--------- ui/component-review/review.ts | 75 +++++++++++------- ui/component-review/styles.ts | 23 ++++++ 3 files changed, 127 insertions(+), 64 deletions(-) diff --git a/crates/context/assets/component-review.js b/crates/context/assets/component-review.js index a8c0c3c8a..9bb43d510 100644 --- a/crates/context/assets/component-review.js +++ b/crates/context/assets/component-review.js @@ -1,4 +1,4 @@ -(()=>{function ji(p,r){let x=r?.changes[p],e=r?.feedback?.[p],n=e?.decision??(r?.submitted?r.draft.decisions[p]:void 0),w=x?.kind==="unchanged"&&(x.carried??(r?.submitted&&n?.action==="approve"))===!0;return{change:x,prior:n,feedbackRound:e?.round??r?.packet.round,carried:w,label:x?.kind==="added"?"New component":x?.kind==="changed"?"Review again":w?"Approval kept":n?.action==="revise"?"Changes still requested":"Awaiting review"}}function S(p,r,x){let e=r.decisions[p.id],n=e?.revision===p.revision?e:void 0,w=ji(p.id,x);if(n?.action==="approve")return{kind:"approved",label:w.carried?"Approval kept":"Approved",priority:3};if(n?.action==="revise")return{kind:"feedback",label:"Feedback ready",priority:2};return{kind:"pending",label:w.change?.kind==="changed"?"Review again":w.change?.kind==="added"?"New · review needed":"Not reviewed",priority:w.change?.kind==="changed"||w.change?.kind==="added"?0:1}}function ho(p){return{packetRevision:p.revision,decisions:{},missing:[],inventoryConfirmed:!1}}function Ko(p){return Object.values(p).every(Number.isFinite)&&p.x>=0&&p.y>=0&&p.w>0&&p.h>0&&p.x+p.w<=1.00001&&p.y+p.h<=1.00001}function li(p,r){let x=p.components.map((f)=>r.decisions[f.id]?.revision===f.revision?r.decisions[f.id]:void 0),e=x.filter((f)=>f?.action==="approve").length,n=x.filter((f)=>f?.action==="revise").length,w=x.length-e-n,g=n>0||r.missing.length>0;return{approved:e,revisions:n,pending:w,hasFeedback:g,canSubmit:r.packetRevision===p.revision&&r.missing.every((f)=>f.name.trim()&&Ko(f.box))&&(g||!w&&r.inventoryConfirmed)}}function go(p,r){let x={...r.decisions};for(let e of p.components)if(!x[e.id]||x[e.id].revision!==e.revision)x[e.id]={revision:e.revision,action:"approve",feedback:"",split:!1};return{...r,decisions:x}}function fo(p,r){if(!li(p,r).canSubmit)throw Error("Review is incomplete or stale");return{schemaVersion:1,requestId:p.id,...structuredClone(r)}}function qi(p){let r=p.preview.kind==="page"||p.preview.sourceKind==="page",x=p.preview.sourceKind==="page";return{code:r,captured:x,label:r?p.medium.match(/html|css|svg/i)?p.medium:"HTML / CSS / SVG":"Raster",caption:r?x?p.preview.isolation?"Component only":"Region capture":"Live component":"Produced asset",fileLabel:x?"Open captured preview":"Open source image"}}function so(p,r,x){let e=p.components.findIndex((n)=>n.id===x);for(let n=1;n<=p.components.length;n++){let w=p.components[(e+n)%p.components.length];if(S(w,r).kind==="pending")return w.id}}function Qi(p,r,x){return x==="all"||S(p,r).kind==="pending"===(x==="pending")}function Wi(p,r,x,e,n){let w=n==="fit"?Math.min(x/p,e/r):n;return{scale:w,width:p*w,height:r*w}}function Zi(p,r,x,e){if(x<=0||e<=x)return 0;return Math.max(0,Math.min(1,((p-r)/x-0.08)/0.84))*(e-x)}var co=` +(()=>{function Hi(p,e){let x=e?.changes[p],r=e?.feedback?.[p],n=r?.decision??(e?.submitted?e.draft.decisions[p]:void 0),l=x?.kind==="unchanged"&&(x.carried??(e?.submitted&&n?.action==="approve"))===!0;return{change:x,prior:n,feedbackRound:r?.round??e?.packet.round,carried:l,label:x?.kind==="added"?"New component":x?.kind==="changed"?"Review again":l?"Approval kept":n?.action==="revise"?"Changes still requested":"Awaiting review"}}function S(p,e,x){let r=e.decisions[p.id],n=r?.revision===p.revision?r:void 0,l=Hi(p.id,x);if(n?.action==="approve")return{kind:"approved",label:l.carried?"Approval kept":"Approved",priority:3};if(n?.action==="revise")return{kind:"feedback",label:"Feedback ready",priority:2};return{kind:"pending",label:l.change?.kind==="changed"?"Review again":l.change?.kind==="added"?"New · review needed":"Not reviewed",priority:l.change?.kind==="changed"||l.change?.kind==="added"?0:1}}function co(p){return{packetRevision:p.revision,decisions:{},missing:[],inventoryConfirmed:!1}}function Eo(p){return Object.values(p).every(Number.isFinite)&&p.x>=0&&p.y>=0&&p.w>0&&p.h>0&&p.x+p.w<=1.00001&&p.y+p.h<=1.00001}function di(p,e){let x=p.components.map((h)=>e.decisions[h.id]?.revision===h.revision?e.decisions[h.id]:void 0),r=x.filter((h)=>h?.action==="approve").length,n=x.filter((h)=>h?.action==="revise").length,l=x.length-r-n,ei=n>0||e.missing.length>0;return{approved:r,revisions:n,pending:l,hasFeedback:ei,canSubmit:e.packetRevision===p.revision&&e.missing.every((h)=>h.name.trim()&&Eo(h.box))&&(ei||!l&&e.inventoryConfirmed)}}function so(p,e){let x={...e.decisions};for(let r of p.components)if(!x[r.id]||x[r.id].revision!==r.revision)x[r.id]={revision:r.revision,action:"approve",feedback:"",split:!1};return{...e,decisions:x}}function wo(p,e){if(!di(p,e).canSubmit)throw Error("Review is incomplete or stale");return{schemaVersion:1,requestId:p.id,...structuredClone(e)}}function Vi(p){let e=p.preview.kind==="page"||p.preview.sourceKind==="page",x=p.preview.sourceKind==="page";return{code:e,captured:x,label:e?p.medium.match(/html|css|svg/i)?p.medium:"HTML / CSS / SVG":"Raster",caption:e?x?p.preview.isolation?"Component only":"Region capture":"Live component":"Produced asset",fileLabel:x?"Open captured preview":"Open source image"}}function mo(p,e,x){let r=p.components.findIndex((n)=>n.id===x);for(let n=1;n<=p.components.length;n++){let l=p.components[(r+n)%p.components.length];if(S(l,e).kind==="pending")return l.id}}function Wi(p,e,x){return x==="all"||S(p,e).kind==="pending"===(x==="pending")}function Zi(p,e,x,r,n){let l=n==="fit"?Math.min(x/p,r/e):n;return{scale:l,width:p*l,height:e*l}}function Ui(p,e,x,r){if(x<=0||r<=x)return 0;return Math.max(0,Math.min(1,((p-e)/x-0.08)/0.84))*(r-x)}var bo=` :host{height:var(--component-review-height,100dvh);min-height:0;overflow:hidden} .review{height:100%;max-width:none;min-height:0;padding:0;display:flex;flex-direction:column;overflow:hidden;background:var(--color-bg,#fafafa)} .review>header{flex-shrink:0;padding:16px 24px;margin:0;border-bottom:1px solid var(--line);gap:16px}.review>header>div{min-width:0}.review h1{font-size:30px;line-height:1}.review>header p{font-size:12px;margin-top:6px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.review .badge{font-size:11px} @@ -53,7 +53,7 @@ .review>.inventory-section{height:160px;padding:6px 14px}.inventory-section>.section-head{flex-direction:row;flex-wrap:nowrap;align-items:center;gap:6px;min-height:34px;margin-bottom:4px}.inventory-section h2{display:none}.inventory-filters{flex:1;width:auto;min-width:0;padding:2px}.inventory-filters button{padding:4px 6px;font-size:10px;min-height:28px}.inventory-filters b{margin-left:3px}.tray-actions #show-all{display:none}.tray-actions #toggle-tray{padding:6px;min-width:32px;min-height:32px}.tray-actions #toggle-tray span{display:none}.tray-actions svg{width:16px;height:16px}.review>.inventory-section.tray-collapsed{height:46px;padding:6px 14px}.review>.inventory-section.tray-expanded{height:160px}.inventory .item{flex-basis:130px;grid-template-rows:auto 1fr auto;padding:6px}.inventory .item-thumb{display:none}.inventory.all{display:flex;overflow-x:auto;overflow-y:hidden}.inventory .item strong{font-size:11px;min-height:22px}.inventory .state{font-size:10px}.inventory .item-number{font-size:10px;min-height:14px}.inventory-empty{padding:8px 0;font-size:12px} .review>footer{padding:8px 14px calc(8px + env(safe-area-inset-bottom));gap:8px;flex-direction:column;align-items:stretch}.review>footer>div:first-child{gap:10px;justify-content:space-between}.review>footer #approve-rest{font-size:10px;min-height:32px;max-width:47%;padding:5px 8px}.review>footer .check{font-size:10px;max-width:48%;gap:4px}.review>footer .check input{width:14px;height:14px}.review>footer .submit-area{justify-content:space-between;gap:10px}.review>footer .submit-area p{font-size:10px;max-width:22ch}.review>footer .primary{min-height:34px;font-size:12px;padding:6px 10px} } -`;var wo=` +`;var vo=` :host{display:block;color:var(--color-text,#292929);font:14px/1.45 var(--font-sans,Arial,sans-serif);--line:var(--color-border,#ddd);--paper:var(--color-panel,#fff);--muted:var(--color-muted,#666);--teal:var(--color-patina,#28625e);--warn:var(--color-warn,#8a5b30);--selection:#43897f} *{box-sizing:border-box}h1,h2,p,figure{margin:0}button,input,textarea{font:inherit}button{cursor:pointer;border:1px solid var(--line);border-radius:4px;background:var(--paper);color:inherit;padding:8px 12px;min-height:36px}button:hover{border-color:var(--teal);color:var(--teal)}button:disabled{cursor:default;opacity:.45}button:focus-visible,input:focus-visible,textarea:focus-visible{outline:2px solid var(--teal);outline-offset:3px}button[aria-pressed=true]{box-shadow:inset 0 0 0 1px var(--teal)}input[type=checkbox]{accent-color:var(--teal);width:16px;height:16px;flex-shrink:0}textarea,input:not([type=checkbox]){width:100%;background:var(--paper);color:inherit;border:1px solid #999;border-radius:4px;padding:9px 10px}textarea{resize:vertical;min-height:80px}::selection{background:#c7ddd8}a{color:var(--teal)} .review{max-width:1600px;margin:auto;padding:24px 28px 0}header{display:flex;align-items:center;justify-content:space-between;gap:20px;margin-bottom:16px}h1{font:400 40px/1.05 var(--font-display,Arial,sans-serif);letter-spacing:-.02em}header p{margin-top:8px;font-size:15px}header p span,.medium{color:var(--muted)}.badge{border:1px solid var(--line);padding:5px 10px;font-size:12px;white-space:nowrap}.preview-note{color:var(--muted);font-size:12px;border-bottom:1px solid var(--line);padding-bottom:16px;margin-bottom:24px} @@ -88,7 +88,7 @@ @media(prefers-reduced-motion:reduce){*{scroll-behavior:auto}} .item-medium{margin-left:auto;font-weight:400;display:flex;align-items:center;gap:4px;font-size:10px;color:var(--muted);min-height:16px}.item-medium .utility-icon{width:14px;height:14px;flex-shrink:0}.material>.utility-icon{width:18px;height:18px;align-self:center;color:var(--teal)} .feedback-actions{display:flex;align-items:center;gap:10px;margin-top:12px;flex-wrap:wrap}.feedback-actions .primary{margin-left:auto;display:inline-flex;align-items:center;gap:10px}.feedback-actions svg{width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:1.5}.feedback-hint,.shortcut-hint{font-size:12px;color:var(--muted)}.feedback-hint{margin-top:6px}.shortcut-hint{flex-basis:100%;text-align:right}.record-verdict{display:flex;flex-wrap:wrap;align-items:baseline;gap:5px 12px;font-size:13px}.record-verdict span{font-size:11px;color:var(--muted)}.review>.record-footer{font-size:12px;color:var(--muted);flex-wrap:wrap;gap:6px 16px}.saved-feedback{font-size:13px;white-space:pre-wrap;overflow-wrap:anywhere;margin:8px 0}.decision-notice{display:flex;align-items:center;gap:12px;font-size:12px;margin-bottom:10px}.decision-notice>span{flex:1;overflow-wrap:anywhere}.decision-notice button{color:var(--teal);flex-shrink:0}.review-summary{padding:4px 2px}.completion-mark{display:flex;align-items:center;justify-content:center;width:32px;height:32px;border-radius:50%;background:#e6efe8;color:#28625e;float:left;margin:0 10px 8px 0}.completion-mark svg{width:24px;height:24px;fill:none;stroke:currentColor;stroke-width:1.6}.completion-link{display:inline-flex;align-items:center;gap:8px;color:var(--teal);border:0;background:transparent;padding:0;font-size:12px;text-align:left}.completion-link svg{width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:1.8;flex-shrink:0}.queue-complete #approve-rest{display:none}.review-summary h2{font-size:22px;line-height:32px}.review-summary p{clear:both;margin-top:8px;color:var(--muted)}.summary-decisions{margin-top:24px;display:grid}.summary-decisions button{display:grid;grid-template-columns:1fr auto;gap:6px 16px;text-align:left;border:0;border-bottom:1px solid var(--line);border-radius:0;padding:12px 0;background:transparent}.summary-decisions strong{font-size:13px;font-weight:500}.summary-decisions span{font-size:12px;color:var(--teal)}.summary-decisions small{grid-column:1/-1;font-size:12px;color:var(--muted);white-space:pre-wrap;overflow-wrap:anywhere} -`+co+` +`+bo+` .review-form:has(#feedback-form){max-height:75%;min-height:0;flex-shrink:1;display:flex;flex-direction:column;overflow:hidden;padding-top:8px} #feedback-form{display:flex;flex-direction:column;flex:1;min-height:0} /* Leave room inside the scrollport for the 2px focus ring and 3px offset. */ @@ -124,47 +124,70 @@ .component-details .material{min-height:0;margin:12px 0 5px;gap:5px 8px}.component-details .material strong{font-size:11px;font-weight:500;color:var(--muted)}.component-details .material>.utility-icon{width:15px;height:15px}.component-details .material span{font-size:10px} @container(max-width:460px){.compare-toolbar .overlay-label{display:none}.compare-toolbar .overlay-control{width:34px;padding:7px}.compare-toolbar .zoom-control select{width:86px;padding-left:27px;padding-right:24px}.compare-toolbar .zoom-control>.utility-icon:first-child{left:8px}.compare-toolbar .zoom-control>.utility-icon:last-child{right:7px}} @media(forced-colors:active){.zoom-control select{appearance:auto;padding:6px 8px}.zoom-control>.utility-icon{display:none}} -`;var Yo={chevronDown:'',code:'',image:'',expand:'',compact:'',hideTray:'',showTray:'',next:'',mark:'',close:'',undo:'',external:'',zoom:''};function $(p){return``}var l=(p)=>p.replace(/[&<>"']/g,(r)=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[r]),oi=(p)=>`${p*100}%`,ri=(p)=>{let r=new URL(p,location.href);if(!["http:","https:"].includes(r.protocol))throw Error("Unsupported preview URL");return l(r.href)};function mo(p,r,x){let e=p.attachShadow({mode:"open"}),n=structuredClone(x.initialDraft??ho(r)),w=()=>[...r.components].sort((s,m)=>S(s,n,x.history).priority-S(m,n,x.history).priority),g=w().find((s)=>S(s,n,x.history).kind!=="approved")?.id??r.components[0]?.id,f=li(r,n).pending?"pending":"reviewed",G=!1,W=!1,y=x.completed??!1,Mi="",K={},z=!!x.completed||!li(r,n).pending,I=null,bo=/Mac|iPhone|iPad/.test(navigator.platform)?"⌘Enter":"Ctrl+Enter",F=!1,V=!1,C=!1,J=!0,ai=!1,H="comp",Ui=H,O="fit",di="checker",Z=!1,ei="isolated",Hi,Ti="fit",A=null,R=null,P=null,hi='',si='',Bi=(s)=>`left:${oi(s.x)};top:${oi(s.y)};width:${oi(s.w)};height:${oi(s.h)}`;function xi(s){e.getElementById(s)?.focus({preventScroll:!0})}function uo(s){let m=so(r,n,s);if(z=!m,m)g=m;H="component",Z=!1,F=!1,O="fit",ei="isolated",f=z?"reviewed":"pending",d(),xi(z?"review-summary":K[g]?"feedback":"approve")}function Vi(s){if(W||y||Z)return;let m=r.components.find((Ji)=>Ji.id===g);if(!m)return;let U=n.decisions[m.id],gi=U?.revision===m.revision?U:void 0;I={id:m.id,name:m.name,action:s,previous:U?{...U}:void 0};let ci=K[m.id]??gi;if(n.decisions[m.id]={revision:m.revision,action:s,feedback:s==="revise"?ci?.feedback??"":"",split:s==="revise"&&(ci?.split??!1)},delete K[m.id],ai)J=!0,ai=!1;uo(m.id)}function vo(){if(W||y||Z)return;let s=r.components.find((gi)=>gi.id===g);if(!s)return;let m=n.decisions[s.id],U=m?.revision===s.revision?m:void 0;if(K[s.id]??={feedback:U?.feedback??"",split:U?.split??!1},J&&(e.querySelector(".workbench")?.clientHeight??0)<420)ai=!0,J=!1;z=!1,d(),xi("feedback")}function Ii(s){let m=`missing-${crypto.randomUUID()}`;n.missing.push({id:m,name:"Missing component",feedback:"",box:s}),n.inventoryConfirmed=!1,z=!1,g=m,H="component",G=!1,A=null,R=null,d(),e.querySelector("#missing-name")?.focus()}function d(){P?.disconnect();let s=e.activeElement,m=s?.id,U=s?.dataset.select,gi=window.scrollX,ci=window.scrollY,Ji=e.querySelector(".inventory")?.scrollLeft??0,wi=Hi===g,yo=H==="component"&&(!wi||Ui!==H);Ui=H;let zo=wi?e.querySelector(".inspection-content")?.scrollTop??0:0,ko=wi&&(e.querySelector(".changed-files")?.open??!1),jo=e.querySelector(".comparison-slot")?.clientHeight??200,Oi=e.querySelector(".pan-viewport"),Ai=Hi===g&&Ti===O,qo=Ai?Oi?.scrollLeft??0:0,Mo=Ai?Oi?.scrollTop??0:0;Hi=g,Ti=O;let h=r.components.find((i)=>i.id===g),k=n.missing.find((i)=>i.id===g),Ri=h?.box??k?.box,Di=h?r.components.indexOf(h)+1:r.components.length+n.missing.findIndex((i)=>i.id===g)+1,Si=h?n.decisions[h.id]:void 0,D=Si?.revision===h?.revision?Si:void 0,Ni=h?K[h.id]:void 0,ii=Object.keys(K).length>0,Ho=h?!r.components.some((i)=>i.id!==h.id&&S(i,n).kind==="pending"):!1,$i=I&&!y?`
${l(I.name)} ${I.action==="approve"?"approved":"flagged for repair"}.
`:"",b=li(r,n),j=x.history,_=h?ji(h.id,j):void 0,ni=j?.packet.components.find((i)=>i.id===h?.id),T=Z&&!!ni,t=T?ni:h,E=T?j.packet:r,Fi=Object.values(j?.changes??{}),Ci=Fi.filter((i)=>i.kind==="changed").length,Pi=Fi.filter((i)=>i.kind==="added").length,Gi=r.components.filter((i)=>S(i,n,j).kind==="approved"&&ji(i.id,j).carried).length,mi=(i)=>{let o=S(i,n,j);return y&&o.kind==="feedback"?{...o,label:"Changes requested"}:o},Vo=b.approved+b.revisions+n.missing.length,io=(f==="pending"?w():r.components).filter((i)=>Qi(i,n,f)),Jo=[b.revisions+n.missing.length?`${b.revisions+n.missing.length} feedback ready`:"",Gi?`${Gi} ${Gi===1?"approval":"approvals"} kept`:"",Ci?`${Ci} changed`:"",Pi?`${Pi} added`:"",j?.removed.length?`${j.removed.length} removed`:""].filter(Boolean).join(" · "),No=Mi||(ii?"Save or cancel your open feedback before sending.":y?x.preview?"Preview submitted. No run changed.":"Review submitted.":b.hasFeedback?"Ready to send for corrections.":b.pending?`${b.pending} left to review`:!n.inventoryConfirmed?"Confirm the map is complete.":"Ready to continue."),B=t?qi(t):null,Ki=t?.preview.kind==="image"&&!B?.code,Yi=Ki||t?.material?.alpha==="transparent",L=!!(t?.context&&ei==="context"),oo=t&&(L?t.context?.kind!=="image":t.preview.kind==="page"),_i=L&&t?.context?t.context.url:t?.preview.url,$o=B?.code?`${B.label} · ${B.captured?"captured from code":"live preview"}`:t?.material?`${t.material.alpha==="transparent"?"Transparent":t.material.alpha==="opaque"?"Opaque":"Transparency unverified"} ${t.material.format}`:"Raster · transparency unverified";e.innerHTML=`
-

${y?"Review record.":"Review the components."}

${l(r.title)} · Round ${r.round}

${y?'Submitted · read-only':x.preview?'Interactive preview':""}
+ +/* A full-page checkpoint is one comparison, not a component inventory. */ +.assembled-review>.workbench{grid-template-columns:minmax(0,1fr);gap:0} +.assembled-review .workbench>.inspector{display:flex} +.assembled-review .inspection-content{padding:16px 20px;scrollbar-gutter:auto} +.assembled-review .compare{max-width:none;gap:20px} +.assembled-review .pan-viewport{background:var(--canvas);border:1px solid #bdc6bd;box-shadow:inset 0 2px 7px #263d2a12} +.assembled-review .inspector>.review-form{padding:12px 20px;max-height:55%} +.assembled-review .decisions{display:flex;justify-content:flex-end;gap:10px} +.assembled-review #approve{order:2;min-width:180px} +.assembled-review .page-review-status{font-size:11px;color:var(--muted);text-align:right;line-height:1.4;overflow-wrap:anywhere} +.assembled-review .page-review-status:empty{display:none} +.assembled-review .review-form:has(#feedback-form){max-height:55%} +.assembled-review .feedback-field textarea{min-height:64px;max-height:120px} +.assembled-review .repair-context:empty{display:none} +@media(max-width:800px){ + .assembled-review>.workbench{display:flex;padding:10px} + .assembled-review .inspection-content{padding:10px} + .assembled-review .compare{grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:8px} + .assembled-review .inspector>.review-form{padding:10px} + .assembled-review .decisions>button{flex:1;min-width:0} + .assembled-review .page-review-status{text-align:left} +} +`;var Lo={chevronDown:'',code:'',image:'',expand:'',compact:'',hideTray:'',showTray:'',next:'',mark:'',close:'',undo:'',external:'',zoom:''};function G(p){return``}var d=(p)=>p.replace(/[&<>"']/g,(e)=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[e]),ii=(p)=>`${p*100}%`,oi=(p)=>{let e=new URL(p,location.href);if(!["http:","https:"].includes(e.protocol))throw Error("Unsupported preview URL");return d(e.href)};function uo(p,e,x){let r=p.attachShadow({mode:"open"}),n=structuredClone(x.initialDraft??co(e)),l=e.stage==="hero"&&e.components.length===1&&!n.missing.length,ei=()=>[...e.components].sort((f,w)=>S(f,n,x.history).priority-S(w,n,x.history).priority),h=ei().find((f)=>S(f,n,x.history).kind!=="approved")?.id??e.components[0]?.id,z=di(e,n).pending?"pending":"reviewed",K=!1,_=!1,u=x.completed??!1,si="",J={},y=!!x.completed||!di(e,n).pending,U=null,yo=/Mac|iPhone|iPad/.test(navigator.platform)?"⌘Enter":"Ctrl+Enter",F=!1,V=!1,C=!1,N=!0,xi=!1,H="comp",Ti=H,I="fit",hi="checker",X=!1,ri="isolated",Ji,Ai="fit",O=null,R=null,P=null,gi='',wi='',Bi=(f)=>`left:${ii(f.x)};top:${ii(f.y)};width:${ii(f.w)};height:${ii(f.h)}`;function ti(f){r.getElementById(f)?.focus({preventScroll:!0})}function zo(f){let w=mo(e,n,f);if(y=!w,w)h=w;H="component",X=!1,F=!1,I="fit",ri="isolated",z=y?"reviewed":"pending",g(),ti(y?"review-summary":J[h]?"feedback":"approve")}async function Ii(){if(_||u||X||Object.keys(J).length)return;_=!0,si="",g();try{await x.onSubmit(wo(e,n)),u=!0,y=!0,K=!1,z="reviewed"}catch(f){si=f instanceof Error?f.message:"Could not save. Try again."}finally{_=!1,g()}}function Ni(f){if(_||u||X)return;let w=e.components.find(($i)=>$i.id===h);if(!w)return;let T=n.decisions[w.id],fi=T?.revision===w.revision?T:void 0;U={id:w.id,name:w.name,action:f,previous:T?{...T}:void 0};let mi=J[w.id]??fi;if(n.decisions[w.id]={revision:w.revision,action:f,feedback:f==="revise"?mi?.feedback??"":"",split:f==="revise"&&(mi?.split??!1)},delete J[w.id],xi)N=!0,xi=!1;if(l)n.inventoryConfirmed=f==="approve",U=null,Ii();else zo(w.id)}function ko(){if(_||u||X)return;let f=e.components.find((fi)=>fi.id===h);if(!f)return;let w=n.decisions[f.id],T=w?.revision===f.revision?w:void 0;if(J[f.id]??={feedback:T?.feedback??"",split:T?.split??!1},N&&(r.querySelector(".workbench")?.clientHeight??0)<420)xi=!0,N=!1;y=!1,g(),ti("feedback")}function Oi(f){let w=`missing-${crypto.randomUUID()}`;n.missing.push({id:w,name:"Missing component",feedback:"",box:f}),n.inventoryConfirmed=!1,y=!1,h=w,H="component",K=!1,O=null,R=null,g(),r.querySelector("#missing-name")?.focus()}function g(){P?.disconnect();let f=r.activeElement,w=f?.id,T=f?.dataset.select,fi=window.scrollX,mi=window.scrollY,$i=r.querySelector(".inventory")?.scrollLeft??0,bi=Ji===h,jo=H==="component"&&(!bi||Ti!==H);Ti=H;let qo=bi?r.querySelector(".inspection-content")?.scrollTop??0:0,Mo=bi&&(r.querySelector(".changed-files")?.open??!1),Ho=r.querySelector(".comparison-slot")?.clientHeight??200,Ri=r.querySelector(".pan-viewport"),Di=Ji===h&&Ai===I,Vo=Di?Ri?.scrollLeft??0:0,Jo=Di?Ri?.scrollTop??0:0;Ji=h,Ai=I;let c=e.components.find((i)=>i.id===h),k=n.missing.find((i)=>i.id===h),Si=c?.box??k?.box,Fi=c?e.components.indexOf(c)+1:e.components.length+n.missing.findIndex((i)=>i.id===h)+1,Ci=c?n.decisions[c.id]:void 0,D=Ci?.revision===c?.revision?Ci:void 0,vi=c?J[c.id]:void 0,ni=Object.keys(J).length>0,No=c?!e.components.some((i)=>i.id!==c.id&&S(i,n).kind==="pending"):!1,Gi=U&&!u?`
${d(U.name)} ${U.action==="approve"?"approved":"flagged for repair"}.
`:"",m=di(e,n),j=x.history,E=c?Hi(c.id,j):void 0,pi=j?.packet.components.find((i)=>i.id===c?.id),A=X&&!!pi,t=A?pi:c,L=A?j.packet:e,Pi=Object.values(j?.changes??{}),io=Pi.filter((i)=>i.kind==="changed").length,oo=Pi.filter((i)=>i.kind==="added").length,Ki=e.components.filter((i)=>S(i,n,j).kind==="approved"&&Hi(i.id,j).carried).length,ui=(i)=>{let o=S(i,n,j);return u&&o.kind==="feedback"?{...o,label:"Changes requested"}:o},$o=m.approved+m.revisions+n.missing.length,eo=(z==="pending"?ei():e.components).filter((i)=>Wi(i,n,z)),Go=[m.revisions+n.missing.length?`${m.revisions+n.missing.length} feedback ready`:"",Ki?`${Ki} ${Ki===1?"approval":"approvals"} kept`:"",io?`${io} changed`:"",oo?`${oo} added`:"",j?.removed.length?`${j.removed.length} removed`:""].filter(Boolean).join(" · "),Ko=si||(ni?"Save or cancel your open feedback before sending.":u?x.preview?"Preview submitted. No run changed.":"Review submitted.":m.hasFeedback?"Ready to send for corrections.":m.pending?`${m.pending} left to review`:!n.inventoryConfirmed?"Confirm the map is complete.":"Ready to continue."),B=t?Vi(t):null,Yi=t?.preview.kind==="image"&&!B?.code,_i=Yi||t?.material?.alpha==="transparent",Q=!!(t?.context&&ri==="context"),ro=t&&(Q?t.context?.kind!=="image":t.preview.kind==="page"),Ei=Q&&t?.context?t.context.url:t?.preview.url,Yo=B?.code?`${B.label} · ${B.captured?"captured from code":"live preview"}`:t?.material?`${t.material.alpha==="transparent"?"Transparent":t.material.alpha==="opaque"?"Opaque":"Transparency unverified"} ${t.material.format}`:"Raster · transparency unverified";r.innerHTML=`
+

${u?"Review record.":l?"Review the assembled page.":"Review the components."}

${d(e.title)} · Round ${e.round}

${u?'Submitted · read-only':x.preview?'Interactive preview':""}
${x.preview?'

Historical hotel artwork for testing this interface. Decisions stay in this preview; no run is changed.

':""} - ${j?`

${b.pending} ${b.pending===1?"component":"components"} to review${Jo}

${b.pending?``:""}${j.removed.length?`
Removed from the map

${j.removed.map((i)=>l(i.name)).join(" · ")}. Confirm these omissions are intentional before accepting the map.

`:""}
`:""} -
-
+ ${j&&!l?`

${m.pending} ${m.pending===1?"component":"components"} to review${Go}

${m.pending?``:""}${j.removed.length?`
Removed from the map

${j.removed.map((i)=>d(i.name)).join(" · ")}. Confirm these omissions are intentional before accepting the map.

`:""}
`:""} + ${!l?`
`:""} +
${!l?`
-

Approved comp

${!y?``:""}
-
- Approved composition for ${l(r.title)} - ${Ri&&!z?`
`:""} - ${r.components.map((i,o)=>{let a=mi(i);return``}).join("")} - ${n.missing.map((i,o)=>``).join("")} +

Approved comp

${!u?``:""}
+
+ Approved composition for ${d(e.title)} + ${Si&&!y?`
`:""} + ${e.components.map((i,o)=>{let a=ui(i);return``}).join("")} + ${n.missing.map((i,o)=>``).join("")}
-
# To review${si} ${y?"Changes requested":"Feedback ready"}${hi} Approved
- ${G?'
Draw around the missing piece.
':""} -
-
-

${z?"Review summary":`${Di} ${l(h?.name??k?.name??"Component")}`}

- ${z?`

${y?"Review sent.":"All components reviewed."}

${b.approved} approved · ${b.revisions} flagged for repair${n.missing.length?` · ${n.missing.length} missing`:""}

${y?"Your decisions are saved.":b.hasFeedback?"Send your feedback to start the next repair round.":"Confirm nothing is missing, then approve and continue."}

${r.components.map((i)=>{let o=n.decisions[i.id],a=mi(i);return``}).join("")}${n.missing.map((i)=>``).join("")}
${$i?`
${$i}
`:""}`:h?` +
# To review${wi} ${u?"Changes requested":"Feedback ready"}${gi} Approved
+ ${K?'
Draw around the missing piece.
':""} +
`:""} +
+ ${!l?`

${y?"Review summary":`${Fi} ${d(c?.name??k?.name??"Component")}`}

`:""}
+ ${y&&!l?`

${u?"Review sent.":"All components reviewed."}

${m.approved} approved · ${m.revisions} flagged for repair${n.missing.length?` · ${n.missing.length} missing`:""}

${u?"Your decisions are saved.":m.hasFeedback?"Send your feedback to start the next repair round.":"Confirm nothing is missing, then approve and continue."}

${e.components.map((i)=>{let o=n.decisions[i.id],a=ui(i);return``}).join("")}${n.missing.map((i)=>``).join("")}
${Gi?`
${Gi}
`:""}`:c?` ${j?`
- ${T&&_?.prior?.action==="revise"?`

Previous feedback · Round ${_.feedbackRound}

${l(_.prior.feedback||"No written feedback was supplied.")}
${_.prior.split?"

Requested: split into separately reviewable components.

":""}
`:_?.carried?'

Unchanged · approval kept

':""} - ${_?.change?.kind==="changed"?`
${_.change.files.length?`${_.change.files.length} changed ${_.change.files.length===1?"file":"files"}`:_.change.reasons.includes("region")?"Region changed":ni?.note!==h.note?"Description changed · files unchanged":"Component definition changed · files unchanged"}${_.change.files.length?`
    ${_.change.files.map((i)=>`
  • ${l(i)}
  • `).join("")}
`:""}${ni&&ni.note!==h.note?`
Previous description
${l(ni.note)}
Current description
${l(h.note)}
`:""}
`:""} + ${A&&E?.prior?.action==="revise"?`

Previous feedback · Round ${E.feedbackRound}

${d(E.prior.feedback||"No written feedback was supplied.")}
${E.prior.split?"

Requested: split into separately reviewable components.

":""}
`:E?.carried?'

Unchanged · approval kept

':""} + ${E?.change?.kind==="changed"?`
${E.change.files.length?`${E.change.files.length} changed ${E.change.files.length===1?"file":"files"}`:E.change.reasons.includes("region")?"Region changed":pi?.note!==c.note?"Description changed · files unchanged":"Component definition changed · files unchanged"}${E.change.files.length?`
    ${E.change.files.map((i)=>`
  • ${d(i)}
  • `).join("")}
`:""}${pi&&pi.note!==c.note?`
Previous description
${d(pi.note)}
Current description
${d(c.note)}
`:""}
`:""}
`:""} -

${l(t.name)}

${ni?`
`:""}
${t?.preview.kind==="image"?`${$("external")}`:""}
+

${d(t.name)}

${pi?`
`:""}
${t?.preview.kind==="image"?`${G("external")}`:""}
-
${T?`Comp · Round ${j.packet.round}`:"In the comp"}
Reference region for ${l(t.name)}
-
${T?`Previous · Round ${j.packet.round}`:L?"In context":j?`${B.caption} · Round ${r.round}`:B.caption}
${!oo?`Produced ${l(t.name)}`:``}${F?`Reference overlay`:""}
+
${A?`Comp · Round ${j.packet.round}`:l?"Approved comp":"In the comp"}
Reference region for ${d(t.name)}
+
${A?`Previous · Round ${j.packet.round}`:l?"Assembled page":Q?"In context":j?`${B.caption} · Round ${e.round}`:B.caption}
${!ro?`Produced ${d(t.name)}`:``}${F?`Reference overlay`:""}
- ${Yi||t.context?`
${t.context?`
`:""}${Yi?`
`:""}
`:""} -
${$(B.code?"code":"image")}${l($o)}${t?.material?`${t.material.width} × ${t.material.height} px`:""}
${E.stage==="components"&&B?.captured&&!t?.preview.isolation?'

Legacy region capture · may include overlapping components.

':""}${t?.context?.layering&&(Ki||L)?`

${l(t.context.layering)}

`:""} -

${l(t.note)}

-
${$i}${T?'

Viewing the previous round. Return to Current to make a decision.

':""}${y?`
${D?.action==="approve"?"Approved":D?.action==="revise"?"Changes requested":"Not reviewed"}Submitted in round ${r.round} · read-only
`:`
Your review Round ${r.round}${T?"

Return to Current to review this round.

":""}
${D?``:""}
`} - ${Ni?`
`:D?.action==="revise"?`

${l(D.feedback||"No note — agent will diagnose.")}

`:""} -
`:k?`

This piece will be added to the unresolved inventory.

${["x","y","w","h"].map((i)=>``).join("")}
`:"

No components supplied.

"} + ${_i||t.context?`
${t.context?`
`:""}${_i?`
`:""}
`:""} +
${!l?`
${G(B.code?"code":"image")}${d(Yo)}${t?.material?`${t.material.width} × ${t.material.height} px`:""}
${L.stage==="components"&&B?.captured&&!t?.preview.isolation?'

Legacy region capture · may include overlapping components.

':""}${t?.context?.layering&&(Yi||Q)?`

${d(t.context.layering)}

`:""} +

${d(t.note)}

+
`:""}
${Gi}${A?'

Viewing the previous round. Return to Current to make a decision.

':""}${u?`
${D?.action==="approve"?"Approved":D?.action==="revise"?"Changes requested":"Not reviewed"}Submitted in round ${e.round} · read-only
`:`
${!l?`
Your review Round ${e.round}${A?"

Return to Current to review this round.

":""}
`:""}${D&&!l?``:""}
`} + ${vi?`
`:D?.action==="revise"?`

${d(D.feedback||"No note — agent will diagnose.")}

`:""} + ${l?`

${d(si||(u?"Your decision is saved.":_?"Sending…":vi?"":"Approval confirms the composition and that nothing is missing."))}

`:""}
`:k?`

This piece will be added to the unresolved inventory.

${["x","y","w","h"].map((i)=>``).join("")}
`:"

No components supplied.

"}
-

Components

-
${io.map((i)=>{let o=r.components.indexOf(i),a=mi(i);return``}).join("")}${(f==="pending"?[]:n.missing).map((i,o)=>``).join("")}${!io.length&&(f==="pending"||!n.missing.length)?`

${f==="pending"?"Nothing left to review. Your decisions are ready.":"No components reviewed yet."}

`:""}
- ${y?`
Round ${r.round} submitted · read-only${b.approved} approved · ${b.revisions} changes requested
`:`
${!b.pending&&!ii?``:""}

${l(No)}

`} -
`;let N=e.querySelector("#comparison-dialog"),Y=e.querySelector(".comparison-panel"),pi=e.querySelector(".comparison-slot");if(V&&Y&&pi)pi.style.height=`${jo}px`,N.append(Y),N.showModal();if(T)e.querySelectorAll(".decisions button,#feedback,#split,#save-feedback,#cancel-feedback,#undo-decision,#approve-rest,#submit,#inventory-confirm").forEach((i)=>i.disabled=!0);if(e.querySelector(".inspection-content").scrollTop=zo,y||W)e.querySelectorAll(".decisions button,#save-feedback,#cancel-feedback,#undo-decision,#approve-rest,#mark,#inventory-confirm,#missing-name,#missing-feedback,#feedback,#split,#remove-missing,[data-coordinate]").forEach((i)=>i.disabled=!0);if(e.querySelector(".inventory").scrollLeft=Ji,!wi&&J)Array.from(e.querySelectorAll(".inventory [data-select]")).find((i)=>i.dataset.select===g)?.scrollIntoView({block:"nearest",inline:"nearest"});if(m)e.getElementById(m)?.focus({preventScroll:!0});else if(U)Array.from(e.querySelectorAll(".item[data-select]")).find((i)=>i.dataset.select===U)?.focus({preventScroll:!0});let u=(i,o)=>e.querySelector(`#${i}`)?.addEventListener("click",o);function ro(i,o=!1){if(G)return;if(z=!1,g=i,H="component",F=!1,O="fit",ei="isolated",Z=!1,d(),o)e.querySelector("#expand-comparison")?.click()}e.querySelectorAll("[data-select]").forEach((i)=>i.onclick=()=>ro(i.dataset.select,i.classList.contains("pin")&&r.components.some((o)=>o.id===i.dataset.select))),u("previous-round",()=>{Z=!0,d()}),u("current-round",()=>{Z=!1,d()}),u("review-changes",()=>{let i=w().filter((v)=>mi(v).kind==="pending"),o=i.findIndex((v)=>v.id===g),a=i[(o+1)%i.length];if(a)z=!1,g=a.id,H="component",f="pending",Z=!1,O="fit",F=!1,ei="isolated",d()}),e.querySelectorAll("[data-filter]").forEach((i)=>i.onclick=()=>{f=i.dataset.filter,z=!b.pending&&f==="pending";let o=w().filter((v)=>Qi(v,n,f));if(!(f!=="pending"&&n.missing.some((v)=>v.id===g))&&!o.some((v)=>v.id===g)&&o.length)g=o[0].id,H="component",Z=!1,O="fit",F=!1,ei="isolated";d()}),u("show-summary",()=>{z=!0,H="component",f="reviewed",d(),xi("review-summary")}),u("approve",()=>Vi("approve")),u("revise",vo),u("clear",()=>{if(h)delete n.decisions[h.id],delete K[h.id];I=null,z=!1,f="pending",d()}),u("cancel-feedback",()=>{if(h)delete K[h.id];if(ai)J=!0,ai=!1;d(),xi("revise")}),e.querySelector("#feedback-form")?.addEventListener("submit",(i)=>{i.preventDefault(),Vi("revise")}),e.querySelector("#feedback")?.addEventListener("keydown",(i)=>{let o=i;if(o.key==="Enter"&&(o.metaKey||o.ctrlKey)&&!o.isComposing)o.preventDefault(),o.stopPropagation(),Vi("revise")}),u("undo-decision",()=>{if(!I||W||y)return;let i=I;if(i.previous)n.decisions[i.id]=i.previous;else delete n.decisions[i.id];delete K[i.id],g=i.id,z=!1,Z=!1,H="component",f="all",I=null,d(),xi("approve")}),u("overlay",()=>{F=!F,d()}),u("isolated",()=>{ei="isolated",d()}),u("context",()=>{ei="context",d()}),e.querySelector("#zoom")?.addEventListener("change",(i)=>{let o=i.target.value;O=o==="fit"?"fit":Number(o),d()}),u("background-checker",()=>{di="checker",d()}),u("background-page",()=>{di="page",d()}),u("show-all",()=>{C=!C,J=!0,d()}),u("toggle-tray",()=>{ai=!1,J=!J,d()}),u("show-comp",()=>{H="comp",d()}),u("show-component",()=>{H="component",d()}),u("mark",()=>{G=!G,d()}),u("add-box",()=>Ii({x:0.35,y:0.35,w:0.2,h:0.2})),u("remove-missing",()=>{n.missing=n.missing.filter((i)=>i.id!==g),g=r.components[0]?.id,d()}),u("approve-rest",()=>{if(ii)return;n=go(r,n),I=null,z=!0,f="reviewed",H="component",d(),xi("review-summary")}),e.querySelector("#inventory-confirm")?.addEventListener("change",(i)=>{n.inventoryConfirmed=i.target.checked,d()}),e.querySelector("#feedback")?.addEventListener("input",(i)=>{if(h&&K[h.id])K[h.id].feedback=i.target.value}),e.querySelector("#split")?.addEventListener("change",(i)=>{if(h&&K[h.id])K[h.id].split=i.target.checked}),e.querySelector("#missing-name")?.addEventListener("input",(i)=>{if(k)k.name=i.target.value;let o=e.querySelector("#submit");if(o)o.disabled=ii||W||y||!li(r,n).canSubmit}),e.querySelector("#missing-feedback")?.addEventListener("input",(i)=>{if(k)k.feedback=i.target.value}),e.querySelectorAll("[data-coordinate]").forEach((i)=>i.addEventListener("change",()=>{if(!k)return;let o=i.dataset.coordinate,a=Number(i.value)/100;if(Number.isFinite(a))k.box[o]=Math.max(o==="w"||o==="h"?0.001:0,Math.min(1,a));k.box.w=Math.min(k.box.w,1-k.box.x),k.box.h=Math.min(k.box.h,1-k.box.y),d()})),u("submit",async()=>{if(ii||W||y)return;W=!0,Mi="",d();try{await x.onSubmit(fo(r,n)),y=!0,z=!0,G=!1,f="reviewed"}catch(i){Mi=i instanceof Error?i.message:"Could not save. Try again."}finally{W=!1,d()}});let X=e.querySelector(".map");function Ei(i){let o=X.getBoundingClientRect();return{x:Math.max(0,Math.min(1,(i.clientX-o.left)/o.width)),y:Math.max(0,Math.min(1,(i.clientY-o.top)/o.height))}}function eo(i){let o=Ei(i);return r.components.filter(({box:a})=>o.x>=a.x&&o.x<=a.x+a.w&&o.y>=a.y&&o.y<=a.y+a.h).sort((a,v)=>a.box.w*a.box.h-v.box.w*v.box.h)[0]}X.addEventListener("click",(i)=>{if(G||i.target.closest("[data-select]"))return;let o=eo(i);if(o)ro(o.id,!0)}),X.addEventListener("pointermove",(i)=>{if(!G)X.style.cursor=eo(i)?"zoom-in":""}),X.addEventListener("pointerdown",(i)=>{if(!G)return;A=Ei(i),X.setPointerCapture(i.pointerId),i.preventDefault()}),X.addEventListener("pointermove",(i)=>{if(!A)return;let o=Ei(i);R={x:Math.min(A.x,o.x),y:Math.min(A.y,o.y),w:Math.abs(o.x-A.x),h:Math.abs(o.y-A.y)};let a=e.querySelector(".draw-box");a.hidden=!1,a.style.cssText=Bi(R)}),X.addEventListener("pointerup",()=>{if(R&&R.w>0.01&&R.h>0.01)Ii(R);else A=null,R=null}),X.addEventListener("pointercancel",()=>{A=null,R=null,d()});let no=e.querySelector(".output"),bi=e.querySelector("iframe"),po=e.querySelector(".workbench"),ti=e.querySelector(".inspection-content"),Li=e.querySelector(".inspector");function Xi(){if(!ti||!Li)return;Li.dataset.scrollAbove=String(ti.scrollTop>1),Li.dataset.scrollBelow=String(ti.scrollHeight-ti.clientHeight-ti.scrollTop>1)}ti?.addEventListener("scroll",Xi,{passive:!0});function ui(){let i=e.querySelector(".map-space");if(i&&i.clientWidth&&i.clientHeight){let c=Wi(r.comp.width,r.comp.height,Math.max(1,i.clientWidth-32),Math.max(1,i.clientHeight-32),"fit");X.style.width=`${c.width}px`,X.style.height=`${c.height}px`}let o=e.querySelector(".inspection-content"),a=Array.from(e.querySelectorAll(".pan-viewport"));if(o?.clientHeight){let c=V?Math.max(100,N.clientHeight-(Y?.querySelector(".compare-toolbar")?.clientHeight??0)-(Y?.querySelector(".expanded-title")?.clientHeight??0)-(Y?.querySelector(".view-controls")?.clientHeight??0)-112):Math.min(248,Math.max(100,o.clientHeight-((a[0]?.getBoundingClientRect().top??o.getBoundingClientRect().top)-o.getBoundingClientRect().top+o.scrollTop)-(Y?.querySelector(".view-controls")?.clientHeight??0)-12));a.forEach((q)=>q.style.height=`${c}px`)}if(t&&a.length){let c=Wi(t.box.w*E.comp.width,t.box.h*E.comp.height,Math.min(...a.map((q)=>q.clientWidth)),Math.min(...a.map((q)=>q.clientHeight)),O);e.querySelectorAll(".crop-stage").forEach((q)=>{q.style.width=`${c.width}px`,q.style.height=`${c.height}px`})}if(a.forEach((c)=>{let q=c.scrollWidth>c.clientWidth+1||c.scrollHeight>c.clientHeight+1;c.classList.toggle("pannable",q),c.style.cursor=V?"":"zoom-in",c.setAttribute("role",V?"region":"button"),c.title=V?q?"Move your pointer to pan. You can also scroll, swipe, or use arrow keys.":"":"Click to enlarge comparison"}),no&&bi&&t){let c=no.clientWidth/(t.box.w*E.comp.width);bi.style.transform=`scale(${c})`,bi.style.left=`${-t.box.x*E.comp.width*c}px`,bi.style.top=`${-t.box.y*E.comp.height*c}px`}let v=po.getBoundingClientRect(),Q=e.querySelector(".region"),yi=e.querySelector(".number"),zi=e.querySelector(".connector path");if(Q&&yi&&zi){let c=Q.getBoundingClientRect(),q=yi.getBoundingClientRect(),M=c.right-v.left,ki=c.top+c.height/2-v.top,lo=q.left-v.left-8,Go=q.top+q.height/2-v.top;zi.setAttribute("d",`M ${M} ${ki} H ${lo-14} V ${Go} H ${lo}`)}}function fi(i){if(!Y||!pi||i===V)return;let o=e.querySelector("#expand-comparison"),a=(V?N:Y).getBoundingClientRect(),v=window.matchMedia("(prefers-reduced-motion: reduce)").matches,Q=Y.querySelector(".pan-viewport"),yi=Q?Q.scrollLeft/Math.max(1,Q.scrollWidth-Q.clientWidth):0,zi=Q?Q.scrollTop/Math.max(1,Q.scrollHeight-Q.clientHeight):0,c=()=>Y.querySelectorAll(".pan-viewport").forEach((M)=>{M.scrollLeft=yi*Math.max(0,M.scrollWidth-M.clientWidth),M.scrollTop=zi*Math.max(0,M.scrollHeight-M.clientHeight)}),q=()=>{pi.append(Y),N.close(),pi.style.height="",V=!1,o.innerHTML=$("expand"),o.setAttribute("aria-label","Enlarge comparison"),o.title="Enlarge comparison",o.setAttribute("aria-expanded","false"),ui(),c(),o.focus({preventScroll:!0})};if(i){pi.style.height=`${a.height}px`,N.append(Y),N.showModal(),V=!0,o.innerHTML=$("compact"),o.setAttribute("aria-label","Restore comparison"),o.title="Restore comparison (Esc)",o.setAttribute("aria-expanded","true"),ui(),c(),o.focus({preventScroll:!0});let M=N.getBoundingClientRect();if(!v)N.animate([{transform:`translate(${a.x-M.x}px,${a.y-M.y}px) scale(${a.width/M.width},${a.height/M.height})`,opacity:0.6},{transform:"none",opacity:1}],{duration:240,easing:"cubic-bezier(.2,.8,.2,1)"})}else{if(o.disabled)return;let M=pi.getBoundingClientRect();if(v){q();return}o.disabled=!0;let ki=N.animate([{transform:"none",opacity:1},{transform:`translate(${M.x-a.x}px,${M.y-a.y}px) scale(${M.width/a.width},${M.height/a.height})`,opacity:0.6}],{duration:200,easing:"cubic-bezier(.4,0,.2,1)",fill:"forwards"});ki.finished.then(()=>{if(N.isConnected)ki.cancel(),o.disabled=!1,q()}).catch(()=>{})}}u("expand-comparison",()=>fi(!V)),N.addEventListener("cancel",(i)=>{i.preventDefault(),i.stopPropagation(),fi(!1)}),N.addEventListener("keydown",(i)=>{if(i.key==="Escape")i.preventDefault(),i.stopPropagation(),fi(!1)}),P=new ResizeObserver(()=>{ui(),Xi()}),P.observe(po),P.observe(N);let ao=e.querySelector(".inspection-content");if(ao)P.observe(ao);let xo=e.querySelector(".repair-context");if(xo)P.observe(xo);let to=Y?.querySelector(".compare-toolbar");if(to)P.observe(to);ui();let vi=Array.from(e.querySelectorAll(".pan-viewport"));if(vi.forEach((i)=>{i.scrollLeft=qo,i.scrollTop=Mo}),vi.forEach((i)=>{i.querySelectorAll("img").forEach((o)=>o.draggable=!1),i.addEventListener("click",()=>{if(!V)fi(!0)}),i.addEventListener("pointermove",(o)=>{if(o.pointerType!=="mouse"||o.buttons||!i.classList.contains("pannable"))return;let a=i.getBoundingClientRect();i.scrollLeft=Zi(o.clientX,a.left,i.clientWidth,i.scrollWidth),i.scrollTop=Zi(o.clientY,a.top,i.clientHeight,i.scrollHeight)}),i.addEventListener("keydown",(o)=>{if(!V&&(o.key==="Enter"||o.key===" ")){o.preventDefault(),o.stopPropagation(),fi(!0);return}let v={ArrowLeft:[-48,0],ArrowRight:[48,0],ArrowUp:[0,-48],ArrowDown:[0,48]}[o.key];if(!v)return;o.preventDefault(),o.stopPropagation(),i.scrollLeft+=v[0],i.scrollTop+=v[1]})}),vi.forEach((i)=>i.addEventListener("scroll",()=>{for(let o of vi)if(o!==i){if(o.scrollLeft!==i.scrollLeft)o.scrollLeft=i.scrollLeft;if(o.scrollTop!==i.scrollTop)o.scrollTop=i.scrollTop}})),yo&&window.matchMedia("(max-width:800px)").matches){let i=e.querySelector(".inspection-content"),o=e.querySelector(".compare");if(i&&o)i.scrollTop+=o.getBoundingClientRect().top-i.getBoundingClientRect().top}if(Xi(),window.scrollTo(gi,ci),!y)x.onDraftChange?.(structuredClone(n))}return d(),{destroy(){P?.disconnect(),e.replaceChildren()},getDraft(){return structuredClone(n)}}}async function _o(){let p=document.getElementById("review"),r=await fetch("/packet",{cache:"no-store"});if(!r.ok)throw Error("The review packet could not be loaded. Reload to retry.");let x=await r.json();if(x.sourceStatus){let e=document.createElement("p");e.textContent=x.sourceStatus,p.before(e)}mo(p,x.packet,{initialDraft:x.draft,history:x.history,completed:!!x.receipt,onSubmit:async(e)=>{let n=await fetch("/decision",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)}),w=await n.json();if(!n.ok)throw Error(w.error??"The review could not be saved. Try again.")}})}_o().catch((p)=>{let r=document.createElement("p");r.textContent=p instanceof Error?p.message:String(p),document.getElementById("review")?.replaceChildren(r)});})(); + ${!l?`

Components

+
${eo.map((i)=>{let o=e.components.indexOf(i),a=ui(i);return``}).join("")}${(z==="pending"?[]:n.missing).map((i,o)=>``).join("")}${!eo.length&&(z==="pending"||!n.missing.length)?`

${z==="pending"?"Nothing left to review. Your decisions are ready.":"No components reviewed yet."}

`:""}
`:""} + ${l?"":u?`
Round ${e.round} submitted · read-only${m.approved} approved · ${m.revisions} changes requested
`:`
${!m.pending&&!ni?``:""}

${d(Ko)}

`} + `;let $=r.querySelector("#comparison-dialog"),Y=r.querySelector(".comparison-panel"),ai=r.querySelector(".comparison-slot");if(V&&Y&&ai)ai.style.height=`${Ho}px`,$.append(Y),$.showModal();if(A)r.querySelectorAll(".decisions button,#feedback,#split,#save-feedback,#cancel-feedback,#undo-decision,#approve-rest,#submit,#inventory-confirm").forEach((i)=>i.disabled=!0);if(r.querySelector(".inspection-content").scrollTop=qo,u||_)r.querySelectorAll(".decisions button,#save-feedback,#cancel-feedback,#undo-decision,#approve-rest,#mark,#inventory-confirm,#missing-name,#missing-feedback,#feedback,#split,#remove-missing,[data-coordinate]").forEach((i)=>i.disabled=!0);let no=r.querySelector(".inventory");if(no)no.scrollLeft=$i;if(!bi&&N)Array.from(r.querySelectorAll(".inventory [data-select]")).find((i)=>i.dataset.select===h)?.scrollIntoView({block:"nearest",inline:"nearest"});if(w)r.getElementById(w)?.focus({preventScroll:!0});else if(T)Array.from(r.querySelectorAll(".item[data-select]")).find((i)=>i.dataset.select===T)?.focus({preventScroll:!0});let b=(i,o)=>r.querySelector(`#${i}`)?.addEventListener("click",o);function po(i,o=!1){if(K)return;if(y=!1,h=i,H="component",F=!1,I="fit",ri="isolated",X=!1,g(),o)r.querySelector("#expand-comparison")?.click()}r.querySelectorAll("[data-select]").forEach((i)=>i.onclick=()=>po(i.dataset.select,i.classList.contains("pin")&&e.components.some((o)=>o.id===i.dataset.select))),b("previous-round",()=>{X=!0,g()}),b("current-round",()=>{X=!1,g()}),b("review-changes",()=>{let i=ei().filter((v)=>ui(v).kind==="pending"),o=i.findIndex((v)=>v.id===h),a=i[(o+1)%i.length];if(a)y=!1,h=a.id,H="component",z="pending",X=!1,I="fit",F=!1,ri="isolated",g()}),r.querySelectorAll("[data-filter]").forEach((i)=>i.onclick=()=>{z=i.dataset.filter,y=!m.pending&&z==="pending";let o=ei().filter((v)=>Wi(v,n,z));if(!(z!=="pending"&&n.missing.some((v)=>v.id===h))&&!o.some((v)=>v.id===h)&&o.length)h=o[0].id,H="component",X=!1,I="fit",F=!1,ri="isolated";g()}),b("show-summary",()=>{y=!0,H="component",z="reviewed",g(),ti("review-summary")}),b("approve",()=>Ni("approve")),b("revise",ko),b("clear",()=>{if(c)delete n.decisions[c.id],delete J[c.id];U=null,y=!1,z="pending",g()}),b("cancel-feedback",()=>{if(c)delete J[c.id];if(xi)N=!0,xi=!1;g(),ti("revise")}),r.querySelector("#feedback-form")?.addEventListener("submit",(i)=>{i.preventDefault(),Ni("revise")}),r.querySelector("#feedback")?.addEventListener("keydown",(i)=>{let o=i;if(o.key==="Enter"&&(o.metaKey||o.ctrlKey)&&!o.isComposing)o.preventDefault(),o.stopPropagation(),Ni("revise")}),b("undo-decision",()=>{if(!U||_||u)return;let i=U;if(i.previous)n.decisions[i.id]=i.previous;else delete n.decisions[i.id];delete J[i.id],h=i.id,y=!1,X=!1,H="component",z="all",U=null,g(),ti("approve")}),b("overlay",()=>{F=!F,g()}),b("isolated",()=>{ri="isolated",g()}),b("context",()=>{ri="context",g()}),r.querySelector("#zoom")?.addEventListener("change",(i)=>{let o=i.target.value;I=o==="fit"?"fit":Number(o),g()}),b("background-checker",()=>{hi="checker",g()}),b("background-page",()=>{hi="page",g()}),b("show-all",()=>{C=!C,N=!0,g()}),b("toggle-tray",()=>{xi=!1,N=!N,g()}),b("show-comp",()=>{H="comp",g()}),b("show-component",()=>{H="component",g()}),b("mark",()=>{K=!K,g()}),b("add-box",()=>Oi({x:0.35,y:0.35,w:0.2,h:0.2})),b("remove-missing",()=>{n.missing=n.missing.filter((i)=>i.id!==h),h=e.components[0]?.id,g()}),b("approve-rest",()=>{if(ni)return;n=so(e,n),U=null,y=!0,z="reviewed",H="component",g(),ti("review-summary")}),r.querySelector("#inventory-confirm")?.addEventListener("change",(i)=>{n.inventoryConfirmed=i.target.checked,g()}),r.querySelector("#feedback")?.addEventListener("input",(i)=>{if(c&&J[c.id])J[c.id].feedback=i.target.value}),r.querySelector("#split")?.addEventListener("change",(i)=>{if(c&&J[c.id])J[c.id].split=i.target.checked}),r.querySelector("#missing-name")?.addEventListener("input",(i)=>{if(k)k.name=i.target.value;let o=r.querySelector("#submit");if(o)o.disabled=ni||_||u||!di(e,n).canSubmit}),r.querySelector("#missing-feedback")?.addEventListener("input",(i)=>{if(k)k.feedback=i.target.value}),r.querySelectorAll("[data-coordinate]").forEach((i)=>i.addEventListener("change",()=>{if(!k)return;let o=i.dataset.coordinate,a=Number(i.value)/100;if(Number.isFinite(a))k.box[o]=Math.max(o==="w"||o==="h"?0.001:0,Math.min(1,a));k.box.w=Math.min(k.box.w,1-k.box.x),k.box.h=Math.min(k.box.h,1-k.box.y),g()})),b("submit",()=>{Ii()});let W=r.querySelector(".map");function Li(i){let o=W.getBoundingClientRect();return{x:Math.max(0,Math.min(1,(i.clientX-o.left)/o.width)),y:Math.max(0,Math.min(1,(i.clientY-o.top)/o.height))}}function ao(i){let o=Li(i);return e.components.filter(({box:a})=>o.x>=a.x&&o.x<=a.x+a.w&&o.y>=a.y&&o.y<=a.y+a.h).sort((a,v)=>a.box.w*a.box.h-v.box.w*v.box.h)[0]}W?.addEventListener("click",(i)=>{if(K||i.target.closest("[data-select]"))return;let o=ao(i);if(o)po(o.id,!0)}),W?.addEventListener("pointermove",(i)=>{if(!K)W.style.cursor=ao(i)?"zoom-in":""}),W?.addEventListener("pointerdown",(i)=>{if(!K)return;O=Li(i),W.setPointerCapture(i.pointerId),i.preventDefault()}),W?.addEventListener("pointermove",(i)=>{if(!O)return;let o=Li(i);R={x:Math.min(O.x,o.x),y:Math.min(O.y,o.y),w:Math.abs(o.x-O.x),h:Math.abs(o.y-O.y)};let a=r.querySelector(".draw-box");a.hidden=!1,a.style.cssText=Bi(R)}),W?.addEventListener("pointerup",()=>{if(R&&R.w>0.01&&R.h>0.01)Oi(R);else O=null,R=null}),W?.addEventListener("pointercancel",()=>{O=null,R=null,g()});let xo=r.querySelector(".output"),yi=r.querySelector("iframe"),to=r.querySelector(".workbench"),li=r.querySelector(".inspection-content"),Xi=r.querySelector(".inspector");function Qi(){if(!li||!Xi)return;Xi.dataset.scrollAbove=String(li.scrollTop>1),Xi.dataset.scrollBelow=String(li.scrollHeight-li.clientHeight-li.scrollTop>1)}li?.addEventListener("scroll",Qi,{passive:!0});function zi(){let i=r.querySelector(".map-space");if(i&&i.clientWidth&&i.clientHeight){let s=Zi(e.comp.width,e.comp.height,Math.max(1,i.clientWidth-32),Math.max(1,i.clientHeight-32),"fit");W.style.width=`${s.width}px`,W.style.height=`${s.height}px`}let o=r.querySelector(".inspection-content"),a=Array.from(r.querySelectorAll(".pan-viewport"));if(o?.clientHeight){let s=V?Math.max(100,$.clientHeight-(Y?.querySelector(".compare-toolbar")?.clientHeight??0)-(Y?.querySelector(".expanded-title")?.clientHeight??0)-(Y?.querySelector(".view-controls")?.clientHeight??0)-112):Math.min(l?Number.POSITIVE_INFINITY:248,Math.max(100,o.clientHeight-((a[0]?.getBoundingClientRect().top??o.getBoundingClientRect().top)-o.getBoundingClientRect().top+o.scrollTop)-(Y?.querySelector(".view-controls")?.clientHeight??0)-12));a.forEach((q)=>q.style.height=`${s}px`)}if(t&&a.length){let s=Zi(t.box.w*L.comp.width,t.box.h*L.comp.height,Math.min(...a.map((q)=>q.clientWidth)),Math.min(...a.map((q)=>q.clientHeight)),I);r.querySelectorAll(".crop-stage").forEach((q)=>{q.style.width=`${s.width}px`,q.style.height=`${s.height}px`})}if(a.forEach((s)=>{let q=s.scrollWidth>s.clientWidth+1||s.scrollHeight>s.clientHeight+1;s.classList.toggle("pannable",q),s.style.cursor=V?"":"zoom-in",s.setAttribute("role",V?"region":"button"),s.title=V?q?"Move your pointer to pan. You can also scroll, swipe, or use arrow keys.":"":"Click to enlarge comparison"}),xo&&yi&&t){let s=xo.clientWidth/(t.box.w*L.comp.width);yi.style.transform=`scale(${s})`,yi.style.left=`${-t.box.x*L.comp.width*s}px`,yi.style.top=`${-t.box.y*L.comp.height*s}px`}let v=to.getBoundingClientRect(),Z=r.querySelector(".region"),ji=r.querySelector(".number"),qi=r.querySelector(".connector path");if(Z&&ji&&qi){let s=Z.getBoundingClientRect(),q=ji.getBoundingClientRect(),M=s.right-v.left,Mi=s.top+s.height/2-v.top,fo=q.left-v.left-8,_o=q.top+q.height/2-v.top;qi.setAttribute("d",`M ${M} ${Mi} H ${fo-14} V ${_o} H ${fo}`)}}function ci(i){if(!Y||!ai||i===V)return;let o=r.querySelector("#expand-comparison"),a=(V?$:Y).getBoundingClientRect(),v=window.matchMedia("(prefers-reduced-motion: reduce)").matches,Z=Y.querySelector(".pan-viewport"),ji=Z?Z.scrollLeft/Math.max(1,Z.scrollWidth-Z.clientWidth):0,qi=Z?Z.scrollTop/Math.max(1,Z.scrollHeight-Z.clientHeight):0,s=()=>Y.querySelectorAll(".pan-viewport").forEach((M)=>{M.scrollLeft=ji*Math.max(0,M.scrollWidth-M.clientWidth),M.scrollTop=qi*Math.max(0,M.scrollHeight-M.clientHeight)}),q=()=>{ai.append(Y),$.close(),ai.style.height="",V=!1,o.innerHTML=G("expand"),o.setAttribute("aria-label","Enlarge comparison"),o.title="Enlarge comparison",o.setAttribute("aria-expanded","false"),zi(),s(),o.focus({preventScroll:!0})};if(i){ai.style.height=`${a.height}px`,$.append(Y),$.showModal(),V=!0,o.innerHTML=G("compact"),o.setAttribute("aria-label","Restore comparison"),o.title="Restore comparison (Esc)",o.setAttribute("aria-expanded","true"),zi(),s(),o.focus({preventScroll:!0});let M=$.getBoundingClientRect();if(!v)$.animate([{transform:`translate(${a.x-M.x}px,${a.y-M.y}px) scale(${a.width/M.width},${a.height/M.height})`,opacity:0.6},{transform:"none",opacity:1}],{duration:240,easing:"cubic-bezier(.2,.8,.2,1)"})}else{if(o.disabled)return;let M=ai.getBoundingClientRect();if(v){q();return}o.disabled=!0;let Mi=$.animate([{transform:"none",opacity:1},{transform:`translate(${M.x-a.x}px,${M.y-a.y}px) scale(${M.width/a.width},${M.height/a.height})`,opacity:0.6}],{duration:200,easing:"cubic-bezier(.4,0,.2,1)",fill:"forwards"});Mi.finished.then(()=>{if($.isConnected)Mi.cancel(),o.disabled=!1,q()}).catch(()=>{})}}b("expand-comparison",()=>ci(!V)),$.addEventListener("cancel",(i)=>{i.preventDefault(),i.stopPropagation(),ci(!1)}),$.addEventListener("keydown",(i)=>{if(i.key==="Escape")i.preventDefault(),i.stopPropagation(),ci(!1)}),P=new ResizeObserver(()=>{zi(),Qi()}),P.observe(to),P.observe($);let lo=r.querySelector(".inspection-content");if(lo)P.observe(lo);let ho=r.querySelector(".repair-context");if(ho)P.observe(ho);let go=Y?.querySelector(".compare-toolbar");if(go)P.observe(go);zi();let ki=Array.from(r.querySelectorAll(".pan-viewport"));if(ki.forEach((i)=>{i.scrollLeft=Vo,i.scrollTop=Jo}),ki.forEach((i)=>{i.querySelectorAll("img").forEach((o)=>o.draggable=!1),i.addEventListener("click",()=>{if(!V)ci(!0)}),i.addEventListener("pointermove",(o)=>{if(o.pointerType!=="mouse"||o.buttons||!i.classList.contains("pannable"))return;let a=i.getBoundingClientRect();i.scrollLeft=Ui(o.clientX,a.left,i.clientWidth,i.scrollWidth),i.scrollTop=Ui(o.clientY,a.top,i.clientHeight,i.scrollHeight)}),i.addEventListener("keydown",(o)=>{if(!V&&(o.key==="Enter"||o.key===" ")){o.preventDefault(),o.stopPropagation(),ci(!0);return}let v={ArrowLeft:[-48,0],ArrowRight:[48,0],ArrowUp:[0,-48],ArrowDown:[0,48]}[o.key];if(!v)return;o.preventDefault(),o.stopPropagation(),i.scrollLeft+=v[0],i.scrollTop+=v[1]})}),ki.forEach((i)=>i.addEventListener("scroll",()=>{for(let o of ki)if(o!==i){if(o.scrollLeft!==i.scrollLeft)o.scrollLeft=i.scrollLeft;if(o.scrollTop!==i.scrollTop)o.scrollTop=i.scrollTop}})),jo&&window.matchMedia("(max-width:800px)").matches){let i=r.querySelector(".inspection-content"),o=r.querySelector(".compare");if(i&&o)i.scrollTop+=o.getBoundingClientRect().top-i.getBoundingClientRect().top}if(Qi(),window.scrollTo(fi,mi),!u)x.onDraftChange?.(structuredClone(n))}return g(),{destroy(){P?.disconnect(),r.replaceChildren()},getDraft(){return structuredClone(n)}}}async function Xo(){let p=document.getElementById("review"),e=await fetch("/packet",{cache:"no-store"});if(!e.ok)throw Error("The review packet could not be loaded. Reload to retry.");let x=await e.json();if(x.sourceStatus){let r=document.createElement("p");r.textContent=x.sourceStatus,p.before(r)}uo(p,x.packet,{initialDraft:x.draft,history:x.history,completed:!!x.receipt,onSubmit:async(r)=>{let n=await fetch("/decision",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(r)}),l=await n.json();if(!n.ok)throw Error(l.error??"The review could not be saved. Try again.")}})}Xo().catch((p)=>{let e=document.createElement("p");e.textContent=p instanceof Error?p.message:String(p),document.getElementById("review")?.replaceChildren(e)});})(); diff --git a/ui/component-review/review.ts b/ui/component-review/review.ts index a72fd442b..617223170 100644 --- a/ui/component-review/review.ts +++ b/ui/component-review/review.ts @@ -16,6 +16,8 @@ export function mountComponentReview(host: HTMLElement, packet: ReviewPacket, op }) { const root = host.attachShadow({mode: 'open'}); let draft = structuredClone(options.initialDraft ?? newDraft(packet)); + // Legacy packets with several regions or marked omissions retain the inventory UI. + const assembled = packet.stage === 'hero' && packet.components.length === 1 && !draft.missing.length; const orderedComponents = () => [...packet.components].sort((a,b)=>componentState(a,draft,options.history).priority-componentState(b,draft,options.history).priority); let selected = orderedComponents().find(c=>componentState(c,draft,options.history).kind!=='approved')?.id ?? packet.components[0]?.id; let inventoryFilter: InventoryFilter = summarize(packet,draft).pending ? 'pending' : 'reviewed'; @@ -58,6 +60,15 @@ export function mountComponentReview(host: HTMLElement, packet: ReviewPacket, op render(); focusReview(finished ? 'review-summary' : edits[selected!] ? 'feedback' : 'approve'); } + async function sendReview() { + if (sending || submitted || previousRound || Object.keys(edits).length) return; + sending=true; error=''; render(); + try { + await options.onSubmit(submission(packet,draft)); + submitted=true; finished=true; marking=false; inventoryFilter='reviewed'; + } catch(e) { error=e instanceof Error ? e.message : 'Could not save. Try again.'; } + finally { sending=false; render(); } + } function updateDecision(action: 'approve' | 'revise') { if (sending || submitted || previousRound) return; const c = packet.components.find(c => c.id === selected); @@ -69,7 +80,12 @@ export function mountComponentReview(host: HTMLElement, packet: ReviewPacket, op draft.decisions[c.id] = {revision:c.revision, action, feedback:action==='revise' ? note?.feedback ?? '' : '', split:action==='revise' && (note?.split ?? false)}; delete edits[c.id]; if(restoreTrayAfterFeedback){trayOpen=true;restoreTrayAfterFeedback=false;} - advance(c.id); + if (assembled) { + // The explicit page approval also confirms completeness. Feedback never does. + draft.inventoryConfirmed = action === 'approve'; + lastDecision=null; + void sendReview(); + } else advance(c.id); } function beginFeedback() { if (sending || submitted || previousRound) return; @@ -150,12 +166,12 @@ export function mountComponentReview(host: HTMLElement, packet: ReviewPacket, op const useFrame = v && (useContext ? v.context?.kind !== 'image' : v.preview.kind === 'page'); const sourceUrl = useContext && v?.context ? v.context.url : v?.preview.url; const materialLabel = presentation?.code ? `${presentation.label} · ${presentation.captured ? 'captured from code' : 'live preview'}` : v?.material ? `${v.material.alpha === 'transparent' ? 'Transparent' : v.material.alpha === 'opaque' ? 'Opaque' : 'Transparency unverified'} ${v.material.format}` : 'Raster · transparency unverified'; - root.innerHTML = `
-

${submitted?'Review record.':'Review the components.'}

${esc(packet.title)} · Round ${packet.round}

${submitted ? 'Submitted · read-only' : options.preview ? 'Interactive preview' : ''}
+ root.innerHTML = `
+

${submitted?'Review record.':assembled?'Review the assembled page.':'Review the components.'}

${esc(packet.title)} · Round ${packet.round}

${submitted ? 'Submitted · read-only' : options.preview ? 'Interactive preview' : ''}
${options.preview ? '

Historical hotel artwork for testing this interface. Decisions stay in this preview; no run is changed.

' : ''} - ${history ? `

${stats.pending} ${stats.pending===1?'component':'components'} to review${summaryDetails}

${stats.pending?``:''}${history.removed.length?`
Removed from the map

${history.removed.map(item=>esc(item.name)).join(' · ')}. Confirm these omissions are intentional before accepting the map.

`:''}
`:''} -
-
+ ${history && !assembled ? `

${stats.pending} ${stats.pending===1?'component':'components'} to review${summaryDetails}

${stats.pending?``:''}${history.removed.length?`
Removed from the map

${history.removed.map(item=>esc(item.name)).join(' · ')}. Confirm these omissions are intentional before accepting the map.

`:''}
`:''} + ${!assembled?`
`:''} +
${!assembled?`

Approved comp

${!submitted?``:''}
@@ -168,10 +184,10 @@ export function mountComponentReview(host: HTMLElement, packet: ReviewPacket, op
# To review${feedbackIcon} ${submitted?'Changes requested':'Feedback ready'}${checkIcon} Approved
${marking ? '
Draw around the missing piece.
' : ''} -
-
-

${finished ? 'Review summary' : `${index} ${esc(c?.name ?? missing?.name ?? 'Component')}`}

- ${finished ? `

${submitted ? 'Review sent.' : 'All components reviewed.'}

${stats.approved} approved · ${stats.revisions} flagged for repair${draft.missing.length ? ` · ${draft.missing.length} missing` : ''}

${submitted ? 'Your decisions are saved.' : stats.hasFeedback ? 'Send your feedback to start the next repair round.' : 'Confirm nothing is missing, then approve and continue.'}

${packet.components.map(item=>{const decision=draft.decisions[item.id];const state=stateFor(item);return ``;}).join('')}${draft.missing.map(item=>``).join('')}
${notice?`
${notice}
`:''}` : c ? ` +
`:''} +
+ ${!assembled?`

${finished ? 'Review summary' : `${index} ${esc(c?.name ?? missing?.name ?? 'Component')}`}

`:''}
+ ${finished && !assembled ? `

${submitted ? 'Review sent.' : 'All components reviewed.'}

${stats.approved} approved · ${stats.revisions} flagged for repair${draft.missing.length ? ` · ${draft.missing.length} missing` : ''}

${submitted ? 'Your decisions are saved.' : stats.hasFeedback ? 'Send your feedback to start the next repair round.' : 'Confirm nothing is missing, then approve and continue.'}

${packet.components.map(item=>{const decision=draft.decisions[item.id];const state=stateFor(item);return ``;}).join('')}${draft.missing.map(item=>``).join('')}
${notice?`
${notice}
`:''}` : c ? ` ${history ? `
${viewingPrevious&&repair?.prior?.action==='revise'?`

Previous feedback · Round ${repair.feedbackRound}

${esc(repair.prior.feedback || 'No written feedback was supplied.')}
${repair.prior.split?'

Requested: split into separately reviewable components.

':''}
`:repair?.carried?`

Unchanged · approval kept

`:''} ${repair?.change?.kind==='changed'?`
${repair.change.files.length?`${repair.change.files.length} changed ${repair.change.files.length===1?'file':'files'}`:repair.change.reasons.includes('region')?'Region changed':priorComponent?.note!==c.note?'Description changed · files unchanged':'Component definition changed · files unchanged'}${repair.change.files.length?`
    ${repair.change.files.map(path=>`
  • ${esc(path)}
  • `).join('')}
`:''}${priorComponent&&priorComponent.note!==c.note?`
Previous description
${esc(priorComponent.note)}
Current description
${esc(c.note)}
`:''}
`:''} @@ -179,21 +195,21 @@ export function mountComponentReview(host: HTMLElement, packet: ReviewPacket, op

${esc(v!.name)}

${priorComponent?`
`:''}
${v?.preview.kind==='image'?`${icon('external')}`:''}
-
${viewingPrevious ? `Comp · Round ${history!.packet.round}` : 'In the comp'}
Reference region for ${esc(v!.name)}
-
${viewingPrevious ? `Previous · Round ${history!.packet.round}` : useContext ? 'In context' : history ? `${presentation!.caption} · Round ${packet.round}` : presentation!.caption}
${!useFrame ? `Produced ${esc(v!.name)}` : ``}${overlay ? `Reference overlay` : ''}
+
${viewingPrevious ? `Comp · Round ${history!.packet.round}` : assembled ? 'Approved comp' : 'In the comp'}
Reference region for ${esc(v!.name)}
+
${viewingPrevious ? `Previous · Round ${history!.packet.round}` : assembled ? 'Assembled page' : useContext ? 'In context' : history ? `${presentation!.caption} · Round ${packet.round}` : presentation!.caption}
${!useFrame ? `Produced ${esc(v!.name)}` : ``}${overlay ? `Reference overlay` : ''}
${hasTransparency || v!.context ? `
${v!.context ? `
` : ''}${hasTransparency?`
`:''}
` : ''} -
${icon(presentation!.code ? 'code' : 'image')}${esc(materialLabel)}${v?.material ? `${v.material.width} × ${v.material.height} px` : ''}
${vp.stage==='components'&&presentation?.captured&&!v?.preview.isolation?'

Legacy region capture · may include overlapping components.

':''}${v?.context?.layering&&(isRaster||useContext)?`

${esc(v.context.layering)}

`:''} +
${!assembled?`
${icon(presentation!.code ? 'code' : 'image')}${esc(materialLabel)}${v?.material ? `${v.material.width} × ${v.material.height} px` : ''}
${vp.stage==='components'&&presentation?.captured&&!v?.preview.isolation?'

Legacy region capture · may include overlapping components.

':''}${v?.context?.layering&&(isRaster||useContext)?`

${esc(v.context.layering)}

`:''}

${esc(v!.note)}

-
${notice}${viewingPrevious?'

Viewing the previous round. Return to Current to make a decision.

':''}${submitted?`
${d?.action==='approve'?'Approved':d?.action==='revise'?'Changes requested':'Not reviewed'}Submitted in round ${packet.round} · read-only
`:`
Your review Round ${packet.round}${viewingPrevious?'

Return to Current to review this round.

':''}
${d ? `` : ''}
`} - ${edit ? `
` : d?.action==='revise' ? `

${esc(d.feedback || 'No note — agent will diagnose.')}

` : ''} -
` : missing ? `

This piece will be added to the unresolved inventory.

${(['x','y','w','h'] as const).map(k=>``).join('')}
` : '

No components supplied.

'} + `:''}
${notice}${viewingPrevious?'

Viewing the previous round. Return to Current to make a decision.

':''}${submitted?`
${d?.action==='approve'?'Approved':d?.action==='revise'?'Changes requested':'Not reviewed'}Submitted in round ${packet.round} · read-only
`:`
${!assembled?`
Your review Round ${packet.round}${viewingPrevious?'

Return to Current to review this round.

':''}
`:''}${d && !assembled ? `` : ''}
`} + ${edit ? `
` : d?.action==='revise' ? `

${esc(d.feedback || 'No note — agent will diagnose.')}

` : ''} + ${assembled?`

${esc(error || (submitted?'Your decision is saved.':sending?'Sending…':edit?'':'Approval confirms the composition and that nothing is missing.'))}

`:''}
` : missing ? `

This piece will be added to the unresolved inventory.

${(['x','y','w','h'] as const).map(k=>``).join('')}
` : '

No components supplied.

'}
-

Components

-
${shownComponents.map(item=>{const i=packet.components.indexOf(item);const state=stateFor(item); return ``}).join('')}${(inventoryFilter==='pending'?[]:draft.missing).map((m,i)=>``).join('')}${!shownComponents.length&&(inventoryFilter==='pending'||!draft.missing.length)?`

${inventoryFilter==='pending'?'Nothing left to review. Your decisions are ready.':'No components reviewed yet.'}

`:''}
- ${submitted?`
Round ${packet.round} submitted · read-only${stats.approved} approved · ${stats.revisions} changes requested
`:`
${!stats.pending&&!uncommitted ? `` : ''}

${esc(statusMessage)}

`} -
`; + ${!assembled?`

Components

+
${shownComponents.map(item=>{const i=packet.components.indexOf(item);const state=stateFor(item); return ``}).join('')}${(inventoryFilter==='pending'?[]:draft.missing).map((m,i)=>``).join('')}${!shownComponents.length&&(inventoryFilter==='pending'||!draft.missing.length)?`

${inventoryFilter==='pending'?'Nothing left to review. Your decisions are ready.':'No components reviewed yet.'}

`:''}
`:''} + ${assembled?'':submitted?`
Round ${packet.round} submitted · read-only${stats.approved} approved · ${stats.revisions} changes requested
`:`
${!stats.pending&&!uncommitted ? `` : ''}

${esc(statusMessage)}

`} + `; const comparisonDialog=root.querySelector('#comparison-dialog')!; const comparisonPanel=root.querySelector('.comparison-panel'); const comparisonSlot=root.querySelector('.comparison-slot'); @@ -204,7 +220,8 @@ export function mountComponentReview(host: HTMLElement, packet: ReviewPacket, op if(viewingPrevious)root.querySelectorAll('.decisions button,#feedback,#split,#save-feedback,#cancel-feedback,#undo-decision,#approve-rest,#submit,#inventory-confirm').forEach(el=>el.disabled=true); root.querySelector('.inspection-content')!.scrollTop=inspectorTop; if(submitted||sending)root.querySelectorAll('.decisions button,#save-feedback,#cancel-feedback,#undo-decision,#approve-rest,#mark,#inventory-confirm,#missing-name,#missing-feedback,#feedback,#split,#remove-missing,[data-coordinate]').forEach(el=>el.disabled=true); - root.querySelector('.inventory')!.scrollLeft = railLeft; + const inventory=root.querySelector('.inventory'); + if(inventory)inventory.scrollLeft = railLeft; if(!keepInspector&&trayOpen)Array.from(root.querySelectorAll('.inventory [data-select]')).find(el=>el.dataset.select===selected)?.scrollIntoView({block:'nearest',inline:'nearest'}); if (focusId) root.getElementById(focusId)?.focus({preventScroll:true}); else if(focusSelection) Array.from(root.querySelectorAll('.item[data-select]')).find(el=>el.dataset.select===focusSelection)?.focus({preventScroll:true}); @@ -266,7 +283,7 @@ export function mountComponentReview(host: HTMLElement, packet: ReviewPacket, op if(Number.isFinite(next)) missing.box[k]=Math.max(k==='w'||k==='h'?.001:0, Math.min(1,next)); missing.box.w=Math.min(missing.box.w,1-missing.box.x);missing.box.h=Math.min(missing.box.h,1-missing.box.y);render(); })); - on('submit',async()=>{if(uncommitted||sending||submitted)return;sending=true;error='';render();try{await options.onSubmit(submission(packet,draft));submitted=true;finished=true;marking=false;inventoryFilter='reviewed';}catch(e){error=e instanceof Error?e.message:'Could not save. Try again.';}finally{sending=false;render();}}); + on('submit',()=>{void sendReview();}); const map = root.querySelector('.map')!; function point(e: MouseEvent) {const r=map.getBoundingClientRect();return {x:Math.max(0,Math.min(1,(e.clientX-r.left)/r.width)),y:Math.max(0,Math.min(1,(e.clientY-r.top)/r.height))};} function componentAt(e:MouseEvent) { @@ -275,15 +292,15 @@ export function mountComponentReview(host: HTMLElement, packet: ReviewPacket, op return packet.components.filter(({box:b})=>p.x>=b.x&&p.x<=b.x+b.w&&p.y>=b.y&&p.y<=b.y+b.h) .sort((a,b)=>a.box.w*a.box.h-b.box.w*b.box.h)[0]; } - map.addEventListener('click',e=>{ + map?.addEventListener('click',e=>{ if(marking||(e.target as Element).closest('[data-select]'))return; const item=componentAt(e);if(item)selectComponent(item.id,true); }); - map.addEventListener('pointermove',e=>{if(!marking)map.style.cursor=componentAt(e)?'zoom-in':'';}); - map.addEventListener('pointerdown',e=>{if(!marking)return;drag=point(e);map.setPointerCapture(e.pointerId);e.preventDefault();}); - map.addEventListener('pointermove',e=>{if(!drag)return;const p=point(e);dragBox={x:Math.min(drag.x,p.x),y:Math.min(drag.y,p.y),w:Math.abs(p.x-drag.x),h:Math.abs(p.y-drag.y)};const outline=root.querySelector('.draw-box')!;outline.hidden=false;outline.style.cssText=boxStyle(dragBox);}); - map.addEventListener('pointerup',()=>{if(dragBox&&dragBox.w>.01&&dragBox.h>.01)addMissing(dragBox);else{drag=null;dragBox=null;}}); - map.addEventListener('pointercancel',()=>{drag=null;dragBox=null;render();}); + map?.addEventListener('pointermove',e=>{if(!marking)map.style.cursor=componentAt(e)?'zoom-in':'';}); + map?.addEventListener('pointerdown',e=>{if(!marking)return;drag=point(e);map.setPointerCapture(e.pointerId);e.preventDefault();}); + map?.addEventListener('pointermove',e=>{if(!drag)return;const p=point(e);dragBox={x:Math.min(drag.x,p.x),y:Math.min(drag.y,p.y),w:Math.abs(p.x-drag.x),h:Math.abs(p.y-drag.y)};const outline=root.querySelector('.draw-box')!;outline.hidden=false;outline.style.cssText=boxStyle(dragBox);}); + map?.addEventListener('pointerup',()=>{if(dragBox&&dragBox.w>.01&&dragBox.h>.01)addMissing(dragBox);else{drag=null;dragBox=null;}}); + map?.addEventListener('pointercancel',()=>{drag=null;dragBox=null;render();}); const stage=root.querySelector('.output'); const frame=root.querySelector('iframe'); const workbench=root.querySelector('.workbench')!; // Reflect native scroll position without re-rendering or resetting the inspector. @@ -306,7 +323,7 @@ export function mountComponentReview(host: HTMLElement, packet: ReviewPacket, op if(content?.clientHeight){ const height=expandedComparison ? Math.max(100,comparisonDialog.clientHeight-(comparisonPanel?.querySelector('.compare-toolbar')?.clientHeight??0)-(comparisonPanel?.querySelector('.expanded-title')?.clientHeight??0)-(comparisonPanel?.querySelector('.view-controls')?.clientHeight??0)-112) - : Math.min(248,Math.max(100,content.clientHeight + : Math.min(assembled?Number.POSITIVE_INFINITY:248,Math.max(100,content.clientHeight - ((panes[0]?.getBoundingClientRect().top ?? content.getBoundingClientRect().top)-content.getBoundingClientRect().top+content.scrollTop) - (comparisonPanel?.querySelector('.view-controls')?.clientHeight ?? 0)-12)); panes.forEach(p=>p.style.height=`${height}px`); diff --git a/ui/component-review/styles.ts b/ui/component-review/styles.ts index 9b3f9d4bd..99d9c2b44 100644 --- a/ui/component-review/styles.ts +++ b/ui/component-review/styles.ts @@ -70,4 +70,27 @@ export const styles = ` .component-details .material{min-height:0;margin:12px 0 5px;gap:5px 8px}.component-details .material strong{font-size:11px;font-weight:500;color:var(--muted)}.component-details .material>.utility-icon{width:15px;height:15px}.component-details .material span{font-size:10px} @container(max-width:460px){.compare-toolbar .overlay-label{display:none}.compare-toolbar .overlay-control{width:34px;padding:7px}.compare-toolbar .zoom-control select{width:86px;padding-left:27px;padding-right:24px}.compare-toolbar .zoom-control>.utility-icon:first-child{left:8px}.compare-toolbar .zoom-control>.utility-icon:last-child{right:7px}} @media(forced-colors:active){.zoom-control select{appearance:auto;padding:6px 8px}.zoom-control>.utility-icon{display:none}} + +/* A full-page checkpoint is one comparison, not a component inventory. */ +.assembled-review>.workbench{grid-template-columns:minmax(0,1fr);gap:0} +.assembled-review .workbench>.inspector{display:flex} +.assembled-review .inspection-content{padding:16px 20px;scrollbar-gutter:auto} +.assembled-review .compare{max-width:none;gap:20px} +.assembled-review .pan-viewport{background:var(--canvas);border:1px solid #bdc6bd;box-shadow:inset 0 2px 7px #263d2a12} +.assembled-review .inspector>.review-form{padding:12px 20px;max-height:55%} +.assembled-review .decisions{display:flex;justify-content:flex-end;gap:10px} +.assembled-review #approve{order:2;min-width:180px} +.assembled-review .page-review-status{font-size:11px;color:var(--muted);text-align:right;line-height:1.4;overflow-wrap:anywhere} +.assembled-review .page-review-status:empty{display:none} +.assembled-review .review-form:has(#feedback-form){max-height:55%} +.assembled-review .feedback-field textarea{min-height:64px;max-height:120px} +.assembled-review .repair-context:empty{display:none} +@media(max-width:800px){ + .assembled-review>.workbench{display:flex;padding:10px} + .assembled-review .inspection-content{padding:10px} + .assembled-review .compare{grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:8px} + .assembled-review .inspector>.review-form{padding:10px} + .assembled-review .decisions>button{flex:1;min-width:0} + .assembled-review .page-review-status{text-align:left} +} `;