@vizzly-testing/cli 0.34.0 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,5 @@
1
1
  import { createApiClient as defaultCreateApiClient, finalizeBuild as defaultFinalizeBuild, getTokenContext as defaultGetTokenContext } from '../api/index.js';
2
+ import { CONFIG_DEFAULTS } from '../config/core.js';
2
3
  import { createUploader as defaultCreateUploader } from '../uploader/index.js';
3
4
  import { getAppBaseUrl } from '../utils/api-url.js';
4
5
  import { loadConfig as defaultLoadConfig } from '../utils/config-loader.js';
@@ -73,12 +74,12 @@ export async function uploadCommand(screenshotsPath, options = {}, globalOptions
73
74
  });
74
75
  let buildId = null;
75
76
  let config = null;
76
- const uploadStartTime = Date.now();
77
+ let uploadStartTime = Date.now();
77
78
  try {
78
79
  output.info('Starting upload process...');
79
80
 
80
81
  // Load configuration with CLI overrides
81
- const allOptions = {
82
+ let allOptions = {
82
83
  ...globalOptions,
83
84
  ...options
84
85
  };
@@ -95,11 +96,12 @@ export async function uploadCommand(screenshotsPath, options = {}, globalOptions
95
96
  }
96
97
 
97
98
  // Collect git metadata if not provided
98
- const branch = await detectBranch(options.branch);
99
- const commit = await detectCommit(options.commit);
100
- const message = options.message || (await detectCommitMessage());
101
- const buildName = await generateBuildNameWithGit(options.buildName);
102
- const pullRequestNumber = detectPullRequestNumber();
99
+ let configuredBuildName = config.build.name && config.build.name !== CONFIG_DEFAULTS.build.name ? config.build.name : undefined;
100
+ let branch = await detectBranch(options.branch || config.build.branch);
101
+ let commit = await detectCommit(options.commit || config.build.commit);
102
+ let message = options.message || config.build.message || (await detectCommitMessage());
103
+ let buildName = await generateBuildNameWithGit(options.buildName || configuredBuildName);
104
+ let pullRequestNumber = detectPullRequestNumber();
103
105
  output.info(`Uploading screenshots from: ${screenshotsPath}`);
104
106
  if (globalOptions.verbose) {
105
107
  output.info('Configuration loaded');
@@ -119,7 +121,7 @@ export async function uploadCommand(screenshotsPath, options = {}, globalOptions
119
121
  });
120
122
 
121
123
  // Prepare upload options with progress callback
122
- const uploadOptions = {
124
+ let uploadOptions = {
123
125
  screenshotsDir: screenshotsPath,
124
126
  buildName,
125
127
  branch,
@@ -127,12 +129,13 @@ export async function uploadCommand(screenshotsPath, options = {}, globalOptions
127
129
  message,
128
130
  environment: config.build.environment,
129
131
  threshold: config.comparison.threshold,
132
+ minClusterSize: config.comparison.minClusterSize,
130
133
  uploadAll: options.uploadAll || false,
131
134
  metadata: options.metadata ? JSON.parse(options.metadata) : {},
132
135
  pullRequestNumber,
133
136
  parallelId: config.parallelId,
134
137
  onProgress: progressData => {
135
- const {
138
+ let {
136
139
  message: progressMessage,
137
140
  current,
138
141
  total,
@@ -158,7 +161,7 @@ export async function uploadCommand(screenshotsPath, options = {}, globalOptions
158
161
 
159
162
  // Start upload
160
163
  output.progress('Starting upload...');
161
- const result = await uploader.upload(uploadOptions);
164
+ let result = await uploader.upload(uploadOptions);
162
165
  buildId = result.buildId; // Ensure we have the buildId
163
166
 
164
167
  // Write session for subsequent commands (like preview)
@@ -263,8 +266,10 @@ export async function uploadCommand(screenshotsPath, options = {}, globalOptions
263
266
  executionTimeMs
264
267
  });
265
268
  output.cleanup();
269
+ let success = buildResult.failedComparisons === 0;
266
270
  return {
267
- success: buildResult.failedComparisons === 0,
271
+ success,
272
+ exitCode: success ? 0 : 1,
268
273
  result
269
274
  };
270
275
  }
@@ -283,6 +288,14 @@ export async function uploadCommand(screenshotsPath, options = {}, globalOptions
283
288
  let waitBuildUrl = buildResult.url || (await buildUrlConstructor(result.buildId, config.apiUrl, config.apiKey, deps));
284
289
  output.blank();
285
290
  output.labelValue('View', output.link('Results', waitBuildUrl));
291
+ if (buildResult.failedComparisons > 0) {
292
+ output.cleanup();
293
+ return {
294
+ success: false,
295
+ exitCode: 1,
296
+ result
297
+ };
298
+ }
286
299
  }
287
300
  output.cleanup();
288
301
  return {
@@ -300,11 +313,10 @@ export async function uploadCommand(screenshotsPath, options = {}, globalOptions
300
313
  message: 'Vizzly API unavailable - upload skipped',
301
314
  executionTimeMs: Date.now() - uploadStartTime
302
315
  });
303
- output.cleanup();
304
316
  } else {
305
317
  output.warn('Vizzly API unavailable - upload skipped. Your tests still ran.');
306
- output.cleanup();
307
318
  }
319
+ output.cleanup();
308
320
  return {
309
321
  success: true,
310
322
  result: {
@@ -381,6 +393,12 @@ export function validateUploadOptions(screenshotsPath, options) {
381
393
  errors.push('Threshold must be a non-negative number (CIEDE2000 Delta E)');
382
394
  }
383
395
  }
396
+ if (options.minClusterSize !== undefined) {
397
+ let minClusterSize = Number(options.minClusterSize);
398
+ if (!Number.isInteger(minClusterSize) || minClusterSize < 1) {
399
+ errors.push('Min cluster size must be a positive integer');
400
+ }
401
+ }
384
402
  if (options.batchSize !== undefined) {
385
403
  let n = Number(options.batchSize);
386
404
  if (!Number.isInteger(n) || n <= 0) {
@@ -194,7 +194,7 @@ Error generating stack: `+e.message+`
194
194
  linear-gradient(-45deg, var(--vz-checkerboard-tile) 25%, transparent 25%),
195
195
  linear-gradient(45deg, transparent 75%, var(--vz-checkerboard-tile) 75%),
196
196
  linear-gradient(-45deg, transparent 75%, var(--vz-checkerboard-tile) 75%)
197
- `,backgroundSize:`16px 16px`,backgroundPosition:`0 0, 0 8px, 8px -8px, -8px 0px`,backgroundColor:`var(--vz-checkerboard-bg)`};function Oo({comparison:e,viewMode:t=`overlay`,showDiffOverlay:n=!0,showBaseline:r=!0,onToggleBaseline:i,onionSkinPosition:a=50,onOnionSkinChange:o,onDiffToggle:s,disableLoadingOverlay:c=!1,zoom:l=`fit`,className:u=``,showRegions:d=!1}){let[f,p]=(0,N.useState)(new Set),[m,h]=(0,N.useState)(new Map),[g,_]=(0,N.useState)(!1),[v,y]=(0,N.useState)(!1),[b,x]=(0,N.useState)(!1),[S,ee]=(0,N.useState)(!1),[C,w]=(0,N.useState)(1),[te,ne]=(0,N.useState)({width:0,height:0}),T=(0,N.useRef)(null),re=(0,N.useRef)(null);(0,N.useEffect)(()=>{let e=()=>{let e=T.current,t=e?.querySelector(`img`);if(!e||!t?.naturalWidth)return;ne({width:t.naturalWidth,height:t.naturalHeight});let n=e.getBoundingClientRect(),r=n.width-40,i=n.height-40,a=r/t.naturalWidth,o=i/t.naturalHeight;w(Math.min(a,o,1))};e();let t=new window.ResizeObserver(e);T.current&&t.observe(T.current);let n=T.current?.querySelectorAll(`img`)||[];for(let t of n)t.addEventListener(`load`,e);return()=>{t.disconnect();for(let t of n)t.removeEventListener(`load`,e)}},[]);let E=(0,N.useMemo)(()=>{let e=l===`fit`,t=e?C:typeof l==`number`?l:1,n=!e&&t>C;return{isFit:e,scale:t,allowsOverflow:n,containerClass:n?`overflow-auto`:`overflow-hidden`}},[l,C]),D=(0,N.useCallback)(e=>{p(t=>new Set([...t,e]))},[]),O=(0,N.useCallback)(e=>{h(t=>new Map(t).set(e,`loaded`))},[]),k=(0,N.useMemo)(()=>e?{current:Eo(e.current,e.timestamp),baseline:Eo(e.baseline,e.timestamp),diff:Eo(e.diff,e.timestamp)}:{},[e]),A=(0,N.useMemo)(()=>e?{id:e.id||e.signature||`unknown`,name:e.name||e.originalName||`Screenshot`}:null,[e]);return(0,N.useEffect)(()=>{if(!A?.id){_(!1),y(!1),x(!1),ee(!1);return}_(!1),y(!1),x(!1),ee(!1)},[A?.id]),!e||e.status===`new`||e.status===`baseline-created`?(0,P.jsx)(`div`,{className:`h-full ${u}`,children:(0,P.jsxs)(`div`,{ref:T,className:`bg-[var(--vz-elevated)] relative h-full ${E.containerClass}`,style:Do,children:[(0,P.jsx)(`div`,{className:`relative ${E.allowsOverflow?`flex justify-center items-start`:`min-w-full min-h-full flex justify-center items-center`}`,style:{...E.allowsOverflow?{minWidth:te.width*E.scale+40,minHeight:te.height*E.scale+40,padding:`20px`}:{zoom:E.scale,padding:`20px`}},children:(0,P.jsx)(`div`,{ref:re,className:`relative inline-block`,style:E.allowsOverflow?{transform:`scale(${E.scale})`,transformOrigin:`top center`}:{},children:e&&(0,P.jsx)(`img`,{src:k.current,alt:e.name||`New screenshot`,className:`block`,onLoad:()=>O(`current-${A?.id}`),onError:()=>D(`current-${A?.id}`)})})}),!c&&e&&!m.has(`current-${A?.id}`)&&(0,P.jsx)(`div`,{className:`absolute inset-0 bg-[var(--vz-raised)] animate-pulse flex items-center justify-center`,children:(0,P.jsx)(`div`,{className:`text-[var(--text-tertiary)]`,children:`Loading...`})}),e&&f.has(`current-${A?.id}`)&&(0,P.jsx)(`div`,{className:`absolute inset-0 bg-[var(--vz-raised)] flex items-center justify-center`,children:(0,P.jsxs)(`div`,{className:`text-center text-[var(--text-tertiary)]`,children:[(0,P.jsx)(hn,{className:`w-8 h-8 mx-auto mb-2`}),(0,P.jsx)(`div`,{className:`text-sm`,children:`Failed to load image`})]})})]})}):(0,P.jsx)(`div`,{className:`h-full ${u}`,children:(0,P.jsxs)(`div`,{ref:T,className:`bg-[var(--vz-elevated)] relative unified-screenshot-container h-full ${E.containerClass}`,style:Do,children:[(0,P.jsx)(`div`,{className:`relative ${E.allowsOverflow?`flex justify-center items-start`:`min-w-full min-h-full flex justify-center items-center`}`,style:{...E.allowsOverflow?{minWidth:te.width*E.scale+40,minHeight:te.height*E.scale+40,padding:`20px`}:{zoom:E.scale,padding:`20px`}},children:(0,P.jsxs)(`div`,{ref:re,className:`relative inline-block`,style:E.allowsOverflow?{transform:`scale(${E.scale})`,transformOrigin:`top center`}:{},children:[t===`toggle`&&k.baseline?(0,P.jsx)(ja,{baselineImageUrl:k.baseline,currentImageUrl:k.current,baselineImageLoaded:g,setBaselineImageLoaded:_,currentImageLoaded:v,setCurrentImageLoaded:y,baselineImageError:b,setBaselineImageError:x,currentImageError:S,setCurrentImageError:ee,screenshot:A,onImageError:D,onImageLoad:O,imageErrors:f,showBaseline:r,onToggle:i}):t===`onion-skin`&&k.baseline?(0,P.jsx)(ka,{baselineImageUrl:k.baseline,currentImageUrl:k.current,sliderPosition:a,onSliderChange:o,screenshot:A,onImageError:D,onImageLoad:O,imageErrors:f}):(0,P.jsx)(Aa,{baselineImageUrl:k.baseline,currentImageUrl:k.current,diffImageUrl:k.diff,showDiffOverlay:n,screenshot:A,onImageError:D,onImageLoad:O,imageErrors:f,onDiffToggle:s}),d&&e?.confirmedRegions?.length>0&&(0,P.jsx)(Ca,{confirmed:e.confirmedRegions,candidates:[],imageWidth:te.width,imageHeight:te.height,showConfirmed:!0,showCandidates:!1,disabled:!0})]})}),!c&&!m.has(`current-${A?.id}`)&&(0,P.jsx)(`div`,{className:`absolute inset-0 bg-[var(--vz-raised)] animate-pulse flex items-center justify-center`,children:(0,P.jsx)(`div`,{className:`text-[var(--text-tertiary)]`,children:`Loading...`})}),f.has(`current-${A?.id}`)&&(0,P.jsx)(`div`,{className:`absolute inset-0 bg-[var(--vz-raised)] flex items-center justify-center`,children:(0,P.jsxs)(`div`,{className:`text-center text-[var(--text-tertiary)]`,children:[(0,P.jsx)(hn,{className:`w-8 h-8 mx-auto mb-2`}),(0,P.jsx)(`div`,{className:`text-sm`,children:`Failed to load image`})]})})]})})}function G(e){return e?.id||e?.signature||e?.name}function ko(e){switch(e){case`failed`:return`changed`;case`new`:case`baseline-created`:return`new`;case`passed`:return`unchanged`;default:return e}}function Ao(e){return(0,P.jsx)(to,{children:(0,P.jsx)(jo,{...e})})}function jo({comparison:e,comparisons:t=[],onClose:n,onAccept:r,onReject:i,onDelete:a,onNavigate:o}){let[s,c]=(0,N.useState)(So.OVERLAY),[l,u]=(0,N.useState)(!0),[d,f]=(0,N.useState)(50),[p,m]=(0,N.useState)(!0),[h,g]=(0,N.useState)(!1),[_,v]=(0,N.useState)(`needs-review`),[y,b]=(0,N.useState)(!0),[x,S]=(0,N.useState)(!1),{zoom:ee,setZoom:C}=bo(`fit`),{isActive:w}=no(),te=(0,N.useRef)(null),ne=(0,N.useCallback)(()=>{g(e=>!e)},[]),T=(0,N.useMemo)(()=>t.map(e=>({...e,name:e.name||e.originalName||`Unknown`,result:ko(e.status),status:e.status,approval_status:e.userAction===`accepted`?`approved`:e.userAction===`rejected`?`rejected`:e.status===`passed`?`approved`:`pending`,diff_percentage:e.diffPercentage??e.diff_percentage,diffClusters:e.diffClusters||[],viewport_width:e.properties?.viewport_width??e.viewport_width,viewport_height:e.properties?.viewport_height??e.viewport_height,browser:e.properties?.browser??e.browser})),[t]),re=(0,N.useMemo)(()=>{if(!e)return null;let t=e.name||e.originalName;return{name:t,comparisons:T.filter(e=>(e.name||e.originalName)===t)}},[e,T]),E=(0,N.useCallback)(e=>{e&&o&&o(e)},[o]),D=(0,N.useMemo)(()=>{let e={all:()=>!0,"needs-review":e=>(e.result===`changed`||e.result===`new`)&&e.approval_status!==`approved`&&e.approval_status!==`rejected`,changes:e=>e.result===`changed`,new:e=>e.result===`new`},t=e[_]||e.all;return T.filter(t)},[T,_]),O=(0,N.useMemo)(()=>{if(!e)return-1;let t=G(e);return D.findIndex(e=>G(e)===t)},[D,e]),k=(0,N.useMemo)(()=>({prev:O>0,next:O<D.length-1&&O!==-1}),[O,D.length]),A=(0,N.useCallback)(()=>{if(k.prev){let e=D[O-1];o(e)}},[k.prev,D,O,o]),ie=(0,N.useCallback)(()=>{if(k.next){let e=D[O+1];o(e)}},[k.next,D,O,o]),ae=(0,N.useCallback)(()=>O<D.length-1?D[O+1]:O>0?D[O-1]:null,[O,D]),oe=(0,N.useCallback)(()=>{if(!r||!e)return;let t=_===`needs-review`?ae():null;r(G(e)),t?o(t):_===`needs-review`&&D.length<=1&&n()},[r,e,_,ae,D.length,o,n]),se=(0,N.useCallback)(()=>{if(!i||!e)return;let t=_===`needs-review`?ae():null;i(G(e)),t?o(t):_===`needs-review`&&D.length<=1&&n()},[i,e,_,ae,D.length,o,n]),ce=(0,N.useCallback)(()=>{!a||!e||a(G(e))},[a,e]),j=(0,N.useCallback)(()=>{c(e=>e===So.OVERLAY?So.TOGGLE:So.OVERLAY)},[]),le=(0,N.useCallback)(()=>{s===So.TOGGLE?b(e=>!e):u(e=>!e)},[s]);if(ro((0,N.useMemo)(()=>({a:r?oe:void 0,r:i?se:void 0,d:le,t:j,1:()=>c(So.OVERLAY),2:()=>c(So.TOGGLE),3:()=>c(So.ONION)}),[oe,se,r,i,j,le])),(0,N.useEffect)(()=>{let e=e=>{if(!(e.target.tagName===`INPUT`||e.target.tagName===`TEXTAREA`))switch(e.key){case`ArrowUp`:case`ArrowLeft`:e.preventDefault(),k.prev&&A();break;case`ArrowDown`:case`ArrowRight`:e.preventDefault(),k.next&&ie();break;case`Escape`:e.preventDefault(),n();break;case`i`:!e.metaKey&&!e.ctrlKey&&(e.preventDefault(),ne());break;case`g`:!e.metaKey&&!e.ctrlKey&&(e.preventDefault(),S(e=>!e));break}};return window.addEventListener(`keydown`,e),()=>window.removeEventListener(`keydown`,e)},[k,A,ie,n,ne]),(0,N.useEffect)(()=>{if(!p||O<0||O>=D.length)return;let e=te.current;if(!e)return;let t=e.closest(`.overflow-y-auto`);if(!t)return;let n=e.getBoundingClientRect(),r=t.getBoundingClientRect();n.top<r.top?t.scrollBy({top:n.top-r.top-8,behavior:`smooth`}):n.bottom>r.bottom&&t.scrollBy({top:n.bottom-r.bottom+8,behavior:`smooth`})},[O,D.length,p]),!e)return(0,P.jsx)(`div`,{className:`min-h-screen bg-[var(--vz-bg)] flex items-center justify-center`,children:(0,P.jsxs)(`div`,{className:`text-center text-[var(--text-tertiary)]`,children:[(0,P.jsx)(`div`,{className:`text-lg mb-2`,children:`Comparison not found`}),(0,P.jsx)(`button`,{type:`button`,onClick:n,className:`text-[var(--accent-info)] hover:text-[var(--text-primary)]`,children:`Return to list`})]})});let ue=ko(e.status),de=e.userAction===`accepted`?`approved`:e.userAction===`rejected`?`rejected`:e.status===`passed`?`approved`:`pending`,M={new:{variant:`info`,label:`New`},changed:{variant:`warning`,label:`Changed`},unchanged:{variant:`default`,label:`Unchanged`}}[ue],fe=e.status===`new`||e.status===`baseline-created`,pe=e.properties||{};return(0,P.jsxs)(`div`,{className:`h-screen bg-[var(--vz-bg)] flex flex-col overflow-hidden`,"data-testid":`fullscreen-viewer`,children:[(0,P.jsxs)(`header`,{className:`flex-shrink-0 bg-[var(--vz-bg)] border-b border-[var(--vz-border-subtle)] z-30`,children:[(0,P.jsxs)(`div`,{className:`px-2 sm:px-4 py-2 flex items-center gap-1 sm:gap-3`,children:[(0,P.jsx)(qa,{onClick:n}),(0,P.jsxs)(`div`,{className:`flex-1 min-w-0 flex items-center gap-2`,children:[(0,P.jsx)(`h1`,{className:`text-sm font-medium text-[var(--text-primary)] truncate`,children:e.name||e.originalName||`Unknown`}),M&&(0,P.jsx)(nr,{variant:M.variant,size:`sm`,className:`hidden xs:flex`,children:M.label}),w&&(0,P.jsx)(nr,{variant:`warning`,size:`sm`,dot:!0,pulseDot:!0,className:`hidden sm:flex`,children:`Review Mode`})]}),(0,P.jsxs)(`div`,{className:`flex items-center`,children:[(0,P.jsx)(`button`,{type:`button`,onClick:A,disabled:!k.prev,className:`p-2 text-[var(--text-tertiary)] hover:text-[var(--text-primary)] disabled:opacity-85 disabled:cursor-not-allowed rounded-md hover:bg-[var(--vz-raised)] transition-colors`,"aria-label":`Previous`,children:(0,P.jsx)(Kt,{className:`w-5 h-5`})}),(0,P.jsxs)(`span`,{className:`hidden sm:block text-xs text-[var(--text-muted)] font-medium tabular-nums px-1 min-w-[3rem] text-center`,children:[O+1,`/`,D.length]}),(0,P.jsx)(`button`,{type:`button`,onClick:ie,disabled:!k.next,className:`p-2 text-[var(--text-tertiary)] hover:text-[var(--text-primary)] disabled:opacity-85 disabled:cursor-not-allowed rounded-md hover:bg-[var(--vz-raised)] transition-colors`,"aria-label":`Next`,children:(0,P.jsx)(Jt,{className:`w-5 h-5`})})]}),(0,P.jsx)(`div`,{className:`hidden sm:block h-5 w-px bg-[var(--vz-border-subtle)]`}),(0,P.jsxs)(`div`,{className:`hidden sm:flex items-center gap-2`,children:[fe&&a?(0,P.jsx)(`button`,{type:`button`,onClick:ce,className:`px-3 py-1.5 text-xs font-medium text-[var(--accent-danger)] hover:text-[var(--text-primary)] hover:bg-[var(--accent-danger-muted)] rounded-md transition-colors`,"data-testid":`btn-delete`,children:`Delete`}):null,(0,P.jsx)(ba,{status:de,onApprove:r?oe:null,onReject:i?se:null,compact:!0})]}),(0,P.jsxs)(`div`,{className:`hidden sm:flex items-center`,children:[(0,P.jsx)(`button`,{type:`button`,onClick:()=>m(!p),className:`flex items-center justify-center p-2 rounded-md transition-colors ${p?`bg-[var(--accent-brand-muted)] text-[var(--accent-brand)]`:`text-[var(--text-tertiary)] hover:text-[var(--text-primary)] hover:bg-[var(--vz-raised)]`}`,title:`Queue`,"aria-label":`Toggle queue`,"data-testid":`toggle-queue-btn`,children:(0,P.jsx)(Tn,{className:`w-5 h-5 pointer-events-none`})}),(e?.confirmedRegions?.length>0||e?.hasConfirmedRegions)&&(0,P.jsx)(`button`,{type:`button`,onClick:()=>S(!x),className:`p-2 rounded-md transition-colors ${x?`bg-[var(--accent-success-muted)] text-[var(--accent-success)]`:`text-[var(--text-tertiary)] hover:text-[var(--text-primary)] hover:bg-[var(--vz-raised)]`}`,title:`Show Regions (G)`,"aria-label":`Toggle regions`,"data-testid":`toggle-regions-btn`,children:(0,P.jsx)(jn,{className:`w-5 h-5 pointer-events-none`})}),(0,P.jsx)(`button`,{type:`button`,onClick:ne,className:`p-2 rounded-md transition-colors ${h?`bg-[var(--accent-info-muted)] text-[var(--accent-info)]`:`text-[var(--text-tertiary)] hover:text-[var(--text-primary)] hover:bg-[var(--vz-raised)]`}`,title:`Details (I)`,"aria-label":`Screenshot details`,"data-testid":`toggle-inspector-btn`,children:(0,P.jsx)(Cn,{className:`w-5 h-5 pointer-events-none`})})]})]}),(0,P.jsxs)(`div`,{className:`px-2 sm:px-4 py-1.5 sm:py-2 border-t border-[var(--vz-border-subtle)] flex items-center gap-2 sm:gap-3`,children:[re&&re.comparisons.length>0&&(0,P.jsx)(go,{variants:re.comparisons,currentVariantId:G(e),onVariantSelect:E}),(0,P.jsx)(`div`,{className:`flex-1`}),e.diff&&(0,P.jsx)(vo,{value:s,onChange:c,compact:!0,className:`hidden sm:flex`}),(0,P.jsx)(xo,{zoom:ee,onZoomChange:C})]})]}),(0,P.jsxs)(`div`,{className:`flex-1 min-h-0 flex overflow-hidden`,children:[(0,P.jsxs)(Va,{isOpen:p,onClose:()=>m(!1),position:`bottom`,desktopPosition:`left`,width:`w-80`,mobileHeight:`30vh`,expandedHeight:`80vh`,alwaysVisibleOnDesktop:!1,showBackdrop:!1,testId:`queue-drawer`,children:[(0,P.jsx)(Ha,{title:`Queue`,subtitle:`${D.length} of ${T.length} items`,onClose:()=>m(!1)}),(0,P.jsx)(Ga,{children:[{id:`needs-review`,label:`To Review`},{id:`changes`,label:`Changes`},{id:`new`,label:`New`},{id:`all`,label:`All`}].map(e=>(0,P.jsx)(Ka,{isActive:_===e.id,onClick:()=>v(e.id),children:e.label},e.id))}),(0,P.jsxs)(Ua,{className:`px-2 py-2 space-y-1`,children:[D.map(t=>{let n=G(t)===G(e);return(0,P.jsx)(`div`,{ref:n?te:null,children:(0,P.jsx)($a,{item:t,isActive:n,thumbnailUrl:Eo(t.current||t.baseline,t.timestamp),onClick:()=>o(t)})},G(t))}),D.length===0&&(0,P.jsx)(`div`,{className:`text-center py-8 text-[var(--text-muted)] text-xs`,children:_===`needs-review`?`All screenshots reviewed!`:`No items match this filter`})]}),(0,P.jsxs)(Wa,{className:`hidden sm:flex flex-col items-center gap-1.5`,children:[(0,P.jsxs)(`p`,{className:`text-xs text-[var(--text-muted)] text-center`,children:[(0,P.jsx)(`span`,{className:`text-[var(--text-tertiary)]`,children:`↑↓`}),` navigate`,` · `,(0,P.jsx)(`span`,{className:`text-[var(--text-tertiary)]`,children:`Space`}),` `,w?`exit`:`review mode`]}),w&&(0,P.jsxs)(`p`,{className:`text-xs text-[var(--text-muted)] text-center`,children:[(0,P.jsx)(`span`,{className:`text-[var(--accent-success)] font-medium`,children:`A`}),` `,`approve`,` · `,(0,P.jsx)(`span`,{className:`text-[var(--accent-danger)] font-medium`,children:`R`}),` `,`reject`,` · `,(0,P.jsx)(`span`,{className:`text-[var(--text-tertiary)] font-medium`,children:`D`}),` `,`diff`,` · `,(0,P.jsx)(`span`,{className:`text-[var(--text-tertiary)] font-medium`,children:`T`}),` `,`view`]})]})]}),(0,P.jsx)(`main`,{className:`flex-1 min-h-0 overflow-hidden relative`,children:(0,P.jsx)(Oo,{comparison:e,viewMode:s===So.ONION?`onion-skin`:s,showDiffOverlay:l,onDiffToggle:()=>u(e=>!e),showBaseline:y,onToggleBaseline:()=>b(e=>!e),onionSkinPosition:d,onOnionSkinChange:f,zoom:ee,disableLoadingOverlay:!0,showRegions:x,className:`w-full h-full`},G(e))}),(0,P.jsxs)(Za,{isOpen:h,onClose:()=>g(!1),title:`Screenshot Details`,subtitle:e.name||e.originalName,className:`order-last`,children:[(0,P.jsx)(Za.Section,{title:`Status`,icon:sn,children:(0,P.jsxs)(`div`,{className:`flex items-center gap-2 flex-wrap`,children:[M&&(0,P.jsx)(nr,{variant:M.variant,size:`sm`,children:M.label}),(0,P.jsx)(nr,{variant:de===`approved`?`success`:de===`rejected`?`danger`:`default`,size:`sm`,children:de})]})}),(0,P.jsxs)(Za.Section,{title:`Screenshot`,icon:an,children:[pe.browser&&(0,P.jsx)(Za.Row,{label:`Browser`,value:pe.browser,icon:()=>(0,P.jsx)(ir,{browser:pe.browser,className:`w-3.5 h-3.5`})}),(0,P.jsx)(Za.Row,{label:`Viewport`,value:pe.viewport_width&&pe.viewport_height?`${pe.viewport_width} × ${pe.viewport_height}`:null,mono:!0}),(0,P.jsx)(Za.Row,{label:`Device`,value:pe.device})]}),ue===`changed`&&e.diffPercentage!=null&&(0,P.jsxs)(Za.Section,{title:`Diff Analysis`,icon:pn,children:[(0,P.jsx)(Za.Row,{label:`Changed`,value:`${Number(e.diffPercentage).toFixed(3)}%`,mono:!0}),e.diffPixels!=null&&(0,P.jsx)(Za.Row,{label:`Pixels`,value:e.diffPixels.toLocaleString(),mono:!0}),e.threshold!=null&&(0,P.jsx)(Za.Row,{label:`Threshold`,value:Number(e.threshold).toFixed(2),mono:!0})]}),(0,P.jsx)(Za.Section,{title:`Identifier`,icon:sn,children:(0,P.jsx)(Za.Row,{label:`Signature`,value:e.signature||e.id,mono:!0,copyable:!0})})]})]}),(0,P.jsxs)(`div`,{className:`sm:hidden flex-shrink-0 bg-[var(--vz-bg)] border-t border-[var(--vz-border-subtle)] safe-area-pb`,children:[(0,P.jsx)(xa,{status:de,onApprove:r?oe:null,onReject:i?se:null}),(0,P.jsxs)(`div`,{className:`px-3 py-2 flex items-center gap-1 border-t border-[var(--vz-border-subtle)]`,children:[(0,P.jsx)(`button`,{type:`button`,onClick:()=>m(!p),className:`flex items-center justify-center p-2.5 rounded-lg transition-colors ${p?`bg-[var(--accent-brand-muted)] text-[var(--accent-brand)]`:`text-[var(--text-tertiary)] hover:text-[var(--text-primary)] hover:bg-[var(--vz-raised)] active:bg-[var(--vz-elevated)]`}`,"aria-label":`Queue`,"data-testid":`mobile-toggle-queue-btn`,children:(0,P.jsx)(Tn,{className:`w-5 h-5 pointer-events-none`})}),(0,P.jsx)(`button`,{type:`button`,onClick:ne,className:`flex items-center justify-center p-2.5 rounded-lg transition-colors ${h?`bg-[var(--accent-info-muted)] text-[var(--accent-info)]`:`text-[var(--text-tertiary)] hover:text-[var(--text-primary)] hover:bg-[var(--vz-raised)] active:bg-[var(--vz-elevated)]`}`,"aria-label":`Screenshot details`,"data-testid":`mobile-toggle-inspector-btn`,children:(0,P.jsx)(Cn,{className:`w-5 h-5 pointer-events-none`})}),(e?.confirmedRegions?.length>0||e?.hasConfirmedRegions)&&(0,P.jsx)(`button`,{type:`button`,onClick:()=>S(!x),className:`flex items-center justify-center p-2.5 rounded-lg transition-colors ${x?`bg-[var(--accent-success-muted)] text-[var(--accent-success)]`:`text-[var(--text-tertiary)] hover:text-[var(--text-primary)] hover:bg-[var(--vz-raised)] active:bg-[var(--vz-elevated)]`}`,"aria-label":`Toggle regions`,"data-testid":`mobile-toggle-regions-btn`,children:(0,P.jsx)(jn,{className:`w-5 h-5 pointer-events-none`})}),fe&&a&&(0,P.jsx)(`button`,{type:`button`,onClick:ce,className:`flex items-center justify-center p-2.5 rounded-lg text-[var(--accent-danger)] hover:text-[var(--text-primary)] hover:bg-[var(--accent-danger-muted)] active:bg-[var(--accent-danger-muted)] transition-colors`,"aria-label":`Delete`,"data-testid":`mobile-delete-btn`,children:(0,P.jsx)(`span`,{className:`text-xs font-medium`,children:`Delete`})}),(0,P.jsx)(`div`,{className:`flex-1`}),(0,P.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,P.jsxs)(`span`,{className:`text-xs text-[var(--text-muted)] tabular-nums`,children:[O+1,` / `,D.length]}),M&&(0,P.jsx)(nr,{variant:M.variant,size:`sm`,children:M.label})]})]})]})]})}function Mo(){let[,e]=gi(),[,t]=vi(`/comparison/:id`),{data:n}=Ui(),r=Wi(),i=qi(),a=Xi(),o=(0,N.useMemo)(()=>n?.comparisons||[],[n?.comparisons]),s=(0,N.useMemo)(()=>{let e=t?.id?decodeURIComponent(t.id):null;return!e||o.length===0?null:o.find(t=>t.id===e||t.signature===e||t.name===e)},[t,o]),{data:c}=Hi(s?.id),l=(0,N.useMemo)(()=>s?c?{...s,diffClusters:c.diffClusters,confirmedRegions:c.confirmedRegions,intensityStats:c.intensityStats,boundingBox:c.boundingBox,regionAnalysis:c.regionAnalysis,hotspotAnalysis:c.hotspotAnalysis}:s:null,[s,c]),u=(0,N.useCallback)(t=>{let n=t.id||t.signature||t.name;n&&e(`/comparison/${encodeURIComponent(n)}`)},[e]),d=(0,N.useCallback)(()=>{e(`/`)},[e]),f=(0,N.useCallback)(e=>{r.mutate(e)},[r]),p=(0,N.useCallback)(e=>{i.mutate(e)},[i]),m=(0,N.useCallback)(t=>{a.mutate(t,{onSuccess:()=>{e(`/`)}})},[a,e]);return l?(0,P.jsx)(Ao,{comparison:l,comparisons:o,onClose:d,onAccept:f,onReject:p,onDelete:m,onNavigate:u}):(0,P.jsx)(`div`,{className:`fixed inset-0 bg-[var(--vz-bg)] z-50 flex items-center justify-center`,children:(0,P.jsxs)(`div`,{className:`text-center`,children:[(0,P.jsx)(`div`,{className:`text-lg text-[var(--text-primary)] font-medium mb-2`,children:`Comparison not found`}),(0,P.jsxs)(`p`,{className:`text-sm text-[var(--text-tertiary)] mb-4`,children:[`ID: `,t?.id||`none`]}),(0,P.jsx)(`button`,{type:`button`,onClick:d,className:`text-[var(--accent-media)] hover:text-[var(--accent-media)] font-medium transition-colors`,children:`Return to list`})]})})}To.NEW,To.BASELINE_CREATED,To.PASSED,To.FAILED,To.ERROR,To.REJECTED;function No(e,t){return[...e].sort((e,n)=>{if(t===`priority`){let t={[To.FAILED]:3,[To.NEW]:2,[To.PASSED]:1},r=e.initialStatus||e.status,i=n.initialStatus||n.status,a=t[r]||0,o=t[i]||0;return a===o?(n.diffPercentage||0)-(e.diffPercentage||0):o-a}return t===`name`?(e.name||``).localeCompare(n.name||``):t===`time`?(n.timestamp||0)-(e.timestamp||0):0})}function Po(e,t){return t===`failed`?e.filter(e=>e.status===To.FAILED):t===`passed`?e.filter(e=>e.status===To.PASSED):t===`new`?e.filter(e=>e.status===To.NEW||e.status===To.BASELINE_CREATED):e}function Fo(e){return e===`new`||e===`baseline-created`}function Io(e){return e===`failed`||Fo(e)}var Lo=()=>{let e=new URLSearchParams(window.location.search);return{filter:e.get(`filter`)||Co.ALL,sortBy:e.get(`sort`)||wo.PRIORITY,searchQuery:e.get(`search`)||``,selectedBrowser:e.get(`browser`)||`all`,selectedViewport:e.get(`viewport`)||`all`}};function Ro(e=[]){let t=Lo(),[n,r]=(0,N.useState)(t.filter),[i,a]=(0,N.useState)(t.sortBy),[o,s]=(0,N.useState)(t.searchQuery),[c,l]=(0,N.useState)(t.selectedBrowser),[u,d]=(0,N.useState)(t.selectedViewport);(0,N.useEffect)(()=>{let e=new URLSearchParams;n!==Co.ALL&&e.set(`filter`,n),i!==wo.PRIORITY&&e.set(`sort`,i),o&&e.set(`search`,o),c!==`all`&&e.set(`browser`,c),u!==`all`&&e.set(`viewport`,u);let t=e.toString()?`?${e.toString()}`:window.location.pathname;window.history.replaceState({},``,t)},[n,i,o,c,u]);let f=(0,N.useMemo)(()=>{let t=new Set,n=new Set;return e.forEach(e=>{if(e.properties?.browser&&t.add(e.properties.browser),e.properties?.viewport_width&&e.properties?.viewport_height){let t=`${e.properties.viewport_width}x${e.properties.viewport_height}`;n.add(t)}else if(e.properties?.viewport){let t=`${e.properties.viewport.width}x${e.properties.viewport.height}`;n.add(t)}}),{browsers:Array.from(t).sort(),viewports:Array.from(n).sort()}},[e]);return{filteredComparisons:(0,N.useMemo)(()=>{let t=Po(e,n);if(o.trim()){let e=o.toLowerCase();t=t.filter(t=>t.name.toLowerCase().includes(e)||t.originalName?.toLowerCase().includes(e))}return c!==`all`&&(t=t.filter(e=>e.properties?.browser===c)),u!==`all`&&(t=t.filter(e=>{let t=null;return e.properties?.viewport_width&&e.properties?.viewport_height?t=`${e.properties.viewport_width}x${e.properties.viewport_height}`:e.properties?.viewport&&(t=`${e.properties.viewport.width}x${e.properties.viewport.height}`),t===u})),No(t,i)},[e,n,i,o,c,u]),filter:n,setFilter:r,sortBy:i,setSortBy:a,searchQuery:o,setSearchQuery:s,selectedBrowser:c,setSelectedBrowser:l,selectedViewport:u,setSelectedViewport:d,availableFilters:f,counts:{all:e.length,failed:e.filter(e=>e.status===`failed`).length,passed:e.filter(e=>e.status===`passed`).length,new:e.filter(e=>Fo(e.status)).length,rejected:e.filter(e=>e.status===`rejected`).length}}}function zo({src:e,alt:t,className:n,style:r,onClick:i}){let[a,o]=(0,N.useState)(e?`loading`:`missing`);if((0,N.useEffect)(()=>{if(!e){o(`missing`);return}o(`loading`)},[e]),a===`missing`)return(0,P.jsx)(`div`,{className:`flex items-center justify-center bg-[var(--vz-raised)] border border-[var(--vz-border)] rounded min-h-[200px]`,style:r,children:(0,P.jsxs)(`div`,{className:`text-[var(--text-tertiary)] text-center`,children:[(0,P.jsx)(Fn,{className:`w-12 h-12 mx-auto mb-3`}),(0,P.jsxs)(`div`,{className:`text-sm`,children:[`No `,t.toLowerCase(),` available`]})]})});if(a===`error`)return(0,P.jsx)(`div`,{className:`flex items-center justify-center bg-[var(--accent-danger-muted)] border border-[color-mix(in_srgb,var(--accent-danger)_28%,transparent)] rounded min-h-[200px]`,style:r,children:(0,P.jsxs)(`div`,{className:`text-[var(--accent-danger)] text-center`,children:[(0,P.jsx)(hn,{className:`w-12 h-12 mx-auto mb-3`}),(0,P.jsxs)(`div`,{className:`text-sm`,children:[`Failed to load `,t.toLowerCase()]}),(0,P.jsx)(`div`,{className:`text-xs text-[var(--text-muted)] mt-1 font-mono break-all px-4`,children:e})]})});let s=null;return i&&(s=e=>{(e.key===`Enter`||e.key===` `)&&(e.preventDefault(),i(e))}),(0,P.jsx)(`img`,{src:e,alt:t,className:n,style:r,loading:`lazy`,decoding:`async`,onLoad:()=>o(`loaded`),onError:()=>o(`error`),onClick:i,onKeyDown:s,role:i?`button`:void 0,tabIndex:i?0:void 0})}function Bo(e){return e.id||e.signature||e.name}function Vo(e){let t=new Map;for(let n of e){let e=n.name||n.originalName||`Unknown`;t.has(e)||t.set(e,{name:e,comparisons:[],browsers:new Set,viewports:[],hasChanges:!1,hasNew:!1,allPassed:!0,maxDiff:0,failedCount:0,newCount:0,passedCount:0,acceptedCount:0,rejectedCount:0});let r=t.get(e);r.comparisons.push(n),n.properties?.browser&&r.browsers.add(n.properties.browser),n.properties?.viewport_width&&n.properties?.viewport_height&&r.viewports.push({width:n.properties.viewport_width,height:n.properties.viewport_height,id:n.id,status:n.status}),n.status===`failed`?(r.hasChanges=!0,r.allPassed=!1,r.failedCount++,r.maxDiff=Math.max(r.maxDiff,n.diffPercentage||0)):n.status===`new`||n.status===`baseline-created`?(r.hasNew=!0,r.allPassed=!1,r.newCount++):n.status===`passed`&&r.passedCount++,n.userAction===`accepted`?r.acceptedCount++:n.userAction===`rejected`&&r.rejectedCount++}return Array.from(t.values()).map(e=>({...e,browsers:Array.from(e.browsers),viewports:e.viewports.sort((e,t)=>t.width*t.height-e.width*e.height),primary:e.comparisons[0]})).sort((e,t)=>e.hasChanges&&!t.hasChanges?-1:!e.hasChanges&&t.hasChanges?1:e.hasNew&&!t.hasNew?-1:!e.hasNew&&t.hasNew?1:e.maxDiff===t.maxDiff?e.name.localeCompare(t.name):t.maxDiff-e.maxDiff)}function Ho({width:e,className:t=`w-3.5 h-3.5`}){return e<=480?(0,P.jsx)(ln,{className:t}):e<=1024?(0,P.jsx)(dn,{className:t}):(0,P.jsx)(an,{className:t})}function Uo({group:e}){return e.acceptedCount===e.comparisons.length?(0,P.jsxs)(nr,{variant:`success`,size:`sm`,children:[(0,P.jsx)(Bt,{className:`w-3 h-3 mr-1`}),`Accepted`]}):e.rejectedCount>0?(0,P.jsxs)(nr,{variant:`warning`,size:`sm`,children:[(0,P.jsx)(Yn,{className:`w-3 h-3 mr-1`}),`Rejected`]}):e.hasChanges?(0,P.jsxs)(nr,{variant:`danger`,size:`sm`,children:[e.failedCount,` changed`]}):e.hasNew?(0,P.jsxs)(nr,{variant:`info`,size:`sm`,children:[e.newCount,` new`]}):(0,P.jsx)(nr,{variant:`success`,size:`sm`,children:`Passed`})}function Wo({group:e,maxVisible:t=4}){let n=e.comparisons.length;if(n<=1){let t=e.viewports[0],n=e.browsers[0];return(0,P.jsxs)(`div`,{className:`flex items-center gap-2 text-xs text-[var(--text-tertiary)]`,children:[t&&(0,P.jsxs)(`span`,{className:`inline-flex items-center gap-1`,children:[(0,P.jsx)(Ho,{width:t.width,className:`w-3 h-3`}),t.width,`×`,t.height]}),n&&(0,P.jsx)(`span`,{children:n})]})}let r=e.viewports.slice(0,t),i=n-t;return(0,P.jsxs)(`div`,{className:`flex items-center gap-1.5 flex-wrap`,children:[r.map((e,t)=>(0,P.jsxs)(`span`,{className:`
197
+ `,backgroundSize:`16px 16px`,backgroundPosition:`0 0, 0 8px, 8px -8px, -8px 0px`,backgroundColor:`var(--vz-checkerboard-bg)`};function Oo({comparison:e,viewMode:t=`overlay`,showDiffOverlay:n=!0,showBaseline:r=!0,onToggleBaseline:i,onionSkinPosition:a=50,onOnionSkinChange:o,onDiffToggle:s,disableLoadingOverlay:c=!1,zoom:l=`fit`,className:u=``,showRegions:d=!1}){let[f,p]=(0,N.useState)(new Set),[m,h]=(0,N.useState)(new Map),[g,_]=(0,N.useState)(!1),[v,y]=(0,N.useState)(!1),[b,x]=(0,N.useState)(!1),[S,ee]=(0,N.useState)(!1),[C,w]=(0,N.useState)(1),[te,ne]=(0,N.useState)({width:0,height:0}),T=(0,N.useRef)(null),re=(0,N.useRef)(null);(0,N.useEffect)(()=>{let e=()=>{let e=T.current,t=e?.querySelector(`img`);if(!e||!t?.naturalWidth)return;ne({width:t.naturalWidth,height:t.naturalHeight});let n=e.getBoundingClientRect(),r=n.width-40,i=n.height-40,a=r/t.naturalWidth,o=i/t.naturalHeight;w(Math.min(a,o,1))};e();let t=new window.ResizeObserver(e);T.current&&t.observe(T.current);let n=T.current?.querySelectorAll(`img`)||[];for(let t of n)t.addEventListener(`load`,e);return()=>{t.disconnect();for(let t of n)t.removeEventListener(`load`,e)}},[]);let E=(0,N.useMemo)(()=>{let e=l===`fit`,t=e?C:typeof l==`number`?l:1,n=!e&&t>C;return{isFit:e,scale:t,allowsOverflow:n,containerClass:n?`overflow-auto`:`overflow-hidden`}},[l,C]),D=(0,N.useCallback)(e=>{p(t=>new Set([...t,e]))},[]),O=(0,N.useCallback)(e=>{h(t=>new Map(t).set(e,`loaded`))},[]),k=(0,N.useMemo)(()=>e?{current:Eo(e.current,e.timestamp),baseline:Eo(e.baseline,e.timestamp),diff:Eo(e.diff,e.timestamp)}:{},[e]),A=(0,N.useMemo)(()=>e?{id:e.id||e.signature||`unknown`,name:e.name||e.originalName||`Screenshot`}:null,[e]);return(0,N.useEffect)(()=>{if(!A?.id){_(!1),y(!1),x(!1),ee(!1);return}_(!1),y(!1),x(!1),ee(!1)},[A?.id]),!e||e.status===`new`||e.status===`baseline-created`?(0,P.jsx)(`div`,{className:`h-full ${u}`,children:(0,P.jsxs)(`div`,{ref:T,className:`bg-[var(--vz-elevated)] relative h-full ${E.containerClass}`,style:Do,children:[(0,P.jsx)(`div`,{className:`relative ${E.allowsOverflow?`flex justify-center items-start`:`min-w-full min-h-full flex justify-center items-center`}`,style:{...E.allowsOverflow?{minWidth:te.width*E.scale+40,minHeight:te.height*E.scale+40,padding:`20px`}:{zoom:E.scale,padding:`20px`}},children:(0,P.jsx)(`div`,{ref:re,className:`relative inline-block`,style:E.allowsOverflow?{transform:`scale(${E.scale})`,transformOrigin:`top center`}:{},children:e&&(0,P.jsx)(`img`,{src:k.current,alt:e.name||`New screenshot`,className:`block`,onLoad:()=>O(`current-${A?.id}`),onError:()=>D(`current-${A?.id}`)})})}),!c&&e&&!m.has(`current-${A?.id}`)&&(0,P.jsx)(`div`,{className:`absolute inset-0 bg-[var(--vz-raised)] animate-pulse flex items-center justify-center`,children:(0,P.jsx)(`div`,{className:`text-[var(--text-tertiary)]`,children:`Loading...`})}),e&&f.has(`current-${A?.id}`)&&(0,P.jsx)(`div`,{className:`absolute inset-0 bg-[var(--vz-raised)] flex items-center justify-center`,children:(0,P.jsxs)(`div`,{className:`text-center text-[var(--text-tertiary)]`,children:[(0,P.jsx)(hn,{className:`w-8 h-8 mx-auto mb-2`}),(0,P.jsx)(`div`,{className:`text-sm`,children:`Failed to load image`})]})})]})}):(0,P.jsx)(`div`,{className:`h-full ${u}`,children:(0,P.jsxs)(`div`,{ref:T,className:`bg-[var(--vz-elevated)] relative unified-screenshot-container h-full ${E.containerClass}`,style:Do,children:[(0,P.jsx)(`div`,{className:`relative ${E.allowsOverflow?`flex justify-center items-start`:`min-w-full min-h-full flex justify-center items-center`}`,style:{...E.allowsOverflow?{minWidth:te.width*E.scale+40,minHeight:te.height*E.scale+40,padding:`20px`}:{zoom:E.scale,padding:`20px`}},children:(0,P.jsxs)(`div`,{ref:re,className:`relative inline-block`,style:E.allowsOverflow?{transform:`scale(${E.scale})`,transformOrigin:`top center`}:{},children:[t===`toggle`&&k.baseline?(0,P.jsx)(ja,{baselineImageUrl:k.baseline,currentImageUrl:k.current,baselineImageLoaded:g,setBaselineImageLoaded:_,currentImageLoaded:v,setCurrentImageLoaded:y,baselineImageError:b,setBaselineImageError:x,currentImageError:S,setCurrentImageError:ee,screenshot:A,onImageError:D,onImageLoad:O,imageErrors:f,showBaseline:r,onToggle:i}):t===`onion-skin`&&k.baseline?(0,P.jsx)(ka,{baselineImageUrl:k.baseline,currentImageUrl:k.current,sliderPosition:a,onSliderChange:o,screenshot:A,onImageError:D,onImageLoad:O,imageErrors:f}):(0,P.jsx)(Aa,{baselineImageUrl:k.baseline,currentImageUrl:k.current,diffImageUrl:k.diff,showDiffOverlay:n,screenshot:A,onImageError:D,onImageLoad:O,imageErrors:f,onDiffToggle:s}),d&&e?.confirmedRegions?.length>0&&(0,P.jsx)(Ca,{confirmed:e.confirmedRegions,candidates:[],imageWidth:te.width,imageHeight:te.height,showConfirmed:!0,showCandidates:!1,disabled:!0})]})}),!c&&!m.has(`current-${A?.id}`)&&(0,P.jsx)(`div`,{className:`absolute inset-0 bg-[var(--vz-raised)] animate-pulse flex items-center justify-center`,children:(0,P.jsx)(`div`,{className:`text-[var(--text-tertiary)]`,children:`Loading...`})}),f.has(`current-${A?.id}`)&&(0,P.jsx)(`div`,{className:`absolute inset-0 bg-[var(--vz-raised)] flex items-center justify-center`,children:(0,P.jsxs)(`div`,{className:`text-center text-[var(--text-tertiary)]`,children:[(0,P.jsx)(hn,{className:`w-8 h-8 mx-auto mb-2`}),(0,P.jsx)(`div`,{className:`text-sm`,children:`Failed to load image`})]})})]})})}function G(e){return e?.id||e?.signature||e?.name}function ko(e){switch(e){case`failed`:return`changed`;case`new`:case`baseline-created`:return`new`;case`passed`:return`unchanged`;default:return e}}function Ao(e){return(0,P.jsx)(to,{children:(0,P.jsx)(jo,{...e})})}function jo({comparison:e,comparisons:t=[],onClose:n,onAccept:r,onReject:i,onDelete:a,onNavigate:o}){let[s,c]=(0,N.useState)(So.OVERLAY),[l,u]=(0,N.useState)(!0),[d,f]=(0,N.useState)(50),[p,m]=(0,N.useState)(!0),[h,g]=(0,N.useState)(!1),[_,v]=(0,N.useState)(`needs-review`),[y,b]=(0,N.useState)(!0),[x,S]=(0,N.useState)(!1),{zoom:ee,setZoom:C}=bo(`fit`),{isActive:w}=no(),te=(0,N.useRef)(null),ne=(0,N.useCallback)(()=>{g(e=>!e)},[]),T=(0,N.useMemo)(()=>t.map(e=>({...e,name:e.name||e.originalName||`Unknown`,result:ko(e.status),status:e.status,approval_status:e.userAction===`accepted`?`approved`:e.userAction===`rejected`?`rejected`:e.status===`passed`?`approved`:`pending`,diff_percentage:e.diffPercentage??e.diff_percentage,diffClusters:e.diffClusters||[],viewport_width:e.properties?.viewport_width??e.viewport_width,viewport_height:e.properties?.viewport_height??e.viewport_height,browser:e.properties?.browser??e.browser})),[t]),re=(0,N.useMemo)(()=>{if(!e)return null;let t=e.name||e.originalName;return{name:t,comparisons:T.filter(e=>(e.name||e.originalName)===t)}},[e,T]),E=(0,N.useCallback)(e=>{e&&o&&o(e)},[o]),D=(0,N.useMemo)(()=>{let e={all:()=>!0,"needs-review":e=>(e.result===`changed`||e.result===`new`)&&e.approval_status!==`approved`&&e.approval_status!==`rejected`,changes:e=>e.result===`changed`,new:e=>e.result===`new`},t=e[_]||e.all;return T.filter(t)},[T,_]),O=(0,N.useMemo)(()=>{if(!e)return-1;let t=G(e);return D.findIndex(e=>G(e)===t)},[D,e]),k=(0,N.useMemo)(()=>({prev:O>0,next:O<D.length-1&&O!==-1}),[O,D.length]),A=(0,N.useCallback)(()=>{if(k.prev){let e=D[O-1];o(e)}},[k.prev,D,O,o]),ie=(0,N.useCallback)(()=>{if(k.next){let e=D[O+1];o(e)}},[k.next,D,O,o]),ae=(0,N.useCallback)(()=>O<D.length-1?D[O+1]:O>0?D[O-1]:null,[O,D]),oe=(0,N.useCallback)(()=>{if(!r||!e)return;let t=_===`needs-review`?ae():null;r(G(e)),t?o(t):_===`needs-review`&&D.length<=1&&n()},[r,e,_,ae,D.length,o,n]),se=(0,N.useCallback)(()=>{if(!i||!e)return;let t=_===`needs-review`?ae():null;i(G(e)),t?o(t):_===`needs-review`&&D.length<=1&&n()},[i,e,_,ae,D.length,o,n]),ce=(0,N.useCallback)(()=>{!a||!e||a(G(e))},[a,e]),j=(0,N.useCallback)(()=>{c(e=>e===So.OVERLAY?So.TOGGLE:So.OVERLAY)},[]),le=(0,N.useCallback)(()=>{s===So.TOGGLE?b(e=>!e):u(e=>!e)},[s]);if(ro((0,N.useMemo)(()=>({a:r?oe:void 0,r:i?se:void 0,d:le,t:j,1:()=>c(So.OVERLAY),2:()=>c(So.TOGGLE),3:()=>c(So.ONION)}),[oe,se,r,i,j,le])),(0,N.useEffect)(()=>{let e=e=>{if(!(e.target.tagName===`INPUT`||e.target.tagName===`TEXTAREA`))switch(e.key){case`ArrowUp`:case`ArrowLeft`:e.preventDefault(),k.prev&&A();break;case`ArrowDown`:case`ArrowRight`:e.preventDefault(),k.next&&ie();break;case`Escape`:e.preventDefault(),n();break;case`i`:!e.metaKey&&!e.ctrlKey&&(e.preventDefault(),ne());break;case`g`:!e.metaKey&&!e.ctrlKey&&(e.preventDefault(),S(e=>!e));break}};return window.addEventListener(`keydown`,e),()=>window.removeEventListener(`keydown`,e)},[k,A,ie,n,ne]),(0,N.useEffect)(()=>{if(!p||O<0||O>=D.length)return;let e=te.current;if(!e)return;let t=e.closest(`.overflow-y-auto`);if(!t)return;let n=e.getBoundingClientRect(),r=t.getBoundingClientRect();n.top<r.top?t.scrollBy({top:n.top-r.top-8,behavior:`smooth`}):n.bottom>r.bottom&&t.scrollBy({top:n.bottom-r.bottom+8,behavior:`smooth`})},[O,D.length,p]),!e)return(0,P.jsx)(`div`,{className:`min-h-screen bg-[var(--vz-bg)] flex items-center justify-center`,children:(0,P.jsxs)(`div`,{className:`text-center text-[var(--text-tertiary)]`,children:[(0,P.jsx)(`div`,{className:`text-lg mb-2`,children:`Comparison not found`}),(0,P.jsx)(`button`,{type:`button`,onClick:n,className:`text-[var(--accent-info)] hover:text-[var(--text-primary)]`,children:`Return to list`})]})});let ue=ko(e.status),de=e.userAction===`accepted`?`approved`:e.userAction===`rejected`?`rejected`:e.status===`passed`?`approved`:`pending`,M={new:{variant:`info`,label:`New`},changed:{variant:`warning`,label:`Changed`},unchanged:{variant:`default`,label:`Unchanged`}}[ue],fe=e.status===`new`||e.status===`baseline-created`,pe=e.properties||{};return(0,P.jsxs)(`div`,{className:`h-screen bg-[var(--vz-bg)] flex flex-col overflow-hidden`,"data-testid":`fullscreen-viewer`,children:[(0,P.jsxs)(`header`,{className:`flex-shrink-0 bg-[var(--vz-bg)] border-b border-[var(--vz-border-subtle)] z-30`,children:[(0,P.jsxs)(`div`,{className:`px-2 sm:px-4 py-2 flex items-center gap-1 sm:gap-3`,children:[(0,P.jsx)(qa,{onClick:n}),(0,P.jsxs)(`div`,{className:`flex-1 min-w-0 flex items-center gap-2`,children:[(0,P.jsx)(`h1`,{className:`text-sm font-medium text-[var(--text-primary)] truncate`,children:e.name||e.originalName||`Unknown`}),M&&(0,P.jsx)(nr,{variant:M.variant,size:`sm`,className:`hidden xs:flex`,children:M.label}),w&&(0,P.jsx)(nr,{variant:`warning`,size:`sm`,dot:!0,pulseDot:!0,className:`hidden sm:flex`,children:`Review Mode`})]}),(0,P.jsxs)(`div`,{className:`flex items-center`,children:[(0,P.jsx)(`button`,{type:`button`,onClick:A,disabled:!k.prev,className:`p-2 text-[var(--text-tertiary)] hover:text-[var(--text-primary)] disabled:opacity-85 disabled:cursor-not-allowed rounded-md hover:bg-[var(--vz-raised)] transition-colors`,"aria-label":`Previous`,children:(0,P.jsx)(Kt,{className:`w-5 h-5`})}),(0,P.jsxs)(`span`,{className:`hidden sm:block text-xs text-[var(--text-muted)] font-medium tabular-nums px-1 min-w-[3rem] text-center`,children:[O+1,`/`,D.length]}),(0,P.jsx)(`button`,{type:`button`,onClick:ie,disabled:!k.next,className:`p-2 text-[var(--text-tertiary)] hover:text-[var(--text-primary)] disabled:opacity-85 disabled:cursor-not-allowed rounded-md hover:bg-[var(--vz-raised)] transition-colors`,"aria-label":`Next`,children:(0,P.jsx)(Jt,{className:`w-5 h-5`})})]}),(0,P.jsx)(`div`,{className:`hidden sm:block h-5 w-px bg-[var(--vz-border-subtle)]`}),(0,P.jsxs)(`div`,{className:`hidden sm:flex items-center gap-2`,children:[fe&&a?(0,P.jsx)(`button`,{type:`button`,onClick:ce,className:`px-3 py-1.5 text-xs font-medium text-[var(--accent-danger)] hover:text-[var(--text-primary)] hover:bg-[var(--accent-danger-muted)] rounded-md transition-colors`,"data-testid":`btn-delete`,children:`Delete`}):null,(0,P.jsx)(ba,{status:de,onApprove:r?oe:null,onReject:i?se:null,compact:!0})]}),(0,P.jsxs)(`div`,{className:`hidden sm:flex items-center`,children:[(0,P.jsx)(`button`,{type:`button`,onClick:()=>m(!p),className:`flex items-center justify-center p-2 rounded-md transition-colors ${p?`bg-[var(--accent-brand-muted)] text-[var(--accent-brand)]`:`text-[var(--text-tertiary)] hover:text-[var(--text-primary)] hover:bg-[var(--vz-raised)]`}`,title:`Queue`,"aria-label":`Toggle queue`,"data-testid":`toggle-queue-btn`,children:(0,P.jsx)(Tn,{className:`w-5 h-5 pointer-events-none`})}),(e?.confirmedRegions?.length>0||e?.hasConfirmedRegions)&&(0,P.jsx)(`button`,{type:`button`,onClick:()=>S(!x),className:`p-2 rounded-md transition-colors ${x?`bg-[var(--accent-success-muted)] text-[var(--accent-success)]`:`text-[var(--text-tertiary)] hover:text-[var(--text-primary)] hover:bg-[var(--vz-raised)]`}`,title:`Show Regions (G)`,"aria-label":`Toggle regions`,"data-testid":`toggle-regions-btn`,children:(0,P.jsx)(jn,{className:`w-5 h-5 pointer-events-none`})}),(0,P.jsx)(`button`,{type:`button`,onClick:ne,className:`p-2 rounded-md transition-colors ${h?`bg-[var(--accent-info-muted)] text-[var(--accent-info)]`:`text-[var(--text-tertiary)] hover:text-[var(--text-primary)] hover:bg-[var(--vz-raised)]`}`,title:`Details (I)`,"aria-label":`Screenshot details`,"data-testid":`toggle-inspector-btn`,children:(0,P.jsx)(Cn,{className:`w-5 h-5 pointer-events-none`})})]})]}),(0,P.jsxs)(`div`,{className:`px-2 sm:px-4 py-1.5 sm:py-2 border-t border-[var(--vz-border-subtle)] flex items-center gap-2 sm:gap-3`,children:[re&&re.comparisons.length>0&&(0,P.jsx)(go,{variants:re.comparisons,currentVariantId:G(e),onVariantSelect:E}),(0,P.jsx)(`div`,{className:`flex-1`}),e.diff&&(0,P.jsx)(vo,{value:s,onChange:c,compact:!0,className:`hidden sm:flex`}),(0,P.jsx)(xo,{zoom:ee,onZoomChange:C})]})]}),(0,P.jsxs)(`div`,{className:`flex-1 min-h-0 flex overflow-hidden`,children:[(0,P.jsxs)(Va,{isOpen:p,onClose:()=>m(!1),position:`bottom`,desktopPosition:`left`,width:`w-80`,mobileHeight:`30vh`,expandedHeight:`80vh`,alwaysVisibleOnDesktop:!1,showBackdrop:!1,testId:`queue-drawer`,children:[(0,P.jsx)(Ha,{title:`Queue`,subtitle:`${D.length} of ${T.length} items`,onClose:()=>m(!1)}),(0,P.jsx)(Ga,{children:[{id:`needs-review`,label:`To Review`},{id:`changes`,label:`Changes`},{id:`new`,label:`New`},{id:`all`,label:`All`}].map(e=>(0,P.jsx)(Ka,{isActive:_===e.id,onClick:()=>v(e.id),children:e.label},e.id))}),(0,P.jsxs)(Ua,{className:`px-2 py-2 space-y-1`,children:[D.map(t=>{let n=G(t)===G(e);return(0,P.jsx)(`div`,{ref:n?te:null,children:(0,P.jsx)($a,{item:t,isActive:n,thumbnailUrl:Eo(t.current||t.baseline,t.timestamp),onClick:()=>o(t)})},G(t))}),D.length===0&&(0,P.jsx)(`div`,{className:`text-center py-8 text-[var(--text-muted)] text-xs`,children:_===`needs-review`?`All screenshots reviewed!`:`No items match this filter`})]}),(0,P.jsxs)(Wa,{className:`hidden sm:flex flex-col items-center gap-1.5`,children:[(0,P.jsxs)(`p`,{className:`text-xs text-[var(--text-muted)] text-center`,children:[(0,P.jsx)(`span`,{className:`text-[var(--text-tertiary)]`,children:`↑↓`}),` navigate`,` · `,(0,P.jsx)(`span`,{className:`text-[var(--text-tertiary)]`,children:`Space`}),` `,w?`exit`:`review mode`]}),w&&(0,P.jsxs)(`p`,{className:`text-xs text-[var(--text-muted)] text-center`,children:[(0,P.jsx)(`span`,{className:`text-[var(--accent-success)] font-medium`,children:`A`}),` `,`approve`,` · `,(0,P.jsx)(`span`,{className:`text-[var(--accent-danger)] font-medium`,children:`R`}),` `,`reject`,` · `,(0,P.jsx)(`span`,{className:`text-[var(--text-tertiary)] font-medium`,children:`D`}),` `,`diff`,` · `,(0,P.jsx)(`span`,{className:`text-[var(--text-tertiary)] font-medium`,children:`T`}),` `,`view`]})]})]}),(0,P.jsx)(`main`,{className:`flex-1 min-h-0 overflow-hidden relative`,children:(0,P.jsx)(Oo,{comparison:e,viewMode:s===So.ONION?`onion-skin`:s,showDiffOverlay:l,onDiffToggle:()=>u(e=>!e),showBaseline:y,onToggleBaseline:()=>b(e=>!e),onionSkinPosition:d,onOnionSkinChange:f,zoom:ee,disableLoadingOverlay:!0,showRegions:x,className:`w-full h-full`},G(e))}),(0,P.jsxs)(Za,{isOpen:h,onClose:()=>g(!1),title:`Screenshot Details`,subtitle:e.name||e.originalName,className:`order-last`,children:[(0,P.jsx)(Za.Section,{title:`Status`,icon:sn,children:(0,P.jsxs)(`div`,{className:`flex items-center gap-2 flex-wrap`,children:[M&&(0,P.jsx)(nr,{variant:M.variant,size:`sm`,children:M.label}),(0,P.jsx)(nr,{variant:de===`approved`?`success`:de===`rejected`?`danger`:`default`,size:`sm`,children:de})]})}),e.warnings?.length>0&&(0,P.jsx)(Za.Section,{title:`Warnings`,icon:hn,children:e.warnings.map(e=>(0,P.jsx)(Za.Row,{label:e.option||`Warning`,value:e.message},`${e.code}-${e.option}`))}),(0,P.jsxs)(Za.Section,{title:`Screenshot`,icon:an,children:[pe.browser&&(0,P.jsx)(Za.Row,{label:`Browser`,value:pe.browser,icon:()=>(0,P.jsx)(ir,{browser:pe.browser,className:`w-3.5 h-3.5`})}),(0,P.jsx)(Za.Row,{label:`Viewport`,value:pe.viewport_width&&pe.viewport_height?`${pe.viewport_width} × ${pe.viewport_height}`:null,mono:!0}),(0,P.jsx)(Za.Row,{label:`Device`,value:pe.device})]}),ue===`changed`&&e.diffPercentage!=null&&(0,P.jsxs)(Za.Section,{title:`Diff Analysis`,icon:pn,children:[(0,P.jsx)(Za.Row,{label:`Changed`,value:`${Number(e.diffPercentage).toFixed(3)}%`,mono:!0}),e.diffPixels!=null&&(0,P.jsx)(Za.Row,{label:`Pixels`,value:e.diffPixels.toLocaleString(),mono:!0}),e.threshold!=null&&(0,P.jsx)(Za.Row,{label:`Threshold`,value:Number(e.threshold).toFixed(2),mono:!0})]}),(0,P.jsx)(Za.Section,{title:`Identifier`,icon:sn,children:(0,P.jsx)(Za.Row,{label:`Signature`,value:e.signature||e.id,mono:!0,copyable:!0})})]})]}),(0,P.jsxs)(`div`,{className:`sm:hidden flex-shrink-0 bg-[var(--vz-bg)] border-t border-[var(--vz-border-subtle)] safe-area-pb`,children:[(0,P.jsx)(xa,{status:de,onApprove:r?oe:null,onReject:i?se:null}),(0,P.jsxs)(`div`,{className:`px-3 py-2 flex items-center gap-1 border-t border-[var(--vz-border-subtle)]`,children:[(0,P.jsx)(`button`,{type:`button`,onClick:()=>m(!p),className:`flex items-center justify-center p-2.5 rounded-lg transition-colors ${p?`bg-[var(--accent-brand-muted)] text-[var(--accent-brand)]`:`text-[var(--text-tertiary)] hover:text-[var(--text-primary)] hover:bg-[var(--vz-raised)] active:bg-[var(--vz-elevated)]`}`,"aria-label":`Queue`,"data-testid":`mobile-toggle-queue-btn`,children:(0,P.jsx)(Tn,{className:`w-5 h-5 pointer-events-none`})}),(0,P.jsx)(`button`,{type:`button`,onClick:ne,className:`flex items-center justify-center p-2.5 rounded-lg transition-colors ${h?`bg-[var(--accent-info-muted)] text-[var(--accent-info)]`:`text-[var(--text-tertiary)] hover:text-[var(--text-primary)] hover:bg-[var(--vz-raised)] active:bg-[var(--vz-elevated)]`}`,"aria-label":`Screenshot details`,"data-testid":`mobile-toggle-inspector-btn`,children:(0,P.jsx)(Cn,{className:`w-5 h-5 pointer-events-none`})}),(e?.confirmedRegions?.length>0||e?.hasConfirmedRegions)&&(0,P.jsx)(`button`,{type:`button`,onClick:()=>S(!x),className:`flex items-center justify-center p-2.5 rounded-lg transition-colors ${x?`bg-[var(--accent-success-muted)] text-[var(--accent-success)]`:`text-[var(--text-tertiary)] hover:text-[var(--text-primary)] hover:bg-[var(--vz-raised)] active:bg-[var(--vz-elevated)]`}`,"aria-label":`Toggle regions`,"data-testid":`mobile-toggle-regions-btn`,children:(0,P.jsx)(jn,{className:`w-5 h-5 pointer-events-none`})}),fe&&a&&(0,P.jsx)(`button`,{type:`button`,onClick:ce,className:`flex items-center justify-center p-2.5 rounded-lg text-[var(--accent-danger)] hover:text-[var(--text-primary)] hover:bg-[var(--accent-danger-muted)] active:bg-[var(--accent-danger-muted)] transition-colors`,"aria-label":`Delete`,"data-testid":`mobile-delete-btn`,children:(0,P.jsx)(`span`,{className:`text-xs font-medium`,children:`Delete`})}),(0,P.jsx)(`div`,{className:`flex-1`}),(0,P.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,P.jsxs)(`span`,{className:`text-xs text-[var(--text-muted)] tabular-nums`,children:[O+1,` / `,D.length]}),M&&(0,P.jsx)(nr,{variant:M.variant,size:`sm`,children:M.label})]})]})]})]})}function Mo(){let[,e]=gi(),[,t]=vi(`/comparison/:id`),{data:n}=Ui(),r=Wi(),i=qi(),a=Xi(),o=(0,N.useMemo)(()=>n?.comparisons||[],[n?.comparisons]),s=(0,N.useMemo)(()=>{let e=t?.id?decodeURIComponent(t.id):null;return!e||o.length===0?null:o.find(t=>t.id===e||t.signature===e||t.name===e)},[t,o]),{data:c}=Hi(s?.id),l=(0,N.useMemo)(()=>s?c?{...s,diffClusters:c.diffClusters,confirmedRegions:c.confirmedRegions,intensityStats:c.intensityStats,boundingBox:c.boundingBox,regionAnalysis:c.regionAnalysis,hotspotAnalysis:c.hotspotAnalysis}:s:null,[s,c]),u=(0,N.useCallback)(t=>{let n=t.id||t.signature||t.name;n&&e(`/comparison/${encodeURIComponent(n)}`)},[e]),d=(0,N.useCallback)(()=>{e(`/`)},[e]),f=(0,N.useCallback)(e=>{r.mutate(e)},[r]),p=(0,N.useCallback)(e=>{i.mutate(e)},[i]),m=(0,N.useCallback)(t=>{a.mutate(t,{onSuccess:()=>{e(`/`)}})},[a,e]);return l?(0,P.jsx)(Ao,{comparison:l,comparisons:o,onClose:d,onAccept:f,onReject:p,onDelete:m,onNavigate:u}):(0,P.jsx)(`div`,{className:`fixed inset-0 bg-[var(--vz-bg)] z-50 flex items-center justify-center`,children:(0,P.jsxs)(`div`,{className:`text-center`,children:[(0,P.jsx)(`div`,{className:`text-lg text-[var(--text-primary)] font-medium mb-2`,children:`Comparison not found`}),(0,P.jsxs)(`p`,{className:`text-sm text-[var(--text-tertiary)] mb-4`,children:[`ID: `,t?.id||`none`]}),(0,P.jsx)(`button`,{type:`button`,onClick:d,className:`text-[var(--accent-media)] hover:text-[var(--accent-media)] font-medium transition-colors`,children:`Return to list`})]})})}To.NEW,To.BASELINE_CREATED,To.PASSED,To.FAILED,To.ERROR,To.REJECTED;function No(e,t){return[...e].sort((e,n)=>{if(t===`priority`){let t={[To.FAILED]:3,[To.NEW]:2,[To.PASSED]:1},r=e.initialStatus||e.status,i=n.initialStatus||n.status,a=t[r]||0,o=t[i]||0;return a===o?(n.diffPercentage||0)-(e.diffPercentage||0):o-a}return t===`name`?(e.name||``).localeCompare(n.name||``):t===`time`?(n.timestamp||0)-(e.timestamp||0):0})}function Po(e,t){return t===`failed`?e.filter(e=>e.status===To.FAILED):t===`passed`?e.filter(e=>e.status===To.PASSED):t===`new`?e.filter(e=>e.status===To.NEW||e.status===To.BASELINE_CREATED):e}function Fo(e){return e===`new`||e===`baseline-created`}function Io(e){return e===`failed`||Fo(e)}var Lo=()=>{let e=new URLSearchParams(window.location.search);return{filter:e.get(`filter`)||Co.ALL,sortBy:e.get(`sort`)||wo.PRIORITY,searchQuery:e.get(`search`)||``,selectedBrowser:e.get(`browser`)||`all`,selectedViewport:e.get(`viewport`)||`all`}};function Ro(e=[]){let t=Lo(),[n,r]=(0,N.useState)(t.filter),[i,a]=(0,N.useState)(t.sortBy),[o,s]=(0,N.useState)(t.searchQuery),[c,l]=(0,N.useState)(t.selectedBrowser),[u,d]=(0,N.useState)(t.selectedViewport);(0,N.useEffect)(()=>{let e=new URLSearchParams;n!==Co.ALL&&e.set(`filter`,n),i!==wo.PRIORITY&&e.set(`sort`,i),o&&e.set(`search`,o),c!==`all`&&e.set(`browser`,c),u!==`all`&&e.set(`viewport`,u);let t=e.toString()?`?${e.toString()}`:window.location.pathname;window.history.replaceState({},``,t)},[n,i,o,c,u]);let f=(0,N.useMemo)(()=>{let t=new Set,n=new Set;return e.forEach(e=>{if(e.properties?.browser&&t.add(e.properties.browser),e.properties?.viewport_width&&e.properties?.viewport_height){let t=`${e.properties.viewport_width}x${e.properties.viewport_height}`;n.add(t)}else if(e.properties?.viewport){let t=`${e.properties.viewport.width}x${e.properties.viewport.height}`;n.add(t)}}),{browsers:Array.from(t).sort(),viewports:Array.from(n).sort()}},[e]);return{filteredComparisons:(0,N.useMemo)(()=>{let t=Po(e,n);if(o.trim()){let e=o.toLowerCase();t=t.filter(t=>t.name.toLowerCase().includes(e)||t.originalName?.toLowerCase().includes(e))}return c!==`all`&&(t=t.filter(e=>e.properties?.browser===c)),u!==`all`&&(t=t.filter(e=>{let t=null;return e.properties?.viewport_width&&e.properties?.viewport_height?t=`${e.properties.viewport_width}x${e.properties.viewport_height}`:e.properties?.viewport&&(t=`${e.properties.viewport.width}x${e.properties.viewport.height}`),t===u})),No(t,i)},[e,n,i,o,c,u]),filter:n,setFilter:r,sortBy:i,setSortBy:a,searchQuery:o,setSearchQuery:s,selectedBrowser:c,setSelectedBrowser:l,selectedViewport:u,setSelectedViewport:d,availableFilters:f,counts:{all:e.length,failed:e.filter(e=>e.status===`failed`).length,passed:e.filter(e=>e.status===`passed`).length,new:e.filter(e=>Fo(e.status)).length,rejected:e.filter(e=>e.status===`rejected`).length}}}function zo({src:e,alt:t,className:n,style:r,onClick:i}){let[a,o]=(0,N.useState)(e?`loading`:`missing`);if((0,N.useEffect)(()=>{if(!e){o(`missing`);return}o(`loading`)},[e]),a===`missing`)return(0,P.jsx)(`div`,{className:`flex items-center justify-center bg-[var(--vz-raised)] border border-[var(--vz-border)] rounded min-h-[200px]`,style:r,children:(0,P.jsxs)(`div`,{className:`text-[var(--text-tertiary)] text-center`,children:[(0,P.jsx)(Fn,{className:`w-12 h-12 mx-auto mb-3`}),(0,P.jsxs)(`div`,{className:`text-sm`,children:[`No `,t.toLowerCase(),` available`]})]})});if(a===`error`)return(0,P.jsx)(`div`,{className:`flex items-center justify-center bg-[var(--accent-danger-muted)] border border-[color-mix(in_srgb,var(--accent-danger)_28%,transparent)] rounded min-h-[200px]`,style:r,children:(0,P.jsxs)(`div`,{className:`text-[var(--accent-danger)] text-center`,children:[(0,P.jsx)(hn,{className:`w-12 h-12 mx-auto mb-3`}),(0,P.jsxs)(`div`,{className:`text-sm`,children:[`Failed to load `,t.toLowerCase()]}),(0,P.jsx)(`div`,{className:`text-xs text-[var(--text-muted)] mt-1 font-mono break-all px-4`,children:e})]})});let s=null;return i&&(s=e=>{(e.key===`Enter`||e.key===` `)&&(e.preventDefault(),i(e))}),(0,P.jsx)(`img`,{src:e,alt:t,className:n,style:r,loading:`lazy`,decoding:`async`,onLoad:()=>o(`loaded`),onError:()=>o(`error`),onClick:i,onKeyDown:s,role:i?`button`:void 0,tabIndex:i?0:void 0})}function Bo(e){return e.id||e.signature||e.name}function Vo(e){let t=new Map;for(let n of e){let e=n.name||n.originalName||`Unknown`;t.has(e)||t.set(e,{name:e,comparisons:[],browsers:new Set,viewports:[],hasChanges:!1,hasNew:!1,allPassed:!0,maxDiff:0,failedCount:0,newCount:0,passedCount:0,acceptedCount:0,rejectedCount:0});let r=t.get(e);r.comparisons.push(n),n.properties?.browser&&r.browsers.add(n.properties.browser),n.properties?.viewport_width&&n.properties?.viewport_height&&r.viewports.push({width:n.properties.viewport_width,height:n.properties.viewport_height,id:n.id,status:n.status}),n.status===`failed`?(r.hasChanges=!0,r.allPassed=!1,r.failedCount++,r.maxDiff=Math.max(r.maxDiff,n.diffPercentage||0)):n.status===`new`||n.status===`baseline-created`?(r.hasNew=!0,r.allPassed=!1,r.newCount++):n.status===`passed`&&r.passedCount++,n.userAction===`accepted`?r.acceptedCount++:n.userAction===`rejected`&&r.rejectedCount++}return Array.from(t.values()).map(e=>({...e,browsers:Array.from(e.browsers),viewports:e.viewports.sort((e,t)=>t.width*t.height-e.width*e.height),primary:e.comparisons[0]})).sort((e,t)=>e.hasChanges&&!t.hasChanges?-1:!e.hasChanges&&t.hasChanges?1:e.hasNew&&!t.hasNew?-1:!e.hasNew&&t.hasNew?1:e.maxDiff===t.maxDiff?e.name.localeCompare(t.name):t.maxDiff-e.maxDiff)}function Ho({width:e,className:t=`w-3.5 h-3.5`}){return e<=480?(0,P.jsx)(ln,{className:t}):e<=1024?(0,P.jsx)(dn,{className:t}):(0,P.jsx)(an,{className:t})}function Uo({group:e}){return e.acceptedCount===e.comparisons.length?(0,P.jsxs)(nr,{variant:`success`,size:`sm`,children:[(0,P.jsx)(Bt,{className:`w-3 h-3 mr-1`}),`Accepted`]}):e.rejectedCount>0?(0,P.jsxs)(nr,{variant:`warning`,size:`sm`,children:[(0,P.jsx)(Yn,{className:`w-3 h-3 mr-1`}),`Rejected`]}):e.hasChanges?(0,P.jsxs)(nr,{variant:`danger`,size:`sm`,children:[e.failedCount,` changed`]}):e.hasNew?(0,P.jsxs)(nr,{variant:`info`,size:`sm`,children:[e.newCount,` new`]}):(0,P.jsx)(nr,{variant:`success`,size:`sm`,children:`Passed`})}function Wo({group:e,maxVisible:t=4}){let n=e.comparisons.length;if(n<=1){let t=e.viewports[0],n=e.browsers[0];return(0,P.jsxs)(`div`,{className:`flex items-center gap-2 text-xs text-[var(--text-tertiary)]`,children:[t&&(0,P.jsxs)(`span`,{className:`inline-flex items-center gap-1`,children:[(0,P.jsx)(Ho,{width:t.width,className:`w-3 h-3`}),t.width,`×`,t.height]}),n&&(0,P.jsx)(`span`,{children:n})]})}let r=e.viewports.slice(0,t),i=n-t;return(0,P.jsxs)(`div`,{className:`flex items-center gap-1.5 flex-wrap`,children:[r.map((e,t)=>(0,P.jsxs)(`span`,{className:`
198
198
  inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-medium
199
199
  ${e.status===`failed`?`bg-[var(--accent-danger-muted)] text-[var(--accent-danger)] border border-[color-mix(in_srgb,var(--accent-danger)_32%,transparent)]`:e.status===`new`||e.status===`baseline-created`?`bg-[var(--accent-media-muted)] text-[var(--accent-media)] border border-[color-mix(in_srgb,var(--accent-media)_30%,transparent)]`:`bg-[var(--vz-raised)] text-[var(--text-tertiary)] border border-[var(--vz-border)]`}
200
200
  `,children:[(0,P.jsx)(Ho,{width:e.width,className:`w-2.5 h-2.5`}),e.width]},t)),i>0&&(0,P.jsxs)(`span`,{className:`text-[10px] text-[var(--text-muted)]`,children:[`+`,i,` more`]}),e.browsers.length>0&&(0,P.jsxs)(`span`,{className:`text-[10px] text-[var(--text-muted)] ml-1`,children:[`· `,e.browsers.join(`, `)]})]})}function Go({group:e,onSelect:t,onAcceptGroup:n,onRejectGroup:r,isAccepting:i}){let{primary:a,hasChanges:o,hasNew:s,maxDiff:c}=e,l=o||s,u=Eo(a.current||a.baseline,a.timestamp);return(0,P.jsxs)(`div`,{"data-testid":`screenshot-group-${(a.id||a.signature||e.name).replace(/[^a-zA-Z0-9-]/g,`-`)}`,className:`
package/dist/sdk/index.js CHANGED
@@ -19,7 +19,7 @@ import { createUploader } from '../uploader/index.js';
19
19
  import { loadConfig } from '../utils/config-loader.js';
20
20
  import { resolveImageBuffer } from '../utils/file-helpers.js';
21
21
  import * as output from '../utils/output.js';
22
- import { createScreenshotProperties } from '../utils/screenshot-options.js';
22
+ import { normalizeScreenshotOptions } from '../utils/screenshot-options.js';
23
23
 
24
24
  /**
25
25
  * Create a new Vizzly instance with custom configuration
@@ -293,9 +293,16 @@ export class VizzlySDK extends EventEmitter {
293
293
 
294
294
  // Resolve Buffer or file path using shared utility
295
295
  let buffer = resolveImageBuffer(imageBuffer, 'screenshot');
296
+ let normalizedOptions = normalizeScreenshotOptions(options);
297
+ for (let warning of normalizedOptions.warnings) {
298
+ output.warn(warning.message, {
299
+ code: warning.code,
300
+ option: warning.option
301
+ });
302
+ }
296
303
 
297
304
  // Generate or use provided build ID
298
- let buildId = options.buildId || this.currentBuildId || 'default';
305
+ let buildId = normalizedOptions.buildId || this.currentBuildId || 'default';
299
306
  this.currentBuildId = buildId;
300
307
 
301
308
  // Convert Buffer to base64 for JSON transport
@@ -305,8 +312,11 @@ export class VizzlySDK extends EventEmitter {
305
312
  name,
306
313
  image: imageBase64,
307
314
  type: 'base64',
308
- properties: createScreenshotProperties(options)
315
+ properties: normalizedOptions.properties
309
316
  };
317
+ if (normalizedOptions.warnings.length > 0) {
318
+ screenshotData.warnings = normalizedOptions.warnings;
319
+ }
310
320
 
311
321
  // POST to the local screenshot server
312
322
  let serverUrl = `http://localhost:${this.config.server?.port || 3000}`;
@@ -362,12 +372,16 @@ export class VizzlySDK extends EventEmitter {
362
372
  let screenshotsDir = options.screenshotsDir || this.config?.upload?.screenshotsDir || './screenshots';
363
373
  let uploadOptions = {
364
374
  screenshotsDir,
365
- buildName: options.buildName || this.config.buildName,
366
- branch: options.branch || this.config.branch,
367
- commit: options.commit || this.config.commit,
368
- message: options.message || this.config.message,
369
- environment: options.environment || this.config.environment || 'production',
370
- threshold: options.threshold ?? this.config.threshold,
375
+ buildName: options.buildName || this.config.build?.name,
376
+ branch: options.branch || this.config.build?.branch,
377
+ commit: options.commit || this.config.build?.commit,
378
+ message: options.message || this.config.build?.message,
379
+ environment: options.environment || this.config.build?.environment || 'production',
380
+ threshold: options.threshold ?? this.config.comparison?.threshold,
381
+ minClusterSize: options.minClusterSize ?? this.config.comparison?.minClusterSize,
382
+ metadata: options.metadata || {},
383
+ pullRequestNumber: options.pullRequestNumber,
384
+ parallelId: options.parallelId || this.config.parallelId,
371
385
  onProgress: progress => {
372
386
  this.emit('upload:progress', progress);
373
387
  if (options.onProgress) {
@@ -47,7 +47,7 @@ export const createApiHandler = (client, {
47
47
  let vizzlyDisabled = false;
48
48
  let screenshotCount = 0;
49
49
  let uploadPromises = [];
50
- const handleScreenshot = async (buildId, name, image, properties = {}, type) => {
50
+ const handleScreenshot = async (buildId, name, image, properties = {}, type, warnings = []) => {
51
51
  let handlerStart = Date.now();
52
52
  output.debug('upload', `${name} received`, {
53
53
  buildId: buildId?.slice(0, 8)
@@ -177,7 +177,8 @@ export const createApiHandler = (client, {
177
177
  body: {
178
178
  success: true,
179
179
  name,
180
- count: screenshotCount
180
+ count: screenshotCount,
181
+ warnings
181
182
  }
182
183
  };
183
184
  };
@@ -305,7 +305,7 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
305
305
  output.debug('tdd', `baseline: ${baseline.buildName}`);
306
306
  }
307
307
  };
308
- const handleScreenshot = async (_buildId, name, image, properties = {}, type) => {
308
+ const handleScreenshot = async (_buildId, name, image, properties = {}, type, warnings = []) => {
309
309
  let handlerStart = Date.now();
310
310
  output.debug('tdd', `${name} received`);
311
311
 
@@ -447,6 +447,7 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
447
447
  aaPercentage: comparison.aaPercentage,
448
448
  heightDiff: comparison.heightDiff,
449
449
  error: comparison.error,
450
+ warnings,
450
451
  originalName: name,
451
452
  timestamp: Date.now(),
452
453
  // Boolean hints so UI can show toggle buttons without fetching heavy data
@@ -490,6 +491,7 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
490
491
  diff: comparison.diff,
491
492
  diffPercentage: comparison.diffPercentage,
492
493
  threshold: comparison.threshold,
494
+ warnings,
493
495
  tddMode: true
494
496
  }
495
497
  };
@@ -503,6 +505,7 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
503
505
  message: `Baseline updated for '${name}'`,
504
506
  baseline: comparison.baseline,
505
507
  current: comparison.current,
508
+ warnings,
506
509
  tddMode: true
507
510
  }
508
511
  };
@@ -530,6 +533,7 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
530
533
  name: comparison.name,
531
534
  baseline: comparison.baseline,
532
535
  current: comparison.current,
536
+ warnings,
533
537
  tddMode: true
534
538
  }
535
539
  };
@@ -32,7 +32,8 @@ export function createScreenshotRouter({
32
32
  name,
33
33
  properties,
34
34
  image,
35
- type
35
+ type,
36
+ warnings
36
37
  } = body;
37
38
  if (!name || !image) {
38
39
  sendError(res, 400, 'name and image are required');
@@ -41,7 +42,7 @@ export function createScreenshotRouter({
41
42
 
42
43
  // Use buildId from request body, or fall back to server's buildId
43
44
  const effectiveBuildId = buildId || defaultBuildId;
44
- const result = await screenshotHandler.handleScreenshot(effectiveBuildId, name, image, properties, type);
45
+ const result = await screenshotHandler.handleScreenshot(effectiveBuildId, name, image, properties, type, warnings);
45
46
  sendJson(res, result.statusCode, result.body);
46
47
  return true;
47
48
  } catch (error) {
@@ -57,9 +58,15 @@ export function createScreenshotRouter({
57
58
  // Flush endpoint - signals test completion and prints summary
58
59
  if (pathname === '/flush') {
59
60
  try {
60
- if (screenshotHandler.getResults) {
61
+ if (screenshotHandler.flush) {
62
+ let stats = await screenshotHandler.flush();
63
+ sendJson(res, 200, {
64
+ success: true,
65
+ ...stats
66
+ });
67
+ } else if (screenshotHandler.getResults) {
61
68
  // This triggers printResults() which outputs the summary
62
- const results = await screenshotHandler.getResults();
69
+ let results = await screenshotHandler.getResults();
63
70
  sendJson(res, 200, {
64
71
  success: true,
65
72
  summary: {
@@ -20,9 +20,12 @@ import { randomUUID } from 'node:crypto';
20
20
  export function createBuildObject(buildOptions, deps = {}) {
21
21
  let {
22
22
  name,
23
+ buildName,
23
24
  branch,
24
25
  commit,
26
+ message,
25
27
  environment = 'test',
28
+ parallelId,
26
29
  metadata = {}
27
30
  } = buildOptions;
28
31
  let randomId = deps.randomId || randomUUID;
@@ -30,10 +33,12 @@ export function createBuildObject(buildOptions, deps = {}) {
30
33
  let timestamp = deps.timestamp || Date.now;
31
34
  return {
32
35
  id: `build-${randomId()}`,
33
- name: name || `build-${timestamp()}`,
36
+ name: name || buildName || `build-${timestamp()}`,
34
37
  branch,
35
38
  commit,
39
+ message,
36
40
  environment,
41
+ parallelId,
37
42
  metadata,
38
43
  status: 'pending',
39
44
  createdAt: now(),
@@ -14,6 +14,7 @@
14
14
  * @param {number} options.port - Server port
15
15
  * @param {string} options.buildId - Build ID
16
16
  * @param {boolean} [options.setBaseline] - Whether to set baseline
17
+ * @param {boolean} [options.failOnDiff] - Whether visual diffs should fail tests
17
18
  * @param {Object} [options.baseEnv] - Base environment (defaults to process.env)
18
19
  * @returns {Object} Environment variables object
19
20
  */
@@ -21,6 +22,7 @@ export function buildTestEnv({
21
22
  port,
22
23
  buildId,
23
24
  setBaseline = false,
25
+ failOnDiff = false,
24
26
  baseEnv = process.env
25
27
  }) {
26
28
  return {
@@ -28,7 +30,8 @@ export function buildTestEnv({
28
30
  VIZZLY_SERVER_URL: `http://localhost:${port}`,
29
31
  VIZZLY_BUILD_ID: buildId,
30
32
  VIZZLY_ENABLED: 'true',
31
- VIZZLY_SET_BASELINE: setBaseline ? 'true' : 'false'
33
+ VIZZLY_SET_BASELINE: setBaseline ? 'true' : 'false',
34
+ VIZZLY_FAIL_ON_DIFF: failOnDiff ? 'true' : 'false'
32
35
  };
33
36
  }
34
37
 
@@ -334,7 +334,8 @@ export async function runTests({
334
334
  let env = buildTestEnv({
335
335
  port: config.server?.port,
336
336
  buildId,
337
- setBaseline
337
+ setBaseline,
338
+ failOnDiff: runOptions.failOnDiff || false
338
339
  });
339
340
  try {
340
341
  await executeTestCommand({
@@ -24,6 +24,7 @@ export function autoDiscoverTddServer(
24
24
  deps?: {
25
25
  exists?: (path: string) => boolean;
26
26
  readFile?: (path: string, encoding: BufferEncoding) => string | Buffer;
27
+ env?: Record<string, string | undefined>;
27
28
  }
28
29
  ): string | null;
29
30
 
@@ -32,7 +33,14 @@ export function autoDiscoverTddServer(
32
33
  */
33
34
  export interface ScreenshotResult {
34
35
  success: boolean;
35
- status?: 'passed' | 'failed' | 'new';
36
+ status?:
37
+ | 'passed'
38
+ | 'failed'
39
+ | 'new'
40
+ | 'match'
41
+ | 'diff'
42
+ | 'baseline-updated'
43
+ | 'error';
36
44
  name?: string;
37
45
  diffPercentage?: number;
38
46
  [key: string]: unknown;
@@ -59,10 +67,16 @@ export interface ScreenshotResult {
59
67
  * @example
60
68
  * // With properties and comparison settings
61
69
  * await vizzlyScreenshot('checkout-form', screenshot, {
62
- * properties: { browser: 'chrome', viewport: '1920x1080' },
70
+ * properties: { browser: 'chrome', viewport: { width: 1920, height: 1080 } },
63
71
  * threshold: 5,
64
- * minClusterSize: 10
72
+ * minClusterSize: 10,
73
+ * fullPage: true,
74
+ * requestTimeout: 5000
65
75
  * });
76
+ *
77
+ * `properties` is the user metadata bag. Comparison options are normalized
78
+ * into the server metadata payload, while `requestTimeout` stays on the
79
+ * client request and `buildId` only routes the screenshot to a build.
66
80
  */
67
81
  export function vizzlyScreenshot(
68
82
  name: string,
@@ -72,7 +86,10 @@ export function vizzlyScreenshot(
72
86
  threshold?: number;
73
87
  minClusterSize?: number;
74
88
  fullPage?: boolean;
75
- [key: string]: unknown;
89
+ /** Transport-only build ID used to route the request. */
90
+ buildId?: string;
91
+ /** Client-side HTTP timeout in milliseconds; not stored as metadata. */
92
+ requestTimeout?: number;
76
93
  }
77
94
  ): Promise<ScreenshotResult | null>;
78
95
 
@@ -81,7 +98,15 @@ export function vizzlyScreenshot(
81
98
  */
82
99
  export interface FlushResult {
83
100
  success: boolean;
84
- summary: {
101
+ uploaded?: number;
102
+ flushed?: boolean;
103
+ total?: number;
104
+ passed?: number;
105
+ failed?: number;
106
+ new?: number;
107
+ errors?: number;
108
+ message?: string;
109
+ summary?: {
85
110
  total: number;
86
111
  passed: number;
87
112
  failed: number;
@@ -124,6 +149,7 @@ export function isVizzlyReady(): boolean;
124
149
  export function configure(config?: {
125
150
  serverUrl?: string;
126
151
  enabled?: boolean;
152
+ failOnDiff?: boolean;
127
153
  }): void;
128
154
 
129
155
  /**
@@ -145,4 +171,5 @@ export function getVizzlyInfo(): {
145
171
  buildId: string | null;
146
172
  tddMode: boolean;
147
173
  disabled: boolean;
174
+ failOnDiff: boolean;
148
175
  };