@cat-factory/app 0.136.1 → 0.137.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.
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { computed } from 'vue'
|
|
10
10
|
import type { MergeAxis, MergeDecision } from '@cat-factory/contracts'
|
|
11
11
|
import StepRunMeta from '~/components/panels/StepRunMeta.vue'
|
|
12
|
-
import
|
|
12
|
+
import ResultWindowShell from '~/components/panels/ResultWindowShell.vue'
|
|
13
13
|
import MarkdownProse from '~/components/common/MarkdownProse.vue'
|
|
14
14
|
|
|
15
15
|
const board = useBoardStore()
|
|
@@ -17,9 +17,12 @@ const execution = useExecutionStore()
|
|
|
17
17
|
const agents = useAgentsStore()
|
|
18
18
|
const { t, n } = useI18n()
|
|
19
19
|
|
|
20
|
-
// Shared seam contract (open/blockId/close
|
|
21
|
-
//
|
|
22
|
-
|
|
20
|
+
// Shared seam contract (open/blockId/close). No loader: the verdict is read straight off
|
|
21
|
+
// the execution step. `manageEscape: false` — `ResultWindowShell` owns Escape (and focus
|
|
22
|
+
// trap + scroll lock + stacking) via the shared overlay behaviour.
|
|
23
|
+
const { open, blockId, instanceId, stepIndex, close } = useResultView('merger', {
|
|
24
|
+
manageEscape: false,
|
|
25
|
+
})
|
|
23
26
|
const block = computed(() => (blockId.value ? board.getBlock(blockId.value) : undefined))
|
|
24
27
|
|
|
25
28
|
const instance = computed(() =>
|
|
@@ -119,151 +122,122 @@ const reasonText = computed(() => {
|
|
|
119
122
|
</script>
|
|
120
123
|
|
|
121
124
|
<template>
|
|
122
|
-
<
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
:instance-id="instanceId"
|
|
146
|
-
:step-index="stepIndex"
|
|
147
|
-
@restarted="close"
|
|
148
|
-
/>
|
|
149
|
-
<button
|
|
150
|
-
class="rounded-md p-1.5 text-slate-400 hover:bg-slate-800 hover:text-slate-200"
|
|
151
|
-
@click="close"
|
|
125
|
+
<ResultWindowShell
|
|
126
|
+
:open="open"
|
|
127
|
+
:icon="meta?.icon ?? 'i-lucide-git-pull-request'"
|
|
128
|
+
icon-class="bg-lime-500/15 text-lime-300"
|
|
129
|
+
:title="headerTitle"
|
|
130
|
+
:subtitle="t('panels.mergerResult.description')"
|
|
131
|
+
:step-ref="{ instanceId, stepIndex }"
|
|
132
|
+
width="3xl"
|
|
133
|
+
@close="close"
|
|
134
|
+
>
|
|
135
|
+
<div class="flex min-h-0 flex-1">
|
|
136
|
+
<div class="min-w-0 flex-1 overflow-y-auto px-5 py-4">
|
|
137
|
+
<template v-if="decision">
|
|
138
|
+
<!-- Decision banner: auto-merged (success) vs awaiting human review (warning). -->
|
|
139
|
+
<div
|
|
140
|
+
class="mb-4 flex items-start gap-3 rounded-lg border p-3"
|
|
141
|
+
:class="
|
|
142
|
+
merged
|
|
143
|
+
? 'border-emerald-800/70 bg-emerald-500/10'
|
|
144
|
+
: 'border-amber-800/70 bg-amber-500/10'
|
|
145
|
+
"
|
|
146
|
+
data-testid="merger-decision"
|
|
147
|
+
:data-outcome="decision.outcome"
|
|
152
148
|
>
|
|
153
|
-
<UIcon
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
class="mb-4 flex items-start gap-3 rounded-lg border p-3"
|
|
163
|
-
:class="
|
|
164
|
-
merged
|
|
165
|
-
? 'border-emerald-800/70 bg-emerald-500/10'
|
|
166
|
-
: 'border-amber-800/70 bg-amber-500/10'
|
|
167
|
-
"
|
|
168
|
-
data-testid="merger-decision"
|
|
169
|
-
:data-outcome="decision.outcome"
|
|
149
|
+
<UIcon
|
|
150
|
+
:name="merged ? 'i-lucide-git-merge' : 'i-lucide-user-round-check'"
|
|
151
|
+
class="mt-0.5 h-5 w-5 shrink-0"
|
|
152
|
+
:class="merged ? 'text-emerald-300' : 'text-amber-300'"
|
|
153
|
+
/>
|
|
154
|
+
<div class="min-w-0">
|
|
155
|
+
<p
|
|
156
|
+
class="text-sm font-semibold"
|
|
157
|
+
:class="merged ? 'text-emerald-200' : 'text-amber-200'"
|
|
170
158
|
>
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
<div class="min-w-0">
|
|
177
|
-
<p
|
|
178
|
-
class="text-sm font-semibold"
|
|
179
|
-
:class="merged ? 'text-emerald-200' : 'text-amber-200'"
|
|
180
|
-
>
|
|
181
|
-
{{ outcomeText }}
|
|
182
|
-
</p>
|
|
183
|
-
<p class="mt-0.5 text-[13px] leading-relaxed text-slate-300">{{ reasonText }}</p>
|
|
184
|
-
</div>
|
|
185
|
-
</div>
|
|
159
|
+
{{ outcomeText }}
|
|
160
|
+
</p>
|
|
161
|
+
<p class="mt-0.5 text-[13px] leading-relaxed text-slate-300">{{ reasonText }}</p>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
186
164
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
</div>
|
|
202
|
-
<span
|
|
203
|
-
class="w-11 shrink-0 text-end text-xs tabular-nums"
|
|
204
|
-
:class="exceeded.has(axis.key) ? 'text-rose-300' : 'text-slate-300'"
|
|
205
|
-
>
|
|
206
|
-
{{ n(axis.score, { key: 'percent' }) }}
|
|
207
|
-
</span>
|
|
208
|
-
<span class="w-24 shrink-0 text-end text-[10px] tabular-nums text-slate-500">
|
|
209
|
-
{{
|
|
210
|
-
t('panels.mergerResult.ceiling', {
|
|
211
|
-
value: n(axis.ceiling, { key: 'percent' }),
|
|
212
|
-
})
|
|
213
|
-
}}
|
|
214
|
-
</span>
|
|
215
|
-
</div>
|
|
165
|
+
<!-- Scores vs the resolved preset's ceilings. -->
|
|
166
|
+
<template v-if="axes.length">
|
|
167
|
+
<h3 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
168
|
+
{{ t('panels.mergerResult.scores') }}
|
|
169
|
+
</h3>
|
|
170
|
+
<div class="space-y-2 rounded-lg border border-slate-800 bg-slate-950/40 p-3">
|
|
171
|
+
<div v-for="axis in axes" :key="axis.key" class="flex items-center gap-2">
|
|
172
|
+
<span class="w-20 shrink-0 text-xs text-slate-400">{{ axis.label }}</span>
|
|
173
|
+
<div class="h-1.5 flex-1 overflow-hidden rounded-full bg-slate-800">
|
|
174
|
+
<div
|
|
175
|
+
class="h-full rounded-full"
|
|
176
|
+
:class="exceeded.has(axis.key) ? 'bg-rose-500' : 'bg-emerald-500'"
|
|
177
|
+
:style="{ width: `${Math.round(axis.score * 100)}%` }"
|
|
178
|
+
/>
|
|
216
179
|
</div>
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
<template v-if="decision.assessment?.rationale">
|
|
221
|
-
<h3
|
|
222
|
-
class="mb-2 mt-4 text-[11px] font-semibold uppercase tracking-wide text-slate-500"
|
|
180
|
+
<span
|
|
181
|
+
class="w-11 shrink-0 text-end text-xs tabular-nums"
|
|
182
|
+
:class="exceeded.has(axis.key) ? 'text-rose-300' : 'text-slate-300'"
|
|
223
183
|
>
|
|
224
|
-
{{
|
|
225
|
-
</
|
|
226
|
-
<
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
</
|
|
234
|
-
</
|
|
184
|
+
{{ n(axis.score, { key: 'percent' }) }}
|
|
185
|
+
</span>
|
|
186
|
+
<span class="w-24 shrink-0 text-end text-[10px] tabular-nums text-slate-500">
|
|
187
|
+
{{
|
|
188
|
+
t('panels.mergerResult.ceiling', {
|
|
189
|
+
value: n(axis.ceiling, { key: 'percent' }),
|
|
190
|
+
})
|
|
191
|
+
}}
|
|
192
|
+
</span>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
</template>
|
|
235
196
|
|
|
236
|
-
|
|
197
|
+
<!-- The agent's prose justification. -->
|
|
198
|
+
<template v-if="decision.assessment?.rationale">
|
|
199
|
+
<h3 class="mb-2 mt-4 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
200
|
+
{{ t('panels.mergerResult.rationale') }}
|
|
201
|
+
</h3>
|
|
237
202
|
<MarkdownProse
|
|
238
|
-
|
|
239
|
-
:text="step.output"
|
|
203
|
+
:text="decision.assessment.rationale"
|
|
240
204
|
class="text-[13px] leading-relaxed text-slate-300"
|
|
241
205
|
/>
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
:total-steps="instance?.steps.length"
|
|
261
|
-
:run-failed="instance?.status === 'failed'"
|
|
262
|
-
:failure-at="instance?.failure?.occurredAt"
|
|
263
|
-
/>
|
|
264
|
-
</aside>
|
|
206
|
+
</template>
|
|
207
|
+
<p v-else class="text-[13px] italic leading-relaxed text-slate-500">
|
|
208
|
+
{{ t('panels.mergerResult.noAssessment') }}
|
|
209
|
+
</p>
|
|
210
|
+
</template>
|
|
211
|
+
|
|
212
|
+
<!-- Pre-structured runs kept only the raw prose output. -->
|
|
213
|
+
<MarkdownProse
|
|
214
|
+
v-else-if="step?.output"
|
|
215
|
+
:text="step.output"
|
|
216
|
+
class="text-[13px] leading-relaxed text-slate-300"
|
|
217
|
+
/>
|
|
218
|
+
<div
|
|
219
|
+
v-else
|
|
220
|
+
class="flex h-full flex-col items-center justify-center gap-2 text-center text-slate-400"
|
|
221
|
+
>
|
|
222
|
+
<UIcon name="i-lucide-git-pull-request" class="h-8 w-8 opacity-40" />
|
|
223
|
+
<p class="text-sm">{{ t('panels.mergerResult.noResult') }}</p>
|
|
265
224
|
</div>
|
|
266
225
|
</div>
|
|
226
|
+
|
|
227
|
+
<!-- Sidebar: shared run metadata. -->
|
|
228
|
+
<aside
|
|
229
|
+
class="hidden w-60 shrink-0 flex-col gap-4 border-s border-slate-800 bg-slate-900/50 px-4 py-4 lg:flex"
|
|
230
|
+
>
|
|
231
|
+
<StepRunMeta
|
|
232
|
+
v-if="step"
|
|
233
|
+
:step="step"
|
|
234
|
+
:instance-id="instanceId ?? undefined"
|
|
235
|
+
:step-number="stepIndex === null ? undefined : stepIndex + 1"
|
|
236
|
+
:total-steps="instance?.steps.length"
|
|
237
|
+
:run-failed="instance?.status === 'failed'"
|
|
238
|
+
:failure-at="instance?.failure?.occurredAt"
|
|
239
|
+
/>
|
|
240
|
+
</aside>
|
|
267
241
|
</div>
|
|
268
|
-
</
|
|
242
|
+
</ResultWindowShell>
|
|
269
243
|
</template>
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Shared modal shell for the agent-run result windows (slice 5 of the modular-vue
|
|
3
|
+
// adoption — docs/initiatives/modular-vue-adoption.md; progress in
|
|
4
|
+
// docs/initiatives/modular-vue-slice5-progress.md).
|
|
5
|
+
//
|
|
6
|
+
// Every result window (the merger verdict, the tester report, the requirements-review
|
|
7
|
+
// loop, the gates, …) used to hand-roll the SAME modal chrome — `<Teleport>`, a
|
|
8
|
+
// backdrop, a bordered card, a header row with an icon/title/close — and, worse,
|
|
9
|
+
// re-implemented the modal *behaviour* inconsistently: only 2 of ~18 trapped focus, each
|
|
10
|
+
// registered its own global Escape listener, and every one hard-coded `z-50` with no
|
|
11
|
+
// stacking. This shell centralises the chrome AND delegates the behaviour to the upstream
|
|
12
|
+
// `useModalBehavior` (`@modular-vue/core`, the slice-5 overlay-host release): focus-trap
|
|
13
|
+
// + focus-return, body-scroll lock, and a shared overlay STACK so the top overlay closes
|
|
14
|
+
// first on Escape. A window becomes body-only markup wrapped in `<ResultWindowShell>`; it
|
|
15
|
+
// keeps its `useResultView` seam but passes `manageEscape: false` (the shell owns Escape).
|
|
16
|
+
//
|
|
17
|
+
// The pick-one SELECTION of which window is active stays exactly the slice-2
|
|
18
|
+
// `resolveComponentRegistry` in `StepResultViewHost.vue` — this shell only owns the
|
|
19
|
+
// per-window chrome + behaviour, so windows convert one at a time behind it.
|
|
20
|
+
import { computed } from 'vue'
|
|
21
|
+
import { useModalBehavior } from '@modular-vue/core'
|
|
22
|
+
import StepRestartControl from '~/components/panels/StepRestartControl.vue'
|
|
23
|
+
|
|
24
|
+
/** A pipeline step reference — passed by step-result windows to surface the shared
|
|
25
|
+
* "restart from here" control. `StepRestartControl` self-hides for an off-path open
|
|
26
|
+
* (null ids), so a block-keyed window simply omits this prop. */
|
|
27
|
+
type StepRef = { instanceId: string | null; stepIndex: number | null }
|
|
28
|
+
|
|
29
|
+
const props = withDefaults(
|
|
30
|
+
defineProps<{
|
|
31
|
+
/** Whether the window is open — drives the modal behaviour's activation. */
|
|
32
|
+
open: boolean
|
|
33
|
+
/** Header icon (a `UIcon` name) + its badge colour classes. */
|
|
34
|
+
icon?: string
|
|
35
|
+
iconClass?: string
|
|
36
|
+
/** Header title (the accessible dialog name) + optional secondary line. */
|
|
37
|
+
title: string
|
|
38
|
+
subtitle?: string
|
|
39
|
+
/** Card width bucket + backdrop layout (the two pre-slice-5 chrome variants). */
|
|
40
|
+
width?: '3xl' | '4xl' | '5xl'
|
|
41
|
+
variant?: 'stretch' | 'centered'
|
|
42
|
+
/** Provide on step-result windows to show the shared restart control; omit on gates
|
|
43
|
+
* and block-keyed windows (no restart mid-gate / pre-run). */
|
|
44
|
+
stepRef?: StepRef
|
|
45
|
+
/** `data-testid` on the dialog root — pass a window's existing id to preserve e2e
|
|
46
|
+
* selectors; defaults to `result-window`. */
|
|
47
|
+
testid?: string
|
|
48
|
+
}>(),
|
|
49
|
+
{
|
|
50
|
+
icon: 'i-lucide-square',
|
|
51
|
+
iconClass: 'bg-slate-500/15 text-slate-300',
|
|
52
|
+
subtitle: undefined,
|
|
53
|
+
width: '3xl',
|
|
54
|
+
variant: 'stretch',
|
|
55
|
+
stepRef: undefined,
|
|
56
|
+
testid: undefined,
|
|
57
|
+
},
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
const emit = defineEmits<{ close: [] }>()
|
|
61
|
+
const { t } = useI18n()
|
|
62
|
+
|
|
63
|
+
function requestClose() {
|
|
64
|
+
emit('close')
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Managed modal behaviour (focus-trap + return, scroll lock, shared-stack Escape). The
|
|
68
|
+
// window unmounts on close, so deactivation + cleanup fire via `active` going false and
|
|
69
|
+
// unmount — no manual teardown here.
|
|
70
|
+
const { dialogRef } = useModalBehavior({
|
|
71
|
+
active: () => props.open,
|
|
72
|
+
onClose: requestClose,
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
const WIDTH: Record<'3xl' | '4xl' | '5xl', string> = {
|
|
76
|
+
'3xl': 'max-w-3xl',
|
|
77
|
+
'4xl': 'max-w-4xl',
|
|
78
|
+
'5xl': 'max-w-5xl',
|
|
79
|
+
}
|
|
80
|
+
const backdropClass = computed(() => [
|
|
81
|
+
'fixed inset-0 z-50 flex max-h-[100dvh] justify-center bg-slate-950/70 backdrop-blur-sm',
|
|
82
|
+
props.variant === 'centered' ? 'items-center p-4' : 'items-stretch',
|
|
83
|
+
])
|
|
84
|
+
const panelClass = computed(() => [
|
|
85
|
+
'flex w-full flex-col overflow-hidden rounded-2xl border border-slate-800 bg-slate-900 shadow-2xl',
|
|
86
|
+
WIDTH[props.width],
|
|
87
|
+
props.variant === 'centered' ? 'max-h-[90dvh]' : 'm-4',
|
|
88
|
+
])
|
|
89
|
+
</script>
|
|
90
|
+
|
|
91
|
+
<template>
|
|
92
|
+
<Teleport to="body">
|
|
93
|
+
<div
|
|
94
|
+
v-if="open"
|
|
95
|
+
:class="backdropClass"
|
|
96
|
+
data-testid="result-window-backdrop"
|
|
97
|
+
@click.self="requestClose"
|
|
98
|
+
>
|
|
99
|
+
<div
|
|
100
|
+
ref="dialogRef"
|
|
101
|
+
tabindex="-1"
|
|
102
|
+
:class="panelClass"
|
|
103
|
+
role="dialog"
|
|
104
|
+
aria-modal="true"
|
|
105
|
+
:aria-label="title"
|
|
106
|
+
:data-testid="testid ?? 'result-window'"
|
|
107
|
+
>
|
|
108
|
+
<header class="flex items-center gap-3 border-b border-slate-800 px-5 py-3">
|
|
109
|
+
<span
|
|
110
|
+
class="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg"
|
|
111
|
+
:class="iconClass"
|
|
112
|
+
>
|
|
113
|
+
<UIcon :name="icon" class="h-4 w-4" />
|
|
114
|
+
</span>
|
|
115
|
+
<div class="min-w-0 flex-1">
|
|
116
|
+
<h2 class="truncate text-sm font-semibold text-slate-100">{{ title }}</h2>
|
|
117
|
+
<p v-if="subtitle" class="truncate text-[11px] text-slate-400">{{ subtitle }}</p>
|
|
118
|
+
</div>
|
|
119
|
+
<!-- Window-specific header content (status badges, counts). -->
|
|
120
|
+
<slot name="header-extras" />
|
|
121
|
+
<StepRestartControl
|
|
122
|
+
v-if="stepRef"
|
|
123
|
+
:instance-id="stepRef.instanceId"
|
|
124
|
+
:step-index="stepRef.stepIndex"
|
|
125
|
+
@restarted="requestClose"
|
|
126
|
+
/>
|
|
127
|
+
<button
|
|
128
|
+
class="rounded-md p-1.5 text-slate-400 hover:bg-slate-800 hover:text-slate-200"
|
|
129
|
+
data-testid="result-window-close"
|
|
130
|
+
:aria-label="t('common.close')"
|
|
131
|
+
@click="requestClose"
|
|
132
|
+
>
|
|
133
|
+
<UIcon name="i-lucide-x" class="h-4 w-4" />
|
|
134
|
+
</button>
|
|
135
|
+
</header>
|
|
136
|
+
<!-- The window body. -->
|
|
137
|
+
<slot />
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
</Teleport>
|
|
141
|
+
</template>
|
|
@@ -300,6 +300,8 @@ const ITEM_ICON: Record<string, string> = {
|
|
|
300
300
|
>
|
|
301
301
|
<div
|
|
302
302
|
class="group flex cursor-pointer items-center gap-2"
|
|
303
|
+
data-testid="pipeline-step"
|
|
304
|
+
:data-step-kind="s.agentKind"
|
|
303
305
|
:title="
|
|
304
306
|
s.output
|
|
305
307
|
? t('pipeline.progress.viewDetailsOutput')
|
|
@@ -18,10 +18,18 @@
|
|
|
18
18
|
* review windows) can flush it in one place instead of every caller having to remember to.
|
|
19
19
|
* It runs synchronously; if it kicks off async work it must capture whatever it needs first,
|
|
20
20
|
* because `blockId`/the derived state go null the moment the view closes.
|
|
21
|
+
*
|
|
22
|
+
* `manageEscape` (default `true`) owns the global Escape-to-close listener. A window that
|
|
23
|
+
* renders through `ResultWindowShell` (slice 5 of the modular-vue adoption) passes `false`:
|
|
24
|
+
* the shell's `useModalBehavior` owns Escape via the shared overlay stack (so the top
|
|
25
|
+
* overlay closes first, and focus/scroll are managed too), and a second listener here would
|
|
26
|
+
* double-fire `close`. Un-converted windows keep the default so they still close on Escape.
|
|
27
|
+
* Remove this option once every result window is on the shell (the listener moves out
|
|
28
|
+
* entirely).
|
|
21
29
|
*/
|
|
22
30
|
export function useResultView(
|
|
23
31
|
viewId: string,
|
|
24
|
-
opts?: { onOpen?: (blockId: string) => void; onClose?: () => void },
|
|
32
|
+
opts?: { onOpen?: (blockId: string) => void; onClose?: () => void; manageEscape?: boolean },
|
|
25
33
|
) {
|
|
26
34
|
const ui = useUiStore()
|
|
27
35
|
|
|
@@ -42,8 +50,10 @@ export function useResultView(
|
|
|
42
50
|
function onKey(e: KeyboardEvent) {
|
|
43
51
|
if (e.key === 'Escape' && open.value) close()
|
|
44
52
|
}
|
|
45
|
-
|
|
46
|
-
|
|
53
|
+
if (opts?.manageEscape !== false) {
|
|
54
|
+
onMounted(() => window.addEventListener('keydown', onKey))
|
|
55
|
+
onBeforeUnmount(() => window.removeEventListener('keydown', onKey))
|
|
56
|
+
}
|
|
47
57
|
|
|
48
58
|
// The load-on-open contract: fire immediately on mount and on any later block switch.
|
|
49
59
|
if (opts?.onOpen) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.137.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",
|
|
@@ -18,12 +18,12 @@
|
|
|
18
18
|
"access": "public"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@modular-frontend/core": "^0.
|
|
22
|
-
"@modular-vue/core": "^1.
|
|
21
|
+
"@modular-frontend/core": "^0.5.0",
|
|
22
|
+
"@modular-vue/core": "^1.4.0",
|
|
23
23
|
"@modular-vue/journeys": "^1.3.0",
|
|
24
24
|
"@modular-vue/nuxt": "^0.4.0",
|
|
25
25
|
"@modular-vue/runtime": "^1.4.1",
|
|
26
|
-
"@modular-vue/vue": "^1.
|
|
26
|
+
"@modular-vue/vue": "^1.4.0",
|
|
27
27
|
"@nuxt/ui": "^4.10.0",
|
|
28
28
|
"@nuxtjs/i18n": "^10.4.1",
|
|
29
29
|
"@pinia/nuxt": "^1.0.1",
|