diff --git a/crates/cli/src/component_capture.rs b/crates/cli/src/component_capture.rs index baac66267..e76d01e30 100644 --- a/crates/cli/src/component_capture.rs +++ b/crates/cli/src/component_capture.rs @@ -132,6 +132,7 @@ fn render_page( return {html:document.documentElement.outerHTML,svg:document.querySelectorAll('svg').length,images:document.images.length,controls:document.querySelectorAll('button,input,select,textarea,a[href]').length}; })()"#.replace("ASSEMBLED", if assembled { "true" } else { "false" }); let dom=page.evaluate_value_in_world(&world,&inspect).map_err(|e|e.message)?; + let fonts = page.evaluate_value_in_world(&world, include_str!("component_fonts.js")).map_err(|e| e.message)?; let isolated = if let Some(targets) = isolation { page.set_transparent_background().map_err(|e| e.message)?; let script = format!("({})({})", include_str!("component_isolation.js"), targets); @@ -179,6 +180,7 @@ fn render_page( proof["isolation"] = isolated; proof["capturedDomSha256"] = json!(hash(captured_dom.as_str().unwrap().as_bytes())); } + proof["fonts"] = fonts; if assembled { proof["kind"] = json!("assembled-page"); proof["scriptPolicy"] = json!("pinned-local-and-inline; network-api-and-workers-disabled"); @@ -321,6 +323,23 @@ mod tests { use super::*; #[test] #[ignore = "requires Chromium"] + fn missing_primary_font_is_rejected_and_pinned_font_is_captured() { + let png = impeccable_comp::png_io::encode_png(&impeccable_comp::raster::create_image(300,100,[255;4]),&[]).unwrap(); + let html = "
Hotel review
"; + let mut inputs = BTreeMap::from([("comp.png".into(),png),("index.html".into(),html.as_bytes().to_vec())]); + let packet = json!({"schemaVersion":2,"stage":"components","comp":{"url":"/files/comp.png","width":300,"height":100},"components":[{"id":"piece","box":{"x":0,"y":0,"w":1,"h":1},"preview":{"kind":"page","url":"/files/index.html","selector":"#piece"},"dependencies":[]}]}); + let error = NativeComponentCapturer.capture(&mut packet.clone(),&inputs).err().unwrap(); + assert!(error.contains("Primary fonts unavailable: ReviewFixtureFont"), "{error}"); + inputs.insert("index.html".into(),format!("{html}").into_bytes()); + inputs.insert("font.ttf".into(),include_bytes!("../../../ui/component-review/fonts/albertsans.ttf").to_vec()); + let mut local = packet.clone();local["components"][0]["dependencies"]=json!(["font.ttf"]); + let capture=NativeComponentCapturer.capture(&mut local,&inputs).unwrap(); + assert_eq!(capture.evidence["components"][0]["views"]["preview"]["fonts"]["primaryFamilies"],json!(["ReviewFixtureFont"])); + inputs.insert("index.html".into(),html.replace("'ReviewFixtureFont',sans-serif","sans-serif").into_bytes()); + NativeComponentCapturer.capture(&mut packet.clone(),&inputs).unwrap(); + } + #[test] + #[ignore = "requires Chromium"] fn shared_document_reports_all_missing_images_before_decode_and_names_corrupt_images() { let image = impeccable_comp::raster::create_image(40,40,[255,255,255,255]); let png = impeccable_comp::png_io::encode_png(&image,&[]).unwrap(); diff --git a/crates/cli/src/component_fonts.js b/crates/cli/src/component_fonts.js new file mode 100644 index 000000000..96e33e67e --- /dev/null +++ b/crates/cli/src/component_fonts.js @@ -0,0 +1,27 @@ +// document.fonts.ready/check alone do not detect a removed @font-face: the +// browser considers a system fallback successful. Probe each visible text +// run's primary family against distinct generic fallbacks without changing DOM. +(() => { + const generic = new Set(['serif','sans-serif','monospace','cursive','fantasy','system-ui','ui-serif','ui-sans-serif','ui-monospace','ui-rounded','math','fangsong']); + const families = new Set(); + const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT); + for (let node; (node = walker.nextNode());) { + if (!node.textContent.trim()) continue; + const element = node.parentElement; + if (!element || element.closest('script,style,noscript')) continue; + const style = getComputedStyle(element); + if (style.visibility === 'hidden' || style.display === 'none' || style.opacity === '0') continue; + const range = document.createRange(); range.selectNodeContents(node); + if (![...range.getClientRects()].some(r => r.width > 0 && r.height > 0)) continue; + const first = (style.fontFamily.match(/"[^"]*"|'[^']*'|[^,]+/g) || [])[0]?.trim(); + const family = first?.replace(/^(['"])(.*)\1$/, '$2'); + if (family && !generic.has(family.toLowerCase())) families.add(family); + } + const ctx = document.createElement('canvas').getContext('2d'); + const sample = 'mmmmmmmmWWWWiiii0123456789'; + const width = font => { ctx.font = `72px ${font}`; return ctx.measureText(sample).width; }; + const missing = [...families].filter(family => ['monospace','serif','sans-serif'].every(base => + Math.abs(width(`${JSON.stringify(family)}, ${base}`) - width(base)) < .001)); + if (missing.length) throw Error(`Primary fonts unavailable: ${missing.join(', ')}. Preserve the intended typography: include local font files and @font-face declarations in the preview dependencies. Removing font imports produces a fallback preview, not a faithful component capture.`); + return {primaryFamilies: [...families].sort(), check: 'visible-primary-family-availability-v1'}; +})() diff --git a/crates/context/assets/component-review.js b/crates/context/assets/component-review.js index 2979ff6ce..3b465252b 100644 --- a/crates/context/assets/component-review.js +++ b/crates/context/assets/component-review.js @@ -1,4 +1,4 @@ -(()=>{function Ni(p,o){let x=o?.changes[p],e=o?.feedback?.[p],n=e?.decision??(o?.submitted?o.draft.decisions[p]:void 0),a=x?.kind==="unchanged"&&(x.carried??(o?.submitted&&n?.action==="approve"))===!0;return{change:x,prior:n,feedbackRound:e?.round??o?.packet.round,carried:a,label:x?.kind==="added"?"New component":x?.kind==="changed"?"Review again":a?"Approval kept":n?.action==="revise"?"Changes still requested":"Awaiting review"}}function I(p,o,x){let e=o.decisions[p.id],n=e?.revision===p.revision?e:void 0,a=Ni(p.id,x);if(n?.action==="approve")return{kind:"approved",label:a.carried?"Approval kept":"Approved",priority:3};if(n?.action==="revise")return{kind:"feedback",label:"Feedback ready",priority:2};return{kind:"pending",label:a.change?.kind==="changed"?"Review again":a.change?.kind==="added"?"New · review needed":"Not reviewed",priority:a.change?.kind==="changed"||a.change?.kind==="added"?0:1}}function Jo(p){return{packetRevision:p.revision,decisions:{},missing:[],inventoryConfirmed:!1}}function ir(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 zi(p,o){let x=p.components.map((g)=>o.decisions[g.id]?.revision===g.revision?o.decisions[g.id]:void 0),e=x.filter((g)=>g?.action==="approve").length,n=x.filter((g)=>g?.action==="revise").length,a=x.length-e-n,b=n>0||o.missing.length>0;return{approved:e,revisions:n,pending:a,hasFeedback:b,canSubmit:o.packetRevision===p.revision&&o.missing.every((g)=>g.name.trim()&&ir(g.box))&&(b||!a&&o.inventoryConfirmed)}}function $o(p,o){let x={...o.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{...o,decisions:x}}function Ko(p,o){if(!zi(p,o).canSubmit)throw Error("Review is incomplete or stale");return{schemaVersion:1,requestId:p.id,...structuredClone(o)}}function yi(p){let o=p.preview.kind==="page"||p.preview.sourceKind==="page",x=p.preview.sourceKind==="page";return{code:o,captured:x,label:o?p.medium.match(/html|css|svg/i)?p.medium:"HTML / CSS / SVG":"Raster",caption:o?x?p.preview.isolation?"Component only":"Region capture":"Live component":"Produced asset",fileLabel:x?"Open captured preview":"Open source image"}}function Qo(p,o,x){let e=p.components.findIndex((n)=>n.id===x);for(let n=1;n<=p.components.length;n++){let a=p.components[(e+n)%p.components.length];if(I(a,o).kind==="pending")return a.id}}function Wi(p,o){if(!o.reviewGroup||!yi(o).code)return[o];return p.components.filter((x)=>x.reviewGroup===o.reviewGroup&&yi(x).code)}function Mi(p,o,x,e,n=[]){let a=e?Wi(p,x):[x];if(a.length===1)return[x];return a.filter((b)=>I(b,o).kind==="pending"&&(b.id===x.id||!n.includes(b.id)))}function Yo(p,o,x){let e=new Set;return p.components.flatMap((n)=>{if(e.has(n.id))return[];let a=Wi(p,n);a.forEach((s)=>e.add(s.id));let b=a.filter((s)=>I(s,o,x).kind==="pending"),g=a.filter((s)=>I(s,o,x).kind==="feedback"),z=b[0]??g[0]??n,_=b.length?"pending":g.length?"feedback":"approved";return[{id:n.id,members:a,representative:z,pending:b.length,kind:_,label:a.length>1?n.reviewGroup.replace(/[-_]+/g," ").replace(/^./,(s)=>s.toUpperCase()):n.name,box:{x:Math.min(...a.map((s)=>s.box.x)),y:Math.min(...a.map((s)=>s.box.y)),w:Math.max(...a.map((s)=>s.box.x+s.box.w))-Math.min(...a.map((s)=>s.box.x)),h:Math.max(...a.map((s)=>s.box.y+s.box.h))-Math.min(...a.map((s)=>s.box.y))},stateLabel:b.length?a.length>1?`${b.length} to review`:I(z,o,x).label:g.length?a.length>1?`${g.length} ${g.length===1?"needs":"need"} work`:"Feedback ready":"Approved"}]})}function io(p,o){let x=new Set(o.preview.isolation?.excludedComponents??[]),e=o.box,n=p.components.filter((a)=>{if(a.id===o.id)return!1;if(x.has(a.id))return!0;let b=a.box,g=Math.max(0,Math.min(e.x+e.w,b.x+b.w)-Math.max(e.x,b.x))*Math.max(0,Math.min(e.y+e.h,b.y+b.h)-Math.max(e.y,b.y));return b.w*b.h=0.98});return{description:o.note.trim(),related:n,excluded:n.filter((a)=>x.has(a.id))}}function oo(p,o,x,e,n){let a=n==="fit"?Math.min(1,x/p,e/o):n;return{scale:a,width:p*a,height:o*a}}function ro(p,o,x,e){if(x<=0||e<=x)return 0;return Math.max(0,Math.min(1,((p-o)/x-0.08)/0.84))*(e-x)}var _o=` +(()=>{function Li(a,o){let x=o?.changes[a],e=o?.feedback?.[a],n=e?.decision??(o?.submitted?o.draft.decisions[a]:void 0),p=x?.kind==="unchanged"&&(x.carried??(o?.submitted&&n?.action==="approve"))===!0;return{change:x,prior:n,feedbackRound:e?.round??o?.packet.round,carried:p,label:x?.kind==="added"?"New component":x?.kind==="changed"?"Review again":p?"Approval kept":n?.action==="revise"?"Changes still requested":"Awaiting review"}}function O(a,o,x){let e=o.decisions[a.id],n=e?.revision===a.revision?e:void 0,p=Li(a.id,x);if(n?.action==="approve")return{kind:"approved",label:p.carried?"Approval kept":"Approved",priority:3};if(n?.action==="revise")return{kind:"feedback",label:"Feedback ready",priority:2};return{kind:"pending",label:p.change?.kind==="changed"?"Review again":p.change?.kind==="added"?"New · review needed":"Not reviewed",priority:p.change?.kind==="changed"||p.change?.kind==="added"?0:1}}function Ko(a){return{packetRevision:a.revision,decisions:{},missing:[],inventoryConfirmed:!1}}function or(a){return Object.values(a).every(Number.isFinite)&&a.x>=0&&a.y>=0&&a.w>0&&a.h>0&&a.x+a.w<=1.00001&&a.y+a.h<=1.00001}function ki(a,o){let x=a.components.map((h)=>o.decisions[h.id]?.revision===h.revision?o.decisions[h.id]:void 0),e=x.filter((h)=>h?.action==="approve").length,n=x.filter((h)=>h?.action==="revise").length,p=x.length-e-n,y=n>0||o.missing.length>0;return{approved:e,revisions:n,pending:p,hasFeedback:y,canSubmit:o.packetRevision===a.revision&&o.missing.every((h)=>h.name.trim()&&or(h.box))&&(y||!p&&o.inventoryConfirmed)}}function Qo(a,o){let x={...o.decisions};for(let e of a.components)if(!x[e.id]||x[e.id].revision!==e.revision)x[e.id]={revision:e.revision,action:"approve",feedback:"",split:!1};return{...o,decisions:x}}function Yo(a,o){if(!ki(a,o).canSubmit)throw Error("Review is incomplete or stale");return{schemaVersion:1,requestId:a.id,...structuredClone(o)}}function zi(a){let o=a.preview.kind==="page"||a.preview.sourceKind==="page",x=a.preview.sourceKind==="page";return{code:o,captured:x,label:o?a.medium.match(/html|css|svg/i)?a.medium:"HTML / CSS / SVG":"Raster",caption:o?x?a.preview.isolation?"Component only":"Region capture":"Live component":"Produced asset",fileLabel:x?"Open captured preview":"Open source image"}}function _o(a,o,x){let e=a.components.findIndex((n)=>n.id===x);for(let n=1;n<=a.components.length;n++){let p=a.components[(e+n)%a.components.length];if(O(p,o).kind==="pending")return p.id}}function Wi(a,o){if(!o.reviewGroup||!zi(o).code)return[o];return a.components.filter((x)=>x.reviewGroup===o.reviewGroup&&zi(x).code)}function Vi(a,o,x,e,n=[]){let p=e?Wi(a,x):[x];if(p.length===1)return[x];return p.filter((y)=>O(y,o).kind==="pending"&&(y.id===x.id||!n.includes(y.id)))}function Xo(a,o,x){let e=new Set;return a.components.flatMap((n)=>{if(e.has(n.id))return[];let p=Wi(a,n);p.forEach((s)=>e.add(s.id));let y=p.filter((s)=>O(s,o,x).kind==="pending"),h=p.filter((s)=>O(s,o,x).kind==="feedback"),v=y[0]??h[0]??n,N=y.length?"pending":h.length?"feedback":"approved";return[{id:n.id,members:p,representative:v,pending:y.length,kind:N,label:p.length>1?n.reviewGroup.replace(/[-_]+/g," ").replace(/^./,(s)=>s.toUpperCase()):n.name,box:{x:Math.min(...p.map((s)=>s.box.x)),y:Math.min(...p.map((s)=>s.box.y)),w:Math.max(...p.map((s)=>s.box.x+s.box.w))-Math.min(...p.map((s)=>s.box.x)),h:Math.max(...p.map((s)=>s.box.y+s.box.h))-Math.min(...p.map((s)=>s.box.y))},stateLabel:y.length?p.length>1?`${y.length} to review`:O(v,o,x).label:h.length?p.length>1?`${h.length} ${h.length===1?"needs":"need"} work`:"Feedback ready":"Approved"}]})}function Gi(a,o){let x=new Set(o.preview.isolation?.excludedComponents??[]),e=o.box,n=a.components.filter((p)=>{if(p.id===o.id)return!1;if(x.has(p.id))return!0;let y=p.box,h=Math.min(e.x+e.w,y.x+y.w)-Math.max(e.x,y.x),v=Math.min(e.y+e.h,y.y+y.h)-Math.max(e.y,y.y);return y.w*y.h1&&v*a.comp.height>1});return{description:o.note.trim(),related:n,excluded:n.filter((p)=>x.has(p.id))}}function ro(a,o,x,e,n){let p=n==="fit"?Math.min(1,x/a,e/o):n;return{scale:p,width:a*p,height:o*p}}function eo(a,o,x,e){if(x<=0||e<=x)return 0;return Math.max(0,Math.min(1,((a-o)/x-0.08)/0.84))*(e-x)}var Eo=` :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 Lo=` +`;var No=` .review-peers{display:grid;gap:8px;padding:8px 0 12px;font-size:12px} .review-peers strong span{font-weight:400}.review-peers>div{display:flex;flex-wrap:wrap;gap:4px} .review-peers button{min-width:28px;min-height:28px;padding:3px;border:1px solid var(--line);border-radius:4px;background:var(--paper);color:inherit} @@ -94,7 +94,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} -`+_o+` +`+Eo+` .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. */ @@ -188,49 +188,51 @@ .review-scope .separate-reviews,.review-scope .scope-excluded{font-size:11px;color:var(--muted)} .instance-row .review-scope{margin:12px 0 0} #comparison-dialog .review-scope{flex:none} -`;var or={chevronDown:'',code:'',image:'',expand:'',compact:'',hideTray:'',showTray:'',next:'',mark:'',close:'',undo:'',external:'',zoom:''};function W(p){return``}var l=(p)=>p.replace(/[&<>"']/g,(o)=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[o]),di=(p)=>`${p*100}%`,S=(p)=>{let o=new URL(p,location.href);if(!["http:","https:"].includes(o.protocol))throw Error("Unsupported preview URL");return l(o.href)};function Xo(p,o,x){let e=p.attachShadow({mode:"open"}),n=structuredClone(x.initialDraft??Jo(o)),a=o.stage==="hero"&&o.components.length===1&&!n.missing.length,b=()=>[...o.components].sort((f,m)=>I(f,n,x.history).priority-I(m,n,x.history).priority),g=b().find((f)=>I(f,n,x.history).kind!=="approved")?.id??o.components[0]?.id,z=zi(o,n).pending?"pending":"reviewed",_=!1,s=!1,y=x.completed??!1,Vi="",q={},k=!!x.completed||!zi(o,n).pending,F=null,C=!0,Eo=/Mac|iPhone|iPad/.test(navigator.platform)?"⌘Enter":"Ctrl+Enter",ti=!1,H=!1,Ji=!1,Zi=null,li=!1,L=!0,mi=!1,K="comp",eo=K,ei="fit",ki="checker",T=!1,hi="isolated",Gi,no="fit",ni=null,pi=null,ai=null,ji='',$i='',po=(f,m)=>{if(f.stage==="hero")return"";let Q=io(f,m);if(!Q.related.length)return"";let gi=Q.related.map((P)=>l(P.name)).join(" · ");return`

