Skip to content

feat(ui): full dashboard redesign with shadcn-vue + light/dark theme#207

Merged
thebtf merged 5 commits into
mainfrom
feat/dashboard-redesign-pr1-foundation
Apr 25, 2026
Merged

feat(ui): full dashboard redesign with shadcn-vue + light/dark theme#207
thebtf merged 5 commits into
mainfrom
feat/dashboard-redesign-pr1-foundation

Conversation

@thebtf
Copy link
Copy Markdown
Owner

@thebtf thebtf commented Apr 25, 2026

Full Dashboard Redesign — shadcn-vue + DESIGN.md + Light/Dark Mode

Spec: .agent/specs/dashboard-redesign-shadcn/spec.md (13 FR, 5 NFR, 6 US)
Plan: .agent/specs/dashboard-redesign-shadcn/plan.md
Tasks: 56 tasks (T001-T056)

5 commits (originally planned as 7 PRs, stacked into 1):

1. Foundation (700356f):

  • shadcn-vue installed (25 component groups, New York style, Zinc base)
  • DESIGN.md created per Google Labs design.md spec
  • CSS variable color system (light :root + .dark)
  • useColorMode composable (localStorage + prefers-color-scheme)
  • Inter + JetBrains Mono fonts (replaced Fira Sans/Code)
  • lucide-vue-next installed

2. Sidebar (c871a63):

  • AppSidebar rewritten with shadcn Sidebar/SidebarProvider
  • Health/activity/retrieval stats block REMOVED from sidebar
  • FontAwesome → Lucide icons in sidebar
  • Theme toggle (Sun/Moon) in sidebar footer
  • App.vue: bg-slate-950 → bg-background (theme-aware)

3. HomeView + Auth (567fbd7):

  • HomeView: 4-section dashboard (status header, metric cards, health grid, recent issues)
  • LoginView: shadcn Card + Tabs + Input + Button
  • RegisterView: shadcn Card + Input + Label
  • SetupView: shadcn Card + Input
  • StatsCards.vue deleted

4. Issues + Management (3a8a0cc):

  • IssuesView: Table, filter buttons, Dialog (create), Select (type/priority)
  • IssueDetailView: Card, Select (status), Dialog (delete/reject), Textarea
  • VaultView: Card (status), Table (credentials), AlertDialog (delete)
  • TokensView: Table, Dialog (create), RadioGroup (scope), AlertDialog (revoke)
  • AdminView: Table, Select (role), Switch (enable/disable)
  • AppHeader: dead search route reference removed

5. Cleanup (b2a0910):

  • Deleted 7 old components (Badge, Card, IconBox, Header, TimeRangeSelector, ConfirmDialog, Pagination)
  • Removed @fortawesome/fontawesome-free
  • Removed .mcp.json (shadcn CLI artifact)
  • Fixed darkMode duplicate, useColorMode watch leak, scrollbar theme
  • Removed legacy claude-* hardcoded colors from tailwind config
  • Added 404 catch-all route
  • Zero FA icon references remaining

Verification

  • vue-tsc --noEmit — clean
  • vite build — clean (3.6s)
  • Zero fas fa- references in .vue files
  • Zero hardcoded hex in templates (except DESIGN.md)
  • All 9 views render in both light and dark mode

Summary by CodeRabbit

Примечания к выпуску

  • Новые функции

    • Добавлена поддержка светлой и тёмной темы с возможностью переключения
    • Внедрена полностью переработанная система дизайна с новыми компонентами интерфейса
    • Добавлена поддержка сворачивания боковой панели навигации
  • Улучшения

    • Полная переработка визуального оформления приложения
    • Обновлены модали, диалоги и формы для единообразного стиля
    • Переработаны таблицы и списки данных с улучшенной читаемостью
    • Обновлены значки и иконография во всём приложении
    • Улучшена типография и контрастность текста

PR 1 of dashboard redesign (CR-001, T001-T011).

- Install shadcn-vue (New York style, Zinc base, CSS variables)
- Add 25 shadcn component groups (badge, button, card, sidebar, table, dialog, etc.)
- Create DESIGN.md per Google Labs design.md spec (colors, typography, spacing, components)
- Rewrite tailwind.config.js with CSS variable color system (legacy claude-* preserved)
- Rewrite main.css with :root (light) + .dark CSS variable blocks
- Create useColorMode composable (localStorage + prefers-color-scheme)
- Replace Fira Sans/Code with Inter + JetBrains Mono (variable fonts)
- Install lucide-vue-next for future icon migration
- Add lib/utils.ts with cn() helper

vue-tsc clean, vite build clean. Existing views unchanged.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 25, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Анализ изменений

Walkthrough

Крупный рефакторинг UI-системы: внедрение дизайн-системы Engram с shadcn-vue, переход на Reka UI компоненты, замена иконок с Font Awesome на Lucide Vue, миграция шрифтов на Inter/JetBrains Mono и переход к CSS-переменным для темизации.

Changes

Когорта / Файлы Описание
Design System & Configuration
ui/DESIGN.md, ui/components.json, ui/package.json, ui/tailwind.config.js, ui/src/env.d.ts, ui/src/assets/main.css, ui/src/main.ts
Введена спецификация дизайн-системы Engram с полной цветовой схемой и типографией. Обновлены зависимости для shadcn-vue, Reka UI, Lucide Vue, переменных шрифтов. Перенесена тема на CSS-переменные с семантической палитрой. Обновлены глобальные стили и анимации.
UI Component Library - Основные компоненты
ui/src/components/ui/button/*, ui/src/components/ui/card/*, ui/src/components/ui/badge/*, ui/src/components/ui/input/*, ui/src/components/ui/label/*
Созданы базовые UI компоненты с поддержкой вариантов через CVA, семантическими стилями и интеграцией с Reka UI.
UI Component Library - Диалоги и модальные окна
ui/src/components/ui/alert-dialog/*, ui/src/components/ui/dialog/*, ui/src/components/ui/sheet/*
Реализованы компоненты для модальных окон и диалогов с поддержкой портализации, анимаций и доступности.
UI Component Library - Меню и навигация
ui/src/components/ui/dropdown-menu/*, ui/src/components/ui/select/*, ui/src/components/ui/tabs/*, ui/src/components/ui/toggle-group/*, ui/src/components/ui/toggle/*
Созданы компоненты для выпадающих меню, выбора, вкладок и переключателей с полной поддержкой вариантов и доступности.
UI Component Library - Боковая панель
ui/src/components/ui/sidebar/*, ui/src/components/ui/sidebar/utils.ts
Реализована полнофункциональная система боковой панели с состоянием коллапса, контекстом, поддержкой мобильных и рабочих стола, сохранением состояния в cookie.
UI Component Library - Дополнительные компоненты
ui/src/components/ui/avatar/*, ui/src/components/ui/progress/*, ui/src/components/ui/radio-group/*, ui/src/components/ui/scroll-area/*, ui/src/components/ui/separator/*, ui/src/components/ui/skeleton/*, ui/src/components/ui/switch/*, ui/src/components/ui/table/*, ui/src/components/ui/textarea/*, ui/src/components/ui/tooltip/*, ui/src/components/ui/sonner/*
Созданы вспомогательные UI компоненты для таблиц, форм, индикаторов и уведомлений с полной стилизацией и состояниями.
App Layout & Core Updates
ui/src/App.vue, ui/src/composables/useColorMode.ts, ui/src/composables/index.ts, ui/src/lib/utils.ts
Обновлено основное приложение для инициализации темы, добавлена поддержка переключения тёмного режима, внедрена утилита для слияния классов Tailwind (cn).
Layout Components
ui/src/components/layout/AppSidebar.vue, ui/src/components/layout/AppHeader.vue, ui/src/components/search/SearchBar.vue
Рефакторинг компонентов навигации: замена иконок на Lucide Vue, интеграция новой системы боковой панели, добавление переключателя тёмного режима.
View Pages - Authentication & Setup
ui/src/views/LoginView.vue, ui/src/views/RegisterView.vue, ui/src/views/SetupView.vue
Переработаны страницы аутентификации: замена на компонент-ориентированный дизайн с Card, Input, Label, Button, добавлены Lucide иконки для визуальной обратной связи.
View Pages - Dashboard & Content
ui/src/views/HomeView.vue, ui/src/views/IssuesView.vue, ui/src/views/IssueDetailView.vue, ui/src/views/TokensView.vue, ui/src/views/VaultView.vue
Полный рефакторинг страниц контента: замена на Table, Dialog, Badge, Select компоненты, обновлены вспомогательные функции для стилизации, добавлены состояния загрузки/ошибок с новыми компонентами.
View Pages - Admin
ui/src/views/AdminView.vue
Рефакторинг админ-панели с использованием Table, Select, Switch компонентов, обновлены обработчики для новых UI элементов.
Cleanup - Removed Components
ui/src/components/Badge.vue, ui/src/components/Card.vue, ui/src/components/Header.vue, ui/src/components/IconBox.vue, ui/src/components/StatsCards.vue, ui/src/components/TimeRangeSelector.vue, ui/src/components/layout/ConfirmDialog.vue, ui/src/components/layout/Pagination.vue, ui/src/components/index.ts
Удалены устаревшие компоненты, заменены на новые shadcn/ui компоненты. Обновлена бочка экспортов компонентов.
Router & Configuration
ui/src/router/index.ts, .gitignore
Добавлен маршрут-подстановка для неопределённых путей, обновлен .gitignore для исключения файлов расширения shadcn.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Poem

🐰 Волшебный рефакторинг пришёл на рассвете,
Shadcn/vue танцует в паутине сетей,
Иконки Lucide сияют как звёзды,
CSS-переменные поют песни темы,
Engram дизайн расцветает для всех!

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/dashboard-redesign-pr1-foundation

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new design system based on shadcn-vue, including various UI components, a sidebar layout, and a color mode composable. The review identified several issues: hardcoded dark theme classes in App.vue that conflict with the new theme system, a redundant array element in the tailwind.config.js darkMode configuration, potential theme flashing and redundant watchers in the useColorMode composable, poor visibility of the scrollbar thumb in light mode, and an impure computed property in the SidebarMenuSkeleton component.

Comment thread ui/src/App.vue
const route = useRoute()
const { authenticated, loading, checkAuth } = useAuth()
const { isReconnecting, reconnectCountdown } = useSSE()
useColorMode()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

While useColorMode() correctly initializes the theme system, the root div in the template (line 24) still has hardcoded dark theme classes (bg-slate-950 text-white). These will override the CSS variables defined in main.css and prevent the light mode from rendering correctly. Consider removing these hardcoded classes to allow the theme system to take effect.

Comment thread ui/tailwind.config.js Outdated
"./src/**/*.{vue,js,ts,jsx,tsx}",
],
darkMode: 'class',
darkMode: ['class', "class"],
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The darkMode configuration contains a duplicate value in the array. It should be a single string "class" or a single-element array ["class"].

Suggested change
darkMode: ['class', "class"],
darkMode: "class",

Comment on lines +5 to +33
const mode = ref<ColorMode>('light')
const isInitialized = ref(false)

function applyMode(value: ColorMode) {
if (value === 'dark') {
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
}
}

export function useColorMode() {
onMounted(() => {
if (!isInitialized.value) {
const stored = localStorage.getItem('theme') as ColorMode | null
if (stored) {
mode.value = stored
} else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
mode.value = 'dark'
}
applyMode(mode.value)
isInitialized.value = true
}
})

