From 05dd4e56f64742b1fc29dd64131c76e57502584e Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Mon, 14 Sep 2026 15:49:06 -0700 Subject: [PATCH] Guide component reviews through decisions and next-item progression --- crates/context/assets/component-review.js | 79 +++++++++------- ui/component-review/model.test.ts | 17 +++- ui/component-review/model.ts | 10 ++ ui/component-review/review.ts | 108 +++++++++++++++------- ui/component-review/styles.ts | 13 ++- 5 files changed, 158 insertions(+), 69 deletions(-) diff --git a/crates/context/assets/component-review.js b/crates/context/assets/component-review.js index 18ccc7deb..c2e06b03d 100644 --- a/crates/context/assets/component-review.js +++ b/crates/context/assets/component-review.js @@ -1,4 +1,4 @@ -(()=>{function te(n,i){let a=i?.changes[n],o=i?.feedback?.[n],r=o?.decision??(i?.submitted?i.draft.decisions[n]:void 0),g=a?.kind==="unchanged"&&(a.carried??(i?.submitted&&r?.action==="approve"))===!0;return{change:a,prior:r,feedbackRound:o?.round??i?.packet.round,carried:g,label:a?.kind==="added"?"New component":a?.kind==="changed"?"Review again":g?"Approval kept":r?.action==="revise"?"Changes still requested":"Awaiting review"}}function I(n,i,a){let o=i.decisions[n.id],r=o?.revision===n.revision?o:void 0,g=te(n.id,a);if(r?.action==="approve")return{kind:"approved",label:g.carried?"Approval kept":"Approved",priority:3};if(r?.action==="revise")return{kind:"feedback",label:"Feedback ready",priority:2};return{kind:"pending",label:g.change?.kind==="changed"?"Review again":g.change?.kind==="added"?"New · review needed":"Not reviewed",priority:g.change?.kind==="changed"||g.change?.kind==="added"?0:1}}function Ye(n){return{packetRevision:n.revision,decisions:{},missing:[],inventoryConfirmed:!1}}function pi(n){return Object.values(n).every(Number.isFinite)&&n.x>=0&&n.y>=0&&n.w>0&&n.h>0&&n.x+n.w<=1.00001&&n.y+n.h<=1.00001}function pe(n,i){let a=n.components.map((h)=>i.decisions[h.id]?.revision===h.revision?i.decisions[h.id]:void 0),o=a.filter((h)=>h?.action==="approve").length,r=a.filter((h)=>h?.action==="revise").length,g=a.length-o-r,l=r>0||i.missing.length>0;return{approved:o,revisions:r,pending:g,hasFeedback:l,canSubmit:i.packetRevision===n.revision&&i.missing.every((h)=>h.name.trim()&&pi(h.box))&&(l||!g&&i.inventoryConfirmed)}}function De(n,i){let a={...i.decisions};for(let o of n.components)if(!a[o.id]||a[o.id].revision!==o.revision)a[o.id]={revision:o.revision,action:"approve",feedback:"",split:!1};return{...i,decisions:a}}function Ze(n,i){if(!pe(n,i).canSubmit)throw Error("Review is incomplete or stale");return{schemaVersion:1,requestId:n.id,...structuredClone(i)}}function ae(n){let i=n.preview.kind==="page"||n.preview.sourceKind==="page",a=n.preview.sourceKind==="page";return{code:i,captured:a,label:i?n.medium.match(/html|css|svg/i)?n.medium:"HTML / CSS / SVG":"Raster",caption:i?a?"Rendered component":"Live component":"Produced asset",fileLabel:a?"Open captured preview":"Open source image"}}function ce(n,i,a,o,r){let g=r==="fit"?Math.min(a/n,o/i):r;return{scale:g,width:n*g,height:i*g}}var Re=` +(()=>{function di(p,e){let a=e?.changes[p],o=e?.feedback?.[p],n=o?.decision??(e?.submitted?e.draft.decisions[p]:void 0),c=a?.kind==="unchanged"&&(a.carried??(e?.submitted&&n?.action==="approve"))===!0;return{change:a,prior:n,feedbackRound:o?.round??e?.packet.round,carried:c,label:a?.kind==="added"?"New component":a?.kind==="changed"?"Review again":c?"Approval kept":n?.action==="revise"?"Changes still requested":"Awaiting review"}}function B(p,e,a){let o=e.decisions[p.id],n=o?.revision===p.revision?o:void 0,c=di(p.id,a);if(n?.action==="approve")return{kind:"approved",label:c.carried?"Approval kept":"Approved",priority:3};if(n?.action==="revise")return{kind:"feedback",label:"Feedback ready",priority:2};return{kind:"pending",label:c.change?.kind==="changed"?"Review again":c.change?.kind==="added"?"New · review needed":"Not reviewed",priority:c.change?.kind==="changed"||c.change?.kind==="added"?0:1}}function Si(p){return{packetRevision:p.revision,decisions:{},missing:[],inventoryConfirmed:!1}}function ue(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 hi(p,e){let a=p.components.map((g)=>e.decisions[g.id]?.revision===g.revision?e.decisions[g.id]:void 0),o=a.filter((g)=>g?.action==="approve").length,n=a.filter((g)=>g?.action==="revise").length,c=a.length-o-n,d=n>0||e.missing.length>0;return{approved:o,revisions:n,pending:c,hasFeedback:d,canSubmit:e.packetRevision===p.revision&&e.missing.every((g)=>g.name.trim()&&ue(g.box))&&(d||!c&&e.inventoryConfirmed)}}function Ci(p,e){let a={...e.decisions};for(let o of p.components)if(!a[o.id]||a[o.id].revision!==o.revision)a[o.id]={revision:o.revision,action:"approve",feedback:"",split:!1};return{...e,decisions:a}}function Fi(p,e){if(!hi(p,e).canSubmit)throw Error("Review is incomplete or stale");return{schemaVersion:1,requestId:p.id,...structuredClone(e)}}function gi(p){let e=p.preview.kind==="page"||p.preview.sourceKind==="page",a=p.preview.sourceKind==="page";return{code:e,captured:a,label:e?p.medium.match(/html|css|svg/i)?p.medium:"HTML / CSS / SVG":"Raster",caption:e?a?"Rendered component":"Live component":"Produced asset",fileLabel:a?"Open captured preview":"Open source image"}}function Pi(p,e,a){let o=p.components.findIndex((n)=>n.id===a);for(let n=1;n<=p.components.length;n++){let c=p.components[(o+n)%p.components.length];if(B(c,e).kind==="pending")return c.id}}function ki(p,e,a,o,n){let c=n==="fit"?Math.min(a/p,o/e):n;return{scale:c,width:p*c,height:e*c}}var ie=` :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} @@ -54,7 +54,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 Ae=` +`;var ee=` :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,47 +88,58 @@ @media(max-width:800px){.inventory-section .section-head{gap:10px}.inventory-filters{width:100%}.inventory-filters button{padding-inline:7px;font-size:11px;flex:1}.inventory-filters b{margin-left:3px}} @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)} -`+Re;var ai={code:'',image:'',expand:'',compact:'',hideTray:'',showTray:'',next:'',mark:'',close:'',undo:'',external:'',zoom:''};function E(n){return``}var x=(n)=>n.replace(/[&<>"']/g,(i)=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[i]),U=(n)=>`${n*100}%`,X=(n)=>{let i=new URL(n,location.href);if(!["http:","https:"].includes(i.protocol))throw Error("Unsupported preview URL");return x(i.href)};function Oe(n,i,a){let o=n.attachShadow({mode:"open"}),r=structuredClone(a.initialDraft??Ye(i)),g=()=>[...i.components].sort((m,k)=>I(m,r,a.history).priority-I(k,r,a.history).priority),l=g().find((m)=>I(m,r,a.history).kind!=="approved")?.id??i.components[0]?.id,h=a.completed?"all":"attention",H=!1,B=!1,ie=a.completed??!1,xe="",Y=!1,$=!1,T=!0,z="comp",be=z,G="fit",S="checker",R=!1,A="isolated",de,me="fit",V=null,J=null,C=null,le='',oe='',we=(m)=>`left:${U(m.x)};top:${U(m.y)};width:${U(m.w)};height:${U(m.h)}`;function ue(m){let k=i.components.find((K)=>K.id===l);if(!k)return;if(r.decisions[k.id]={revision:k.revision,action:m,feedback:r.decisions[k.id]?.feedback??"",split:m==="revise"&&(r.decisions[k.id]?.split??!1)},d(),m==="revise"){let K=o.querySelector("#feedback");K?.focus({preventScroll:!0});let F=o.querySelector(".review-form");if(F&&K){let D=K.getBoundingClientRect().bottom-F.getBoundingClientRect().bottom;if(D>0)F.scrollTop+=D+8}let P=o.querySelector(".inspection-content"),ne=o.querySelector(".compare");if(P&&ne)P.scrollTop+=ne.getBoundingClientRect().top-P.getBoundingClientRect().top}}function ve(m){let k=`missing-${crypto.randomUUID()}`;r.missing.push({id:k,name:"Missing component",feedback:"",box:m}),r.inventoryConfirmed=!1,l=k,z="component",H=!1,V=null,J=null,d(),o.querySelector("#missing-name")?.focus()}function d(){C?.disconnect();let m=o.activeElement,k=m?.id,K=m?.dataset.select,F=window.scrollX,P=window.scrollY,ne=o.querySelector(".inventory")?.scrollLeft??0,D=de===l,Ie=z==="component"&&(!D||be!==z);be=z;let Be=D?o.querySelector(".inspection-content")?.scrollTop??0:0,Se=D&&(o.querySelector(".changed-files")?.open??!1),ye=o.querySelector(".pan-viewport"),ze=de===l&&me===G,Ce=ze?ye?.scrollLeft??0:0,Fe=ze?ye?.scrollTop??0:0;de=l,me=G;let f=i.components.find((e)=>e.id===l),b=r.missing.find((e)=>e.id===l),ke=f?.box??b?.box,je=f?i.components.indexOf(f)+1:i.components.length+r.missing.findIndex((e)=>e.id===l)+1,Me=f?r.decisions[f.id]:void 0,L=Me?.revision===f?.revision?Me:void 0,u=pe(i,r),w=a.history,j=f?te(f.id,w):void 0,Z=w?.packet.components.find((e)=>e.id===f?.id),N=R&&!!Z,p=N?Z:f,M=N?w.packet:i,He=Object.values(w?.changes??{}),Te=He.filter((e)=>e.kind==="changed").length,qe=He.filter((e)=>e.kind==="added").length,se=i.components.filter((e)=>I(e,r,w).kind==="approved"&&te(e.id,w).carried).length,ee=(e)=>I(e,r,w),Pe=i.components.length-u.approved+r.missing.length,Ee=(h==="attention"?g():i.components).filter((e)=>h==="all"||ee(e).kind==="approved"===(h==="approved")),ei=[u.revisions+r.missing.length?`${u.revisions+r.missing.length} feedback ready`:"",se?`${se} ${se===1?"approval":"approvals"} kept`:"",Te?`${Te} changed`:"",qe?`${qe} added`:"",w?.removed.length?`${w.removed.length} removed`:""].filter(Boolean).join(" · "),ii=xe||(ie?a.preview?"Preview submitted. No run changed.":"Review submitted.":u.hasFeedback?"Ready to send for corrections.":u.pending?`${u.pending} left to review`:!r.inventoryConfirmed?"Confirm the map is complete.":"Ready to continue."),Q=p?ae(p):null,Ve=p?.preview.kind==="image"&&!Q?.code,W=!!(p?.context&&A==="context"),Je=p&&(W?p.context?.kind!=="image":p.preview.kind==="page"),Ne=W&&p?.context?p.context.url:p?.preview.url,oi=Q?.code?`${Q.label} · ${Q.captured?"captured from code":"live preview"}`:p?.material?`${p.material.alpha==="transparent"?"Transparent":p.material.alpha==="opaque"?"Opaque":"Transparency unverified"} ${p.material.format}`:"Raster · transparency unverified";if(o.innerHTML=`
-

Review the components.

${x(i.title)} · Round ${i.round}

${a.preview?'Interactive preview':""}
+.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}.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:16px 8px}.review-summary h2{font-size:24px}.review-summary p{margin-top:12px;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} +`+ie+` +.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} +.feedback-fields{flex:1;min-height:0;overflow:auto;padding:3px} +.review-form:has(#feedback-form) .decision-notice{flex-shrink:0} +.review-form:has(#feedback-form) .decisions{display:none} +.feedback-actions{flex-shrink:0;position:static;background:var(--surface,var(--paper));padding:8px 0;margin-top:8px;box-shadow:0 -5px 8px -6px #24332838} +.feedback-actions .shortcut-hint{flex-basis:auto;order:-1;margin-right:auto} +.feedback-actions .primary{margin-left:0} +@media(max-height:800px),(max-width:800px){.feedback-actions .shortcut-hint{display:none}.feedback-actions .primary{margin-left:auto}.review-form .feedback textarea{min-height:56px}.review-form:has(#feedback-form) .decision-notice{margin-bottom:4px}} +`;var ve={code:'',image:'',expand:'',compact:'',hideTray:'',showTray:'',next:'',mark:'',close:'',undo:'',external:'',zoom:''};function K(p){return``}var x=(p)=>p.replace(/[&<>"']/g,(e)=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[e]),D=(p)=>`${p*100}%`,O=(p)=>{let e=new URL(p,location.href);if(!["http:","https:"].includes(e.protocol))throw Error("Unsupported preview URL");return x(e.href)};function oe(p,e,a){let o=p.attachShadow({mode:"open"}),n=structuredClone(a.initialDraft??Si(e)),c=()=>[...e.components].sort((s,b)=>B(s,n,a.history).priority-B(b,n,a.history).priority),d=c().find((s)=>B(s,n,a.history).kind!=="approved")?.id??e.components[0]?.id,g=a.completed?"all":"attention",V=!1,J=!1,Q=a.completed??!1,si="",H={},M=!1,W=null,ne=/Mac|iPhone|iPad/.test(navigator.platform)?"⌘Enter":"Ctrl+Enter",L=!1,U=!1,k=!0,F=!1,y="comp",ji=y,_="fit",ei="checker",N=!1,R="isolated",fi,Hi="fit",T=null,X=null,oi=null,ci='',ai='',Mi=(s)=>`left:${D(s.x)};top:${D(s.y)};width:${D(s.w)};height:${D(s.h)}`;function ni(s){o.getElementById(s)?.focus({preventScroll:!0})}function re(s){let b=Pi(e,n,s);if(M=!b,b)d=b;if(y="component",N=!1,L=!1,_="fit",R="isolated",g==="approved")g="attention";h(),ni(M?"review-summary":H[d]?"feedback":"approve")}function bi(s){if(J||Q||N)return;let b=e.components.find((mi)=>mi.id===d);if(!b)return;let $=n.decisions[b.id],ri=$?.revision===b.revision?$:void 0;W={id:b.id,name:b.name,action:s,previous:$?{...$}:void 0};let ti=H[b.id]??ri;if(n.decisions[b.id]={revision:b.revision,action:s,feedback:s==="revise"?ti?.feedback??"":"",split:s==="revise"&&(ti?.split??!1)},delete H[b.id],F)k=!0,F=!1;re(b.id)}function pe(){if(J||Q||N)return;let s=e.components.find((ri)=>ri.id===d);if(!s)return;let b=n.decisions[s.id],$=b?.revision===s.revision?b:void 0;if(H[s.id]??={feedback:$?.feedback??"",split:$?.split??!1},k&&(o.querySelector(".workbench")?.clientHeight??0)<420)F=!0,k=!1;M=!1,h(),ni("feedback")}function qi(s){let b=`missing-${crypto.randomUUID()}`;n.missing.push({id:b,name:"Missing component",feedback:"",box:s}),n.inventoryConfirmed=!1,M=!1,d=b,y="component",V=!1,T=null,X=null,h(),o.querySelector("#missing-name")?.focus()}function h(){oi?.disconnect();let s=o.activeElement,b=s?.id,$=s?.dataset.select,ri=window.scrollX,ti=window.scrollY,mi=o.querySelector(".inventory")?.scrollLeft??0,xi=fi===d,ae=y==="component"&&(!xi||ji!==y);ji=y;let te=xi?o.querySelector(".inspection-content")?.scrollTop??0:0,xe=xi&&(o.querySelector(".changed-files")?.open??!1),Ei=o.querySelector(".pan-viewport"),Vi=fi===d&&Hi===_,le=Vi?Ei?.scrollLeft??0:0,de=Vi?Ei?.scrollTop??0:0;fi=d,Hi=_;let l=e.components.find((i)=>i.id===d),w=n.missing.find((i)=>i.id===d),Ji=l?.box??w?.box,Ni=l?e.components.indexOf(l)+1:e.components.length+n.missing.findIndex((i)=>i.id===d)+1,$i=l?n.decisions[l.id]:void 0,S=$i?.revision===l?.revision?$i:void 0,wi=l?H[l.id]:void 0,P=Object.keys(H).length>0,he=l?!e.components.some((i)=>i.id!==l.id&&B(i,n).kind==="pending"):!1,Gi=W?`
${x(W.name)} ${W.action==="approve"?"approved":"flagged for repair"}.
`:"",u=hi(e,n),v=a.history,q=l?di(l.id,v):void 0,C=v?.packet.components.find((i)=>i.id===l?.id),Y=N&&!!C,t=Y?C:l,E=Y?v.packet:e,Ki=Object.values(v?.changes??{}),Qi=Ki.filter((i)=>i.kind==="changed").length,Wi=Ki.filter((i)=>i.kind==="added").length,ui=e.components.filter((i)=>B(i,n,v).kind==="approved"&&di(i.id,v).carried).length,pi=(i)=>B(i,n,v),ge=e.components.length-u.approved+n.missing.length,_i=(g==="attention"?c():e.components).filter((i)=>g==="all"||pi(i).kind==="approved"===(g==="approved")),se=[u.revisions+n.missing.length?`${u.revisions+n.missing.length} feedback ready`:"",ui?`${ui} ${ui===1?"approval":"approvals"} kept`:"",Qi?`${Qi} changed`:"",Wi?`${Wi} added`:"",v?.removed.length?`${v.removed.length} removed`:""].filter(Boolean).join(" · "),fe=si||(P?"Save or cancel your open feedback before sending.":Q?a.preview?"Preview submitted. No run changed.":"Review submitted.":u.hasFeedback?"Ready to send for corrections.":u.pending?`${u.pending} left to review`:!n.inventoryConfirmed?"Confirm the map is complete.":"Ready to continue."),Z=t?gi(t):null,Ti=t?.preview.kind==="image"&&!Z?.code,I=!!(t?.context&&R==="context"),Xi=t&&(I?t.context?.kind!=="image":t.preview.kind==="page"),Yi=I&&t?.context?t.context.url:t?.preview.url,ce=Z?.code?`${Z.label} · ${Z.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";if(o.innerHTML=`
+

Review the components.

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

${a.preview?'Interactive preview':""}
${a.preview?'

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

':""} - ${w?`

${u.pending} ${u.pending===1?"component":"components"} to review${ei}

${u.pending?``:""}${w.removed.length?`
Removed from the map

${w.removed.map((e)=>x(e.name)).join(" · ")}. Confirm these omissions are intentional before accepting the map.

`:""}
`:""} -
-
+ ${v?`

${u.pending} ${u.pending===1?"component":"components"} to review${se}

${u.pending?``:""}${v.removed.length?`
Removed from the map

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

`:""}
`:""} +
+
-

Approved comp

-
- Approved composition for ${x(i.title)} - ${ke?`
`:""} - ${i.components.map((e,t)=>{let s=ee(e);return``}).join("")} - ${r.missing.map((e,t)=>``).join("")} +

Approved comp

+
+ Approved composition for ${x(e.title)} + ${Ji&&!M?`
`:""} + ${e.components.map((i,r)=>{let f=pi(i);return``}).join("")} + ${n.missing.map((i,r)=>``).join("")}
-
# To review${oe} Feedback ready${le} Approved
- ${H?'
Draw around the missing piece.
':""} +
# To review${ai} Feedback ready${ci} Approved
+ ${V?'
Draw around the missing piece.
':""}
-

${je} ${x(f?.name??b?.name??"Component")}

- ${f?`
${E(Q.code?"code":"image")}${x(oi)}${p?.material?`${p.material.width} × ${p.material.height} px`:""}${p?.preview.kind==="image"?`${E("external")}`:""}
- ${w?`
- ${j?.prior?.action==="revise"?`

Previous feedback Round ${j.feedbackRound}

Needs work

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

Requested: split into separately reviewable components.

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

Unchanged · approval kept

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

${M?"Review summary":`${Ni} ${x(l?.name??w?.name??"Component")}`}

+ ${M?`

Components reviewed.

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

Check your decisions below. ${u.hasFeedback?"Send feedback when you’re ready.":"Confirm nothing is missing, then approve and continue."}

${e.components.map((i)=>{let r=n.decisions[i.id];return``}).join("")}${n.missing.map((i)=>``).join("")}
${Gi}
`:l?`
${K(Z.code?"code":"image")}${x(ce)}${t?.material?`${t.material.width} × ${t.material.height} px`:""}${t?.preview.kind==="image"?`${K("external")}`:""}
+ ${v?`
+ ${q?.prior?.action==="revise"?`

Previous feedback Round ${q.feedbackRound}

Needs work

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

Requested: split into separately reviewable components.

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

Unchanged · approval kept

':""} + ${q?.change?.kind==="changed"?`
${q.change.files.length?`${q.change.files.length} changed ${q.change.files.length===1?"file":"files"}`:q.change.reasons.includes("region")?"Region changed":C?.note!==l.note?"Description changed · files unchanged":"Component definition changed · files unchanged"}${q.change.files.length?`
    ${q.change.files.map((i)=>`
  • ${x(i)}
  • `).join("")}
`:""}${C&&C.note!==l.note?`
Previous description
${x(C.note)}
Current description
${x(l.note)}
`:""}
`:""}
`:""} - ${Z?`
`:""} -
+ ${C?`
`:""} +
-
${N?`Comp · Round ${w.packet.round}`:"In the comp"}
Reference region for ${x(p.name)}
-
${N?`Previous · Round ${w.packet.round}`:w?`Current · Round ${i.round}`:W?"In the page":Q.caption}
${!Je?`Produced ${x(p.name)}`:``}${Y?`Reference overlay`:""}
+
${Y?`Comp · Round ${v.packet.round}`:"In the comp"}
Reference region for ${x(t.name)}
+
${Y?`Previous · Round ${v.packet.round}`:v?`Current · Round ${e.round}`:I?"In the page":Z.caption}
${!Xi?`Produced ${x(t.name)}`:``}${L?`Reference overlay`:""}
- ${Ve?`
${p.context?`
`:""}
`:""} -

${x(p?.context?.layering??"Layer placement not recorded.")}

-

${x(p.note)}

-
${N?'

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

':""}
Your review Round ${i.round}${N?"

Return to Current to review this round.

":""}
${L?``:""}
- ${L?.action==="revise"?``:""} -
`:b?`

This piece will be added to the unresolved inventory.

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

No components supplied.

"} + ${Ti?`
${t.context?`
`:""}
`:""} +

${x(t?.context?.layering??"Layer placement not recorded.")}

+

${x(t.note)}

+
${Gi}${Y?'

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

':""}
Your review Round ${e.round}${Y?"

Return to Current to review this round.

":""}
${S?``:""}
+ ${wi?`
`:S?.action==="revise"?`

${x(S.feedback||"No note — agent will diagnose.")}

`:""} +
`:w?`

This piece will be added to the unresolved inventory.

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

No components supplied.

"}
-

Components

-
${Ee.map((e)=>{let t=i.components.indexOf(e),s=ee(e);return``}).join("")}${(h==="approved"?[]:r.missing).map((e,t)=>``).join("")}${!Ee.length&&(h==="approved"||!r.missing.length)?`

${h==="attention"?"Every component is approved. Confirm the map is complete, then continue.":"No components approved yet."}

`:""}
-

${x(ii)}

-
`,N)o.querySelectorAll(".decisions button,#feedback,#split,#approve-rest,#submit,#inventory-confirm").forEach((e)=>e.disabled=!0);if(o.querySelector(".inspection-content").scrollTop=Be,ie||B)o.querySelectorAll(".decisions button,#approve-rest,#mark,#inventory-confirm,#missing-name,#missing-feedback,#feedback,#split,#remove-missing,[data-coordinate]").forEach((e)=>e.disabled=!0);if(o.querySelector(".inventory").scrollLeft=ne,!D&&T)Array.from(o.querySelectorAll(".inventory [data-select]")).find((e)=>e.dataset.select===l)?.scrollIntoView({block:"nearest",inline:"nearest"});if(k)o.getElementById(k)?.focus({preventScroll:!0});else if(K)Array.from(o.querySelectorAll(".item[data-select]")).find((e)=>e.dataset.select===K)?.focus({preventScroll:!0});let c=(e,t)=>o.querySelector(`#${e}`)?.addEventListener("click",t);o.querySelectorAll("[data-select]").forEach((e)=>e.onclick=()=>{if(H)return;l=e.dataset.select,z="component",Y=!1,G="fit",A="isolated",R=!1,d()}),c("previous-round",()=>{R=!0,d()}),c("current-round",()=>{R=!1,d()}),c("review-changes",()=>{let e=g().filter((y)=>ee(y).kind==="pending"),t=e.findIndex((y)=>y.id===l),s=e[(t+1)%e.length];if(s)l=s.id,z="component",h="attention",R=!1,G="fit",Y=!1,A="isolated",d()}),o.querySelectorAll("[data-filter]").forEach((e)=>e.onclick=()=>{h=e.dataset.filter;let t=g().filter((y)=>h==="all"||ee(y).kind==="approved"===(h==="approved"));if(!(h!=="approved"&&r.missing.some((y)=>y.id===l))&&!t.some((y)=>y.id===l)&&t.length)l=t[0].id,z="component",R=!1,G="fit",Y=!1,A="isolated";d()}),c("approve",()=>ue("approve")),c("revise",()=>ue("revise")),c("clear",()=>{if(f)delete r.decisions[f.id];d()}),c("overlay",()=>{Y=!Y,d()}),c("isolated",()=>{A="isolated",d()}),c("context",()=>{A="context",d()}),o.querySelector("#zoom")?.addEventListener("change",(e)=>{let t=e.target.value;G=t==="fit"?"fit":Number(t),d()}),c("background-checker",()=>{S="checker",d()}),c("background-page",()=>{S="page",d()}),c("show-all",()=>{$=!$,T=!0,d()}),c("toggle-tray",()=>{T=!T,d()}),c("show-comp",()=>{z="comp",d()}),c("show-component",()=>{z="component",d()}),c("mark",()=>{H=!H,d()}),c("add-box",()=>ve({x:0.35,y:0.35,w:0.2,h:0.2})),c("remove-missing",()=>{r.missing=r.missing.filter((e)=>e.id!==l),l=i.components[0]?.id,d()}),c("approve-rest",()=>{r=De(i,r),d()}),o.querySelector("#inventory-confirm")?.addEventListener("change",(e)=>{r.inventoryConfirmed=e.target.checked,d()}),o.querySelector("#feedback")?.addEventListener("input",(e)=>{if(f)r.decisions[f.id].feedback=e.target.value}),o.querySelector("#split")?.addEventListener("change",(e)=>{if(f)r.decisions[f.id].split=e.target.checked}),o.querySelector("#missing-name")?.addEventListener("input",(e)=>{if(b)b.name=e.target.value;let t=o.querySelector("#submit");if(t)t.disabled=!pe(i,r).canSubmit}),o.querySelector("#missing-feedback")?.addEventListener("input",(e)=>{if(b)b.feedback=e.target.value}),o.querySelectorAll("[data-coordinate]").forEach((e)=>e.addEventListener("change",()=>{if(!b)return;let t=e.dataset.coordinate,s=Number(e.value)/100;if(Number.isFinite(s))b.box[t]=Math.max(t==="w"||t==="h"?0.001:0,Math.min(1,s));b.box.w=Math.min(b.box.w,1-b.box.x),b.box.h=Math.min(b.box.h,1-b.box.y),d()})),c("submit",async()=>{B=!0,xe="",d();try{await a.onSubmit(Ze(i,r)),ie=!0}catch(e){xe=e instanceof Error?e.message:"Could not save. Try again."}finally{B=!1,d()}});let _=o.querySelector(".map");function $e(e){let t=_.getBoundingClientRect();return{x:Math.max(0,Math.min(1,(e.clientX-t.left)/t.width)),y:Math.max(0,Math.min(1,(e.clientY-t.top)/t.height))}}_.addEventListener("pointerdown",(e)=>{if(!H)return;V=$e(e),_.setPointerCapture(e.pointerId),e.preventDefault()}),_.addEventListener("pointermove",(e)=>{if(!V)return;let t=$e(e);J={x:Math.min(V.x,t.x),y:Math.min(V.y,t.y),w:Math.abs(t.x-V.x),h:Math.abs(t.y-V.y)};let s=o.querySelector(".draw-box");s.hidden=!1,s.style.cssText=we(J)}),_.addEventListener("pointerup",()=>{if(J&&J.w>0.01&&J.h>0.01)ve(J);else V=null,J=null}),_.addEventListener("pointercancel",()=>{V=null,J=null,d()});let Ge=o.querySelector(".output"),re=o.querySelector("iframe"),Ke=o.querySelector(".workbench"),O=o.querySelector(".inspection-content"),he=o.querySelector(".inspector");function ge(){if(!O||!he)return;he.dataset.scrollAbove=String(O.scrollTop>1),he.dataset.scrollBelow=String(O.scrollHeight-O.clientHeight-O.scrollTop>1)}O?.addEventListener("scroll",ge,{passive:!0});function Le(){let e=o.querySelector(".map-space");if(e&&e.clientWidth&&e.clientHeight){let v=ce(i.comp.width,i.comp.height,Math.max(1,e.clientWidth-32),Math.max(1,e.clientHeight-32),"fit");_.style.width=`${v.width}px`,_.style.height=`${v.height}px`}let t=o.querySelector(".inspection-content"),s=Array.from(o.querySelectorAll(".pan-viewport"));if(t?.clientHeight)s.forEach((v)=>v.style.height=`${Math.min(248,Math.max(100,t.clientHeight-40))}px`);if(p&&s.length){let v=ce(p.box.w*M.comp.width,p.box.h*M.comp.height,Math.min(...s.map((q)=>q.clientWidth)),Math.min(...s.map((q)=>q.clientHeight)),G);o.querySelectorAll(".crop-stage").forEach((q)=>{q.style.width=`${v.width}px`,q.style.height=`${v.height}px`})}if(Ge&&re&&p){let v=Ge.clientWidth/(p.box.w*M.comp.width);re.style.transform=`scale(${v})`,re.style.left=`${-p.box.x*M.comp.width*v}px`,re.style.top=`${-p.box.y*M.comp.height*v}px`}let y=Ke.getBoundingClientRect(),We=o.querySelector(".region"),_e=o.querySelector(".number"),Ue=o.querySelector(".connector path");if(We&&_e&&Ue){let v=We.getBoundingClientRect(),q=_e.getBoundingClientRect(),ni=v.right-y.left,ri=v.top+v.height/2-y.top,Xe=q.left-y.left-8,ti=q.top+q.height/2-y.top;Ue.setAttribute("d",`M ${ni} ${ri} H ${Xe-14} V ${ti} H ${Xe}`)}}C=new ResizeObserver(()=>{Le(),ge()}),C.observe(Ke);let Qe=o.querySelector(".inspection-content");if(Qe)C.observe(Qe);Le();let fe=Array.from(o.querySelectorAll(".pan-viewport"));if(fe.forEach((e)=>{e.scrollLeft=Ce,e.scrollTop=Fe}),fe.forEach((e)=>e.addEventListener("scroll",()=>{for(let t of fe)if(t!==e){if(t.scrollLeft!==e.scrollLeft)t.scrollLeft=e.scrollLeft;if(t.scrollTop!==e.scrollTop)t.scrollTop=e.scrollTop}})),Ie&&window.matchMedia("(max-width:800px)").matches){let e=o.querySelector(".inspection-content"),t=o.querySelector(".compare");if(e&&t)e.scrollTop+=t.getBoundingClientRect().top-e.getBoundingClientRect().top}ge(),window.scrollTo(F,P)}return d(),{destroy(){C?.disconnect(),o.replaceChildren()},getDraft(){return structuredClone(r)}}}async function xi(){let n=document.getElementById("review"),i=await fetch("/packet",{cache:"no-store"});if(!i.ok)throw Error("The review packet could not be loaded. Reload to retry.");let a=await i.json();if(a.sourceStatus){let o=document.createElement("p");o.textContent=a.sourceStatus,n.before(o)}Oe(n,a.packet,{initialDraft:a.draft,history:a.history,completed:!!a.receipt,onSubmit:async(o)=>{let r=await fetch("/decision",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)}),g=await r.json();if(!r.ok)throw Error(g.error??"The review could not be saved. Try again.")}})}xi().catch((n)=>{let i=document.createElement("p");i.textContent=n instanceof Error?n.message:String(n),document.getElementById("review")?.replaceChildren(i)});})(); +

Components

+
${_i.map((i)=>{let r=e.components.indexOf(i),f=pi(i);return``}).join("")}${(g==="approved"?[]:n.missing).map((i,r)=>``).join("")}${!_i.length&&(g==="approved"||!n.missing.length)?`

${g==="attention"?"Every component is approved. Confirm the map is complete, then continue.":"No components approved yet."}

`:""}
+

${x(fe)}

+ `,Y)o.querySelectorAll(".decisions button,#feedback,#split,#save-feedback,#cancel-feedback,#undo-decision,#approve-rest,#submit,#inventory-confirm").forEach((i)=>i.disabled=!0);if(o.querySelector(".inspection-content").scrollTop=te,Q||J)o.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(o.querySelector(".inventory").scrollLeft=mi,!xi&&k)Array.from(o.querySelectorAll(".inventory [data-select]")).find((i)=>i.dataset.select===d)?.scrollIntoView({block:"nearest",inline:"nearest"});if(b)o.getElementById(b)?.focus({preventScroll:!0});else if($)Array.from(o.querySelectorAll(".item[data-select]")).find((i)=>i.dataset.select===$)?.focus({preventScroll:!0});let m=(i,r)=>o.querySelector(`#${i}`)?.addEventListener("click",r);o.querySelectorAll("[data-select]").forEach((i)=>i.onclick=()=>{if(V)return;M=!1,d=i.dataset.select,y="component",L=!1,_="fit",R="isolated",N=!1,h()}),m("previous-round",()=>{N=!0,h()}),m("current-round",()=>{N=!1,h()}),m("review-changes",()=>{let i=c().filter((j)=>pi(j).kind==="pending"),r=i.findIndex((j)=>j.id===d),f=i[(r+1)%i.length];if(f)M=!1,d=f.id,y="component",g="attention",N=!1,_="fit",L=!1,R="isolated",h()}),o.querySelectorAll("[data-filter]").forEach((i)=>i.onclick=()=>{M=!1,g=i.dataset.filter;let r=c().filter((j)=>g==="all"||pi(j).kind==="approved"===(g==="approved"));if(!(g!=="approved"&&n.missing.some((j)=>j.id===d))&&!r.some((j)=>j.id===d)&&r.length)d=r[0].id,y="component",N=!1,_="fit",L=!1,R="isolated";h()}),m("approve",()=>bi("approve")),m("revise",pe),m("clear",()=>{if(l)delete n.decisions[l.id],delete H[l.id];W=null,h()}),m("cancel-feedback",()=>{if(l)delete H[l.id];if(F)k=!0,F=!1;h(),ni("revise")}),o.querySelector("#feedback-form")?.addEventListener("submit",(i)=>{i.preventDefault(),bi("revise")}),o.querySelector("#feedback")?.addEventListener("keydown",(i)=>{let r=i;if(r.key==="Enter"&&(r.metaKey||r.ctrlKey)&&!r.isComposing)r.preventDefault(),r.stopPropagation(),bi("revise")}),m("undo-decision",()=>{if(!W||J||Q)return;let i=W;if(i.previous)n.decisions[i.id]=i.previous;else delete n.decisions[i.id];delete H[i.id],d=i.id,M=!1,N=!1,y="component",g="all",W=null,h(),ni("approve")}),m("overlay",()=>{L=!L,h()}),m("isolated",()=>{R="isolated",h()}),m("context",()=>{R="context",h()}),o.querySelector("#zoom")?.addEventListener("change",(i)=>{let r=i.target.value;_=r==="fit"?"fit":Number(r),h()}),m("background-checker",()=>{ei="checker",h()}),m("background-page",()=>{ei="page",h()}),m("show-all",()=>{U=!U,k=!0,h()}),m("toggle-tray",()=>{F=!1,k=!k,h()}),m("show-comp",()=>{y="comp",h()}),m("show-component",()=>{y="component",h()}),m("mark",()=>{V=!V,h()}),m("add-box",()=>qi({x:0.35,y:0.35,w:0.2,h:0.2})),m("remove-missing",()=>{n.missing=n.missing.filter((i)=>i.id!==d),d=e.components[0]?.id,h()}),m("approve-rest",()=>{if(P)return;n=Ci(e,n),W=null,M=!0,y="component",h(),ni("review-summary")}),o.querySelector("#inventory-confirm")?.addEventListener("change",(i)=>{n.inventoryConfirmed=i.target.checked,h()}),o.querySelector("#feedback")?.addEventListener("input",(i)=>{if(l&&H[l.id])H[l.id].feedback=i.target.value}),o.querySelector("#split")?.addEventListener("change",(i)=>{if(l&&H[l.id])H[l.id].split=i.target.checked}),o.querySelector("#missing-name")?.addEventListener("input",(i)=>{if(w)w.name=i.target.value;let r=o.querySelector("#submit");if(r)r.disabled=P||J||Q||!hi(e,n).canSubmit}),o.querySelector("#missing-feedback")?.addEventListener("input",(i)=>{if(w)w.feedback=i.target.value}),o.querySelectorAll("[data-coordinate]").forEach((i)=>i.addEventListener("change",()=>{if(!w)return;let r=i.dataset.coordinate,f=Number(i.value)/100;if(Number.isFinite(f))w.box[r]=Math.max(r==="w"||r==="h"?0.001:0,Math.min(1,f));w.box.w=Math.min(w.box.w,1-w.box.x),w.box.h=Math.min(w.box.h,1-w.box.y),h()})),m("submit",async()=>{if(P||J||Q)return;J=!0,si="",h();try{await a.onSubmit(Fi(e,n)),Q=!0}catch(i){si=i instanceof Error?i.message:"Could not save. Try again."}finally{J=!1,h()}});let A=o.querySelector(".map");function Li(i){let r=A.getBoundingClientRect();return{x:Math.max(0,Math.min(1,(i.clientX-r.left)/r.width)),y:Math.max(0,Math.min(1,(i.clientY-r.top)/r.height))}}A.addEventListener("pointerdown",(i)=>{if(!V)return;T=Li(i),A.setPointerCapture(i.pointerId),i.preventDefault()}),A.addEventListener("pointermove",(i)=>{if(!T)return;let r=Li(i);X={x:Math.min(T.x,r.x),y:Math.min(T.y,r.y),w:Math.abs(r.x-T.x),h:Math.abs(r.y-T.y)};let f=o.querySelector(".draw-box");f.hidden=!1,f.style.cssText=Mi(X)}),A.addEventListener("pointerup",()=>{if(X&&X.w>0.01&&X.h>0.01)qi(X);else T=null,X=null}),A.addEventListener("pointercancel",()=>{T=null,X=null,h()});let Ui=o.querySelector(".output"),li=o.querySelector("iframe"),Zi=o.querySelector(".workbench"),ii=o.querySelector(".inspection-content"),vi=o.querySelector(".inspector");function yi(){if(!ii||!vi)return;vi.dataset.scrollAbove=String(ii.scrollTop>1),vi.dataset.scrollBelow=String(ii.scrollHeight-ii.clientHeight-ii.scrollTop>1)}ii?.addEventListener("scroll",yi,{passive:!0});function Ii(){let i=o.querySelector(".map-space");if(i&&i.clientWidth&&i.clientHeight){let z=ki(e.comp.width,e.comp.height,Math.max(1,i.clientWidth-32),Math.max(1,i.clientHeight-32),"fit");A.style.width=`${z.width}px`,A.style.height=`${z.height}px`}let r=o.querySelector(".inspection-content"),f=Array.from(o.querySelectorAll(".pan-viewport"));if(r?.clientHeight)f.forEach((z)=>z.style.height=`${Math.min(248,Math.max(100,r.clientHeight-40))}px`);if(t&&f.length){let z=ki(t.box.w*E.comp.width,t.box.h*E.comp.height,Math.min(...f.map((G)=>G.clientWidth)),Math.min(...f.map((G)=>G.clientHeight)),_);o.querySelectorAll(".crop-stage").forEach((G)=>{G.style.width=`${z.width}px`,G.style.height=`${z.height}px`})}if(Ui&&li&&t){let z=Ui.clientWidth/(t.box.w*E.comp.width);li.style.transform=`scale(${z})`,li.style.left=`${-t.box.x*E.comp.width*z}px`,li.style.top=`${-t.box.y*E.comp.height*z}px`}let j=Zi.getBoundingClientRect(),Bi=o.querySelector(".region"),Di=o.querySelector(".number"),Oi=o.querySelector(".connector path");if(Bi&&Di&&Oi){let z=Bi.getBoundingClientRect(),G=Di.getBoundingClientRect(),be=z.right-j.left,me=z.top+z.height/2-j.top,Ri=G.left-j.left-8,we=G.top+G.height/2-j.top;Oi.setAttribute("d",`M ${be} ${me} H ${Ri-14} V ${we} H ${Ri}`)}}oi=new ResizeObserver(()=>{Ii(),yi()}),oi.observe(Zi);let Ai=o.querySelector(".inspection-content");if(Ai)oi.observe(Ai);Ii();let zi=Array.from(o.querySelectorAll(".pan-viewport"));if(zi.forEach((i)=>{i.scrollLeft=le,i.scrollTop=de}),zi.forEach((i)=>i.addEventListener("scroll",()=>{for(let r of zi)if(r!==i){if(r.scrollLeft!==i.scrollLeft)r.scrollLeft=i.scrollLeft;if(r.scrollTop!==i.scrollTop)r.scrollTop=i.scrollTop}})),ae&&window.matchMedia("(max-width:800px)").matches){let i=o.querySelector(".inspection-content"),r=o.querySelector(".compare");if(i&&r)i.scrollTop+=r.getBoundingClientRect().top-i.getBoundingClientRect().top}yi(),window.scrollTo(ri,ti)}return h(),{destroy(){oi?.disconnect(),o.replaceChildren()},getDraft(){return structuredClone(n)}}}async function ye(){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 a=await e.json();if(a.sourceStatus){let o=document.createElement("p");o.textContent=a.sourceStatus,p.before(o)}oe(p,a.packet,{initialDraft:a.draft,history:a.history,completed:!!a.receipt,onSubmit:async(o)=>{let n=await fetch("/decision",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)}),c=await n.json();if(!n.ok)throw Error(c.error??"The review could not be saved. Try again.")}})}ye().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/model.test.ts b/ui/component-review/model.test.ts index 8c3080117..e578e29d7 100644 --- a/ui/component-review/model.test.ts +++ b/ui/component-review/model.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from 'bun:test'; -import { approveRemaining, newDraft, submission, summarize, validBox, type ReviewPacket } from './model'; +import { nextUnreviewed, approveRemaining, newDraft, submission, summarize, validBox, type ReviewPacket } from './model'; const packet: ReviewPacket = { id:'review-1', revision:'packet-1', title:'Test', round:1, comp:{url:'/comp.png',width:100,height:100}, components:[{id:'art',revision:'art-1',name:'Art',medium:'Raster',note:'',box:{x:0,y:0,w:1,h:1},preview:{kind:'image',url:'/art.png'}},{id:'control',revision:'control-1',name:'Button',medium:'HTML',note:'',box:{x:0,y:0,w:.1,h:.1},preview:{kind:'page',url:'/page.html'}}] }; describe('component review drafts',()=>{ test('bulk approval still requires explicit inventory confirmation',()=>{const draft=approveRemaining(packet,newDraft(packet));expect(summarize(packet,draft).canSubmit).toBe(false);draft.inventoryConfirmed=true;expect(submission(packet,draft).requestId).toBe(packet.id);}); @@ -45,3 +45,18 @@ test('captured code keeps its implementation identity without trusting medium as expect(componentPresentation(captured)).toMatchObject({code:true,captured:true,label:'HTML',caption:'Rendered component',fileLabel:'Open captured preview'}); expect(componentPresentation(packet.components[1]).caption).toBe('Live component'); }); + + +test('review progression skips decisions, wraps, and revisits stale components', () => { + const draft = newDraft(packet); + expect(nextUnreviewed(packet,draft)).toBe(packet.components[0].id); + const first=packet.components[0],second=packet.components[1]; + draft.decisions[first.id]={revision:first.revision,action:'revise',feedback:'',split:false}; + expect(nextUnreviewed(packet,draft,first.id)).toBe(second.id); + expect(nextUnreviewed(packet,draft,second.id)).toBe(second.id); + draft.decisions[second.id]={revision:second.revision,action:'approve',feedback:'',split:false}; + expect(nextUnreviewed(packet,draft,second.id)).toBeUndefined(); + draft.decisions[first.id].revision='stale'; + expect(nextUnreviewed(packet,draft,second.id)).toBe(first.id); + expect(nextUnreviewed({...packet,components:[]},draft)).toBeUndefined(); +}); diff --git a/ui/component-review/model.ts b/ui/component-review/model.ts index 666317505..6bd1b3643 100644 --- a/ui/component-review/model.ts +++ b/ui/component-review/model.ts @@ -80,3 +80,13 @@ export function componentPresentation(component: Component) { fileLabel: captured ? 'Open captured preview' : 'Open source image', }; } + +/** Continue in component order, wrapping once and skipping current decisions. + * Carried approvals arrive as versioned decisions; stale ones remain pending. */ +export function nextUnreviewed(packet: ReviewPacket, draft: Draft, after?: string): string | undefined { + const start = packet.components.findIndex(c => c.id === after); + for (let step = 1; step <= packet.components.length; step++) { + const component = packet.components[(start + step) % packet.components.length]; + if (componentState(component, draft).kind === 'pending') return component.id; + } +} diff --git a/ui/component-review/review.ts b/ui/component-review/review.ts index c577c3785..1bbcc79b8 100644 --- a/ui/component-review/review.ts +++ b/ui/component-review/review.ts @@ -1,4 +1,4 @@ -import { componentPresentation, approveRemaining, componentState, repairStatus, newDraft, submission, summarize, type Box, type Draft, type ReviewPacket, type ReviewHistory } from './model'; +import { componentPresentation, nextUnreviewed, approveRemaining, componentState, repairStatus, newDraft, submission, summarize, type Box, type Decision, type Draft, type ReviewPacket, type ReviewHistory } from './model'; import { comparisonSize } from './viewport'; import { styles } from './styles'; import { icon } from './icons'; @@ -23,9 +23,14 @@ export function mountComponentReview(host: HTMLElement, packet: ReviewPacket, op let sending = false; let submitted = options.completed ?? false; let error = ''; + const edits: Record = {}; + let finished = false; + let lastDecision: {id: string; name: string; action: 'approve' | 'revise'; previous?: Decision} | null = null; + const shortcutLabel = /Mac|iPhone|iPad/.test(navigator.platform) ? '⌘Enter' : 'Ctrl+Enter'; let overlay = false; let showAll = false; let trayOpen = true; + let restoreTrayAfterFeedback = false; let mobilePane: 'comp' | 'component' = 'comp'; let renderedMobilePane = mobilePane as 'comp' | 'component'; let zoom: 'fit' | number = 'fit'; @@ -40,27 +45,48 @@ export function mountComponentReview(host: HTMLElement, packet: ReviewPacket, op const checkIcon = ''; const feedbackIcon = ''; const boxStyle = (b: Box) => `left:${pct(b.x)};top:${pct(b.y)};width:${pct(b.w)};height:${pct(b.h)}`; + function focusReview(id: string) { + root.getElementById(id)?.focus({preventScroll:true}); + } + function advance(after: string) { + const next = nextUnreviewed(packet, draft, after); + finished = !next; + if (next) selected = next; + mobilePane='component'; previousRound=false; overlay=false; zoom='fit'; outputMode='isolated'; + if (inventoryFilter==='approved') inventoryFilter='attention'; + render(); + focusReview(finished ? 'review-summary' : edits[selected!] ? 'feedback' : 'approve'); + } function updateDecision(action: 'approve' | 'revise') { + if (sending || submitted || previousRound) return; const c = packet.components.find(c => c.id === selected); if (!c) return; - draft.decisions[c.id] = {revision:c.revision, action, feedback:draft.decisions[c.id]?.feedback ?? '', split: action === 'revise' && (draft.decisions[c.id]?.split ?? false)}; + const saved = draft.decisions[c.id]; + const current = saved?.revision===c.revision ? saved : undefined; + lastDecision = {id:c.id, name:c.name, action, previous:saved ? {...saved} : undefined}; + const note = edits[c.id] ?? current; + 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); + } + function beginFeedback() { + if (sending || submitted || previousRound) return; + const c = packet.components.find(c=>c.id===selected); + if (!c) return; + const saved = draft.decisions[c.id]; + const current = saved?.revision===c.revision ? saved : undefined; + edits[c.id] ??= {feedback:current?.feedback ?? '',split:current?.split ?? false}; + if(trayOpen && (root.querySelector('.workbench')?.clientHeight ?? 0)<420){restoreTrayAfterFeedback=true;trayOpen=false;} + finished=false; render(); - if(action==='revise') { - const field=root.querySelector('#feedback'); - field?.focus({preventScroll:true}); - const form=root.querySelector('.review-form'); - if(form&&field){const overflow=field.getBoundingClientRect().bottom-form.getBoundingClientRect().bottom;if(overflow>0)form.scrollTop+=overflow+8;} - const pane=root.querySelector('.inspection-content'); - const comparison=root.querySelector('.compare'); - if(pane&&comparison)pane.scrollTop+=comparison.getBoundingClientRect().top-pane.getBoundingClientRect().top; - - } + focusReview('feedback'); } function addMissing(box: Box) { const id = `missing-${crypto.randomUUID()}`; draft.missing.push({ id, name:'Missing component', feedback:'', box }); draft.inventoryConfirmed = false; - selected = id; mobilePane='component'; marking = false; drag = null; dragBox = null; + finished=false; selected = id; mobilePane='component'; marking = false; drag = null; dragBox = null; render(); root.querySelector('#missing-name')?.focus(); } @@ -86,6 +112,10 @@ export function mountComponentReview(host: HTMLElement, packet: ReviewPacket, op const index = c ? packet.components.indexOf(c) + 1 : packet.components.length + draft.missing.findIndex(m => m.id === selected) + 1; const savedDecision = c ? draft.decisions[c.id] : undefined; const d = savedDecision?.revision === c?.revision ? savedDecision : undefined; + const edit = c ? edits[c.id] : undefined; + const uncommitted = Object.keys(edits).length > 0; + const isLast = c ? !packet.components.some(item=>item.id!==c.id && componentState(item,draft).kind==='pending') : false; + const notice = lastDecision ? `
${esc(lastDecision.name)} ${lastDecision.action==='approve'?'approved':'flagged for repair'}.
` : ''; const stats = summarize(packet, draft); const history = options.history; const repair = c ? repairStatus(c.id, history) : undefined; @@ -107,7 +137,7 @@ export function mountComponentReview(host: HTMLElement, packet: ReviewPacket, op addedCount ? `${addedCount} added` : '', history?.removed.length ? `${history.removed.length} removed` : '', ].filter(Boolean).join(' · '); - const statusMessage = error || (submitted ? (options.preview ? 'Preview submitted. No run changed.' : 'Review submitted.') : stats.hasFeedback ? 'Ready to send for corrections.' : stats.pending ? `${stats.pending} left to review` : !draft.inventoryConfirmed ? 'Confirm the map is complete.' : 'Ready to continue.'); + const statusMessage = error || (uncommitted ? 'Save or cancel your open feedback before sending.' : submitted ? (options.preview ? 'Preview submitted. No run changed.' : 'Review submitted.') : stats.hasFeedback ? 'Ready to send for corrections.' : stats.pending ? `${stats.pending} left to review` : !draft.inventoryConfirmed ? 'Confirm the map is complete.' : 'Ready to continue.'); const presentation = v ? componentPresentation(v) : null; const isRaster = v?.preview.kind === 'image' && !presentation?.code; const useContext = !!(v?.context && outputMode === 'context'); @@ -124,7 +154,7 @@ export function mountComponentReview(host: HTMLElement, packet: ReviewPacket, op

Approved comp

Approved composition for ${esc(packet.title)} - ${box ? `
` : ''} + ${box && !finished ? `
` : ''} ${packet.components.map((item,i) => {const state=stateFor(item);return ``}).join('')} ${draft.missing.map((item,i)=>``).join('')} @@ -134,8 +164,8 @@ export function mountComponentReview(host: HTMLElement, packet: ReviewPacket, op ${marking ? '
Draw around the missing piece.
' : ''}
-

${index} ${esc(c?.name ?? missing?.name ?? 'Component')}

- ${c ? `
${icon(presentation!.code ? 'code' : 'image')}${esc(materialLabel)}${v?.material ? `${v.material.width} × ${v.material.height} px` : ''}${v?.preview.kind==='image'?`${icon('external')}`:''}
+

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

+ ${finished ? `

Components reviewed.

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

Check your decisions below. ${stats.hasFeedback ? 'Send feedback when you’re ready.' : 'Confirm nothing is missing, then approve and continue.'}

${packet.components.map(item=>{const decision=draft.decisions[item.id];return ``;}).join('')}${draft.missing.map(item=>``).join('')}
${notice}
` : c ? `
${icon(presentation!.code ? 'code' : 'image')}${esc(materialLabel)}${v?.material ? `${v.material.width} × ${v.material.height} px` : ''}${v?.preview.kind==='image'?`${icon('external')}`:''}
${history ? `
${repair?.prior?.action==='revise'?`

Previous feedback Round ${repair.feedbackRound}

Needs work

${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)}
`:''}
`:''} @@ -149,36 +179,48 @@ export function mountComponentReview(host: HTMLElement, packet: ReviewPacket, op ${isRaster ? `
${v!.context ? `
` : ''}
` : ''}

${esc(v?.context?.layering ?? 'Layer placement not recorded.')}

${esc(v!.note)}

-
${viewingPrevious?'

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

':''}
Your review Round ${packet.round}${viewingPrevious?'

Return to Current to review this round.

':''}
${d ? `` : ''}
- ${d?.action === 'revise' ? `` : ''} +
${notice}${viewingPrevious?'

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

':''}
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.

'}

Components

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

${inventoryFilter==='attention'?'Every component is approved. Confirm the map is complete, then continue.':'No components approved yet.'}

`:''}
-

