@cat-factory/app 0.54.0 → 0.54.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.
@@ -52,115 +52,109 @@ function openApprovalFor(approvalId: string) {
52
52
  </script>
53
53
 
54
54
  <template>
55
- <Transition name="focus-fade">
56
- <div
57
- v-if="block && statusMeta && typeMeta"
58
- class="absolute inset-0 z-30 flex flex-col bg-slate-950/95 backdrop-blur"
59
- >
60
- <!-- header / breadcrumb -->
61
- <header class="flex items-center gap-3 border-b border-slate-800 px-6 py-4">
62
- <UButton
63
- icon="i-lucide-arrow-left"
64
- color="neutral"
65
- variant="ghost"
66
- size="sm"
67
- @click="close"
68
- >
69
- {{ t('focus.board') }}
70
- </UButton>
71
- <UIcon name="i-lucide-chevron-right" class="h-4 w-4 text-slate-600" />
55
+ <!-- The focus-fade Transition is owned by the parent mount (pages/index.vue), so the
56
+ leave animation survives this component being unmounted by its v-if gate. -->
57
+ <div
58
+ v-if="block && statusMeta && typeMeta"
59
+ class="absolute inset-0 z-30 flex flex-col bg-slate-950/95 backdrop-blur"
60
+ >
61
+ <!-- header / breadcrumb -->
62
+ <header class="flex items-center gap-3 border-b border-slate-800 px-6 py-4">
63
+ <UButton icon="i-lucide-arrow-left" color="neutral" variant="ghost" size="sm" @click="close">
64
+ {{ t('focus.board') }}
65
+ </UButton>
66
+ <UIcon name="i-lucide-chevron-right" class="h-4 w-4 text-slate-600" />
67
+ <div
68
+ class="flex h-9 w-9 items-center justify-center rounded-lg"
69
+ :style="{ backgroundColor: typeMeta.accent + '22' }"
70
+ >
71
+ <UIcon :name="typeMeta.icon" class="h-5 w-5" :style="{ color: typeMeta.accent }" />
72
+ </div>
73
+ <div>
74
+ <h1 class="text-lg font-semibold text-white">{{ block.title }}</h1>
75
+ <div class="text-xs text-slate-500">
76
+ {{ t('focus.typeSubtitle', { type: typeMeta.label }) }}
77
+ </div>
78
+ </div>
79
+ <UBadge :color="statusMeta.chip as any" variant="subtle" class="ml-2">
80
+ {{ statusMeta.label }}
81
+ </UBadge>
82
+ <div class="ml-auto flex items-center gap-2">
83
+ <UDropdownMenu :items="runMenu">
84
+ <UButton
85
+ color="primary"
86
+ variant="soft"
87
+ size="sm"
88
+ icon="i-lucide-play"
89
+ trailing-icon="i-lucide-chevron-down"
90
+ >
91
+ {{ instance ? t('focus.rerunPipeline') : t('focus.runPipeline') }}
92
+ </UButton>
93
+ </UDropdownMenu>
94
+ <UButton icon="i-lucide-x" color="neutral" variant="ghost" @click="close" />
95
+ </div>
96
+ </header>
97
+
98
+ <div class="grid flex-1 grid-cols-[1fr_300px] gap-6 overflow-hidden p-6">
99
+ <!-- main: pipeline flow -->
100
+ <section
101
+ class="flex flex-col overflow-auto rounded-2xl border border-slate-800 bg-slate-900/60 p-6"
102
+ >
103
+ <div class="mb-4 flex items-center gap-2">
104
+ <UIcon name="i-lucide-workflow" class="h-4 w-4 text-slate-500" />
105
+ <h2 class="text-sm font-semibold uppercase tracking-wide text-slate-400">
106
+ {{ instance ? instance.pipelineName : t('focus.noPipelineRunning') }}
107
+ </h2>
108
+ </div>
109
+
110
+ <PipelineProgress
111
+ v-if="instance"
112
+ :instance="instance"
113
+ @open-decision="openDecisionFor"
114
+ @open-approval="openApprovalFor"
115
+ />
116
+
72
117
  <div
73
- class="flex h-9 w-9 items-center justify-center rounded-lg"
74
- :style="{ backgroundColor: typeMeta.accent + '22' }"
118
+ v-else
119
+ class="flex flex-1 items-center justify-center rounded-xl border border-dashed border-slate-700 text-sm text-slate-500"
75
120
  >
76
- <UIcon :name="typeMeta.icon" class="h-5 w-5" :style="{ color: typeMeta.accent }" />
121
+ {{ t('focus.emptyPipelineHint') }}
77
122
  </div>
123
+ </section>
124
+
125
+ <!-- side: details -->
126
+ <aside
127
+ class="space-y-4 overflow-auto rounded-2xl border border-slate-800 bg-slate-900/60 p-5"
128
+ >
78
129
  <div>
79
- <h1 class="text-lg font-semibold text-white">{{ block.title }}</h1>
80
- <div class="text-xs text-slate-500">
81
- {{ t('focus.typeSubtitle', { type: typeMeta.label }) }}
130
+ <div class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
131
+ {{ t('focus.description') }}
82
132
  </div>
133
+ <p class="text-sm text-slate-300">{{ block.description }}</p>
83
134
  </div>
84
- <UBadge :color="statusMeta.chip as any" variant="subtle" class="ml-2">
85
- {{ statusMeta.label }}
86
- </UBadge>
87
- <div class="ml-auto flex items-center gap-2">
88
- <UDropdownMenu :items="runMenu">
89
- <UButton
90
- color="primary"
91
- variant="soft"
92
- size="sm"
93
- icon="i-lucide-play"
94
- trailing-icon="i-lucide-chevron-down"
95
- >
96
- {{ instance ? t('focus.rerunPipeline') : t('focus.runPipeline') }}
97
- </UButton>
98
- </UDropdownMenu>
99
- <UButton icon="i-lucide-x" color="neutral" variant="ghost" @click="close" />
100
- </div>
101
- </header>
102
-
103
- <div class="grid flex-1 grid-cols-[1fr_300px] gap-6 overflow-hidden p-6">
104
- <!-- main: pipeline flow -->
105
- <section
106
- class="flex flex-col overflow-auto rounded-2xl border border-slate-800 bg-slate-900/60 p-6"
107
- >
108
- <div class="mb-4 flex items-center gap-2">
109
- <UIcon name="i-lucide-workflow" class="h-4 w-4 text-slate-500" />
110
- <h2 class="text-sm font-semibold uppercase tracking-wide text-slate-400">
111
- {{ instance ? instance.pipelineName : t('focus.noPipelineRunning') }}
112
- </h2>
135
+ <div v-if="instance">
136
+ <div class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
137
+ {{ t('focus.overallProgress') }}
113
138
  </div>
114
-
115
- <PipelineProgress
116
- v-if="instance"
117
- :instance="instance"
118
- @open-decision="openDecisionFor"
119
- @open-approval="openApprovalFor"
120
- />
121
-
122
- <div
123
- v-else
124
- class="flex flex-1 items-center justify-center rounded-xl border border-dashed border-slate-700 text-sm text-slate-500"
125
- >
126
- {{ t('focus.emptyPipelineHint') }}
139
+ <UProgress :model-value="Math.round(block.progress * 100)" />
140
+ <div class="mt-1 text-[11px] text-slate-400">
141
+ {{ n(block.progress, 'percent') }}
127
142
  </div>
128
- </section>
129
-
130
- <!-- side: details -->
131
- <aside
132
- class="space-y-4 overflow-auto rounded-2xl border border-slate-800 bg-slate-900/60 p-5"
133
- >
134
- <div>
135
- <div class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
136
- {{ t('focus.description') }}
137
- </div>
138
- <p class="text-sm text-slate-300">{{ block.description }}</p>
139
- </div>
140
- <div v-if="instance">
141
- <div class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
142
- {{ t('focus.overallProgress') }}
143
- </div>
144
- <UProgress :model-value="Math.round(block.progress * 100)" />
145
- <div class="mt-1 text-[11px] text-slate-400">
146
- {{ n(block.progress, 'percent') }}
147
- </div>
143
+ </div>
144
+ <div>
145
+ <div class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
146
+ {{ t('focus.dependencies') }}
148
147
  </div>
149
- <div>
150
- <div class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
151
- {{ t('focus.dependencies') }}
152
- </div>
153
- <div v-if="deps.length" class="flex flex-wrap gap-1">
154
- <UBadge v-for="d in deps" :key="d.id" color="neutral" variant="subtle" size="sm">
155
- {{ d.title }}
156
- </UBadge>
157
- </div>
158
- <div v-else class="text-[11px] text-slate-500">{{ t('focus.noDependencies') }}</div>
148
+ <div v-if="deps.length" class="flex flex-wrap gap-1">
149
+ <UBadge v-for="d in deps" :key="d.id" color="neutral" variant="subtle" size="sm">
150
+ {{ d.title }}
151
+ </UBadge>
159
152
  </div>
160
- </aside>
161
- </div>
153
+ <div v-else class="text-[11px] text-slate-500">{{ t('focus.noDependencies') }}</div>
154
+ </div>
155
+ </aside>
162
156
  </div>
163
- </Transition>
157
+ </div>
164
158
  </template>
165
159
 
166
160
  <style scoped>
@@ -30,6 +30,7 @@ const library =
30
30
  : useFragmentLibrary(props.kind, props.ownerId)
31
31
  const documents = useDocumentsStore()
32
32
  const toast = useToast()
33
+ const { t, d } = useI18n()
33
34
 
34
35
  const isWorkspace = props.kind === 'workspace'
35
36
  /** Linking a document at the account scope needs a workspace connection to fetch through. */
@@ -60,13 +61,16 @@ const tabs = computed<Tab[]>(() =>
60
61
  )
61
62
  const tab = ref<Tab>(props.showCatalog ? 'catalog' : 'authored')
62
63
 
63
- const ownerLabel = isWorkspace ? 'This board' : 'This account'
64
+ const ownerLabel = computed(() =>
65
+ isWorkspace ? t('fragments.owner.workspace') : t('fragments.owner.account'),
66
+ )
64
67
 
65
- const tierLabel: Record<ResolvedFragment['tier'], string> = {
66
- builtin: 'Built-in',
67
- account: 'Account',
68
- workspace: 'This board',
69
- }
68
+ // Exhaustive tier→label map of literal `t(...)` keys (keeps the typed-key drift guard live).
69
+ const tierLabel = computed<Record<ResolvedFragment['tier'], string>>(() => ({
70
+ builtin: t('fragments.tier.builtin'),
71
+ account: t('fragments.tier.account'),
72
+ workspace: t('fragments.tier.workspace'),
73
+ }))
70
74
  // `as const` keeps the literal color names (assignable to UBadge's `color`
71
75
  // union) instead of widening to `string`; `satisfies` still checks the shape.
72
76
  const tierColor = {
@@ -75,11 +79,11 @@ const tierColor = {
75
79
  workspace: 'primary',
76
80
  } as const satisfies Record<ResolvedFragment['tier'], string>
77
81
 
78
- function tabLabel(t: Tab): string {
79
- if (t === 'catalog') return 'Resolved catalog'
80
- if (t === 'authored') return ownerLabel
81
- if (t === 'documents') return 'Documents'
82
- return 'Repo sources'
82
+ function tabLabel(which: Tab): string {
83
+ if (which === 'catalog') return t('fragments.tab.catalog')
84
+ if (which === 'authored') return ownerLabel.value
85
+ if (which === 'documents') return t('fragments.tab.documents')
86
+ return t('fragments.tab.sources')
83
87
  }
84
88
 
85
89
  function notifyError(title: string, e: unknown) {
@@ -110,18 +114,18 @@ async function createFragment() {
110
114
  .filter(Boolean),
111
115
  })
112
116
  draft.value = { title: '', summary: '', body: '', tags: '' }
113
- toast.add({ title: 'Fragment added', icon: 'i-lucide-check' })
117
+ toast.add({ title: t('fragments.toast.added'), icon: 'i-lucide-check' })
114
118
  } catch (e) {
115
- notifyError('Could not add fragment', e)
119
+ notifyError(t('fragments.toast.addFailed'), e)
116
120
  }
117
121
  }
118
122
 
119
123
  async function removeFragment(id: string) {
120
124
  try {
121
125
  await library.remove(id)
122
- toast.add({ title: 'Fragment removed', icon: 'i-lucide-trash-2' })
126
+ toast.add({ title: t('fragments.toast.removed'), icon: 'i-lucide-trash-2' })
123
127
  } catch (e) {
124
- notifyError('Could not remove fragment', e)
128
+ notifyError(t('fragments.toast.removeFailed'), e)
125
129
  }
126
130
  }
127
131
 
@@ -148,18 +152,18 @@ async function linkDocumentFragment() {
148
152
  .filter(Boolean),
149
153
  })