watch(mode, (newMode) => {
localStorage.setItem('theme', newMode)
applyMode(newMode)
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current implementation creates a new watch every time useColorMode is called, which leads to redundant watchers if the composable is used in multiple components. Additionally, initializing the theme inside onMounted causes a visible flash of the default theme before the stored preference is applied. Moving the initialization and the watcher to the module level ensures they only run once and execute immediately.

const mode = ref<ColorMode>('light')

function applyMode(value: ColorMode) {
  if (typeof document === 'undefined') return
  document.documentElement.classList.toggle('dark', value === 'dark')
}

if (typeof window !== 'undefined') {
  const stored = localStorage.getItem('theme') as ColorMode | null
  mode.value = stored || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
  applyMode(mode.value)
}

watch(mode, (newMode) => {
  localStorage.setItem('theme', newMode)
  applyMode(newMode)
})

export function useColorMode() {

Comment thread ui/src/assets/main.css Outdated
width: 6px;
}
.scrollbar-thin::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The scrollbar thumb color is hardcoded to a semi-transparent white, which will have poor visibility in light mode. It should use a theme-aware color variable.

Suggested change
background: rgba(255, 255, 255, 0.2);
background: hsl(var(--muted-foreground) / 0.3);

Comment on lines +12 to +14
const width = computed(() => {
return `${Math.floor(Math.random() * 40) + 50}%`
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using Math.random() inside a computed property makes it impure, which can cause hydration mismatches and instability. Since the width only needs to be random per component instance, it should be a constant generated in the setup block.

const width = (Math.floor(Math.random() * 40) + 50) + "%"

T013-T018: Rewrite AppSidebar.vue using shadcn Sidebar component tree.

- Replace custom sidebar with shadcn Sidebar/SidebarProvider/SidebarInset
- Remove health/activity/retrieval stats block (moves to HomeView in PR 3)
- Replace FontAwesome icons with Lucide (LayoutDashboard, CircleAlert, Lock, Key, etc.)
- Add theme toggle (Sun/Moon) in sidebar footer via useColorMode
- Keep connection status dot (useSSE) and auth-disabled badge
- Collapsible icon mode with SidebarRail
- Fix App.vue: bg-slate-950 → bg-background (CSS variable, theme-aware)
- Wrap authenticated layout in SidebarProvider + SidebarInset
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 18

🧹 Nitpick comments (6)
ui/src/components/ui/sidebar/SidebarGroupAction.vue (1)

13-22: Добавить явный fallback на button для семантики и доступности интерактивного элемента.

Компонент <Primitive> по умолчанию рендерит <div>, что лишает элемент семантического значения и клавиатурной доступности. Для action-элемента с интерактивной стилизацией (focus-visible, hover состояния) необходимо явно задавать as="button" и type="button" как значение по умолчанию, если пропс as не передан.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/components/ui/sidebar/SidebarGroupAction.vue` around lines 13 - 22,
Компонент рендерит интерактивный элемент через <Primitive> но по умолчанию он
выдаёт <div>, лишая кнопки семантики и клавиатурной доступности; исправьте это,
задав дефолтный проп as = 'button' и дефолтный атрибут type = 'button' когда
проп as не передан (или явно равен 'button'), сохранив поведение
asChild/props.class; измените определение пропсов/значений в
SidebarGroupAction.vue (используемые идентификаторы: Primitive, as, asChild,
props.class) так, чтобы в шаблоне <Primitive ... :as="as" ... v-bind="rest" />
передавался type="button" при рендере кнопки и не ломал кастомный as, оставляя
поведение неизменным для других случаев.
ui/src/components/ui/sidebar/SidebarMenuSubItem.vue (1)

1-3: Можно удалить пустой <script setup> для чище SFC.

Сейчас блок не несёт логики и только добавляет шум.

Предлагаемый cleanup
-<script setup lang="ts">
-
-</script>
-
 <template>
   <li>
     <slot />
   </li>
 </template>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/components/ui/sidebar/SidebarMenuSubItem.vue` around lines 1 - 3,
Удалите пустой блок <script setup lang="ts"> из SFC — он не содержит логики и
только загромождает компонент; откройте файл SidebarMenuSubItem.vue и удалите
весь пустой <script setup>...</script> блок, убедившись, что ни одна зависимость
или объявление (например переменные/импорты в этом блоке) не используются в
шаблоне или стилях перед удалением.
ui/src/composables/useColorMode.ts (1)

19-24: Небезопасное чтение theme из localStorage без валидации значения.

Сейчас любое строковое значение может попасть в mode.value из-за as ColorMode | null. Лучше явно проверять только 'light' | 'dark', иначе очищать ключ и идти в fallback.

Предлагаемый фикс
-      const stored = localStorage.getItem('theme') as ColorMode | null
-      if (stored) {
+      const stored = localStorage.getItem('theme')
+      if (stored === 'light' || stored === 'dark') {
         mode.value = stored
+      } else if (stored !== null) {
+        localStorage.removeItem('theme')
       } else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
         mode.value = 'dark'
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/composables/useColorMode.ts` around lines 19 - 24, The code reads
localStorage.getItem('theme') and casts it to ColorMode without validation;
update the logic in useColorMode to only accept 'light' or 'dark' values from
stored (use the localStorage.getItem('theme') result), otherwise remove the
invalid key and fall back to the prefers-color-scheme check; specifically
validate the retrieved string before assigning mode.value (or clearing
localStorage) and preserve the existing window.matchMedia fallback behavior when
the value is invalid or absent.
ui/src/components/ui/toggle-group/ToggleGroupItem.vue (1)

19-19: Рекомендуется добавить дефолтное значение для inject.

Хотя опциональная цепочка на строках 30-31 безопасно обрабатывает undefined, явное указание дефолтного значения в inject делает намерение более очевидным и предотвращает случайное использование context напрямую без проверки.

♻️ Предлагаемое изменение
-const context = inject<ToggleGroupVariants>("toggleGroup")
+const context = inject<ToggleGroupVariants>("toggleGroup", undefined)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/components/ui/toggle-group/ToggleGroupItem.vue` at line 19, Добавьте
явное дефолтное значение в вызов inject чтобы показать намерение и предотвратить
случайное использование context без проверки: измените вызов
inject<ToggleGroupVariants>("toggleGroup") так, чтобы он передавал вторым
аргументом безопасный дефолт (например null/undefined или минимальный
объект-фиктив, совместимый с типом ToggleGroupVariants), оставив использование
context с опциональной цепочкой; это затрагивает переменную context и тип
ToggleGroupVariants в компоненте ToggleGroupItem.vue.
ui/src/components/ui/dialog/DialogScrollContent.vue (1)

46-51: Отсутствует атрибут доступности span.sr-only.

Для улучшения доступности рекомендуется связать визуально скрытый текст с кнопкой закрытия через aria-label на самой кнопке, либо убедиться, что screenreader корректно читает вложенный sr-only элемент.

♿ Альтернативный вариант с aria-label
       <DialogClose
         class="absolute top-4 right-4 p-0.5 transition-colors rounded-md hover:bg-secondary"
+        aria-label="Close"
       >
         <X class="w-4 h-4" />
-        <span class="sr-only">Close</span>
       </DialogClose>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/components/ui/dialog/DialogScrollContent.vue` around lines 46 - 51, В
кнопке закрытия DialogClose отсутствует связанный атрибут доступности: либо
добавьте aria-label="Close" (или переводимый ресурс) на компонент DialogClose,
либо убедитесь, что вложенный span.sr-only с текстом "Close" действительно
доступен для чтения скринридерами (например, сделав DialogClose ролевой кнопкой
и не делая span aria-hidden); обновите компонент DialogClose/вложенный
span.sr-only соответственно чтобы screenreader мог объявить кнопку закрытия
(ссылки: идентификаторы DialogClose и span.sr-only, иконка X).
ui/tailwind.config.js (1)

7-7: Дублирование значения 'class' в darkMode.

Массив darkMode содержит ['class', "class"] — это избыточно. Достаточно одного значения.

♻️ Предлагаемое исправление
-  darkMode: ['class', "class"],
+  darkMode: 'class',
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/tailwind.config.js` at line 7, The darkMode config currently contains a
duplicated value ['class', "class"]; open the darkMode key and remove the
duplicate so it only specifies a single 'class' (e.g., darkMode: ['class'] or
darkMode: 'class'), ensuring the darkMode setting is not duplicated; update the
darkMode entry and commit the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.mcp.json:
- Around line 5-10: В args массива, где вызывается пакет
`@jpisnice/shadcn-ui-mcp-server` (в команде с "bunx"), зафиксируйте версию пакета
на 2.0.0: замените непинный идентификатор "@jpisnice/shadcn-ui-mcp-server" на
явно версионированный "@jpisnice/shadcn-ui-mcp-server@2.0.0" в том же массиве
args (см. символы "command": "bunx" и массив "args") чтобы обеспечить
воспроизводимость и использовать совместимую версию с флагом "--framework vue".

In `@ui/DESIGN.md`:
- Line 156: The badgeVariants in badge/index.ts currently include the CSS class
"shadow" for the "default" and "destructive" variants which contradicts the
DESIGN.md rule; remove the "shadow" class from those variants in the
badgeVariants definition (or, if you prefer to allow shadows for small
interactive badges, update DESIGN.md to document the exception and keep the
code), ensuring the change is applied to the variants named "default" and
"destructive" in the badgeVariants export.

In `@ui/src/assets/main.css`:
- Around line 93-99: Переименовать keyframes-правило и все ссылки на него в CSS
с pulseDot в kebab-case pulse-dot: обновить объявление `@keyframes` pulseDot ->
`@keyframes` pulse-dot и изменить селектор/правило .pulse-dot { animation:
pulseDot 2s infinite; } на использование animation: pulse-dot 2s infinite;,
чтобы соответствовать keyframes-name-pattern и Stylelint.

In `@ui/src/components/ui/avatar/AvatarImage.vue`:
- Around line 9-11: Удалите внутренний <slot /> из компонента AvatarImage:
текущий код рендерит <AvatarImage v-bind="props" class="h-full w-full
object-cover"> <slot /> </AvatarImage>, но в reka-ui v2.9.6 AvatarImage —
leaf-компонент (рендерит img) и дочерний контент игнорируется; замените
использование слота на использование AvatarFallback для fallback-контента и
убедитесь, что компонент AvatarImage больше не экспонирует слот в разметке или
пропсах (поискать символы AvatarImage и AvatarFallback в данном файле и
удалить/заменить только слот, не меняя остальную передачу props).

In `@ui/src/components/ui/button/Button.vue`:
- Around line 21-25: Компонент Button.vue не задаёт безопасный type для случая
as="button", из‑за чего кнопка по умолчанию становится submit; в коде вокруг
Primitive (используются props/as/asChild/buttonVariants/cn) добавьте передачу
атрибута type так, чтобы при as === 'button' и отсутствии явного props.type
передавался type="button", а в остальных случаях сохранялось поведение
переданного props.type или отсутствие type; реализуйте это в пропах, вычисляя
type значение (например :type="as === 'button' && !props.type ? 'button' :
props.type") и передайте его в Primitive.

In `@ui/src/components/ui/dropdown-menu/DropdownMenuItem.vue`:
- Around line 8-12: В текущем коде проп inset попадает в forwardedProps и затем
в v-bind; чтобы этого избежать, исключите inset при создании delegatedProps
вместе с class (используя reactiveOmit(props, "class", "inset") или аналог)
таким что delegatedProps не содержит inset, а forwardedProps =
useForwardProps(delegatedProps) будет автоматически не включать inset; обновите
места где определены props, delegatedProps и forwardedProps (defineProps,
reactiveOmit, useForwardProps) чтобы гарантировать, что inset остаётся только
для локальной стилизации.

In `@ui/src/components/ui/dropdown-menu/DropdownMenuLabel.vue`:
- Around line 10-18: В текущей реализации `inset` попадает в `forwardedProps`
and пробрасывается в `<DropdownMenuLabel>` как атрибут; исключите `inset` из
передаваемых пропсов — например, обновите создание
`delegatedProps`/`forwardedProps` (вместо reactiveOmit(props, "class") вызвать
reactiveOmit(props, "class", "inset") или удалить `inset` из `forwardedProps`
перед v-bind) и продолжайте использовать локальную переменную `inset` для
стилизации в шаблоне (cn(..., inset && 'pl-8', props.class)).

In `@ui/src/components/ui/dropdown-menu/DropdownMenuTrigger.vue`:
- Line 11: На элементе DropdownMenuTrigger класс outline-none убирает
стандартный индикатор фокуса без альтернативы; замените или удалите outline-none
и добавьте доступный стиль для клавиатурного фокуса (например через
focus-visible или явный фокус-стиль типа focus-visible:ring /
focus-visible:outline) чтобы сохранить видимый индикатор при навигации
клавиатурой; проверьте компонент DropdownMenuTrigger и место передачи
forwardedProps, чтобы новый фокус-стиль применялся корректно и не ломался
другими классами.

In `@ui/src/components/ui/input/Input.vue`:
- Around line 6-24: The component's public contract declares modelValue as
string | number but the underlying <input> only produces strings, so update the
types to be string-only: change defineProps to defaultValue?: string and
modelValue?: string, change defineEmits to emit payload: string, and keep
useVModel usage (useVModel(props, "modelValue", emits, { passive: true,
defaultValue: props.defaultValue })). Alternatively, if numeric support is
required, change the <input> to type="number" or add explicit parse/format logic
around useVModel to convert between string and number.

In `@ui/src/components/ui/progress/Progress.vue`:
- Around line 31-34: The progress offset is computed as `100 - modelValue`,
which assumes modelValue is already a percent; change the calculation to compute
a percentage from absolute values: derive percent = ((props.modelValue ?? 0) /
(props.max ?? 100)) * 100, guard against division by zero (treat max falsy as
100) and clamp the result to 0..100, then use `transform: translateX(-${100 -
percent}%)` for the ProgressIndicator; update the usage around ProgressIndicator
and props.modelValue/props.max references so visual progress is correct for
custom max and out-of-range values.

In `@ui/src/components/ui/select/SelectLabel.vue`:
- Around line 7-13: The component currently only forwards the class prop to the
inner SelectLabel, dropping all other SelectLabelProps; update the template to
proxy all props like other components by using reactiveOmit to omit the local
"class" (or whatever needs separate handling) and then v-bind the remaining
props into <SelectLabel> so SelectLabelProps are preserved; specifically
import/use reactiveOmit on the defined props (props of type SelectLabelProps & {
class?: ... }), create an omittedProps variable (e.g., reactiveOmit(props,
['class'])), and change the <SelectLabel> usage to v-bind the omittedProps while
still applying cn('px-2 ...', props.class).

In `@ui/src/components/ui/sheet/SheetContent.vue`:
- Around line 46-50: Кнопка закрытия (DialogClose) не имеет доступного имени —
добавьте скрытый текст или атрибут aria-label аналогично реализации в
DialogScrollContent.vue: внутри компонента DialogClose (рядом с X) добавьте
элемент со sr-only текстом, например «Close»/«Закрыть», или поставьте
aria-label="Закрыть" на DialogClose, чтобы скринридеры могли озвучивать кнопку;
обновите тесты/снимки при необходимости.

In `@ui/src/components/ui/sidebar/SidebarMenuSub.vue`:
- Around line 11-13: The data-sidebar attribute on the SidebarMenuSub component
appears to be a typo: change the attribute value from "menu-badge" to a value
matching this component's purpose (e.g., "submenu" or "menu-sub") in the <ul>
inside SidebarMenuSub so selectors/styles target the correct element; update any
CSS/JS selectors that referenced data-sidebar="menu-badge" to the new value to
keep behavior consistent.

In `@ui/src/components/ui/sidebar/SidebarProvider.vue`:
- Around line 30-35: In setOpen, avoid directly accessing document.cookie for
SSR safety: use the same safe document reference used elsewhere (e.g.
defaultDocument) or guard with a null check before writing the cookie. Update
the assignment that currently uses document.cookie to use
defaultDocument?.cookie (or wrap the write in if (defaultDocument) { ... }) and
keep the existing SIDEBAR_COOKIE_NAME and SIDEBAR_COOKIE_MAX_AGE symbols so the
cookie-setting behavior remains unchanged.

In `@ui/src/components/ui/sidebar/SidebarRail.vue`:
- Around line 14-19: В компоненте SidebarRail.vue кнопка с атрибутом
data-sidebar="rail" сейчас не имеет явного типа, из‑за чего внутри формы будет
работать как submit; исправьте это, добавив type="button" к той же кнопке (тот
элемент в шаблоне, где указаны data-sidebar="rail", aria-label="Toggle Sidebar",
title="Toggle Sidebar" и :class="cn(...)") чтобы она не отправляла форму по
умолчанию.

In `@ui/src/components/ui/sonner/Sonner.vue`:
- Around line 8-24: The component currently omits props.toastOptions via
reactiveOmit (delegatedProps) so callers cannot provide custom toastOptions;
instead stop excluding toastOptions and merge the incoming props.toastOptions
with the local class defaults before passing to the Sonner component: read
props.toastOptions (or delegatedProps.toastOptions if you keep reactiveOmit)
into a mergedToastOptions object using the spread operator to combine { classes:
{ ...localClasses } } with props.toastOptions (ensuring nested classes merge,
not overwrite), then pass :toast-options="mergedToastOptions" to the Sonner
element and keep v-bind="delegatedProps" for the rest of the props.

In `@ui/src/components/ui/textarea/Textarea.vue`:
- Around line 6-24: Компонент Textarea использует числовые типы для
modelValue/defaultValue/эмита, что противоречит поведению нативного <textarea>;
замените все упоминания number на string. В defineProps уберите number и
оставьте defaultValue?: string и modelValue?: string; в defineEmits обновите
сигнатуру события (update:modelValue) чтобы payload был string; при вызове
useVModel (переменная modelValue) передавайте defaultValue как string (удалите
число из union). Проверьте, что в шаблоне связывание v-model="modelValue" и
любые места использования modelValue ожидают строку.

In `@ui/src/components/ui/toggle-group/ToggleGroup.vue`:
- Around line 20-23: The provided context object in ToggleGroup.vue is
non-reactive because it captures props at mount; change the
provide("toggleGroup", ...) to supply a computed/ref wrapper (e.g., a computed
object exposing variant and size) so updates to props.variant/props.size
propagate, and update ToggleGroupItem.vue to inject the provided value as a
ComputedRef (inject("toggleGroup") typed as ComputedRef<ToggleGroupVariants>)
and read values via the .value in the template/logic (e.g., use
context.value.variant / context.value.size with fallbacks).

---

Nitpick comments:
In `@ui/src/components/ui/dialog/DialogScrollContent.vue`:
- Around line 46-51: В кнопке закрытия DialogClose отсутствует связанный атрибут
доступности: либо добавьте aria-label="Close" (или переводимый ресурс) на
компонент DialogClose, либо убедитесь, что вложенный span.sr-only с текстом
"Close" действительно доступен для чтения скринридерами (например, сделав
DialogClose ролевой кнопкой и не делая span aria-hidden); обновите компонент
DialogClose/вложенный span.sr-only соответственно чтобы screenreader мог
объявить кнопку закрытия (ссылки: идентификаторы DialogClose и span.sr-only,
иконка X).

In `@ui/src/components/ui/sidebar/SidebarGroupAction.vue`:
- Around line 13-22: Компонент рендерит интерактивный элемент через <Primitive>
но по умолчанию он выдаёт <div>, лишая кнопки семантики и клавиатурной
доступности; исправьте это, задав дефолтный проп as = 'button' и дефолтный
атрибут type = 'button' когда проп as не передан (или явно равен 'button'),
сохранив поведение asChild/props.class; измените определение пропсов/значений в
SidebarGroupAction.vue (используемые идентификаторы: Primitive, as, asChild,
props.class) так, чтобы в шаблоне <Primitive ... :as="as" ... v-bind="rest" />
передавался type="button" при рендере кнопки и не ломал кастомный as, оставляя
поведение неизменным для других случаев.

In `@ui/src/components/ui/sidebar/SidebarMenuSubItem.vue`:
- Around line 1-3: Удалите пустой блок <script setup lang="ts"> из SFC — он не
содержит логики и только загромождает компонент; откройте файл
SidebarMenuSubItem.vue и удалите весь пустой <script setup>...</script> блок,
убедившись, что ни одна зависимость или объявление (например переменные/импорты
в этом блоке) не используются в шаблоне или стилях перед удалением.

In `@ui/src/components/ui/toggle-group/ToggleGroupItem.vue`:
- Line 19: Добавьте явное дефолтное значение в вызов inject чтобы показать
намерение и предотвратить случайное использование context без проверки: измените
вызов inject<ToggleGroupVariants>("toggleGroup") так, чтобы он передавал вторым
аргументом безопасный дефолт (например null/undefined или минимальный
объект-фиктив, совместимый с типом ToggleGroupVariants), оставив использование
context с опциональной цепочкой; это затрагивает переменную context и тип
ToggleGroupVariants в компоненте ToggleGroupItem.vue.

In `@ui/src/composables/useColorMode.ts`:
- Around line 19-24: The code reads localStorage.getItem('theme') and casts it
to ColorMode without validation; update the logic in useColorMode to only accept
'light' or 'dark' values from stored (use the localStorage.getItem('theme')
result), otherwise remove the invalid key and fall back to the
prefers-color-scheme check; specifically validate the retrieved string before
assigning mode.value (or clearing localStorage) and preserve the existing
window.matchMedia fallback behavior when the value is invalid or absent.

In `@ui/tailwind.config.js`:
- Line 7: The darkMode config currently contains a duplicated value ['class',
"class"]; open the darkMode key and remove the duplicate so it only specifies a
single 'class' (e.g., darkMode: ['class'] or darkMode: 'class'), ensuring the
darkMode setting is not duplicated; update the darkMode entry and commit the
change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4f4592f6-3599-4ed9-bf93-1645fdfda137

📥 Commits

Reviewing files that changed from the base of the PR and between 5e7620f and 700356f.

⛔ Files ignored due to path filters (1)
  • ui/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (156)
  • .mcp.json
  • ui/DESIGN.md
  • ui/components.json
  • ui/package.json
  • ui/src/App.vue
  • ui/src/assets/main.css
  • ui/src/components/ui/alert-dialog/AlertDialog.vue
  • ui/src/components/ui/alert-dialog/AlertDialogAction.vue
  • ui/src/components/ui/alert-dialog/AlertDialogCancel.vue
  • ui/src/components/ui/alert-dialog/AlertDialogContent.vue
  • ui/src/components/ui/alert-dialog/AlertDialogDescription.vue
  • ui/src/components/ui/alert-dialog/AlertDialogFooter.vue
  • ui/src/components/ui/alert-dialog/AlertDialogHeader.vue
  • ui/src/components/ui/alert-dialog/AlertDialogTitle.vue
  • ui/src/components/ui/alert-dialog/AlertDialogTrigger.vue
  • ui/src/components/ui/alert-dialog/index.ts
  • ui/src/components/ui/avatar/Avatar.vue
  • ui/src/components/ui/avatar/AvatarFallback.vue
  • ui/src/components/ui/avatar/AvatarImage.vue
  • ui/src/components/ui/avatar/index.ts
  • ui/src/components/ui/badge/Badge.vue
  • ui/src/components/ui/badge/index.ts
  • ui/src/components/ui/button/Button.vue
  • ui/src/components/ui/button/index.ts
  • ui/src/components/ui/card/Card.vue
  • ui/src/components/ui/card/CardContent.vue
  • ui/src/components/ui/card/CardDescription.vue
  • ui/src/components/ui/card/CardFooter.vue
  • ui/src/components/ui/card/CardHeader.vue
  • ui/src/components/ui/card/CardTitle.vue
  • ui/src/components/ui/card/index.ts
  • ui/src/components/ui/dialog/Dialog.vue
  • ui/src/components/ui/dialog/DialogClose.vue
  • ui/src/components/ui/dialog/DialogContent.vue
  • ui/src/components/ui/dialog/DialogDescription.vue
  • ui/src/components/ui/dialog/DialogFooter.vue
  • ui/src/components/ui/dialog/DialogHeader.vue
  • ui/src/components/ui/dialog/DialogScrollContent.vue
  • ui/src/components/ui/dialog/DialogTitle.vue
  • ui/src/components/ui/dialog/DialogTrigger.vue
  • ui/src/components/ui/dialog/index.ts
  • ui/src/components/ui/dropdown-menu/DropdownMenu.vue
  • ui/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue
  • ui/src/components/ui/dropdown-menu/DropdownMenuContent.vue
  • ui/src/components/ui/dropdown-menu/DropdownMenuGroup.vue
  • ui/src/components/ui/dropdown-menu/DropdownMenuItem.vue
  • ui/src/components/ui/dropdown-menu/DropdownMenuLabel.vue
  • ui/src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue
  • ui/src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue
  • ui/src/components/ui/dropdown-menu/DropdownMenuSeparator.vue
  • ui/src/components/ui/dropdown-menu/DropdownMenuShortcut.vue
  • ui/src/components/ui/dropdown-menu/DropdownMenuSub.vue
  • ui/src/components/ui/dropdown-menu/DropdownMenuSubContent.vue
  • ui/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue
  • ui/src/components/ui/dropdown-menu/DropdownMenuTrigger.vue
  • ui/src/components/ui/dropdown-menu/index.ts
  • ui/src/components/ui/input/Input.vue
  • ui/src/components/ui/input/index.ts
  • ui/src/components/ui/label/Label.vue
  • ui/src/components/ui/label/index.ts
  • ui/src/components/ui/progress/Progress.vue
  • ui/src/components/ui/progress/index.ts
  • ui/src/components/ui/radio-group/RadioGroup.vue
  • ui/src/components/ui/radio-group/RadioGroupItem.vue
  • ui/src/components/ui/radio-group/index.ts
  • ui/src/components/ui/scroll-area/ScrollArea.vue
  • ui/src/components/ui/scroll-area/ScrollBar.vue
  • ui/src/components/ui/scroll-area/index.ts
  • ui/src/components/ui/select/Select.vue
  • ui/src/components/ui/select/SelectContent.vue
  • ui/src/components/ui/select/SelectGroup.vue
  • ui/src/components/ui/select/SelectItem.vue
  • ui/src/components/ui/select/SelectItemText.vue
  • ui/src/components/ui/select/SelectLabel.vue
  • ui/src/components/ui/select/SelectScrollDownButton.vue
  • ui/src/components/ui/select/SelectScrollUpButton.vue
  • ui/src/components/ui/select/SelectSeparator.vue
  • ui/src/components/ui/select/SelectTrigger.vue
  • ui/src/components/ui/select/SelectValue.vue
  • ui/src/components/ui/select/index.ts
  • ui/src/components/ui/separator/Separator.vue
  • ui/src/components/ui/separator/index.ts
  • ui/src/components/ui/sheet/Sheet.vue
  • ui/src/components/ui/sheet/SheetClose.vue
  • ui/src/components/ui/sheet/SheetContent.vue
  • ui/src/components/ui/sheet/SheetDescription.vue
  • ui/src/components/ui/sheet/SheetFooter.vue
  • ui/src/components/ui/sheet/SheetHeader.vue
  • ui/src/components/ui/sheet/SheetTitle.vue
  • ui/src/components/ui/sheet/SheetTrigger.vue
  • ui/src/components/ui/sheet/index.ts
  • ui/src/components/ui/sidebar/Sidebar.vue
  • ui/src/components/ui/sidebar/SidebarContent.vue
  • ui/src/components/ui/sidebar/SidebarFooter.vue
  • ui/src/components/ui/sidebar/SidebarGroup.vue
  • ui/src/components/ui/sidebar/SidebarGroupAction.vue
  • ui/src/components/ui/sidebar/SidebarGroupContent.vue
  • ui/src/components/ui/sidebar/SidebarGroupLabel.vue
  • ui/src/components/ui/sidebar/SidebarHeader.vue
  • ui/src/components/ui/sidebar/SidebarInput.vue
  • ui/src/components/ui/sidebar/SidebarInset.vue
  • ui/src/components/ui/sidebar/SidebarMenu.vue
  • ui/src/components/ui/sidebar/SidebarMenuAction.vue
  • ui/src/components/ui/sidebar/SidebarMenuBadge.vue
  • ui/src/components/ui/sidebar/SidebarMenuButton.vue
  • ui/src/components/ui/sidebar/SidebarMenuButtonChild.vue
  • ui/src/components/ui/sidebar/SidebarMenuItem.vue
  • ui/src/components/ui/sidebar/SidebarMenuSkeleton.vue
  • ui/src/components/ui/sidebar/SidebarMenuSub.vue
  • ui/src/components/ui/sidebar/SidebarMenuSubButton.vue
  • ui/src/components/ui/sidebar/SidebarMenuSubItem.vue
  • ui/src/components/ui/sidebar/SidebarProvider.vue
  • ui/src/components/ui/sidebar/SidebarRail.vue
  • ui/src/components/ui/sidebar/SidebarSeparator.vue
  • ui/src/components/ui/sidebar/SidebarTrigger.vue
  • ui/src/components/ui/sidebar/index.ts
  • ui/src/components/ui/sidebar/utils.ts
  • ui/src/components/ui/skeleton/Skeleton.vue
  • ui/src/components/ui/skeleton/index.ts
  • ui/src/components/ui/sonner/Sonner.vue
  • ui/src/components/ui/sonner/index.ts
  • ui/src/components/ui/switch/Switch.vue
  • ui/src/components/ui/switch/index.ts
  • ui/src/components/ui/table/Table.vue
  • ui/src/components/ui/table/TableBody.vue
  • ui/src/components/ui/table/TableCaption.vue
  • ui/src/components/ui/table/TableCell.vue
  • ui/src/components/ui/table/TableEmpty.vue
  • ui/src/components/ui/table/TableFooter.vue
  • ui/src/components/ui/table/TableHead.vue
  • ui/src/components/ui/table/TableHeader.vue
  • ui/src/components/ui/table/TableRow.vue
  • ui/src/components/ui/table/index.ts
  • ui/src/components/ui/tabs/Tabs.vue
  • ui/src/components/ui/tabs/TabsContent.vue
  • ui/src/components/ui/tabs/TabsList.vue
  • ui/src/components/ui/tabs/TabsTrigger.vue
  • ui/src/components/ui/tabs/index.ts
  • ui/src/components/ui/textarea/Textarea.vue
  • ui/src/components/ui/textarea/index.ts
  • ui/src/components/ui/toggle-group/ToggleGroup.vue
  • ui/src/components/ui/toggle-group/ToggleGroupItem.vue
  • ui/src/components/ui/toggle-group/index.ts
  • ui/src/components/ui/toggle/Toggle.vue
  • ui/src/components/ui/toggle/index.ts
  • ui/src/components/ui/tooltip/Tooltip.vue
  • ui/src/components/ui/tooltip/TooltipContent.vue
  • ui/src/components/ui/tooltip/TooltipProvider.vue
  • ui/src/components/ui/tooltip/TooltipTrigger.vue
  • ui/src/components/ui/tooltip/index.ts
  • ui/src/composables/index.ts
  • ui/src/composables/useColorMode.ts
  • ui/src/env.d.ts
  • ui/src/lib/utils.ts
  • ui/src/main.ts
  • ui/tailwind.config.js

Comment thread .mcp.json Outdated
Comment on lines +5 to +10
"command": "bunx",
"args": [
"-y",
"@jpisnice/shadcn-ui-mcp-server",
"--framework",
"vue"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Какая последняя стабильная версия npm-пакета @jpisnice/shadcn-ui-mcp-server и поддерживает ли она аргумент "--framework vue"?

💡 Result:

Последняя стабильная версия npm-пакета @jpisnice/shadcn-ui-mcp-server — 2.0.0 (опубликована 30 января 2026 года). Она поддерживает аргумент "--framework vue", как указано в официальной документации на npmjs.com и GitHub: npx @jpisnice/shadcn-ui-mcp-server --framework vue.

Citations:


Зафиксируйте версию MCP-сервера @jpisnice/shadcn-ui-mcp-server на 2.0.0.

На Line 8 используется непинованный пакет, что снижает воспроизводимость сборки и повышает риски supply-chain при последующих установках. Последняя стабильная версия — 2.0.0, которая поддерживает требуемый аргумент --framework vue.

Предлагаемое исправление
-        "@jpisnice/shadcn-ui-mcp-server",
+        "@jpisnice/shadcn-ui-mcp-server@2.0.0",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"command": "bunx",
"args": [
"-y",
"@jpisnice/shadcn-ui-mcp-server",
"--framework",
"vue"
"command": "bunx",
"args": [
"-y",
"@jpisnice/shadcn-ui-mcp-server@2.0.0",
"--framework",
"vue"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.mcp.json around lines 5 - 10, В args массива, где вызывается пакет
`@jpisnice/shadcn-ui-mcp-server` (в команде с "bunx"), зафиксируйте версию пакета
на 2.0.0: замените непинный идентификатор "@jpisnice/shadcn-ui-mcp-server" на
явно версионированный "@jpisnice/shadcn-ui-mcp-server@2.0.0" в том же массиве
args (см. символы "command": "bunx" и массив "args") чтобы обеспечить
воспроизводимость и использовать совместимую версию с флагом "--framework vue".

Comment thread ui/DESIGN.md
- Do maintain WCAG AA contrast ratios (4.5:1 for normal text)
- Do use Inter for all UI text — never mix in other sans-serif fonts
- Do use consistent 8px border radius on cards and inputs
- Don't use shadows for elevation — use borders instead
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Несоответствие документации и реализации компонентов.

Документация запрещает использование теней, однако в badgeVariants (файл badge/index.ts) варианты default и destructive используют класс shadow. Рекомендуется либо обновить документацию, уточнив исключения (например, "тени допустимы для мелких интерактивных элементов"), либо удалить shadow из badge-вариантов.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/DESIGN.md` at line 156, The badgeVariants in badge/index.ts currently
include the CSS class "shadow" for the "default" and "destructive" variants
which contradicts the DESIGN.md rule; remove the "shadow" class from those
variants in the badgeVariants definition (or, if you prefer to allow shadows for
small interactive badges, update DESIGN.md to document the exception and keep
the code), ensuring the change is applied to the variants named "default" and
"destructive" in the badgeVariants export.

Comment thread ui/src/assets/main.css
Comment on lines +93 to 99
.pulse-dot {
animation: pulseDot 2s infinite;
}
@keyframes pulseDot {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Имя @keyframes не соответствует kebab-case.

Stylelint указывает на нарушение keyframes-name-pattern: pulseDot должен быть pulse-dot.

🔧 Предлагаемое исправление
 .pulse-dot {
-  animation: pulseDot 2s infinite;
+  animation: pulse-dot 2s infinite;
 }
-@keyframes pulseDot {
+@keyframes pulse-dot {
   0%, 100% { opacity: 1; }
   50% { opacity: 0.5; }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.pulse-dot {
animation: pulseDot 2s infinite;
}
@keyframes pulseDot {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.pulse-dot {
animation: pulse-dot 2s infinite;
}
`@keyframes` pulse-dot {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
🧰 Tools
🪛 Stylelint (17.9.0)

[error] 96-96: Expected keyframe name "pulseDot" to be kebab-case (keyframes-name-pattern)

(keyframes-name-pattern)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/assets/main.css` around lines 93 - 99, Переименовать keyframes-правило
и все ссылки на него в CSS с pulseDot в kebab-case pulse-dot: обновить
объявление `@keyframes` pulseDot -> `@keyframes` pulse-dot и изменить
селектор/правило .pulse-dot { animation: pulseDot 2s infinite; } на
использование animation: pulse-dot 2s infinite;, чтобы соответствовать
keyframes-name-pattern и Stylelint.

Comment on lines +9 to +11
<AvatarImage v-bind="props" class="h-full w-full object-cover">
<slot />
</AvatarImage>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

For reka-ui version 2.9.6, does AvatarImage support rendering default slot children, or is it a leaf image component without child content?

💡 Result:

For reka-ui v2.9.6, AvatarImage does not support rendering default-slot children; it’s effectively a leaf “img” element part (no place for child content). Instead, any fallback/placeholder content should be provided via AvatarFallback (a separate part), not as children of AvatarImage. Evidence: the official Avatar docs show composition as , and the AvatarImage API lists only props (e.g., src, as/asChild, crossOrigin, referrerPolicy) and its loading-status event—there is no documented slot/children usage for AvatarImage itself.

Citations:


Удалите слот из AvatarImage: дочерний контент не будет отрисован.

На строке 10 добавлен <slot />, но AvatarImage в reka-ui v2.9.6 — это leaf-компонент (рендерит img), и дочерний контент игнорируется. Это делает API компонента вводящим в заблуждение для потребителей. Используйте AvatarFallback для fallback-контента вместо дочерних элементов AvatarImage.

Предлагаемое исправление
-  <AvatarImage v-bind="props" class="h-full w-full object-cover">
-    <slot />
-  </AvatarImage>
+  <AvatarImage v-bind="props" class="h-full w-full object-cover" />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/components/ui/avatar/AvatarImage.vue` around lines 9 - 11, Удалите
внутренний <slot /> из компонента AvatarImage: текущий код рендерит <AvatarImage
v-bind="props" class="h-full w-full object-cover"> <slot /> </AvatarImage>, но в
reka-ui v2.9.6 AvatarImage — leaf-компонент (рендерит img) и дочерний контент
игнорируется; замените использование слота на использование AvatarFallback для
fallback-контента и убедитесь, что компонент AvatarImage больше не экспонирует
слот в разметке или пропсах (поискать символы AvatarImage и AvatarFallback в
данном файле и удалить/заменить только слот, не меняя остальную передачу props).

Comment on lines +21 to +25
<Primitive
:as="as"
:as-child="asChild"
:class="cn(buttonVariants({ variant, size }), props.class)"
>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Добавьте безопасный type для случая as="button".

Сейчас при рендере как <button> без type компонент становится submit-кнопкой по умолчанию, что может вызывать непреднамеренную отправку форм.

Предлагаемое исправление
   <Primitive
     :as="as"
     :as-child="asChild"
+    :type="as === 'button' ? 'button' : undefined"
     :class="cn(buttonVariants({ variant, size }), props.class)"
   >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Primitive
:as="as"
:as-child="asChild"
:class="cn(buttonVariants({ variant, size }), props.class)"
>
<Primitive
:as="as"
:as-child="asChild"
:type="as === 'button' ? 'button' : undefined"
:class="cn(buttonVariants({ variant, size }), props.class)"
>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/components/ui/button/Button.vue` around lines 21 - 25, Компонент
Button.vue не задаёт безопасный type для случая as="button", из‑за чего кнопка
по умолчанию становится submit; в коде вокруг Primitive (используются
props/as/asChild/buttonVariants/cn) добавьте передачу атрибута type так, чтобы
при as === 'button' и отсутствии явного props.type передавался type="button", а
в остальных случаях сохранялось поведение переданного props.type или отсутствие
type; реализуйте это в пропах, вычисляя type значение (например :type="as ===
'button' && !props.type ? 'button' : props.type") и передайте его в Primitive.

Comment on lines +30 to +35
function setOpen(value: boolean) {
open.value = value // emits('update:open', value)

// This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${open.value}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Несогласованность SSR-защиты при доступе к document.

В строке 14 используется defaultDocument?.cookie для SSR-безопасности, но в строке 34 происходит прямой доступ к document.cookie без проверки. Это может вызвать ошибку при серверном рендеринге.

🛡️ Предлагаемое исправление
 function setOpen(value: boolean) {
   open.value = value // emits('update:open', value)

   // This sets the cookie to keep the sidebar state.
-  document.cookie = `${SIDEBAR_COOKIE_NAME}=${open.value}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
+  if (defaultDocument) {
+    defaultDocument.cookie = `${SIDEBAR_COOKIE_NAME}=${open.value}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
+  }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function setOpen(value: boolean) {
open.value = value // emits('update:open', value)
// This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${open.value}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
}
function setOpen(value: boolean) {
open.value = value // emits('update:open', value)
// This sets the cookie to keep the sidebar state.
if (defaultDocument) {
defaultDocument.cookie = `${SIDEBAR_COOKIE_NAME}=${open.value}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/components/ui/sidebar/SidebarProvider.vue` around lines 30 - 35, In
setOpen, avoid directly accessing document.cookie for SSR safety: use the same
safe document reference used elsewhere (e.g. defaultDocument) or guard with a
null check before writing the cookie. Update the assignment that currently uses
document.cookie to use defaultDocument?.cookie (or wrap the write in if
(defaultDocument) { ... }) and keep the existing SIDEBAR_COOKIE_NAME and
SIDEBAR_COOKIE_MAX_AGE symbols so the cookie-setting behavior remains unchanged.

Comment on lines +14 to +19
<button
data-sidebar="rail"
aria-label="Toggle Sidebar"
:tabindex="-1"
title="Toggle Sidebar"
:class="cn(
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Для rail-кнопки лучше явно указать type="button".

Иначе внутри формы она будет вести себя как submit по умолчанию.

Предлагаемое исправление
   <button
+    type="button"
     data-sidebar="rail"
     aria-label="Toggle Sidebar"
     :tabindex="-1"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<button
data-sidebar="rail"
aria-label="Toggle Sidebar"
:tabindex="-1"
title="Toggle Sidebar"
:class="cn(
<button
type="button"
data-sidebar="rail"
aria-label="Toggle Sidebar"
:tabindex="-1"
title="Toggle Sidebar"
:class="cn(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/components/ui/sidebar/SidebarRail.vue` around lines 14 - 19, В
компоненте SidebarRail.vue кнопка с атрибутом data-sidebar="rail" сейчас не
имеет явного типа, из‑за чего внутри формы будет работать как submit; исправьте
это, добавив type="button" к той же кнопке (тот элемент в шаблоне, где указаны
data-sidebar="rail", aria-label="Toggle Sidebar", title="Toggle Sidebar" и
:class="cn(...)") чтобы она не отправляла форму по умолчанию.

Comment on lines +8 to +24
const delegatedProps = reactiveOmit(props, "toastOptions")
</script>

<template>
<Sonner
class="toaster group"
:toast-options="{
classes: {
toast: 'group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg',
description: 'group-[.toast]:text-muted-foreground',
actionButton:
'group-[.toast]:bg-primary group-[.toast]:text-primary-foreground',
cancelButton:
'group-[.toast]:bg-muted group-[.toast]:text-muted-foreground',
},
}"
v-bind="delegatedProps"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Для vue-sonner v2.x: подтверждает ли документация, что ToasterProps.toastOptions предназначен для пользовательской настройки (например duration/unstyled/classNames), и рекомендуется ли сохранять возможность его передачи из обёртки?

💡 Result:

Да, документация vue-sonner v2.x подтверждает, что ToasterProps.toastOptions предназначен для пользовательской настройки, включая duration, unstyled и classNames (classes). Официальная документация на GitHub и npm показывает примеры: Это глобальная настройка стилей для всех тостов, включая per-type стили (error, success и т.д.). Рекомендация сохранять передачу toastOptions из обёртки (wrapper): да, поскольку это стандартный prop Toaster, и примеры обёрток (shadcn-vue, vuetify-sonner) используют spread props или toastOptions для передачи пользовательских настроек, обеспечивая гибкость. Нет указаний на deprecation в v2 (changelog фокусируется на CSS/refactor, API сохранён).

Citations:


🏁 Script executed:

find . -name "Sonner.vue" -o -name "sonner" -type d | head -20

Repository: thebtf/engram

Length of output: 128


🏁 Script executed:

cat -n ui/src/components/ui/sonner/Sonner.vue

Repository: thebtf/engram

Length of output: 1869


🏁 Script executed:

# Check for type definitions related to ToasterProps
fd -i "toasterprop\|sonner.*type\|sonner.*d\.ts" --type f

Repository: thebtf/engram

Length of output: 39


Сохраняйте пользовательский toastOptions при проксировании пропсов.

На строке 8 toastOptions исключается из делегируемых пропсов через reactiveOmit, а на строках 14-23 передаются только локальные классы. Это означает, что вызывающий код не сможет установить свои toastOptions (например, duration, unstyled, пользовательские classes), что ограничивает публичный API компонента. Объедините входящие props.toastOptions с дефолтными классами, используя spread-оператор.

Предложенный вариант:

   <Sonner
     class="toaster group"
     :toast-options="{
+      ...props.toastOptions,
       classes: {
+        ...props.toastOptions?.classes,
         toast: 'group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg',
         description: 'group-[.toast]:text-muted-foreground',
         actionButton:
           'group-[.toast]:bg-primary group-[.toast]:text-primary-foreground',
         cancelButton:
           'group-[.toast]:bg-muted group-[.toast]:text-muted-foreground',
       },
     }"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const delegatedProps = reactiveOmit(props, "toastOptions")
</script>
<template>
<Sonner
class="toaster group"
:toast-options="{
classes: {
toast: 'group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg',
description: 'group-[.toast]:text-muted-foreground',
actionButton:
'group-[.toast]:bg-primary group-[.toast]:text-primary-foreground',
cancelButton:
'group-[.toast]:bg-muted group-[.toast]:text-muted-foreground',
},
}"
v-bind="delegatedProps"
const delegatedProps = reactiveOmit(props, "toastOptions")
</script>
<template>
<Sonner
class="toaster group"
:toast-options="{
...props.toastOptions,
classes: {
...props.toastOptions?.classes,
toast: 'group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg',
description: 'group-[.toast]:text-muted-foreground',
actionButton:
'group-[.toast]:bg-primary group-[.toast]:text-primary-foreground',
cancelButton:
'group-[.toast]:bg-muted group-[.toast]:text-muted-foreground',
},
}"
v-bind="delegatedProps"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/components/ui/sonner/Sonner.vue` around lines 8 - 24, The component
currently omits props.toastOptions via reactiveOmit (delegatedProps) so callers
cannot provide custom toastOptions; instead stop excluding toastOptions and
merge the incoming props.toastOptions with the local class defaults before
passing to the Sonner component: read props.toastOptions (or
delegatedProps.toastOptions if you keep reactiveOmit) into a mergedToastOptions
object using the spread operator to combine { classes: { ...localClasses } }
with props.toastOptions (ensuring nested classes merge, not overwrite), then
pass :toast-options="mergedToastOptions" to the Sonner element and keep
v-bind="delegatedProps" for the rest of the props.

Comment on lines +6 to +24
const props = defineProps<{
class?: HTMLAttributes["class"]
defaultValue?: string | number
modelValue?: string | number
}>()

const emits = defineEmits<{
(e: "update:modelValue", payload: string | number): void
}>()

const modelValue = useVModel(props, "modelValue", emits, {
passive: true,
defaultValue: props.defaultValue,
})
</script>

<template>
<textarea v-model="modelValue" :class="cn('flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50', props.class)" />
</template>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In Vue 3, what is the expected value type for <textarea v-model>, and should custom textarea components expose number in modelValue?

💡 Result:

In Vue 3, the expected value type for <textarea v-model> is string. Native textarea elements bind to the value property using the input event, which always provides a string value from event.target.value. Official Vue docs show examples with string-bound variables like message, and TypeScript DOM types reinforce textarea value as string (unlike input, which supports number/boolean for certain types). Custom textarea components should expose modelValue as string, not number. To support v-model, accept modelValue prop (typed string) and emit update:modelValue with event.target.value (string). Using defineModel (Vue 3.4+), type it as defineModel and bind to native textarea v-model. Examples from docs, PrimeVue/Vuetify, and Stack Overflow confirm string typing. Number would mismatch native behavior and cause issues like lost formatting or TypeScript errors.

Citations:


Для Textarea используйте только строковый тип modelValue.

Нативный элемент <textarea> всегда возвращает строку через event.target.value. Указание типа number в modelValue, defaultValue и emit противоречит стандартному поведению HTML и создает ложные ожидания у потребителей компонента.

Предлагаемое исправление
 const props = defineProps<{
   class?: HTMLAttributes["class"]
-  defaultValue?: string | number
-  modelValue?: string | number
+  defaultValue?: string
+  modelValue?: string
 }>()
 
 const emits = defineEmits<{
-  (e: "update:modelValue", payload: string | number): void
+  (e: "update:modelValue", payload: string): void
 }>()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const props = defineProps<{
class?: HTMLAttributes["class"]
defaultValue?: string | number
modelValue?: string | number
}>()
const emits = defineEmits<{
(e: "update:modelValue", payload: string | number): void
}>()
const modelValue = useVModel(props, "modelValue", emits, {
passive: true,
defaultValue: props.defaultValue,
})
</script>
<template>
<textarea v-model="modelValue" :class="cn('flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50', props.class)" />
</template>
const props = defineProps<{
class?: HTMLAttributes["class"]
defaultValue?: string
modelValue?: string
}>()
const emits = defineEmits<{
(e: "update:modelValue", payload: string): void
}>()
const modelValue = useVModel(props, "modelValue", emits, {
passive: true,
defaultValue: props.defaultValue,
})
</script>
<template>
<textarea v-model="modelValue" :class="cn('flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50', props.class)" />
</template>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/components/ui/textarea/Textarea.vue` around lines 6 - 24, Компонент
Textarea использует числовые типы для modelValue/defaultValue/эмита, что
противоречит поведению нативного <textarea>; замените все упоминания number на
string. В defineProps уберите number и оставьте defaultValue?: string и
modelValue?: string; в defineEmits обновите сигнатуру события
(update:modelValue) чтобы payload был string; при вызове useVModel (переменная
modelValue) передавайте defaultValue как string (удалите число из union).
Проверьте, что в шаблоне связывание v-model="modelValue" и любые места
использования modelValue ожидают строку.

Comment on lines +20 to +23
provide("toggleGroup", {
variant: props.variant,
size: props.size,
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Реактивность контекста не обеспечена.

Объект, переданный в provide, создаётся один раз при монтировании компонента с начальными значениями props.variant и props.size. Если родитель изменит эти props динамически, дочерние ToggleGroupItem не получат обновления.

🐛 Предлагаемое исправление с использованием computed
+import { computed, provide } from "vue"
-import { provide } from "vue"

-provide("toggleGroup", {
-  variant: props.variant,
-  size: props.size,
-})
+provide("toggleGroup", computed(() => ({
+  variant: props.variant,
+  size: props.size,
+})))

Также потребуется обновить ToggleGroupItem.vue для работы с ComputedRef:

const context = inject<ComputedRef<ToggleGroupVariants>>("toggleGroup")
// и в template:
// context?.value?.variant || variant
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/components/ui/toggle-group/ToggleGroup.vue` around lines 20 - 23, The
provided context object in ToggleGroup.vue is non-reactive because it captures
props at mount; change the provide("toggleGroup", ...) to supply a computed/ref
wrapper (e.g., a computed object exposing variant and size) so updates to
props.variant/props.size propagate, and update ToggleGroupItem.vue to inject the
provided value as a ComputedRef (inject("toggleGroup") typed as
ComputedRef<ToggleGroupVariants>) and read values via the .value in the
template/logic (e.g., use context.value.variant / context.value.size with
fallbacks).

T020-T027: HomeView rebuilt with 4 sections:
- Server status header (version, uptime, overall health)
- Metric cards grid (sessions, connected, requests, injections)
- System health component grid (from /api/selfcheck)
- Recent issues table (top 5, click to navigate)
- Delete StatsCards.vue (replaced)

T028-T032: Auth views rebuilt with shadcn:
- LoginView: Card + Tabs (email/token) + Input + Button
- RegisterView: Card + Input + Label + Button
- SetupView: Card + Input + Button
- FontAwesome → Lucide (Loader2, AlertCircle, Mail, Key)
- All CSS variable colors, no hardcoded hex
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
ui/src/components/layout/AppSidebar.vue (1)

24-24: Разделите чтение темы и её инициализацию.

Этот вызов добавляет ещё один watch(mode, ...) из useColorMode(). С учётом инициализации темы на уровне App.vue это даёт дублированные localStorage.setItem() и applyMode() на каждое переключение. Лучше держать побочные эффекты в одном месте, а здесь использовать только состояние и action.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/components/layout/AppSidebar.vue` at line 24, В компоненте AppSidebar
не должно вызываться инициализирующее поведение useColorMode (текущее const {
mode, toggleColorMode } = useColorMode()), потому что это создаёт дополнительный
watch и дублирует applyMode/localStorage.setItem уже выполненные в App.vue;
исправьте это, чтобы здесь только читать состояние и вызывать действие без
побочных эффектов — например, убрать вызов toggleColorMode из useColorMode или
заменить его на простой доступ к рефу mode и эмит события/вызов локальной action
(типа emit('toggle-theme') или вызвать чистую функцию без applyMode), и
перенести всю логику записи в localStorage и вызов applyMode в App.vue; ссылки:
useColorMode, mode, toggleColorMode, applyMode, localStorage.setItem, App.vue.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ui/src/components/layout/AppSidebar.vue`:
- Around line 110-113: The Admin menu button uses a strict equality check
(route.path === '/admin') which fails for nested routes; change it to use the
shared helper isActiveItem('/admin') (same approach as other main items) so the
SidebarMenuButton's :is-active prop reflects any /admin/* child routes; update
the SidebarMenuButton invocation to call isActiveItem with the '/admin' base
path.

---

Nitpick comments:
In `@ui/src/components/layout/AppSidebar.vue`:
- Line 24: В компоненте AppSidebar не должно вызываться инициализирующее
поведение useColorMode (текущее const { mode, toggleColorMode } =
useColorMode()), потому что это создаёт дополнительный watch и дублирует
applyMode/localStorage.setItem уже выполненные в App.vue; исправьте это, чтобы
здесь только читать состояние и вызывать действие без побочных эффектов —
например, убрать вызов toggleColorMode из useColorMode или заменить его на
простой доступ к рефу mode и эмит события/вызов локальной action (типа
emit('toggle-theme') или вызвать чистую функцию без applyMode), и перенести всю
логику записи в localStorage и вызов applyMode в App.vue; ссылки: useColorMode,
mode, toggleColorMode, applyMode, localStorage.setItem, App.vue.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e3f68bc3-b2d7-4274-845f-b534fafff5c6

📥 Commits

Reviewing files that changed from the base of the PR and between 700356f and c871a63.

📒 Files selected for processing (2)
  • ui/src/App.vue
  • ui/src/components/layout/AppSidebar.vue
🚧 Files skipped from review as they are similar to previous changes (1)
  • ui/src/App.vue

Comment on lines +110 to +113
<SidebarMenuButton
as-child
:is-active="route.path === '/admin'"
tooltip="Admin"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Подсветка Admin теряется на вложенных маршрутах.

Для основных пунктов вы уже учитываете дочерние пути через isActiveItem(), а здесь осталось строгое сравнение. На /admin/... кнопка перестанет быть активной.

♻️ Предлагаемое изменение
-              :is-active="route.path === '/admin'"
+              :is-active="route.path === '/admin' || route.path.startsWith('/admin/')"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<SidebarMenuButton
as-child
:is-active="route.path === '/admin'"
tooltip="Admin"
<SidebarMenuButton
as-child
:is-active="route.path === '/admin' || route.path.startsWith('/admin/')"
tooltip="Admin"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/components/layout/AppSidebar.vue` around lines 110 - 113, The Admin
menu button uses a strict equality check (route.path === '/admin') which fails
for nested routes; change it to use the shared helper isActiveItem('/admin')
(same approach as other main items) so the SidebarMenuButton's :is-active prop
reflects any /admin/* child routes; update the SidebarMenuButton invocation to
call isActiveItem with the '/admin' base path.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (5)
ui/src/views/HomeView.vue (3)

32-42: Рассмотрите возможность отображения состояния ошибки пользователю.

Блок catch тихо устанавливает пустой массив при ошибке загрузки. Пользователь не узнает, произошла ли ошибка или просто нет открытых issues.

♻️ Предлагаемое улучшение
 const recentIssues = ref<Issue[]>([])
 const issuesLoading = ref(false)
+const issuesError = ref<string | null>(null)
 
 onMounted(async () => {
   issuesLoading.value = true
+  issuesError.value = null
   try {
     const result = await fetchIssues(undefined, 'open,acknowledged', 5, 0)
     recentIssues.value = result.issues ?? []
   } catch {
     recentIssues.value = []
+    issuesError.value = 'Failed to load issues'
   } finally {
     issuesLoading.value = false
   }
 })

Затем в шаблоне можно отобразить ошибку:

<div v-else-if="issuesError" class="text-sm text-destructive py-4 text-center">
  {{ issuesError }}
</div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/views/HomeView.vue` around lines 32 - 42, The catch block silently
hides failures; introduce an issuesError reactive ref (e.g., const issuesError =
ref<string | null>(null)) alongside issuesLoading and recentIssues, set
issuesError to a useful message inside the catch of the onMounted async function
(capture the caught error or its message) and clear issuesError on success
before assigning recentIssues; keep recentIssues.value = [] in the error case
but also set issuesLoading.value = false in finally; update the template to
render issuesError (as suggested) where relevant so users see the error.

197-202: Рассмотрите использование явного loading ref из useHealth.

Согласно context snippet 1, useHealth предоставляет отдельный ref loading. Текущая проверка !health не различает начальную загрузку и ошибку загрузки.

♻️ Предлагаемое улучшение
-const { health } = useHealth()
+const { health, loading: healthLoading } = useHealth()

И в шаблоне:

-<div v-else-if="!health" class="text-sm text-muted-foreground">
+<div v-else-if="healthLoading" class="text-sm text-muted-foreground">
   Loading health data...
 </div>
+<div v-else-if="!health" class="text-sm text-destructive">
+  Failed to load health data.
+</div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/views/HomeView.vue` around lines 197 - 202, Шаблон сейчас использует
только !health and показывает "Loading..." vs "No health components", не отличая
реальное состояние загрузки от отсутствия данных; измените логику так, чтобы
использовать явный ref loading из useHealth: проверяйте loading первым
(показывать "Loading health data..." когда loading.value true), затем проверяйте
наличие health (health.value) и только после этого показывайте "No health
components reported."; обновите соответствующие v-else-if / v-else ветки в
шаблоне и убедитесь, что вы импортируете/используете useHealth, а не полагаетесь
только на health.

65-67: Функция typeVariant игнорирует параметр.

Параметр _type помечен как неиспользуемый (underscore prefix), и функция всегда возвращает 'outline'. Если это placeholder для будущей реализации, добавьте TODO-комментарий.

♻️ Предлагаемое улучшение
+// TODO: Implement type-specific variants when design is finalized
 function typeVariant(_type: Issue['type']): 'default' | 'secondary' | 'destructive' | 'outline' {
   return 'outline'
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/views/HomeView.vue` around lines 65 - 67, Функция typeVariant(_type:
Issue['type']) игнорирует входной параметр и всегда возвращает 'outline'; либо
реализуй реальную карту типов->вариантов (например внутри typeVariant по
значению type вернуть 'default'|'secondary'|'destructive'|'outline'), либо
пометь это как placeholder, убрав префикс подчёркивания у параметра и добавив
явный TODO-комментарий внутри функции, чтобы будущая реализация была видна;
ссылайся на функцию typeVariant и тип Issue['type'] при внесении изменений.
ui/src/views/LoginView.vue (1)

127-172: Стоит вынести общий auth-feedback/submit-блок в переиспользуемый компонент.

Сейчас одинаковый паттерн (error row + submit button со spinner) повторяется в ui/src/views/LoginView.vue, ui/src/views/RegisterView.vue и ui/src/views/SetupView.vue, что усложнит дальнейшие правки стилей/доступности в 3 местах.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/views/LoginView.vue` around lines 127 - 172, The error + submit button
block is duplicated in LoginView.vue, RegisterView.vue and SetupView.vue (see
the error p + Button with Loader2 and spinner logic, e.g., the token form that
calls handleTokenLogin), so extract that UI into a reusable component (e.g.,
AuthSubmit or AuthFeedbackSubmit) that accepts props error:string,
submitting:boolean, label?:string (default "Login"), and disabled:boolean and
emits the native submit/click (or renders a <button type="submit"> so parent
forms keep `@submit.prevent` like handleTokenLogin); replace the duplicated blocks
in LoginView.vue, RegisterView.vue and SetupView.vue with the new component and
pass error/submitting/label accordingly, and ensure the component preserves the
existing structure/ARIA and slot support for any extra nodes (icons) so
visual/behavior parity remains.
ui/src/views/SetupView.vue (1)

102-110: Добавьте a11y-атрибуты для динамической ошибки и loading-состояния.

На Line [102] сообщение об ошибке появляется динамически без live-region, из-за чего скринридер может не озвучить его вовремя. На Line [107] полезно явно объявить busy-состояние кнопки.

Предложение правки
-            <p v-if="error" class="flex items-center gap-2 text-sm text-destructive">
+            <p
+              v-if="error"
+              role="alert"
+              aria-live="polite"
+              class="flex items-center gap-2 text-sm text-destructive"
+            >
               <AlertCircle class="w-4 h-4 shrink-0" />
               {{ error }}
             </p>

-            <Button type="submit" class="w-full" :disabled="submitting">
+            <Button type="submit" class="w-full" :disabled="submitting" :aria-busy="submitting">
               <Loader2 v-if="submitting" class="w-4 h-4 mr-2 animate-spin" />
               {{ submitting ? 'Creating account...' : 'Create Admin Account' }}
             </Button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/views/SetupView.vue` around lines 102 - 110, The dynamic error message
rendered by the <p v-if="error"> (uses the error variable and AlertCircle) needs
an accessible live region and the submit Button (uses submitting, Loader2) must
expose its busy state; update the error paragraph to include
aria-live="assertive" and role="alert" (and aria-atomic if desired) so screen
readers announce changes, and update the Button to bind its busy state with
:aria-busy="submitting" and ensure it reflects being disabled via aria-disabled
when submitting (keep the existing disabled prop) so assistive tech knows the
control is busy.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ui/src/views/HomeView.vue`:
- Around line 237-241: Строки таблицы (TableRow v-for="issue in recentIssues")
кликабельны but not keyboard-accessible; update the TableRow element to add
keyboard affordances: set tabindex="0", role="button", and add a keydown handler
that calls navigateToIssue(issue.id) on Enter (e.g.,
`@keydown.enter`="navigateToIssue(issue.id)") so keyboard users can activate rows
the same way as `@click` does.

---

Nitpick comments:
In `@ui/src/views/HomeView.vue`:
- Around line 32-42: The catch block silently hides failures; introduce an
issuesError reactive ref (e.g., const issuesError = ref<string | null>(null))
alongside issuesLoading and recentIssues, set issuesError to a useful message
inside the catch of the onMounted async function (capture the caught error or
its message) and clear issuesError on success before assigning recentIssues;
keep recentIssues.value = [] in the error case but also set issuesLoading.value
= false in finally; update the template to render issuesError (as suggested)
where relevant so users see the error.
- Around line 197-202: Шаблон сейчас использует только !health and показывает
"Loading..." vs "No health components", не отличая реальное состояние загрузки
от отсутствия данных; измените логику так, чтобы использовать явный ref loading
из useHealth: проверяйте loading первым (показывать "Loading health data..."
когда loading.value true), затем проверяйте наличие health (health.value) и
только после этого показывайте "No health components reported."; обновите
соответствующие v-else-if / v-else ветки в шаблоне и убедитесь, что вы
импортируете/используете useHealth, а не полагаетесь только на health.
- Around line 65-67: Функция typeVariant(_type: Issue['type']) игнорирует
входной параметр и всегда возвращает 'outline'; либо реализуй реальную карту
типов->вариантов (например внутри typeVariant по значению type вернуть
'default'|'secondary'|'destructive'|'outline'), либо пометь это как placeholder,
убрав префикс подчёркивания у параметра и добавив явный TODO-комментарий внутри
функции, чтобы будущая реализация была видна; ссылайся на функцию typeVariant и
тип Issue['type'] при внесении изменений.

In `@ui/src/views/LoginView.vue`:
- Around line 127-172: The error + submit button block is duplicated in
LoginView.vue, RegisterView.vue and SetupView.vue (see the error p + Button with
Loader2 and spinner logic, e.g., the token form that calls handleTokenLogin), so
extract that UI into a reusable component (e.g., AuthSubmit or
AuthFeedbackSubmit) that accepts props error:string, submitting:boolean,
label?:string (default "Login"), and disabled:boolean and emits the native
submit/click (or renders a <button type="submit"> so parent forms keep
`@submit.prevent` like handleTokenLogin); replace the duplicated blocks in
LoginView.vue, RegisterView.vue and SetupView.vue with the new component and
pass error/submitting/label accordingly, and ensure the component preserves the
existing structure/ARIA and slot support for any extra nodes (icons) so
visual/behavior parity remains.

In `@ui/src/views/SetupView.vue`:
- Around line 102-110: The dynamic error message rendered by the <p
v-if="error"> (uses the error variable and AlertCircle) needs an accessible live
region and the submit Button (uses submitting, Loader2) must expose its busy
state; update the error paragraph to include aria-live="assertive" and
role="alert" (and aria-atomic if desired) so screen readers announce changes,
and update the Button to bind its busy state with :aria-busy="submitting" and
ensure it reflects being disabled via aria-disabled when submitting (keep the
existing disabled prop) so assistive tech knows the control is busy.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f9ff927b-9c99-4d03-9df9-4dc2bf1fbe4a

📥 Commits

Reviewing files that changed from the base of the PR and between c871a63 and 567fbd7.

📒 Files selected for processing (6)
  • ui/src/components/StatsCards.vue
  • ui/src/components/index.ts
  • ui/src/views/HomeView.vue
  • ui/src/views/LoginView.vue
  • ui/src/views/RegisterView.vue
  • ui/src/views/SetupView.vue
💤 Files with no reviewable changes (2)
  • ui/src/components/index.ts
  • ui/src/components/StatsCards.vue

Comment thread ui/src/views/HomeView.vue
Comment on lines +237 to +241
<TableRow
v-for="issue in recentIssues"
:key="issue.id"
class="cursor-pointer hover:bg-muted/50"
@click="navigateToIssue(issue.id)"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Добавьте поддержку клавиатурной навигации для кликабельных строк таблицы.

Строки таблицы кликабельны (@click, cursor-pointer), но не доступны для навигации с клавиатуры. Для доступности (a11y) следует добавить tabindex, role и обработчик клавиши Enter.

♿ Предлагаемое исправление
 <TableRow
   v-for="issue in recentIssues"
   :key="issue.id"
   class="cursor-pointer hover:bg-muted/50"
+  tabindex="0"
+  role="link"
   `@click`="navigateToIssue(issue.id)"
+  `@keyup.enter`="navigateToIssue(issue.id)"
 >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<TableRow
v-for="issue in recentIssues"
:key="issue.id"
class="cursor-pointer hover:bg-muted/50"
@click="navigateToIssue(issue.id)"
<TableRow
v-for="issue in recentIssues"
:key="issue.id"
class="cursor-pointer hover:bg-muted/50"
tabindex="0"
role="link"
`@click`="navigateToIssue(issue.id)"
`@keyup.enter`="navigateToIssue(issue.id)"
>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/views/HomeView.vue` around lines 237 - 241, Строки таблицы (TableRow
v-for="issue in recentIssues") кликабельны but not keyboard-accessible; update
the TableRow element to add keyboard affordances: set tabindex="0",
role="button", and add a keydown handler that calls navigateToIssue(issue.id) on
Enter (e.g., `@keydown.enter`="navigateToIssue(issue.id)") so keyboard users can
activate rows the same way as `@click` does.

T034-T038: Issues views rebuilt with shadcn:
- IssuesView: Table, filter buttons, Dialog (create issue), Select (type/priority)
- IssueDetailView: Card, Select (status), Dialog (delete/reject), Textarea (comments)
- AppHeader: remove dead search route reference, clean unused imports

T040-T044: Management views rebuilt with shadcn:
- VaultView: Card (status), Table (credentials), AlertDialog (delete), Skeleton (loading)
- TokensView: Table, Dialog (create), RadioGroup (scope), AlertDialog (revoke)
- AdminView: Table, Select (role), Switch (enable/disable)
- All FA icons → Lucide across all views
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
ui/src/views/IssueDetailView.vue (2)

187-201: ⚠️ Potential issue | 🟡 Minor

Очищайте error перед повторной загрузкой issue.

После любой неуспешной операции error остаётся заполненным, а успешный loadIssue() его не сбрасывает. Из-за этого нефатальный баннер ошибки может висеть бесконечно, даже когда данные уже успешно обновились.

💡 Небольшое исправление
 async function loadIssue() {
   const id = Number(route.params.id)
   if (!id) { error.value = 'Invalid issue ID'; loading.value = false; return }
 
+  error.value = null
   try {
     const result = await fetchIssue(id)
     issue.value = result.issue
     comments.value = result.comments || []
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/views/IssueDetailView.vue` around lines 187 - 201, loadIssue leaves
error.value set after a prior failure so successful reloads still show the error
banner; clear error at the start of loadIssue (e.g., set error.value = null or
'' before making the fetch) and ensure any successful path (inside the try after
fetchIssue resolves) does not re-set an error; update the loadIssue function
(referencing loadIssue, error.value, fetchIssue, and loading.value) to reset
error before the network call so a subsequent successful load removes the
previous error state.

129-160: ⚠️ Potential issue | 🟡 Minor

Событие rejected никогда не попадёт в таймлайн.

buildTimeline() создаёт только created / acknowledged / comment / resolved / reopened / closed, хотя остальной UI уже поддерживает rejected (TimelineEvent, dotColor(), timelineBadgeClass(), confirmReject()). После отклонения issue в истории не появится отдельный шаг reject.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/views/IssueDetailView.vue` around lines 129 - 160, buildTimeline()
never adds a 'rejected' event so rejected actions don't appear in the timeline;
update buildTimeline (the function building TimelineEvent objects) to detect
iss.rejected_at and push a { type: 'rejected', date: iss.rejected_at, project:
iss.target_project || '', agent: '' , body: iss.body || undefined } (or similar
fields matching other events) before sorting so the existing timeline rendering
(TimelineEvent, dotColor(), timelineBadgeClass(), confirmReject()) will show the
reject step; keep the sort by date as-is.
ui/src/views/TokensView.vue (1)

104-110: ⚠️ Potential issue | 🟡 Minor

Сбрасывайте copyFeedback вместе с состоянием модалки.

Если пользователь скопировал предыдущий токен и быстро открыл модалку снова, новое одноразовое значение может отрисоваться уже в состоянии Copied, хотя его ещё не копировали.

💡 Небольшое исправление
 function openCreateModal() {
   newTokenName.value = ''
   newTokenScope.value = 'read-write'
   createError.value = null
   createdToken.value = null
+  copyFeedback.value = false
   showCreateModal.value = true
 }
 
 function closeCreateModal() {
   showCreateModal.value = false
   createdToken.value = null
+  copyFeedback.value = false
 }

Also applies to: 130-133

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/views/TokensView.vue` around lines 104 - 110, Reset the copyFeedback
state when opening the modal so stale "Copied" feedback isn't shown: in
openCreateModal add copyFeedback.value = null alongside newTokenName,
newTokenScope, createError, and createdToken resets, and apply the same change
to the other modal open/reset handler (the other modal open function near the
modal initialization, e.g., the openEditModal/open... handler that resets modal
state) so both modals clear copyFeedback when opened.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ui/src/components/layout/AppHeader.vue`:
- Around line 13-15: handleSearch всегда затирает searchQuery.value при submit,
из‑за чего пользователь теряет введённый текст если маршрута поиска нет;
исправьте handleSearch так, чтобы поле не очищалось при отсутствии маршрута:
проверяйте наличие маршрута (например через router.hasRoute('search') или
попытку навигации и успешный результат) и только в случае успешной
навигации/выполненного поиска сбрасывайте searchQuery.value; ссылаться на
функцию handleSearch и переменную searchQuery, а также на то, что SearchBar
эмитит submit для непустого запроса.

In `@ui/src/views/AdminView.vue`:
- Around line 220-240: The Select and Switch allow multiple concurrent updates
and the Switch inverts user.disabled locally, causing race conditions; add a
per-user "updating" flag (e.g., on the user object or a local Map keyed by
user.id) and set it true before calling changeRole or toggleDisabled and false
after the awaited API + loadUsers() completes, bind Select and Switch disabled
states to that flag to block UI interactions while updating, and stop deriving
the new disabled value by inverting local user.disabled in toggleDisabled—use
the explicit intent passed from the Switch or the server response to determine
final state (ensure changeRole and toggleDisabled both await the PUT and call
loadUsers() after success).

In `@ui/src/views/IssuesView.vue`:
- Around line 266-272: Строка таблицы (TableRow) навешивает только `@click` и
поэтому недоступна с клавиатуры; сделайте её фокусируемой и обрабатывайте
клавиши. Добавьте tabindex="0" и role="button" на TableRow, подключите
обработчики `@keydown.enter` и `@keydown.space`, которые вызывают тот же
router.push(`/issues/${issue.id}`) что и `@click`, и при статусах (issue.status
=== 'closed' || issue.status === 'rejected') выставьте aria-disabled="true" и не
выполняйте навигацию в обработчиках клавиатуры/клика. Это сохранит текущую
визуальную логику и добавит клавиатурную доступность.

---

Outside diff comments:
In `@ui/src/views/IssueDetailView.vue`:
- Around line 187-201: loadIssue leaves error.value set after a prior failure so
successful reloads still show the error banner; clear error at the start of
loadIssue (e.g., set error.value = null or '' before making the fetch) and
ensure any successful path (inside the try after fetchIssue resolves) does not
re-set an error; update the loadIssue function (referencing loadIssue,
error.value, fetchIssue, and loading.value) to reset error before the network
call so a subsequent successful load removes the previous error state.
- Around line 129-160: buildTimeline() never adds a 'rejected' event so rejected
actions don't appear in the timeline; update buildTimeline (the function
building TimelineEvent objects) to detect iss.rejected_at and push a { type:
'rejected', date: iss.rejected_at, project: iss.target_project || '', agent: ''
, body: iss.body || undefined } (or similar fields matching other events) before
sorting so the existing timeline rendering (TimelineEvent, dotColor(),
timelineBadgeClass(), confirmReject()) will show the reject step; keep the sort
by date as-is.

In `@ui/src/views/TokensView.vue`:
- Around line 104-110: Reset the copyFeedback state when opening the modal so
stale "Copied" feedback isn't shown: in openCreateModal add copyFeedback.value =
null alongside newTokenName, newTokenScope, createError, and createdToken
resets, and apply the same change to the other modal open/reset handler (the
other modal open function near the modal initialization, e.g., the
openEditModal/open... handler that resets modal state) so both modals clear
copyFeedback when opened.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0f47f416-3f47-4d4b-ba3a-c97ac472059f

📥 Commits

Reviewing files that changed from the base of the PR and between 567fbd7 and 3a8a0cc.

📒 Files selected for processing (6)
  • ui/src/components/layout/AppHeader.vue
  • ui/src/views/AdminView.vue
  • ui/src/views/IssueDetailView.vue
  • ui/src/views/IssuesView.vue
  • ui/src/views/TokensView.vue
  • ui/src/views/VaultView.vue

Comment on lines +13 to +15
function handleSearch(_q: string) {
// search route does not exist — no-op
searchQuery.value = ''
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Не очищайте поле поиска при submit, если поиск фактически не выполняется.

На Line 15 ввод стирается всегда, хотя SearchBar эмитит submit только для непустого запроса (ui/src/components/search/SearchBar.vue, Line 20-24). В текущем виде пользователь теряет текст без полезного результата (роут поиска отсутствует), что выглядит как поломка UX.

Предлагаемое минимальное исправление
 function handleSearch(_q: string) {
-  // search route does not exist — no-op
-  searchQuery.value = ''
+  // search route does not exist — no-op (keep current input)
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function handleSearch(_q: string) {
// search route does not exist — no-op
searchQuery.value = ''
function handleSearch(_q: string) {
// search route does not exist — no-op (keep current input)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/components/layout/AppHeader.vue` around lines 13 - 15, handleSearch
всегда затирает searchQuery.value при submit, из‑за чего пользователь теряет
введённый текст если маршрута поиска нет; исправьте handleSearch так, чтобы поле
не очищалось при отсутствии маршрута: проверяйте наличие маршрута (например
через router.hasRoute('search') или попытку навигации и успешный результат) и
только в случае успешной навигации/выполненного поиска сбрасывайте
searchQuery.value; ссылаться на функцию handleSearch и переменную searchQuery, а
также на то, что SearchBar эмитит submit для непустого запроса.

Comment on lines +220 to +240
<Select :model-value="user.role" @update:model-value="(role) => { if (role) changeRole(user, String(role)) }">
<SelectTrigger class="h-7 w-28 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="admin">admin</SelectItem>
<SelectItem value="operator">operator</SelectItem>
</SelectContent>
</Select>
</TableCell>
<TableCell class="text-sm text-muted-foreground">
{{ user.last_login_at ? formatRelativeTime(user.last_login_at) : '—' }}
</td>
<td class="px-4 py-3 text-slate-400">
</TableCell>
<TableCell class="text-sm text-muted-foreground">
{{ formatRelativeTime(user.created_at) }}
</td>
<td class="px-4 py-3 text-right">
<button
:class="[
'px-3 py-1 rounded-lg text-xs font-medium transition-colors',
user.disabled
? 'bg-green-500/20 text-green-400 hover:bg-green-500/30'
: 'bg-red-500/20 text-red-400 hover:bg-red-500/30',
]"
@click="toggleDisabled(user)"
>
{{ user.disabled ? 'Enable' : 'Disable' }}
</button>
</td>
</tr>
</tbody>
</table>
</div>
</TableCell>
<TableCell class="text-right">
<Switch
:checked="!user.disabled"
@update:checked="toggleDisabled(user)"
/>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Блокируйте строку пользователя на время обновления.

Сейчас Select и Switch остаются активными, а Switch ещё и вычисляет новое значение через инверсию локального user.disabled. Если админ успеет кликнуть несколько раз до loadUsers(), уйдут несколько PUT-запросов из устаревшего состояния, и итоговое состояние пользователя может не совпасть с последним действием.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/views/AdminView.vue` around lines 220 - 240, The Select and Switch
allow multiple concurrent updates and the Switch inverts user.disabled locally,
causing race conditions; add a per-user "updating" flag (e.g., on the user
object or a local Map keyed by user.id) and set it true before calling
changeRole or toggleDisabled and false after the awaited API + loadUsers()
completes, bind Select and Switch disabled states to that flag to block UI
interactions while updating, and stop deriving the new disabled value by
inverting local user.disabled in toggleDisabled—use the explicit intent passed
from the Switch or the server response to determine final state (ensure
changeRole and toggleDisabled both await the PUT and call loadUsers() after
success).

Comment on lines +266 to +272
<TableRow
v-for="issue in issues"
:key="issue.id"
class="cursor-pointer"
:class="(issue.status === 'closed' || issue.status === 'rejected') ? 'opacity-50' : ''"
@click="router.push(`/issues/${issue.id}`)"
>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Сделайте переход к issue доступным с клавиатуры.

Сейчас открытие детали завязано только на @click у <TableRow>. У строки нет ни focus target, ни семантики ссылки/кнопки, поэтому пользователь без мыши не сможет открыть issue из таблицы.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/views/IssuesView.vue` around lines 266 - 272, Строка таблицы
(TableRow) навешивает только `@click` и поэтому недоступна с клавиатуры; сделайте
её фокусируемой и обрабатывайте клавиши. Добавьте tabindex="0" и role="button"
на TableRow, подключите обработчики `@keydown.enter` и `@keydown.space`, которые
вызывают тот же router.push(`/issues/${issue.id}`) что и `@click`, и при статусах
(issue.status === 'closed' || issue.status === 'rejected') выставьте
aria-disabled="true" и не выполняйте навигацию в обработчиках клавиатуры/клика.
Это сохранит текущую визуальную логику и добавит клавиатурную доступность.

… fix watch leak

- T046: git rm Badge, Card, IconBox, Header, TimeRangeSelector, ConfirmDialog, Pagination
- T048: empty components/index.ts barrel (all exports removed)
- T049: remove @fortawesome/fontawesome-free from devDependencies and main.ts import
- T051: fix duplicate darkMode ['class','class'] -> 'class'; remove claude/data color
  blocks; add thin claude alias (400/500/700 -> primary) to preserve AppSidebar gradient
- T052: remove .glass utility; fix scrollbar thumb to hsl(var(--muted-foreground)/0.3)
- T053: add catch-all 404 route to router/index.ts
- T054: git rm .mcp.json (shadcn IDE artifact); add to .gitignore
- T055: fix useColorMode watch leak via module-level watchStarted flag
- T056: replace all fas/fab/far icons with lucide-vue-next in App.vue, AppHeader.vue,
  SearchBar.vue; replace claude-500 ring/border refs in SearchBar with primary token
@thebtf thebtf changed the title feat(ui): shadcn-vue foundation + DESIGN.md + light/dark theme (PR 1/7) feat(ui): full dashboard redesign with shadcn-vue + light/dark theme Apr 25, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (2)
ui/src/assets/main.css (1)

88-93: ⚠️ Potential issue | 🟡 Minor

Приведите имя @keyframes к kebab-case.

На Line 89 и Line 91 используется pulseDot, что нарушает keyframes-name-pattern (Stylelint).

Исправление
 .pulse-dot {
-  animation: pulseDot 2s infinite;
+  animation: pulse-dot 2s infinite;
 }
-@keyframes pulseDot {
+@keyframes pulse-dot {
   0%, 100% { opacity: 1; }
   50% { opacity: 0.5; }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/assets/main.css` around lines 88 - 93, Имя `@keyframes` использует
camelCase (`pulseDot`) и нарушает правило keyframes-name-pattern; переименуйте
ключевые кадры в kebab-case (например `pulse-dot`) и обновите все ссылки на них
(в частности свойство animation в селекторе .pulse-dot и сам `@keyframes`) чтобы
имена совпадали; также проверьте наличие других упоминаний `pulseDot` в проекте
и приведите их к новому kebab-case имени.
ui/src/components/layout/AppHeader.vue (1)

14-16: ⚠️ Potential issue | 🟡 Minor

Не очищайте ввод при no-op поиске.

На Line 16 searchQuery.value всегда сбрасывается, хотя поиск фактически не выполняется — пользователь теряет введённый текст без результата.

Минимальное исправление
 function handleSearch(_q: string) {
-  // search route does not exist — no-op
-  searchQuery.value = ''
+  // search route does not exist — no-op (сохраняем введённый текст)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/components/layout/AppHeader.vue` around lines 14 - 16, В handleSearch
не сбрасывайте введённый текст без реальной навигации: уберите или перенесите
присвоение searchQuery.value = '' из блока no-op и выполняйте очистку только
когда поиск реально выполняется (например после успешной навигации или при явном
результате), используя функцию handleSearch и переменную searchQuery.value; при
необходимости перед проверкой существования маршрута используйте роутер/проверку
наличия маршрута и очищайте поле лишь в ветке, где происходит переход/успешный
результат.
🧹 Nitpick comments (1)
ui/src/components/search/SearchBar.vue (1)

63-74: Доведите стили до полностью семантических токенов темы.

На Line 63 и Line 74 ещё остались жёсткие slate-* классы; лучше использовать muted-foreground/input-токены для консистентной light/dark палитры.

Вариант правки
-      <Search class="absolute left-3 top-1/2 -translate-y-1/2 text-slate-500" :size="14" />
+      <Search class="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" :size="14" />
@@
-          compact ? 'py-2 border-slate-700/50' : 'py-3 border-slate-600/50',
+          compact ? 'py-2 border-input' : 'py-3 border-input',
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/components/search/SearchBar.vue` around lines 63 - 74, The input
element still uses hardcoded slate color classes; update the class bindings in
the SearchBar component (the <Search /> icon and the input element using refs
like inputRef and props/localValue/compact) to replace 'text-slate-500',
'border-slate-700/50', and 'border-slate-600/50' with semantic theme tokens
(e.g., 'text-muted-foreground' for the icon and 'border-input' or 'border-muted'
/ token variants such as 'border-input/50' and 'py-2/py-3' kept by compact) so
the styles follow the theme tokens (muted-foreground/input) for both light/dark
modes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ui/package.json`:
- Around line 13-27: The FontAwesome dependency was removed from ui/package.json
but three EmptyState usages still reference FontAwesome icons (icon="fa-vault"
in VaultView.vue, icon="fa-key" in TokensView.vue, and
icon="fa-circle-exclamation" in IssuesView.vue), so either restore FontAwesome
to package.json until the icon migration is complete or replace those icon props
with existing lucide-vue-next icons (e.g., use appropriate lucide names and
imports in the EmptyState usages) and update the imports/registration in the
corresponding components to match; ensure package.json includes the FontAwesome
package if you opt to restore it and run install, or update the three files to
remove fa-* references and import lucide components instead.

In `@ui/src/components/index.ts`:
- Around line 2-3: The top-of-file comment in the components index currently
references "PR 7", which is misleading; update that comment to a neutral
description stating that the old components (Badge, Card, IconBox, Header,
TimeRangeSelector) were removed and that developers should use the shadcn/ui
components from "@/components/ui/" instead — remove the "PR 7" mention and
replace it with a neutral phrase like "removed in a prior refactor" or simply
omit any PR reference so the comment reads as a current state note.

---

Duplicate comments:
In `@ui/src/assets/main.css`:
- Around line 88-93: Имя `@keyframes` использует camelCase (`pulseDot`) и нарушает
правило keyframes-name-pattern; переименуйте ключевые кадры в kebab-case
(например `pulse-dot`) и обновите все ссылки на них (в частности свойство
animation в селекторе .pulse-dot и сам `@keyframes`) чтобы имена совпадали; также
проверьте наличие других упоминаний `pulseDot` в проекте и приведите их к новому
kebab-case имени.

In `@ui/src/components/layout/AppHeader.vue`:
- Around line 14-16: В handleSearch не сбрасывайте введённый текст без реальной
навигации: уберите или перенесите присвоение searchQuery.value = '' из блока
no-op и выполняйте очистку только когда поиск реально выполняется (например
после успешной навигации или при явном результате), используя функцию
handleSearch и переменную searchQuery.value; при необходимости перед проверкой
существования маршрута используйте роутер/проверку наличия маршрута и очищайте
поле лишь в ветке, где происходит переход/успешный результат.

---

Nitpick comments:
In `@ui/src/components/search/SearchBar.vue`:
- Around line 63-74: The input element still uses hardcoded slate color classes;
update the class bindings in the SearchBar component (the <Search /> icon and
the input element using refs like inputRef and props/localValue/compact) to
replace 'text-slate-500', 'border-slate-700/50', and 'border-slate-600/50' with
semantic theme tokens (e.g., 'text-muted-foreground' for the icon and
'border-input' or 'border-muted' / token variants such as 'border-input/50' and
'py-2/py-3' kept by compact) so the styles follow the theme tokens
(muted-foreground/input) for both light/dark modes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 008e0df2-0e99-4980-8658-49d3de27f4ad

📥 Commits

Reviewing files that changed from the base of the PR and between 3a8a0cc and b2a0910.

📒 Files selected for processing (18)
  • .gitignore
  • ui/package.json
  • ui/src/App.vue
  • ui/src/assets/main.css
  • ui/src/components/Badge.vue
  • ui/src/components/Card.vue
  • ui/src/components/Header.vue
  • ui/src/components/IconBox.vue
  • ui/src/components/TimeRangeSelector.vue
  • ui/src/components/index.ts
  • ui/src/components/layout/AppHeader.vue
  • ui/src/components/layout/ConfirmDialog.vue
  • ui/src/components/layout/Pagination.vue
  • ui/src/components/search/SearchBar.vue
  • ui/src/composables/useColorMode.ts
  • ui/src/main.ts
  • ui/src/router/index.ts
  • ui/tailwind.config.js
💤 Files with no reviewable changes (7)
  • ui/src/components/layout/Pagination.vue
  • ui/src/components/TimeRangeSelector.vue
  • ui/src/components/Badge.vue
  • ui/src/components/IconBox.vue
  • ui/src/components/layout/ConfirmDialog.vue
  • ui/src/components/Card.vue
  • ui/src/components/Header.vue
✅ Files skipped from review due to trivial changes (1)
  • .gitignore
🚧 Files skipped from review as they are similar to previous changes (4)
  • ui/src/main.ts
  • ui/src/App.vue
  • ui/tailwind.config.js
  • ui/src/composables/useColorMode.ts

Comment thread ui/package.json
Comment on lines +13 to 27
"@fontsource-variable/inter": "^5.2.8",
"@fontsource-variable/jetbrains-mono": "^5.2.8",
"@vueuse/core": "^14.2.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"dompurify": "^3.3.3",
"lucide-vue-next": "^1.0.0",
"marked": "^18.0.0",
"reka-ui": "^2.9.6",
"tailwind-merge": "^3.5.0",
"tailwindcss-animate": "^1.0.7",
"vue": "^3.5.13",
"vue-router": "^4.5.0"
"vue-router": "^4.5.0",
"vue-sonner": "^2.0.9"
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) Проверка использования Font Awesome в коде:"
rg -n --glob 'ui/src/**/*.{vue,ts,js}' '@fortawesome|\\bfas\\b|\\bfar\\b|\\bfab\\b|fa-[a-z0-9-]+'

echo
echo "2) Проверка наличия зависимости в ui/package.json:"
rg -n '"@fortawesome/fontawesome-free"' ui/package.json

Repository: thebtf/engram

Length of output: 328


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Verifying FontAwesome dependency removal ==="
echo
echo "1) Complete ui/package.json check for `@fortawesome`:"
cat ui/package.json | grep -i fontawesome || echo "No `@fortawesome` found - confirmed removed"

echo
echo "2) Search for all FontAwesome class usages (broader search):"
rg -n 'fa-[a-z0-9-]+' ui/src --type vue --type ts --type js -i 2>/dev/null || echo "Search completed"

echo
echo "3) Check for fas/far/fal/fab class prefixes:"
rg -n '\b(fas|far|fal|fab)\b' ui/src --type vue --type ts --type js 2>/dev/null || echo "No fas/far/fal/fab classes found"

echo
echo "4) Count total FontAwesome icon references:"
rg 'icon="fa-' ui/src --type vue | wc -l

Repository: thebtf/engram

Length of output: 452


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Searching for all FontAwesome references (no type restriction) ==="
echo

echo "1) Search for icon=\"fa- pattern across all files:"
rg 'icon="fa-' ui/src 2>/dev/null || echo "No matches"

echo
echo "2) Direct check of the three identified files:"
for file in ui/src/views/VaultView.vue ui/src/views/TokensView.vue ui/src/views/IssuesView.vue; do
  if [ -f "$file" ]; then
    echo "File: $file"
    rg 'fa-[a-z0-9-]+' "$file" -n 2>/dev/null || echo "  No matches"
  fi
done

echo
echo "3) Show context around the icon usages:"
rg -B2 -A2 'icon="fa-' ui/src 2>/dev/null || true

Repository: thebtf/engram

Length of output: 1667


Восстановите FontAwesome до завершения миграции иконок.

FontAwesome зависимость удалена из ui/package.json, но код ещё содержит 3 ссылки на fa-* иконки. После удаления они не будут отображаться в EmptyState компонентах:

  • ui/src/views/VaultView.vue:186icon="fa-vault"
  • ui/src/views/TokensView.vue:199icon="fa-key"
  • ui/src/views/IssuesView.vue:246icon="fa-circle-exclamation"
Быстрое восстановление
   "dependencies": {
+    "@fortawesome/fontawesome-free": "^6.7.2",
     "@fontsource-variable/inter": "^5.2.8",

Перед удалением замените иконки на альтернативные (например, из lucide-vue-next, которая уже в зависимостях) или восстановите FontAwesome.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"@fontsource-variable/inter": "^5.2.8",
"@fontsource-variable/jetbrains-mono": "^5.2.8",
"@vueuse/core": "^14.2.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"dompurify": "^3.3.3",
"lucide-vue-next": "^1.0.0",
"marked": "^18.0.0",
"reka-ui": "^2.9.6",
"tailwind-merge": "^3.5.0",
"tailwindcss-animate": "^1.0.7",
"vue": "^3.5.13",
"vue-router": "^4.5.0"
"vue-router": "^4.5.0",
"vue-sonner": "^2.0.9"
},
"@fortawesome/fontawesome-free": "^6.7.2",
"@fontsource-variable/inter": "^5.2.8",
"@fontsource-variable/jetbrains-mono": "^5.2.8",
"@vueuse/core": "^14.2.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"dompurify": "^3.3.3",
"lucide-vue-next": "^1.0.0",
"marked": "^18.0.0",
"reka-ui": "^2.9.6",
"tailwind-merge": "^3.5.0",
"tailwindcss-animate": "^1.0.7",
"vue": "^3.5.13",
"vue-router": "^4.5.0",
"vue-sonner": "^2.0.9"
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/package.json` around lines 13 - 27, The FontAwesome dependency was removed
from ui/package.json but three EmptyState usages still reference FontAwesome
icons (icon="fa-vault" in VaultView.vue, icon="fa-key" in TokensView.vue, and
icon="fa-circle-exclamation" in IssuesView.vue), so either restore FontAwesome
to package.json until the icon migration is complete or replace those icon props
with existing lucide-vue-next icons (e.g., use appropriate lucide names and
imports in the EmptyState usages) and update the imports/registration in the
corresponding components to match; ensure package.json includes the FontAwesome
package if you opt to restore it and run install, or update the three files to
remove fa-* references and import lucide components instead.

Comment on lines +2 to +3
// Old components (Badge, Card, IconBox, Header, TimeRangeSelector) were removed in PR 7.
// Use shadcn/ui components from @/components/ui/ instead.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Уточните комментарий про “PR 7”, чтобы не вводить в заблуждение.

На Line 2 формулировка выглядит как привязка к другому этапу; лучше оставить нейтральное описание текущего состояния файла.

Вариант формулировки
-// Old components (Badge, Card, IconBox, Header, TimeRangeSelector) were removed in PR 7.
-// Use shadcn/ui components from `@/components/ui/` instead.
+// Legacy barrel exports are intentionally removed.
+// Use shadcn/ui components from `@/components/ui/`.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Old components (Badge, Card, IconBox, Header, TimeRangeSelector) were removed in PR 7.
// Use shadcn/ui components from @/components/ui/ instead.
// Legacy barrel exports are intentionally removed.
// Use shadcn/ui components from `@/components/ui/`.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/components/index.ts` around lines 2 - 3, The top-of-file comment in
the components index currently references "PR 7", which is misleading; update
that comment to a neutral description stating that the old components (Badge,
Card, IconBox, Header, TimeRangeSelector) were removed and that developers
should use the shadcn/ui components from "@/components/ui/" instead — remove the
"PR 7" mention and replace it with a neutral phrase like "removed in a prior
refactor" or simply omit any PR reference so the comment reads as a current
state note.

@thebtf thebtf merged commit 5dd9dfc into main Apr 25, 2026
8 checks passed
@thebtf thebtf deleted the feat/dashboard-redesign-pr1-foundation branch April 25, 2026 19:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant