@cat-factory/app 0.137.2 → 0.138.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.
@@ -10,9 +10,9 @@
10
10
  import { computed, onUnmounted, reactive, ref, watch } from 'vue'
11
11
  import type { VisualConfirmStepState } from '~/types/execution'
12
12
  import { useArtifactBlobs } from '~/composables/useArtifactBlobs'
13
- import { useFocusTrap } from '~/composables/useFocusTrap'
14
13
  import ImageCompare from '~/components/media/ImageCompare.vue'
15
14
  import ArtifactLightbox from '~/components/media/ArtifactLightbox.vue'
15
+ import ResultWindowShell from '~/components/panels/ResultWindowShell.vue'
16
16
  import StepRunMeta from '~/components/panels/StepRunMeta.vue'
17
17
 
18
18
  const board = useBoardStore()
@@ -26,8 +26,17 @@ const access = useWorkspaceAccess()
26
26
  const blobs = useArtifactBlobs()
27
27
  onUnmounted(() => blobs.revokeAll())
28
28
 
29
- const { open, blockId, instanceId, stepIndex, close } = useResultView('visual-confirm')
29
+ // `manageEscape: false` `ResultWindowShell` owns Escape (and the focus trap + scroll lock +
30
+ // stacking); the nested lightbox layers above it on the same shared overlay stack.
31
+ const { open, blockId, instanceId, stepIndex, close } = useResultView('visual-confirm', {
32
+ manageEscape: false,
33
+ })
30
34
  const block = computed(() => (blockId.value ? board.getBlock(blockId.value) : undefined))
35
+ const headerTitle = computed(() =>
36
+ block.value
37
+ ? t('visualConfirm.titleWithBlock', { title: block.value.title })
38
+ : t('visualConfirm.title'),
39
+ )
31
40
 