Reviewing ${l(Q.description||m.name)}

Separate review items ${gi}

${Q.excluded.length?`

Hidden in this preview: ${Q.excluded.map((P)=>l(P.name)).join(" · ")}

`:""}
`},Ti=(f)=>`left:${di(f.x)};top:${di(f.y)};width:${di(f.w)};height:${di(f.h)}`;function bi(f){e.getElementById(f)?.focus({preventScroll:!0})}function No(f){let m=Qo(o,n,f);if(k=!m,m)g=m;K="component",T=!1,ti=!1,ei="fit",hi="isolated",C=!0,z=k?"reviewed":"pending";let Q=()=>{w(),bi(k?"review-summary":q[g]?"feedback":"approve")};if(k&&H&&Zi)Zi(Q);else Q()}async function ao(){if(s||y||T||Object.keys(q).length)return;s=!0,Vi="",w();try{await x.onSubmit(Ko(o,n)),y=!0,k=!0,_=!1,z="reviewed"}catch(f){Vi=f instanceof Error?f.message:"Could not save. Try again."}finally{s=!1,w()}}function Ui(f){if(s||y||T||Ji)return;let m=o.components.find((Z)=>Z.id===g);if(!m||!Mi(o,n,m,C,Object.keys(q)).length)return;let Q=n.decisions[m.id],gi=Q?.revision===m.revision?Q:void 0,P=Mi(o,n,m,C,Object.keys(q));F={id:m.id,name:P.length>1?`${m.reviewGroup} · ${P.length} instances`:m.name,action:f,previous:Object.fromEntries(P.map((Z)=>[Z.id,n.decisions[Z.id]?{...n.decisions[Z.id]}:void 0]))};let Ki=q[m.id]??gi;for(let Z of P)n.decisions[Z.id]={revision:Z.revision,action:f,feedback:f==="revise"?Ki?.feedback??"":"",split:f==="revise"&&(Ki?.split??!1)};if(delete q[m.id],mi)L=!0,mi=!1;if(a)n.inventoryConfirmed=f==="approve",F=null,ao();else No(m.id)}function Wo(){if(s||y||T||Ji)return;let f=o.components.find((gi)=>gi.id===g);if(!f||!Mi(o,n,f,C,Object.keys(q)).length)return;let m=n.decisions[f.id],Q=m?.revision===f.revision?m:void 0;if(q[f.id]??={feedback:Q?.feedback??"",split:Q?.split??!1},L&&(e.querySelector(".workbench")?.clientHeight??0)<420)mi=!0,L=!1;k=!1,w(),bi("feedback")}function xo(f){let m=`missing-${crypto.randomUUID()}`;n.missing.push({id:m,name:"Missing component",feedback:"",box:f}),n.inventoryConfirmed=!1,k=!1,g=m,K="component",_=!1,ni=null,pi=null,w(),e.querySelector("#missing-name")?.focus()}function w(){if(ai?.disconnect(),y||k&&!a)H=!1;let f=e.activeElement,m=f?.id,Q=f?.dataset.select,gi=window.scrollX,P=window.scrollY,Ki=e.querySelector(".inventory")?.scrollLeft??0,Z=Gi===g,Zo=K==="component"&&(!Z||eo!==K);eo=K;let Go=Z?e.querySelector(".inspection-content")?.scrollTop??0:0,To=Z&&(e.querySelector(".changed-files")?.open??!1),Uo=e.querySelector(".comparison-slot")?.clientHeight??200,to=e.querySelector(".pan-viewport"),lo=Gi===g&&no===ei,Ao=lo?to?.scrollLeft??0:0,Bo=lo?to?.scrollTop??0:0;Gi=g,no=ei;let c=o.components.find((i)=>i.id===g),M=n.missing.find((i)=>i.id===g),Y=Yo(o,n,x.history),X=Y.find((i)=>i.members.some((r)=>r.id===g)),R=!!X&&X.members.length>1&&C,ho=R?X.box:c?.box??M?.box,go=X?Y.indexOf(X)+1:Y.length+n.missing.findIndex((i)=>i.id===g)+1,fo=c?n.decisions[c.id]:void 0,xi=fo?.revision===c?.revision?fo:void 0,Qi=c?q[c.id]:void 0,fi=Object.keys(q).length>0,so=c?Wi(o,c):[],si=c?Mi(o,n,c,C,Object.keys(q)):[],Io=c?!o.components.some((i)=>!si.some((r)=>r.id===i.id)&&I(i,n).kind==="pending"):!1,Ai=F&&!y?`
${l(F.name)} ${F.action==="approve"?"approved":"flagged for repair"}.
`:"",j=zi(o,n),V=x.history,U=c?Ni(c.id,V):void 0,wi=V?.packet.components.find((i)=>i.id===c?.id),ii=T&&!!wi,d=ii?wi:c,E=ii?V.packet:o,wo=Object.values(V?.changes??{}),co=wo.filter((i)=>i.kind==="changed").length,mo=wo.filter((i)=>i.kind==="added").length,Bi=o.components.filter((i)=>I(i,n,V).kind==="approved"&&Ni(i.id,V).carried).length,Ii=(i)=>{let r=I(i,n,V);return y&&r.kind==="feedback"?{...r,label:"Changes requested"}:r},qi=Y.filter((i)=>i.kind==="pending").length,Ro=Y.length-qi+n.missing.length,bo=Y.filter((i)=>z==="all"||i.kind==="pending"===(z==="pending")).map((i)=>i.representative),Oo=(i)=>Y.find((r)=>r.members.some((t)=>t.id===i))?.label??i,Do=[j.revisions+n.missing.length?`${j.revisions+n.missing.length} feedback ready`:"",Bi?`${Bi} ${Bi===1?"approval":"approvals"} kept`:"",co?`${co} changed`:"",mo?`${mo} added`:"",V?.removed.length?`${V.removed.length} removed`:""].filter(Boolean).join(" · "),So=Vi||(fi?"Save or cancel your open feedback before sending.":y?x.preview?"Preview submitted. No run changed.":"Review submitted.":j.hasFeedback?"Ready to send for corrections.":j.pending?`${qi} left to review`:!n.inventoryConfirmed?"Confirm the map is complete.":"Ready to continue."),oi=d?yi(d):null,Ri=d?.preview.kind==="image"&&!oi?.code,Oi=Ri||d?.material?.alpha==="transparent",O=!!(d?.context&&hi==="context"),vo=d&&(O?d.context?.kind!=="image":d.preview.kind==="page"),Di=O&&d?.context?d.context.url:d?.preview.url,uo=d?io(E,d):null,Fo=oi?.code?`${oi.label} · ${oi.captured?"captured from code":"live preview"}`:d?.material?`${d.material.alpha==="transparent"?"Transparent":d.material.alpha==="opaque"?"Opaque":"Transparency unverified"} ${d.material.format}`:"Raster · transparency unverified";e.innerHTML=`
-

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

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

