@cat-factory/app 0.54.3 → 0.55.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/app/app.vue +14 -0
  2. package/app/components/auth/UserMenu.vue +1 -1
  3. package/app/components/board/AddTaskModal.vue +5 -4
  4. package/app/components/board/nodes/AgentChip.vue +2 -2
  5. package/app/components/board/nodes/BlockNode.vue +12 -8
  6. package/app/components/board/nodes/DraggableTask.vue +1 -1
  7. package/app/components/board/nodes/EpicNode.vue +1 -1
  8. package/app/components/board/nodes/ModuleFrame.vue +7 -4
  9. package/app/components/board/nodes/TaskCard.vue +2 -2
  10. package/app/components/board/nodes/TaskPipelineMini.vue +3 -3
  11. package/app/components/brainstorm/BrainstormWindow.vue +8 -7
  12. package/app/components/clarity/ClarityReviewWindow.vue +8 -7
  13. package/app/components/consensus/ConsensusSessionWindow.vue +2 -2
  14. package/app/components/documents/ContextDocumentPicker.vue +4 -4
  15. package/app/components/documents/SpawnPreviewModal.vue +4 -4
  16. package/app/components/focus/BlockFocusView.vue +11 -4
  17. package/app/components/fragments/FragmentLibraryManager.vue +5 -5
  18. package/app/components/gates/GateResultView.vue +3 -3
  19. package/app/components/github/GitHubPanel.vue +5 -2
  20. package/app/components/humanTest/HumanTestWindow.vue +1 -1
  21. package/app/components/kaizen/KaizenPanel.vue +5 -5
  22. package/app/components/kaizen/KaizenStepStatus.vue +2 -2
  23. package/app/components/layout/BoardSwitcher.vue +4 -4
  24. package/app/components/layout/CommandBar.vue +1 -1
  25. package/app/components/layout/IntegrationBackTitle.vue +2 -1
  26. package/app/components/layout/IntegrationsHub.vue +10 -4
  27. package/app/components/layout/LanguageSwitcher.vue +1 -1
  28. package/app/components/layout/NotificationsInbox.vue +1 -1
  29. package/app/components/layout/PersonalSetupModal.vue +5 -2
  30. package/app/components/layout/SideBar.vue +5 -3
  31. package/app/components/media/ArtifactLightbox.vue +5 -5
  32. package/app/components/media/ImageCompare.vue +6 -1
  33. package/app/components/observability/StepMetricsBar.vue +6 -2
  34. package/app/components/palettes/AgentPalette.vue +4 -4
  35. package/app/components/panels/AgentStepDetail.vue +12 -12
  36. package/app/components/panels/GenericStructuredResultView.vue +1 -1
  37. package/app/components/panels/InspectorPanel.vue +2 -2
  38. package/app/components/panels/ObservabilityPanel.vue +6 -6
  39. package/app/components/panels/StepMetadataCard.vue +1 -1
  40. package/app/components/panels/inspector/ContainerSummary.vue +2 -2
  41. package/app/components/panels/inspector/EpicChildren.vue +3 -3
  42. package/app/components/panels/inspector/RecurringScheduleSettings.vue +1 -1
  43. package/app/components/panels/inspector/ServiceFragments.vue +1 -1
  44. package/app/components/panels/inspector/ServiceTestConfig.vue +1 -1
  45. package/app/components/panels/inspector/TaskDependencies.vue +1 -1
  46. package/app/components/panels/inspector/TaskEstimateBadge.vue +1 -1
  47. package/app/components/panels/inspector/TaskExecution.vue +9 -9
  48. package/app/components/panels/inspector/TaskRunSettings.vue +2 -2
  49. package/app/components/panels/inspector/TaskStructure.vue +1 -1
  50. package/app/components/pipeline/IterationCapPrompt.vue +1 -0
  51. package/app/components/pipeline/PipelineBuilder.vue +8 -8
  52. package/app/components/pipeline/PipelineProgress.vue +4 -4
  53. package/app/components/providers/ApiKeysSection.vue +2 -2
  54. package/app/components/providers/PersonalSubscriptionSection.vue +2 -2
  55. package/app/components/providers/VendorCredentialsModal.vue +2 -2
  56. package/app/components/provisioning/ProvisioningLogsDrawer.vue +1 -1
  57. package/app/components/requirements/RequirementsReviewWindow.vue +9 -8
  58. package/app/components/sandbox/SandboxPanel.vue +17 -17
  59. package/app/components/settings/LocalModelEndpointsPanel.vue +1 -1
  60. package/app/components/settings/ModelConfigurationPanel.vue +4 -3
  61. package/app/components/settings/OpenRouterCatalogPanel.vue +3 -3
  62. package/app/components/settings/ServiceFragmentDefaultsPanel.vue +1 -1
  63. package/app/components/spec/ServiceSpecWindow.vue +3 -3
  64. package/app/components/tasks/ContextIssuePicker.vue +5 -5
  65. package/app/components/tasks/TaskContextIssues.vue +1 -1
  66. package/app/components/tasks/TaskImportModal.vue +1 -1
  67. package/app/components/testing/TestReportWindow.vue +5 -5
  68. package/app/components/visualConfirm/VisualConfirmationWindow.vue +2 -2
  69. package/app/pages/index.vue +1 -1
  70. package/i18n/i18n.config.ts +9 -0
  71. package/i18n/locales/he.json +3385 -0
  72. package/nuxt.config.ts +1 -0
  73. package/package.json +2 -2