32
41
  const instance = computed(() =>
33
42
  instanceId.value === null ? null : (execution.getInstance(instanceId.value) ?? null),
@@ -95,14 +104,6 @@ function expand(artifactId: string) {
95
104
  lightboxOpen.value = true
96
105
  }
97
106
 
98
- // Focus management for the modal panel. While the lightbox is open it owns the trap, so the
99
- // window hands off (active = open && !lightbox) to avoid two Tab traps fighting.
100
- const dialogRoot = ref<HTMLElement | null>(null)
101
- useFocusTrap(
102
- dialogRoot,
103
- computed(() => open.value && !lightboxOpen.value),
104
- )
105
-
106
107
  // --- Request a fix: per-view notes + a freeform box, composed into one findings string. ---
107
108
  const perViewNotes = reactive<Record<string, string>>({})
108
109
  const noteOpen = reactive<Record<string, boolean>>({})
@@ -187,278 +188,239 @@ const canApprove = computed(
187
188
  </script>
188
189
 
189
190
  <template>
190
- <Teleport to="body">
191
- <div
192
- v-if="open"
193
- class="fixed inset-0 z-50 flex max-h-[100dvh] items-stretch justify-center bg-slate-950/70 backdrop-blur-sm"
194
- @click.self="close"
195
- >
191
+ <ResultWindowShell
192
+ :open="open"
193
+ icon="i-lucide-image-play"
194
+ icon-class="bg-amber-500/15 text-amber-300"
195
+ :title="headerTitle"
196
+ :subtitle="phase ? PHASE_LABEL[phase] : t('visualConfirm.subtitle')"
197
+ width="5xl"
198
+ @close="close"
199
+ >
200
+ <div class="flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto px-5 py-4">
196
201
  <div
197
- ref="dialogRoot"
198
- tabindex="-1"
199
- role="dialog"
200
- aria-modal="true"
201
- :aria-label="t('visualConfirm.ariaLabel')"
202
- class="m-4 flex w-full max-w-5xl flex-col overflow-hidden rounded-2xl border border-slate-800 bg-slate-900 shadow-2xl focus:outline-none"
202
+ v-if="!vc"
203
+ class="flex flex-col items-center justify-center gap-2 py-10 text-center text-slate-400"
203
204
  >
204
- <header class="flex items-center gap-3 border-b border-slate-800 px-5 py-3">
205
- <span
206
- class="flex h-8 w-8 items-center justify-center rounded-lg bg-amber-500/15 text-amber-300"
207
- >
208
- <UIcon name="i-lucide-image-play" class="h-4 w-4" />
209
- </span>
210
- <div class="min-w-0 flex-1">
211
- <h2 class="truncate text-sm font-semibold text-slate-100">
212
- {{
213
- block
214
- ? t('visualConfirm.titleWithBlock', { title: block.title })
215
- : t('visualConfirm.title')
216
- }}
217
- </h2>
218
- <p class="truncate text-[11px] text-slate-400">
219
- {{ phase ? PHASE_LABEL[phase] : t('visualConfirm.subtitle') }}
220
- </p>
221
- </div>
222
- <button
223
- class="rounded-md p-1.5 text-slate-400 hover:bg-slate-800 hover:text-slate-200"
224
- @click="close"
225
- >
226
- <UIcon name="i-lucide-x" class="h-4 w-4" />
227
- </button>
228
- </header>
229
-
230
- <div class="flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto px-5 py-4">
231
- <div
232
- v-if="!vc"
233
- class="flex flex-col items-center justify-center gap-2 py-10 text-center text-slate-400"
234
- >
235
- <UIcon name="i-lucide-image-play" class="h-8 w-8 opacity-40" />
236
- <p class="text-sm">{{ t('visualConfirm.notStarted') }}</p>
237
- </div>
205
+ <UIcon name="i-lucide-image-play" class="h-8 w-8 opacity-40" />
206
+ <p class="text-sm">{{ t('visualConfirm.notStarted') }}</p>
207
+ </div>
238
208
 
239
- <template v-else>
240
- <p
241
- v-if="vc.degradedReason"
242
- class="rounded-lg border border-amber-700/40 bg-amber-500/5 px-3 py-2 text-[12px] text-amber-300/90"
243
- >
244
- {{ vc.degradedReason }}
245
- </p>
209
+ <template v-else>
210
+ <p
211
+ v-if="vc.degradedReason"
212
+ class="rounded-lg border border-amber-700/40 bg-amber-500/5 px-3 py-2 text-[12px] text-amber-300/90"
213
+ >
214
+ {{ vc.degradedReason }}
215
+ </p>
246
216
 
247
- <p
248
- v-if="working"
249
- class="flex items-center gap-2 rounded-lg border border-slate-800 bg-slate-950/40 px-3 py-2 text-[12px] text-slate-300"
250
- >
251
- <UIcon name="i-lucide-loader" class="h-3.5 w-3.5 animate-spin text-amber-300" />
252
- {{ phase ? PHASE_LABEL[phase] : '' }}
253
- </p>
217
+ <p
218
+ v-if="working"
219
+ class="flex items-center gap-2 rounded-lg border border-slate-800 bg-slate-950/40 px-3 py-2 text-[12px] text-slate-300"
220
+ >
221
+ <UIcon name="i-lucide-loader" class="h-3.5 w-3.5 animate-spin text-amber-300" />
222
+ {{ phase ? PHASE_LABEL[phase] : '' }}
223
+ </p>
254
224
 
255
- <!-- Actual-vs-reference gallery. Keyed by `view` (the contract's unique per-pair
225
+ <!-- Actual-vs-reference gallery. Keyed by `view` (the contract's unique per-pair
256
226
  identity) so a pair's note/expand state stays bound to its view across recaptures. -->
257
- <section v-if="pairs.length" class="space-y-4">
258
- <div v-for="p in pairs" :key="p.view" class="space-y-2">
259
- <ImageCompare
260
- :view="p.view"
261
- :actual-id="p.actualArtifactId"
262
- :reference-id="p.referenceArtifactId"
263
- :blobs="blobs"
264
- :busy="busy"
265
- @expand="expand"
266
- @upload-reference="(file: File) => uploadFor(p.view, file)"
267
- />
268
- <!-- Per-view note (folded into the fixer findings) -->
269
- <div v-if="awaitingHuman" class="px-1">
270
- <button
271
- class="flex items-center gap-1.5 text-[11px] text-slate-400 hover:text-slate-200"
272
- @click="noteOpen[p.view] = !noteOpen[p.view]"
273
- >
274
- <UIcon
275
- :name="noteOpen[p.view] ? 'i-lucide-chevron-down' : 'i-lucide-chevron-right'"
276
- class="h-3 w-3"
277
- />
278
- {{ t('visualConfirm.noteIssue', { view: p.view }) }}
279
- <span
280
- v-if="perViewNotes[p.view]?.trim()"
281
- class="rounded-full bg-amber-500/15 px-1.5 text-[9px] text-amber-300"
282
- >{{ t('visualConfirm.noted') }}</span
283
- >
284
- </button>
285
- <textarea
286
- v-if="noteOpen[p.view]"
287
- v-model="perViewNotes[p.view]"
288
- rows="2"
289
- :placeholder="t('visualConfirm.notePlaceholder', { view: p.view })"
290
- class="mt-1 w-full rounded-md border border-slate-700 bg-slate-950 px-2 py-1.5 text-[12px] text-slate-200 placeholder:text-slate-600 focus:border-amber-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-amber-500/60"
291
- />
292
- </div>
293
- </div>
294
- </section>
295
- <p v-else class="text-[12px] italic text-slate-500">
296
- {{ t('visualConfirm.noScreenshots') }}
297
- </p>
298
-
299
- <!-- Upload a reference for any view -->
300
- <section class="rounded-lg border border-slate-800 bg-slate-900/60 p-3">
301
- <h3 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
302
- {{ t('visualConfirm.upload.heading') }}
303
- </h3>
304
- <div class="flex flex-wrap items-center gap-2">
305
- <input
306
- v-model="uploadView"
307
- list="vc-views"
308
- :placeholder="t('visualConfirm.upload.viewPlaceholder')"
309
- class="rounded-md border border-slate-700 bg-slate-950 px-2 py-1 text-[12px] text-slate-200 placeholder:text-slate-600"
310
- />
311
- <datalist id="vc-views">
312
- <option v-for="p in pairs" :key="p.view" :value="p.view" />
313
- </datalist>
314
- <input
315
- ref="fileInput"
316
- type="file"
317
- accept="image/png,image/jpeg"
318
- :disabled="busy || !uploadView.trim()"
319
- class="text-[12px] text-slate-300 file:me-2 file:rounded file:border-0 file:bg-slate-800 file:px-2 file:py-1 file:text-slate-200 disabled:opacity-40"
320
- @change="onFilePicked"
227
+ <section v-if="pairs.length" class="space-y-4">
228
+ <div v-for="p in pairs" :key="p.view" class="space-y-2">
229
+ <ImageCompare
230
+ :view="p.view"
231
+ :actual-id="p.actualArtifactId"
232
+ :reference-id="p.referenceArtifactId"
233
+ :blobs="blobs"
234
+ :busy="busy"
235
+ @expand="expand"
236
+ @upload-reference="(file: File) => uploadFor(p.view, file)"
237
+ />
238
+ <!-- Per-view note (folded into the fixer findings) -->
239
+ <div v-if="awaitingHuman" class="px-1">
240
+ <button
241
+ class="flex items-center gap-1.5 text-[11px] text-slate-400 hover:text-slate-200"
242
+ @click="noteOpen[p.view] = !noteOpen[p.view]"
243
+ >
244
+ <UIcon
245
+ :name="noteOpen[p.view] ? 'i-lucide-chevron-down' : 'i-lucide-chevron-right'"
246
+ class="h-3 w-3"
321
247
  />
322
- </div>
323
- <p class="mt-1.5 text-[10px] text-slate-600">
324
- {{
325
- uploadView.trim()
326
- ? t('visualConfirm.upload.tipReady')
327
- : t('visualConfirm.upload.tipNeedView')
328
- }}
329
- </p>
330
- </section>
331
-
332
- <!-- Request fix -->
333
- <section
334
- v-if="awaitingHuman"
335
- class="rounded-lg border border-slate-800 bg-slate-900/60 p-3"
336
- >
337
- <h3 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
338
- {{ t('visualConfirm.requestFix.heading') }}
339
- </h3>
248
+ {{ t('visualConfirm.noteIssue', { view: p.view }) }}
249
+ <span
250
+ v-if="perViewNotes[p.view]?.trim()"
251
+ class="rounded-full bg-amber-500/15 px-1.5 text-[9px] text-amber-300"
252
+ >{{ t('visualConfirm.noted') }}</span
253
+ >
254
+ </button>
340
255
  <textarea
341
- v-model="globalFindings"
342
- rows="3"
343
- :placeholder="t('visualConfirm.requestFix.placeholder')"
344
- class="w-full rounded-md border border-slate-700 bg-slate-950 px-3 py-2 text-[13px] text-slate-200 placeholder:text-slate-600 focus:border-amber-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-amber-500/60"
256
+ v-if="noteOpen[p.view]"
257
+ v-model="perViewNotes[p.view]"
258
+ rows="2"
259
+ :placeholder="t('visualConfirm.notePlaceholder', { view: p.view })"
260
+ class="mt-1 w-full rounded-md border border-slate-700 bg-slate-950 px-2 py-1.5 text-[12px] text-slate-200 placeholder:text-slate-600 focus:border-amber-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-amber-500/60"
345
261
  />
346
- <div class="mt-2 flex items-center justify-between">
347
- <span class="text-[11px] text-slate-500">
348
- {{ t('visualConfirm.requestFix.foldedHint') }}
349
- </span>
350
- <UButton
351
- size="sm"
352
- color="warning"
353
- icon="i-lucide-wrench"
354
- :loading="busy"
355
- :disabled="busy || !hasFindings || !access.canExecuteRuns.value"
356
- :title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
357
- @click="submitFix"
358
- >
359
- {{ t('visualConfirm.requestFix.send') }}
360
- </UButton>
361
- </div>
362
- </section>
262
+ </div>
263
+ </div>
264
+ </section>
265
+ <p v-else class="text-[12px] italic text-slate-500">
266
+ {{ t('visualConfirm.noScreenshots') }}
267
+ </p>
363
268
 
364
- <!-- Rounds history -->
365
- <section
366
- v-if="vc.rounds && vc.rounds.length"
367
- class="rounded-lg border border-slate-800 bg-slate-900/60 p-3"
368
- >
369
- <h3 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
370
- {{ t('visualConfirm.history.heading', { count: vc.attempts }, vc.attempts) }}
371
- </h3>
372
- <ol class="space-y-2">
373
- <li v-for="(r, i) in vc.rounds" :key="i" class="flex items-start gap-2 text-[12px]">
374
- <UIcon
375
- name="i-lucide-wrench"
376
- class="mt-0.5 h-3.5 w-3.5 shrink-0 text-slate-400"
377
- />
378
- <div class="min-w-0 flex-1">
379
- <span class="text-slate-200">{{
380
- t('visualConfirm.history.fixRequested')
381
- }}</span>
382
- <span
383
- class="ms-1.5 rounded px-1 text-[10px] uppercase"
384
- :class="
385
- r.outcome === 'completed'
386
- ? 'bg-emerald-500/15 text-emerald-300'
387
- : r.outcome === 'failed'
388
- ? 'bg-rose-500/15 text-rose-300'
389
- : 'bg-slate-500/15 text-slate-300'
390
- "
391
- >
392
- {{
393
- r.outcome
394
- ? OUTCOME_LABELS[r.outcome]
395
- : t('visualConfirm.outcome.inProgress')
396
- }}
397
- </span>
398
- <p v-if="r.findings" class="whitespace-pre-wrap leading-snug text-slate-400">
399
- {{ r.findings }}
400
- </p>
401
- </div>
402
- </li>
403
- </ol>
404
- </section>
405
- </template>
406
- </div>
269
+ <!-- Upload a reference for any view -->
270
+ <section class="rounded-lg border border-slate-800 bg-slate-900/60 p-3">
271
+ <h3 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
272
+ {{ t('visualConfirm.upload.heading') }}
273
+ </h3>
274
+ <div class="flex flex-wrap items-center gap-2">
275
+ <input
276
+ v-model="uploadView"
277
+ list="vc-views"
278
+ :placeholder="t('visualConfirm.upload.viewPlaceholder')"
279
+ class="rounded-md border border-slate-700 bg-slate-950 px-2 py-1 text-[12px] text-slate-200 placeholder:text-slate-600"
280
+ />
281
+ <datalist id="vc-views">
282
+ <option v-for="p in pairs" :key="p.view" :value="p.view" />
283
+ </datalist>
284
+ <input
285
+ ref="fileInput"
286
+ type="file"
287
+ accept="image/png,image/jpeg"
288
+ :disabled="busy || !uploadView.trim()"
289
+ class="text-[12px] text-slate-300 file:me-2 file:rounded file:border-0 file:bg-slate-800 file:px-2 file:py-1 file:text-slate-200 disabled:opacity-40"
290
+ @change="onFilePicked"
291
+ />
292
+ </div>
293
+ <p class="mt-1.5 text-[10px] text-slate-600">
294
+ {{
295
+ uploadView.trim()
296
+ ? t('visualConfirm.upload.tipReady')
297
+ : t('visualConfirm.upload.tipNeedView')
298
+ }}
299
+ </p>
300
+ </section>
407
301
 
408
- <footer
409
- v-if="vc"
410
- class="flex items-center justify-between gap-3 border-t border-slate-800 px-5 py-3"
302
+ <!-- Request fix -->
303
+ <section
304
+ v-if="awaitingHuman"
305
+ class="rounded-lg border border-slate-800 bg-slate-900/60 p-3"
411
306
  >
412
- <StepRunMeta
413
- v-if="step"
414
- :step="step"
415
- :instance-id="instanceId ?? undefined"
416
- :step-number="stepIndex === null ? undefined : stepIndex + 1"
417
- :total-steps="instance?.steps.length"
418
- :run-failed="instance?.status === 'failed'"
419
- :failure-at="instance?.failure?.occurredAt"
307
+ <h3 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
308
+ {{ t('visualConfirm.requestFix.heading') }}
309
+ </h3>
310
+ <textarea
311
+ v-model="globalFindings"
312
+ rows="3"
313
+ :placeholder="t('visualConfirm.requestFix.placeholder')"
314
+ class="w-full rounded-md border border-slate-700 bg-slate-950 px-3 py-2 text-[13px] text-slate-200 placeholder:text-slate-600 focus:border-amber-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-amber-500/60"
420
315
  />
421
- <div class="flex items-center gap-2">
422
- <label
423
- v-if="awaitingHuman && needsAck"
424
- class="flex items-center gap-1.5 text-[11px] text-amber-300/90"
425
- >
426
- <input v-model="ackDegraded" type="checkbox" class="accent-amber-500" />
427
- {{ t('visualConfirm.reviewedManually') }}
428
- </label>
316
+ <div class="mt-2 flex items-center justify-between">
317
+ <span class="text-[11px] text-slate-500">
318
+ {{ t('visualConfirm.requestFix.foldedHint') }}
319
+ </span>
429
320
  <UButton
430
321
  size="sm"
431
- variant="soft"
432
- color="neutral"
433
- icon="i-lucide-refresh-cw"
322
+ color="warning"
323
+ icon="i-lucide-wrench"
434
324
  :loading="busy"
435
- :disabled="busy || !awaitingHuman || !access.canExecuteRuns.value"
325
+ :disabled="busy || !hasFindings || !access.canExecuteRuns.value"
436
326
  :title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
437
- @click="recapture"
327
+ @click="submitFix"
438
328
  >
439
- {{ t('visualConfirm.recapture') }}
440
- </UButton>
441
- <UButton
442
- color="primary"
443
- icon="i-lucide-circle-check"
444
- :loading="busy"
445
- :disabled="!canApprove || !access.canExecuteRuns.value"
446
- :title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
447
- @click="approve"
448
- >
449
- {{ t('visualConfirm.approve') }}
329
+ {{ t('visualConfirm.requestFix.send') }}
450
330
  </UButton>
451
331
  </div>
452
- </footer>
453
- </div>
332
+ </section>
333
+
334
+ <!-- Rounds history -->
335
+ <section
336
+ v-if="vc.rounds && vc.rounds.length"
337
+ class="rounded-lg border border-slate-800 bg-slate-900/60 p-3"
338
+ >
339
+ <h3 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
340
+ {{ t('visualConfirm.history.heading', { count: vc.attempts }, vc.attempts) }}
341
+ </h3>
342
+ <ol class="space-y-2">
343
+ <li v-for="(r, i) in vc.rounds" :key="i" class="flex items-start gap-2 text-[12px]">
344
+ <UIcon name="i-lucide-wrench" class="mt-0.5 h-3.5 w-3.5 shrink-0 text-slate-400" />
345
+ <div class="min-w-0 flex-1">
346
+ <span class="text-slate-200">{{ t('visualConfirm.history.fixRequested') }}</span>
347
+ <span
348
+ class="ms-1.5 rounded px-1 text-[10px] uppercase"
349
+ :class="
350
+ r.outcome === 'completed'
351
+ ? 'bg-emerald-500/15 text-emerald-300'
352
+ : r.outcome === 'failed'
353
+ ? 'bg-rose-500/15 text-rose-300'
354
+ : 'bg-slate-500/15 text-slate-300'
355
+ "
356
+ >
357
+ {{
358
+ r.outcome ? OUTCOME_LABELS[r.outcome] : t('visualConfirm.outcome.inProgress')
359
+ }}
360
+ </span>
361
+ <p v-if="r.findings" class="whitespace-pre-wrap leading-snug text-slate-400">
362
+ {{ r.findings }}
363
+ </p>
364
+ </div>
365
+ </li>
366
+ </ol>
367
+ </section>
368
+ </template>
454
369
  </div>
455
370
 
456
- <!-- Shared zoom/pan viewer for any screenshot in the gallery. -->
457
- <ArtifactLightbox
458
- v-model:open="lightboxOpen"
459
- v-model:index="lightboxIndex"
460
- :items="lightboxItems"
461
- :blobs="blobs"
462
- />
463
- </Teleport>
371
+ <footer
372
+ v-if="vc"
373
+ class="flex items-center justify-between gap-3 border-t border-slate-800 px-5 py-3"
374
+ >
375
+ <StepRunMeta
376
+ v-if="step"
377
+ :step="step"
378
+ :instance-id="instanceId ?? undefined"
379
+ :step-number="stepIndex === null ? undefined : stepIndex + 1"
380
+ :total-steps="instance?.steps.length"
381
+ :run-failed="instance?.status === 'failed'"
382
+ :failure-at="instance?.failure?.occurredAt"
383
+ />
384
+ <div class="flex items-center gap-2">
385
+ <label
386
+ v-if="awaitingHuman && needsAck"
387
+ class="flex items-center gap-1.5 text-[11px] text-amber-300/90"
388
+ >
389
+ <input v-model="ackDegraded" type="checkbox" class="accent-amber-500" />
390
+ {{ t('visualConfirm.reviewedManually') }}
391
+ </label>
392
+ <UButton
393
+ size="sm"
394
+ variant="soft"
395
+ color="neutral"
396
+ icon="i-lucide-refresh-cw"
397
+ :loading="busy"
398
+ :disabled="busy || !awaitingHuman || !access.canExecuteRuns.value"
399
+ :title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
400
+ @click="recapture"
401
+ >
402
+ {{ t('visualConfirm.recapture') }}
403
+ </UButton>
404
+ <UButton
405
+ color="primary"
406
+ icon="i-lucide-circle-check"
407
+ :loading="busy"
408
+ :disabled="!canApprove || !access.canExecuteRuns.value"
409
+ :title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
410
+ @click="approve"
411
+ >
412
+ {{ t('visualConfirm.approve') }}
413
+ </UButton>
414
+ </div>
415
+ </footer>
416
+ </ResultWindowShell>
417
+
418
+ <!-- Shared zoom/pan viewer for any screenshot in the gallery — a sibling overlay that layers
419
+ above this window on the shared modal stack while open. -->
420
+ <ArtifactLightbox
421
+ v-model:open="lightboxOpen"
422
+ v-model:index="lightboxIndex"
423
+ :items="lightboxItems"
424
+ :blobs="blobs"
425
+ />
464
426
  </template>
@@ -4445,7 +4445,6 @@
4445
4445
  "footer": "Szenarien sind die Bereiche, die der Tester zum Prüfen gewählt hat (seine Akzeptanzszenarien aus der Spezifikation). Ergebnisse und Bedenken werden nach Name darunter gruppiert."
4446
4446
  },
4447
4447
  "visualConfirm": {
4448
- "ariaLabel": "Visuelle Bestätigung",
4449
4448
  "title": "Visuelle Bestätigung",
4450
4449
  "titleWithBlock": "Visuelle Bestätigung — {title}",
4451
4450
  "subtitle": "Die UI mit den Referenzdesigns vergleichen",
@@ -4281,7 +4281,6 @@
4281
4281
  }
4282
4282
  },
4283
4283
  "visualConfirm": {
4284
- "ariaLabel": "Visual confirmation",
4285
4284
  "title": "Visual confirmation",
4286
4285
  "titleWithBlock": "Visual confirmation — {title}",
4287
4286
  "subtitle": "Review the UI against the reference designs",
@@ -4111,7 +4111,6 @@
4111
4111
  }
4112
4112
  },
