@cat-factory/app 0.96.2 → 0.96.3

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.
@@ -94,6 +94,21 @@ body {
94
94
  }
95
95
  }
96
96
 
97
+ /* Respect prefers-reduced-motion: the decorative, infinite attention pulses (the
98
+ * blocked / decision-needed halo, the PR-ready halo) and the marching-ants edge
99
+ * animation convey nothing the static state doesn't already, so we disable them for
100
+ * users who ask for less motion. Loading spinners are deliberately left alone — a
101
+ * spinner's motion IS its meaning ("work in progress"). */
102
+ @media (prefers-reduced-motion: reduce) {
103
+ .board-pulse,
104
+ .board-pulse-green {
105
+ animation: none;
106
+ }
107
+ .vue-flow__edge.is-live .vue-flow__edge-path {
108
+ animation: none;
109
+ }
110
+ }
111
+
97
112
  /* Custom block node should not show the default node chrome */
98
113
  .vue-flow__node-block {
99
114
  padding: 0;
@@ -79,8 +79,9 @@ const ITEM_ICON: Record<string, string> = {
79
79
  {{ t('board.task.buildSteps') }}
80
80
  </div>
81
81
  <div v-for="(s, i) in steps" :key="i" class="rounded bg-slate-900/60 px-1.5 py-1">
82
- <div
83
- class="flex cursor-pointer items-center gap-1"
82
+ <button
83
+ type="button"
84
+ class="flex w-full cursor-pointer items-center gap-1 rounded text-start focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500/60"
84
85
  :title="`${agentKindMeta(s.agentKind).label} — ${agentKindMeta(s.agentKind).description}\n${t('board.task.clickToViewStep')}`"
85
86
  @click.stop="openStep(i)"
86
87
  >
@@ -116,7 +117,7 @@ const ITEM_ICON: Record<string, string> = {
116
117
  : STATE_META[s.state].color,
117
118
  }"
118
119
  />
119
- </div>
120
+ </button>
120
121
 
121
122
  <!-- pending approval gate: jump straight to the conclusions reader. Suppressed
122
123
  while a reviewer gate is folding/re-reviewing in the background (no human needed). -->
@@ -8,6 +8,7 @@
8
8
  // options or hits the iteration cap. The converged direction — not the original description — is
9
9
  // what the downstream stage (the requirements review / the architect) consumes.
10
10
  import IterationCapPrompt from '~/components/pipeline/IterationCapPrompt.vue'
11
+ import IconButton from '~/components/common/IconButton.vue'
11
12
  import { parseOutputOutline } from '~/utils/agentOutput'
12
13
  import type {
13
14
  BrainstormItem,
@@ -281,7 +282,14 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
281
282
  <UBadge v-if="session" color="neutral" variant="subtle" size="sm">
282
283
  {{ t('brainstorm.iteration', { current: iteration, max: maxIterations }) }}
283
284
  </UBadge>
284
- <UButton icon="i-lucide-x" color="neutral" variant="ghost" size="sm" @click="close" />
285
+ <IconButton
286
+ icon="i-lucide-x"
287
+ color="neutral"
288
+ variant="ghost"
289
+ size="sm"
290
+ :label="t('common.close')"
291
+ @click="close"
292
+ />
285
293
  </div>
286
294
  </header>
287
295
 
@@ -9,6 +9,7 @@
9
9
  // description — is what every downstream agent step (the bug investigator, the coder)
10
10
  // consumes.
11
11
  import IterationCapPrompt from '~/components/pipeline/IterationCapPrompt.vue'
12
+ import IconButton from '~/components/common/IconButton.vue'
12
13
  import { parseOutputOutline } from '~/utils/agentOutput'
13
14
  import type {
14
15
  ClarityItemStatus,
@@ -327,7 +328,14 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
327
328
  <UBadge v-if="review" color="neutral" variant="subtle" size="sm">
328
329
  {{ t('clarity.iteration', { current: iteration, max: maxIterations }) }}
329
330
  </UBadge>
330
- <UButton icon="i-lucide-x" color="neutral" variant="ghost" size="sm" @click="close" />
331
+ <IconButton
332
+ icon="i-lucide-x"
333
+ color="neutral"
334
+ variant="ghost"
335
+ size="sm"
336
+ :label="t('common.close')"
337
+ @click="close"
338
+ />
331
339
  </div>
332
340
  </header>
333
341
 
@@ -0,0 +1,19 @@
1
+ <script setup lang="ts">
2
+ // Shared icon-only button primitive that ENFORCES the accessible-name convention (UX-62/63):
3
+ // every icon button must carry a `label`, applied as BOTH `title` (pointer tooltip) and
4
+ // `aria-label` (screen readers) — `title` alone never fires on touch and is invisible to
5
+ // assistive tech, so an unlabeled icon button is a bug this component makes unrepresentable.
6
+ // All other UButton props/listeners (icon, color, variant, size, @click, :loading, :disabled)
7
+ // pass straight through via `$attrs`; `label` is declared as a prop so it strips off before
8
+ // reaching UButton (whose own `label` prop would otherwise render visible text). Mirrors the
9
+ // shape of `common/CopyButton.vue`.
10
+ defineProps<{
11
+ /** Accessible name + tooltip. Required — the whole point of the primitive. */
12
+ label: string
13
+ }>()
14
+ defineOptions({ inheritAttrs: false })
15
+ </script>
16
+
17
+ <template>
18
+ <UButton v-bind="$attrs" :title="label" :aria-label="label" />
19
+ </template>
@@ -4,6 +4,7 @@ import type { Block } from '~/types/domain'
4
4
  import { blockTypeMeta, STATUS_META } from '~/utils/catalog'
5
5
  import { pipelineAllowedForManualStart } from '~/utils/pipeline'
6
6
  import PipelineProgress from '~/components/pipeline/PipelineProgress.vue'
7
+ import IconButton from '~/components/common/IconButton.vue'
7
8
 
8
9
  const board = useBoardStore()
9
10
  const pipelines = usePipelinesStore()
@@ -106,7 +107,13 @@ function openApprovalFor(approvalId: string) {
106
107
  {{ instance ? t('focus.rerunPipeline') : t('focus.runPipeline') }}
107
108
  </UButton>
108
109
  </UDropdownMenu>
109
- <UButton icon="i-lucide-x" color="neutral" variant="ghost" @click="close" />
110
+ <IconButton
111
+ icon="i-lucide-x"
112
+ color="neutral"
113
+ variant="ghost"
114
+ :label="t('common.close')"
115
+ @click="close"
116
+ />
110
117
  </div>
111
118
  </header>
112
119
 
@@ -200,7 +200,7 @@ const STATUS_META: Record<
200
200
  v-model="drafts[item.id]"
201
201
  rows="2"
202
202
  :placeholder="t('followUp.answerPlaceholder')"
203
- 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"
203
+ 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"
204
204
  />
205
205
  <div class="flex items-center gap-2">
206
206
  <UButton
@@ -297,7 +297,7 @@ const conflictVerdict = computed(() => {
297
297
  rows="3"
298
298
  :disabled="fixBusy"
299
299
  :placeholder="t('gates.humanReview.requestFixPlaceholder')"
300
- class="w-full resize-y rounded-md border border-slate-800 bg-slate-950/60 px-3 py-2 text-[13px] text-slate-200 placeholder:text-slate-600 focus:border-violet-500/60 focus:outline-none"
300
+ class="w-full resize-y rounded-md border border-slate-800 bg-slate-950/60 px-3 py-2 text-[13px] text-slate-200 placeholder:text-slate-600 focus:border-violet-500/60 focus:outline-none focus-visible:ring-2 focus-visible:ring-violet-500/60"
301
301
  />
302
302
  <div class="mt-2 flex justify-end">
303
303
  <UButton
@@ -289,7 +289,7 @@ const canDestroy = computed(
289
289
  v-model="findings"
290
290
  rows="4"
291
291
  :placeholder="t('humanTest.fix.placeholder')"
292
- 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"
292
+ 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"
293
293
  />
294
294
  <UButton
295
295
  size="sm"
@@ -18,6 +18,7 @@ import TaskExecution from '~/components/panels/inspector/TaskExecution.vue'
18
18
  import TaskEstimateBadge from '~/components/panels/inspector/TaskEstimateBadge.vue'
19
19
  import EpicChildren from '~/components/panels/inspector/EpicChildren.vue'
20
20
  import InitiativeInspector from '~/components/panels/inspector/InitiativeInspector.vue'
21
+ import IconButton from '~/components/common/IconButton.vue'
21
22
  import RecurringScheduleSettings from '~/components/panels/inspector/RecurringScheduleSettings.vue'
22
23
  import AgentFailureCard from '~/components/board/AgentFailureCard.vue'
23
24
  import AgentStopButton from '~/components/board/AgentStopButton.vue'
@@ -265,11 +266,12 @@ const showOriginalDescription = ref(false)
265
266
  </div>
266
267
  </div>
267
268
  </div>
268
- <UButton
269
+ <IconButton
269
270
  icon="i-lucide-x"
270
271
  color="neutral"
271
272
  variant="ghost"
272
273
  size="xs"
274
+ :label="t('common.close')"
273
275
  @click="ui.select(null)"
274
276
  />
275
277
  </div>
@@ -607,4 +607,12 @@ const ITEM_ICON: Record<string, string> = {
607
607
  .followup-blink {
608
608
  animation: followup-blink 1.4s ease-in-out infinite;
609
609
  }
610
+ /* These decorative attention halos carry no information the static state lacks, so
611
+ silence them under prefers-reduced-motion (matches the board-pulse rule in main.css). */
612
+ @media (prefers-reduced-motion: reduce) {
613
+ .step-active,
614
+ .followup-blink {
615
+ animation: none;
616
+ }
617
+ }
610
618
  </style>
@@ -10,6 +10,7 @@
10
10
  import { parseOutputOutline } from '~/utils/agentOutput'
11
11
  import StepRestartControl from '~/components/panels/StepRestartControl.vue'
12
12
  import IterationCapPrompt from '~/components/pipeline/IterationCapPrompt.vue'
13
+ import IconButton from '~/components/common/IconButton.vue'
13
14
  import type {
14
15
  RequirementRecommendation,
15
16
  RequirementReview,
@@ -489,7 +490,14 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
489
490
  :step-index="stepIndex"
490
491
  @restarted="close"
491
492
  />
492
- <UButton icon="i-lucide-x" color="neutral" variant="ghost" size="sm" @click="close" />
493
+ <IconButton
494
+ icon="i-lucide-x"
495
+ color="neutral"
496
+ variant="ghost"
497
+ size="sm"
498
+ :label="t('common.close')"
499
+ @click="close"
500
+ />
493
501
  </div>
494
502
  </header>
495
503
 
@@ -12,6 +12,7 @@ import type {
12
12
  RequirementPriority,
13
13
  SpecModule,
14
14
  } from '~/types/spec'
15
+ import IconButton from '~/components/common/IconButton.vue'
15
16
 
16
17
  const { t } = useI18n()
17
18
  const board = useBoardStore()
@@ -176,7 +177,14 @@ function kindLabel(item: RequirementItem): string {
176
177
  {{ t('spec.mode.gherkin') }}
177
178
  </UButton>
178
179
  </div>
179
- <UButton icon="i-lucide-x" color="neutral" variant="ghost" size="sm" @click="close" />
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
+ />
180
188
  </div>
181
189
  </header>
182
190
 
@@ -286,7 +286,7 @@ const canApprove = computed(
286
286
  v-model="perViewNotes[p.view]"
287
287
  rows="2"
288
288
  :placeholder="t('visualConfirm.notePlaceholder', { view: p.view })"
289
- 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"
289
+ 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"
290
290
  />
291
291
  </div>
292
292
  </div>
@@ -340,7 +340,7 @@ const canApprove = computed(
340
340
  v-model="globalFindings"
341
341
  rows="3"
342
342
  :placeholder="t('visualConfirm.requestFix.placeholder')"
343
- 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"
343
+ 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"
344
344
  />
345
345
  <div class="mt-2 flex items-center justify-between">
346
346
  <span class="text-[11px] text-slate-500">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cat-factory/app",
3
- "version": "0.96.2",
3
+ "version": "0.96.3",
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",