${esc(statusMessage)}

+

${esc(statusMessage)}

`; - if(viewingPrevious)root.querySelectorAll('.decisions button,#feedback,#split,#approve-rest,#submit,#inventory-confirm').forEach(el=>el.disabled=true); + 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,#approve-rest,#mark,#inventory-confirm,#missing-name,#missing-feedback,#feedback,#split,#remove-missing,[data-coordinate]').forEach(el=>el.disabled=true); + 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; 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}); const on = (id:string, action:()=>void) => root.querySelector(`#${id}`)?.addEventListener('click', action); - root.querySelectorAll('[data-select]').forEach(el => el.onclick = () => {if(marking)return; selected=el.dataset.select!; mobilePane='component'; overlay=false; zoom='fit'; outputMode='isolated'; previousRound=false; render();}); + root.querySelectorAll('[data-select]').forEach(el => el.onclick = () => {if(marking)return; finished=false; selected=el.dataset.select!; mobilePane='component'; overlay=false; zoom='fit'; outputMode='isolated'; previousRound=false; render();}); on('previous-round',()=>{previousRound=true;render();}); on('current-round',()=>{previousRound=false;render();}); - on('review-changes',()=>{const pending=orderedComponents().filter(item=>stateFor(item).kind==='pending');const current=pending.findIndex(item=>item.id===selected);const next=pending[(current+1)%pending.length];if(next){selected=next.id;mobilePane='component';inventoryFilter='attention';previousRound=false;zoom='fit';overlay=false;outputMode='isolated';render();}}); + on('review-changes',()=>{const pending=orderedComponents().filter(item=>stateFor(item).kind==='pending');const current=pending.findIndex(item=>item.id===selected);const next=pending[(current+1)%pending.length];if(next){finished=false;selected=next.id;mobilePane='component';inventoryFilter='attention';previousRound=false;zoom='fit';overlay=false;outputMode='isolated';render();}}); root.querySelectorAll('[data-filter]').forEach(button=>button.onclick=()=>{ - inventoryFilter=button.dataset.filter as typeof inventoryFilter; + finished=false;inventoryFilter=button.dataset.filter as typeof inventoryFilter; const matches=orderedComponents().filter(item=>inventoryFilter==='all'||(stateFor(item).kind==='approved')===(inventoryFilter==='approved')); const selectedMissing=inventoryFilter!=='approved'&&draft.missing.some(item=>item.id===selected); if(!selectedMissing&&!matches.some(item=>item.id===selected)&&matches.length){selected=matches[0].id;mobilePane='component';previousRound=false;zoom='fit';overlay=false;outputMode='isolated';} render(); }); - on('approve',()=>updateDecision('approve')); on('revise',()=>updateDecision('revise')); - on('clear',()=>{if(c)delete draft.decisions[c.id]; render();}); + on('approve',()=>updateDecision('approve')); on('revise',beginFeedback); + on('clear',()=>{if(c){delete draft.decisions[c.id];delete edits[c.id];}lastDecision=null; render();}); + on('cancel-feedback',()=>{if(c)delete edits[c.id];if(restoreTrayAfterFeedback){trayOpen=true;restoreTrayAfterFeedback=false;}render();focusReview('revise');}); + root.querySelector('#feedback-form')?.addEventListener('submit',e=>{e.preventDefault();updateDecision('revise');}); + root.querySelector('#feedback')?.addEventListener('keydown',e=>{ + const key=e as KeyboardEvent; + if(key.key==='Enter'&&(key.metaKey||key.ctrlKey)&&!key.isComposing){key.preventDefault();key.stopPropagation();updateDecision('revise');} + }); + on('undo-decision',()=>{ + if(!lastDecision||sending||submitted)return; + const previous=lastDecision; + if(previous.previous)draft.decisions[previous.id]=previous.previous;else delete draft.decisions[previous.id]; + delete edits[previous.id];selected=previous.id;finished=false;previousRound=false;mobilePane='component';inventoryFilter='all';lastDecision=null;render();focusReview('approve'); + }); on('overlay',()=>{overlay=!overlay; render();}); on('isolated',()=>{outputMode='isolated';render();}); on('context',()=>{outputMode='context';render();}); @@ -186,16 +228,16 @@ export function mountComponentReview(host: HTMLElement, packet: ReviewPacket, op on('background-checker',()=>{backdrop='checker';render();}); on('background-page',()=>{backdrop='page';render();}); on('show-all',()=>{showAll=!showAll;trayOpen=true;render();}); - on('toggle-tray',()=>{trayOpen=!trayOpen;render();}); + on('toggle-tray',()=>{restoreTrayAfterFeedback=false;trayOpen=!trayOpen;render();}); on('show-comp',()=>{mobilePane='comp';render();}); on('show-component',()=>{mobilePane='component';render();}); on('mark',()=>{marking=!marking;render();}); on('add-box',()=>addMissing({x:.35,y:.35,w:.2,h:.2})); on('remove-missing',()=>{draft.missing=draft.missing.filter(m=>m.id!==selected);selected=packet.components[0]?.id;render();}); - on('approve-rest',()=>{draft=approveRemaining(packet,draft);render();}); + on('approve-rest',()=>{if(uncommitted)return;draft=approveRemaining(packet,draft);lastDecision=null;finished=true;mobilePane='component';render();focusReview('review-summary');}); root.querySelector('#inventory-confirm')?.addEventListener('change',e=>{draft.inventoryConfirmed=(e.target as HTMLInputElement).checked;render();}); - root.querySelector('#feedback')?.addEventListener('input',e=>{if(c)draft.decisions[c.id].feedback=(e.target as HTMLTextAreaElement).value;}); - root.querySelector('#split')?.addEventListener('change',e=>{if(c)draft.decisions[c.id].split=(e.target as HTMLInputElement).checked;}); - root.querySelector('#missing-name')?.addEventListener('input',e=>{if(missing)missing.name=(e.target as HTMLInputElement).value; const submit=root.querySelector('#submit');if(submit)submit.disabled=!summarize(packet,draft).canSubmit;}); + root.querySelector('#feedback')?.addEventListener('input',e=>{if(c&&edits[c.id])edits[c.id].feedback=(e.target as HTMLTextAreaElement).value;}); + root.querySelector('#split')?.addEventListener('change',e=>{if(c&&edits[c.id])edits[c.id].split=(e.target as HTMLInputElement).checked;}); + root.querySelector('#missing-name')?.addEventListener('input',e=>{if(missing)missing.name=(e.target as HTMLInputElement).value; const submit=root.querySelector('#submit');if(submit)submit.disabled=uncommitted||sending||submitted||!summarize(packet,draft).canSubmit;}); root.querySelector('#missing-feedback')?.addEventListener('input',e=>{if(missing)missing.feedback=(e.target as HTMLTextAreaElement).value;}); root.querySelectorAll('[data-coordinate]').forEach(el=>el.addEventListener('change',()=>{ if(!missing)return; const k=el.dataset.coordinate as keyof Box; @@ -203,7 +245,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()=>{sending=true;error='';render();try{await options.onSubmit(submission(packet,draft));submitted=true;}catch(e){error=e instanceof Error?e.message:'Could not save. Try again.';}finally{sending=false;render();}}); + on('submit',async()=>{if(uncommitted||sending||submitted)return;sending=true;error='';render();try{await options.onSubmit(submission(packet,draft));submitted=true;}catch(e){error=e instanceof Error?e.message:'Could not save. Try again.';}finally{sending=false;render();}}); const map = root.querySelector('.map')!; function point(e: PointerEvent) {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))};} map.addEventListener('pointerdown',e=>{if(!marking)return;drag=point(e);map.setPointerCapture(e.pointerId);e.preventDefault();}); diff --git a/ui/component-review/styles.ts b/ui/component-review/styles.ts index a5716d6f8..4a86f023b 100644 --- a/ui/component-review/styles.ts +++ b/ui/component-review/styles.ts @@ -33,4 +33,15 @@ export const styles = ` @media(max-width:800px){.inventory-section .section-head{gap:10px}.inventory-filters{width:100%}.inventory-filters button{padding-inline:7px;font-size:11px;flex:1}.inventory-filters b{margin-left:3px}} @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)} -` + appLayout; +.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}.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:16px 8px}.review-summary h2{font-size:24px}.review-summary p{margin-top:12px;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} +` + appLayout + ` +.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} +.feedback-fields{flex:1;min-height:0;overflow:auto;padding:3px} +.review-form:has(#feedback-form) .decision-notice{flex-shrink:0} +.review-form:has(#feedback-form) .decisions{display:none} +.feedback-actions{flex-shrink:0;position:static;background:var(--surface,var(--paper));padding:8px 0;margin-top:8px;box-shadow:0 -5px 8px -6px #24332838} +.feedback-actions .shortcut-hint{flex-basis:auto;order:-1;margin-right:auto} +.feedback-actions .primary{margin-left:0} +@media(max-height:800px),(max-width:800px){.feedback-actions .shortcut-hint{display:none}.feedback-actions .primary{margin-left:auto}.review-form .feedback textarea{min-height:56px}.review-form:has(#feedback-form) .decision-notice{margin-bottom:4px}} +`;