${y?'Submitted · read-only':x.preview?'Interactive preview':""}
+ +.reference-layer{position:absolute;box-sizing:border-box;border:1px dashed #fff;outline:1px solid #173e43;pointer-events:none;z-index:2}.reference-layer>span{position:absolute;top:0;left:0;max-width:100%;padding:1px 4px;background:#173e43;color:white;font:10px/1.4 var(--font-sans,sans-serif);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;opacity:0;transition:opacity .12s}.instance-row:hover .reference-layer>span,.instance-row:focus-visible .reference-layer>span,.pan-viewport:hover .reference-layer>span,.pan-viewport:focus .reference-layer>span{opacity:1} +`;var rr={chevronDown:'',code:'',image:'',expand:'',compact:'',hideTray:'',showTray:'',next:'',mark:'',close:'',undo:'',external:'',zoom:''};function T(a){return``}var l=(a)=>a.replace(/[&<>"']/g,(o)=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[o]),gi=(a)=>`${a*100}%`,C=(a)=>{let o=new URL(a,location.href);if(!["http:","https:"].includes(o.protocol))throw Error("Unsupported preview URL");return l(o.href)};function Zo(a,o,x){let e=a.attachShadow({mode:"open"}),n=structuredClone(x.initialDraft??Ko(o)),p=o.stage==="hero"&&o.components.length===1&&!n.missing.length,y=()=>[...o.components].sort((f,c)=>O(f,n,x.history).priority-O(c,n,x.history).priority),h=y().find((f)=>O(f,n,x.history).kind!=="approved")?.id??o.components[0]?.id,v=ki(o,n).pending?"pending":"reviewed",N=!1,s=!1,z=x.completed??!1,Ji="",M={},k=!!x.completed||!ki(o,n).pending,P=null,ii=!0,Lo=/Mac|iPhone|iPad/.test(navigator.platform)?"⌘Enter":"Ctrl+Enter",li=!1,V=!1,$i=!1,Ti=null,di=!1,L=!0,bi=!1,Y="comp",no=Y,ni="fit",ji="checker",A=!1,hi="isolated",Ui,ao="fit",ai=null,pi=null,xi=null,qi='',Ki='',po=(f,c)=>{if(f.stage==="hero")return"";let j=Gi(f,c);if(!j.related.length)return"";let q=j.related.map((_)=>l(_.name)).join(" · ");return`

Reviewing ${l(j.description||c.name)}

Outlined · reviewed separately ${q}

${j.excluded.length?`

Hidden in this preview: ${j.excluded.map((_)=>l(_.name)).join(" · ")}

`:""}
`},Qi=(f)=>`left:${gi(f.x)};top:${gi(f.y)};width:${gi(f.w)};height:${gi(f.h)}`,xo=(f,c)=>f.stage==="hero"?"":Gi(f,c).related.map((j)=>{let q=c.box,_=j.box,fi=Math.max(q.x,_.x),X=Math.max(q.y,_.y),Bi={x:(fi-q.x)/q.w,y:(X-q.y)/q.h,w:Math.max(0,Math.min(q.x+q.w,_.x+_.w)-fi)/q.w,h:Math.max(0,Math.min(q.y+q.h,_.y+_.h)-X)/q.h};return`${l(j.name)}`}).join("");function vi(f){e.getElementById(f)?.focus({preventScroll:!0})}function Wo(f){let c=_o(o,n,f);if(k=!c,c)h=c;Y="component",A=!1,li=!1,ni="fit",hi="isolated",ii=!0,v=k?"reviewed":"pending";let j=()=>{w(),vi(k?"review-summary":M[h]?"feedback":"approve")};if(k&&V&&Ti)Ti(j);else j()}async function to(){if(s||z||A||Object.keys(M).length)return;s=!0,Ji="",w();try{await x.onSubmit(Yo(o,n)),z=!0,k=!0,N=!1,v="reviewed"}catch(f){Ji=f instanceof Error?f.message:"Could not save. Try again."}finally{s=!1,w()}}function Ai(f){if(s||z||A||$i)return;let c=o.components.find((X)=>X.id===h);if(!c||!Vi(o,n,c,ii,Object.keys(M)).length)return;let j=n.decisions[c.id],q=j?.revision===c.revision?j:void 0,_=Vi(o,n,c,ii,Object.keys(M));P={id:c.id,name:_.length>1?`${c.reviewGroup} · ${_.length} instances`:c.name,action:f,previous:Object.fromEntries(_.map((X)=>[X.id,n.decisions[X.id]?{...n.decisions[X.id]}:void 0]))};let fi=M[c.id]??q;for(let X of _)n.decisions[X.id]={revision:X.revision,action:f,feedback:f==="revise"?fi?.feedback??"":"",split:f==="revise"&&(fi?.split??!1)};if(delete M[c.id],bi)L=!0,bi=!1;if(p)n.inventoryConfirmed=f==="approve",P=null,to();else Wo(c.id)}function Go(){if(s||z||A||$i)return;let f=o.components.find((q)=>q.id===h);if(!f||!Vi(o,n,f,ii,Object.keys(M)).length)return;let c=n.decisions[f.id],j=c?.revision===f.revision?c:void 0;if(M[f.id]??={feedback:j?.feedback??"",split:j?.split??!1},L&&(e.querySelector(".workbench")?.clientHeight??0)<420)bi=!0,L=!1;k=!1,w(),vi("feedback")}function lo(f){let c=`missing-${crypto.randomUUID()}`;n.missing.push({id:c,name:"Missing component",feedback:"",box:f}),n.inventoryConfirmed=!1,k=!1,h=c,Y="component",N=!1,ai=null,pi=null,w(),e.querySelector("#missing-name")?.focus()}function w(){if(xi?.disconnect(),z||k&&!p)V=!1;let f=e.activeElement,c=f?.id,j=f?.dataset.select,q=window.scrollX,_=window.scrollY,fi=e.querySelector(".inventory")?.scrollLeft??0,X=Ui===h,Bi=Y==="component"&&(!X||no!==Y);no=Y;let To=X?e.querySelector(".inspection-content")?.scrollTop??0:0,Uo=X&&(e.querySelector(".changed-files")?.open??!1),Ao=e.querySelector(".comparison-slot")?.clientHeight??200,go=e.querySelector(".pan-viewport"),ho=Ui===h&&ao===ni,Bo=ho?go?.scrollLeft??0:0,Io=ho?go?.scrollTop??0:0;Ui=h,ao=ni;let m=o.components.find((i)=>i.id===h),J=n.missing.find((i)=>i.id===h),E=Xo(o,n,x.history),W=E.find((i)=>i.members.some((r)=>r.id===h)),D=!!W&&W.members.length>1&&ii,fo=D?W.box:m?.box??J?.box,so=W?E.indexOf(W)+1:E.length+n.missing.findIndex((i)=>i.id===h)+1,wo=m?n.decisions[m.id]:void 0,ti=wo?.revision===m?.revision?wo:void 0,Yi=m?M[m.id]:void 0,si=Object.keys(M).length>0,co=m?Wi(o,m):[],wi=m?Vi(o,n,m,ii,Object.keys(M)):[],Ro=m?!o.components.some((i)=>!wi.some((r)=>r.id===i.id)&&O(i,n).kind==="pending"):!1,Ii=P&&!z?`
${l(P.name)} ${P.action==="approve"?"approved":"flagged for repair"}.
`:"",H=ki(o,n),$=x.history,B=m?Li(m.id,$):void 0,ci=$?.packet.components.find((i)=>i.id===m?.id),oi=A&&!!ci,d=oi?ci:m,Z=oi?$.packet:o,mo=Object.values($?.changes??{}),bo=mo.filter((i)=>i.kind==="changed").length,vo=mo.filter((i)=>i.kind==="added").length,Ri=o.components.filter((i)=>O(i,n,$).kind==="approved"&&Li(i.id,$).carried).length,Oi=(i)=>{let r=O(i,n,$);return z&&r.kind==="feedback"?{...r,label:"Changes requested"}:r},Hi=E.filter((i)=>i.kind==="pending").length,Oo=E.length-Hi+n.missing.length,uo=E.filter((i)=>v==="all"||i.kind==="pending"===(v==="pending")).map((i)=>i.representative),Do=(i)=>E.find((r)=>r.members.some((t)=>t.id===i))?.label??i,So=[H.revisions+n.missing.length?`${H.revisions+n.missing.length} feedback ready`:"",Ri?`${Ri} ${Ri===1?"approval":"approvals"} kept`:"",bo?`${bo} changed`:"",vo?`${vo} added`:"",$?.removed.length?`${$.removed.length} removed`:""].filter(Boolean).join(" · "),Fo=Ji||(si?"Save or cancel your open feedback before sending.":z?x.preview?"Preview submitted. No run changed.":"Review submitted.":H.hasFeedback?"Ready to send for corrections.":H.pending?`${Hi} left to review`:!n.inventoryConfirmed?"Confirm the map is complete.":"Ready to continue."),ri=d?zi(d):null,Di=d?.preview.kind==="image"&&!ri?.code,Si=Di||d?.material?.alpha==="transparent",S=!!(d?.context&&hi==="context"),yo=d&&(S?d.context?.kind!=="image":d.preview.kind==="page"),Fi=S&&d?.context?d.context.url:d?.preview.url,zo=d?Gi(Z,d):null,Co=ri?.code?`${ri.label} · ${ri.captured?"captured from code":"live preview"}`:d?.material?`${d.material.alpha==="transparent"?"Transparent":d.material.alpha==="opaque"?"Opaque":"Transparency unverified"} ${d.material.format}`:"Raster · transparency unverified";e.innerHTML=`
+

${z?"Review record.":p?"Review the assembled page.":"Review the components."}

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

${z?'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.

':""} - ${V&&!a?`

${qi} ${qi===1?"item":"items"} to review${Do}

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

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

`:""}
`:""} - ${!a?`
`:""} -
${!a?` + ${$&&!p?`

${Hi} ${Hi===1?"item":"items"} to review${So}

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

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

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

Approved comp

${!y?``:""}
-
- Approved composition for ${l(o.title)} - ${ho&&!k?`
`:""} - ${Y.map((i,r)=>{let t=i.representative,h={kind:i.kind,label:i.stateLabel},B=!k&&X?.id===i.id;return``}).join("")} - ${R&&!k?X.members.map((i)=>`
`).join(""):""} - ${n.missing.map((i,r)=>``).join("")} +

Approved comp

${!z?``:""}
+
+ Approved composition for ${l(o.title)} + ${fo&&!k?`
`:""} + ${E.map((i,r)=>{let t=i.representative,g={kind:i.kind,label:i.stateLabel},R=!k&&W?.id===i.id;return``}).join("")} + ${D&&!k?W.members.map((i)=>`
`).join(""):""} + ${n.missing.map((i,r)=>``).join("")}
-
# To review${$i} ${y?"Changes requested":"Feedback ready"}${ji} Approved
- ${_?'
Draw around the missing piece.
':""} +
# To review${Ki} ${z?"Changes requested":"Feedback ready"}${qi} Approved
+ ${N?'
Draw around the missing piece.
':""}
`:""} -
- ${!a?`

${k?"Review summary":`${go} ${l(R?X.label:c?.name??M?.name??"Component")}`}

`:""}
- ${k&&!a?`

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

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

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

${Y.map((i)=>{let r=i.representative,t=n.decisions[r.id],h=Ii(r);return``}).join("")}${n.missing.map((i)=>``).join("")}
${Ai?`
${Ai}
`:""}`:c?` - ${V?`
- ${ii&&U?.prior?.action==="revise"?`

Previous feedback · Round ${U.feedbackRound}

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

Requested: split into separately reviewable components.

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

Unchanged · approval kept

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

${k?"Review summary":`${so} ${l(D?W.label:m?.name??J?.name??"Component")}`}

`:""}
+ ${k&&!p?`

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

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

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

${E.map((i)=>{let r=i.representative,t=n.decisions[r.id],g=Oi(r);return``}).join("")}${n.missing.map((i)=>``).join("")}
${Ii?`
${Ii}
`:""}`:m?` + ${$?`
+ ${oi&&B?.prior?.action==="revise"?`

Previous feedback · Round ${B.feedbackRound}

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

Requested: split into separately reviewable components.

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

Unchanged · approval kept

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

${l(R?X.label:d.name)}

${so.length>1?`
${so.length} instances${!R?'':'Select to inspect'}
`:""}
${wi?`
`:""}
${d?.preview.kind==="image"?`${W("external")}`:""}
- ${R?`
Full comp cropComponent preview
${X.members.map((i,r)=>{let t=Ii(i);return``}).join("")}
`:""} - ${!R&&d?po(E,d):""}
-
${ii?`Comp · Round ${V.packet.round}`:a?"Approved comp":uo?.related.length?"Full comp crop":"In the comp"}
Reference region for ${l(d.name)}
-
${ii?`Previous · Round ${V.packet.round}`:a?"Assembled page":O?"In context":V?`${oi.caption} · Round ${o.round}`:oi.caption}
${!vo?`Produced ${l(d.name)}`:``}${ti?`Reference overlay`:""}
+

${l(D?W.label:d.name)}

${co.length>1?`
${co.length} instances${!D?'':'Select to inspect'}
`:""}
${ci?`
`:""}
${d?.preview.kind==="image"?`${T("external")}`:""}
+ ${D?`
Full comp cropComponent preview
${W.members.map((i,r)=>{let t=Oi(i);return``}).join("")}
`:""} + ${!D&&d?po(Z,d):""}
+
${oi?`Comp · Round ${$.packet.round}`:p?"Approved comp":zo?.related.length?"Full comp crop":"In the comp"}
Reference region for ${l(d.name)}${xo(Z,d)}
+
${oi?`Previous · Round ${$.packet.round}`:p?"Assembled page":S?"In context":$?`${ri.caption} · Round ${o.round}`:ri.caption}
${!yo?`Produced ${l(d.name)}`:``}${li?`Reference overlay`:""}
- ${Oi||d.context?`
${d.context?`
`:""}${Oi?`
`:""}
`:""} -
${!a&&!R?`
${W(oi.code?"code":"image")}${l(Fo)}${d?.material?`${d.material.width} × ${d.material.height} px`:""}
${E.stage==="components"&&oi?.captured&&!d?.preview.isolation?'

Legacy region capture · may include overlapping components.

':""}${d?.context?.layering&&(Ri||O)?`

${l(d.context.layering)}

`:""} -

${!uo?.related.length?l(d.note):""}

-
`:""}
${Ai}${ii?'

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

':""}${y?`
${xi?.action==="approve"?"Approved":xi?.action==="revise"?"Changes requested":"Not reviewed"}Submitted in round ${o.round} · read-only
`:`
${!a?`
Your review Round ${o.round}${ii?"

Return to Current to review this round.

":""}
`:""}${xi&&!a&&!R?``:""}
`} - ${Qi?`
`:xi?.action==="revise"?`

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

`:""} - ${a?`

${l(Vi||(y?"Your decision is saved.":s?"Sending…":Qi?"":"Approval confirms the composition and that nothing is missing."))}

`:""}
`:M?`

This piece will be added to the unresolved inventory.

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

No components supplied.

"} + ${Si||d.context?`
${d.context?`
`:""}${Si?`
`:""}
`:""} +
${!p&&!D?`
${T(ri.code?"code":"image")}${l(Co)}${d?.material?`${d.material.width} × ${d.material.height} px`:""}
${Z.stage==="components"&&ri?.captured&&!d?.preview.isolation?'

Legacy region capture · may include overlapping components.

':""}${d?.context?.layering&&(Di||S)?`

${l(d.context.layering)}

`:""} +

${!zo?.related.length?l(d.note):""}

+
`:""}
${Ii}${oi?'

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

':""}${z?`
${ti?.action==="approve"?"Approved":ti?.action==="revise"?"Changes requested":"Not reviewed"}Submitted in round ${o.round} · read-only
`:`
${!p?`
Your review Round ${o.round}${oi?"

Return to Current to review this round.

":""}
`:""}${ti&&!p&&!D?``:""}
`} + ${Yi?`
`:ti?.action==="revise"?`

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

`:""} + ${p?`

${l(Ji||(z?"Your decision is saved.":s?"Sending…":Yi?"":"Approval confirms the composition and that nothing is missing."))}

`:""}
`:J?`

This piece will be added to the unresolved inventory.

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

No components supplied.

"}
- ${!a?`

Components

-
${bo.map((i)=>{let r=Y.find((B)=>B.members.some((G)=>G.id===i.id)),t=Y.indexOf(r),h={kind:r.kind,label:r.stateLabel};return``}).join("")}${(z==="pending"?[]:n.missing).map((i,r)=>``).join("")}${!bo.length&&(z==="pending"||!n.missing.length)?`

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

`:""}
`:""} - ${a?"":y?`
Round ${o.round} submitted · read-only${j.approved} approved · ${j.revisions} changes requested
`:`
${!j.pending&&!fi?``:""}

${l(So)}

`} -
`;let N=e.querySelector("#comparison-dialog"),A=e.querySelector(".comparison-panel"),ci=e.querySelector(".comparison-slot"),ri=e.querySelector(".inspector > .review-form"),Co=e.querySelector(".inspector"),yo=()=>{if(N.append(A),ri)N.append(ri)};if(H&&A&&ci)ci.style.height=`${Uo}px`,yo(),N.showModal();if(ii)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=Go,y||s)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);let zo=e.querySelector(".inventory");if(zo)zo.scrollLeft=Ki;if(!Z&&L)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(Q)Array.from(e.querySelectorAll(".item[data-select]")).find((i)=>i.dataset.select===Q)?.focus({preventScroll:!0});let u=(i,r)=>e.querySelector(`#${i}`)?.addEventListener("click",r);function Si(i,r=!1,t=!1){if(_)return;if(!t)i=Y.find((h)=>h.members.some((B)=>B.id===i))?.representative.id??i;if(C=!t,k=!1,g=i,K="component",ti=!1,ei="fit",hi="isolated",T=!1,w(),r)e.querySelector("#expand-comparison")?.click()}e.querySelectorAll("[data-select]").forEach((i)=>i.onclick=()=>Si(i.dataset.select,i.classList.contains("pin")&&o.components.some((r)=>r.id===i.dataset.select))),u("previous-round",()=>{T=!0,w()}),u("current-round",()=>{T=!1,w()}),u("review-changes",()=>{let i=b().filter((h)=>Ii(h).kind==="pending"),r=i.findIndex((h)=>h.id===g),t=i[(r+1)%i.length];if(t)C=!0,k=!1,g=t.id,K="component",z="pending",T=!1,ei="fit",ti=!1,hi="isolated",w()}),e.querySelectorAll("[data-filter]").forEach((i)=>i.onclick=()=>{z=i.dataset.filter,k=!j.pending&&z==="pending";let r=Y.filter((h)=>z==="all"||h.kind==="pending"===(z==="pending")).map((h)=>h.representative);if(!(z!=="pending"&&n.missing.some((h)=>h.id===g))&&!r.some((h)=>h.id===g)&&r.length)C=!0,g=r[0].id,K="component",T=!1,ei="fit",ti=!1,hi="isolated";w()}),u("show-summary",()=>{k=!0,K="component",z="reviewed",w(),bi("review-summary")}),u("approve",()=>Ui("approve")),u("revise",Wo),u("clear",()=>{if(c)delete n.decisions[c.id],delete q[c.id];F=null,k=!1,z="pending",w()}),u("cancel-feedback",()=>{if(c)delete q[c.id];if(mi)L=!0,mi=!1;w(),bi("revise")}),e.querySelector("#feedback-form")?.addEventListener("submit",(i)=>{i.preventDefault(),Ui("revise")}),e.querySelector("#feedback")?.addEventListener("keydown",(i)=>{let r=i;if(r.key==="Enter"&&(r.metaKey||r.ctrlKey)&&!r.isComposing)r.preventDefault(),r.stopPropagation(),Ui("revise")}),u("undo-decision",()=>{if(!F||s||y)return;let i=F;for(let[r,t]of Object.entries(i.previous))if(t)n.decisions[r]=t;else delete n.decisions[r];delete q[i.id],g=i.id,k=!1,T=!1,K="component",z="all",F=null,w(),bi("approve")}),u("all-instances",()=>{C=!0,g=X.representative.id,T=!1,w()}),e.querySelectorAll("[data-instance]").forEach((i)=>i.onclick=()=>Si(i.dataset.instance,!1,!0)),u("overlay",()=>{ti=!ti,w()}),u("isolated",()=>{hi="isolated",w()}),u("context",()=>{hi="context",w()}),e.querySelector("#zoom")?.addEventListener("change",(i)=>{let r=i.target.value;ei=r==="fit"?"fit":Number(r),w()}),u("background-checker",()=>{ki="checker",w()}),u("background-page",()=>{ki="page",w()}),u("show-all",()=>{li=!li,L=!0,w()}),u("toggle-tray",()=>{mi=!1,L=!L,w()}),u("show-comp",()=>{K="comp",w()}),u("show-component",()=>{K="component",w()}),u("mark",()=>{_=!_,w()}),u("add-box",()=>xo({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=o.components[0]?.id,w()}),u("approve-rest",()=>{if(fi)return;n=$o(o,n),F=null,k=!0,z="reviewed",K="component",w(),bi("review-summary")}),e.querySelector("#inventory-confirm")?.addEventListener("change",(i)=>{n.inventoryConfirmed=i.target.checked,w()}),e.querySelector("#feedback")?.addEventListener("input",(i)=>{if(c&&q[c.id])q[c.id].feedback=i.target.value}),e.querySelector("#split")?.addEventListener("change",(i)=>{if(c&&q[c.id])q[c.id].split=i.target.checked}),e.querySelector("#missing-name")?.addEventListener("input",(i)=>{if(M)M.name=i.target.value;let r=e.querySelector("#submit");if(r)r.disabled=fi||s||y||!zi(o,n).canSubmit}),e.querySelector("#missing-feedback")?.addEventListener("input",(i)=>{if(M)M.feedback=i.target.value}),e.querySelectorAll("[data-coordinate]").forEach((i)=>i.addEventListener("change",()=>{if(!M)return;let r=i.dataset.coordinate,t=Number(i.value)/100;if(Number.isFinite(t))M.box[r]=Math.max(r==="w"||r==="h"?0.001:0,Math.min(1,t));M.box.w=Math.min(M.box.w,1-M.box.x),M.box.h=Math.min(M.box.h,1-M.box.y),w()})),u("submit",()=>{ao()});let D=e.querySelector(".map");function Fi(i){let r=D.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))}}function ko(i){let r=Fi(i);return o.components.filter(({box:t})=>r.x>=t.x&&r.x<=t.x+t.w&&r.y>=t.y&&r.y<=t.y+t.h).sort((t,h)=>t.box.w*t.box.h-h.box.w*h.box.h)[0]}D?.addEventListener("click",(i)=>{if(_||i.target.closest("[data-select]"))return;let r=ko(i);if(r)Si(r.id,!0)}),D?.addEventListener("pointermove",(i)=>{if(!_)D.style.cursor=ko(i)?"zoom-in":""}),D?.addEventListener("pointerdown",(i)=>{if(!_)return;ni=Fi(i),D.setPointerCapture(i.pointerId),i.preventDefault()}),D?.addEventListener("pointermove",(i)=>{if(!ni)return;let r=Fi(i);pi={x:Math.min(ni.x,r.x),y:Math.min(ni.y,r.y),w:Math.abs(r.x-ni.x),h:Math.abs(r.y-ni.y)};let t=e.querySelector(".draw-box");t.hidden=!1,t.style.cssText=Ti(pi)}),D?.addEventListener("pointerup",()=>{if(pi&&pi.w>0.01&&pi.h>0.01)xo(pi);else ni=null,pi=null}),D?.addEventListener("pointercancel",()=>{ni=null,pi=null,w()});let jo=e.querySelector(".output"),Yi=e.querySelector("iframe"),qo=e.querySelector(".workbench"),vi=e.querySelector(".inspection-content"),Ci=e.querySelector(".inspector");function Pi(){if(!vi||!Ci)return;Ci.dataset.scrollAbove=String(vi.scrollTop>1),Ci.dataset.scrollBelow=String(vi.scrollHeight-vi.clientHeight-vi.scrollTop>1)}vi?.addEventListener("scroll",Pi,{passive:!0});function _i(){let i=e.querySelector(".map-space");if(i&&i.clientWidth&&i.clientHeight){let v=oo(o.comp.width,o.comp.height,Math.max(1,i.clientWidth-32),Math.max(1,i.clientHeight-32),"fit");D.style.width=`${v.width}px`,D.style.height=`${v.height}px`}let r=e.querySelector(".inspection-content"),t=Array.from(e.querySelectorAll(".pan-viewport"));if(r?.clientHeight){let v=H?Math.max(100,N.clientHeight-(A?.querySelector(".compare-toolbar")?.clientHeight??0)-(A?.querySelector(".expanded-title")?.clientHeight??0)-(A?.querySelector(".view-controls")?.clientHeight??0)-(ri?.getBoundingClientRect().height??0)-124):Math.min(a?Number.POSITIVE_INFINITY:248,Math.max(100,r.clientHeight-((t[0]?.getBoundingClientRect().top??r.getBoundingClientRect().top)-r.getBoundingClientRect().top+r.scrollTop)-(A?.querySelector(".view-controls")?.clientHeight??0)-12));t.forEach((J)=>J.style.height=`${v}px`)}if(d&&t.length&&!R){let v=oo(d.box.w*E.comp.width,d.box.h*E.comp.height,Math.min(...t.map((J)=>J.clientWidth)),Math.min(...t.map((J)=>J.clientHeight)),ei);e.querySelectorAll(".crop-stage").forEach((J)=>{J.style.width=`${v.width}px`,J.style.height=`${v.height}px`})}if(t.forEach((v)=>{let J=v.scrollWidth>v.clientWidth+1||v.scrollHeight>v.clientHeight+1;v.classList.toggle("pannable",J),v.style.cursor=H?"":"zoom-in",v.setAttribute("role",H?"region":"button"),v.title=H?J?"Move your pointer to pan. You can also scroll, swipe, or use arrow keys.":"":"Click to enlarge comparison"}),jo&&Yi&&d){let v=jo.clientWidth/(d.box.w*E.comp.width);Yi.style.transform=`scale(${v})`,Yi.style.left=`${-d.box.x*E.comp.width*v}px`,Yi.style.top=`${-d.box.y*E.comp.height*v}px`}let h=qo.getBoundingClientRect(),B=e.querySelector(".region"),G=e.querySelector(".number"),Xi=e.querySelector(".connector path");if(B&&G&&Xi){let v=B.getBoundingClientRect(),J=G.getBoundingClientRect(),Ei=v.right-h.left,$=v.top+v.height/2-h.top,Hi=J.left-h.left-8,Po=J.top+J.height/2-h.top;Xi.setAttribute("d",`M ${Ei} ${$} H ${Hi-14} V ${Po} H ${Hi}`)}}function ui(i,r){if(!A||!ci||i===H)return;let t=e.querySelector("#expand-comparison"),h=(H?N:A).getBoundingClientRect(),B=window.matchMedia("(prefers-reduced-motion: reduce)").matches,G=A.querySelector(".pan-viewport"),Xi=G?G.scrollLeft/Math.max(1,G.scrollWidth-G.clientWidth):0,v=G?G.scrollTop/Math.max(1,G.scrollHeight-G.clientHeight):0,J=()=>A.querySelectorAll(".pan-viewport").forEach(($)=>{$.scrollLeft=Xi*Math.max(0,$.scrollWidth-$.clientWidth),$.scrollTop=v*Math.max(0,$.scrollHeight-$.clientHeight)}),Ei=()=>{if(ci.append(A),ri)Co.append(ri),ri.inert=!1;N.close(),ci.style.height="",Ji=!1,H=!1,t.innerHTML=W("expand"),t.setAttribute("aria-label","Enlarge comparison"),t.title="Enlarge comparison",t.setAttribute("aria-expanded","false"),_i(),J(),t.focus({preventScroll:!0}),r?.()};if(i){ci.style.height=`${h.height}px`,yo(),N.showModal(),H=!0,t.innerHTML=W("compact"),t.setAttribute("aria-label","Restore comparison"),t.title="Restore comparison (Esc)",t.setAttribute("aria-expanded","true"),_i(),J(),t.focus({preventScroll:!0});let $=N.getBoundingClientRect();if(!B)N.animate([{transform:`translate(${h.x-$.x}px,${h.y-$.y}px) scale(${h.width/$.width},${h.height/$.height})`,opacity:0.6},{transform:"none",opacity:1}],{duration:240,easing:"cubic-bezier(.2,.8,.2,1)"})}else{if(t.disabled)return;let $=ci.getBoundingClientRect();if(B){Ei();return}if(t.disabled=!0,Ji=!0,ri)ri.inert=!0;let Hi=N.animate([{transform:"none",opacity:1},{transform:`translate(${$.x-h.x}px,${$.y-h.y}px) scale(${$.width/h.width},${$.height/h.height})`,opacity:0.6}],{duration:200,easing:"cubic-bezier(.4,0,.2,1)",fill:"forwards"});Hi.finished.then(()=>{if(N.isConnected)Hi.cancel(),t.disabled=!1,Ei()}).catch(()=>{})}}if(Zi=(i)=>ui(!1,i),u("expand-comparison",()=>ui(!H)),N.addEventListener("cancel",(i)=>{i.preventDefault(),i.stopPropagation(),ui(!1)}),N.addEventListener("keydown",(i)=>{if(i.key==="Escape")i.preventDefault(),i.stopPropagation(),ui(!1)}),ai=new ResizeObserver(()=>{_i(),Pi()}),ai.observe(qo),ai.observe(N),ri)ai.observe(ri);let Ho=e.querySelector(".inspection-content");if(Ho)ai.observe(Ho);let Mo=e.querySelector(".repair-context");if(Mo)ai.observe(Mo);let Vo=A?.querySelector(".compare-toolbar");if(Vo)ai.observe(Vo);_i();let Li=Array.from(e.querySelectorAll(".pan-viewport"));if(Li.forEach((i)=>{i.scrollLeft=Ao,i.scrollTop=Bo}),Li.forEach((i)=>{i.querySelectorAll("img").forEach((r)=>r.draggable=!1),i.addEventListener("click",()=>{if(!H)ui(!0)}),i.addEventListener("pointermove",(r)=>{if(r.pointerType!=="mouse"||r.buttons||!i.classList.contains("pannable"))return;let t=i.getBoundingClientRect();i.scrollLeft=ro(r.clientX,t.left,i.clientWidth,i.scrollWidth),i.scrollTop=ro(r.clientY,t.top,i.clientHeight,i.scrollHeight)}),i.addEventListener("keydown",(r)=>{if(!H&&(r.key==="Enter"||r.key===" ")){r.preventDefault(),r.stopPropagation(),ui(!0);return}let h={ArrowLeft:[-48,0],ArrowRight:[48,0],ArrowUp:[0,-48],ArrowDown:[0,48]}[r.key];if(!h)return;r.preventDefault(),r.stopPropagation(),i.scrollLeft+=h[0],i.scrollTop+=h[1]})}),Li.forEach((i)=>i.addEventListener("scroll",()=>{for(let r of Li)if(r!==i){if(r.scrollLeft!==i.scrollLeft)r.scrollLeft=i.scrollLeft;if(r.scrollTop!==i.scrollTop)r.scrollTop=i.scrollTop}})),Zo&&window.matchMedia("(max-width:800px)").matches){let i=e.querySelector(".inspection-content"),r=e.querySelector(".compare");if(i&&r)i.scrollTop+=r.getBoundingClientRect().top-i.getBoundingClientRect().top}if(Pi(),window.scrollTo(gi,P),!y)x.onDraftChange?.(structuredClone(n))}return w(),{destroy(){ai?.disconnect(),e.replaceChildren()},getDraft(){return structuredClone(n)}}}async function rr(){let p=document.getElementById("review"),o=await fetch("/packet",{cache:"no-store"});if(!o.ok)throw Error("The review packet could not be loaded. Reload to retry.");let x=await o.json();if(x.sourceStatus){let e=document.createElement("p");e.textContent=x.sourceStatus,p.before(e)}Xo(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)}),a=await n.json();if(!n.ok)throw Error(a.error??"The review could not be saved. Try again.")}})}rr().catch((p)=>{let o=document.createElement("p");o.textContent=p instanceof Error?p.message:String(p),document.getElementById("review")?.replaceChildren(o)});})(); + ${!p?`

Components

+
${uo.map((i)=>{let r=E.find((R)=>R.members.some((U)=>U.id===i.id)),t=E.indexOf(r),g={kind:r.kind,label:r.stateLabel};return``}).join("")}${(v==="pending"?[]:n.missing).map((i,r)=>``).join("")}${!uo.length&&(v==="pending"||!n.missing.length)?`

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

`:""}
`:""} + ${p?"":z?`
Round ${o.round} submitted · read-only${H.approved} approved · ${H.revisions} changes requested
`:`
${!H.pending&&!si?``:""}

${l(Fo)}

`} + `;let G=e.querySelector("#comparison-dialog"),I=e.querySelector(".comparison-panel"),mi=e.querySelector(".comparison-slot"),ei=e.querySelector(".inspector > .review-form"),Po=e.querySelector(".inspector"),ko=()=>{if(G.append(I),ei)G.append(ei)};if(V&&I&&mi)mi.style.height=`${Ao}px`,ko(),G.showModal();if(oi)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=To,z||s)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);let jo=e.querySelector(".inventory");if(jo)jo.scrollLeft=fi;if(!X&&L)Array.from(e.querySelectorAll(".inventory [data-select]")).find((i)=>i.dataset.select===h)?.scrollIntoView({block:"nearest",inline:"nearest"});if(c)e.getElementById(c)?.focus({preventScroll:!0});else if(j)Array.from(e.querySelectorAll(".item[data-select]")).find((i)=>i.dataset.select===j)?.focus({preventScroll:!0});let u=(i,r)=>e.querySelector(`#${i}`)?.addEventListener("click",r);function Ci(i,r=!1,t=!1){if(N)return;if(!t)i=E.find((g)=>g.members.some((R)=>R.id===i))?.representative.id??i;if(ii=!t,k=!1,h=i,Y="component",li=!1,ni="fit",hi="isolated",A=!1,w(),r)e.querySelector("#expand-comparison")?.click()}e.querySelectorAll("[data-select]").forEach((i)=>i.onclick=()=>Ci(i.dataset.select,i.classList.contains("pin")&&o.components.some((r)=>r.id===i.dataset.select))),u("previous-round",()=>{A=!0,w()}),u("current-round",()=>{A=!1,w()}),u("review-changes",()=>{let i=y().filter((g)=>Oi(g).kind==="pending"),r=i.findIndex((g)=>g.id===h),t=i[(r+1)%i.length];if(t)ii=!0,k=!1,h=t.id,Y="component",v="pending",A=!1,ni="fit",li=!1,hi="isolated",w()}),e.querySelectorAll("[data-filter]").forEach((i)=>i.onclick=()=>{v=i.dataset.filter,k=!H.pending&&v==="pending";let r=E.filter((g)=>v==="all"||g.kind==="pending"===(v==="pending")).map((g)=>g.representative);if(!(v!=="pending"&&n.missing.some((g)=>g.id===h))&&!r.some((g)=>g.id===h)&&r.length)ii=!0,h=r[0].id,Y="component",A=!1,ni="fit",li=!1,hi="isolated";w()}),u("show-summary",()=>{k=!0,Y="component",v="reviewed",w(),vi("review-summary")}),u("approve",()=>Ai("approve")),u("revise",Go),u("clear",()=>{if(m)delete n.decisions[m.id],delete M[m.id];P=null,k=!1,v="pending",w()}),u("cancel-feedback",()=>{if(m)delete M[m.id];if(bi)L=!0,bi=!1;w(),vi("revise")}),e.querySelector("#feedback-form")?.addEventListener("submit",(i)=>{i.preventDefault(),Ai("revise")}),e.querySelector("#feedback")?.addEventListener("keydown",(i)=>{let r=i;if(r.key==="Enter"&&(r.metaKey||r.ctrlKey)&&!r.isComposing)r.preventDefault(),r.stopPropagation(),Ai("revise")}),u("undo-decision",()=>{if(!P||s||z)return;let i=P;for(let[r,t]of Object.entries(i.previous))if(t)n.decisions[r]=t;else delete n.decisions[r];delete M[i.id],h=i.id,k=!1,A=!1,Y="component",v="all",P=null,w(),vi("approve")}),u("all-instances",()=>{ii=!0,h=W.representative.id,A=!1,w()}),e.querySelectorAll("[data-instance]").forEach((i)=>i.onclick=()=>Ci(i.dataset.instance,!1,!0)),u("overlay",()=>{li=!li,w()}),u("isolated",()=>{hi="isolated",w()}),u("context",()=>{hi="context",w()}),e.querySelector("#zoom")?.addEventListener("change",(i)=>{let r=i.target.value;ni=r==="fit"?"fit":Number(r),w()}),u("background-checker",()=>{ji="checker",w()}),u("background-page",()=>{ji="page",w()}),u("show-all",()=>{di=!di,L=!0,w()}),u("toggle-tray",()=>{bi=!1,L=!L,w()}),u("show-comp",()=>{Y="comp",w()}),u("show-component",()=>{Y="component",w()}),u("mark",()=>{N=!N,w()}),u("add-box",()=>lo({x:0.35,y:0.35,w:0.2,h:0.2})),u("remove-missing",()=>{n.missing=n.missing.filter((i)=>i.id!==h),h=o.components[0]?.id,w()}),u("approve-rest",()=>{if(si)return;n=Qo(o,n),P=null,k=!0,v="reviewed",Y="component",w(),vi("review-summary")}),e.querySelector("#inventory-confirm")?.addEventListener("change",(i)=>{n.inventoryConfirmed=i.target.checked,w()}),e.querySelector("#feedback")?.addEventListener("input",(i)=>{if(m&&M[m.id])M[m.id].feedback=i.target.value}),e.querySelector("#split")?.addEventListener("change",(i)=>{if(m&&M[m.id])M[m.id].split=i.target.checked}),e.querySelector("#missing-name")?.addEventListener("input",(i)=>{if(J)J.name=i.target.value;let r=e.querySelector("#submit");if(r)r.disabled=si||s||z||!ki(o,n).canSubmit}),e.querySelector("#missing-feedback")?.addEventListener("input",(i)=>{if(J)J.feedback=i.target.value}),e.querySelectorAll("[data-coordinate]").forEach((i)=>i.addEventListener("change",()=>{if(!J)return;let r=i.dataset.coordinate,t=Number(i.value)/100;if(Number.isFinite(t))J.box[r]=Math.max(r==="w"||r==="h"?0.001:0,Math.min(1,t));J.box.w=Math.min(J.box.w,1-J.box.x),J.box.h=Math.min(J.box.h,1-J.box.y),w()})),u("submit",()=>{to()});let F=e.querySelector(".map");function Pi(i){let r=F.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))}}function qo(i){let r=Pi(i);return o.components.filter(({box:t})=>r.x>=t.x&&r.x<=t.x+t.w&&r.y>=t.y&&r.y<=t.y+t.h).sort((t,g)=>t.box.w*t.box.h-g.box.w*g.box.h)[0]}F?.addEventListener("click",(i)=>{if(N||i.target.closest("[data-select]"))return;let r=qo(i);if(r)Ci(r.id,!0)}),F?.addEventListener("pointermove",(i)=>{if(!N)F.style.cursor=qo(i)?"zoom-in":""}),F?.addEventListener("pointerdown",(i)=>{if(!N)return;ai=Pi(i),F.setPointerCapture(i.pointerId),i.preventDefault()}),F?.addEventListener("pointermove",(i)=>{if(!ai)return;let r=Pi(i);pi={x:Math.min(ai.x,r.x),y:Math.min(ai.y,r.y),w:Math.abs(r.x-ai.x),h:Math.abs(r.y-ai.y)};let t=e.querySelector(".draw-box");t.hidden=!1,t.style.cssText=Qi(pi)}),F?.addEventListener("pointerup",()=>{if(pi&&pi.w>0.01&&pi.h>0.01)lo(pi);else ai=null,pi=null}),F?.addEventListener("pointercancel",()=>{ai=null,pi=null,w()});let Ho=e.querySelector(".output"),_i=e.querySelector("iframe"),Mo=e.querySelector(".workbench"),ui=e.querySelector(".inspection-content"),io=e.querySelector(".inspector");function oo(){if(!ui||!io)return;io.dataset.scrollAbove=String(ui.scrollTop>1),io.dataset.scrollBelow=String(ui.scrollHeight-ui.clientHeight-ui.scrollTop>1)}ui?.addEventListener("scroll",oo,{passive:!0});function Xi(){let i=e.querySelector(".map-space");if(i&&i.clientWidth&&i.clientHeight){let b=ro(o.comp.width,o.comp.height,Math.max(1,i.clientWidth-32),Math.max(1,i.clientHeight-32),"fit");F.style.width=`${b.width}px`,F.style.height=`${b.height}px`}let r=e.querySelector(".inspection-content"),t=Array.from(e.querySelectorAll(".pan-viewport"));if(r?.clientHeight){let b=V?Math.max(100,G.clientHeight-(I?.querySelector(".compare-toolbar")?.clientHeight??0)-(I?.querySelector(".expanded-title")?.clientHeight??0)-(I?.querySelector(".view-controls")?.clientHeight??0)-(ei?.getBoundingClientRect().height??0)-124):Math.min(p?Number.POSITIVE_INFINITY:248,Math.max(100,r.clientHeight-((t[0]?.getBoundingClientRect().top??r.getBoundingClientRect().top)-r.getBoundingClientRect().top+r.scrollTop)-(I?.querySelector(".view-controls")?.clientHeight??0)-12));t.forEach((K)=>K.style.height=`${b}px`)}if(d&&t.length&&!D){let b=ro(d.box.w*Z.comp.width,d.box.h*Z.comp.height,Math.min(...t.map((K)=>K.clientWidth)),Math.min(...t.map((K)=>K.clientHeight)),ni);e.querySelectorAll(".crop-stage").forEach((K)=>{K.style.width=`${b.width}px`,K.style.height=`${b.height}px`})}if(t.forEach((b)=>{let K=b.scrollWidth>b.clientWidth+1||b.scrollHeight>b.clientHeight+1;b.classList.toggle("pannable",K),b.style.cursor=V?"":"zoom-in",b.setAttribute("role",V?"region":"button"),b.title=V?K?"Move your pointer to pan. You can also scroll, swipe, or use arrow keys.":"":"Click to enlarge comparison"}),Ho&&_i&&d){let b=Ho.clientWidth/(d.box.w*Z.comp.width);_i.style.transform=`scale(${b})`,_i.style.left=`${-d.box.x*Z.comp.width*b}px`,_i.style.top=`${-d.box.y*Z.comp.height*b}px`}let g=Mo.getBoundingClientRect(),R=e.querySelector(".region"),U=e.querySelector(".number"),Ni=e.querySelector(".connector path");if(R&&U&&Ni){let b=R.getBoundingClientRect(),K=U.getBoundingClientRect(),Zi=b.right-g.left,Q=b.top+b.height/2-g.top,Mi=K.left-g.left-8,ir=K.top+K.height/2-g.top;Ni.setAttribute("d",`M ${Zi} ${Q} H ${Mi-14} V ${ir} H ${Mi}`)}}function yi(i,r){if(!I||!mi||i===V)return;let t=e.querySelector("#expand-comparison"),g=(V?G:I).getBoundingClientRect(),R=window.matchMedia("(prefers-reduced-motion: reduce)").matches,U=I.querySelector(".pan-viewport"),Ni=U?U.scrollLeft/Math.max(1,U.scrollWidth-U.clientWidth):0,b=U?U.scrollTop/Math.max(1,U.scrollHeight-U.clientHeight):0,K=()=>I.querySelectorAll(".pan-viewport").forEach((Q)=>{Q.scrollLeft=Ni*Math.max(0,Q.scrollWidth-Q.clientWidth),Q.scrollTop=b*Math.max(0,Q.scrollHeight-Q.clientHeight)}),Zi=()=>{if(mi.append(I),ei)Po.append(ei),ei.inert=!1;G.close(),mi.style.height="",$i=!1,V=!1,t.innerHTML=T("expand"),t.setAttribute("aria-label","Enlarge comparison"),t.title="Enlarge comparison",t.setAttribute("aria-expanded","false"),Xi(),K(),t.focus({preventScroll:!0}),r?.()};if(i){mi.style.height=`${g.height}px`,ko(),G.showModal(),V=!0,t.innerHTML=T("compact"),t.setAttribute("aria-label","Restore comparison"),t.title="Restore comparison (Esc)",t.setAttribute("aria-expanded","true"),Xi(),K(),t.focus({preventScroll:!0});let Q=G.getBoundingClientRect();if(!R)G.animate([{transform:`translate(${g.x-Q.x}px,${g.y-Q.y}px) scale(${g.width/Q.width},${g.height/Q.height})`,opacity:0.6},{transform:"none",opacity:1}],{duration:240,easing:"cubic-bezier(.2,.8,.2,1)"})}else{if(t.disabled)return;let Q=mi.getBoundingClientRect();if(R){Zi();return}if(t.disabled=!0,$i=!0,ei)ei.inert=!0;let Mi=G.animate([{transform:"none",opacity:1},{transform:`translate(${Q.x-g.x}px,${Q.y-g.y}px) scale(${Q.width/g.width},${Q.height/g.height})`,opacity:0.6}],{duration:200,easing:"cubic-bezier(.4,0,.2,1)",fill:"forwards"});Mi.finished.then(()=>{if(G.isConnected)Mi.cancel(),t.disabled=!1,Zi()}).catch(()=>{})}}if(Ti=(i)=>yi(!1,i),u("expand-comparison",()=>yi(!V)),G.addEventListener("cancel",(i)=>{i.preventDefault(),i.stopPropagation(),yi(!1)}),G.addEventListener("keydown",(i)=>{if(i.key==="Escape")i.preventDefault(),i.stopPropagation(),yi(!1)}),xi=new ResizeObserver(()=>{Xi(),oo()}),xi.observe(Mo),xi.observe(G),ei)xi.observe(ei);let Vo=e.querySelector(".inspection-content");if(Vo)xi.observe(Vo);let Jo=e.querySelector(".repair-context");if(Jo)xi.observe(Jo);let $o=I?.querySelector(".compare-toolbar");if($o)xi.observe($o);Xi();let Ei=Array.from(e.querySelectorAll(".pan-viewport"));if(Ei.forEach((i)=>{i.scrollLeft=Bo,i.scrollTop=Io}),Ei.forEach((i)=>{i.querySelectorAll("img").forEach((r)=>r.draggable=!1),i.addEventListener("click",()=>{if(!V)yi(!0)}),i.addEventListener("pointermove",(r)=>{if(r.pointerType!=="mouse"||r.buttons||!i.classList.contains("pannable"))return;let t=i.getBoundingClientRect();i.scrollLeft=eo(r.clientX,t.left,i.clientWidth,i.scrollWidth),i.scrollTop=eo(r.clientY,t.top,i.clientHeight,i.scrollHeight)}),i.addEventListener("keydown",(r)=>{if(!V&&(r.key==="Enter"||r.key===" ")){r.preventDefault(),r.stopPropagation(),yi(!0);return}let g={ArrowLeft:[-48,0],ArrowRight:[48,0],ArrowUp:[0,-48],ArrowDown:[0,48]}[r.key];if(!g)return;r.preventDefault(),r.stopPropagation(),i.scrollLeft+=g[0],i.scrollTop+=g[1]})}),Ei.forEach((i)=>i.addEventListener("scroll",()=>{for(let r of Ei)if(r!==i){if(r.scrollLeft!==i.scrollLeft)r.scrollLeft=i.scrollLeft;if(r.scrollTop!==i.scrollTop)r.scrollTop=i.scrollTop}})),Bi&&window.matchMedia("(max-width:800px)").matches){let i=e.querySelector(".inspection-content"),r=e.querySelector(".compare");if(i&&r)i.scrollTop+=r.getBoundingClientRect().top-i.getBoundingClientRect().top}if(oo(),window.scrollTo(q,_),!z)x.onDraftChange?.(structuredClone(n))}return w(),{destroy(){xi?.disconnect(),e.replaceChildren()},getDraft(){return structuredClone(n)}}}async function er(){let a=document.getElementById("review"),o=await fetch("/packet",{cache:"no-store"});if(!o.ok)throw Error("The review packet could not be loaded. Reload to retry.");let x=await o.json();if(x.sourceStatus){let e=document.createElement("p");e.textContent=x.sourceStatus,a.before(e)}Zo(a,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)}),p=await n.json();if(!n.ok)throw Error(p.error??"The review could not be saved. Try again.")}})}er().catch((a)=>{let o=document.createElement("p");o.textContent=a instanceof Error?a.message:String(a),document.getElementById("review")?.replaceChildren(o)});})(); diff --git a/skill/reference/component-review.md b/skill/reference/component-review.md index 320ef28b1..b5fef0c53 100644 --- a/skill/reference/component-review.md +++ b/skill/reference/component-review.md @@ -50,6 +50,8 @@ The runtime also captures an unmodified **In context** view from that same docum Component capture supports stable HTML/CSS and inline SVG. Supply a static review state for motion and keep the implementation's real inputs. A scripted, canvas or otherwise unsupported component is a blocker to report, not permission to substitute a raster or omit it. +Keep the implementation's intended fonts in the review document. Vendor external fonts locally and declare them as dependencies; removing their imports changes the component being reviewed. Capture rejects unavailable primary font families rather than presenting a silent fallback. + ## Present and wait If the harness exposes `component_review`, call it with `manifest_path` set to `.impeccable/review/components.json`. The host captures the component files, presents this same review interface and returns the user's decisions. A suspended request is waiting for the user; it is not a failed build or an approval. diff --git a/ui/component-review/model.test.ts b/ui/component-review/model.test.ts index b224317ad..6315bd8c3 100644 --- a/ui/component-review/model.test.ts +++ b/ui/component-review/model.test.ts @@ -126,3 +126,13 @@ test('scope distinguishes other mapped pieces from explicit capture exclusions', expect(reviewScope(p,child).related).toEqual([]); expect(reviewScope(p,card).description).toBe('Card outline'); }); + +test('reference scope includes crossing foreground pieces but ignores adjacent pixel seams',()=>{ + const p=structuredClone(packet),photo=p.components[0]; + photo.box={x:0,y:0,w:.4,h:1}; + const other=p.components[1];other.box={x:.2,y:.2,w:.3,h:.1}; + p.components.push({...other,id:'neighbor',box:{x:.4,y:0,w:.4,h:1}}); + p.components.push({...other,id:'seam',box:{x:.39999,y:.3,w:.2,h:.1}}); + expect(reviewScope(p,photo).related.map(c=>c.id)).toEqual(['control']); + expect(reviewScope(p,photo).excluded).toEqual([]); +}); diff --git a/ui/component-review/model.ts b/ui/component-review/model.ts index 303d816f5..4b942104f 100644 --- a/ui/component-review/model.ts +++ b/ui/component-review/model.ts @@ -143,11 +143,12 @@ export function reviewScope(packet: ReviewPacket, component: Component) { if (other.id === component.id) return false; if (excluded.has(other.id)) return true; const o = other.box; - const overlap = Math.max(0, Math.min(b.x+b.w,o.x+o.w)-Math.max(b.x,o.x)) * - Math.max(0, Math.min(b.y+b.h,o.y+o.h)-Math.max(b.y,o.y)); - // Strictly smaller, substantially contained regions; omit backgrounds and - // neighboring cards. This never changes the review or any reference pixels. - return o.w*o.h < b.w*b.h && overlap / (o.w*o.h) >= .98; + const overlapWidth = Math.min(b.x+b.w,o.x+o.w)-Math.max(b.x,o.x); + const overlapHeight = Math.min(b.y+b.h,o.y+o.h)-Math.max(b.y,o.y); + // Smaller mapped pieces may cross a photograph's edge (headings and route + // lines often do). Include their visible intersection, ignoring subpixel + // boundary noise. This describes scope only; no pixels or decisions change. + return o.w*o.h < b.w*b.h && overlapWidth*packet.comp.width > 1 && overlapHeight*packet.comp.height > 1; }); return {description:component.note.trim(), related, excluded:related.filter(c=>excluded.has(c.id))}; } diff --git a/ui/component-review/review.ts b/ui/component-review/review.ts index 61ccf4a73..8b02d3f4f 100644 --- a/ui/component-review/review.ts +++ b/ui/component-review/review.ts @@ -55,9 +55,15 @@ export function mountComponentReview(host: HTMLElement, packet: ReviewPacket, op const scope = reviewScope(p,item); if (!scope.related.length) return ''; const names = scope.related.map(other=>esc(other.name)).join(' · '); - return `

Reviewing ${esc(scope.description || item.name)}

Separate review items ${names}

${scope.excluded.length?`

Hidden in this preview: ${scope.excluded.map(other=>esc(other.name)).join(' · ')}

`:''}
`; + return `

Reviewing ${esc(scope.description || item.name)}

Outlined · reviewed separately ${names}

${scope.excluded.length?`

Hidden in this preview: ${scope.excluded.map(other=>esc(other.name)).join(' · ')}

`:''}
`; }; const boxStyle = (b: Box) => `left:${pct(b.x)};top:${pct(b.y)};width:${pct(b.w)};height:${pct(b.h)}`; + const referenceLayers = (p: ReviewPacket, item: Component) => p.stage === 'hero' ? '' : reviewScope(p,item).related.map(other => { + const b=item.box,o=other.box; + const x=Math.max(b.x,o.x),y=Math.max(b.y,o.y); + const relative={x:(x-b.x)/b.w,y:(y-b.y)/b.h,w:Math.max(0,Math.min(b.x+b.w,o.x+o.w)-x)/b.w,h:Math.max(0,Math.min(b.y+b.h,o.y+o.h)-y)/b.h}; + return `${esc(other.name)}`; + }).join(''); function focusReview(id: string) { root.getElementById(id)?.focus({preventScroll:true}); } @@ -220,9 +226,9 @@ export function mountComponentReview(host: HTMLElement, packet: ReviewPacket, op `:''}