package/app/app.vue CHANGED
@@ -1,5 +1,19 @@
1
1
  <script setup lang="ts">
2
2
  import AuthGate from '~/components/auth/AuthGate.vue'
3
+
4
+ // Drive the document's `lang`/`dir` from the active i18n locale. `useLocaleHead` derives
5
+ // these from the locale definitions in `nuxt.config.ts` (including the `dir: 'rtl'` flag on
6
+ // Hebrew), so switching to a RTL locale flips `<html dir="rtl" lang="he">` and the browser
7
+ // mirrors text alignment, flex/grid main-axis direction, and logical CSS properties. Wrapped
8
+ // in a getter so `useHead` tracks the reactive ref and re-applies on every locale change.
9
+ const localeHead = useLocaleHead()
10
+ useHead(() => ({
11
+ htmlAttrs: {
12
+ lang: localeHead.value.htmlAttrs?.lang,
13
+ // `useLocaleHead` types `dir` loosely as string; our locales only ever set ltr/rtl.
14
+ dir: localeHead.value.htmlAttrs?.dir as 'ltr' | 'rtl' | 'auto' | undefined,
15
+ },
16
+ }))
3
17
  </script>
4
18
 
5
19
  <template>
@@ -30,7 +30,7 @@ const items = computed<DropdownMenuItem[][]>(() => [
30
30
  <UDropdownMenu v-if="auth.user" :items="items" :content="{ side: 'top', align: 'start' }">
31
31
  <button
32
32
  type="button"
33
- class="flex w-full items-center gap-2 rounded-lg border border-slate-800 bg-slate-900/60 p-2 text-left transition hover:bg-slate-800/60"
33
+ class="flex w-full items-center gap-2 rounded-lg border border-slate-800 bg-slate-900/60 p-2 text-start transition hover:bg-slate-800/60"
34
34
  >
35
35
  <UAvatar
36
36
  :src="auth.user.avatarUrl ?? undefined"
@@ -700,13 +700,13 @@ async function add() {
700
700
  color="neutral"
701
701
  variant="soft"
702
702
  size="xs"
703
- class="ml-1 shrink-0"
703
+ class="ms-1 shrink-0"
704
704
  >
705
705
  {{ t('board.addTask.importsOnAdd') }}
706
706
  </UBadge>
707
707
  <button
708
708
  type="button"
709
- class="ml-auto shrink-0 text-slate-400 hover:text-slate-200"
709
+ class="ms-auto shrink-0 text-slate-400 hover:text-slate-200"
710
710
  @click="removePending(item)"
711
711
  >
712
712
  <UIcon name="i-lucide-x" class="h-3.5 w-3.5" />
@@ -772,13 +772,13 @@ async function add() {
772
772
  color="neutral"
773
773
  variant="soft"
774
774
  size="xs"
775
- class="ml-1 shrink-0"
775
+ class="ms-1 shrink-0"
776
776
  >
777
777
  {{ t('board.addTask.importsOnAdd') }}
778
778
  </UBadge>
779
779
  <button
780
780
  type="button"
781
- class="ml-auto shrink-0 text-slate-400 hover:text-slate-200"
781
+ class="ms-auto shrink-0 text-slate-400 hover:text-slate-200"
782
782
  @click="removePending(item)"
783
783
  >
784
784
  <UIcon name="i-lucide-x" class="h-3.5 w-3.5" />
@@ -806,6 +806,7 @@ async function add() {
806
806
  color="primary"
807
807
  data-testid="add-task-submit"
808
808
  :icon="isRecurring ? 'i-lucide-arrow-right' : 'i-lucide-plus'"
809
+ :ui="{ leadingIcon: 'rtl:-scale-x-100', trailingIcon: 'rtl:-scale-x-100' }"
809
810
  :loading="saving"
810
811
  :disabled="!canAdd"
811
812
  @click="add"
@@ -37,13 +37,13 @@ const dim = computed(() => (props.size === 'sm' ? 'h-7 w-7' : 'h-9 w-9'))
37
37
  <UIcon :name="archetype.icon" class="text-base" :style="{ color: archetype.color }" />
38
38
  <span
39
39
  v-if="step.state === 'working'"
40
- class="absolute -bottom-1 -right-1 rounded-full bg-slate-900 p-0.5"
40
+ class="absolute -bottom-1 -end-1 rounded-full bg-slate-900 p-0.5"
41
41
  >
42
42
  <UIcon :name="stateIcon.working!" class="h-3 w-3 animate-spin text-indigo-300" />
43
43
  </span>
44
44
  <span
45
45
  v-else-if="stateIcon[step.state]"
46
- class="absolute -bottom-1 -right-1 rounded-full bg-slate-900 p-0.5"
46
+ class="absolute -bottom-1 -end-1 rounded-full bg-slate-900 p-0.5"
47
47
  >
48
48
  <UIcon
49
49
  :name="stateIcon[step.state]!"
@@ -234,16 +234,16 @@ const ITEM_ICON: Record<string, string> = {
234
234
  <UIcon
235
235
  v-if="bootstrapping"
236
236
  name="i-lucide-loader-circle"
237
- class="ml-auto h-3.5 w-3.5 shrink-0 animate-spin text-amber-400"
237
+ class="ms-auto h-3.5 w-3.5 shrink-0 animate-spin text-amber-400"
238
238
  :title="t('board.frame.bootstrapping')"
239
239
  />
240
240
  <UIcon
241
241
  v-else-if="runFailed"
242
242
  name="i-lucide-alert-triangle"
243
- class="ml-auto h-3.5 w-3.5 shrink-0 text-rose-400"
243
+ class="ms-auto h-3.5 w-3.5 shrink-0 text-rose-400"
244
244
  :title="t('board.frame.runFailed')"
245
245
  />
246
- <span v-else-if="hasTasks" class="ml-auto shrink-0 text-[11px] text-slate-300">
246
+ <span v-else-if="hasTasks" class="ms-auto shrink-0 text-[11px] text-slate-300">
247
247
  {{ mergedTasks }}/{{ taskCount }}
248
248
  </span>
249
249
  </div>
@@ -263,7 +263,7 @@ const ITEM_ICON: Record<string, string> = {
263
263
  class="h-3.5 w-3.5 shrink-0 animate-spin text-amber-400"
264
264
  />
265
265
  <span class="text-amber-300">{{ t('board.frame.bootstrapping') }}</span>
266
- <span v-if="bootstrapSubtasks" class="ml-auto text-amber-200/80">
266
+ <span v-if="bootstrapSubtasks" class="ms-auto text-amber-200/80">
267
267
  {{ bootstrapSubtasks.completed }}/{{ bootstrapSubtasks.total }}
268
268
  </span>
269
269
  </div>
@@ -321,7 +321,7 @@ const ITEM_ICON: Record<string, string> = {
321
321
  <span v-if="prTasks" class="text-emerald-400"
322
322
  >· {{ t('board.frame.prCount', { count: prTasks }) }}</span
323
323
  >
324
- <UIcon name="i-lucide-chevron-down" class="ml-auto h-3 w-3" />
324
+ <UIcon name="i-lucide-chevron-down" class="ms-auto h-3 w-3" />
325
325
  </button>
326
326
  </div>
327
327
  </div>
@@ -341,7 +341,7 @@ const ITEM_ICON: Record<string, string> = {
341
341
  class="h-4 w-4 shrink-0 animate-spin text-amber-400"
342
342
  />
343
343
  <span class="text-amber-300">{{ t('board.frame.bootstrappingRepository') }}</span>
344
- <span v-if="bootstrapSubtasks" class="ml-auto text-amber-200/80">
344
+ <span v-if="bootstrapSubtasks" class="ms-auto text-amber-200/80">
345
345
  {{
346
346
  t('board.frame.bootstrapStepsCount', {
347
347
  completed: bootstrapSubtasks.completed,
@@ -392,7 +392,7 @@ const ITEM_ICON: Record<string, string> = {
392
392
  <div class="p-4">
393
393
  <!-- frame header: the whole header (the title row AND the stats line below it)
394
394
  is the drag handle for the expanded frame. `nopan` stops Vue Flow's pane
395
- from panning on a left-drag that starts here (it pans via d3-zoom's
395
+ from panning on a start-drag that starts here (it pans via d3-zoom's
396
396
  mousedown, which our pointerdown stopPropagation can't intercept), so the
397
397
  grab drives the frame move. The action buttons on the right opt out via
398
398
  `.nodrag` (onFrameHandle ignores them). `pb-3` provides the gap down to the
@@ -499,7 +499,11 @@ const ITEM_ICON: Record<string, string> = {
499
499
 
500
500
  <!-- resize handles (drag the borders to resize the service, Miro-style).
501
501
  `nopan` (alongside `nodrag`) so the pane doesn't pan while resizing —
502
- same reason as the header handle above. -->
502
+ same reason as the header handle above. These stay PHYSICAL
503
+ (`right-0`, not `end-0`): the resize math in useFrameResize grows the
504
+ right/bottom edge from an unmirrored clientX/clientY delta, so a
505
+ logical (RTL-flipped) grip would render on the opposite edge from the
506
+ one the drag actually moves. -->
503
507
  <div
504
508
  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"
505
509
  :title="t('board.frame.dragToResize')"
@@ -40,7 +40,7 @@ function onHandle(e: PointerEvent) {
40
40
  pointerEvents: draggingId === taskId ? 'none' : undefined,
41
41
  }"
42
42
  >
43
- <!-- drag handle (`nopan` so the pane doesn't pan on a left-drag from here) -->
43
+ <!-- drag handle (`nopan` so the pane doesn't pan on a start-drag from here) -->
44
44
  <div
45
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"
46
46
  :title="t('board.frame.dragTask')"
@@ -35,7 +35,7 @@ const selected = computed(() => ui.selectedBlockId === props.id)
35
35
  <span class="text-[10px] font-semibold uppercase tracking-wide text-violet-300">{{
36
36
  t('board.epic.label')
37
37
  }}</span>
38
- <span class="ml-auto text-[10px] text-slate-400">{{ done }}/{{ total }}</span>
38
+ <span class="ms-auto text-[10px] text-slate-400">{{ done }}/{{ total }}</span>
39
39
  </div>
40
40
  <div class="mt-1 truncate text-sm font-medium text-slate-100" :title="block.title">
41
41
  {{ block.title }}
@@ -49,7 +49,7 @@ function onResize(e: PointerEvent, edge: 'e' | 's' | 'se') {
49
49
  zIndex: draggingId === moduleId ? 50 : 5,
50
50
  }"
51
51
  >
52
- <!-- module header / drag handle (`nopan` so a left-drag moves it, not the pane) -->
52
+ <!-- module header / drag handle (`nopan` so a start-drag moves it, not the pane) -->
53
53
  <div
54
54
  class="nodrag nopan flex h-[30px] cursor-grab touch-none items-center gap-1 rounded-t-xl bg-violet-500/15 px-2 active:cursor-grabbing"
55
55
  @pointerdown="onHandle"
@@ -61,10 +61,10 @@ function onResize(e: PointerEvent, edge: 'e' | 's' | 'se') {
61
61
  :style="{ color: MODULE_META.color }"
62
62
  />
63
63
  <span class="truncate text-[11px] font-semibold text-violet-100">{{ mod.title }}</span>
64
- <span v-if="inflight" class="ml-auto shrink-0 text-[9px] text-violet-300/70">
64
+ <span v-if="inflight" class="ms-auto shrink-0 text-[9px] text-violet-300/70">
65
65
  {{ t('board.frame.taskCount', { count: inflight }, inflight) }}
66
66
  </span>
67
- <span v-else-if="total" class="ml-auto shrink-0 text-[9px] text-violet-300/70">
67
+ <span v-else-if="total" class="ms-auto shrink-0 text-[9px] text-violet-300/70">
68
68
  {{ t('board.frame.taskCount', { count: total }, total) }}
69
69
  </span>
70
70
  </div>
@@ -75,7 +75,10 @@ function onResize(e: PointerEvent, edge: 'e' | 's' | 'se') {
75
75
  </div>
76
76
 
77
77
  <!-- resize handles (drag the borders to resize the module, Miro-style).
78
- `nopan` (with `nodrag`) so resizing doesn't pan the pane. -->
78
+ `nopan` (with `nodrag`) so resizing doesn't pan the pane. Kept PHYSICAL
79
+ (`right-0`, not `end-0`) for the same reason as the service-frame grips in
80
+ BlockNode: the resize delta is unmirrored, so a logical grip would sit on
81
+ the opposite edge from the one the drag moves. -->
79
82
  <div
80
83
  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"
81
84
  :title="t('board.frame.dragToResize')"
@@ -181,7 +181,7 @@ function selectTask() {
181
181
  :data-block-id="task.id"
182
182
  :data-status="task.status"
183
183
  data-testid="task-card"
184
- class="nodrag w-full cursor-pointer rounded-lg border bg-slate-950/70 p-2 text-left transition"
184
+ class="nodrag w-full cursor-pointer rounded-lg border bg-slate-950/70 p-2 text-start transition"
185
185
  :class="[
186
186
  selected ? 'border-white' : 'border-slate-700 hover:border-slate-500',
187
187
  task.status === 'pr_ready' ? 'board-pulse-green' : attention ? 'board-pulse' : '',
@@ -204,7 +204,7 @@ function selectTask() {
204
204
  "
205
205
  />
206
206
  <span
207
- class="ml-auto truncate text-[9px] uppercase tracking-wide"
207
+ class="ms-auto truncate text-[9px] uppercase tracking-wide"
208
208
  :class="
209
209
  runFailed
210
210
  ? 'text-rose-400'
@@ -99,7 +99,7 @@ const ITEM_ICON: Record<string, string> = {
99
99
  />
100
100
  <span
101
101
  v-if="s.subtasks && s.subtasks.total > 0"
102
- class="ml-auto shrink-0 font-mono text-[9px] tabular-nums text-slate-400"
102
+ class="ms-auto shrink-0 font-mono text-[9px] tabular-nums text-slate-400"
103
103
  >
104
104
  {{ s.subtasks.completed }}/{{ s.subtasks.total }}
105
105
  </span>
@@ -108,7 +108,7 @@ const ITEM_ICON: Record<string, string> = {
108
108
  :name="
109
109
  isFailedStep(s.state, runFailed) ? FAILED_STEP_META.icon : STATE_META[s.state].icon
110
110
  "
111
- class="ml-auto h-2.5 w-2.5 shrink-0"
111
+ class="ms-auto h-2.5 w-2.5 shrink-0"
112
112
  :class="s.state === 'working' && !runFailed ? 'animate-spin' : ''"
113
113
  :style="{
114
114
  color: isFailedStep(s.state, runFailed)
@@ -162,7 +162,7 @@ const ITEM_ICON: Record<string, string> = {
162
162
  {{ agentKindMeta(companionByStep[i]!.kind).label }}
163
163
  </span>
164
164
  <span
165
- class="ml-auto shrink-0"
165
+ class="ms-auto shrink-0"
166
166
  :class="COMPANION_STATE_META[companionByStep[i]!.state].text"
167
167
  >
168
168
  {{ COMPANION_STATE_META[companionByStep[i]!.state].label }}
@@ -275,7 +275,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
275
275
  </h1>
276
276
  <p v-if="block" class="truncate text-xs text-slate-500">{{ block.title }}</p>
277
277
  </div>
278
- <div class="ml-auto flex items-center gap-1.5">
278
+ <div class="ms-auto flex items-center gap-1.5">
279
279
  <UBadge v-if="session" color="neutral" variant="subtle" size="sm">
280
280
  {{ t('brainstorm.iteration', { current: iteration, max: maxIterations }) }}
281
281
  </UBadge>
@@ -376,7 +376,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
376
376
  size="xs"
377
377
  variant="soft"
378
378
  :color="STATUS_COLOR[item.status]"
379
- class="ml-auto"
379
+ class="ms-auto"
380
380
  >
381
381
  {{ t(STATUS_LABELS[item.status]) }}
382
382
  </UBadge>
@@ -388,7 +388,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
388
388
  <!-- recorded choice -->
389
389
  <div
390
390
  v-if="item.reply"
391
- class="mt-2 rounded-md border-l-2 border-slate-700 bg-slate-950/40 px-3 py-1.5 text-sm text-slate-300"
391
+ class="mt-2 rounded-md border-s-2 border-slate-700 bg-slate-950/40 px-3 py-1.5 text-sm text-slate-300"
392
392
  >
393
393
  <span class="text-[10px] uppercase tracking-wide text-slate-500">
394
394
  {{ t('brainstorm.yourChoice') }}
@@ -464,7 +464,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
464
464
  <div v-for="s in outline.sections" :key="s.id" class="mb-2">
465
465
  <button
466
466
  v-if="s.title"
467
- class="group flex w-full items-center gap-2 text-left"
467
+ class="group flex w-full items-center gap-2 text-start"
468
468
  @click="toggle(s.id)"
469
469
  >
470
470
  <UIcon
@@ -480,7 +480,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
480
480
  </button>
481
481
  <div
482
482
  v-show="!s.title || !collapsed[s.id]"
483
- class="reader-prose mt-1 pl-5.5 text-[13px] leading-relaxed text-slate-300"
483
+ class="reader-prose mt-1 ps-5.5 text-[13px] leading-relaxed text-slate-300"
484
484
  v-html="s.bodyHtml"
485
485
  />
486
486
  </div>
@@ -489,7 +489,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
489
489
  </div>
490
490
 
491
491
  <!-- right action rail -->
492
- <aside class="hidden w-72 shrink-0 flex-col border-l border-slate-800 lg:flex">
492
+ <aside class="hidden w-72 shrink-0 flex-col border-s border-slate-800 lg:flex">
493
493
  <div class="flex flex-col gap-4 px-4 py-5">
494
494
  <div v-if="session" class="space-y-2 text-xs text-slate-400">
495
495
  <div class="flex items-center justify-between">
@@ -506,7 +506,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
506
506
  </div>
507
507
  <div v-if="session.model" class="flex items-center justify-between">
508
508
  <span>{{ t('brainstorm.rail.model') }}</span>
509
- <span class="truncate pl-2 text-slate-500">{{ session.model }}</span>
509
+ <span class="truncate ps-2 text-slate-500">{{ session.model }}</span>
510
510
  </div>
511
511
  </div>
512
512
 
@@ -521,6 +521,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
521
521
  size="sm"
522
522
  block
523
523
  icon="i-lucide-arrow-right"
524
+ :ui="{ leadingIcon: 'rtl:-scale-x-100', trailingIcon: 'rtl:-scale-x-100' }"
524
525
  :loading="acting"
525
526
  @click="proceed"
526
527
  >
@@ -258,7 +258,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
258
258
  <h1 class="truncate text-base font-semibold text-white">{{ t('clarity.title') }}</h1>
259
259
  <p v-if="block" class="truncate text-xs text-slate-500">{{ block.title }}</p>
260
260
  </div>
261
- <div class="ml-auto flex items-center gap-1.5">
261
+ <div class="ms-auto flex items-center gap-1.5">
262
262
  <UBadge v-if="review" color="neutral" variant="subtle" size="sm">
263
263
  {{ t('clarity.iteration', { current: iteration, max: maxIterations }) }}
264
264
  </UBadge>
@@ -360,7 +360,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
360
360
  size="xs"
361
361
  variant="soft"
362
362
  :color="STATUS_COLOR[item.status]"
363
- class="ml-auto"
363
+ class="ms-auto"
364
364
  >
365
365
  {{ t(STATUS_LABELS[item.status]) }}
366
366
  </UBadge>
@@ -372,7 +372,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
372
372
  <!-- recorded answer -->
373
373
  <div
374
374
  v-if="item.reply"
375
- class="mt-2 rounded-md border-l-2 border-slate-700 bg-slate-950/40 px-3 py-1.5 text-sm text-slate-300"
375
+ class="mt-2 rounded-md border-s-2 border-slate-700 bg-slate-950/40 px-3 py-1.5 text-sm text-slate-300"
376
376
  >
377
377
  <span class="text-[10px] uppercase tracking-wide text-slate-500">
378
378
  {{ t('clarity.answerLabel') }}
@@ -449,7 +449,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
449
449
  <div v-for="s in outline.sections" :key="s.id" class="mb-2">
450
450
  <button
451
451
  v-if="s.title"
452
- class="group flex w-full items-center gap-2 text-left"
452
+ class="group flex w-full items-center gap-2 text-start"
453
453
  @click="toggle(s.id)"
454
454
  >
455
455
  <UIcon
@@ -465,7 +465,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
465
465
  </button>
466
466
  <div
467
467
  v-show="!s.title || !collapsed[s.id]"
468
- class="reader-prose mt-1 pl-5.5 text-[13px] leading-relaxed text-slate-300"
468
+ class="reader-prose mt-1 ps-5.5 text-[13px] leading-relaxed text-slate-300"
469
469
  v-html="s.bodyHtml"
470
470
  />
471
471
  </div>
@@ -474,7 +474,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
474
474
  </div>
475
475
 
476
476
  <!-- right action rail -->
477
- <aside class="hidden w-72 shrink-0 flex-col border-l border-slate-800 lg:flex">
477
+ <aside class="hidden w-72 shrink-0 flex-col border-s border-slate-800 lg:flex">
478
478
  <div class="flex flex-col gap-4 px-4 py-5">
479
479
  <div v-if="review" class="space-y-2 text-xs text-slate-400">
480
480
  <div class="flex items-center justify-between">
@@ -491,7 +491,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
491
491
  </div>
492
492
  <div v-if="review.model" class="flex items-center justify-between">
493
493
  <span>{{ t('clarity.rail.model') }}</span>
494
- <span class="truncate pl-2 text-slate-500">{{ review.model }}</span>
494
+ <span class="truncate ps-2 text-slate-500">{{ review.model }}</span>
495
495
  </div>
496
496
  </div>
497
497
 
@@ -506,6 +506,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
506
506
  size="sm"
507
507
  block
508
508
  icon="i-lucide-arrow-right"
509
+ :ui="{ leadingIcon: 'rtl:-scale-x-100', trailingIcon: 'rtl:-scale-x-100' }"
509
510
  :loading="acting"
510
511
  @click="proceed"
511
512
  >
@@ -195,7 +195,7 @@ function topScore(c: ConsensusContribution): { label: string; value: number } |
195
195
  t('consensus.expert', { letter: String.fromCharCode(65 + i) })
196
196
  }}</span>
197
197
  <span class="text-slate-400"> · {{ p.role }}</span>
198
- <span v-if="p.modelId" class="ml-1 text-slate-500">({{ p.modelId }})</span>
198
+ <span v-if="p.modelId" class="ms-1 text-slate-500">({{ p.modelId }})</span>
199
199
  </div>
200
200
  </div>
201
201
  </section>
@@ -219,7 +219,7 @@ function topScore(c: ConsensusContribution): { label: string; value: number } |
219
219
  <span class="text-xs text-slate-500">{{ roleFor(c.participantId) }}</span>
220
220
  <span
221
221
  v-if="topScore(c)"
222
- class="ml-auto rounded bg-slate-800 px-1.5 py-0.5 text-xs text-slate-300"
222
+ class="ms-auto rounded bg-slate-800 px-1.5 py-0.5 text-xs text-slate-300"
223
223
  >{{
224
224
  t('consensus.topScore', {
225
225
  label: topScore(c)!.label,
@@ -199,12 +199,12 @@ onMounted(() => {
199
199
  v-for="d in importedRows"
200
200
  :key="`imp:${d.externalId}`"
201
201
  type="button"
202
- class="flex w-full items-center gap-1.5 rounded-md px-2 py-1.5 text-left text-xs text-slate-300 hover:bg-slate-800/70"
202
+ class="flex w-full items-center gap-1.5 rounded-md px-2 py-1.5 text-start text-xs text-slate-300 hover:bg-slate-800/70"
203
203
  @click="pickImported(d.externalId, d.title, d.excerpt)"
204
204
  >
205
205
  <UIcon :name="icon" class="h-3.5 w-3.5 shrink-0 text-indigo-400" />
206
206
  <span class="truncate">{{ d.title }}</span>
207
- <UBadge color="neutral" variant="soft" size="xs" class="ml-auto shrink-0">{{
207
+ <UBadge color="neutral" variant="soft" size="xs" class="ms-auto shrink-0">{{
208
208
  t('documents.picker.importedBadge')
209
209
  }}</UBadge>
210
210
  </button>
@@ -214,7 +214,7 @@ onMounted(() => {
214
214
  v-for="r in searchRows"
215
215
  :key="`hit:${r.externalId}`"
216
216
  type="button"
217
- class="flex w-full items-center gap-1.5 rounded-md px-2 py-1.5 text-left text-xs text-slate-300 hover:bg-slate-800/70"
217
+ class="flex w-full items-center gap-1.5 rounded-md px-2 py-1.5 text-start text-xs text-slate-300 hover:bg-slate-800/70"
218
218
  @click="pickSearch(r)"
219
219
  >
220
220
  <UIcon :name="icon" class="h-3.5 w-3.5 shrink-0 text-slate-400" />
@@ -225,7 +225,7 @@ onMounted(() => {
225
225
  <button
226
226
  v-if="refRow"
227
227
  type="button"
228
- class="flex w-full items-center gap-1.5 rounded-md px-2 py-1.5 text-left text-xs text-slate-300 hover:bg-slate-800/70"
228
+ class="flex w-full items-center gap-1.5 rounded-md px-2 py-1.5 text-start text-xs text-slate-300 hover:bg-slate-800/70"
229
229
  @click="pickRef(refRow)"
230
230
  >
231
231
  <UIcon name="i-lucide-link" class="h-3.5 w-3.5 shrink-0 text-slate-400" />
@@ -115,7 +115,7 @@ async function spawn() {
115
115
  {{ t('documents.spawn.buildingPlan') }}
116
116
  </div>
117
117
 
118
- <div v-else-if="plan" class="max-h-80 space-y-3 overflow-y-auto pr-1">
118
+ <div v-else-if="plan" class="max-h-80 space-y-3 overflow-y-auto pe-1">
119
119
  <div
120
120
  v-for="(frame, fi) in plan.frames"
121
121
  :key="fi"
@@ -127,7 +127,7 @@ async function spawn() {
127
127
  <UBadge variant="subtle" size="sm" color="neutral">{{ frame.type }}</UBadge>
128
128
  </div>
129
129
 
130
- <ul v-if="frame.tasks.length" class="mt-2 space-y-1 pl-6">
130
+ <ul v-if="frame.tasks.length" class="mt-2 space-y-1 ps-6">
131
131
  <li
132
132
  v-for="(task, ti) in frame.tasks"
133
133
  :key="`t-${ti}`"
@@ -138,12 +138,12 @@ async function spawn() {
138
138
  </li>
139
139
  </ul>
140
140
 
141
- <div v-for="(mod, mi) in frame.modules" :key="`m-${mi}`" class="mt-2 pl-4">
141
+ <div v-for="(mod, mi) in frame.modules" :key="`m-${mi}`" class="mt-2 ps-4">
142
142
  <div class="flex items-center gap-1.5 text-xs font-medium text-slate-200">
143
143
  <UIcon name="i-lucide-folder" class="h-3.5 w-3.5 text-amber-400" />
144
144
  {{ mod.name }}
145
145
  </div>
146
- <ul class="mt-1 space-y-1 pl-5">
146
+ <ul class="mt-1 space-y-1 ps-5">
147
147
  <li
148
148
  v-for="(task, ti) in mod.tasks"
149
149
  :key="`mt-${ti}`"
@@ -60,10 +60,17 @@ function openApprovalFor(approvalId: string) {
60
60
  >
61
61
  <!-- header / breadcrumb -->
62
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">
63
+ <UButton
64
+ icon="i-lucide-arrow-left"
65
+ :ui="{ leadingIcon: 'rtl:-scale-x-100', trailingIcon: 'rtl:-scale-x-100' }"
66
+ color="neutral"
67
+ variant="ghost"
68
+ size="sm"
69
+ @click="close"
70
+ >
64
71
  {{ t('focus.board') }}
65
72
  </UButton>
66
- <UIcon name="i-lucide-chevron-right" class="h-4 w-4 text-slate-600" />
73
+ <UIcon name="i-lucide-chevron-right" class="h-4 w-4 text-slate-600 rtl:-scale-x-100" />
67
74
  <div
68
75
  class="flex h-9 w-9 items-center justify-center rounded-lg"
69
76
  :style="{ backgroundColor: typeMeta.accent + '22' }"
@@ -76,10 +83,10 @@ function openApprovalFor(approvalId: string) {
76
83
  {{ t('focus.typeSubtitle', { type: typeMeta.label }) }}
77
84
  </div>
78
85
  </div>
79
- <UBadge :color="statusMeta.chip as any" variant="subtle" class="ml-2">
86
+ <UBadge :color="statusMeta.chip as any" variant="subtle" class="ms-2">
80
87
  {{ statusMeta.label }}
81
88
  </UBadge>
82
- <div class="ml-auto flex items-center gap-2">
89
+ <div class="ms-auto flex items-center gap-2">
83
90
  <UDropdownMenu :items="runMenu">
84
91
  <UButton
85
92
  color="primary"
@@ -284,7 +284,7 @@ async function unlinkSource(id: string) {
284
284
  >
285
285
  {{ t('fragments.catalog.live', { source: f.documentRef.source }) }}
286
286
  </UBadge>
287
- <span class="ml-auto font-mono text-[11px] text-slate-500">{{ f.id }}</span>
287
+ <span class="ms-auto font-mono text-[11px] text-slate-500">{{ f.id }}</span>
288
288
  </div>
289
289
  <p class="mt-1 text-sm text-slate-400">{{ f.summary }}</p>
290
290
  <div v-if="f.tags?.length" class="mt-1 flex flex-wrap gap-1">
@@ -316,7 +316,7 @@ async function unlinkSource(id: string) {
316
316
  size="xs"
317
317
  color="error"
318
318
  variant="ghost"
319
- class="ml-auto"
319
+ class="ms-auto"
320
320
  @click="removeFragment(f.id)"
321
321
  />
322
322
  </div>
@@ -380,7 +380,7 @@ async function unlinkSource(id: string) {
380
380
  {{ t('fragments.documents.lastResolved', { date: d(new Date(f.resolvedAt), 'long') }) }}
381
381
  </p>
382
382
  </div>
383
- <div class="ml-auto flex gap-1">
383
+ <div class="ms-auto flex gap-1">
384
384
  <UButton
385
385
  icon="i-lucide-refresh-cw"
386
386
  size="xs"
@@ -465,7 +465,7 @@ async function unlinkSource(id: string) {
465
465
  size="xs"
466
466
  color="warning"
467
467
  variant="subtle"
468
- class="ml-auto"
468
+ class="ms-auto"
469
469
  >
470
470
  {{
471
471
  t(
@@ -475,7 +475,7 @@ async function unlinkSource(id: string) {
475
475
  )
476
476
  }}
477
477
  </UBadge>
478
- <div class="ml-auto flex gap-1">
478
+ <div class="ms-auto flex gap-1">
479
479
  <UButton
480
480
  icon="i-lucide-search-check"
481
481
  size="xs"
@@ -324,7 +324,7 @@ const conflictVerdict = computed(() => {
324
324
  {{ c.name }}
325
325
  <UIcon
326
326
  name="i-lucide-external-link"
327
- class="ml-0.5 inline h-3 w-3 opacity-60 group-hover:opacity-100"
327
+ class="ms-0.5 inline h-3 w-3 opacity-60 group-hover:opacity-100"
328
328
  />
329
329
  </a>
330
330
  <span v-else class="min-w-0 flex-1 truncate text-[13px] text-slate-200">{{
@@ -397,7 +397,7 @@ const conflictVerdict = computed(() => {
397
397
  size="sm"
398
398
  >{{ OUTCOME_LABELS[a.outcome] }}</UBadge
399
399
  >
400
- <span v-if="formatClock(a.at)" class="ml-auto text-[11px] text-slate-500">{{
400
+ <span v-if="formatClock(a.at)" class="ms-auto text-[11px] text-slate-500">{{
401
401
  formatClock(a.at)
402
402
  }}</span>
403
403
  </div>
@@ -415,7 +415,7 @@ const conflictVerdict = computed(() => {
415
415
 
416
416
  <!-- Sidebar: gate state -->
417
417
  <aside
418
- class="hidden w-60 shrink-0 flex-col gap-4 border-l border-slate-800 bg-slate-900/50 px-4 py-4 lg:flex"
418
+ class="hidden w-60 shrink-0 flex-col gap-4 border-s border-slate-800 bg-slate-900/50 px-4 py-4 lg:flex"
419
419
  >
420
420
  <div v-if="gate">
421
421
  <h4 class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
@@ -361,7 +361,7 @@ async function merge(pr: GitHubPullRequest) {
361
361
  v-for="r in github.availableRepos"
362
362
  :key="r.githubId"
363
363
  type="button"
364
- class="flex w-full items-center gap-2 rounded px-2 py-1.5 text-left transition hover:bg-slate-800/60"
364
+ class="flex w-full items-center gap-2 rounded px-2 py-1.5 text-start transition hover:bg-slate-800/60"
365
365
  @click="toggleSelected(r.githubId)"
366
366
  >
367
367
  <UIcon
@@ -400,7 +400,10 @@ async function merge(pr: GitHubPullRequest) {
400
400
  class="rounded-md border border-slate-800 bg-slate-900/60"
401
401
  >
402
402
  <div class="flex items-center justify-between gap-2 px-3 py-2">
403
- <button class="flex min-w-0 items-center gap-2 text-left" @click="toggleRepo(repo)">
403
+ <button
404
+ class="flex min-w-0 items-center gap-2 text-start"
405
+ @click="toggleRepo(repo)"
406
+ >
404
407
  <UIcon
405
408
  :name="
406
409
  expandedRepo === repo.githubId
@@ -309,7 +309,7 @@ const canDestroy = computed(
309
309
  : t('humanTest.history.pulledMain')
310
310
  }}</span>
311
311
  <span
312
- class="ml-1.5 rounded px-1 text-[10px] uppercase"
312
+ class="ms-1.5 rounded px-1 text-[10px] uppercase"
313
313
  :class="
314
314
  r.outcome === 'completed'
315
315
  ? 'bg-emerald-500/15 text-emerald-300'