150
154
  docDraft.value = { source: '', ref: '', tags: '' }
151
- toast.add({ title: 'Document linked as a living fragment', icon: 'i-lucide-link' })
155
+ toast.add({ title: t('fragments.toast.documentLinked'), icon: 'i-lucide-link' })
152
156
  } catch (e) {
153
- notifyError('Could not link document', e)
157
+ notifyError(t('fragments.toast.linkDocumentFailed'), e)
154
158
  }
155
159
  }
156
160
 
157
161
  async function refreshFragment(id: string) {
158
162
  try {
159
163
  await library.refreshDocumentFragment(id)
160
- toast.add({ title: 'Fragment re-resolved from source', icon: 'i-lucide-refresh-cw' })
164
+ toast.add({ title: t('fragments.toast.refreshed'), icon: 'i-lucide-refresh-cw' })
161
165
  } catch (e) {
162
- notifyError('Could not refresh fragment', e)
166
+ notifyError(t('fragments.toast.refreshFailed'), e)
163
167
  }
164
168
  }
165
169
 
@@ -180,9 +184,9 @@ async function linkSource() {
180
184
  })
181
185
  sourceDraft.value = { repoOwner: '', repoName: '', dirPath: '', gitRef: '' }
182
186
  await library.syncSource(source.id)