${esc(groupOverview?unit!.label:v!.name)}

${peers.length>1?`
${peers.length} instances${!groupOverview?'':'Select to inspect'}
`:''}
${priorComponent?`
`:''}
${v?.preview.kind==='image'?`${icon('external')}`:''}
- ${groupOverview?`
Full comp cropComponent preview
${unit!.members.map((m,n)=>{const st=stateFor(m);return ``;}).join('')}
`:''} + ${groupOverview?`
Full comp cropComponent preview
${unit!.members.map((m,n)=>{const st=stateFor(m);return ``;}).join('')}
`:''} ${!groupOverview&&v?scopeMarkup(vp,v):''}
-
${viewingPrevious ? `Comp · Round ${history!.packet.round}` : assembled ? 'Approved comp' : visibleScope?.related.length ? 'Full comp crop' : 'In the comp'}
Reference region for ${esc(v!.name)}
+
${viewingPrevious ? `Comp · Round ${history!.packet.round}` : assembled ? 'Approved comp' : visibleScope?.related.length ? 'Full comp crop' : 'In the comp'}
Reference region for ${esc(v!.name)}${referenceLayers(vp,v!)}
${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?`
`:''}
` : ''} diff --git a/ui/component-review/styles.ts b/ui/component-review/styles.ts index 9fc4d98d7..8e95638c0 100644 --- a/ui/component-review/styles.ts +++ b/ui/component-review/styles.ts @@ -134,4 +134,6 @@ export const styles = ` .review-scope .separate-reviews,.review-scope .scope-excluded{font-size:11px;color:var(--muted)} .instance-row .review-scope{margin:12px 0 0} #comparison-dialog .review-scope{flex:none} + +.reference-layer{position:absolute;box-sizing:border-box;border:1px dashed #fff;outline:1px solid #173e43;pointer-events:none;z-index:2}.reference-layer>span{position:absolute;top:0;left:0;max-width:100%;padding:1px 4px;background:#173e43;color:white;font:10px/1.4 var(--font-sans,sans-serif);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;opacity:0;transition:opacity .12s}.instance-row:hover .reference-layer>span,.instance-row:focus-visible .reference-layer>span,.pan-viewport:hover .reference-layer>span,.pan-viewport:focus .reference-layer>span{opacity:1} `;