@cat-factory/app 0.137.2 → 0.138.1
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.
- package/app/components/documents/TaskContextDocs.vue +1 -0
- package/app/components/followUp/FollowUpWindow.vue +174 -200
- package/app/components/forkDecision/ForkDecisionWindow.vue +250 -275
- package/app/components/gates/GateResultView.vue +309 -333
- package/app/components/humanTest/HumanTestWindow.vue +212 -236
- package/app/components/media/ArtifactLightbox.vue +20 -19
- package/app/components/panels/GenericStructuredResultView.vue +60 -88
- package/app/components/ralph/RalphLoopResultView.vue +150 -171
- package/app/components/testing/TestReportWindow.vue +505 -559
- package/app/components/visualConfirm/VisualConfirmationWindow.vue +224 -262
- package/i18n/locales/de.json +0 -1
- package/i18n/locales/en.json +0 -1
- package/i18n/locales/es.json +0 -1
- package/i18n/locales/fr.json +0 -1
- package/i18n/locales/he.json +0 -1
- package/i18n/locales/it.json +0 -1
- package/i18n/locales/ja.json +0 -1
- package/i18n/locales/pl.json +0 -1
- package/i18n/locales/tr.json +0 -1
- package/i18n/locales/uk.json +0 -1
- package/package.json +1 -1
- package/app/composables/useFocusTrap.ts +0 -72
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
// The round outcome union (contracts state it inline, so derive it off the round type).
|
|
15
15
|
type HumanTestRoundOutcome = NonNullable<HumanTestRound['outcome']>
|
|
16
16
|
import StepRunMeta from '~/components/panels/StepRunMeta.vue'
|
|
17
|
+
import ResultWindowShell from '~/components/panels/ResultWindowShell.vue'
|
|
17
18
|
|
|
18
19
|
const board = useBoardStore()
|
|
19
20
|
const execution = useExecutionStore()
|
|
@@ -23,10 +24,16 @@ const toast = useToast()
|
|
|
23
24
|
const access = useWorkspaceAccess()
|
|
24
25
|
const { confirmAction, toastDone } = useConfirmAction()
|
|
25
26
|
|
|
26
|
-
// Shared seam contract (open/blockId/close
|
|
27
|
-
//
|
|
28
|
-
|
|
27
|
+
// Shared seam contract (open/blockId/close). No `onOpen` loader: the gate state rides on the
|
|
28
|
+
// execution step, pushed over the stream. `manageEscape: false` — `ResultWindowShell` owns
|
|
29
|
+
// Escape (and focus trap + scroll lock + stacking).
|
|
30
|
+
const { open, blockId, instanceId, stepIndex, close } = useResultView('human-test', {
|
|
31
|
+
manageEscape: false,
|
|
32
|
+
})
|
|
29
33
|
const block = computed(() => (blockId.value ? board.getBlock(blockId.value) : undefined))
|
|
34
|
+
const headerTitle = computed(() =>
|
|
35
|
+
block.value ? t('humanTest.titleWithBlock', { title: block.value.title }) : t('humanTest.title'),
|
|
36
|
+
)
|
|
30
37
|
|
|
31
38
|
const instance = computed(() =>
|
|
32
39
|
instanceId.value === null ? null : (execution.getInstance(instanceId.value) ?? null),
|
|
@@ -135,250 +142,219 @@ const canDestroy = computed(
|
|
|
135
142
|
</script>
|
|
136
143
|
|
|
137
144
|
<template>
|
|
138
|
-
<
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
145
|
+
<ResultWindowShell
|
|
146
|
+
:open="open"
|
|
147
|
+
icon="i-lucide-user-check"
|
|
148
|
+
icon-class="bg-amber-500/15 text-amber-300"
|
|
149
|
+
:title="headerTitle"
|
|
150
|
+
:subtitle="phase ? t(PHASE_LABEL[phase]) : t('humanTest.subtitle')"
|
|
151
|
+
width="3xl"
|
|
152
|
+
@close="close"
|
|
153
|
+
>
|
|
154
|
+
<div class="flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto px-5 py-4">
|
|
144
155
|
<div
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
aria-modal="true"
|
|
156
|
+
v-if="!ht"
|
|
157
|
+
class="flex flex-col items-center justify-center gap-2 py-10 text-center text-slate-400"
|
|
148
158
|
>
|
|
149
|
-
|
|
150
|
-
<
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
<
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
159
|
+
<UIcon name="i-lucide-user-check" class="h-8 w-8 opacity-40" />
|
|
160
|
+
<p class="text-sm">{{ t('humanTest.notStarted') }}</p>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
<template v-else>
|
|
164
|
+
<!-- Environment -->
|
|
165
|
+
<section class="rounded-lg border border-slate-800 bg-slate-900/60 p-4">
|
|
166
|
+
<h3 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
167
|
+
{{ t('humanTest.environment.heading') }}
|
|
168
|
+
</h3>
|
|
169
|
+
<div v-if="env" class="space-y-2">
|
|
170
|
+
<div class="flex items-center gap-2 text-[13px]">
|
|
171
|
+
<UIcon
|
|
172
|
+
name="i-lucide-circle-dot"
|
|
173
|
+
class="h-3.5 w-3.5"
|
|
174
|
+
:class="ENV_STATUS_COLOR[env.status]"
|
|
175
|
+
/>
|
|
176
|
+
<span :class="ENV_STATUS_COLOR[env.status]">{{
|
|
177
|
+
t(ENV_STATUS_LABEL[env.status])
|
|
178
|
+
}}</span>
|
|
179
|
+
</div>
|
|
180
|
+
<a
|
|
181
|
+
v-if="env.url"
|
|
182
|
+
:href="env.url"
|
|
183
|
+
target="_blank"
|
|
184
|
+
rel="noopener"
|
|
185
|
+
class="inline-flex items-center gap-1.5 break-all text-[13px] text-sky-300 hover:underline"
|
|
186
|
+
>
|
|
187
|
+
<UIcon name="i-lucide-external-link" class="h-3.5 w-3.5 shrink-0" />
|
|
188
|
+
{{ env.url }}
|
|
189
|
+
</a>
|
|
190
|
+
<p v-else class="text-[12px] italic text-slate-500">
|
|
191
|
+
{{ t('humanTest.environment.noUrl') }}
|
|
192
|
+
</p>
|
|
193
|
+
<p v-if="env.expiresAt" class="text-[11px] text-slate-500">
|
|
194
|
+
{{ t('humanTest.environment.expires', { date: d(new Date(env.expiresAt), 'long') }) }}
|
|
164
195
|
</p>
|
|
165
196
|
</div>
|
|
166
|
-
<
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
>
|
|
170
|
-
|
|
171
|
-
</
|
|
172
|
-
</header>
|
|
197
|
+
<p v-else class="text-[12px] text-amber-300/90">
|
|
198
|
+
{{ ht.degradedReason ?? t('humanTest.environment.none') }}
|
|
199
|
+
</p>
|
|
200
|
+
<p v-if="env && ht.degradedReason" class="mt-2 text-[12px] text-amber-300/90">
|
|
201
|
+
{{ ht.degradedReason }}
|
|
202
|
+
</p>
|
|
173
203
|
|
|
174
|
-
|
|
175
|
-
<div
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
204
|
+
<!-- Env management -->
|
|
205
|
+
<div class="mt-3 flex flex-wrap gap-2">
|
|
206
|
+
<UButton
|
|
207
|
+
size="xs"
|
|
208
|
+
variant="soft"
|
|
209
|
+
color="neutral"
|
|
210
|
+
icon="i-lucide-refresh-cw"
|
|
211
|
+
:loading="busy"
|
|
212
|
+
:disabled="busy || !canManageEnv || !access.canExecuteRuns.value"
|
|
213
|
+
:title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
|
|
214
|
+
@click="recreate"
|
|
215
|
+
>
|
|
216
|
+
{{ t('humanTest.actions.recreate') }}
|
|
217
|
+
</UButton>
|
|
218
|
+
<UButton
|
|
219
|
+
size="xs"
|
|
220
|
+
variant="soft"
|
|
221
|
+
color="neutral"
|
|
222
|
+
icon="i-lucide-trash-2"
|
|
223
|
+
:disabled="busy || !canDestroy || !access.canExecuteRuns.value"
|
|
224
|
+
:title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
|
|
225
|
+
@click="destroy"
|
|
226
|
+
>
|
|
227
|
+
{{ t('humanTest.actions.destroy') }}
|
|
228
|
+
</UButton>
|
|
229
|
+
<UButton
|
|
230
|
+
size="xs"
|
|
231
|
+
variant="soft"
|
|
232
|
+
color="neutral"
|
|
233
|
+
icon="i-lucide-git-merge"
|
|
234
|
+
:loading="busy"
|
|
235
|
+
:disabled="busy || !canManageEnv || !access.canExecuteRuns.value"
|
|
236
|
+
:title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
|
|
237
|
+
@click="pullMain"
|
|
238
|
+
>
|
|
239
|
+
{{ t('humanTest.actions.pullMain') }}
|
|
240
|
+
</UButton>
|
|
181
241
|
</div>
|
|
242
|
+
</section>
|
|
182
243
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
<UIcon
|
|
192
|
-
name="i-lucide-circle-dot"
|
|
193
|
-
class="h-3.5 w-3.5"
|
|
194
|
-
:class="ENV_STATUS_COLOR[env.status]"
|
|
195
|
-
/>
|
|
196
|
-
<span :class="ENV_STATUS_COLOR[env.status]">{{
|
|
197
|
-
t(ENV_STATUS_LABEL[env.status])
|
|
198
|
-
}}</span>
|
|
199
|
-
</div>
|
|
200
|
-
<a
|
|
201
|
-
v-if="env.url"
|
|
202
|
-
:href="env.url"
|
|
203
|
-
target="_blank"
|
|
204
|
-
rel="noopener"
|
|
205
|
-
class="inline-flex items-center gap-1.5 break-all text-[13px] text-sky-300 hover:underline"
|
|
206
|
-
>
|
|
207
|
-
<UIcon name="i-lucide-external-link" class="h-3.5 w-3.5 shrink-0" />
|
|
208
|
-
{{ env.url }}
|
|
209
|
-
</a>
|
|
210
|
-
<p v-else class="text-[12px] italic text-slate-500">
|
|
211
|
-
{{ t('humanTest.environment.noUrl') }}
|
|
212
|
-
</p>
|
|
213
|
-
<p v-if="env.expiresAt" class="text-[11px] text-slate-500">
|
|
214
|
-
{{
|
|
215
|
-
t('humanTest.environment.expires', { date: d(new Date(env.expiresAt), 'long') })
|
|
216
|
-
}}
|
|
217
|
-
</p>
|
|
218
|
-
</div>
|
|
219
|
-
<p v-else class="text-[12px] text-amber-300/90">
|
|
220
|
-
{{ ht.degradedReason ?? t('humanTest.environment.none') }}
|
|
221
|
-
</p>
|
|
222
|
-
<p v-if="env && ht.degradedReason" class="mt-2 text-[12px] text-amber-300/90">
|
|
223
|
-
{{ ht.degradedReason }}
|
|
224
|
-
</p>
|
|
225
|
-
|
|
226
|
-
<!-- Env management -->
|
|
227
|
-
<div class="mt-3 flex flex-wrap gap-2">
|
|
228
|
-
<UButton
|
|
229
|
-
size="xs"
|
|
230
|
-
variant="soft"
|
|
231
|
-
color="neutral"
|
|
232
|
-
icon="i-lucide-refresh-cw"
|
|
233
|
-
:loading="busy"
|
|
234
|
-
:disabled="busy || !canManageEnv || !access.canExecuteRuns.value"
|
|
235
|
-
:title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
|
|
236
|
-
@click="recreate"
|
|
237
|
-
>
|
|
238
|
-
{{ t('humanTest.actions.recreate') }}
|
|
239
|
-
</UButton>
|
|
240
|
-
<UButton
|
|
241
|
-
size="xs"
|
|
242
|
-
variant="soft"
|
|
243
|
-
color="neutral"
|
|
244
|
-
icon="i-lucide-trash-2"
|
|
245
|
-
:disabled="busy || !canDestroy || !access.canExecuteRuns.value"
|
|
246
|
-
:title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
|
|
247
|
-
@click="destroy"
|
|
248
|
-
>
|
|
249
|
-
{{ t('humanTest.actions.destroy') }}
|
|
250
|
-
</UButton>
|
|
251
|
-
<UButton
|
|
252
|
-
size="xs"
|
|
253
|
-
variant="soft"
|
|
254
|
-
color="neutral"
|
|
255
|
-
icon="i-lucide-git-merge"
|
|
256
|
-
:loading="busy"
|
|
257
|
-
:disabled="busy || !canManageEnv || !access.canExecuteRuns.value"
|
|
258
|
-
:title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
|
|
259
|
-
@click="pullMain"
|
|
260
|
-
>
|
|
261
|
-
{{ t('humanTest.actions.pullMain') }}
|
|
262
|
-
</UButton>
|
|
263
|
-
</div>
|
|
264
|
-
</section>
|
|
265
|
-
|
|
266
|
-
<!-- Working state -->
|
|
267
|
-
<p
|
|
268
|
-
v-if="working"
|
|
269
|
-
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"
|
|
270
|
-
>
|
|
271
|
-
<UIcon name="i-lucide-loader" class="h-3.5 w-3.5 animate-spin text-amber-300" />
|
|
272
|
-
{{ phase ? t(PHASE_LABEL[phase]) : '' }}
|
|
273
|
-
</p>
|
|
244
|
+
<!-- Working state -->
|
|
245
|
+
<p
|
|
246
|
+
v-if="working"
|
|
247
|
+
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"
|
|
248
|
+
>
|
|
249
|
+
<UIcon name="i-lucide-loader" class="h-3.5 w-3.5 animate-spin text-amber-300" />
|
|
250
|
+
{{ phase ? t(PHASE_LABEL[phase]) : '' }}
|
|
251
|
+
</p>
|
|
274
252
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
253
|
+
<!-- Findings / fix -->
|
|
254
|
+
<section
|
|
255
|
+
v-if="awaitingHuman"
|
|
256
|
+
class="rounded-lg border border-slate-800 bg-slate-900/60 p-4"
|
|
257
|
+
>
|
|
258
|
+
<div class="flex items-center justify-between">
|
|
259
|
+
<h3 class="text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
260
|
+
{{ t('humanTest.fix.heading') }}
|
|
261
|
+
</h3>
|
|
262
|
+
<button
|
|
263
|
+
class="text-[12px] text-slate-400 hover:text-slate-200"
|
|
264
|
+
@click="showFindings = !showFindings"
|
|
279
265
|
>
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
<UButton
|
|
299
|
-
size="sm"
|
|
300
|
-
color="warning"
|
|
301
|
-
icon="i-lucide-wrench"
|
|
302
|
-
:loading="busy"
|
|
303
|
-
:disabled="busy || !findings.trim() || !access.canExecuteRuns.value"
|
|
304
|
-
:title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
|
|
305
|
-
@click="submitFix"
|
|
306
|
-
>
|
|
307
|
-
{{ t('humanTest.fix.send') }}
|
|
308
|
-
</UButton>
|
|
309
|
-
</div>
|
|
310
|
-
</section>
|
|
311
|
-
|
|
312
|
-
<!-- Rounds history -->
|
|
313
|
-
<section
|
|
314
|
-
v-if="ht.rounds && ht.rounds.length"
|
|
315
|
-
class="rounded-lg border border-slate-800 bg-slate-900/60 p-4"
|
|
266
|
+
{{ showFindings ? t('humanTest.fix.cancel') : t('humanTest.fix.requestFix') }}
|
|
267
|
+
</button>
|
|
268
|
+
</div>
|
|
269
|
+
<div v-if="showFindings" class="mt-2 space-y-2">
|
|
270
|
+
<textarea
|
|
271
|
+
v-model="findings"
|
|
272
|
+
rows="4"
|
|
273
|
+
:placeholder="t('humanTest.fix.placeholder')"
|
|
274
|
+
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"
|
|
275
|
+
/>
|
|
276
|
+
<UButton
|
|
277
|
+
size="sm"
|
|
278
|
+
color="warning"
|
|
279
|
+
icon="i-lucide-wrench"
|
|
280
|
+
:loading="busy"
|
|
281
|
+
:disabled="busy || !findings.trim() || !access.canExecuteRuns.value"
|
|
282
|
+
:title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
|
|
283
|
+
@click="submitFix"
|
|
316
284
|
>
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
<li v-for="(r, i) in ht.rounds" :key="i" class="flex items-start gap-2 text-[12px]">
|
|
322
|
-
<UIcon
|
|
323
|
-
:name="r.kind === 'fix' ? 'i-lucide-wrench' : 'i-lucide-git-merge'"
|
|
324
|
-
class="mt-0.5 h-3.5 w-3.5 shrink-0 text-slate-400"
|
|
325
|
-
/>
|
|
326
|
-
<div class="min-w-0 flex-1">
|
|
327
|
-
<span class="text-slate-200">{{
|
|
328
|
-
r.kind === 'fix'
|
|
329
|
-
? t('humanTest.history.fixRequested')
|
|
330
|
-
: t('humanTest.history.pulledMain')
|
|
331
|
-
}}</span>
|
|
332
|
-
<span
|
|
333
|
-
class="ms-1.5 rounded px-1 text-[10px] uppercase"
|
|
334
|
-
:class="
|
|
335
|
-
r.outcome === 'completed'
|
|
336
|
-
? 'bg-emerald-500/15 text-emerald-300'
|
|
337
|
-
: r.outcome === 'failed'
|
|
338
|
-
? 'bg-rose-500/15 text-rose-300'
|
|
339
|
-
: 'bg-slate-500/15 text-slate-300'
|
|
340
|
-
"
|
|
341
|
-
>
|
|
342
|
-
{{
|
|
343
|
-
r.outcome
|
|
344
|
-
? t(ROUND_OUTCOME_LABEL[r.outcome])
|
|
345
|
-
: t('humanTest.roundOutcome.inProgress')
|
|
346
|
-
}}
|
|
347
|
-
</span>
|
|
348
|
-
<p v-if="r.findings" class="leading-snug text-slate-400">{{ r.findings }}</p>
|
|
349
|
-
</div>
|
|
350
|
-
</li>
|
|
351
|
-
</ol>
|
|
352
|
-
</section>
|
|
353
|
-
</template>
|
|
354
|
-
</div>
|
|
285
|
+
{{ t('humanTest.fix.send') }}
|
|
286
|
+
</UButton>
|
|
287
|
+
</div>
|
|
288
|
+
</section>
|
|
355
289
|
|
|
356
|
-
<!--
|
|
357
|
-
<
|
|
358
|
-
v-if="ht"
|
|
359
|
-
class="
|
|
290
|
+
<!-- Rounds history -->
|
|
291
|
+
<section
|
|
292
|
+
v-if="ht.rounds && ht.rounds.length"
|
|
293
|
+
class="rounded-lg border border-slate-800 bg-slate-900/60 p-4"
|
|
360
294
|
>
|
|
361
|
-
<
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
295
|
+
<h3 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
296
|
+
{{ t('humanTest.history.heading', { count: ht.attempts }, ht.attempts) }}
|
|
297
|
+
</h3>
|
|
298
|
+
<ol class="space-y-2">
|
|
299
|
+
<li v-for="(r, i) in ht.rounds" :key="i" class="flex items-start gap-2 text-[12px]">
|
|
300
|
+
<UIcon
|
|
301
|
+
:name="r.kind === 'fix' ? 'i-lucide-wrench' : 'i-lucide-git-merge'"
|
|
302
|
+
class="mt-0.5 h-3.5 w-3.5 shrink-0 text-slate-400"
|
|
303
|
+
/>
|
|
304
|
+
<div class="min-w-0 flex-1">
|
|
305
|
+
<span class="text-slate-200">{{
|
|
306
|
+
r.kind === 'fix'
|
|
307
|
+
? t('humanTest.history.fixRequested')
|
|
308
|
+
: t('humanTest.history.pulledMain')
|
|
309
|
+
}}</span>
|
|
310
|
+
<span
|
|
311
|
+
class="ms-1.5 rounded px-1 text-[10px] uppercase"
|
|
312
|
+
:class="
|
|
313
|
+
r.outcome === 'completed'
|
|
314
|
+
? 'bg-emerald-500/15 text-emerald-300'
|
|
315
|
+
: r.outcome === 'failed'
|
|
316
|
+
? 'bg-rose-500/15 text-rose-300'
|
|
317
|
+
: 'bg-slate-500/15 text-slate-300'
|
|
318
|
+
"
|
|
319
|
+
>
|
|
320
|
+
{{
|
|
321
|
+
r.outcome
|
|
322
|
+
? t(ROUND_OUTCOME_LABEL[r.outcome])
|
|
323
|
+
: t('humanTest.roundOutcome.inProgress')
|
|
324
|
+
}}
|
|
325
|
+
</span>
|
|
326
|
+
<p v-if="r.findings" class="leading-snug text-slate-400">{{ r.findings }}</p>
|
|
327
|
+
</div>
|
|
328
|
+
</li>
|
|
329
|
+
</ol>
|
|
330
|
+
</section>
|
|
331
|
+
</template>
|
|
382
332
|
</div>
|
|
383
|
-
|
|
333
|
+
|
|
334
|
+
<!-- Footer: the primary confirm action -->
|
|
335
|
+
<footer
|
|
336
|
+
v-if="ht"
|
|
337
|
+
class="flex items-center justify-between gap-3 border-t border-slate-800 px-5 py-3"
|
|
338
|
+
>
|
|
339
|
+
<StepRunMeta
|
|
340
|
+
v-if="step"
|
|
341
|
+
:step="step"
|
|
342
|
+
:instance-id="instanceId ?? undefined"
|
|
343
|
+
:step-number="stepIndex === null ? undefined : stepIndex + 1"
|
|
344
|
+
:total-steps="instance?.steps.length"
|
|
345
|
+
:run-failed="instance?.status === 'failed'"
|
|
346
|
+
:failure-at="instance?.failure?.occurredAt"
|
|
347
|
+
/>
|
|
348
|
+
<UButton
|
|
349
|
+
color="primary"
|
|
350
|
+
icon="i-lucide-circle-check"
|
|
351
|
+
:loading="busy"
|
|
352
|
+
:disabled="busy || !awaitingHuman || !access.canExecuteRuns.value"
|
|
353
|
+
:title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
|
|
354
|
+
@click="confirm"
|
|
355
|
+
>
|
|
356
|
+
{{ t('humanTest.confirm') }}
|
|
357
|
+
</UButton>
|
|
358
|
+
</footer>
|
|
359
|
+
</ResultWindowShell>
|
|
384
360
|
</template>
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
// Zoom is pure CSS `transform` (GPU, no canvas), so even large PNGs stay smooth. Keyboard:
|
|
8
8
|
// Esc close · ←/→ prev/next · +/- zoom · 0 reset · double-click toggle fit↔2×.
|
|
9
9
|
import { computed, ref, watch } from 'vue'
|
|
10
|
+
import { useModalBehavior } from '@modular-vue/core'
|
|
10
11
|
import type { ArtifactBlobs } from '~/composables/useArtifactBlobs'
|
|
11
|
-
import { useFocusTrap } from '~/composables/useFocusTrap'
|
|
12
12
|
|
|
13
13
|
interface LightboxItem {
|
|
14
14
|
artifactId: string
|
|
@@ -36,13 +36,19 @@ const scale = ref(1)
|
|
|
36
36
|
const tx = ref(0)
|
|
37
37
|
const ty = ref(0)
|
|
38
38
|
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
)
|
|
39
|
+
// Modal behaviour via the shared overlay stack (`@modular-vue/core`, the slice-5 release):
|
|
40
|
+
// focus-trap + return, body-scroll lock, and — because it pushes onto the shared stack while
|
|
41
|
+
// open — it becomes the TOP overlay over an owning review window (`ResultWindowShell`), so
|
|
42
|
+
// Escape closes the lightbox first and the owner's trap goes inert until it closes. This is
|
|
43
|
+
// the reconciliation that replaces the old `active: open && !lightboxOpen` guard the windows
|
|
44
|
+
// used to fight over Tab.
|
|
45
|
+
function close() {
|
|
46
|
+
emit('update:open', false)
|
|
47
|
+
}
|
|
48
|
+
const { dialogRef, isTop } = useModalBehavior({
|
|
49
|
+
active: () => props.open,
|
|
50
|
+
onClose: close,
|
|
51
|
+
})
|
|
46
52
|
|
|
47
53
|
const current = computed(() => props.items[props.index] ?? null)
|
|
48
54
|
const total = computed(() => props.items.length)
|
|
@@ -74,9 +80,6 @@ watch(
|
|
|
74
80
|
{ immediate: true },
|
|
75
81
|
)
|
|
76
82
|
|
|
77
|
-
function close() {
|
|
78
|
-
emit('update:open', false)
|
|
79
|
-
}
|
|
80
83
|
function go(delta: number) {
|
|
81
84
|
if (!total.value) return
|
|
82
85
|
const next = (props.index + delta + total.value) % total.value
|
|
@@ -122,13 +125,12 @@ function onPointerUp() {
|
|
|
122
125
|
dragging.value = false
|
|
123
126
|
}
|
|
124
127
|
|
|
128
|
+
// Escape / Tab / focus are owned by `useModalBehavior` (the shared stack). This handler owns
|
|
129
|
+
// only the lightbox-specific navigation + zoom keys, gated on being the top overlay so it
|
|
130
|
+
// stays inert if another overlay ever layers above it.
|
|
125
131
|
function onKey(e: KeyboardEvent) {
|
|
126
|
-
if (!props.open) return
|
|
132
|
+
if (!props.open || !isTop.value) return
|
|
127
133
|
switch (e.key) {
|
|
128
|
-
case 'Escape':
|
|
129
|
-
e.stopPropagation()
|
|
130
|
-
close()
|
|
131
|
-
break
|
|
132
134
|
case 'ArrowLeft':
|
|
133
135
|
go(-1)
|
|
134
136
|
break
|
|
@@ -148,8 +150,6 @@ function onKey(e: KeyboardEvent) {
|
|
|
148
150
|
break
|
|
149
151
|
}
|
|
150
152
|
}
|
|
151
|
-
// Capture-phase so Esc closes the lightbox BEFORE the underlying window's own Esc handler
|
|
152
|
-
// (both use window keydown; the lightbox is the topmost surface so it wins).
|
|
153
153
|
onMounted(() => window.addEventListener('keydown', onKey, true))
|
|
154
154
|
onBeforeUnmount(() => window.removeEventListener('keydown', onKey, true))
|
|
155
155
|
</script>
|
|
@@ -158,11 +158,12 @@ onBeforeUnmount(() => window.removeEventListener('keydown', onKey, true))
|
|
|
158
158
|
<Teleport to="body">
|
|
159
159
|
<div
|
|
160
160
|
v-if="open"
|
|
161
|
-
ref="
|
|
161
|
+
ref="dialogRef"
|
|
162
162
|
tabindex="-1"
|
|
163
163
|
class="fixed inset-0 z-[60] flex flex-col bg-slate-950/95 backdrop-blur-sm focus:outline-none"
|
|
164
164
|
role="dialog"
|
|
165
165
|
aria-modal="true"
|
|
166
|
+
data-testid="artifact-lightbox"
|
|
166
167
|
:aria-label="
|
|
167
168
|
current
|
|
168
169
|
? t('media.lightbox.ariaLabel', { label: current.label })
|