feat(ui): full dashboard redesign with shadcn-vue + light/dark theme#207
Conversation
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.
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
Анализ измененийWalkthroughКрупный рефакторинг UI-системы: внедрение дизайн-системы Engram с shadcn-vue, переход на Reka UI компоненты, замена иконок с Font Awesome на Lucide Vue, миграция шрифтов на Inter/JetBrains Mono и переход к CSS-переменным для темизации. Changes
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Poem
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
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.
| const route = useRoute() | ||
| const { authenticated, loading, checkAuth } = useAuth() | ||
| const { isReconnecting, reconnectCountdown } = useSSE() | ||
| useColorMode() |
There was a problem hiding this comment.
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.
| "./src/**/*.{vue,js,ts,jsx,tsx}", | ||
| ], | ||
| darkMode: 'class', | ||
| darkMode: ['class', "class"], |
| 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) | ||
| }) |
There was a problem hiding this comment.
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() {| width: 6px; | ||
| } | ||
| .scrollbar-thin::-webkit-scrollbar-thumb { | ||
| background: rgba(255, 255, 255, 0.2); |
There was a problem hiding this comment.
| const width = computed(() => { | ||
| return `${Math.floor(Math.random() * 40) + 50}%` | ||
| }) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (1)
ui/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (156)
.mcp.jsonui/DESIGN.mdui/components.jsonui/package.jsonui/src/App.vueui/src/assets/main.cssui/src/components/ui/alert-dialog/AlertDialog.vueui/src/components/ui/alert-dialog/AlertDialogAction.vueui/src/components/ui/alert-dialog/AlertDialogCancel.vueui/src/components/ui/alert-dialog/AlertDialogContent.vueui/src/components/ui/alert-dialog/AlertDialogDescription.vueui/src/components/ui/alert-dialog/AlertDialogFooter.vueui/src/components/ui/alert-dialog/AlertDialogHeader.vueui/src/components/ui/alert-dialog/AlertDialogTitle.vueui/src/components/ui/alert-dialog/AlertDialogTrigger.vueui/src/components/ui/alert-dialog/index.tsui/src/components/ui/avatar/Avatar.vueui/src/components/ui/avatar/AvatarFallback.vueui/src/components/ui/avatar/AvatarImage.vueui/src/components/ui/avatar/index.tsui/src/components/ui/badge/Badge.vueui/src/components/ui/badge/index.tsui/src/components/ui/button/Button.vueui/src/components/ui/button/index.tsui/src/components/ui/card/Card.vueui/src/components/ui/card/CardContent.vueui/src/components/ui/card/CardDescription.vueui/src/components/ui/card/CardFooter.vueui/src/components/ui/card/CardHeader.vueui/src/components/ui/card/CardTitle.vueui/src/components/ui/card/index.tsui/src/components/ui/dialog/Dialog.vueui/src/components/ui/dialog/DialogClose.vueui/src/components/ui/dialog/DialogContent.vueui/src/components/ui/dialog/DialogDescription.vueui/src/components/ui/dialog/DialogFooter.vueui/src/components/ui/dialog/DialogHeader.vueui/src/components/ui/dialog/DialogScrollContent.vueui/src/components/ui/dialog/DialogTitle.vueui/src/components/ui/dialog/DialogTrigger.vueui/src/components/ui/dialog/index.tsui/src/components/ui/dropdown-menu/DropdownMenu.vueui/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vueui/src/components/ui/dropdown-menu/DropdownMenuContent.vueui/src/components/ui/dropdown-menu/DropdownMenuGroup.vueui/src/components/ui/dropdown-menu/DropdownMenuItem.vueui/src/components/ui/dropdown-menu/DropdownMenuLabel.vueui/src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vueui/src/components/ui/dropdown-menu/DropdownMenuRadioItem.vueui/src/components/ui/dropdown-menu/DropdownMenuSeparator.vueui/src/components/ui/dropdown-menu/DropdownMenuShortcut.vueui/src/components/ui/dropdown-menu/DropdownMenuSub.vueui/src/components/ui/dropdown-menu/DropdownMenuSubContent.vueui/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vueui/src/components/ui/dropdown-menu/DropdownMenuTrigger.vueui/src/components/ui/dropdown-menu/index.tsui/src/components/ui/input/Input.vueui/src/components/ui/input/index.tsui/src/components/ui/label/Label.vueui/src/components/ui/label/index.tsui/src/components/ui/progress/Progress.vueui/src/components/ui/progress/index.tsui/src/components/ui/radio-group/RadioGroup.vueui/src/components/ui/radio-group/RadioGroupItem.vueui/src/components/ui/radio-group/index.tsui/src/components/ui/scroll-area/ScrollArea.vueui/src/components/ui/scroll-area/ScrollBar.vueui/src/components/ui/scroll-area/index.tsui/src/components/ui/select/Select.vueui/src/components/ui/select/SelectContent.vueui/src/components/ui/select/SelectGroup.vueui/src/components/ui/select/SelectItem.vueui/src/components/ui/select/SelectItemText.vueui/src/components/ui/select/SelectLabel.vueui/src/components/ui/select/SelectScrollDownButton.vueui/src/components/ui/select/SelectScrollUpButton.vueui/src/components/ui/select/SelectSeparator.vueui/src/components/ui/select/SelectTrigger.vueui/src/components/ui/select/SelectValue.vueui/src/components/ui/select/index.tsui/src/components/ui/separator/Separator.vueui/src/components/ui/separator/index.tsui/src/components/ui/sheet/Sheet.vueui/src/components/ui/sheet/SheetClose.vueui/src/components/ui/sheet/SheetContent.vueui/src/components/ui/sheet/SheetDescription.vueui/src/components/ui/sheet/SheetFooter.vueui/src/components/ui/sheet/SheetHeader.vueui/src/components/ui/sheet/SheetTitle.vueui/src/components/ui/sheet/SheetTrigger.vueui/src/components/ui/sheet/index.tsui/src/components/ui/sidebar/Sidebar.vueui/src/components/ui/sidebar/SidebarContent.vueui/src/components/ui/sidebar/SidebarFooter.vueui/src/components/ui/sidebar/SidebarGroup.vueui/src/components/ui/sidebar/SidebarGroupAction.vueui/src/components/ui/sidebar/SidebarGroupContent.vueui/src/components/ui/sidebar/SidebarGroupLabel.vueui/src/components/ui/sidebar/SidebarHeader.vueui/src/components/ui/sidebar/SidebarInput.vueui/src/components/ui/sidebar/SidebarInset.vueui/src/components/ui/sidebar/SidebarMenu.vueui/src/components/ui/sidebar/SidebarMenuAction.vueui/src/components/ui/sidebar/SidebarMenuBadge.vueui/src/components/ui/sidebar/SidebarMenuButton.vueui/src/components/ui/sidebar/SidebarMenuButtonChild.vueui/src/components/ui/sidebar/SidebarMenuItem.vueui/src/components/ui/sidebar/SidebarMenuSkeleton.vueui/src/components/ui/sidebar/SidebarMenuSub.vueui/src/components/ui/sidebar/SidebarMenuSubButton.vueui/src/components/ui/sidebar/SidebarMenuSubItem.vueui/src/components/ui/sidebar/SidebarProvider.vueui/src/components/ui/sidebar/SidebarRail.vueui/src/components/ui/sidebar/SidebarSeparator.vueui/src/components/ui/sidebar/SidebarTrigger.vueui/src/components/ui/sidebar/index.tsui/src/components/ui/sidebar/utils.tsui/src/components/ui/skeleton/Skeleton.vueui/src/components/ui/skeleton/index.tsui/src/components/ui/sonner/Sonner.vueui/src/components/ui/sonner/index.tsui/src/components/ui/switch/Switch.vueui/src/components/ui/switch/index.tsui/src/components/ui/table/Table.vueui/src/components/ui/table/TableBody.vueui/src/components/ui/table/TableCaption.vueui/src/components/ui/table/TableCell.vueui/src/components/ui/table/TableEmpty.vueui/src/components/ui/table/TableFooter.vueui/src/components/ui/table/TableHead.vueui/src/components/ui/table/TableHeader.vueui/src/components/ui/table/TableRow.vueui/src/components/ui/table/index.tsui/src/components/ui/tabs/Tabs.vueui/src/components/ui/tabs/TabsContent.vueui/src/components/ui/tabs/TabsList.vueui/src/components/ui/tabs/TabsTrigger.vueui/src/components/ui/tabs/index.tsui/src/components/ui/textarea/Textarea.vueui/src/components/ui/textarea/index.tsui/src/components/ui/toggle-group/ToggleGroup.vueui/src/components/ui/toggle-group/ToggleGroupItem.vueui/src/components/ui/toggle-group/index.tsui/src/components/ui/toggle/Toggle.vueui/src/components/ui/toggle/index.tsui/src/components/ui/tooltip/Tooltip.vueui/src/components/ui/tooltip/TooltipContent.vueui/src/components/ui/tooltip/TooltipProvider.vueui/src/components/ui/tooltip/TooltipTrigger.vueui/src/components/ui/tooltip/index.tsui/src/composables/index.tsui/src/composables/useColorMode.tsui/src/env.d.tsui/src/lib/utils.tsui/src/main.tsui/tailwind.config.js
| "command": "bunx", | ||
| "args": [ | ||
| "-y", | ||
| "@jpisnice/shadcn-ui-mcp-server", | ||
| "--framework", | ||
| "vue" |
There was a problem hiding this comment.
🧩 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:
- 1: https://registry.npmjs.org/%40jpisnice%2Fshadcn-ui-mcp-server
- 2: https://www.npmjs.com/package/@jpisnice/shadcn-ui-mcp-server
- 3: https://github.com/Jpisnice/shadcn-ui-mcp-server/releases
- 4: https://github.com/Jpisnice/shadcn-ui-mcp-server
- 5: https://glama.ai/mcp/servers/%40Jpisnice/shadcn-ui-mcp-server
Зафиксируйте версию 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.
| "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".
| - 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 |
There was a problem hiding this comment.
Несоответствие документации и реализации компонентов.
Документация запрещает использование теней, однако в 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.
| .pulse-dot { | ||
| animation: pulseDot 2s infinite; | ||
| } | ||
| @keyframes pulseDot { | ||
| 0%, 100% { opacity: 1; } | ||
| 50% { opacity: 0.5; } | ||
| } |
There was a problem hiding this comment.
Имя @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.
| .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.
| <AvatarImage v-bind="props" class="h-full w-full object-cover"> | ||
| <slot /> | ||
| </AvatarImage> |
There was a problem hiding this comment.
🧩 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:
- 1: https://reka-ui.com/docs/components/avatar
- 2: https://reka-ui.com/meta/AvatarImage
- 3: https://www.reka-ui.com/docs/components/avatar
Удалите слот из 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).
| <Primitive | ||
| :as="as" | ||
| :as-child="asChild" | ||
| :class="cn(buttonVariants({ variant, size }), props.class)" | ||
| > |
There was a problem hiding this comment.
Добавьте безопасный 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.
| <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.
| 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}` | ||
| } |
There was a problem hiding this comment.
Несогласованность 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.
| 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.
| <button | ||
| data-sidebar="rail" | ||
| aria-label="Toggle Sidebar" | ||
| :tabindex="-1" | ||
| title="Toggle Sidebar" | ||
| :class="cn( |
There was a problem hiding this comment.
Для 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.
| <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(...)") чтобы она не отправляла форму по умолчанию.
| 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" |
There was a problem hiding this comment.
🧩 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:
- 1: https://registry.npmjs.org/vue-sonner
- 2: https://github.com/xiaoluoboding/vue-sonner
- 3: https://github.com/wobsoriano/vue-sonner
- 4: https://raw.githubusercontent.com/xiaoluoboding/vue-sonner/3b07b505fd46e4f7d5a932a4870a4cc06d995175/CHANGELOG.md
- 5: https://github.com/xiaoluoboding/vue-sonner/releases/tag/v2.0.0
- 6: https://www.npmjs.com/package/vue-sonner
🏁 Script executed:
find . -name "Sonner.vue" -o -name "sonner" -type d | head -20Repository: thebtf/engram
Length of output: 128
🏁 Script executed:
cat -n ui/src/components/ui/sonner/Sonner.vueRepository: thebtf/engram
Length of output: 1869
🏁 Script executed:
# Check for type definitions related to ToasterProps
fd -i "toasterprop\|sonner.*type\|sonner.*d\.ts" --type fRepository: 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.
| 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.
| 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> |
There was a problem hiding this comment.
🧩 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:
- 1: https://vuejs.org/guide/essentials/forms
- 2: https://v3.vuejs.org/guide/forms.html
- 3: https://vuejs.org/guide/components/v-model
- 4: Type of the value property in the TextareaHTMLAttributes interface vuejs/core#7737
- 5: https://stackoverflow.com/questions/62276944/how-databind-a-textarea-component-text-value-and-update-it
- 6: https://www.webmound.com/use-v-model-custom-components-vue-3/
Для 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.
| 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 ожидают строку.
| provide("toggleGroup", { | ||
| variant: props.variant, | ||
| size: props.size, | ||
| }) |
There was a problem hiding this comment.
Реактивность контекста не обеспечена.
Объект, переданный в 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
There was a problem hiding this comment.
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
📒 Files selected for processing (2)
ui/src/App.vueui/src/components/layout/AppSidebar.vue
🚧 Files skipped from review as they are similar to previous changes (1)
- ui/src/App.vue
| <SidebarMenuButton | ||
| as-child | ||
| :is-active="route.path === '/admin'" | ||
| tooltip="Admin" |
There was a problem hiding this comment.
Подсветка 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.
| <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.
There was a problem hiding this comment.
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: Рассмотрите использование явногоloadingref изuseHealth.Согласно context snippet 1,
useHealthпредоставляет отдельный refloading. Текущая проверка!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
📒 Files selected for processing (6)
ui/src/components/StatsCards.vueui/src/components/index.tsui/src/views/HomeView.vueui/src/views/LoginView.vueui/src/views/RegisterView.vueui/src/views/SetupView.vue
💤 Files with no reviewable changes (2)
- ui/src/components/index.ts
- ui/src/components/StatsCards.vue
| <TableRow | ||
| v-for="issue in recentIssues" | ||
| :key="issue.id" | ||
| class="cursor-pointer hover:bg-muted/50" | ||
| @click="navigateToIssue(issue.id)" |
There was a problem hiding this comment.
Добавьте поддержку клавиатурной навигации для кликабельных строк таблицы.
Строки таблицы кликабельны (@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.
| <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
There was a problem hiding this comment.
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
📒 Files selected for processing (6)
ui/src/components/layout/AppHeader.vueui/src/views/AdminView.vueui/src/views/IssueDetailView.vueui/src/views/IssuesView.vueui/src/views/TokensView.vueui/src/views/VaultView.vue
| function handleSearch(_q: string) { | ||
| // search route does not exist — no-op | ||
| searchQuery.value = '' |
There was a problem hiding this comment.
Не очищайте поле поиска при 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.
| 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 для непустого запроса.
| <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)" | ||
| /> |
There was a problem hiding this comment.
Блокируйте строку пользователя на время обновления.
Сейчас 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).
| <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}`)" | ||
| > |
There was a problem hiding this comment.
Сделайте переход к 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
There was a problem hiding this comment.
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
📒 Files selected for processing (18)
.gitignoreui/package.jsonui/src/App.vueui/src/assets/main.cssui/src/components/Badge.vueui/src/components/Card.vueui/src/components/Header.vueui/src/components/IconBox.vueui/src/components/TimeRangeSelector.vueui/src/components/index.tsui/src/components/layout/AppHeader.vueui/src/components/layout/ConfirmDialog.vueui/src/components/layout/Pagination.vueui/src/components/search/SearchBar.vueui/src/composables/useColorMode.tsui/src/main.tsui/src/router/index.tsui/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
| "@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" | ||
| }, |
There was a problem hiding this comment.
🧩 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.jsonRepository: 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 -lRepository: 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 || trueRepository: thebtf/engram
Length of output: 1667
Восстановите FontAwesome до завершения миграции иконок.
FontAwesome зависимость удалена из ui/package.json, но код ещё содержит 3 ссылки на fa-* иконки. После удаления они не будут отображаться в EmptyState компонентах:
ui/src/views/VaultView.vue:186—icon="fa-vault"ui/src/views/TokensView.vue:199—icon="fa-key"ui/src/views/IssuesView.vue:246—icon="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.
| "@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.
| // Old components (Badge, Card, IconBox, Header, TimeRangeSelector) were removed in PR 7. | ||
| // Use shadcn/ui components from @/components/ui/ instead. |
There was a problem hiding this comment.
Уточните комментарий про “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.
| // 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.
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.mdTasks: 56 tasks (T001-T056)
5 commits (originally planned as 7 PRs, stacked into 1):
1. Foundation (
700356f)::root+.dark)2. Sidebar (
c871a63):3. HomeView + Auth (
567fbd7):4. Issues + Management (
3a8a0cc):5. Cleanup (
b2a0910):Verification
vue-tsc --noEmit— cleanvite build— clean (3.6s)fas fa-references in .vue filesSummary by CodeRabbit
Примечания к выпуску
Новые функции
Улучшения