From 11e672f1106cdc05cb6c1e0f068a96cfbc0bab4d Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Mon, 21 Sep 2026 15:25:39 -0700 Subject: [PATCH] Fix component preview fonts and clarify reference scope Reject unavailable primary font families before publishing captures and record font evidence. Outline separately reviewed regions in single and grouped comparisons without masking the approved comp. Add browser and scope regressions. AI assistance: implemented and validated with OpenAI Codex. --- crates/cli/src/component_capture.rs | 19 ++++++ crates/cli/src/component_fonts.js | 27 ++++++++ crates/context/assets/component-review.js | 78 ++++++++++++----------- skill/reference/component-review.md | 2 + ui/component-review/model.test.ts | 10 +++ ui/component-review/model.ts | 11 ++-- ui/component-review/review.ts | 12 +++- ui/component-review/styles.ts | 2 + 8 files changed, 115 insertions(+), 46 deletions(-) create mode 100644 crates/cli/src/component_fonts.js 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} `;