4113
4113
  "visualConfirm": {
4114
- "ariaLabel": "Confirmación visual",
4115
4114
  "title": "Confirmación visual",
4116
4115
  "titleWithBlock": "Confirmación visual — {title}",
4117
4116
  "subtitle": "Revisa la interfaz frente a los diseños de referencia",
@@ -4111,7 +4111,6 @@
4111
4111
  }
4112
4112
  },
4113
4113
  "visualConfirm": {
4114
- "ariaLabel": "Confirmation visuelle",
4115
4114
  "title": "Confirmation visuelle",
4116
4115
  "titleWithBlock": "Confirmation visuelle — {title}",
4117
4116
  "subtitle": "Comparez l'interface aux maquettes de référence",
@@ -4122,7 +4122,6 @@
4122
4122
  "footer": "תרחישים הם התחומים שהבודק בחר לבחון (תרחישי קבלת המפרט שלו). תוצאות וחששות מקובצים תחתם לפי שם."
4123
4123
  },
4124
4124
  "visualConfirm": {
4125
- "ariaLabel": "אישור חזותי",
4126
4125
  "title": "אישור חזותי",
4127
4126
  "titleWithBlock": "אישור חזותי — {title}",
4128
4127
  "subtitle": "סקור את הממשק מול עיצובי הייחוס",
@@ -4445,7 +4445,6 @@
4445
4445
  "footer": "Gli scenari sono le aree che il Tester ha scelto di verificare (i suoi scenari di accettazione della specifica). Esiti e rilievi sono raggruppati sotto di essi per nome."
4446
4446
  },