183
- toast.add({ title: 'Source linked & synced', icon: 'i-lucide-git-branch' })
187
+ toast.add({ title: t('fragments.toast.sourceLinked'), icon: 'i-lucide-git-branch' })
184
188
  } catch (e) {
185
- notifyError('Could not link source', e)
189
+ notifyError(t('fragments.toast.linkSourceFailed'), e)
186
190
  }
187
191
  }
188
192
 
@@ -190,12 +194,15 @@ async function syncSource(id: string) {
190
194
  try {
191
195
  const result = await library.syncSource(id)
192
196
  toast.add({
193
- title: `Synced: ${result.upserted} updated, ${result.tombstoned} removed`,
197
+ title: t('fragments.toast.synced', {
198
+ updated: result.upserted,
199
+ removed: result.tombstoned,
200
+ }),
194
201
  icon: 'i-lucide-refresh-cw',
195
202
  color: 'info',
196
203
  })
197
204
  } catch (e) {
198
- notifyError('Could not sync source', e)
205
+ notifyError(t('fragments.toast.syncFailed'), e)
199
206
  }
200
207
  }
201
208
 
@@ -203,20 +210,22 @@ async function checkSource(id: string) {
203
210
  try {
204
211
  const status = await library.checkSource(id)
205
212
  toast.add({
206
- title: status.changed ? `${status.changedCount} change(s) available` : 'Up to date',
213
+ title: status.changed
214
+ ? t('fragments.toast.changesAvailable', { count: status.changedCount }, status.changedCount)
215
+ : t('fragments.toast.upToDate'),
207
216
  icon: status.changed ? 'i-lucide-bell-dot' : 'i-lucide-check',
208
217
  })
209
218
  } catch (e) {
210
- notifyError('Could not check source', e)
219
+ notifyError(t('fragments.toast.checkSourceFailed'), e)
211
220
  }
212
221
  }
