@cat-factory/app 0.46.9 → 0.46.10
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/board/AddTaskModal.vue +106 -88
- package/app/components/board/AgentFailureCard.vue +8 -5
- package/app/components/board/AgentStopButton.vue +8 -5
- package/app/components/board/BoardCanvas.vue +13 -9
- package/app/components/board/RecurringPipelineModal.vue +33 -23
- package/app/components/board/nodes/BlockNode.vue +50 -34
- package/app/components/board/nodes/DecisionBadge.vue +6 -3
- package/app/components/board/nodes/DraggableTask.vue +2 -1
- package/app/components/board/nodes/EpicNode.vue +10 -3
- package/app/components/board/nodes/ModuleFrame.vue +6 -5
- package/app/components/board/nodes/TaskCard.vue +40 -22
- package/app/components/board/nodes/TaskPipelineMini.vue +4 -3
- package/app/components/layout/BoardToolbar.vue +26 -19
- package/i18n/locales/en.json +204 -0
- package/i18n/locales/es.json +194 -2
- package/i18n/locales/fr.json +194 -2
- package/i18n/locales/pl.json +194 -2
- package/i18n/locales/uk.json +194 -2
- package/package.json +1 -1
|
@@ -13,6 +13,7 @@ const pipelines = usePipelinesStore()
|
|
|
13
13
|
const recurring = useRecurringPipelinesStore()
|
|
14
14
|
const tracker = useTrackerStore()
|
|
15
15
|
const toast = useToast()
|
|
16
|
+
const { t } = useI18n()
|
|
16
17
|
|
|
17
18
|
const open = computed({
|
|
18
19
|
get: () => ui.addRecurringFrameId !== null,
|
|
@@ -54,7 +55,9 @@ const pipelineMenu = computed(() => [
|
|
|
54
55
|
})),
|
|
55
56
|
])
|
|
56
57
|
const selectedPipeline = computed(() => pipelines.getPipeline(pipelineId.value))
|
|
57
|
-
const selectedPipelineLabel = computed(
|
|
58
|
+
const selectedPipelineLabel = computed(
|
|
59
|
+
() => selectedPipeline.value?.name ?? t('board.recurring.pickPipeline'),
|
|
60
|
+
)
|
|
58
61
|
|
|
59
62
|
// Infer the template from the picked pipeline so the backend seeds the right block
|
|
60
63
|
// description (and so we know to show the tracker config).
|
|
@@ -108,7 +111,7 @@ async function add() {
|
|
|
108
111
|
ui.closeAddRecurring()
|
|
109
112
|
} catch (e) {
|
|
110
113
|
toast.add({
|
|
111
|
-
title: '
|
|
114
|
+
title: t('board.recurring.addFailedTitle'),
|
|
112
115
|
description: e instanceof Error ? e.message : String(e),
|
|
113
116
|
icon: 'i-lucide-triangle-alert',
|
|
114
117
|
color: 'error',
|
|
@@ -120,24 +123,27 @@ async function add() {
|
|
|
120
123
|
</script>
|
|
121
124
|
|
|
122
125
|
<template>
|
|
123
|
-
<UModal v-model:open="open" title="
|
|
126
|
+
<UModal v-model:open="open" :title="t('board.recurring.title')">
|
|
124
127
|
<template #body>
|
|
125
128
|
<div class="space-y-4">
|
|
126
129
|
<p v-if="frame" class="text-xs text-slate-400">
|
|
127
|
-
|
|
128
|
-
|
|
130
|
+
<i18n-t keypath="board.recurring.on" tag="span" scope="global">
|
|
131
|
+
<template #frame>
|
|
132
|
+
<span class="font-medium text-slate-200">{{ frame.title }}</span>
|
|
133
|
+
</template>
|
|
134
|
+
</i18n-t>
|
|
129
135
|
</p>
|
|
130
136
|
|
|
131
|
-
<UFormField label="
|
|
137
|
+
<UFormField :label="t('board.recurring.name')" required>
|
|
132
138
|
<UInput
|
|
133
139
|
v-model="name"
|
|
134
|
-
placeholder="
|
|
140
|
+
:placeholder="t('board.recurring.namePlaceholder')"
|
|
135
141
|
autofocus
|
|
136
142
|
class="w-full"
|
|
137
143
|
/>
|
|
138
144
|
</UFormField>
|
|
139
145
|
|
|
140
|
-
<UFormField label="
|
|
146
|
+
<UFormField :label="t('board.recurring.pipeline')" required>
|
|
141
147
|
<UDropdownMenu :items="pipelineMenu" class="w-full">
|
|
142
148
|
<UButton
|
|
143
149
|
color="neutral"
|
|
@@ -152,12 +158,12 @@ async function add() {
|
|
|
152
158
|
</UDropdownMenu>
|
|
153
159
|
</UFormField>
|
|
154
160
|
|
|
155
|
-
<UFormField label="
|
|
161
|
+
<UFormField :label="t('board.recurring.prompt')">
|
|
156
162
|
<UTextarea
|
|
157
163
|
v-model="description"
|
|
158
164
|
:rows="3"
|
|
159
165
|
autoresize
|
|
160
|
-
placeholder="
|
|
166
|
+
:placeholder="t('board.recurring.promptPlaceholder')"
|
|
161
167
|
class="w-full"
|
|
162
168
|
/>
|
|
163
169
|
</UFormField>
|
|
@@ -166,11 +172,10 @@ async function add() {
|
|
|
166
172
|
|
|
167
173
|
<div v-if="isTechDebt" class="space-y-3 rounded-lg border border-slate-800 p-3">
|
|
168
174
|
<p class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
169
|
-
|
|
175
|
+
{{ t('board.recurring.issueTracker') }}
|
|
170
176
|
</p>
|
|
171
177
|
<p class="text-[11px] text-slate-500">
|
|
172
|
-
|
|
173
|
-
where (saved for the whole workspace).
|
|
178
|
+
{{ t('board.recurring.issueTrackerHint') }}
|
|
174
179
|
</p>
|
|
175
180
|
<div class="flex gap-1">
|
|
176
181
|
<UButton
|
|
@@ -180,7 +185,7 @@ async function add() {
|
|
|
180
185
|
icon="i-lucide-github"
|
|
181
186
|
@click="trackerKind = 'github'"
|
|
182
187
|
>
|
|
183
|
-
|
|
188
|
+
{{ t('board.recurring.githubIssues') }}
|
|
184
189
|
</UButton>
|
|
185
190
|
<UButton
|
|
186
191
|
size="xs"
|
|
@@ -189,7 +194,7 @@ async function add() {
|
|
|
189
194
|
icon="i-lucide-square-check"
|
|
190
195
|
@click="trackerKind = 'jira'"
|
|
191
196
|
>
|
|
192
|
-
|
|
197
|
+
{{ t('board.recurring.jira') }}
|
|
193
198
|
</UButton>
|
|
194
199
|
<UButton
|
|
195
200
|
size="xs"
|
|
@@ -198,27 +203,32 @@ async function add() {
|
|
|
198
203
|
icon="i-lucide-square-kanban"
|
|
199
204
|
@click="trackerKind = 'linear'"
|
|
200
205
|
>
|
|
201
|
-
|
|
206
|
+
{{ t('board.recurring.linear') }}
|
|
202
207
|
</UButton>
|
|
203
208
|
</div>
|
|
204
|
-
<UFormField v-if="trackerKind === 'jira'" label="
|
|
205
|
-
<UInput
|
|
209
|
+
<UFormField v-if="trackerKind === 'jira'" :label="t('board.recurring.jiraProjectKey')">
|
|
210
|
+
<UInput
|
|
211
|
+
v-model="jiraProjectKey"
|
|
212
|
+
:placeholder="t('board.recurring.jiraProjectKeyPlaceholder')"
|
|
213
|
+
class="w-full"
|
|
214
|
+
/>
|
|
206
215
|
</UFormField>
|
|
207
|
-
<UFormField v-if="trackerKind === 'linear'" label="
|
|
216
|
+
<UFormField v-if="trackerKind === 'linear'" :label="t('board.recurring.linearTeamId')">
|
|
208
217
|
<UInput v-model="linearTeamId" placeholder="team_…" class="w-full" />
|
|
209
218
|
</UFormField>
|
|
210
219
|
</div>
|
|
211
220
|
|
|
212
221
|
<p class="text-[11px] text-slate-500">
|
|
213
|
-
|
|
214
|
-
history is visible in the inspector.
|
|
222
|
+
{{ t('board.recurring.footerHint') }}
|
|
215
223
|
</p>
|
|
216
224
|
</div>
|
|
217
225
|
</template>
|
|
218
226
|
|
|
219
227
|
<template #footer>
|
|
220
228
|
<div class="flex w-full justify-end gap-2">
|
|
221
|
-
<UButton color="neutral" variant="ghost" @click="ui.closeAddRecurring()">
|
|
229
|
+
<UButton color="neutral" variant="ghost" @click="ui.closeAddRecurring()">{{
|
|
230
|
+
t('common.cancel')
|
|
231
|
+
}}</UButton>
|
|
222
232
|
<UButton
|
|
223
233
|
color="primary"
|
|
224
234
|
icon="i-lucide-repeat"
|
|
@@ -226,7 +236,7 @@ async function add() {
|
|
|
226
236
|
:disabled="!canAdd"
|
|
227
237
|
@click="add"
|
|
228
238
|
>
|
|
229
|
-
|
|
239
|
+
{{ t('board.recurring.submit') }}
|
|
230
240
|
</UButton>
|
|
231
241
|
</div>
|
|
232
242
|
</template>
|
|
@@ -22,6 +22,7 @@ const tasks = useTasksStore()
|
|
|
22
22
|
const agentRuns = useAgentRunsStore()
|
|
23
23
|
const services = useServicesStore()
|
|
24
24
|
const reviews = useReviewStage()
|
|
25
|
+
const { t } = useI18n()
|
|
25
26
|
const { lod } = useSemanticZoom()
|
|
26
27
|
// Coarse-pointer (touch) bumps the frame-header actions from `xs` to `sm` so
|
|
27
28
|
// they clear a comfortable tap target on phones without affecting mouse desktops.
|
|
@@ -57,15 +58,17 @@ const canvas = computed(() => board.containerSize(props.id))
|
|
|
57
58
|
const frameStatus = computed<BlockStatus>(() => board.frameStatus(props.id))
|
|
58
59
|
const statusMeta = computed(() => STATUS_META[frameStatus.value])
|
|
59
60
|
const accent = computed(() => statusMeta.value.color)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
61
|
+
// Exhaustive (tier-2) status → label-key map: a new BlockStatus without a label
|
|
62
|
+
// fails the typecheck rather than leaking a raw key into the frame badge.
|
|
63
|
+
const FRAME_LABEL_KEYS: Record<BlockStatus, string> = {
|
|
64
|
+
planned: 'board.frame.status.planned',
|
|
65
|
+
ready: 'board.frame.status.ready',
|
|
66
|
+
in_progress: 'board.frame.status.in_progress',
|
|
67
|
+
blocked: 'board.frame.status.blocked',
|
|
68
|
+
pr_ready: 'board.frame.status.pr_ready',
|
|
69
|
+
done: 'board.frame.status.done',
|
|
67
70
|
}
|
|
68
|
-
const statusLabel = computed(() =>
|
|
71
|
+
const statusLabel = computed(() => t(FRAME_LABEL_KEYS[frameStatus.value]))
|
|
69
72
|
|
|
70
73
|
const selected = computed(() => ui.selectedBlockId === props.id)
|
|
71
74
|
// Services are always expanded to their task canvas, at every zoom level: there is no
|
|
@@ -211,7 +214,7 @@ const ITEM_ICON: Record<string, string> = {
|
|
|
211
214
|
v-if="blockApprovals.length"
|
|
212
215
|
:count="blockApprovals.length"
|
|
213
216
|
:compact="lod === 'far'"
|
|
214
|
-
label="
|
|
217
|
+
:label="t('board.decisionBadge.approvalNeeded')"
|
|
215
218
|
icon="i-lucide-shield-check"
|
|
216
219
|
@open="openFirstApproval"
|
|
217
220
|
/>
|
|
@@ -232,13 +235,13 @@ const ITEM_ICON: Record<string, string> = {
|
|
|
232
235
|
v-if="bootstrapping"
|
|
233
236
|
name="i-lucide-loader-circle"
|
|
234
237
|
class="ml-auto h-3.5 w-3.5 shrink-0 animate-spin text-amber-400"
|
|
235
|
-
title="
|
|
238
|
+
:title="t('board.frame.bootstrapping')"
|
|
236
239
|
/>
|
|
237
240
|
<UIcon
|
|
238
241
|
v-else-if="runFailed"
|
|
239
242
|
name="i-lucide-alert-triangle"
|
|
240
243
|
class="ml-auto h-3.5 w-3.5 shrink-0 text-rose-400"
|
|
241
|
-
title="
|
|
244
|
+
:title="t('board.frame.runFailed')"
|
|
242
245
|
/>
|
|
243
246
|
<span v-else-if="hasTasks" class="ml-auto shrink-0 text-[11px] text-slate-300">
|
|
244
247
|
{{ mergedTasks }}/{{ taskCount }}
|
|
@@ -259,7 +262,7 @@ const ITEM_ICON: Record<string, string> = {
|
|
|
259
262
|
name="i-lucide-loader-circle"
|
|
260
263
|
class="h-3.5 w-3.5 shrink-0 animate-spin text-amber-400"
|
|
261
264
|
/>
|
|
262
|
-
<span class="text-amber-300">
|
|
265
|
+
<span class="text-amber-300">{{ t('board.frame.bootstrapping') }}</span>
|
|
263
266
|
<span v-if="bootstrapSubtasks" class="ml-auto text-amber-200/80">
|
|
264
267
|
{{ bootstrapSubtasks.completed }}/{{ bootstrapSubtasks.total }}
|
|
265
268
|
</span>
|
|
@@ -292,18 +295,18 @@ const ITEM_ICON: Record<string, string> = {
|
|
|
292
295
|
variant="subtle"
|
|
293
296
|
size="sm"
|
|
294
297
|
class="shrink-0"
|
|
295
|
-
title="
|
|
298
|
+
:title="t('board.frame.sharedTitle')"
|
|
296
299
|
>
|
|
297
|
-
|
|
300
|
+
{{ t('board.frame.shared') }}
|
|
298
301
|
</UBadge>
|
|
299
302
|
</div>
|
|
300
303
|
<div class="flex items-center justify-between">
|
|
301
304
|
<UBadge :color="statusMeta.chip as any" variant="subtle" size="sm">{{
|
|
302
305
|
statusLabel
|
|
303
306
|
}}</UBadge>
|
|
304
|
-
<span class="text-[11px] text-slate-400"
|
|
305
|
-
|
|
306
|
-
>
|
|
307
|
+
<span class="text-[11px] text-slate-400">{{
|
|
308
|
+
t('board.frame.taskCount', { count: taskCount }, taskCount)
|
|
309
|
+
}}</span>
|
|
307
310
|
</div>
|
|
308
311
|
<button
|
|
309
312
|
type="button"
|
|
@@ -311,9 +314,13 @@ const ITEM_ICON: Record<string, string> = {
|
|
|
311
314
|
@click.stop="toggleExpand"
|
|
312
315
|
>
|
|
313
316
|
<UIcon name="i-lucide-layers" class="h-3 w-3 text-slate-400" />
|
|
314
|
-
<span v-if="hasTasks">{{
|
|
315
|
-
|
|
316
|
-
|
|
317
|
+
<span v-if="hasTasks">{{
|
|
318
|
+
t('board.frame.mergedOfTotal', { merged: mergedTasks, total: taskCount })
|
|
319
|
+
}}</span>
|
|
320
|
+
<span v-else>{{ t('board.frame.noTasksYet') }}</span>
|
|
321
|
+
<span v-if="prTasks" class="text-emerald-400"
|
|
322
|
+
>· {{ t('board.frame.prCount', { count: prTasks }) }}</span
|
|
323
|
+
>
|
|
317
324
|
<UIcon name="i-lucide-chevron-down" class="ml-auto h-3 w-3" />
|
|
318
325
|
</button>
|
|
319
326
|
</div>
|
|
@@ -333,9 +340,14 @@ const ITEM_ICON: Record<string, string> = {
|
|
|
333
340
|
name="i-lucide-loader-circle"
|
|
334
341
|
class="h-4 w-4 shrink-0 animate-spin text-amber-400"
|
|
335
342
|
/>
|
|
336
|
-
<span class="text-amber-300">
|
|
343
|
+
<span class="text-amber-300">{{ t('board.frame.bootstrappingRepository') }}</span>
|
|
337
344
|
<span v-if="bootstrapSubtasks" class="ml-auto text-amber-200/80">
|
|
338
|
-
{{
|
|
345
|
+
{{
|
|
346
|
+
t('board.frame.bootstrapStepsCount', {
|
|
347
|
+
completed: bootstrapSubtasks.completed,
|
|
348
|
+
total: bootstrapSubtasks.total,
|
|
349
|
+
})
|
|
350
|
+
}}
|
|
339
351
|
</span>
|
|
340
352
|
</div>
|
|
341
353
|
<div class="mt-1.5 h-1 w-full overflow-hidden rounded bg-amber-900/40">
|
|
@@ -389,7 +401,7 @@ const ITEM_ICON: Record<string, string> = {
|
|
|
389
401
|
the gap). -->
|
|
390
402
|
<div
|
|
391
403
|
class="nopan cursor-grab touch-none space-y-3 pb-3 active:cursor-grabbing"
|
|
392
|
-
title="
|
|
404
|
+
:title="t('board.frame.dragService')"
|
|
393
405
|
@pointerdown="onFrameHandle"
|
|
394
406
|
>
|
|
395
407
|
<div class="flex items-start justify-between gap-2">
|
|
@@ -420,7 +432,7 @@ const ITEM_ICON: Record<string, string> = {
|
|
|
420
432
|
variant="ghost"
|
|
421
433
|
color="neutral"
|
|
422
434
|
icon="i-lucide-plus"
|
|
423
|
-
title="
|
|
435
|
+
:title="t('board.frame.addTaskTitle')"
|
|
424
436
|
@click.stop="addTask"
|
|
425
437
|
/>
|
|
426
438
|
<UButton
|
|
@@ -430,7 +442,7 @@ const ITEM_ICON: Record<string, string> = {
|
|
|
430
442
|
variant="ghost"
|
|
431
443
|
color="neutral"
|
|
432
444
|
icon="i-lucide-ticket"
|
|
433
|
-
title="
|
|
445
|
+
:title="t('board.frame.createTaskFromIssueTitle')"
|
|
434
446
|
@click.stop="createTaskFromIssue"
|
|
435
447
|
/>
|
|
436
448
|
<UButton
|
|
@@ -439,7 +451,7 @@ const ITEM_ICON: Record<string, string> = {
|
|
|
439
451
|
variant="ghost"
|
|
440
452
|
color="neutral"
|
|
441
453
|
icon="i-lucide-repeat"
|
|
442
|
-
title="
|
|
454
|
+
:title="t('board.frame.addRecurringTitle')"
|
|
443
455
|
@click.stop="addRecurring"
|
|
444
456
|
/>
|
|
445
457
|
<UButton
|
|
@@ -448,18 +460,22 @@ const ITEM_ICON: Record<string, string> = {
|
|
|
448
460
|
variant="ghost"
|
|
449
461
|
color="neutral"
|
|
450
462
|
icon="i-lucide-chevron-up"
|
|
451
|
-
title="
|
|
463
|
+
:title="t('board.frame.collapseTitle')"
|
|
452
464
|
@click.stop="toggleExpand"
|
|
453
465
|
/>
|
|
454
466
|
</div>
|
|
455
467
|
</div>
|
|
456
468
|
|
|
457
469
|
<div class="flex items-center gap-2 text-[10px] uppercase tracking-wide text-slate-500">
|
|
458
|
-
<span>{{
|
|
470
|
+
<span>{{
|
|
471
|
+
t('board.frame.implemented', { merged: mergedTasks, total: taskCount })
|
|
472
|
+
}}</span>
|
|
459
473
|
<span v-if="modules.length"
|
|
460
|
-
>· {{ modules.length }
|
|
474
|
+
>· {{ t('board.frame.moduleCount', { count: modules.length }, modules.length) }}</span
|
|
475
|
+
>
|
|
476
|
+
<span v-if="prTasks" class="text-emerald-400"
|
|
477
|
+
>· {{ t('board.frame.prReadyCount', { count: prTasks }) }}</span
|
|
461
478
|
>
|
|
462
|
-
<span v-if="prTasks" class="text-emerald-400">· {{ prTasks }} PR ready</span>
|
|
463
479
|
</div>
|
|
464
480
|
</div>
|
|
465
481
|
|
|
@@ -478,7 +494,7 @@ const ITEM_ICON: Record<string, string> = {
|
|
|
478
494
|
class="absolute inset-4 flex items-center justify-center gap-1 rounded-lg border border-dashed border-slate-700 text-[11px] text-slate-500 hover:border-slate-500 hover:text-slate-300"
|
|
479
495
|
@click.stop="addTask"
|
|
480
496
|
>
|
|
481
|
-
<UIcon name="i-lucide-plus" class="h-3.5 w-3.5" />
|
|
497
|
+
<UIcon name="i-lucide-plus" class="h-3.5 w-3.5" /> {{ t('board.frame.addFirstTask') }}
|
|
482
498
|
</button>
|
|
483
499
|
|
|
484
500
|
<!-- resize handles (drag the borders to resize the service, Miro-style).
|
|
@@ -486,17 +502,17 @@ const ITEM_ICON: Record<string, string> = {
|
|
|
486
502
|
same reason as the header handle above. -->
|
|
487
503
|
<div
|
|
488
504
|
class="nodrag nopan absolute right-0 top-0 h-full w-2 cursor-ew-resize touch-none hover:bg-sky-400/20 pointer-coarse:w-4"
|
|
489
|
-
title="
|
|
505
|
+
:title="t('board.frame.dragToResize')"
|
|
490
506
|
@pointerdown="onResize($event, 'e')"
|
|
491
507
|
/>
|
|
492
508
|
<div
|
|
493
509
|
class="nodrag nopan absolute bottom-0 left-0 h-2 w-full cursor-ns-resize touch-none hover:bg-sky-400/20 pointer-coarse:h-4"
|
|
494
|
-
title="
|
|
510
|
+
:title="t('board.frame.dragToResize')"
|
|
495
511
|
@pointerdown="onResize($event, 's')"
|
|
496
512
|
/>
|
|
497
513
|
<div
|
|
498
514
|
class="nodrag nopan absolute bottom-0 right-0 h-4 w-4 cursor-nwse-resize touch-none pointer-coarse:h-11 pointer-coarse:w-11"
|
|
499
|
-
title="
|
|
515
|
+
:title="t('board.frame.dragToResize')"
|
|
500
516
|
@pointerdown="onResize($event, 'se')"
|
|
501
517
|
>
|
|
502
518
|
<span
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
withDefaults(
|
|
2
|
+
const props = withDefaults(
|
|
3
3
|
defineProps<{
|
|
4
4
|
count?: number
|
|
5
5
|
compact?: boolean
|
|
@@ -7,9 +7,12 @@ withDefaults(
|
|
|
7
7
|
label?: string
|
|
8
8
|
icon?: string
|
|
9
9
|
}>(),
|
|
10
|
-
{
|
|
10
|
+
{ icon: 'i-lucide-circle-help' },
|
|
11
11
|
)
|
|
12
12
|
defineEmits<{ (e: 'open'): void }>()
|
|
13
|
+
|
|
14
|
+
const { t } = useI18n()
|
|
15
|
+
const displayLabel = computed(() => props.label ?? t('board.decisionBadge.decisionNeeded'))
|
|
13
16
|
</script>
|
|
14
17
|
|
|
15
18
|
<template>
|
|
@@ -19,7 +22,7 @@ defineEmits<{ (e: 'open'): void }>()
|
|
|
19
22
|
@click.stop="$emit('open')"
|
|
20
23
|
>
|
|
21
24
|
<UIcon :name="icon" class="h-3.5 w-3.5" />
|
|
22
|
-
<span v-if="!compact">{{
|
|
25
|
+
<span v-if="!compact">{{ displayLabel }}</span>
|
|
23
26
|
<span v-if="count && count > 1" class="rounded-full bg-amber-950/30 px-1">
|
|
24
27
|
{{ count }}
|
|
25
28
|
</span>
|
|
@@ -4,6 +4,7 @@ import { useBlockDrag } from '~/composables/useBlockDrag'
|
|
|
4
4
|
|
|
5
5
|
const props = defineProps<{ taskId: string }>()
|
|
6
6
|
const board = useBoardStore()
|
|
7
|
+
const { t } = useI18n()
|
|
7
8
|
const expansion = useTaskExpansionStore()
|
|
8
9
|
const task = computed(() => board.getBlock(props.taskId))
|
|
9
10
|
const { draggingId, startDrag } = useBlockDrag()
|
|
@@ -42,7 +43,7 @@ function onHandle(e: PointerEvent) {
|
|
|
42
43
|
<!-- drag handle (`nopan` so the pane doesn't pan on a left-drag from here) -->
|
|
43
44
|
<div
|
|
44
45
|
class="nodrag nopan flex cursor-grab touch-none items-center justify-center rounded-t-lg border border-b-0 border-slate-700 bg-slate-800/80 py-px active:cursor-grabbing pointer-coarse:py-2"
|
|
45
|
-
title="
|
|
46
|
+
:title="t('board.frame.dragTask')"
|
|
46
47
|
@pointerdown="onHandle"
|
|
47
48
|
>
|
|
48
49
|
<UIcon
|
|
@@ -10,6 +10,7 @@ const props = defineProps<{ id: string }>()
|
|
|
10
10
|
|
|
11
11
|
const board = useBoardStore()
|
|
12
12
|
const ui = useUiStore()
|
|
13
|
+
const { t } = useI18n()
|
|
13
14
|
|
|
14
15
|
const block = computed<Block | undefined>(() => board.getBlock(props.id))
|
|
15
16
|
const members = computed(() => board.epicMembers(props.id))
|
|
@@ -31,7 +32,9 @@ const selected = computed(() => ui.selectedBlockId === props.id)
|
|
|
31
32
|
>
|
|
32
33
|
<div class="flex items-center gap-1.5">
|
|
33
34
|
<UIcon name="i-lucide-layers" class="h-3.5 w-3.5 shrink-0 text-violet-400" />
|
|
34
|
-
<span class="text-[10px] font-semibold uppercase tracking-wide text-violet-300">
|
|
35
|
+
<span class="text-[10px] font-semibold uppercase tracking-wide text-violet-300">{{
|
|
36
|
+
t('board.epic.label')
|
|
37
|
+
}}</span>
|
|
35
38
|
<span class="ml-auto text-[10px] text-slate-400">{{ done }}/{{ total }}</span>
|
|
36
39
|
</div>
|
|
37
40
|
<div class="mt-1 truncate text-sm font-medium text-slate-100" :title="block.title">
|
|
@@ -43,7 +46,11 @@ const selected = computed(() => ui.selectedBlockId === props.id)
|
|
|
43
46
|
:style="{ width: total ? `${Math.round((done / total) * 100)}%` : '0%' }"
|
|
44
47
|
/>
|
|
45
48
|
</div>
|
|
46
|
-
<div v-if="active" class="mt-1 text-[10px] text-slate-400">
|
|
47
|
-
|
|
49
|
+
<div v-if="active" class="mt-1 text-[10px] text-slate-400">
|
|
50
|
+
{{ t('board.epic.activeCount', { count: active }) }}
|
|
51
|
+
</div>
|
|
52
|
+
<div v-else-if="total === 0" class="mt-1 text-[10px] text-slate-500">
|
|
53
|
+
{{ t('board.epic.noTasksYet') }}
|
|
54
|
+
</div>
|
|
48
55
|
</div>
|
|
49
56
|
</template>
|
|
@@ -7,6 +7,7 @@ import { useFrameResize } from '~/composables/useFrameResize'
|
|
|
7
7
|
const props = defineProps<{ moduleId: string }>()
|
|
8
8
|
const board = useBoardStore()
|
|
9
9
|
const ui = useUiStore()
|
|
10
|
+
const { t } = useI18n()
|
|
10
11
|
|
|
11
12
|
const mod = computed(() => board.getBlock(props.moduleId))
|
|
12
13
|
const tasks = computed(() => board.tasksOf(props.moduleId))
|
|
@@ -61,10 +62,10 @@ function onResize(e: PointerEvent, edge: 'e' | 's' | 'se') {
|
|
|
61
62
|
/>
|
|
62
63
|
<span class="truncate text-[11px] font-semibold text-violet-100">{{ mod.title }}</span>
|
|
63
64
|
<span v-if="inflight" class="ml-auto shrink-0 text-[9px] text-violet-300/70">
|
|
64
|
-
{{
|
|
65
|
+
{{ t('board.frame.taskCount', { count: inflight }, inflight) }}
|
|
65
66
|
</span>
|
|
66
67
|
<span v-else-if="total" class="ml-auto shrink-0 text-[9px] text-violet-300/70">
|
|
67
|
-
{{
|
|
68
|
+
{{ t('board.frame.taskCount', { count: total }, total) }}
|
|
68
69
|
</span>
|
|
69
70
|
</div>
|
|
70
71
|
|
|
@@ -77,17 +78,17 @@ function onResize(e: PointerEvent, edge: 'e' | 's' | 'se') {
|
|
|
77
78
|
`nopan` (with `nodrag`) so resizing doesn't pan the pane. -->
|
|
78
79
|
<div
|
|
79
80
|
class="nodrag nopan absolute right-0 top-0 h-full w-2 cursor-ew-resize touch-none hover:bg-violet-400/20 pointer-coarse:w-4"
|
|
80
|
-
title="
|
|
81
|
+
:title="t('board.frame.dragToResize')"
|
|
81
82
|
@pointerdown="onResize($event, 'e')"
|
|
82
83
|
/>
|
|
83
84
|
<div
|
|
84
85
|
class="nodrag nopan absolute bottom-0 left-0 h-2 w-full cursor-ns-resize touch-none hover:bg-violet-400/20 pointer-coarse:h-4"
|
|
85
|
-
title="
|
|
86
|
+
:title="t('board.frame.dragToResize')"
|
|
86
87
|
@pointerdown="onResize($event, 's')"
|
|
87
88
|
/>
|
|
88
89
|
<div
|
|
89
90
|
class="nodrag nopan absolute bottom-0 right-0 h-4 w-4 cursor-nwse-resize touch-none pointer-coarse:h-11 pointer-coarse:w-11"
|
|
90
|
-
title="
|
|
91
|
+
:title="t('board.frame.dragToResize')"
|
|
91
92
|
@pointerdown="onResize($event, 'se')"
|
|
92
93
|
>
|
|
93
94
|
<span
|