4447
4447
  "visualConfirm": {
4448
- "ariaLabel": "Conferma visiva",
4449
4448
  "title": "Conferma visiva",
4450
4449
  "titleWithBlock": "Conferma visiva — {title}",
4451
4450
  "subtitle": "Confronta l'interfaccia con i design di riferimento",
@@ -4123,7 +4123,6 @@
4123
4123
  "footer": "シナリオはTesterが検証対象に選んだ領域(スペックの受け入れシナリオ)です。結果と懸念は名前ごとにその下にグループ化されます。"
4124
4124
  },
4125
4125
  "visualConfirm": {
4126
- "ariaLabel": "ビジュアル確認",
4127
4126
  "title": "ビジュアル確認",
4128
4127
  "titleWithBlock": "ビジュアル確認 — {title}",
4129
4128
  "subtitle": "リファレンスデザインと照らしてUIをレビュー",
@@ -4111,7 +4111,6 @@
4111
4111
  }
4112
4112
  },
4113
4113
  "visualConfirm": {
4114
- "ariaLabel": "Potwierdzenie wizualne",
4115
4114
  "title": "Potwierdzenie wizualne",
4116
4115
  "titleWithBlock": "Potwierdzenie wizualne — {title}",
4117
4116
  "subtitle": "Porównaj interfejs z projektami referencyjnymi",
@@ -4123,7 +4123,6 @@
4123
4123
  "footer": "Senaryolar, Tester'ın test etmek için seçtiği alanlardır (spesifikasyon kabul senaryoları). Sonuçlar ve endişeler ada göre bunların altında gruplanır."
4124
4124
  },
4125
4125
  "visualConfirm": {
4126
- "ariaLabel": "Görsel doğrulama",
4127
4126
  "title": "Görsel doğrulama",
4128
4127
  "titleWithBlock": "Görsel doğrulama — {title}",
4129
4128
  "subtitle": "Arayüzü referans tasarımlarla karşılaştırarak inceleyin",
@@ -4111,7 +4111,6 @@
4111
4111
  }
4112
4112
  },
4113
4113
  "visualConfirm": {
4114
- "ariaLabel": "Візуальне підтвердження",
4115
4114
  "title": "Візуальне підтвердження",
4116
4115
  "titleWithBlock": "Візуальне підтвердження — {title}",
4117
4116
  "subtitle": "Порівняйте інтерфейс з еталонними дизайнами",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cat-factory/app",
3
- "version": "0.137.2",
3
+ "version": "0.138.0",
4
4
  "description": "Reusable Nuxt layer for the Agent Architecture Board SPA (components, stores, composables, pages). Consume it from a thin deployment app via `extends: ['@cat-factory/app']` and point it at your backend with NUXT_PUBLIC_API_BASE. See deploy/frontend for an example.",
5
5
  "repository": {
6
6
  "type": "git",