213
222
 
214
223
  async function unlinkSource(id: string) {
215
224
  try {
216
225
  await library.unlinkSource(id)
217
- toast.add({ title: 'Source unlinked', icon: 'i-lucide-unplug' })
226
+ toast.add({ title: t('fragments.toast.sourceUnlinked'), icon: 'i-lucide-unplug' })
218
227
  } catch (e) {
219
- notifyError('Could not unlink source', e)
228
+ notifyError(t('fragments.toast.unlinkSourceFailed'), e)
220
229
  }
221
230
  }
222
231
  </script>
@@ -225,14 +234,10 @@ async function unlinkSource(id: string) {
225
234
  <div class="flex flex-col gap-4">
226
235
  <p class="text-sm text-slate-400">
227
236
  <template v-if="isWorkspace">
228
- Curate the best-practice guidelines agents follow on this board. Fragments are merged from
229
- the built-in catalog, your account, and this board — later tiers override earlier ones —
230
- then the relevant ones are selected for each agent run.
237
+ {{ t('fragments.intro.workspace') }}
231
238
  </template>
232
239
  <template v-else>
233
- Curate best-practice guidelines shared by every board in this account. Account fragments are
234
- merged below the built-in catalog and above nothing, and a board can override or add its own
235
- on top. The relevant ones are selected for each agent run.
240
+ {{ t('fragments.intro.account') }}
236
241
  </template>
237
242
  </p>
238
243
 
@@ -252,7 +257,13 @@ async function unlinkSource(id: string) {
252
257
  <!-- Resolved (merged) catalog — workspace scope only -->
253
258
  <div v-if="tab === 'catalog'" class="flex flex-col gap-2">
254
259
  <p class="text-xs text-slate-500">
255
- {{ library.resolved.length }} fragment(s) resolved · {{ library.builtinCount }} built-in.
260
+ {{
261
+ t(
262
+ 'fragments.catalog.summary',
263
+ { count: library.resolved.length, builtin: library.builtinCount },
264
+ library.resolved.length,
265
+ )
266
+ }}
256
267
  </p>
257
268
  <div
258
269
  v-for="f in library.resolved"
@@ -271,7 +282,7 @@ async function unlinkSource(id: string) {
271
282
  variant="subtle"
272
283
  icon="i-lucide-radio"
273
284
  >
274
- Live · {{ f.documentRef.source }}
285
+ {{ t('fragments.catalog.live', { source: f.documentRef.source }) }}
275
286
  </UBadge>
276
287
  <span class="ml-auto font-mono text-[11px] text-slate-500">{{ f.id }}</span>
277
288
  </div>
@@ -294,7 +305,9 @@ async function unlinkSource(id: string) {
294
305
  <div class="min-w-0">
295
306
  <div class="flex items-center gap-2">
296
307
  <span class="font-medium text-slate-100">{{ f.title }}</span>
297
- <UBadge v-if="f.source" size="xs" color="info" variant="subtle">from repo</UBadge>
308
+ <UBadge v-if="f.source" size="xs" color="info" variant="subtle">{{
309
+ t('fragments.authored.fromRepo')
310
+ }}</UBadge>
298
311
  </div>
299
312
  <p class="text-sm text-slate-400">{{ f.summary }}</p>
300
313
  </div>
@@ -308,21 +321,27 @@ async function unlinkSource(id: string) {
308
321
  />
309
322
  </div>
310
323
  <p v-if="!library.fragments.length" class="text-sm text-slate-500">
311
- No {{ isWorkspace ? 'board' : 'account' }}-specific fragments yet. Add one below, or
312
- override a built-in by using its id.
324
+ {{
325
+ isWorkspace
326
+ ? t('fragments.authored.empty.workspace')
327
+ : t('fragments.authored.empty.account')
328
+ }}
313
329
  </p>
314
330
 
315
331
  <div class="rounded-md border border-slate-800 p-3">
316
- <p class="mb-2 text-sm font-medium">Add a fragment</p>
332
+ <p class="mb-2 text-sm font-medium">{{ t('fragments.authored.addTitle') }}</p>
317
333
  <div class="flex flex-col gap-2">
318
- <UInput v-model="draft.title" placeholder="Title" />
319
- <UInput v-model="draft.summary" placeholder="One-line summary (used by the selector)" />
334
+ <UInput v-model="draft.title" :placeholder="t('fragments.authored.titlePlaceholder')" />
335
+ <UInput
336
+ v-model="draft.summary"
337
+ :placeholder="t('fragments.authored.summaryPlaceholder')"
338
+ />
320
339
  <UTextarea
321
340
  v-model="draft.body"
322
- placeholder="Guidance body (injected into the prompt)"
341
+ :placeholder="t('fragments.authored.bodyPlaceholder')"
323
342
  :rows="4"
324
343
  />
325
- <UInput v-model="draft.tags" placeholder="Tags, comma-separated (e.g. backend, db)" />
344
+ <UInput v-model="draft.tags" :placeholder="t('fragments.authored.tagsPlaceholder')" />
326
345
  <UButton
327
346
  icon="i-lucide-plus"
328
347
  size="sm"
@@ -331,7 +350,7 @@ async function unlinkSource(id: string) {
331
350
  class="self-start"
332
351
  @click="createFragment"
333
352
  >
334
- Add fragment
353
+ {{ t('fragments.authored.add') }}
335
354
  </UButton>
336
355
  </div>
337
356
  </div>
@@ -340,9 +359,7 @@ async function unlinkSource(id: string) {
340
359
  <!-- Document-backed (living) fragments -->
341
360
  <div v-else-if="tab === 'documents'" class="flex flex-col gap-3">
342
361
  <p class="text-xs text-slate-500">
343
- Link a Confluence/Notion page or a GitHub file as a best-practice fragment. Its guidance is
344
- re-resolved from the source at run time — edit the doc and the next agent run follows the
345
- new version (no re-import).
362
+ {{ t('fragments.documents.intro') }}
346
363
  </p>
347
364
 
348
365
  <div
@@ -360,7 +377,7 @@ async function unlinkSource(id: string) {
360
377
  </div>
361
378
  <p class="text-sm text-slate-400">{{ f.summary }}</p>
362
379
  <p v-if="f.resolvedAt" class="text-[11px] text-slate-500">
363
- last resolved {{ new Date(f.resolvedAt).toLocaleString() }}
380
+ {{ t('fragments.documents.lastResolved', { date: d(new Date(f.resolvedAt), 'long') }) }}
364
381
  </p>
365
382
  </div>
366
383
  <div class="ml-auto flex gap-1">
@@ -369,7 +386,7 @@ async function unlinkSource(id: string) {
369
386
  size="xs"
370
387
  variant="ghost"
371
388
  :loading="library.loading"
372
- title="Re-resolve from source now"
389
+ :title="t('fragments.documents.refreshTitle')"
373
390
  @click="refreshFragment(f.id)"
374
391
  />
375
392
  <UButton
@@ -382,17 +399,16 @@ async function unlinkSource(id: string) {
382
399
  </div>
383
400
  </div>
384
401
  <p v-if="!documentFragments.length" class="text-sm text-slate-500">
385
- No document-backed fragments yet. Link one below.
402
+ {{ t('fragments.documents.empty') }}
386
403
  </p>
387
404
 
388
405
  <div class="rounded-md border border-slate-800 p-3">
389
- <p class="mb-2 text-sm font-medium">Link a document</p>
406
+ <p class="mb-2 text-sm font-medium">{{ t('fragments.documents.linkTitle') }}</p>
390
407
  <div v-if="docLinkDisabled" class="text-sm text-slate-500">
391
- Create a board with a connected document source first — account-level document fragments
392
- are fetched through one of this account's boards.
408
+ {{ t('fragments.documents.disabledHint') }}
393
409
  </div>
394
410
  <div v-else-if="!documents.connectedSources.length" class="text-sm text-slate-500">
395
- Connect a document source (Confluence, Notion or GitHub) under Integrations first.
411
+ {{ t('fragments.documents.connectFirst') }}
396
412
  </div>
397
413
  <div v-else class="flex flex-col gap-2">
398
414
  <div class="flex flex-wrap gap-2">
@@ -407,11 +423,8 @@ async function unlinkSource(id: string) {
407
423
  {{ s.label }}
408
424
  </UButton>
409
425
  </div>
410
- <UInput
411
- v-model="docDraft.ref"
412
- placeholder="Page id or URL (e.g. a Confluence/Notion page or GitHub file URL)"
413
- />
414
- <UInput v-model="docDraft.tags" placeholder="Tags, comma-separated (optional)" />
426
+ <UInput v-model="docDraft.ref" :placeholder="t('fragments.documents.refPlaceholder')" />
427
+ <UInput v-model="docDraft.tags" :placeholder="t('fragments.documents.tagsPlaceholder')" />
415
428
  <UButton
416
429
  icon="i-lucide-link"
417
430
  size="sm"
@@ -420,7 +433,7 @@ async function unlinkSource(id: string) {
420
433
  class="self-start"
421
434
  @click="linkDocumentFragment"
422
435
  >
423
- Link as living fragment
436
+ {{ t('fragments.documents.link') }}
424
437
  </UButton>
425
438
  </div>
426
439
  </div>
@@ -440,7 +453,11 @@ async function unlinkSource(id: string) {
440
453
  }}<span class="text-slate-500">/{{ s.dirPath || '' }}</span>
441
454
  </span>
442
455
  <p class="text-xs text-slate-500">
443
- {{ s.lastSyncedAt ? 'synced' : 'never synced' }} · ref {{ s.gitRef }}
456
+ {{
457
+ s.lastSyncedAt
458
+ ? t('fragments.sources.metaSynced', { ref: s.gitRef })
459
+ : t('fragments.sources.metaNever', { ref: s.gitRef })
460
+ }}
444
461
  </p>
445
462
  </div>
446
463
  <UBadge
@@ -450,7 +467,13 @@ async function unlinkSource(id: string) {
450
467
  variant="subtle"
451
468
  class="ml-auto"
452
469
  >
453
- {{ library.sourceChanges[s.id] }} change(s)
470
+ {{
471
+ t(
472
+ 'fragments.sources.changes',
473
+ { count: library.sourceChanges[s.id] },
474
+ library.sourceChanges[s.id] ?? 0,
475
+ )
476
+ }}
454
477
  </UBadge>
455
478
  <div class="ml-auto flex gap-1">
456
479
  <UButton
@@ -476,23 +499,35 @@ async function unlinkSource(id: string) {
476
499
  </div>
477
500
  </div>
478
501
  <p v-if="!library.sources.length" class="text-sm text-slate-500">
479
- No linked guideline repos. Link one below to import its Markdown files as fragments.
502
+ {{ t('fragments.sources.empty') }}
480
503
  </p>
481
504
 
482
505
  <div class="rounded-md border border-slate-800 p-3">
483
- <p class="mb-2 text-sm font-medium">Link a guideline repo</p>
506
+ <p class="mb-2 text-sm font-medium">{{ t('fragments.sources.linkTitle') }}</p>
484
507
  <div class="flex flex-col gap-2">
485
508
  <div class="flex gap-2">
486
- <UInput v-model="sourceDraft.repoOwner" placeholder="owner" class="flex-1" />
487
- <UInput v-model="sourceDraft.repoName" placeholder="repo" class="flex-1" />
509
+ <UInput
510
+ v-model="sourceDraft.repoOwner"
511
+ :placeholder="t('fragments.sources.ownerPlaceholder')"
512
+ class="flex-1"
513
+ />
514
+ <UInput
515
+ v-model="sourceDraft.repoName"
516
+ :placeholder="t('fragments.sources.repoPlaceholder')"
517
+ class="flex-1"
518
+ />
488
519
  </div>
489
520
  <div class="flex gap-2">
490
521
  <UInput
491
522
  v-model="sourceDraft.dirPath"
492
- placeholder="dir path (e.g. guidelines)"
523
+ :placeholder="t('fragments.sources.dirPlaceholder')"
524
+ class="flex-1"
525
+ />
526
+ <UInput
527
+ v-model="sourceDraft.gitRef"
528
+ :placeholder="t('fragments.sources.refPlaceholder')"
493
529
  class="flex-1"
494
530
  />
495
- <UInput v-model="sourceDraft.gitRef" placeholder="ref (default HEAD)" class="flex-1" />
496
531
  </div>
497
532
  <UButton
498
533
  icon="i-lucide-link"
@@ -502,7 +537,7 @@ async function unlinkSource(id: string) {
502
537
  class="self-start"
503
538
  @click="linkSource"
504
539
  >
505
- Link & sync
540
+ {{ t('fragments.sources.link') }}
506
541
  </UButton>
507
542
  </div>
508
543
  </div>
@@ -7,6 +7,7 @@ import FragmentLibraryManager from '~/components/fragments/FragmentLibraryManage
7
7
 
8
8
  const ui = useUiStore()
9
9
  const workspace = useWorkspaceStore()
10
+ const { t } = useI18n()
10
11
 
11
12
  const open = computed({
12
13
  get: () => ui.fragmentLibraryOpen,
@@ -17,7 +18,7 @@ const open = computed({
17
18
  </script>
18
19
 
19
20
  <template>
20
- <UModal v-model:open="open" title="Prompt-fragment library" :ui="{ content: 'max-w-3xl' }">
21
+ <UModal v-model:open="open" :title="t('fragments.panel.title')" :ui="{ content: 'max-w-3xl' }">
21
22
  <template #body>
22
23
  <FragmentLibraryManager
23
24
  v-if="workspace.workspaceId"