@cat-factory/app 0.137.1 → 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>
@@ -44,6 +44,24 @@ describe('inspector panel group', () => {
44
44
  ])
45
45
  })
46
46
 
47
+ it('a document frame hides the test-infra / test-credentials / release-health panels', () => {
48
+ // A doc repo stands up no test env and ships no release, so those panels don't apply.
49
+ expect(visibleIds(block('frame', 'document'))).toEqual([
50
+ 'container-summary',
51
+ 'service-fragments',
52
+ ])
53
+ })
54
+
55
+ it('a library frame still shows the test/deploy panels (only document is excluded)', () => {
56
+ expect(visibleIds(block('frame', 'library'))).toEqual([
57
+ 'container-summary',
58
+ 'service-test-config',
59
+ 'service-test-secrets',
60
+ 'service-fragments',
61
+ 'service-release-health',
62
+ ])
63
+ })
64
+
47
65
  it('a frontend frame swaps connections for frontend-config', () => {
48
66
  expect(visibleIds(block('frame', 'frontend'))).toEqual([
49
67
  'container-summary',
@@ -75,6 +75,14 @@ const isTask = (b: Block) => b.level === 'task'
75
75
  const isFrame = (b: Block) => b.level === 'frame'
76
76
  /** frame OR module — the "container" panels. */
77
77
  const isContainer = (b: Block) => b.level === 'frame' || b.level === 'module'
78
+ /**
79
+ * A frame that gets built, tested and deployed as a running thing — i.e. every frame
80
+ * EXCEPT a `document` repo. A document repo (`FrameRepoType` `document`) only runs
81
+ * doc/spike tasks + doc pipelines: it stands up no test environment, holds no test
82
+ * credentials and never ships a release to watch, so the test-infrastructure /
83
+ * test-credentials / post-release-health panels don't apply to it and are hidden.
84
+ */
85
+ const isDeployableFrame = (b: Block) => isFrame(b) && b.type !== 'document'
78
86
 
79
87
  /**
80
88
  * The built-in panel specs. The `when` predicates and orders reproduce
@@ -83,7 +91,9 @@ const isContainer = (b: Block) => b.level === 'frame' || b.level === 'module'
83
91
  * - task body: context docs/issues, then schedule → execution → estimate →
84
92
  * dependencies → run-settings → agent-config → structure.
85
93
  * - service/module body: container summary (frame|module), then the frame-only
86
- * panels, with frontend-config / service-connections further gated on `type`.
94
+ * panels, with frontend-config / service-connections further gated on `type`,
95
+ * and the test-infrastructure / test-credentials / post-release-health panels
96
+ * hidden for a `document` frame (a doc repo has no test env / release to config).
87
97
  * - epic / initiative bodies: their single inspector.
88
98
  *
89
99
  * The frame-only "view requirements" button and the cross-cutting run banners
@@ -104,10 +114,10 @@ export const INSPECTOR_PANEL_SPECS: readonly InspectorPanelSpec[] = [
104
114
  { id: 'container-summary', order: 110, when: isContainer },
105
115
  { id: 'frontend-config', order: 120, when: (b) => isFrame(b) && b.type === 'frontend' },
106
116
  { id: 'service-connections', order: 130, when: (b) => isFrame(b) && b.type === 'service' },
107
- { id: 'service-test-config', order: 140, when: isFrame },
108
- { id: 'service-test-secrets', order: 150, when: isFrame },
117
+ { id: 'service-test-config', order: 140, when: isDeployableFrame },
118
+ { id: 'service-test-secrets', order: 150, when: isDeployableFrame },
109
119
  { id: 'service-fragments', order: 160, when: isFrame },
110
- { id: 'service-release-health', order: 170, when: isFrame },
120
+ { id: 'service-release-health', order: 170, when: isDeployableFrame },
111
121
  { id: 'epic-children', order: 200, when: (b) => b.level === 'epic' },
112
122
  { id: 'initiative-inspector', order: 210, when: (b) => b.level === 'initiative' },
113
123
  ]
@@ -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",