@cat-factory/app 0.242.0 → 0.242.2
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/README.md +10 -0
- package/app/components/layout/BoardToolbar.vue +4 -1
- package/app/components/layout/BoardTopOverlays.spec.ts +72 -0
- package/app/components/layout/BoardTopOverlays.vue +90 -0
- package/app/components/layout/ConnectionStatusBanner.vue +4 -4
- package/app/components/layout/GitHubPatBanner.vue +3 -1
- package/app/components/layout/SpendWarningBanner.vue +3 -4
- package/app/components/layout/TranslationWarningBanner.vue +5 -3
- package/app/components/tutorial/TutorialOverlay.logic.spec.ts +39 -0
- package/app/components/tutorial/TutorialOverlay.logic.ts +23 -0
- package/app/components/tutorial/TutorialOverlay.vue +47 -7
- package/app/pages/index.vue +125 -164
- package/i18n/locales/de.json +1 -1
- package/i18n/locales/en.json +1 -1
- package/i18n/locales/es.json +1 -1
- package/i18n/locales/fr.json +1 -1
- package/i18n/locales/he.json +1 -1
- package/i18n/locales/it.json +1 -1
- package/i18n/locales/ja.json +1 -1
- package/i18n/locales/pl.json +1 -1
- package/i18n/locales/tr.json +1 -1
- package/i18n/locales/uk.json +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -58,6 +58,16 @@ over the WebSocket. How that sync works is written up in
|
|
|
58
58
|
| `types/` | TypeScript domain unions (`domain.ts`) and wire types mirroring the contracts. |
|
|
59
59
|
| `utils/` | Small pure helpers. |
|
|
60
60
|
|
|
61
|
+
### The board's top overlay region has ONE owner
|
|
62
|
+
|
|
63
|
+
**A surface that appears at the top of the board renders as a member of `BoardTopOverlays`, and places nothing itself.** No `absolute`/`fixed`, no `top-*`, no z-index of its own: the band is a flex column, and it owns placement and stacking for everything in it. A member contributes only its card plus `pointer-events-auto` (the column is click-through, so its empty strip never intercepts clicks on the board underneath).
|
|
64
|
+
|
|
65
|
+
The reason this is a rule is that the alternative failed exactly once per surface. Each of the toolbar, the spend/connection/PAT banners and the four advisory banners used to anchor itself at `top-0` with its own z-index, so which one you could see came down to who picked the higher number: a standing advisory covered the zoom and fit controls outright, and the board-basics tour then ringed a control nobody could see. Tuning an offset onto one of them (`top-16`, sized against the toolbar pill) fixes the pair that was noticed and leaves the rest, and it goes stale the first time the pill wraps or grows a scrollbar. In one column the overlap is not tuned, it is unrepresentable, and a toolbar that grows pushes the banners down by exactly what it grew.
|
|
66
|
+
|
|
67
|
+
Order within the column is by what the user loses by not reading it now; the toolbar stays first so a tour anchor and the everyday zoom controls do not move as advisories come and go. Full-width page chrome (the translation-warning strip) is a different surface: it sits in NORMAL FLOW at the top of the shell, so it takes its own height rather than covering the row beneath it.
|
|
68
|
+
|
|
69
|
+
`app/components/layout/BoardTopOverlays.spec.ts` enforces the no-self-placement half, reading the member list from the component's own imports.
|
|
70
|
+
|
|
61
71
|
### A store must be instantiable outside a component `setup`
|
|
62
72
|
|
|
63
73
|
A Pinia setup store runs its body on the FIRST `useStore()` anywhere in the app, and that
|
|
@@ -134,8 +134,11 @@ const decisionItems = computed(() =>
|
|
|
134
134
|
</script>
|
|
135
135
|
|
|
136
136
|
<template>
|
|
137
|
+
<!-- Positioning and stacking are owned by `BoardTopOverlays`, the one component that lays out
|
|
138
|
+
the board's top overlay region; this renders only the pill and re-enables pointer events
|
|
139
|
+
on it. Self-anchoring here is what let a banner cover the toolbar outright. -->
|
|
137
140
|
<div
|
|
138
|
-
class="
|
|
141
|
+
class="pointer-events-auto flex max-w-full items-center gap-1 overflow-x-auto rounded-full border border-slate-700 bg-slate-900/90 px-2 py-1.5 shadow-xl backdrop-blur"
|
|
139
142
|
>
|
|
140
143
|
<!-- zoom controls -->
|
|
141
144
|
<IconButton
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { dirname, join } from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { describe, expect, it } from 'vitest'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `BoardTopOverlays` is the single owner of the board's top overlay region, and that only
|
|
8
|
+
* holds while its members render as members: a card in a flex column, with no placement of
|
|
9
|
+
* its own.
|
|
10
|
+
*
|
|
11
|
+
* The reason this is a test and not a note in the component is that the failure is SILENT and
|
|
12
|
+
* only visible on a deployment in the state the banner reports. A member that re-anchors
|
|
13
|
+
* itself (`absolute top-0`, a z-index of its own) leaves the column and lands back on top of
|
|
14
|
+
* whatever picked a lower number, which is how a standing advisory came to cover the zoom/fit
|
|
15
|
+
* controls outright, and the board-basics tour to ring a control nobody could see. Nothing
|
|
16
|
+
* else catches it: the layout still renders, no type is wrong, and the unit suite mounts no
|
|
17
|
+
* components.
|
|
18
|
+
*
|
|
19
|
+
* The member list is READ FROM THE COMPONENT rather than restated here, so a banner added to
|
|
20
|
+
* the column is covered by the same commit that adds it, and one removed from the column
|
|
21
|
+
* stops being checked instead of failing as a phantom.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const layoutDir = dirname(fileURLToPath(import.meta.url))
|
|
25
|
+
const band = readFileSync(join(layoutDir, 'BoardTopOverlays.vue'), 'utf8')
|
|
26
|
+
|
|
27
|
+
/** The components the band lays out, from its own imports. */
|
|
28
|
+
function bandMembers(): string[] {
|
|
29
|
+
return [...band.matchAll(/^import (\w+) from '~\/components\/layout\/(\w+)\.vue'$/gm)].map(
|
|
30
|
+
(m) => m[2] as string,
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Every class token the file applies, from its static `class="…"` attributes. */
|
|
35
|
+
function classTokens(source: string): string[] {
|
|
36
|
+
return [...source.matchAll(/\bclass="([^"]*)"/g)].flatMap((m) => (m[1] as string).split(/\s+/))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A token that takes a member out of the column's flow. Responsive and state variants count
|
|
41
|
+
* (`sm:absolute`, `lg:top-0`): the overlap they cause is no less real for being conditional.
|
|
42
|
+
*/
|
|
43
|
+
function selfPlacing(token: string): boolean {
|
|
44
|
+
const base = token.includes(':') ? (token.split(':').pop() as string) : token
|
|
45
|
+
return (
|
|
46
|
+
base === 'fixed' ||
|
|
47
|
+
base === 'absolute' ||
|
|
48
|
+
base === 'sticky' ||
|
|
49
|
+
/^-?(top|bottom|inset)-/.test(base) ||
|
|
50
|
+
base === 'inset-0' ||
|
|
51
|
+
base.startsWith('z-')
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe('BoardTopOverlays members', () => {
|
|
56
|
+
const members = bandMembers()
|
|
57
|
+
|
|
58
|
+
it('lays out every top-region surface, so none of them is left placing itself', () => {
|
|
59
|
+
// A relation over the source, not a count: the column's job is to be the ONLY placement
|
|
60
|
+
// authority in this region, and a pinned number would fail on every ordinary addition
|
|
61
|
+
// while saying nothing about what broke.
|
|
62
|
+
expect(members.length).toBeGreaterThan(0)
|
|
63
|
+
expect(new Set(members).size).toBe(members.length)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it.each(bandMembers())('%s places nothing itself', (member) => {
|
|
67
|
+
const offenders = classTokens(readFileSync(join(layoutDir, `${member}.vue`), 'utf8')).filter(
|
|
68
|
+
selfPlacing,
|
|
69
|
+
)
|
|
70
|
+
expect(offenders).toEqual([])
|
|
71
|
+
})
|
|
72
|
+
})
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import BoardToolbar from '~/components/layout/BoardToolbar.vue'
|
|
3
|
+
import ConnectionStatusBanner from '~/components/layout/ConnectionStatusBanner.vue'
|
|
4
|
+
import SpendWarningBanner from '~/components/layout/SpendWarningBanner.vue'
|
|
5
|
+
import GitHubPatBanner from '~/components/layout/GitHubPatBanner.vue'
|
|
6
|
+
import AiProvidersBanner from '~/components/layout/AiProvidersBanner.vue'
|
|
7
|
+
import ProviderConfigBanner from '~/components/layout/ProviderConfigBanner.vue'
|
|
8
|
+
import InfraSetupBanner from '~/components/layout/InfraSetupBanner.vue'
|
|
9
|
+
import DefaultTestEnvBanner from '~/components/layout/DefaultTestEnvBanner.vue'
|
|
10
|
+
|
|
11
|
+
// The single owner of the board's top overlay region: the toolbar pill, the corner nav
|
|
12
|
+
// trigger, and every advisory banner, laid out in ONE flex column.
|
|
13
|
+
//
|
|
14
|
+
// The column is the point. Each of these used to anchor itself (`absolute top-0`, its own
|
|
15
|
+
// z-index), which makes overlap a matter of who picked the higher number: the four advisory
|
|
16
|
+
// banners already deferred to a shared column, but the toolbar, the spend/connection/PAT
|
|
17
|
+
// banners and the nav trigger each positioned themselves. A standing advisory therefore
|
|
18
|
+
// covered the zoom/fit controls outright, for everyone, and the board-basics tour rang a
|
|
19
|
+
// control nobody could see. Stacking them in flow makes that overlap structurally impossible
|
|
20
|
+
// of merely tuned: no offset constant to keep in step with the pill's height, and a toolbar
|
|
21
|
+
// that grows (a wrapped row, a scrollbar on a narrow viewport) pushes the column down by
|
|
22
|
+
// exactly what it grew.
|
|
23
|
+
//
|
|
24
|
+
// The column is `pointer-events-none` so its empty strip never intercepts clicks on the board
|
|
25
|
+
// underneath; each member re-enables pointer events on its own card.
|
|
26
|
+
//
|
|
27
|
+
// The nav trigger sits INSIDE this region rather than beside it. It has to: this column owns
|
|
28
|
+
// the region's z-index, so an outside sibling could only be ordered against the whole column,
|
|
29
|
+
// where what is actually needed is for the trigger to stay above the toolbar pill (whose
|
|
30
|
+
// max-width reaches the corner on the narrowest viewports) and below a banner that has
|
|
31
|
+
// something urgent to say. Both are local orderings, and they only exist here.
|
|
32
|
+
|
|
33
|
+
// The live-stream state the connection strip renders. Passed down rather than resolved here:
|
|
34
|
+
// the page owns the single stream instance, and creating another would open a second socket.
|
|
35
|
+
defineProps<{
|
|
36
|
+
connected: boolean
|
|
37
|
+
everConnected: boolean
|
|
38
|
+
connectionFailed: boolean
|
|
39
|
+
}>()
|
|
40
|
+
|
|
41
|
+
const ui = useUiStore()
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
<template>
|
|
45
|
+
<div
|
|
46
|
+
class="pointer-events-none absolute inset-x-0 top-0 z-40 flex flex-col items-center gap-2 px-4 pt-3"
|
|
47
|
+
>
|
|
48
|
+
<!-- Compact-viewport nav trigger: the SideBar is an off-canvas drawer below lg, so surface
|
|
49
|
+
a hamburger to open it. Out of the column's flow (it is a corner control, not a
|
|
50
|
+
centered one) and above the toolbar pill it can overlap there. -->
|
|
51
|
+
<UButton
|
|
52
|
+
class="pointer-events-auto absolute start-3 top-3 z-10 lg:hidden"
|
|
53
|
+
icon="i-lucide-menu"
|
|
54
|
+
color="neutral"
|
|
55
|
+
variant="soft"
|
|
56
|
+
size="sm"
|
|
57
|
+
:aria-label="ui.mobileNavOpen ? $t('nav.closeMenu') : $t('nav.openMenu')"
|
|
58
|
+
data-testid="mobile-nav-toggle"
|
|
59
|
+
@click="ui.toggleMobileNav()"
|
|
60
|
+
/>
|
|
61
|
+
|
|
62
|
+
<!-- FIRST, and deliberately so: the toolbar is standing board chrome, and a tour step
|
|
63
|
+
anchors it. Below the advisories it would move every time one appears or clears. -->
|
|
64
|
+
<BoardToolbar />
|
|
65
|
+
|
|
66
|
+
<!-- Then the advisories, most urgent first. Ordering is by what the user loses by not
|
|
67
|
+
reading it now, not by how loud the card is.
|
|
68
|
+
- Connection status: what is on screen right now may already be stale.
|
|
69
|
+
- Spend exceeded: runs are blocked until the budget moves.
|
|
70
|
+
- GitHub PAT (local mode): every repo-operating step will fail.
|
|
71
|
+
- AI readiness: no usable model source, or the default preset names unavailable models.
|
|
72
|
+
- Infrastructure provider: env/runner-pool wired but missing mandatory config.
|
|
73
|
+
- Infra setup: an executor / test env / storage this deployment needs is undefined, so
|
|
74
|
+
a class of agents cannot run.
|
|
75
|
+
- Default test environment: this BOARD has never chosen the provisioning mechanism its
|
|
76
|
+
new services should default to. Last, because it asks for a convenience default and
|
|
77
|
+
so yields to the prompts about things that are outright broken. -->
|
|
78
|
+
<ConnectionStatusBanner
|
|
79
|
+
:connected="connected"
|
|
80
|
+
:ever-connected="everConnected"
|
|
81
|
+
:connection-failed="connectionFailed"
|
|
82
|
+
/>
|
|
83
|
+
<SpendWarningBanner />
|
|
84
|
+
<GitHubPatBanner />
|
|
85
|
+
<AiProvidersBanner />
|
|
86
|
+
<ProviderConfigBanner />
|
|
87
|
+
<InfraSetupBanner />
|
|
88
|
+
<DefaultTestEnvBanner />
|
|
89
|
+
</div>
|
|
90
|
+
</template>
|
|
@@ -59,10 +59,10 @@ onBeforeUnmount(clearTimer)
|
|
|
59
59
|
|
|
60
60
|
<template>
|
|
61
61
|
<Transition name="fade">
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
>
|
|
62
|
+
<!-- Positioning/stacking is owned by `BoardTopOverlays`; this renders only the pill, which
|
|
63
|
+
re-enables pointer events on itself (the row around it stays click-through, since it is
|
|
64
|
+
far wider than the pill it centres). -->
|
|
65
|
+
<div v-if="reconnecting || offline" class="pointer-events-none flex w-full justify-center">
|
|
66
66
|
<div
|
|
67
67
|
v-if="reconnecting"
|
|
68
68
|
class="pointer-events-auto flex items-center gap-2 rounded-full border border-amber-500/60 bg-amber-950/90 px-3 py-1.5 text-xs text-amber-100 shadow-lg backdrop-blur"
|
|
@@ -16,7 +16,9 @@ const show = computed(() => !!setupUrl.value && !dismissed.value)
|
|
|
16
16
|
|
|
17
17
|
<template>
|
|
18
18
|
<Transition name="fade">
|
|
19
|
-
|
|
19
|
+
<!-- Positioning/stacking is owned by `BoardTopOverlays`; this renders only its card and
|
|
20
|
+
re-enables pointer events on it. -->
|
|
21
|
+
<div v-if="show" class="pointer-events-auto w-full max-w-3xl">
|
|
20
22
|
<div
|
|
21
23
|
class="w-full max-w-3xl rounded-2xl border-2 border-amber-500/70 bg-amber-950/95 p-5 shadow-2xl backdrop-blur"
|
|
22
24
|
role="alert"
|
|
@@ -32,10 +32,9 @@ async function resume() {
|
|
|
32
32
|
|
|
33
33
|
<template>
|
|
34
34
|
<Transition name="fade">
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
>
|
|
35
|
+
<!-- Positioning/stacking is owned by `BoardTopOverlays`; this renders only its card and
|
|
36
|
+
re-enables pointer events on it. -->
|
|
37
|
+
<div v-if="exceeded && spend" class="pointer-events-auto w-full max-w-3xl">
|
|
39
38
|
<div
|
|
40
39
|
class="w-full max-w-3xl rounded-2xl border-2 border-red-500/70 bg-red-950/95 p-5 shadow-2xl backdrop-blur"
|
|
41
40
|
role="alert"
|
|
@@ -5,8 +5,10 @@ import { computed } from 'vue'
|
|
|
5
5
|
// Shown whenever the active locale is NOT English: the non-English catalogs are
|
|
6
6
|
// community/AI-provided and may be inaccurate, so warn the user and point them at the
|
|
7
7
|
// repository to report mistakes or open a fix PR. Rendered as a slim full-width strip at
|
|
8
|
-
// the very top
|
|
9
|
-
//
|
|
8
|
+
// the very top of the shell, IN NORMAL FLOW rather than fixed over the page: a strip that
|
|
9
|
+
// takes its own height cannot cover anything, where the fixed one sat over the board's top
|
|
10
|
+
// controls for the whole of every non-English session. Dismissal is persisted per-locale in
|
|
11
|
+
// localStorage: once dismissed for a locale
|
|
10
12
|
// it stays hidden across reloads, but switching to a different (separately-translated)
|
|
11
13
|
// locale shows it again, since that catalog is a fresh, independently-translated context.
|
|
12
14
|
const REPO_URL = 'https://github.com/kibertoad/cat-factory'
|
|
@@ -29,7 +31,7 @@ function dismiss() {
|
|
|
29
31
|
v-if="show"
|
|
30
32
|
data-testid="translation-warning"
|
|
31
33
|
role="alert"
|
|
32
|
-
class="
|
|
34
|
+
class="flex shrink-0 items-center gap-3 border-b border-amber-500/40 bg-amber-950/95 px-4 py-2 text-[13px] text-amber-100 shadow-lg backdrop-blur"
|
|
33
35
|
>
|
|
34
36
|
<UIcon name="i-lucide-languages" class="h-4 w-4 shrink-0 text-amber-400" />
|
|
35
37
|
<p class="min-w-0 flex-1">
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
2
|
import {
|
|
3
3
|
boardNodeIdFor,
|
|
4
|
+
focusLeftCard,
|
|
4
5
|
isSafeTargetId,
|
|
5
6
|
isTargetClickAdvance,
|
|
6
7
|
resolveSkip,
|
|
@@ -208,3 +209,41 @@ describe('shouldFocusCard', () => {
|
|
|
208
209
|
expect(shouldFocusCard('target-click')).toBe(false)
|
|
209
210
|
})
|
|
210
211
|
})
|
|
212
|
+
|
|
213
|
+
describe('focusLeftCard', () => {
|
|
214
|
+
const card = () => {
|
|
215
|
+
const el = document.createElement('div')
|
|
216
|
+
const next = document.createElement('button')
|
|
217
|
+
el.appendChild(next)
|
|
218
|
+
document.body.appendChild(el)
|
|
219
|
+
return { el, next }
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
it('keeps the card focusable while focus is still inside it', () => {
|
|
223
|
+
// Tabbing from the card onto its own Next button must NOT drop `tabindex`: the card would
|
|
224
|
+
// become click-focusable again while the user is still in it, which is the exact state the
|
|
225
|
+
// attribute exists to avoid.
|
|
226
|
+
const { el, next } = card()
|
|
227
|
+
expect(focusLeftCard(el, next)).toBe(false)
|
|
228
|
+
expect(focusLeftCard(el, el)).toBe(false)
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it('treats focus moving to anything outside the card as having left', () => {
|
|
232
|
+
const { el } = card()
|
|
233
|
+
const outside = document.createElement('input')
|
|
234
|
+
document.body.appendChild(outside)
|
|
235
|
+
expect(focusLeftCard(el, outside)).toBe(true)
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
it('treats focus falling to nothing as having left', () => {
|
|
239
|
+
// The `relatedTarget: null` case is not an edge: it is what happens when the pressed
|
|
240
|
+
// control unmounts under the press, which Back at step 1 does every time.
|
|
241
|
+
const { el } = card()
|
|
242
|
+
expect(focusLeftCard(el, null)).toBe(true)
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
it('reports left when there is no card', () => {
|
|
246
|
+
// An unmounted card holds no focus, so the flag must not be left standing.
|
|
247
|
+
expect(focusLeftCard(null, null)).toBe(true)
|
|
248
|
+
})
|
|
249
|
+
})
|
|
@@ -46,6 +46,29 @@ export function shouldFocusCard(cause: TutorialAdvanceCause): boolean {
|
|
|
46
46
|
return cause !== 'target-click'
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Has focus actually LEFT the coach mark, as opposed to moving between its own controls?
|
|
51
|
+
*
|
|
52
|
+
* The card's `tabindex` is not a standing attribute (see `focusCard` in the component): it
|
|
53
|
+
* exists only while the card holds focus, because the same attribute that lets `focusCard`
|
|
54
|
+
* put focus here also makes the card CLICK-focusable, and a press that moves focus onto the
|
|
55
|
+
* card is what kills text selection on the steps pointing into an open modal. So the card
|
|
56
|
+
* drops the attribute the moment focus leaves, and this is the "leaves" test.
|
|
57
|
+
*
|
|
58
|
+
* Tabbing from the card onto its own Next button is NOT leaving: dropping the attribute
|
|
59
|
+
* there would make the card click-focusable again while the user is still inside it, which
|
|
60
|
+
* is the precise state the attribute is meant to be absent in.
|
|
61
|
+
*
|
|
62
|
+
* A `relatedTarget` of `null` (focus fell to `<body>`, as it does when the pressed control
|
|
63
|
+
* unmounts) counts as leaving. That is the honest reading: nothing inside the card holds
|
|
64
|
+
* focus any more.
|
|
65
|
+
*/
|
|
66
|
+
export function focusLeftCard(card: Node | null, relatedTarget: EventTarget | null): boolean {
|
|
67
|
+
if (!card) return true
|
|
68
|
+
if (!(relatedTarget instanceof Node)) return true
|
|
69
|
+
return !card.contains(relatedTarget)
|
|
70
|
+
}
|
|
71
|
+
|
|
49
72
|
/**
|
|
50
73
|
* What a `data-testid` may look like. Every one of the ~470 test ids in this layer is
|
|
51
74
|
* lowercase kebab-case, and the e2e suite's convention keeps it that way, so this rejects
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import type { CoachMarkLayout, TutorialRect, TutorialStep, TutorialTour } from '~/utils/tutorial'
|
|
12
12
|
import {
|
|
13
13
|
boardNodeIdFor,
|
|
14
|
+
focusLeftCard,
|
|
14
15
|
isTargetClickAdvance,
|
|
15
16
|
resolveSkip,
|
|
16
17
|
shouldFocusCard,
|
|
@@ -317,6 +318,28 @@ watch(step, async (s) => {
|
|
|
317
318
|
measure({ requery: true })
|
|
318
319
|
})
|
|
319
320
|
|
|
321
|
+
/**
|
|
322
|
+
* Whether the card currently carries `tabindex="-1"`.
|
|
323
|
+
*
|
|
324
|
+
* The attribute is NOT standing, and that is the whole design. It is what lets `focusCard`
|
|
325
|
+
* put focus on the card, and it equally makes the card CLICK-focusable, which is the half we
|
|
326
|
+
* never want: a press on the card's text focuses it, that focus move leaves an open modal's
|
|
327
|
+
* focus trap, the trap yanks focus back, and Chromium abandons the selection the press was
|
|
328
|
+
* starting (`selectstart` never fires). The one string a user reliably tries to copy, the
|
|
329
|
+
* sample repo slug in the add-service tour, could not be selected. `preventDefault` on the
|
|
330
|
+
* press is not the alternative: cancelling pointerdown or mousedown cancels the selection
|
|
331
|
+
* itself.
|
|
332
|
+
*
|
|
333
|
+
* So the card is focusable only across the window where it actually holds focus: applied by
|
|
334
|
+
* `focusCard` just before it focuses, dropped again by `onCardFocusOut`. Outside that window
|
|
335
|
+
* a press moves focus nowhere, on every input type, with no timing window to lose. Lifting
|
|
336
|
+
* the attribute for the DURATION OF A GESTURE cannot work instead, because the gesture has no
|
|
337
|
+
* reliable end event to restore on (a touch's compatibility `mousedown` arrives at touch END,
|
|
338
|
+
* long after any tick-scoped restore) and because `focusCard`'s own `await nextTick()` is a
|
|
339
|
+
* microtask that beats a macrotask restore, leaving `.focus()` a silent no-op.
|
|
340
|
+
*/
|
|
341
|
+
const cardFocusable = ref(false)
|
|
342
|
+
|
|
320
343
|
/**
|
|
321
344
|
* Put focus on the tooltip so the tour's own controls are one Tab away. WHETHER to do that is
|
|
322
345
|
* `shouldFocusCard`'s call, in the logic module, so it is pinned by a test — this function is
|
|
@@ -324,8 +347,22 @@ watch(step, async (s) => {
|
|
|
324
347
|
*/
|
|
325
348
|
async function focusCard(cause: TutorialAdvanceCause) {
|
|
326
349
|
if (!shouldFocusCard(cause)) return
|
|
350
|
+
cardFocusable.value = true
|
|
351
|
+
// One tick for BOTH the step's own re-render and the attribute above: `.focus()` on a card
|
|
352
|
+
// that is not yet focusable is a silent no-op, and focus then sits whereever the press left
|
|
353
|
+
// it (`<body>`, when the pressed control was a Back button that this step unmounts).
|
|
327
354
|
await nextTick()
|
|
328
|
-
cardEl.value
|
|
355
|
+
const el = cardEl.value
|
|
356
|
+
el?.focus({ preventScroll: true })
|
|
357
|
+
// Focus did not land: the card unmounted, or a modal's focus trap pulled it straight back.
|
|
358
|
+
// Clear the flag here rather than waiting for a `focusout` that will never fire, or the
|
|
359
|
+
// card is left click-focusable, which is exactly the state this is all avoiding.
|
|
360
|
+
if (!el || document.activeElement !== el) cardFocusable.value = false
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** Focus has left the card, so it stops being focusable until something focuses it again. */
|
|
364
|
+
function onCardFocusOut(event: FocusEvent) {
|
|
365
|
+
if (focusLeftCard(cardEl.value, event.relatedTarget)) cardFocusable.value = false
|
|
329
366
|
}
|
|
330
367
|
|
|
331
368
|
/**
|
|
@@ -520,20 +557,23 @@ onUnmounted(() => {
|
|
|
520
557
|
this card would close the user's half-filled form instead of pressing a button). -->
|
|
521
558
|
<!-- `tabindex="-1"` so `focusCard()` can put focus here when the tour starts and on every
|
|
522
559
|
Next/Back — without it a keyboard user has to tab the whole page to reach Next, since
|
|
523
|
-
this is teleported to the end of `body`.
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
560
|
+
this is teleported to the end of `body`. BOUND rather than static, and absent while
|
|
561
|
+
the card does not hold focus: see `cardFocusable`, without which a press on the card's
|
|
562
|
+
text focuses it and an open modal's focus trap cancels the selection being started.
|
|
563
|
+
No `aria-modal`: a coach mark is NOT modal, and half the catalog asks the user to
|
|
564
|
+
operate the real control behind it. No `aria-describedby` on the body either: the
|
|
565
|
+
live region above already reads the body as part of a complete announcement, and
|
|
566
|
+
pointing at it here would have every focus move read it a second time. -->
|
|
528
567
|
<div
|
|
529
568
|
ref="cardEl"
|
|
530
569
|
role="dialog"
|
|
531
|
-
tabindex="-1"
|
|
570
|
+
:tabindex="cardFocusable ? -1 : undefined"
|
|
532
571
|
:aria-label="t('tutorial.overlay.ariaLabel')"
|
|
533
572
|
class="pointer-events-auto fixed z-[70] w-80 max-w-[calc(100vw-16px)] rounded-xl border border-slate-700 bg-slate-900 p-4 shadow-2xl outline-none focus-visible:ring-2 focus-visible:ring-primary-400"
|
|
534
573
|
:style="{ top: `${layout.top}px`, left: `${layout.left}px` }"
|
|
535
574
|
data-testid="tutorial-tooltip"
|
|
536
575
|
@pointerdown.stop
|
|
576
|
+
@focusout="onCardFocusOut"
|
|
537
577
|
>
|
|
538
578
|
<div class="mb-1 flex items-start justify-between gap-3">
|
|
539
579
|
<h3 class="text-sm font-semibold text-slate-100">{{ t(step.titleKey) }}</h3>
|
package/app/pages/index.vue
CHANGED
|
@@ -1,15 +1,8 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import BoardCanvas from '~/components/board/BoardCanvas.vue'
|
|
3
3
|
import SideBar from '~/components/layout/SideBar.vue'
|
|
4
|
-
import
|
|
5
|
-
import SpendWarningBanner from '~/components/layout/SpendWarningBanner.vue'
|
|
6
|
-
import ConnectionStatusBanner from '~/components/layout/ConnectionStatusBanner.vue'
|
|
4
|
+
import BoardTopOverlays from '~/components/layout/BoardTopOverlays.vue'
|
|
7
5
|
import TranslationWarningBanner from '~/components/layout/TranslationWarningBanner.vue'
|
|
8
|
-
import GitHubPatBanner from '~/components/layout/GitHubPatBanner.vue'
|
|
9
|
-
import AiProvidersBanner from '~/components/layout/AiProvidersBanner.vue'
|
|
10
|
-
import ProviderConfigBanner from '~/components/layout/ProviderConfigBanner.vue'
|
|
11
|
-
import InfraSetupBanner from '~/components/layout/InfraSetupBanner.vue'
|
|
12
|
-
import DefaultTestEnvBanner from '~/components/layout/DefaultTestEnvBanner.vue'
|
|
13
6
|
// Always-mounted, fast-path surfaces (opened frequently during a run / board edits, or
|
|
14
7
|
// store-driven so they must react from anywhere — kept eager for snappy open/close).
|
|
15
8
|
import PipelineBuilder from '~/components/pipeline/PipelineBuilder.vue'
|
|
@@ -398,170 +391,138 @@ watch(
|
|
|
398
391
|
</script>
|
|
399
392
|
|
|
400
393
|
<template>
|
|
401
|
-
|
|
402
|
-
|
|
394
|
+
<!-- A COLUMN, so the translation strip below takes its own height instead of covering the
|
|
395
|
+
row: page chrome that overlays the app is how the board's top controls came to be
|
|
396
|
+
buried. Everything else lives in the row beneath it. -->
|
|
397
|
+
<div class="flex h-screen w-screen flex-col overflow-hidden bg-slate-950 text-slate-100">
|
|
398
|
+
<!-- Non-English locale warning (unofficial translation); slim full-width strip above
|
|
399
|
+
everything, in flow. -->
|
|
403
400
|
<TranslationWarningBanner />
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
- Infra-setup (this deployment needs an executor / test env / storage the operator hasn't
|
|
414
|
-
defined yet, so a class of agents can't run).
|
|
415
|
-
- Default test environment (this BOARD has never chosen the provisioning mechanism its
|
|
416
|
-
new services should default to). Last in the column: it asks for a convenience default,
|
|
417
|
-
so it yields to the prompts about things that are outright broken. -->
|
|
418
|
-
<div
|
|
419
|
-
v-if="workspace.ready && !needsGitHubInstall && !githubProbePending"
|
|
420
|
-
class="pointer-events-none absolute inset-x-0 top-0 z-40 flex flex-col items-center gap-2 px-4 pt-4"
|
|
421
|
-
>
|
|
422
|
-
<AiProvidersBanner />
|
|
423
|
-
<ProviderConfigBanner />
|
|
424
|
-
<InfraSetupBanner />
|
|
425
|
-
<DefaultTestEnvBanner />
|
|
426
|
-
</div>
|
|
427
|
-
|
|
428
|
-
<!-- Resolving whether the GitHub App is installed, before we decide what to show. -->
|
|
429
|
-
<div
|
|
430
|
-
v-if="workspace.ready && githubProbePending"
|
|
431
|
-
class="m-auto flex flex-col items-center gap-3 text-slate-400"
|
|
432
|
-
>
|
|
433
|
-
<UIcon name="i-lucide-loader" class="h-8 w-8 animate-spin" />
|
|
434
|
-
<span class="text-sm">{{ $t('app.loading') }}</span>
|
|
435
|
-
</div>
|
|
401
|
+
<div class="relative flex min-h-0 flex-1">
|
|
402
|
+
<!-- Resolving whether the GitHub App is installed, before we decide what to show. -->
|
|
403
|
+
<div
|
|
404
|
+
v-if="workspace.ready && githubProbePending"
|
|
405
|
+
class="m-auto flex flex-col items-center gap-3 text-slate-400"
|
|
406
|
+
>
|
|
407
|
+
<UIcon name="i-lucide-loader" class="h-8 w-8 animate-spin" />
|
|
408
|
+
<span class="text-sm">{{ $t('app.loading') }}</span>
|
|
409
|
+
</div>
|
|
436
410
|
|
|
437
|
-
|
|
438
|
-
|
|
411
|
+
<!-- App enabled but not installed on this workspace: hard onboarding gate. -->
|
|
412
|
+
<GitHubOnboarding v-else-if="workspace.ready && needsGitHubInstall" />
|
|
439
413
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
/>
|
|
452
|
-
<SideBar />
|
|
453
|
-
<main class="relative min-w-0 flex-1">
|
|
454
|
-
<BoardCanvas />
|
|
455
|
-
<!-- Compact-viewport navbar trigger: the SideBar is an off-canvas drawer below
|
|
456
|
-
lg, so surface a hamburger to open it. Hidden on lg+ (static sidebar). -->
|
|
457
|
-
<!-- z-30 keeps the trigger above the centered BoardToolbar (z-20), whose
|
|
458
|
-
max-width can otherwise reach this corner on the narrowest viewports. -->
|
|
459
|
-
<UButton
|
|
460
|
-
class="absolute start-3 top-3 z-30 lg:hidden"
|
|
461
|
-
icon="i-lucide-menu"
|
|
462
|
-
color="neutral"
|
|
463
|
-
variant="soft"
|
|
464
|
-
size="sm"
|
|
465
|
-
:aria-label="ui.mobileNavOpen ? $t('nav.closeMenu') : $t('nav.openMenu')"
|
|
466
|
-
data-testid="mobile-nav-toggle"
|
|
467
|
-
@click="ui.toggleMobileNav()"
|
|
414
|
+
<template v-else-if="workspace.ready">
|
|
415
|
+
<!-- Headless readiness marker for the e2e suite: reflects whether the real-time
|
|
416
|
+
WebSocket is actually connected (and thus subscribed + resynced). A live spec must
|
|
417
|
+
wait for this before driving a run, otherwise the run's first status events are
|
|
418
|
+
broadcast to a not-yet-subscribed browser and missed, leaving the card stuck on a
|
|
419
|
+
stale status until its assertion times out (the source of the e2e flakiness). Hidden
|
|
420
|
+
and inert; no visual or behavioural effect. -->
|
|
421
|
+
<span
|
|
422
|
+
data-testid="workspace-stream"
|
|
423
|
+
:data-connected="streamConnected ? 'true' : 'false'"
|
|
424
|
+
hidden
|
|
468
425
|
/>
|
|
469
|
-
<
|
|
470
|
-
<
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
426
|
+
<SideBar />
|
|
427
|
+
<main class="relative min-w-0 flex-1">
|
|
428
|
+
<BoardCanvas />
|
|
429
|
+
<!-- Toolbar, nav trigger and every advisory banner, in ONE stacked region that owns
|
|
430
|
+
their placement, so no two of them can cover each other. -->
|
|
431
|
+
<BoardTopOverlays
|
|
432
|
+
:connected="streamConnected"
|
|
433
|
+
:ever-connected="streamEverConnected"
|
|
434
|
+
:connection-failed="streamConnectionFailed"
|
|
435
|
+
/>
|
|
436
|
+
<InspectorPanel />
|
|
437
|
+
<!-- Code-split focus view. The fade lives here (not inside the component) so the
|
|
438
|
+
leave animation still plays when `focusBlockId` clears and the v-if unmounts
|
|
439
|
+
the chunk — an inner Transition would be torn down before it could run. -->
|
|
440
|
+
<Transition name="focus-fade">
|
|
441
|
+
<BlockFocusView v-if="ui.focusBlockId" />
|
|
442
|
+
</Transition>
|
|
443
|
+
</main>
|
|
484
444
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
445
|
+
<!-- Always-mounted, fast-path surfaces. -->
|
|
446
|
+
<PipelineBuilder />
|
|
447
|
+
<DecisionModal />
|
|
448
|
+
<AgentStepDetail />
|
|
449
|
+
<StepResultViewHost />
|
|
450
|
+
<!-- Consumer-contributed top-level overlays (extension slice D). Renders nothing until a
|
|
451
|
+
consumer opens one via `ui.openOverlay` / `useAppOverlays().open(...)`. -->
|
|
452
|
+
<AppOverlayHost />
|
|
453
|
+
<AddTaskModal />
|
|
454
|
+
<ReviewFrictionDialog v-if="ui.reviewFrictionContext" />
|
|
455
|
+
<CreateInitiativeModal />
|
|
456
|
+
<CommandBar />
|
|
457
|
+
<PersonalCredentialModal />
|
|
458
|
+
<ConfirmDialog />
|
|
459
|
+
<KeyboardShortcutsHelp />
|
|
500
460
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
461
|
+
<!-- Lazy panels: mounted only while their ui open-flag is set, so each loads on
|
|
462
|
+
first open (its own chunk) rather than bloating the initial bundle. -->
|
|
463
|
+
<TaskSourceConnectModal v-if="ui.taskConnect" />
|
|
464
|
+
<TaskImportModal v-if="ui.taskImport" />
|
|
465
|
+
<BugHuntModal v-if="ui.bugHunt" />
|
|
466
|
+
<RecurringPipelineModal v-if="ui.addRecurringFrameId" />
|
|
467
|
+
<ObservabilityPanel v-if="ui.observabilityInstanceId" />
|
|
468
|
+
<OperatorDashboardPanel v-if="ui.operatorDashboardOpen" />
|
|
469
|
+
<ReportsPanel v-if="ui.reportsOpen" />
|
|
470
|
+
<KaizenPanel v-if="ui.kaizenScreenOpen" />
|
|
471
|
+
<DocumentSourceConnectModal v-if="ui.documentConnect" />
|
|
472
|
+
<DocumentImportModal v-if="ui.documentImport" />
|
|
473
|
+
<DocumentTemplatesModal v-if="ui.documentTemplates" />
|
|
474
|
+
<SpawnPreviewModal v-if="ui.spawnPreview" />
|
|
475
|
+
<BootstrapModal v-if="ui.bootstrapOpen" />
|
|
476
|
+
<AddServiceFromRepoModal v-if="ui.addServiceOpen" />
|
|
477
|
+
<GitHubPanel v-if="ui.githubOpen" />
|
|
478
|
+
<SlackPanel v-if="ui.slackOpen" />
|
|
479
|
+
<FragmentLibraryPanel v-if="ui.fragmentLibraryOpen" />
|
|
480
|
+
<FoundationalServicePanel v-if="ui.foundationalServicesOpen" />
|
|
481
|
+
<PipelineHealthModal v-if="ui.pipelineHealthOpen" />
|
|
482
|
+
<RiskPolicyHealthModal v-if="ui.riskPolicyHealthOpen" />
|
|
483
|
+
<ModelPresetHealthModal v-if="ui.modelPresetHealthOpen" />
|
|
484
|
+
<IntegrationsHub v-if="ui.integrationsOpen" />
|
|
485
|
+
<ModelProvidersHub v-if="ui.modelProvidersOpen" />
|
|
486
|
+
<PersonalSetupModal v-if="ui.personalSetupOpen" />
|
|
487
|
+
<WorkspaceSettingsPanel v-if="ui.workspaceSettingsOpen" />
|
|
488
|
+
<AccountSettingsPanel v-if="ui.accountSettingsOpen" />
|
|
489
|
+
<ObservabilityConnectionPanel v-if="ui.observabilityConnectionOpen" />
|
|
490
|
+
<ApiTokensPanel v-if="ui.apiTokensOpen" />
|
|
491
|
+
<InfrastructureWindow v-if="ui.infrastructureOpen" />
|
|
492
|
+
<EnvironmentSetupWizard v-if="ui.environmentWizardOpen" />
|
|
493
|
+
<ModelConfigurationPanel v-if="ui.modelConfigOpen" />
|
|
494
|
+
<LocalModelEndpointsPanel v-if="ui.localModelsOpen" />
|
|
495
|
+
<SandboxPanel v-if="ui.sandboxOpen" />
|
|
496
|
+
<UserSecretsSection v-if="ui.userSecretsOpen" />
|
|
497
|
+
<OpenRouterCatalogPanel v-if="ui.openRouterOpen" />
|
|
498
|
+
<VendorCredentialsModal v-if="ui.vendorCredentialsOpen" />
|
|
499
|
+
<AiProviderOnboardingModal v-if="ui.aiProviderSetupOpen" />
|
|
500
|
+
<AiPresetMismatchDialog v-if="ui.aiPresetMismatchOpen" />
|
|
501
|
+
<TutorialPrompt v-if="tutorial.promptOpen" />
|
|
502
|
+
<TutorialCatalogue v-if="tutorial.catalogueOpen" />
|
|
503
|
+
<TutorialOverlay v-if="tutorial.touring" />
|
|
504
|
+
<!-- Mounted off the PENDING id rather than off whether it may currently be SHOWN: the
|
|
505
|
+
component holds a suppressed offer (a tour is running, a tutorial window is open) and
|
|
506
|
+
renders it once the way is clear, which is the whole reason the offer survives the
|
|
507
|
+
moment it was raised in. -->
|
|
508
|
+
<TutorialNudge v-if="tutorial.pendingNudgeId" />
|
|
509
|
+
</template>
|
|
550
510
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
511
|
+
<!-- Backend unreachable / bootstrap failed -->
|
|
512
|
+
<div v-else-if="workspace.error" class="m-auto max-w-md p-8 text-center">
|
|
513
|
+
<UIcon name="i-lucide-plug-zap" class="mx-auto mb-3 h-10 w-10 text-amber-400" />
|
|
514
|
+
<h1 class="mb-1 text-lg font-semibold">{{ $t('app.backendUnreachable') }}</h1>
|
|
515
|
+
<p class="mb-4 text-sm text-slate-400">{{ workspace.error }}</p>
|
|
516
|
+
<UButton color="primary" icon="i-lucide-rotate-ccw" @click="workspace.init()">
|
|
517
|
+
{{ $t('common.retry') }}
|
|
518
|
+
</UButton>
|
|
519
|
+
</div>
|
|
560
520
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
521
|
+
<!-- Initial load -->
|
|
522
|
+
<div v-else class="m-auto flex flex-col items-center gap-3 text-slate-400">
|
|
523
|
+
<UIcon name="i-lucide-loader" class="h-8 w-8 animate-spin" />
|
|
524
|
+
<span class="text-sm">{{ $t('app.loadingBoard') }}</span>
|
|
525
|
+
</div>
|
|
565
526
|
</div>
|
|
566
527
|
</div>
|
|
567
528
|
</template>
|
package/i18n/locales/de.json
CHANGED
|
@@ -6984,7 +6984,7 @@
|
|
|
6984
6984
|
},
|
|
6985
6985
|
"commandBar": {
|
|
6986
6986
|
"title": "Die Befehlspalette",
|
|
6987
|
-
"body": "Der schnellste Weg zu jeder Aktion: öffnen und lostippen.
|
|
6987
|
+
"body": "Der schnellste Weg zu jeder Aktion: öffnen und lostippen. Um dieses Tutorial jederzeit neu zu starten, wähle dort „@:{'nav.tutorials'}“."
|
|
6988
6988
|
},
|
|
6989
6989
|
"toolbar": {
|
|
6990
6990
|
"title": "Board-Steuerung",
|
package/i18n/locales/en.json
CHANGED
|
@@ -7236,7 +7236,7 @@
|
|
|
7236
7236
|
},
|
|
7237
7237
|
"commandBar": {
|
|
7238
7238
|
"title": "The command palette",
|
|
7239
|
-
"body": "The fastest way to reach any action: open it and start typing.
|
|
7239
|
+
"body": "The fastest way to reach any action: open it and start typing. To restart this tutorial at any time, pick \"@:{'nav.tutorials'}\" there."
|
|
7240
7240
|
},
|
|
7241
7241
|
"toolbar": {
|
|
7242
7242
|
"title": "Board controls",
|
package/i18n/locales/es.json
CHANGED
|
@@ -6984,7 +6984,7 @@
|
|
|
6984
6984
|
},
|
|
6985
6985
|
"commandBar": {
|
|
6986
6986
|
"title": "La paleta de comandos",
|
|
6987
|
-
"body": "La forma más rápida de llegar a cualquier acción: ábrela y empieza a escribir.
|
|
6987
|
+
"body": "La forma más rápida de llegar a cualquier acción: ábrela y empieza a escribir. Para reiniciar este tutorial en cualquier momento, elige ahí \"@:{'nav.tutorials'}\"."
|
|
6988
6988
|
},
|
|
6989
6989
|
"toolbar": {
|
|
6990
6990
|
"title": "Controles del tablero",
|
package/i18n/locales/fr.json
CHANGED
|
@@ -6984,7 +6984,7 @@
|
|
|
6984
6984
|
},
|
|
6985
6985
|
"commandBar": {
|
|
6986
6986
|
"title": "La palette de commandes",
|
|
6987
|
-
"body": "Le moyen le plus rapide d'atteindre n'importe quelle action : ouvrez-la et commencez à taper.
|
|
6987
|
+
"body": "Le moyen le plus rapide d'atteindre n'importe quelle action : ouvrez-la et commencez à taper. Pour relancer ce tutoriel à tout moment, choisissez-y « @:{'nav.tutorials'} »."
|
|
6988
6988
|
},
|
|
6989
6989
|
"toolbar": {
|
|
6990
6990
|
"title": "Contrôles du tableau",
|
package/i18n/locales/he.json
CHANGED
|
@@ -6984,7 +6984,7 @@
|
|
|
6984
6984
|
},
|
|
6985
6985
|
"commandBar": {
|
|
6986
6986
|
"title": "לוח הפקודות",
|
|
6987
|
-
"body": "הדרך המהירה ביותר להגיע לכל פעולה: פתח והתחל להקליד.
|
|
6987
|
+
"body": "הדרך המהירה ביותר להגיע לכל פעולה: פתח והתחל להקליד. כדי להפעיל מחדש את המדריך הזה בכל עת, בחר שם \"@:{'nav.tutorials'}\"."
|
|
6988
6988
|
},
|
|
6989
6989
|
"toolbar": {
|
|
6990
6990
|
"title": "פקדי הלוח",
|
package/i18n/locales/it.json
CHANGED
|
@@ -6984,7 +6984,7 @@
|
|
|
6984
6984
|
},
|
|
6985
6985
|
"commandBar": {
|
|
6986
6986
|
"title": "La palette dei comandi",
|
|
6987
|
-
"body": "Il modo più rapido per raggiungere qualsiasi azione: aprila e inizia a digitare.
|
|
6987
|
+
"body": "Il modo più rapido per raggiungere qualsiasi azione: aprila e inizia a digitare. Per riavviare questo tutorial in qualsiasi momento, scegli lì \"@:{'nav.tutorials'}\"."
|
|
6988
6988
|
},
|
|
6989
6989
|
"toolbar": {
|
|
6990
6990
|
"title": "Controlli della board",
|
package/i18n/locales/ja.json
CHANGED
|
@@ -6984,7 +6984,7 @@
|
|
|
6984
6984
|
},
|
|
6985
6985
|
"commandBar": {
|
|
6986
6986
|
"title": "コマンドパレット",
|
|
6987
|
-
"body": "
|
|
6987
|
+
"body": "あらゆる操作への最短ルートです。開いて入力を始めるだけ。このチュートリアルをやり直したいときは、ここで「@:{'nav.tutorials'}」を選んでください。"
|
|
6988
6988
|
},
|
|
6989
6989
|
"toolbar": {
|
|
6990
6990
|
"title": "ボードの操作",
|
package/i18n/locales/pl.json
CHANGED
|
@@ -6984,7 +6984,7 @@
|
|
|
6984
6984
|
},
|
|
6985
6985
|
"commandBar": {
|
|
6986
6986
|
"title": "Paleta poleceń",
|
|
6987
|
-
"body": "Najszybsza droga do każdej akcji: otwórz i zacznij pisać.
|
|
6987
|
+
"body": "Najszybsza droga do każdej akcji: otwórz i zacznij pisać. Aby w każdej chwili ponownie uruchomić ten samouczek, wybierz tam „@:{'nav.tutorials'}”."
|
|
6988
6988
|
},
|
|
6989
6989
|
"toolbar": {
|
|
6990
6990
|
"title": "Sterowanie tablicą",
|
package/i18n/locales/tr.json
CHANGED
|
@@ -6984,7 +6984,7 @@
|
|
|
6984
6984
|
},
|
|
6985
6985
|
"commandBar": {
|
|
6986
6986
|
"title": "Komut paleti",
|
|
6987
|
-
"body": "Herhangi bir eyleme ulaşmanın en hızlı yolu: açın ve yazmaya başlayın. Bu eğitimi
|
|
6987
|
+
"body": "Herhangi bir eyleme ulaşmanın en hızlı yolu: açın ve yazmaya başlayın. Bu eğitimi istediğiniz zaman yeniden başlatmak için oradan \"@:{'nav.tutorials'}\" seçeneğini seçin."
|
|
6988
6988
|
},
|
|
6989
6989
|
"toolbar": {
|
|
6990
6990
|
"title": "Pano denetimleri",
|
package/i18n/locales/uk.json
CHANGED
|
@@ -6984,7 +6984,7 @@
|
|
|
6984
6984
|
},
|
|
6985
6985
|
"commandBar": {
|
|
6986
6986
|
"title": "Палітра команд",
|
|
6987
|
-
"body": "Найшвидший шлях до будь-якої дії: відкрийте й почніть друкувати.
|
|
6987
|
+
"body": "Найшвидший шлях до будь-якої дії: відкрийте й почніть друкувати. Щоб будь-коли перезапустити цей тур, виберіть там «@:{'nav.tutorials'}»."
|
|
6988
6988
|
},
|
|
6989
6989
|
"toolbar": {
|
|
6990
6990
|
"title": "Керування дошкою",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/app",
|
|
3
|
-
"version": "0.242.
|
|
3
|
+
"version": "0.242.2",
|
|
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",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"valibot": "^1.4.2",
|
|
41
41
|
"vue": "3.5.40",
|
|
42
42
|
"wretch": "^3.0.9",
|
|
43
|
-
"@cat-factory/contracts": "0.
|
|
43
|
+
"@cat-factory/contracts": "0.264.0"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@toad-contracts/testing": "0.3.2",
|