@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.
@@ -71,6 +71,7 @@ const attachMenu = computed<DropdownMenuItem[][]>(() => {
71
71
  v-for="doc in linked"
72
72
  :key="`${doc.source}:${doc.externalId}`"
73
73
  :href="doc.url"
74
+ :title="doc.url"
74
75
  target="_blank"
75
76
  rel="noopener"
76
77
  class="flex items-center gap-1.5 rounded-md border border-slate-800 bg-slate-900/60 px-2 py-1.5 text-xs text-slate-300 hover:bg-slate-800/60"
@@ -12,6 +12,7 @@ import { useBoardStore } from '~/stores/board'
12
12
  import { useFollowUpsStore } from '~/stores/followUps'
13
13
  import type { FollowUpItem } from '~/types/execution'
14
14
  import { FOLLOW_UP_COMPANION_META } from '~/utils/catalog'
15
+ import ResultWindowShell from '~/components/panels/ResultWindowShell.vue'
15
16
 
16
17
  const execution = useExecutionStore()
17
18
  const board = useBoardStore()
@@ -20,9 +21,16 @@ const access = useWorkspaceAccess()
20
21
 
21
22
  const { t } = useI18n()
22
23
 
23
- const { open, blockId, instanceId, stepIndex, close } = useResultView('follow-ups')
24
+ // `manageEscape: false` `ResultWindowShell` owns Escape (and focus trap + scroll lock +
25
+ // stacking) via the shared overlay behaviour.
26
+ const { open, blockId, instanceId, stepIndex, close } = useResultView('follow-ups', {
27
+ manageEscape: false,
28
+ })
24
29
 
25
30
  const block = computed(() => (blockId.value ? board.getBlock(blockId.value) : undefined))
31
+ const headerTitle = computed(() =>
32
+ block.value ? t('followUp.titleWithBlock', { title: block.value.title }) : t('followUp.title'),
33
+ )
26
34
  const instance = computed(() =>
27
35
  instanceId.value === null ? null : (execution.getInstance(instanceId.value) ?? null),
28
36
  )
@@ -83,216 +91,182 @@ const STATUS_META: Record<
83
91
  </script>
84
92
 
85
93
  <template>
86
- <Teleport to="body">
87
- <div
88
- v-if="open"
89
- class="fixed inset-0 z-50 flex max-h-[100dvh] items-stretch justify-center bg-slate-950/70 backdrop-blur-sm"
90
- @click.self="close"
91
- >
94
+ <ResultWindowShell
95
+ :open="open"
96
+ :icon="FOLLOW_UP_COMPANION_META.icon"
97
+ icon-class="bg-pink-500/15 text-pink-300"
98
+ :title="headerTitle"
99
+ :subtitle="t('followUp.subtitle')"
100
+ width="3xl"
101
+ @close="close"
102
+ >
103
+ <template #header-extras>
104
+ <UBadge :color="pendingCount > 0 ? 'warning' : 'success'" variant="subtle" size="sm">
105
+ {{
106
+ pendingCount > 0
107
+ ? t('followUp.badge.toDecide', { count: pendingCount }, pendingCount)
108
+ : t('followUp.badge.allDecided')
109
+ }}
110
+ </UBadge>
111
+ </template>
112
+
113
+ <div class="min-h-0 flex-1 overflow-y-auto px-5 py-4">
114
+ <!-- Empty -->
92
115
  <div
93
- class="m-4 flex w-full max-w-3xl flex-col overflow-hidden rounded-2xl border border-slate-800 bg-slate-900 shadow-2xl"
94
- role="dialog"
95
- aria-modal="true"
116
+ v-if="items.length === 0"
117
+ class="flex h-full flex-col items-center justify-center gap-2 py-10 text-center text-slate-400"
96
118
  >
97
- <!-- Header -->
98
- <header class="flex items-center gap-3 border-b border-slate-800 px-5 py-3">
99
- <span
100
- class="flex h-8 w-8 items-center justify-center rounded-lg bg-pink-500/15 text-pink-300"
101
- >
102
- <UIcon :name="FOLLOW_UP_COMPANION_META.icon" class="h-4 w-4" />
103
- </span>
104
- <div class="min-w-0 flex-1">
105
- <h2 class="truncate text-sm font-semibold text-slate-100">
106
- {{
107
- block ? t('followUp.titleWithBlock', { title: block.title }) : t('followUp.title')
108
- }}
109
- </h2>
110
- <p class="truncate text-[11px] text-slate-400">
111
- {{ t('followUp.subtitle') }}
112
- </p>
113
- </div>
114
- <UBadge :color="pendingCount > 0 ? 'warning' : 'success'" variant="subtle" size="sm">
115
- {{
116
- pendingCount > 0
117
- ? t('followUp.badge.toDecide', { count: pendingCount }, pendingCount)
118
- : t('followUp.badge.allDecided')
119
- }}
120
- </UBadge>
121
- <button
122
- class="rounded-md p-1.5 text-slate-400 hover:bg-slate-800 hover:text-slate-200"
123
- @click="close"
124
- >
125
- <UIcon name="i-lucide-x" class="h-4 w-4" />
126
- </button>
127
- </header>
119
+ <UIcon :name="FOLLOW_UP_COMPANION_META.icon" class="h-8 w-8 opacity-40" />
120
+ <p class="text-sm">{{ t('followUp.empty.title') }}</p>
121
+ <p class="max-w-sm text-[11px] text-slate-500">
122
+ {{ t('followUp.empty.hint') }}
123
+ </p>
124
+ </div>
128
125
 
129
- <div class="min-h-0 flex-1 overflow-y-auto px-5 py-4">
130
- <!-- Empty -->
131
- <div
132
- v-if="items.length === 0"
133
- class="flex h-full flex-col items-center justify-center gap-2 py-10 text-center text-slate-400"
134
- >
135
- <UIcon :name="FOLLOW_UP_COMPANION_META.icon" class="h-8 w-8 opacity-40" />
136
- <p class="text-sm">{{ t('followUp.empty.title') }}</p>
137
- <p class="max-w-sm text-[11px] text-slate-500">
138
- {{ t('followUp.empty.hint') }}
139
- </p>
140
- </div>
126
+ <div v-else class="space-y-3">
127
+ <p
128
+ v-if="followUps.error"
129
+ class="rounded-md bg-rose-500/10 px-3 py-2 text-[12px] text-rose-300"
130
+ >
131
+ {{ followUps.error }}
132
+ </p>
141
133
 
142
- <div v-else class="space-y-3">
143
- <p
144
- v-if="followUps.error"
145
- class="rounded-md bg-rose-500/10 px-3 py-2 text-[12px] text-rose-300"
146
- >
147
- {{ followUps.error }}
148
- </p>
134
+ <article
135
+ v-for="item in items"
136
+ :key="item.id"
137
+ class="rounded-xl border border-slate-800 bg-slate-900/60 px-4 py-3"
138
+ :class="item.status === 'pending' ? 'border-amber-500/40' : ''"
139
+ >
140
+ <div class="flex items-start gap-2">
141
+ <UIcon
142
+ :name="item.kind === 'question' ? 'i-lucide-circle-help' : 'i-lucide-compass'"
143
+ class="mt-0.5 h-4 w-4 shrink-0"
144
+ :class="item.kind === 'question' ? 'text-sky-300' : 'text-pink-300'"
145
+ />
146
+ <div class="min-w-0 flex-1">
147
+ <div class="flex items-center gap-2">
148
+ <h3 class="min-w-0 flex-1 truncate text-[13px] font-medium text-slate-100">
149
+ {{ item.title }}
150
+ </h3>
151
+ <UBadge :color="STATUS_META[item.status].badge" variant="subtle" size="sm">
152
+ {{ t(STATUS_LABEL_KEYS[item.status]) }}
153
+ </UBadge>
154
+ </div>
155
+ <p v-if="item.detail" class="mt-1 whitespace-pre-wrap text-[12px] text-slate-300">
156
+ {{ item.detail }}
157
+ </p>
158
+ <p v-if="item.suggestedAction" class="mt-1 text-[11px] text-slate-400">
159
+ <span class="text-slate-500">{{ t('followUp.suggested') }}</span>
160
+ {{ item.suggestedAction }}
161
+ </p>
162
+ <p v-if="item.status === 'filed' && item.ticketUrl" class="mt-1 text-[11px]">
163
+ <a
164
+ :href="item.ticketUrl"
165
+ target="_blank"
166
+ rel="noopener"
167
+ class="text-emerald-300 hover:underline"
168
+ >
169
+ {{ item.ticketExternalId ?? t('followUp.viewIssue') }}
170
+ </a>
171
+ </p>
172
+ <p
173
+ v-if="item.status === 'answered' && item.answer"
174
+ class="mt-1 text-[11px] text-slate-300"
175
+ >
176
+ <span class="text-slate-500">{{ t('followUp.yourAnswer') }}</span>
177
+ {{ item.answer }}
178
+ </p>
149
179
 
150
- <article
151
- v-for="item in items"
152
- :key="item.id"
153
- class="rounded-xl border border-slate-800 bg-slate-900/60 px-4 py-3"
154
- :class="item.status === 'pending' ? 'border-amber-500/40' : ''"
155
- >
156
- <div class="flex items-start gap-2">
157
- <UIcon
158
- :name="item.kind === 'question' ? 'i-lucide-circle-help' : 'i-lucide-compass'"
159
- class="mt-0.5 h-4 w-4 shrink-0"
160
- :class="item.kind === 'question' ? 'text-sky-300' : 'text-pink-300'"
161
- />
162
- <div class="min-w-0 flex-1">
180
+ <!-- Actions (only while the item is still undecided) -->
181
+ <div v-if="item.status === 'pending'" class="mt-2.5">
182
+ <!-- A question: answer it -->
183
+ <div v-if="item.kind === 'question'" class="space-y-2">
184
+ <textarea
185
+ v-model="drafts[item.id]"
186
+ rows="2"
187
+ :placeholder="t('followUp.answerPlaceholder')"
188
+ class="w-full resize-y rounded-md border border-slate-700 bg-slate-950/60 px-2.5 py-1.5 text-[12px] text-slate-100 placeholder:text-slate-600 focus:border-sky-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-sky-500/60"
189
+ />
163
190
  <div class="flex items-center gap-2">
164
- <h3 class="min-w-0 flex-1 truncate text-[13px] font-medium text-slate-100">
165
- {{ item.title }}
166
- </h3>
167
- <UBadge :color="STATUS_META[item.status].badge" variant="subtle" size="sm">
168
- {{ t(STATUS_LABEL_KEYS[item.status]) }}
169
- </UBadge>
170
- </div>
171
- <p v-if="item.detail" class="mt-1 whitespace-pre-wrap text-[12px] text-slate-300">
172
- {{ item.detail }}
173
- </p>
174
- <p v-if="item.suggestedAction" class="mt-1 text-[11px] text-slate-400">
175
- <span class="text-slate-500">{{ t('followUp.suggested') }}</span>
176
- {{ item.suggestedAction }}
177
- </p>
178
- <p v-if="item.status === 'filed' && item.ticketUrl" class="mt-1 text-[11px]">
179
- <a
180
- :href="item.ticketUrl"
181
- target="_blank"
182
- rel="noopener"
183
- class="text-emerald-300 hover:underline"
191
+ <UButton
192
+ size="xs"
193
+ color="primary"
194
+ :loading="followUps.isActing(item.id)"
195
+ :disabled="!(drafts[item.id] ?? '').trim() || !access.canExecuteRuns.value"
196
+ :title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
197
+ @click="onAnswer(item)"
184
198
  >
185
- {{ item.ticketExternalId ?? t('followUp.viewIssue') }}
186
- </a>
187
- </p>
188
- <p
189
- v-if="item.status === 'answered' && item.answer"
190
- class="mt-1 text-[11px] text-slate-300"
191
- >
192
- <span class="text-slate-500">{{ t('followUp.yourAnswer') }}</span>
193
- {{ item.answer }}
194
- </p>
195
-
196
- <!-- Actions (only while the item is still undecided) -->
197
- <div v-if="item.status === 'pending'" class="mt-2.5">
198
- <!-- A question: answer it -->
199
- <div v-if="item.kind === 'question'" class="space-y-2">
200
- <textarea
201
- v-model="drafts[item.id]"
202
- rows="2"
203
- :placeholder="t('followUp.answerPlaceholder')"
204
- class="w-full resize-y rounded-md border border-slate-700 bg-slate-950/60 px-2.5 py-1.5 text-[12px] text-slate-100 placeholder:text-slate-600 focus:border-sky-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-sky-500/60"
205
- />
206
- <div class="flex items-center gap-2">
207
- <UButton
208
- size="xs"
209
- color="primary"
210
- :loading="followUps.isActing(item.id)"
211
- :disabled="
212
- !(drafts[item.id] ?? '').trim() || !access.canExecuteRuns.value
213
- "
214
- :title="
215
- access.canExecuteRuns.value ? undefined : t('access.noRunExecute')
216
- "
217
- @click="onAnswer(item)"
218
- >
219
- {{ t('followUp.actions.answerAndSend') }}
220
- </UButton>
221
- <UButton
222
- size="xs"
223
- color="neutral"
224
- variant="ghost"
225
- :loading="followUps.isActing(item.id)"
226
- :disabled="!access.canExecuteRuns.value"
227
- :title="
228
- access.canExecuteRuns.value ? undefined : t('access.noRunExecute')
229
- "
230
- @click="onDismiss(item)"
231
- >
232
- {{ t('followUp.actions.dismiss') }}
233
- </UButton>
234
- </div>
235
- </div>
236
-
237
- <!-- A follow-up: file / send back / dismiss -->
238
- <div v-else class="flex flex-wrap items-center gap-2">
239
- <UButton
240
- size="xs"
241
- color="primary"
242
- icon="i-lucide-ticket"
243
- :loading="followUps.isActing(item.id)"
244
- :disabled="!access.canExecuteRuns.value"
245
- :title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
246
- @click="onFile(item)"
247
- >
248
- {{ t('followUp.actions.fileAsIssue') }}
249
- </UButton>
250
- <UButton
251
- size="xs"
252
- color="info"
253
- variant="soft"
254
- icon="i-lucide-corner-up-left"
255
- :loading="followUps.isActing(item.id)"
256
- :disabled="!access.canExecuteRuns.value"
257
- :title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
258
- @click="onQueue(item)"
259
- >
260
- {{ t('followUp.actions.sendToCoder') }}
261
- </UButton>
262
- <UButton
263
- size="xs"
264
- color="neutral"
265
- variant="ghost"
266
- :loading="followUps.isActing(item.id)"
267
- @click="onDismiss(item)"
268
- >
269
- {{ t('followUp.actions.dismiss') }}
270
- </UButton>
271
- </div>
199
+ {{ t('followUp.actions.answerAndSend') }}
200
+ </UButton>
201
+ <UButton
202
+ size="xs"
203
+ color="neutral"
204
+ variant="ghost"
205
+ :loading="followUps.isActing(item.id)"
206
+ :disabled="!access.canExecuteRuns.value"
207
+ :title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
208
+ @click="onDismiss(item)"
209
+ >
210
+ {{ t('followUp.actions.dismiss') }}
211
+ </UButton>
272
212
  </div>
273
213
  </div>
214
+
215
+ <!-- A follow-up: file / send back / dismiss -->
216
+ <div v-else class="flex flex-wrap items-center gap-2">
217
+ <UButton
218
+ size="xs"
219
+ color="primary"
220
+ icon="i-lucide-ticket"
221
+ :loading="followUps.isActing(item.id)"
222
+ :disabled="!access.canExecuteRuns.value"
223
+ :title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
224
+ @click="onFile(item)"
225
+ >
226
+ {{ t('followUp.actions.fileAsIssue') }}
227
+ </UButton>
228
+ <UButton
229
+ size="xs"
230
+ color="info"
231
+ variant="soft"
232
+ icon="i-lucide-corner-up-left"
233
+ :loading="followUps.isActing(item.id)"
234
+ :disabled="!access.canExecuteRuns.value"
235
+ :title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
236
+ @click="onQueue(item)"
237
+ >
238
+ {{ t('followUp.actions.sendToCoder') }}
239
+ </UButton>
240
+ <UButton
241
+ size="xs"
242
+ color="neutral"
243
+ variant="ghost"
244
+ :loading="followUps.isActing(item.id)"
245
+ @click="onDismiss(item)"
246
+ >
247
+ {{ t('followUp.actions.dismiss') }}
248
+ </UButton>
249
+ </div>
274
250
  </div>
275
- </article>
251
+ </div>
276
252
  </div>
277
- </div>
278
-
279
- <footer
280
- class="flex items-center justify-between border-t border-slate-800 px-5 py-2.5 text-[11px] text-slate-400"
281
- >
282
- <span>
283
- {{
284
- t(
285
- 'followUp.footer.summary',
286
- { count: items.length, undecided: pendingCount },
287
- items.length,
288
- )
289
- }}
290
- </span>
291
- <span v-if="maxLoops > 0">{{
292
- t('followUp.footer.loops', { loops, max: maxLoops })
293
- }}</span>
294
- </footer>
253
+ </article>
295
254
  </div>
296
255
  </div>
297
- </Teleport>
256
+
257
+ <footer
258
+ class="flex items-center justify-between border-t border-slate-800 px-5 py-2.5 text-[11px] text-slate-400"
259
+ >
260
+ <span>
261
+ {{
262
+ t(
263
+ 'followUp.footer.summary',
264
+ { count: items.length, undecided: pendingCount },
265
+ items.length,
266
+ )
267
+ }}
268
+ </span>
269
+ <span v-if="maxLoops > 0">{{ t('followUp.footer.loops', { loops, max: maxLoops }) }}</span>
270
+ </footer>
271
+ </ResultWindowShell>
298
272
  </template>