@cat-factory/app 0.138.1 → 0.139.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.
- package/app/components/brainstorm/BrainstormWindow.vue +338 -374
- package/app/components/clarity/ClarityReviewWindow.vue +323 -353
- package/app/components/consensus/ConsensusSessionWindow.vue +138 -150
- package/app/components/docs/DocInterviewWindow.vue +108 -136
- package/app/components/followUp/FollowUpWindow.vue +3 -5
- package/app/components/forkDecision/ForkDecisionWindow.vue +1 -3
- package/app/components/gates/GateResultView.vue +3 -5
- package/app/components/humanTest/HumanTestWindow.vue +3 -5
- package/app/components/initiative/InitiativePlanningWindow.vue +89 -112
- package/app/components/initiative/InitiativeTrackerWindow.vue +389 -424
- package/app/components/panels/GenericStructuredResultView.vue +3 -5
- package/app/components/panels/MergerResultView.vue +3 -5
- package/app/components/panels/ResultWindowShell.vue +1 -1
- package/app/components/prReview/PrReviewWindow.vue +181 -205
- package/app/components/ralph/RalphLoopResultView.vue +3 -5
- package/app/components/requirements/RequirementsReviewWindow.vue +512 -557
- package/app/components/spec/ServiceSpecWindow.vue +235 -266
- package/app/components/testing/TestReportWindow.vue +4 -6
- package/app/components/visualConfirm/VisualConfirmationWindow.vue +3 -5
- package/app/composables/useResultView.ts +10 -21
- package/package.json +1 -1
|
@@ -12,7 +12,7 @@ import type {
|
|
|
12
12
|
RequirementPriority,
|
|
13
13
|
SpecModule,
|
|
14
14
|
} from '~/types/spec'
|
|
15
|
-
import
|
|
15
|
+
import ResultWindowShell from '~/components/panels/ResultWindowShell.vue'
|
|
16
16
|
|
|
17
17
|
const { t } = useI18n()
|
|
18
18
|
const board = useBoardStore()
|
|
@@ -121,291 +121,260 @@ function kindLabel(item: RequirementItem): string {
|
|
|
121
121
|
</script>
|
|
122
122
|
|
|
123
123
|
<template>
|
|
124
|
-
<
|
|
124
|
+
<ResultWindowShell
|
|
125
|
+
:open="open"
|
|
126
|
+
icon="i-lucide-scroll-text"
|
|
127
|
+
icon-class="bg-indigo-500/15 text-indigo-300"
|
|
128
|
+
:title="t('spec.title')"
|
|
129
|
+
:subtitle="block ? spec?.service || block.title : undefined"
|
|
130
|
+
variant="centered"
|
|
131
|
+
width="5xl"
|
|
132
|
+
@close="close"
|
|
133
|
+
>
|
|
134
|
+
<!-- view toggle: Gherkin only when the spec (and its feature files) are on main -->
|
|
135
|
+
<template v-if="present" #header-extras>
|
|
136
|
+
<div class="flex items-center rounded-lg border border-slate-700 p-0.5">
|
|
137
|
+
<UButton
|
|
138
|
+
:color="mode === 'structured' ? 'primary' : 'neutral'"
|
|
139
|
+
:variant="mode === 'structured' ? 'soft' : 'ghost'"
|
|
140
|
+
size="xs"
|
|
141
|
+
icon="i-lucide-list-tree"
|
|
142
|
+
@click="
|
|
143
|
+
() => {
|
|
144
|
+
mode = 'structured'
|
|
145
|
+
}
|
|
146
|
+
"
|
|
147
|
+
>
|
|
148
|
+
{{ t('spec.mode.structured') }}
|
|
149
|
+
</UButton>
|
|
150
|
+
<UButton
|
|
151
|
+
:color="mode === 'gherkin' ? 'primary' : 'neutral'"
|
|
152
|
+
:variant="mode === 'gherkin' ? 'soft' : 'ghost'"
|
|
153
|
+
size="xs"
|
|
154
|
+
icon="i-lucide-square-check-big"
|
|
155
|
+
:disabled="!hasGherkin"
|
|
156
|
+
:title="hasGherkin ? t('spec.mode.gherkinTooltip') : t('spec.mode.gherkinNone')"
|
|
157
|
+
@click="
|
|
158
|
+
() => {
|
|
159
|
+
mode = 'gherkin'
|
|
160
|
+
}
|
|
161
|
+
"
|
|
162
|
+
>
|
|
163
|
+
{{ t('spec.mode.gherkin') }}
|
|
164
|
+
</UButton>
|
|
165
|
+
</div>
|
|
166
|
+
</template>
|
|
167
|
+
|
|
168
|
+
<!-- loading -->
|
|
125
169
|
<div
|
|
126
|
-
v-if="
|
|
127
|
-
class="
|
|
128
|
-
@click.self="close"
|
|
170
|
+
v-if="loading && !view"
|
|
171
|
+
class="flex flex-1 items-center justify-center gap-2 text-sm text-slate-400"
|
|
129
172
|
>
|
|
130
|
-
<
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
173
|
+
<UIcon name="i-lucide-loader-circle" class="h-4 w-4 animate-spin" />
|
|
174
|
+
{{ t('spec.loading') }}
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
<!-- error -->
|
|
178
|
+
<div
|
|
179
|
+
v-else-if="errored"
|
|
180
|
+
class="flex flex-1 flex-col items-center justify-center gap-3 p-8 text-center text-sm text-slate-400"
|
|
181
|
+
>
|
|
182
|
+
<UIcon name="i-lucide-triangle-alert" class="h-6 w-6 text-amber-400" />
|
|
183
|
+
{{ t('spec.error') }}
|
|
184
|
+
<UButton
|
|
185
|
+
icon="i-lucide-rotate-cw"
|
|
186
|
+
color="neutral"
|
|
187
|
+
variant="soft"
|
|
188
|
+
size="xs"
|
|
189
|
+
:loading="loading"
|
|
190
|
+
@click="retry"
|
|
134
191
|
>
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
192
|
+
{{ t('common.retry') }}
|
|
193
|
+
</UButton>
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
<!-- empty: no spec on the repo's default branch yet -->
|
|
197
|
+
<div
|
|
198
|
+
v-else-if="!present"
|
|
199
|
+
class="flex flex-1 flex-col items-center justify-center gap-3 p-8 text-center"
|
|
200
|
+
>
|
|
201
|
+
<UIcon name="i-lucide-scroll-text" class="h-8 w-8 text-slate-600" />
|
|
202
|
+
<div>
|
|
203
|
+
<p class="text-sm font-medium text-slate-300">{{ t('spec.empty.title') }}</p>
|
|
204
|
+
<p class="mx-auto mt-1 max-w-md text-xs text-slate-500">
|
|
205
|
+
{{ t('spec.empty.description') }}
|
|
206
|
+
</p>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
|
|
210
|
+
<!-- spec body: navigable tree + detail -->
|
|
211
|
+
<div v-else class="flex min-h-0 flex-1">
|
|
212
|
+
<!-- nav: modules → feature groups -->
|
|
213
|
+
<nav class="w-64 shrink-0 overflow-y-auto border-e border-slate-800 px-3 py-4">
|
|
214
|
+
<UButton
|
|
215
|
+
block
|
|
216
|
+
class="mb-2 justify-start"
|
|
217
|
+
:color="selected === null ? 'primary' : 'neutral'"
|
|
218
|
+
:variant="selected === null ? 'soft' : 'ghost'"
|
|
219
|
+
size="xs"
|
|
220
|
+
icon="i-lucide-info"
|
|
221
|
+
@click="
|
|
222
|
+
() => {
|
|
223
|
+
selected = null
|
|
224
|
+
}
|
|
225
|
+
"
|
|
226
|
+
>
|
|
227
|
+
{{ t('spec.overview') }}
|
|
228
|
+
</UButton>
|
|
229
|
+
<div v-for="(mod, mi) in modules" :key="mi" class="mb-3">
|
|
230
|
+
<div class="px-2 pb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
231
|
+
{{ mod.name }}
|
|
147
232
|
</div>
|
|
148
|
-
<
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
:
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
() => {
|
|
158
|
-
mode = 'structured'
|
|
159
|
-
}
|
|
233
|
+
<ul class="space-y-0.5">
|
|
234
|
+
<li v-for="(group, gi) in mod.groups ?? []" :key="gi">
|
|
235
|
+
<button
|
|
236
|
+
type="button"
|
|
237
|
+
class="flex w-full items-center justify-between gap-2 rounded-md px-2 py-1.5 text-start text-[13px] transition"
|
|
238
|
+
:class="
|
|
239
|
+
selected?.m === mi && selected?.g === gi
|
|
240
|
+
? 'bg-indigo-500/15 text-indigo-200'
|
|
241
|
+
: 'text-slate-300 hover:bg-slate-800'
|
|
160
242
|
"
|
|
243
|
+
@click="selectGroup(mi, gi)"
|
|
161
244
|
>
|
|
162
|
-
{{
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
mode = 'gherkin'
|
|
174
|
-
}
|
|
175
|
-
"
|
|
176
|
-
>
|
|
177
|
-
{{ t('spec.mode.gherkin') }}
|
|
178
|
-
</UButton>
|
|
179
|
-
</div>
|
|
180
|
-
<IconButton
|
|
181
|
-
icon="i-lucide-x"
|
|
182
|
-
color="neutral"
|
|
183
|
-
variant="ghost"
|
|
184
|
-
size="sm"
|
|
185
|
-
:label="t('common.close')"
|
|
186
|
-
@click="close"
|
|
187
|
-
/>
|
|
188
|
-
</div>
|
|
189
|
-
</header>
|
|
190
|
-
|
|
191
|
-
<!-- loading -->
|
|
192
|
-
<div
|
|
193
|
-
v-if="loading && !view"
|
|
194
|
-
class="flex flex-1 items-center justify-center gap-2 text-sm text-slate-400"
|
|
195
|
-
>
|
|
196
|
-
<UIcon name="i-lucide-loader-circle" class="h-4 w-4 animate-spin" />
|
|
197
|
-
{{ t('spec.loading') }}
|
|
245
|
+
<span class="truncate">{{ group.name }}</span>
|
|
246
|
+
<span class="shrink-0 text-[10px] text-slate-500">{{ reqCount(group) }}</span>
|
|
247
|
+
</button>
|
|
248
|
+
</li>
|
|
249
|
+
<li
|
|
250
|
+
v-if="(mod.groups?.length ?? 0) === 0"
|
|
251
|
+
class="px-2 py-1 text-[11px] italic text-slate-600"
|
|
252
|
+
>
|
|
253
|
+
{{ t('spec.noFeatureGroups') }}
|
|
254
|
+
</li>
|
|
255
|
+
</ul>
|
|
198
256
|
</div>
|
|
257
|
+
</nav>
|
|
199
258
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
<
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
</div>
|
|
259
|
+
<!-- detail -->
|
|
260
|
+
<div class="min-w-0 flex-1 overflow-y-auto px-6 py-5">
|
|
261
|
+
<!-- service overview -->
|
|
262
|
+
<template v-if="selected === null">
|
|
263
|
+
<h2 class="text-lg font-semibold text-white">{{ spec?.service }}</h2>
|
|
264
|
+
<p v-if="spec?.summary" class="mt-2 whitespace-pre-line text-sm text-slate-300">
|
|
265
|
+
{{ spec.summary }}
|
|
266
|
+
</p>
|
|
267
|
+
<p v-else class="mt-2 text-sm text-slate-500">{{ t('spec.noSummary') }}</p>
|
|
268
|
+
<p class="mt-4 text-xs text-slate-500">
|
|
269
|
+
{{
|
|
270
|
+
hasGherkin
|
|
271
|
+
? t('spec.moduleHintGherkin', { count: modules.length }, modules.length)
|
|
272
|
+
: t('spec.moduleHint', { count: modules.length }, modules.length)
|
|
273
|
+
}}
|
|
274
|
+
</p>
|
|
275
|
+
</template>
|
|
218
276
|
|
|
219
|
-
<!--
|
|
220
|
-
<
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
>
|
|
224
|
-
<UIcon name="i-lucide-scroll-text" class="h-8 w-8 text-slate-600" />
|
|
225
|
-
<div>
|
|
226
|
-
<p class="text-sm font-medium text-slate-300">{{ t('spec.empty.title') }}</p>
|
|
227
|
-
<p class="mx-auto mt-1 max-w-md text-xs text-slate-500">
|
|
228
|
-
{{ t('spec.empty.description') }}
|
|
229
|
-
</p>
|
|
277
|
+
<!-- selected feature group -->
|
|
278
|
+
<template v-else-if="selectedGroup">
|
|
279
|
+
<div class="mb-1 text-[11px] uppercase tracking-wide text-slate-500">
|
|
280
|
+
{{ selectedModule?.name }}
|
|
230
281
|
</div>
|
|
231
|
-
|
|
282
|
+
<h2 class="text-lg font-semibold text-white">{{ selectedGroup.name }}</h2>
|
|
283
|
+
<p v-if="selectedGroup.summary" class="mt-1 text-sm text-slate-400">
|
|
284
|
+
{{ selectedGroup.summary }}
|
|
285
|
+
</p>
|
|
232
286
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
size="xs"
|
|
243
|
-
icon="i-lucide-info"
|
|
244
|
-
@click="
|
|
245
|
-
() => {
|
|
246
|
-
selected = null
|
|
247
|
-
}
|
|
248
|
-
"
|
|
287
|
+
<!-- GHERKIN view: the rendered .feature file for this group -->
|
|
288
|
+
<template v-if="mode === 'gherkin'">
|
|
289
|
+
<pre
|
|
290
|
+
v-if="selectedFeature"
|
|
291
|
+
class="mt-4 overflow-x-auto rounded-lg border border-slate-800 bg-slate-950/60 p-4 text-[12.5px] leading-relaxed text-slate-200"
|
|
292
|
+
><code>{{ selectedFeature.content }}</code></pre>
|
|
293
|
+
<div
|
|
294
|
+
v-else
|
|
295
|
+
class="mt-4 rounded-lg border border-dashed border-slate-700 p-6 text-center text-sm text-slate-500"
|
|
249
296
|
>
|
|
250
|
-
{{ t('spec.
|
|
251
|
-
</
|
|
252
|
-
|
|
297
|
+
{{ t('spec.noGherkinForGroup') }}
|
|
298
|
+
</div>
|
|
299
|
+
</template>
|
|
300
|
+
|
|
301
|
+
<!-- STRUCTURED view: requirements + acceptance + domain rules -->
|
|
302
|
+
<template v-else>
|
|
303
|
+
<div v-if="reqCount(selectedGroup) === 0" class="mt-4 text-sm text-slate-500">
|
|
304
|
+
{{ t('spec.noRequirements') }}
|
|
305
|
+
</div>
|
|
306
|
+
<ul class="mt-4 space-y-4">
|
|
307
|
+
<li
|
|
308
|
+
v-for="req in selectedGroup.requirements ?? []"
|
|
309
|
+
:key="req.id"
|
|
310
|
+
class="rounded-lg border border-slate-800 bg-slate-900/60 p-4"
|
|
311
|
+
>
|
|
312
|
+
<div class="flex items-start justify-between gap-3">
|
|
313
|
+
<h3 class="text-sm font-semibold text-slate-100">{{ req.title }}</h3>
|
|
314
|
+
<div class="flex shrink-0 items-center gap-1.5">
|
|
315
|
+
<UBadge :color="priorityMeta(req).chip as any" variant="subtle" size="sm">
|
|
316
|
+
{{ priorityMeta(req).label }}
|
|
317
|
+
</UBadge>
|
|
318
|
+
<UBadge color="neutral" variant="subtle" size="sm">{{ kindLabel(req) }}</UBadge>
|
|
319
|
+
</div>
|
|
320
|
+
</div>
|
|
321
|
+
<p class="mt-1.5 whitespace-pre-line text-[13px] leading-relaxed text-slate-300">
|
|
322
|
+
{{ req.statement }}
|
|
323
|
+
</p>
|
|
324
|
+
<!-- acceptance criteria (Given/When/Then) -->
|
|
325
|
+
<div v-if="(req.acceptance?.length ?? 0) > 0" class="mt-3 space-y-1.5">
|
|
326
|
+
<div
|
|
327
|
+
v-for="ac in req.acceptance ?? []"
|
|
328
|
+
:key="ac.id"
|
|
329
|
+
class="rounded-md border border-slate-800 bg-slate-950/50 px-3 py-2 text-[12.5px] leading-relaxed"
|
|
330
|
+
>
|
|
331
|
+
<p class="text-slate-300">
|
|
332
|
+
<span class="font-semibold text-emerald-400">{{
|
|
333
|
+
t('spec.acceptance.given')
|
|
334
|
+
}}</span>
|
|
335
|
+
{{ ac.given }}
|
|
336
|
+
</p>
|
|
337
|
+
<p class="text-slate-300">
|
|
338
|
+
<span class="font-semibold text-sky-400">{{
|
|
339
|
+
t('spec.acceptance.when')
|
|
340
|
+
}}</span>
|
|
341
|
+
{{ ac.when }}
|
|
342
|
+
</p>
|
|
343
|
+
<p class="text-slate-300">
|
|
344
|
+
<span class="font-semibold text-violet-400">{{
|
|
345
|
+
t('spec.acceptance.then')
|
|
346
|
+
}}</span>
|
|
347
|
+
{{ ac.outcome }}
|
|
348
|
+
</p>
|
|
349
|
+
</div>
|
|
350
|
+
</div>
|
|
351
|
+
</li>
|
|
352
|
+
</ul>
|
|
353
|
+
|
|
354
|
+
<!-- domain rules / invariants scoped to this group -->
|
|
355
|
+
<div v-if="(selectedGroup.rules?.length ?? 0) > 0" class="mt-6">
|
|
253
356
|
<div
|
|
254
|
-
class="
|
|
357
|
+
class="mb-2 flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wide text-slate-400"
|
|
255
358
|
>
|
|
256
|
-
|
|
359
|
+
<UIcon name="i-lucide-shield-check" class="h-3.5 w-3.5" />
|
|
360
|
+
{{ t('spec.domainRules') }}
|
|
257
361
|
</div>
|
|
258
|
-
<ul class="space-y-
|
|
259
|
-
<li v-for="(group, gi) in mod.groups ?? []" :key="gi">
|
|
260
|
-
<button
|
|
261
|
-
type="button"
|
|
262
|
-
class="flex w-full items-center justify-between gap-2 rounded-md px-2 py-1.5 text-start text-[13px] transition"
|
|
263
|
-
:class="
|
|
264
|
-
selected?.m === mi && selected?.g === gi
|
|
265
|
-
? 'bg-indigo-500/15 text-indigo-200'
|
|
266
|
-
: 'text-slate-300 hover:bg-slate-800'
|
|
267
|
-
"
|
|
268
|
-
@click="selectGroup(mi, gi)"
|
|
269
|
-
>
|
|
270
|
-
<span class="truncate">{{ group.name }}</span>
|
|
271
|
-
<span class="shrink-0 text-[10px] text-slate-500">{{ reqCount(group) }}</span>
|
|
272
|
-
</button>
|
|
273
|
-
</li>
|
|
362
|
+
<ul class="space-y-1.5">
|
|
274
363
|
<li
|
|
275
|
-
v-
|
|
276
|
-
|
|
364
|
+
v-for="rule in selectedGroup.rules ?? []"
|
|
365
|
+
:key="rule.id"
|
|
366
|
+
class="rounded-md border border-slate-800 bg-slate-900/60 px-3 py-2 text-[13px] text-slate-300"
|
|
277
367
|
>
|
|
278
|
-
{{
|
|
368
|
+
{{ rule.rule }}
|
|
369
|
+
<span v-if="rule.rationale" class="text-slate-500">{{
|
|
370
|
+
t('spec.ruleRationale', { rationale: rule.rationale })
|
|
371
|
+
}}</span>
|
|
279
372
|
</li>
|
|
280
373
|
</ul>
|
|
281
374
|
</div>
|
|
282
|
-
</
|
|
283
|
-
|
|
284
|
-
<!-- detail -->
|
|
285
|
-
<div class="min-w-0 flex-1 overflow-y-auto px-6 py-5">
|
|
286
|
-
<!-- service overview -->
|
|
287
|
-
<template v-if="selected === null">
|
|
288
|
-
<h2 class="text-lg font-semibold text-white">{{ spec?.service }}</h2>
|
|
289
|
-
<p v-if="spec?.summary" class="mt-2 whitespace-pre-line text-sm text-slate-300">
|
|
290
|
-
{{ spec.summary }}
|
|
291
|
-
</p>
|
|
292
|
-
<p v-else class="mt-2 text-sm text-slate-500">{{ t('spec.noSummary') }}</p>
|
|
293
|
-
<p class="mt-4 text-xs text-slate-500">
|
|
294
|
-
{{
|
|
295
|
-
hasGherkin
|
|
296
|
-
? t('spec.moduleHintGherkin', { count: modules.length }, modules.length)
|
|
297
|
-
: t('spec.moduleHint', { count: modules.length }, modules.length)
|
|
298
|
-
}}
|
|
299
|
-
</p>
|
|
300
|
-
</template>
|
|
301
|
-
|
|
302
|
-
<!-- selected feature group -->
|
|
303
|
-
<template v-else-if="selectedGroup">
|
|
304
|
-
<div class="mb-1 text-[11px] uppercase tracking-wide text-slate-500">
|
|
305
|
-
{{ selectedModule?.name }}
|
|
306
|
-
</div>
|
|
307
|
-
<h2 class="text-lg font-semibold text-white">{{ selectedGroup.name }}</h2>
|
|
308
|
-
<p v-if="selectedGroup.summary" class="mt-1 text-sm text-slate-400">
|
|
309
|
-
{{ selectedGroup.summary }}
|
|
310
|
-
</p>
|
|
311
|
-
|
|
312
|
-
<!-- GHERKIN view: the rendered .feature file for this group -->
|
|
313
|
-
<template v-if="mode === 'gherkin'">
|
|
314
|
-
<pre
|
|
315
|
-
v-if="selectedFeature"
|
|
316
|
-
class="mt-4 overflow-x-auto rounded-lg border border-slate-800 bg-slate-950/60 p-4 text-[12.5px] leading-relaxed text-slate-200"
|
|
317
|
-
><code>{{ selectedFeature.content }}</code></pre>
|
|
318
|
-
<div
|
|
319
|
-
v-else
|
|
320
|
-
class="mt-4 rounded-lg border border-dashed border-slate-700 p-6 text-center text-sm text-slate-500"
|
|
321
|
-
>
|
|
322
|
-
{{ t('spec.noGherkinForGroup') }}
|
|
323
|
-
</div>
|
|
324
|
-
</template>
|
|
325
|
-
|
|
326
|
-
<!-- STRUCTURED view: requirements + acceptance + domain rules -->
|
|
327
|
-
<template v-else>
|
|
328
|
-
<div v-if="reqCount(selectedGroup) === 0" class="mt-4 text-sm text-slate-500">
|
|
329
|
-
{{ t('spec.noRequirements') }}
|
|
330
|
-
</div>
|
|
331
|
-
<ul class="mt-4 space-y-4">
|
|
332
|
-
<li
|
|
333
|
-
v-for="req in selectedGroup.requirements ?? []"
|
|
334
|
-
:key="req.id"
|
|
335
|
-
class="rounded-lg border border-slate-800 bg-slate-900/60 p-4"
|
|
336
|
-
>
|
|
337
|
-
<div class="flex items-start justify-between gap-3">
|
|
338
|
-
<h3 class="text-sm font-semibold text-slate-100">{{ req.title }}</h3>
|
|
339
|
-
<div class="flex shrink-0 items-center gap-1.5">
|
|
340
|
-
<UBadge :color="priorityMeta(req).chip as any" variant="subtle" size="sm">
|
|
341
|
-
{{ priorityMeta(req).label }}
|
|
342
|
-
</UBadge>
|
|
343
|
-
<UBadge color="neutral" variant="subtle" size="sm">{{
|
|
344
|
-
kindLabel(req)
|
|
345
|
-
}}</UBadge>
|
|
346
|
-
</div>
|
|
347
|
-
</div>
|
|
348
|
-
<p
|
|
349
|
-
class="mt-1.5 whitespace-pre-line text-[13px] leading-relaxed text-slate-300"
|
|
350
|
-
>
|
|
351
|
-
{{ req.statement }}
|
|
352
|
-
</p>
|
|
353
|
-
<!-- acceptance criteria (Given/When/Then) -->
|
|
354
|
-
<div v-if="(req.acceptance?.length ?? 0) > 0" class="mt-3 space-y-1.5">
|
|
355
|
-
<div
|
|
356
|
-
v-for="ac in req.acceptance ?? []"
|
|
357
|
-
:key="ac.id"
|
|
358
|
-
class="rounded-md border border-slate-800 bg-slate-950/50 px-3 py-2 text-[12.5px] leading-relaxed"
|
|
359
|
-
>
|
|
360
|
-
<p class="text-slate-300">
|
|
361
|
-
<span class="font-semibold text-emerald-400">{{
|
|
362
|
-
t('spec.acceptance.given')
|
|
363
|
-
}}</span>
|
|
364
|
-
{{ ac.given }}
|
|
365
|
-
</p>
|
|
366
|
-
<p class="text-slate-300">
|
|
367
|
-
<span class="font-semibold text-sky-400">{{
|
|
368
|
-
t('spec.acceptance.when')
|
|
369
|
-
}}</span>
|
|
370
|
-
{{ ac.when }}
|
|
371
|
-
</p>
|
|
372
|
-
<p class="text-slate-300">
|
|
373
|
-
<span class="font-semibold text-violet-400">{{
|
|
374
|
-
t('spec.acceptance.then')
|
|
375
|
-
}}</span>
|
|
376
|
-
{{ ac.outcome }}
|
|
377
|
-
</p>
|
|
378
|
-
</div>
|
|
379
|
-
</div>
|
|
380
|
-
</li>
|
|
381
|
-
</ul>
|
|
382
|
-
|
|
383
|
-
<!-- domain rules / invariants scoped to this group -->
|
|
384
|
-
<div v-if="(selectedGroup.rules?.length ?? 0) > 0" class="mt-6">
|
|
385
|
-
<div
|
|
386
|
-
class="mb-2 flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wide text-slate-400"
|
|
387
|
-
>
|
|
388
|
-
<UIcon name="i-lucide-shield-check" class="h-3.5 w-3.5" />
|
|
389
|
-
{{ t('spec.domainRules') }}
|
|
390
|
-
</div>
|
|
391
|
-
<ul class="space-y-1.5">
|
|
392
|
-
<li
|
|
393
|
-
v-for="rule in selectedGroup.rules ?? []"
|
|
394
|
-
:key="rule.id"
|
|
395
|
-
class="rounded-md border border-slate-800 bg-slate-900/60 px-3 py-2 text-[13px] text-slate-300"
|
|
396
|
-
>
|
|
397
|
-
{{ rule.rule }}
|
|
398
|
-
<span v-if="rule.rationale" class="text-slate-500">{{
|
|
399
|
-
t('spec.ruleRationale', { rationale: rule.rationale })
|
|
400
|
-
}}</span>
|
|
401
|
-
</li>
|
|
402
|
-
</ul>
|
|
403
|
-
</div>
|
|
404
|
-
</template>
|
|
405
|
-
</template>
|
|
406
|
-
</div>
|
|
407
|
-
</div>
|
|
375
|
+
</template>
|
|
376
|
+
</template>
|
|
408
377
|
</div>
|
|
409
378
|
</div>
|
|
410
|
-
</
|
|
379
|
+
</ResultWindowShell>
|
|
411
380
|
</template>
|
|
@@ -30,12 +30,10 @@ const blobs = useArtifactBlobs()
|
|
|
30
30
|
onUnmounted(() => blobs.revokeAll())
|
|
31
31
|
|
|
32
32
|
// Shared seam contract (open/blockId/close). No `onOpen` loader: this window reads its report
|
|
33
|
-
// straight off the execution step, so there's nothing to fetch on open. `
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
const { open, blockId, instanceId, stepIndex, close } = useResultView('tester'
|
|
37
|
-
manageEscape: false,
|
|
38
|
-
})
|
|
33
|
+
// straight off the execution step, so there's nothing to fetch on open. `ResultWindowShell`
|
|
34
|
+
// owns Escape (and the focus trap + scroll lock + stacking) via the shared overlay behaviour;
|
|
35
|
+
// the nested lightbox layers above it on the same stack.
|
|
36
|
+
const { open, blockId, instanceId, stepIndex, close } = useResultView('tester')
|
|
39
37
|
const block = computed(() => (blockId.value ? board.getBlock(blockId.value) : undefined))
|
|
40
38
|
const headerTitle = computed(
|
|
41
39
|
() => `${t('testing.title')}${block.value ? ` — ${block.value.title}` : ''}`,
|
|
@@ -26,11 +26,9 @@ const access = useWorkspaceAccess()
|
|
|
26
26
|
const blobs = useArtifactBlobs()
|
|
27
27
|
onUnmounted(() => blobs.revokeAll())
|
|
28
28
|
|
|
29
|
-
// `
|
|
30
|
-
//
|
|
31
|
-
const { open, blockId, instanceId, stepIndex, close } = useResultView('visual-confirm'
|
|
32
|
-
manageEscape: false,
|
|
33
|
-
})
|
|
29
|
+
// `ResultWindowShell` owns Escape (and the focus trap + scroll lock + stacking); the nested
|
|
30
|
+
// lightbox layers above it on the same shared overlay stack.
|
|
31
|
+
const { open, blockId, instanceId, stepIndex, close } = useResultView('visual-confirm')
|
|
34
32
|
const block = computed(() => (blockId.value ? board.getBlock(blockId.value) : undefined))
|
|
35
33
|
const headerTitle = computed(() =>
|
|
36
34
|
block.value
|
|
@@ -13,23 +13,20 @@
|
|
|
13
13
|
* A synchronous window (one that reads its data straight off the execution step, like the
|
|
14
14
|
* test report) simply omits `onOpen`.
|
|
15
15
|
*
|
|
16
|
-
* `onClose` runs on EVERY close path — the X button, backdrop click, and the Escape key
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
16
|
+
* `onClose` runs on EVERY close path — the X button, backdrop click, and the Escape key —
|
|
17
|
+
* BEFORE the view is torn down, so a window with unsaved draft input (the review windows) can
|
|
18
|
+
* flush it in one place instead of every caller having to remember to. It runs synchronously;
|
|
19
|
+
* if it kicks off async work it must capture whatever it needs first, because `blockId`/the
|
|
20
|
+
* derived state go null the moment the view closes.
|
|
21
21
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
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).
|
|
22
|
+
* Escape-to-close is NOT owned here: every result window renders through `ResultWindowShell`
|
|
23
|
+
* (slice 5 of the modular-vue adoption), whose `useModalBehavior` owns Escape via the shared
|
|
24
|
+
* overlay stack (top overlay closes first, focus/scroll managed too). A listener here would
|
|
25
|
+
* double-fire `close`, so it was removed once the last window converted onto the shell.
|
|
29
26
|
*/
|
|
30
27
|
export function useResultView(
|
|
31
28
|
viewId: string,
|
|
32
|
-
opts?: { onOpen?: (blockId: string) => void; onClose?: () => void
|
|
29
|
+
opts?: { onOpen?: (blockId: string) => void; onClose?: () => void },
|
|
33
30
|
) {
|
|
34
31
|
const ui = useUiStore()
|
|
35
32
|
|
|
@@ -47,14 +44,6 @@ export function useResultView(
|
|
|
47
44
|
ui.closeResultView()
|
|
48
45
|
}
|
|
49
46
|
|
|
50
|
-
function onKey(e: KeyboardEvent) {
|
|
51
|
-
if (e.key === 'Escape' && open.value) close()
|
|
52
|
-
}
|
|
53
|
-
if (opts?.manageEscape !== false) {
|
|
54
|
-
onMounted(() => window.addEventListener('keydown', onKey))
|
|
55
|
-
onBeforeUnmount(() => window.removeEventListener('keydown', onKey))
|
|
56
|
-
}
|
|
57
|
-
|
|
58
47
|
// The load-on-open contract: fire immediately on mount and on any later block switch.
|
|
59
48
|
if (opts?.onOpen) {
|
|
60
49
|
watch(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.139.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",
|