diff --git a/.cursor/rules/kpi-hybrid.mdc b/.cursor/rules/kpi-hybrid.mdc
index cfe7f38..80fed71 100644
--- a/.cursor/rules/kpi-hybrid.mdc
+++ b/.cursor/rules/kpi-hybrid.mdc
@@ -1,11 +1,11 @@
---
-description: Только hybrid KPI — KpiStatGrid / row tile DNA (stats-12). Запрет SectionCards и hand-roll.
+description: Только hybrid KPI — KpiStatGrid / row tile DNA (stats-12 + IconTile). Запрет SectionCards и hand-roll.
alwaysApply: true
---
-# KPI hybrid — только kit (stats-12 DNA)
+# KPI hybrid — только kit (stats-12 + IconTile)
-Preview: [stats-12](https://reui.io/preview/base/stats-12). SoT DNA = EvoBGP. Markup в проекте: `apps/web/src/components/reui-kit/kpi-stat-grid.tsx`.
+Preview: [stats-12](https://reui.io/preview/base/stats-12). Primitive: [icon-tile](https://reui.io/docs/components/base/icon-tile). SoT DNA = EvoBGP. Markup: `apps/web/src/components/reui-kit/kpi-stat-grid.tsx`.
Связанные: [`reui-mcp.mdc`](reui-mcp.mdc), [`web-shadcn.mdc`](web-shadcn.mdc).
@@ -14,16 +14,17 @@ Preview: [stats-12](https://reui.io/preview/base/stats-12). SoT DNA = EvoBGP. Ma
| Зона | Компонент / DNA |
|------|-----------------|
| KPI-полосы / dashboard metrics | только `reui-kit/KpiStatGrid` (через `OpsDashboard` / `DetailPanel.Metrics` при наличии) |
-| Markup | horizontal compact hybrid: icon left `Item` `size-10.5` `bg-muted` + `border-background` + shadow + `ItemMedia` + label/Badge + value ± `variant` |
-| Row icon tiles (data-grid) | та же DNA — semantic `text-*` на `bg-muted` |
-| Quick Actions | только `reui-kit/QuickActionGrid` (sibling hybrid DNA) |
+| Markup | horizontal compact hybrid: icon left `IconTile` `variant="elevated"` `className="size-10.5"` + label/Badge + value ± `variant` |
+| Row icon tiles (data-grid) | та же DNA — semantic `text-*` на IconTile elevated |
+| Quick Actions | только `reui-kit/QuickActionGrid` (sibling hybrid DNA, IconTile elevated) |
-Импорты UI: `@evobgp/ui/components/*`.
+Импорты UI: `@evobgp/ui/components/*`. IconTile: `@/components/reui/icon-tile`.
## NEVER
- SectionCards / vertical-only KPI / hand-roll Frame/Card KPI
-- Другой size / radius / solid brand fill вместо `bg-muted`
+- `Item` `size-10.5` `bg-muted` hybrid вместо IconTile
+- Другой size / radius / solid brand fill вместо `elevated`
- `card-35` как замена stats-12 hybrid KPI
- Копипаст ReUI block в route — adapt через `reui-kit/`
-- Голый lucide `size-4` в name-cell без hybrid tile
+- Голый lucide `size-4` в name-cell без IconTile
diff --git a/.cursor/rules/reui-mcp.mdc b/.cursor/rules/reui-mcp.mdc
index 7a85287..81f73e7 100644
--- a/.cursor/rules/reui-mcp.mdc
+++ b/.cursor/rules/reui-mcp.mdc
@@ -75,9 +75,9 @@ alwaysApply: true
## Установленные ReUI (apps/web)
-**Components:** `frame`, `data-grid/*`, `filters`, `kanban`, `badge`, `alert`, `autocomplete`, `number-field`, `date-selector`, `color-picker`, `timeline`, `rating`, `phone-input`, `icon-stack`
+**Components:** `frame`, `data-grid/*`, `filters`, `badge`, `alert`, `autocomplete`, `number-field`, `date-selector`, `color-picker`, `timeline`, `rating`, `phone-input`, `icon-stack`, `icon-tile`, `stepper`
-**Kit:** `ResourcePage`, `KpiStatGrid`, `QuickActionGrid`, `OpsDashboard`, `KanbanBoard`, `DetailPanel`, `SettingsShell`
+**Kit:** `ResourcePage`, `KpiStatGrid`, `QuickActionGrid`, `OpsDashboard`, `DetailPanel`, `SettingsShell`
**Blocks (reference):** `stats-12`, `card-35`, `auth-13`, `app-shell-12`, `settings-16`, `settings-8`, `empty-state-12`, `form-7`, `data-grid-filtering-2`, `dashboard-1`, …
@@ -110,6 +110,7 @@ REUI_LICENSE_KEY=
- Hand-roll data-grid/filters/KPI/Quick Actions/settings rows при наличии `@reui` / kit
- Смешивать Card и Frame на ops-экране
- Ставить shadcn/ui blocks выше ReUI PRO
+- **settings-7** как target (`surface: card`) — только [settings-3](https://reui.io/preview/base/settings-3) / [settings-16](https://reui.io/preview/base/settings-16)
## Чеклист
diff --git a/.cursor/rules/web-shadcn.mdc b/.cursor/rules/web-shadcn.mdc
index 800ab69..d9f7ba0 100644
--- a/.cursor/rules/web-shadcn.mdc
+++ b/.cursor/rules/web-shadcn.mdc
@@ -101,6 +101,9 @@ pnpm --filter @evobgp/web run build
**WEB-22** | NEVER | Legacy Svelte UI удалён. Не восстанавливать `web-legacy-svelte/` и не копировать Svelte-паттерны в React-код.
*Проверка:* отсутствие каталога `web-legacy-svelte/`; `pnpm --filter @evobgp/web run typecheck`.
+**WEB-23** | NEVER | **settings-7** как target ([preview](https://reui.io/preview/base/settings-7) — `surface: card`). Новые settings-экраны — [settings-3](https://reui.io/preview/base/settings-3) / [settings-16](https://reui.io/preview/base/settings-16) (`surface: frame`). Существующий `apps/web/src/components/settings/*` на DNA settings-7 **не** копировать как эталон; миграция — отдельный CHANGE.
+*Проверка:* review MCP `get_block` / cite; нет новых экранов с settings-7.
+
---
## Documentation Sync (Web)
diff --git a/apps/web/src/components/blocks/settings-7/components/setting-row.tsx b/apps/web/src/components/blocks/settings-7/components/setting-row.tsx
index 274402a..371dbe1 100644
--- a/apps/web/src/components/blocks/settings-7/components/setting-row.tsx
+++ b/apps/web/src/components/blocks/settings-7/components/setting-row.tsx
@@ -1,62 +1,2 @@
-import { type ReactNode } from "react"
-
-import { cn } from "@evobgp/ui/lib/utils"
-import {
- Field,
- FieldContent,
- FieldDescription,
- FieldLabel,
- FieldSeparator,
- FieldTitle,
-} from "@evobgp/ui/components/field"
-
-interface SettingRowProps {
- title: string
- description?: ReactNode
- children: ReactNode
- last?: boolean
- labelFor?: string
- contentClassName?: string
- titleAddon?: ReactNode
-}
-
-export function SettingRow({
- title,
- description,
- children,
- last,
- labelFor,
- contentClassName,
- titleAddon,
-}: SettingRowProps) {
- return (
- <>
-
-
-
- {labelFor ? (
- {title}
- ) : (
- {title}
- )}
- {titleAddon}
-
-
- {description ? (
-
{description}
- ) : null}
-
-
-
-
- {children}
-
-
-
-
- {!last ? : null}
- >
- )
-}
\ No newline at end of file
+/** @deprecated Import from `@/components/settings/setting-row` — settings-7 is not a target. */
+export { SettingRow } from '@/components/settings/setting-row'
diff --git a/apps/web/src/components/blocks/settings-7/components/settings-card.tsx b/apps/web/src/components/blocks/settings-7/components/settings-card.tsx
index 7cf1c2d..8aaa87a 100644
--- a/apps/web/src/components/blocks/settings-7/components/settings-card.tsx
+++ b/apps/web/src/components/blocks/settings-7/components/settings-card.tsx
@@ -1,53 +1,2 @@
-import { type ReactNode } from 'react'
-
-import { cn } from '@evobgp/ui/lib/utils'
-import {
- Frame,
- FrameDescription,
- FrameFooter,
- FrameHeader,
- FramePanel,
- FrameTitle,
-} from '@/components/reui/frame'
-
-interface SettingsCardProps {
- title: string
- description?: string
- children: ReactNode
- footer?: ReactNode
- className?: string
- contentClassName?: string
- footerClassName?: string
-}
-
-/** Settings section — Frame surface. Preview: https://reui.io/preview/base/settings-16 */
-export function SettingsCard({
- title,
- description,
- children,
- footer,
- className,
- contentClassName,
- footerClassName,
-}: SettingsCardProps) {
- return (
-
-
-
- {title}
- {description ? {description} : null}
-
-
- {children}
-
- {footer ? (
-
- {footer}
-
- ) : null}
-
-
- )
-}
+/** @deprecated Import from `@/components/settings/settings-card` — settings-7 is not a target. */
+export { SettingsCard } from '@/components/settings/settings-card'
diff --git a/apps/web/src/components/blocks/settings-7/components/settings-field-group.tsx b/apps/web/src/components/blocks/settings-7/components/settings-field-group.tsx
index 0ee4249..923e57d 100644
--- a/apps/web/src/components/blocks/settings-7/components/settings-field-group.tsx
+++ b/apps/web/src/components/blocks/settings-7/components/settings-field-group.tsx
@@ -1,37 +1,2 @@
-import { type ReactNode } from "react"
-
-import { cn } from "@evobgp/ui/lib/utils"
-import {
- FieldDescription,
- FieldGroup,
- FieldLegend,
- FieldSet,
-} from "@evobgp/ui/components/field"
-
-interface SettingsFieldGroupProps {
- legend: string
- description: string
- children: ReactNode
- className?: string
- fieldGroupClassName?: string
-}
-
-export function SettingsFieldGroup({
- legend,
- description,
- children,
- className,
- fieldGroupClassName,
-}: SettingsFieldGroupProps) {
- return (
-
- )
-}
\ No newline at end of file
+/** @deprecated Import from `@/components/settings/settings-field-group` — settings-7 is not a target. */
+export { SettingsFieldGroup } from '@/components/settings/settings-field-group'
diff --git a/apps/web/src/components/confirm-dialog.tsx b/apps/web/src/components/confirm-dialog.tsx
index 2e33cc8..05b6b82 100644
--- a/apps/web/src/components/confirm-dialog.tsx
+++ b/apps/web/src/components/confirm-dialog.tsx
@@ -1,19 +1,16 @@
-import { Button } from '@evobgp/ui/components/button'
-import {
- Drawer,
- DrawerClose,
- DrawerContent,
- DrawerDescription,
- DrawerHeader,
- DrawerTitle,
- DrawerTrigger,
-} from '@evobgp/ui/components/drawer'
-import type { ReactElement, ReactNode } from 'react'
+import { useState, type ReactElement, type ReactNode } from 'react'
import {
- confirmDrawerContentClassName,
- DrawerActionsFooter,
-} from '@/components/drawer-layout'
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+ AlertDialogTrigger,
+} from '@evobgp/ui/components/alert-dialog'
type ConfirmDialogBaseProps = {
title: string
@@ -41,90 +38,52 @@ type ConfirmDialogControlled = ConfirmDialogBaseProps & {
type ConfirmDialogProps = ConfirmDialogWithTrigger | ConfirmDialogControlled
-function ConfirmDrawerBody({
- title,
- description,
- confirmLabel = 'Подтвердить',
- cancelLabel = 'Отмена',
- destructive,
- onConfirm,
- confirmDisabled,
- confirmLoading,
- confirmLoadingLabel,
- controlled,
-}: ConfirmDialogBaseProps & { controlled?: boolean }) {
- const confirmText =
- confirmLoading && confirmLoadingLabel
- ? confirmLoadingLabel
- : confirmLoading
- ? `${confirmLabel}…`
- : confirmLabel
-
- return (
- <>
-
- {title}
- {description ? {description} : null}
-
-
- }>
- {cancelLabel}
-
- {controlled ? (
-
- ) : (
-
- }
- onClick={onConfirm}
- >
- {confirmText}
-
- )}
-
- >
- )
-}
-
+/** Confirm overlay — Alert Dialog. Preview: https://reui.io/preview/base/components/c-alert-dialog-11 */
export function ConfirmDialog(props: ConfirmDialogProps) {
- const bodyProps: ConfirmDialogBaseProps = {
- title: props.title,
- description: props.description,
- confirmLabel: props.confirmLabel,
- cancelLabel: props.cancelLabel,
- destructive: props.destructive,
- onConfirm: props.onConfirm,
- confirmDisabled: props.confirmDisabled,
- confirmLoading: props.confirmLoading,
- confirmLoadingLabel: props.confirmLoadingLabel,
+ const [uncontrolledOpen, setUncontrolledOpen] = useState(false)
+ const isControlled = props.open !== undefined
+ const open = isControlled ? props.open : uncontrolledOpen
+
+ function setOpen(next: boolean) {
+ if (!isControlled) setUncontrolledOpen(next)
+ if (isControlled) props.onOpenChange?.(next)
}
- if (props.trigger) {
- return (
-
-
-
-
-
-
- )
+ const confirmText =
+ props.confirmLoading && props.confirmLoadingLabel
+ ? props.confirmLoadingLabel
+ : props.confirmLoading
+ ? `${props.confirmLabel ?? 'Подтвердить'}…`
+ : (props.confirmLabel ?? 'Подтвердить')
+
+ function handleConfirm() {
+ props.onConfirm()
+ if (!props.confirmLoading && !isControlled) setOpen(false)
}
return (
-
-
-
-
-
+
+ {props.trigger ? : null}
+
+
+ {props.title}
+ {props.description ? (
+ {props.description}
+ ) : null}
+
+
+
+ {props.cancelLabel ?? 'Отмена'}
+
+
+ {confirmText}
+
+
+
+
)
}
diff --git a/apps/web/src/components/drawer-layout.tsx b/apps/web/src/components/drawer-layout.tsx
deleted file mode 100644
index 5f67399..0000000
--- a/apps/web/src/components/drawer-layout.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import type { ReactNode } from 'react'
-
-import { cn } from '@evobgp/ui/lib/utils'
-import { DrawerFooter } from '@evobgp/ui/components/drawer'
-
-/** Shared footer layout for right-side form and confirm drawers. */
-export function DrawerActionsFooter({
- children,
- className,
-}: {
- children: ReactNode
- className?: string
-}) {
- return (
-
-
- {children}
-
-
- )
-}
-
-export const formDrawerContentClassName =
- 'flex h-full max-h-dvh flex-col sm:max-w-lg'
-
-export const confirmDrawerContentClassName = 'flex h-auto max-h-dvh flex-col sm:max-w-sm'
diff --git a/apps/web/src/components/empty-state.tsx b/apps/web/src/components/empty-state.tsx
index 43bcd20..45a1c2a 100644
--- a/apps/web/src/components/empty-state.tsx
+++ b/apps/web/src/components/empty-state.tsx
@@ -1,28 +1,103 @@
+/**
+ * ReUI Empty + IconStack — Frame-friendly (empty-state-14 / empty-state-12).
+ * Preview: https://reui.io/preview/base/empty-state-14 · https://reui.io/preview/base/empty-state-12
+ * Docs: https://reui.io/docs/components/base/icon-stack
+ */
+import { InboxIcon, type LucideIcon } from 'lucide-react'
import type { ReactNode } from 'react'
+
+import { IconStack } from '@/components/reui/icon-stack'
+import {
+ Empty,
+ EmptyContent,
+ EmptyDescription,
+ EmptyHeader,
+ EmptyMedia,
+ EmptyTitle,
+} from '@evobgp/ui/components/empty'
import { cn } from '@evobgp/ui/lib/utils'
interface EmptyStateProps {
+ icon?: LucideIcon | ReactNode
title: string
description?: string
- icon?: ReactNode
action?: ReactNode
className?: string
+ stackedIcon?: boolean
+ centered?: boolean
}
-export function EmptyState({ title, description, icon, action, className }: EmptyStateProps) {
+function isLucideIcon(icon: LucideIcon | ReactNode): icon is LucideIcon {
+ if (typeof icon === 'function') return true
+ if (icon != null && typeof icon === 'object' && '$$typeof' in icon) {
+ if ('props' in icon) return false
+ return true
+ }
+ return false
+}
+
+export function EmptyState({
+ icon,
+ title,
+ description,
+ action,
+ className,
+ stackedIcon = true,
+ centered = true,
+}: EmptyStateProps) {
+ const Icon = isLucideIcon(icon) ? icon : InboxIcon
+ const customIcon = icon && !isLucideIcon(icon) ? icon : null
+
+ const body = (
+
+
+
+ {customIcon ? (
+ stackedIcon ? (
+
+ {customIcon}
+
+ ) : (
+ {customIcon}
+ )
+ ) : stackedIcon ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+
+ {title}
+ {description ? (
+ {description}
+ ) : null}
+
+
+ {action ? (
+ {action}
+ ) : null}
+
+ )
+
+ if (!centered) return body
+
return (
- {icon ?
{icon}
: null}
-
-
{title}
- {description ?
{description}
: null}
-
- {action ?
{action}
: null}
+ {body}
)
}
diff --git a/apps/web/src/components/form-drawer.tsx b/apps/web/src/components/form-drawer.tsx
index cdfeaa1..731bf38 100644
--- a/apps/web/src/components/form-drawer.tsx
+++ b/apps/web/src/components/form-drawer.tsx
@@ -2,18 +2,13 @@ import type { ReactNode } from 'react'
import { cn } from '@evobgp/ui/lib/utils'
import {
- Drawer,
- DrawerContent,
- DrawerDescription,
- DrawerHeader,
- DrawerTitle,
-} from '@evobgp/ui/components/drawer'
-import { ScrollArea } from '@evobgp/ui/components/scroll-area'
-
-import {
- DrawerActionsFooter,
- formDrawerContentClassName,
-} from '@/components/drawer-layout'
+ Sheet,
+ SheetContent,
+ SheetDescription,
+ SheetFooter,
+ SheetHeader,
+ SheetTitle,
+} from '@evobgp/ui/components/sheet'
interface FormDrawerProps {
open: boolean
@@ -25,6 +20,7 @@ interface FormDrawerProps {
className?: string
}
+/** Create/edit overlay — Sheet DNA (sheet-8 / sheet-1). Preview: https://reui.io/preview/base/sheet-8 */
export function FormDrawer({
open,
onOpenChange,
@@ -35,17 +31,22 @@ export function FormDrawer({
className,
}: FormDrawerProps) {
return (
-
-
-
- {title}
- {description ? {description} : null}
-
-
- {children}
-
- {footer}
-
-
+
+
+
+ {title}
+ {description ? {description} : null}
+
+ {children}
+
+
+ {footer}
+
+
+
+
)
}
diff --git a/apps/web/src/components/layout/app-shell.tsx b/apps/web/src/components/layout/app-shell.tsx
index 7d46168..479737a 100644
--- a/apps/web/src/components/layout/app-shell.tsx
+++ b/apps/web/src/components/layout/app-shell.tsx
@@ -47,6 +47,7 @@ import { CommandPalette, type CommandPaletteItem } from '@/components/layout/com
import { NavUser } from '@/components/layout/nav-user'
import { SystemMonitorPopover } from '@/components/layout/system-monitor-popover'
import { can, isAuthEnabled, permissionForPath } from '@/lib/auth'
+import { SKIP_TO_CONTENT_CLASS } from '@/lib/ui-surface'
interface NavItem {
to: string
@@ -161,6 +162,9 @@ export function AppShell({ children }: { children: ReactNode }) {
} as CSSProperties
}
>
+
+ К содержимому
+
@@ -196,7 +200,7 @@ export function AppShell({ children }: { children: ReactNode }) {
-
+
@@ -220,9 +224,9 @@ export function AppShell({ children }: { children: ReactNode }) {
-
+
{children}
-
+
diff --git a/apps/web/src/components/patterns/illustrated-empty-state.tsx b/apps/web/src/components/patterns/illustrated-empty-state.tsx
index c39798f..4338e97 100644
--- a/apps/web/src/components/patterns/illustrated-empty-state.tsx
+++ b/apps/web/src/components/patterns/illustrated-empty-state.tsx
@@ -2,7 +2,10 @@ import type { ReactNode } from 'react'
import type { LucideIcon } from 'lucide-react'
import { IconStack } from '@/components/reui/icon-stack'
-import { Card, CardContent } from '@evobgp/ui/components/card'
+import {
+ Frame,
+ FramePanel,
+} from '@/components/reui/frame'
import {
Empty,
EmptyDescription,
@@ -11,7 +14,7 @@ import {
EmptyTitle,
} from '@evobgp/ui/components/empty'
-/** empty-state-11 pattern adapted to Card surface. */
+/** empty-state-14: Frame + IconStack. Preview: https://reui.io/preview/base/empty-state-14 */
export function IllustratedEmptyState({
title,
description,
@@ -24,8 +27,8 @@ export function IllustratedEmptyState({
action?: ReactNode
}) {
return (
-
-
+
+
{action ? {action}
: null}
@@ -40,7 +43,7 @@ export function IllustratedEmptyState({
-
-
+
+
)
}
diff --git a/apps/web/src/components/patterns/kpi-sparkline-card.tsx b/apps/web/src/components/patterns/kpi-sparkline-card.tsx
index 3ecd64a..d2e4caf 100644
--- a/apps/web/src/components/patterns/kpi-sparkline-card.tsx
+++ b/apps/web/src/components/patterns/kpi-sparkline-card.tsx
@@ -4,7 +4,7 @@ import { Badge } from '@/components/reui/badge'
import { CardDotField } from '@/components/dashboard/card-dot-field'
import { PanelCorners } from '@/components/patterns/panel-corners'
import { toneStyles, type MetricTone } from '@/components/patterns/metric-tone-styles'
-import { Card, CardContent } from '@evobgp/ui/components/card'
+import { Frame, FramePanel } from '@/components/reui/frame'
export type KpiSparklineMetric = {
id: string
@@ -44,21 +44,21 @@ function Sparkline({ values, color }: { values: readonly number[]; color: string
)
}
-/** KPI card with sparkline (dashboard-5 / chart-15 pattern, Card surface). */
+/** KPI sparkline tile — chart-15 Frame DNA. Preview: https://reui.io/preview/base/chart-15 */
export function KpiSparklineCard({ metric }: { metric: KpiSparklineMetric }) {
const tone = toneStyles[metric.tone]
return (
-
-
-
-
-
+
+
+
+
+
{metric.title}
{metric.label}
-
-
+
+
{metric.value}
@@ -71,7 +71,7 @@ export function KpiSparklineCard({ metric }: { metric: KpiSparklineMetric }) {
-
-
+
+
)
}
diff --git a/apps/web/src/components/query-state.tsx b/apps/web/src/components/query-state.tsx
index fac6d8b..9a86e15 100644
--- a/apps/web/src/components/query-state.tsx
+++ b/apps/web/src/components/query-state.tsx
@@ -34,23 +34,29 @@ export function QueryState
({
children,
}: QueryStateProps) {
if (isLoading) {
- return <>{skeleton ?? }>
+ return (
+
+ {skeleton ?? }
+
+ )
}
if (isError) {
return (
- }
- title="Ошибка загрузки"
- description={error instanceof Error ? error.message : 'Не удалось загрузить данные'}
- action={
- onRetry ? (
-
- ) : null
- }
- />
+
+
+
+ Повторить
+
+ ) : null
+ }
+ />
+
)
}
if (empty || data == null) {
diff --git a/apps/web/src/components/reui-kit/kpi-stat-grid.tsx b/apps/web/src/components/reui-kit/kpi-stat-grid.tsx
index 28b3fac..e3153ab 100644
--- a/apps/web/src/components/reui-kit/kpi-stat-grid.tsx
+++ b/apps/web/src/components/reui-kit/kpi-stat-grid.tsx
@@ -5,7 +5,7 @@ import { Frame, FramePanel } from '@/components/reui/frame'
import { Badge } from '@/components/reui/badge'
import { cn } from '@evobgp/ui/lib/utils'
import { kpiCols } from './kpi-cols'
-import { Item, ItemMedia } from '@evobgp/ui/components/item'
+import { IconTile } from '@/components/reui/icon-tile'
import { Skeleton } from '@evobgp/ui/components/skeleton'
export type KpiStatVariant = 'default' | 'warning' | 'destructive'
@@ -83,16 +83,13 @@ function KpiStatCardBody({ item }: { item: KpiStatItem }) {
return (
{item.icon ? (
-
-
-
- {item.icon}
-
-
+ {item.icon}
+
) : null}
diff --git a/apps/web/src/components/reui-kit/quick-action-grid.tsx b/apps/web/src/components/reui-kit/quick-action-grid.tsx
index b0c283f..b00d9f2 100644
--- a/apps/web/src/components/reui-kit/quick-action-grid.tsx
+++ b/apps/web/src/components/reui-kit/quick-action-grid.tsx
@@ -8,7 +8,7 @@ import {
FrameTitle,
} from '@/components/reui/frame'
import { Badge } from '@/components/reui/badge'
-import { Item, ItemMedia } from '@evobgp/ui/components/item'
+import { IconTile } from '@/components/reui/icon-tile'
import { cn } from '@evobgp/ui/lib/utils'
import { kpiCols } from './kpi-cols'
@@ -54,16 +54,13 @@ function QuickActionBody({ action }: { action: QuickActionItem }) {
return (
{action.icon ? (
-
-
-
- {action.icon}
-
-
+ {action.icon}
+
) : null}
diff --git a/apps/web/src/components/reui-kit/settings-shell.tsx b/apps/web/src/components/reui-kit/settings-shell.tsx
index a7b59c5..e0a40bf 100644
--- a/apps/web/src/components/reui-kit/settings-shell.tsx
+++ b/apps/web/src/components/reui-kit/settings-shell.tsx
@@ -9,7 +9,7 @@ import { PageHeader } from '@/components/page-header'
export interface SettingsTabConfig {
id: string
- to: string
+ to: '/settings' | '/tenant-settings'
label: string
icon?: ReactNode
}
@@ -72,19 +72,35 @@ export function SettingsShell({
>
{tabs.map((tab) => {
const isActive = pathname.startsWith(tab.to)
+ const className = cn(
+ 'flex items-center gap-2 rounded-lg px-3 py-2 text-sm transition-colors',
+ isMobile && 'shrink-0',
+ !isMobile && 'w-full',
+ isActive
+ ? 'bg-muted text-foreground font-medium shadow-sm ring-1 ring-border/60'
+ : 'text-muted-foreground hover:bg-muted/60 hover:text-foreground',
+ )
+ if (tab.to === '/tenant-settings') {
+ return (
+
+ {tab.icon}
+ {tab.label}
+
+ )
+ }
return (
{tab.icon}
{tab.label}
diff --git a/apps/web/src/components/reui/icon-tile.tsx b/apps/web/src/components/reui/icon-tile.tsx
new file mode 100644
index 0000000..cc2d209
--- /dev/null
+++ b/apps/web/src/components/reui/icon-tile.tsx
@@ -0,0 +1,125 @@
+import { mergeProps } from "@base-ui/react/merge-props"
+import { useRender } from "@base-ui/react/use-render"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@evobgp/ui/lib/utils"
+
+/**
+ * CSS variable architecture:
+ *
+ * The root owns four variables so every part of the tile stays in proportion
+ * and stays overridable from a single `className`:
+ *
+ * --icon-tile-size tile width/height
+ * --icon-tile-icon-size glyph size applied to child svgs
+ * --icon-tile-radius corner radius (also drives the nested inner card)
+ * --icon-tile-inset gap between the outer ring and the inner card
+ *
+ * The `frame` and `soft` variants paint their inner card with an `::after`
+ * pseudo element instead of a wrapper node. `isolate` makes the root a stacking
+ * context, so the negative z-index pseudo paints above the root background but
+ * below the in-flow icon - no extra DOM, and `render` composition keeps working.
+ *
+ * Tone: `soft` and `solid` derive every fill and border from `currentColor`, so
+ * a single text color class (e.g. `text-success`) retints the whole tile. They
+ * default to `text-primary`; override it to recolor without touching internals.
+ */
+const iconTileVariants = cva(
+ [
+ "relative inline-flex shrink-0 items-center justify-center align-middle",
+ "size-(--icon-tile-size) rounded-(--icon-tile-radius)",
+ "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=size-])]:size-(--icon-tile-icon-size)",
+ ],
+ {
+ variants: {
+ variant: {
+ /** Plain bordered surface. The quiet default for list rows and toolbars. */
+ outline: "border border-border bg-background dark:bg-input/32",
+ /** Raised muted fill with a background-colored ring. Reads as a physical chip. */
+ elevated:
+ "border-2 border-background bg-muted text-accent-foreground shadow-[0_1px_3px_0_rgb(0_0_0/0.14)] dark:border",
+ /**
+ * Tinted double container: an opacity-filled outer ring with no border
+ * around a bordered inner card, all derived from `currentColor`. The
+ * quiet, colorful sibling of `frame`. Retint with a text color class.
+ */
+ soft: [
+ "isolate p-(--icon-tile-inset) text-primary bg-current/10",
+ "after:absolute after:-z-10 after:inset-(--icon-tile-inset)",
+ "after:rounded-[calc(var(--icon-tile-radius)-var(--icon-tile-inset))]",
+ "after:border after:border-current/20 after:bg-current/5",
+ ],
+ /** Filled tone with a contrasting glyph. Retint with `bg-*` + a text color. */
+ solid: "bg-primary text-primary-foreground",
+ /** Double container - a muted ring around an inset card, matching Frame. */
+ frame: [
+ "isolate border border-border bg-muted/50 p-(--icon-tile-inset)",
+ "after:absolute after:-z-10 after:inset-(--icon-tile-inset)",
+ "after:rounded-[calc(var(--icon-tile-radius)-var(--icon-tile-inset))]",
+ "after:border after:border-border after:bg-card after:shadow-xs",
+ ],
+ },
+ size: {
+ xs: "[--icon-tile-size:--spacing(6)] [--icon-tile-icon-size:--spacing(3.5)] [--icon-tile-inset:--spacing(0.5)]",
+ sm: "[--icon-tile-size:--spacing(8)] [--icon-tile-icon-size:--spacing(4)] [--icon-tile-inset:--spacing(0.5)]",
+ default:
+ "[--icon-tile-size:--spacing(10)] [--icon-tile-icon-size:--spacing(4.5)] [--icon-tile-inset:--spacing(0.75)]",
+ lg: "[--icon-tile-size:--spacing(12)] [--icon-tile-icon-size:--spacing(5.5)] [--icon-tile-inset:--spacing(0.75)]",
+ xl: "[--icon-tile-size:--spacing(14)] [--icon-tile-icon-size:--spacing(7)] [--icon-tile-inset:--spacing(1)]",
+ },
+ /**
+ * `default`: active style radius. `full`: circular.
+ * The plain value is the fallback outside a style scope; the `style-*`
+ * tokens win by specificity inside one, and survive the registry
+ * transform as the single resolved value per generated style.
+ *
+ * Each style token is clamped to a fraction of the tile. A flat radius
+ * is a circle once it reaches half the box, so the soft styles used to
+ * render `xs` (24px) and `sm` (32px) as plain circles and swallow the
+ * `full` variant's meaning. Clamping keeps one corner ratio at every
+ * size instead, so a tile still reads as the same shape when it scales.
+ * Luma and Rhea take the gentler quarter ratio; Maia stays at a third.
+ */
+ radius: {
+ default:
+ "[--icon-tile-radius:min(var(--radius-md),calc(var(--icon-tile-size)/3))] [--icon-tile-radius:min(var(--radius-md),calc(var(--icon-tile-size)/3))]",
+ full: "[--icon-tile-radius:calc(infinity*1px)]",
+ },
+ },
+ defaultVariants: {
+ variant: "outline",
+ size: "default",
+ radius: "default",
+ },
+ }
+)
+
+interface IconTileProps extends useRender.ComponentProps<"span"> {
+ variant?: VariantProps
["variant"]
+ size?: VariantProps["size"]
+ radius?: VariantProps["radius"]
+}
+
+function IconTile({
+ className,
+ variant = "outline",
+ size = "default",
+ radius = "default",
+ render,
+ ...props
+}: IconTileProps) {
+ const defaultProps = {
+ "data-slot": "icon-tile",
+ "data-variant": variant,
+ "data-size": size,
+ className: cn(iconTileVariants({ variant, size, radius, className })),
+ }
+
+ return useRender({
+ defaultTagName: "span",
+ render,
+ props: mergeProps<"span">(defaultProps, props),
+ })
+}
+
+export { IconTile, iconTileVariants, type IconTileProps }
\ No newline at end of file
diff --git a/apps/web/src/components/settings/appearance-settings-tab.tsx b/apps/web/src/components/settings/appearance-settings-tab.tsx
index 9bb186c..7e6502b 100644
--- a/apps/web/src/components/settings/appearance-settings-tab.tsx
+++ b/apps/web/src/components/settings/appearance-settings-tab.tsx
@@ -3,9 +3,9 @@ import { useTheme } from 'next-themes'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
-import { SettingRow } from '@/components/blocks/settings-7/components/setting-row'
-import { SettingsCard } from '@/components/blocks/settings-7/components/settings-card'
-import { SettingsFieldGroup } from '@/components/blocks/settings-7/components/settings-field-group'
+import { SettingRow } from '@/components/settings/setting-row'
+import { SettingsCard } from '@/components/settings/settings-card'
+import { SettingsFieldGroup } from '@/components/settings/settings-field-group'
import { Badge } from '@/components/reui/badge'
import {
ToggleGroup,
diff --git a/apps/web/src/components/settings/connection-settings-tab.tsx b/apps/web/src/components/settings/connection-settings-tab.tsx
index 0508fe9..9ad302c 100644
--- a/apps/web/src/components/settings/connection-settings-tab.tsx
+++ b/apps/web/src/components/settings/connection-settings-tab.tsx
@@ -4,9 +4,9 @@ import { Save } from 'lucide-react'
import { useEffect, useState } from 'react'
import { toast } from 'sonner'
-import { SettingRow } from '@/components/blocks/settings-7/components/setting-row'
-import { SettingsCard } from '@/components/blocks/settings-7/components/settings-card'
-import { SettingsFieldGroup } from '@/components/blocks/settings-7/components/settings-field-group'
+import { SettingRow } from '@/components/settings/setting-row'
+import { SettingsCard } from '@/components/settings/settings-card'
+import { SettingsFieldGroup } from '@/components/settings/settings-field-group'
import { Badge } from '@/components/reui/badge'
import { LoadingButton } from '@/components/loading-button'
import {
diff --git a/apps/web/src/components/settings/sections-settings-tab.tsx b/apps/web/src/components/settings/sections-settings-tab.tsx
index e1cb265..abf8024 100644
--- a/apps/web/src/components/settings/sections-settings-tab.tsx
+++ b/apps/web/src/components/settings/sections-settings-tab.tsx
@@ -6,7 +6,7 @@ import {
} from 'lucide-react'
import { Fragment } from 'react'
-import { SettingsCard } from '@/components/blocks/settings-7/components/settings-card'
+import { SettingsCard } from '@/components/settings/settings-card'
import { Badge } from '@/components/reui/badge'
import { Button } from '@evobgp/ui/components/button'
import {
diff --git a/apps/web/src/components/settings/session-settings-tab.tsx b/apps/web/src/components/settings/session-settings-tab.tsx
index a6c8dd7..5b10e60 100644
--- a/apps/web/src/components/settings/session-settings-tab.tsx
+++ b/apps/web/src/components/settings/session-settings-tab.tsx
@@ -1,9 +1,9 @@
import { useQuery } from '@tanstack/react-query'
import { MonitorIcon } from 'lucide-react'
-import { SettingRow } from '@/components/blocks/settings-7/components/setting-row'
-import { SettingsCard } from '@/components/blocks/settings-7/components/settings-card'
-import { SettingsFieldGroup } from '@/components/blocks/settings-7/components/settings-field-group'
+import { SettingRow } from '@/components/settings/setting-row'
+import { SettingsCard } from '@/components/settings/settings-card'
+import { SettingsFieldGroup } from '@/components/settings/settings-field-group'
import { Badge } from '@/components/reui/badge'
import { QueryState } from '@/components/query-state'
import { TOKEN_STORAGE_KEY } from '@/lib/api-client'
diff --git a/apps/web/src/components/settings/setting-row.tsx b/apps/web/src/components/settings/setting-row.tsx
new file mode 100644
index 0000000..fa79498
--- /dev/null
+++ b/apps/web/src/components/settings/setting-row.tsx
@@ -0,0 +1,63 @@
+import { type ReactNode } from 'react'
+
+import { cn } from '@evobgp/ui/lib/utils'
+import {
+ Field,
+ FieldContent,
+ FieldDescription,
+ FieldLabel,
+ FieldSeparator,
+ FieldTitle,
+} from '@evobgp/ui/components/field'
+
+interface SettingRowProps {
+ title: string
+ description?: ReactNode
+ children: ReactNode
+ last?: boolean
+ labelFor?: string
+ contentClassName?: string
+ titleAddon?: ReactNode
+}
+
+/** Settings row — settings-3 DNA. Preview: https://reui.io/preview/base/settings-3 */
+export function SettingRow({
+ title,
+ description,
+ children,
+ last,
+ labelFor,
+ contentClassName,
+ titleAddon,
+}: SettingRowProps) {
+ return (
+ <>
+
+
+
+ {labelFor ? (
+ {title}
+ ) : (
+ {title}
+ )}
+ {titleAddon}
+
+
+ {description ? (
+
{description}
+ ) : null}
+
+
+
+
+ {children}
+
+
+
+
+ {!last ? : null}
+ >
+ )
+}
diff --git a/apps/web/src/components/settings/settings-card.tsx b/apps/web/src/components/settings/settings-card.tsx
new file mode 100644
index 0000000..6b6139b
--- /dev/null
+++ b/apps/web/src/components/settings/settings-card.tsx
@@ -0,0 +1,53 @@
+import { type ReactNode } from 'react'
+
+import { cn } from '@evobgp/ui/lib/utils'
+import {
+ Frame,
+ FrameDescription,
+ FrameFooter,
+ FrameHeader,
+ FramePanel,
+ FrameTitle,
+} from '@/components/reui/frame'
+
+interface SettingsCardProps {
+ title: string
+ description?: string
+ children: ReactNode
+ footer?: ReactNode
+ className?: string
+ contentClassName?: string
+ footerClassName?: string
+}
+
+/** Settings section — Frame surface. Preview: https://reui.io/preview/base/settings-16 · https://reui.io/preview/base/settings-3 */
+export function SettingsCard({
+ title,
+ description,
+ children,
+ footer,
+ className,
+ contentClassName,
+ footerClassName,
+}: SettingsCardProps) {
+ return (
+
+
+
+ {title}
+ {description ? {description} : null}
+
+
+ {children}
+
+ {footer ? (
+
+ {footer}
+
+ ) : null}
+
+
+ )
+}
diff --git a/apps/web/src/components/settings/settings-field-group.tsx b/apps/web/src/components/settings/settings-field-group.tsx
new file mode 100644
index 0000000..11315b5
--- /dev/null
+++ b/apps/web/src/components/settings/settings-field-group.tsx
@@ -0,0 +1,36 @@
+import { type ReactNode } from 'react'
+
+import { cn } from '@evobgp/ui/lib/utils'
+import {
+ FieldDescription,
+ FieldGroup,
+ FieldLegend,
+ FieldSet,
+} from '@evobgp/ui/components/field'
+
+interface SettingsFieldGroupProps {
+ legend: string
+ description: string
+ children: ReactNode
+ className?: string
+ fieldGroupClassName?: string
+}
+
+/** Settings field group — settings-3 DNA. Preview: https://reui.io/preview/base/settings-3 */
+export function SettingsFieldGroup({
+ legend,
+ description,
+ children,
+ className,
+ fieldGroupClassName,
+}: SettingsFieldGroupProps) {
+ return (
+
+ )
+}
diff --git a/apps/web/src/lib/ui-surface.ts b/apps/web/src/lib/ui-surface.ts
index 1ef0101..fae28ee 100644
--- a/apps/web/src/lib/ui-surface.ts
+++ b/apps/web/src/lib/ui-surface.ts
@@ -12,6 +12,10 @@
*/
export const UI_SURFACE = 'frame' as const
+/** Skip link — keyboard: first Tab. */
+export const SKIP_TO_CONTENT_CLASS =
+ 'bg-background text-foreground ring-ring sr-only focus:not-sr-only focus:absolute focus:top-2 focus:left-2 focus:z-50 focus:rounded-md focus:px-3 focus:py-2 focus:text-sm focus:font-medium focus:shadow-sm focus:ring-2'
+
/** Grid for ReUI stats-12 KPI rows (3–6 tiles). */
export const kpiStatGridClassName = '@container w-full'
diff --git a/apps/web/src/routes/_auth/_settings.tsx b/apps/web/src/routes/_auth/_settings.tsx
new file mode 100644
index 0000000..3ca4cf5
--- /dev/null
+++ b/apps/web/src/routes/_auth/_settings.tsx
@@ -0,0 +1,7 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+import { SettingsShell } from '@/components/reui-kit/settings-shell'
+
+export const Route = createFileRoute('/_auth/_settings')({
+ component: SettingsShell,
+})
diff --git a/apps/web/src/routes/_auth/settings.tsx b/apps/web/src/routes/_auth/_settings/settings.tsx
similarity index 55%
rename from apps/web/src/routes/_auth/settings.tsx
rename to apps/web/src/routes/_auth/_settings/settings.tsx
index 99bbac4..f7c2cb9 100644
--- a/apps/web/src/routes/_auth/settings.tsx
+++ b/apps/web/src/routes/_auth/_settings/settings.tsx
@@ -1,11 +1,8 @@
import { createFileRoute } from '@tanstack/react-router'
import { z } from 'zod'
-import { PageHeader } from '@/components/page-header'
import { SettingsPageShell } from '@/components/settings/settings-page-shell'
-import {
- type SettingsTab,
-} from '@/components/settings/settings-tabs-data'
+import { type SettingsTab } from '@/components/settings/settings-tabs-data'
const settingsSearchSchema = z.object({
tab: z
@@ -14,7 +11,7 @@ const settingsSearchSchema = z.object({
reason: z.enum(['token-required']).optional(),
})
-export const Route = createFileRoute('/_auth/settings')({
+export const Route = createFileRoute('/_auth/_settings/settings')({
component: SettingsComponent,
validateSearch: (search) => settingsSearchSchema.parse(search),
})
@@ -35,17 +32,10 @@ function SettingsComponent() {
}
return (
-
+
)
}
diff --git a/apps/web/src/routes/_auth/tenant-settings.tsx b/apps/web/src/routes/_auth/_settings/tenant-settings.tsx
similarity index 97%
rename from apps/web/src/routes/_auth/tenant-settings.tsx
rename to apps/web/src/routes/_auth/_settings/tenant-settings.tsx
index 593611f..03c73f3 100644
--- a/apps/web/src/routes/_auth/tenant-settings.tsx
+++ b/apps/web/src/routes/_auth/_settings/tenant-settings.tsx
@@ -12,7 +12,6 @@ import { FrameDataGrid } from '@/components/reui-kit'
import { SelectField } from '@/components/select-field'
import { SettingsKvGrid } from '@/components/settings/settings-kv-grid'
import { SettingsSettingField } from '@/components/settings/settings-setting-field'
-import { PageHeader } from '@/components/page-header'
import { QueryState } from '@/components/query-state'
import { LoadingButton } from '@/components/loading-button'
@@ -28,7 +27,7 @@ import {
} from '@/queries/settings'
import { apiMutate } from '@/lib/api-client'
-export const Route = createFileRoute('/_auth/tenant-settings')({
+export const Route = createFileRoute('/_auth/_settings/tenant-settings')({
component: TenantSettingsComponent,
validateSearch: (search: Record) => ({
tab: (search.tab === 'revision' || search.tab === 'runtime-logs' || search.tab === 'additional'
@@ -74,7 +73,7 @@ const BIRD_HINTS: Partial> = {
}
function TenantSettingsComponent() {
- const search = useSearch({ from: '/_auth/tenant-settings' })
+ const search = useSearch({ from: '/_auth/_settings/tenant-settings' })
const navigate = Route.useNavigate()
const settingsQ = useQuery(settingsQueryOptions())
const qc = useQueryClient()
@@ -116,11 +115,6 @@ function TenantSettingsComponent() {
return (
-
-
diff --git a/apps/web/src/routes/_auth/modules/new.tsx b/apps/web/src/routes/_auth/modules/new.tsx
index 1c9e1fd..40a17bb 100644
--- a/apps/web/src/routes/_auth/modules/new.tsx
+++ b/apps/web/src/routes/_auth/modules/new.tsx
@@ -1,7 +1,13 @@
import { createFileRoute, Link } from '@tanstack/react-router'
-import { Card, CardContent } from '@evobgp/ui/components/card'
import { Button } from '@evobgp/ui/components/button'
import { PageHeader } from '@/components/page-header'
+import {
+ Frame,
+ FrameDescription,
+ FrameHeader,
+ FramePanel,
+ FrameTitle,
+} from '@/components/reui/frame'
export const Route = createFileRoute('/_auth/modules/new')({
component: NewModuleComponent,
@@ -14,11 +20,14 @@ function NewModuleComponent() {
title="Новый модуль"
description="Создание модуля — через API или будущая форма"
/>
-
-
-
- Форма создания модуля будет добавлена позже. Сейчас модули можно создать через API:
-
+
+
+ Создание через API
+
+ Форма в UI появится позже. Сейчас модуль можно создать запросом ниже.
+
+
+
{`POST /v1/modules
{ "type": "DOMAINS", "name": "Мой список" }`}
@@ -26,8 +35,8 @@ function NewModuleComponent() {
}>
Назад к списку
-
-
+
+
)
}
diff --git a/docs/ui-design-contract.md b/docs/ui-design-contract.md
index a0d5aad..97018b0 100644
--- a/docs/ui-design-contract.md
+++ b/docs/ui-design-contract.md
@@ -28,7 +28,7 @@ Settings: секции через Frame + `gap` (без hairline `Separator` п
| Lists | `data-grid-filtering-2` | https://reui.io/preview/base/data-grid-filtering-2 |
| Settings | `settings-16` + SettingRow | https://reui.io/preview/base/settings-16 |
| Auth | `auth-13` | https://reui.io/preview/base/auth-13 |
-| Empty | `empty-state-12` | https://reui.io/preview/base/empty-state-12 |
+| Empty | `empty-state-14` (prefer) / `empty-state-12` | https://reui.io/preview/base/empty-state-14 · https://reui.io/preview/base/empty-state-12 |
| Forms | `form-7` → Sheet/Drawer | https://reui.io/preview/base/form-7 |
## Kit API (`reui-kit/`)
@@ -75,9 +75,9 @@ Gating: DB `show_quick_actions` / `showQuickActions` / `ui_show_quick_actions` (
NavUser (footer): avatar + name/email; dropdown — Настройки / Тема (segmented) / Выйти. Preview: [app-shell-1](https://reui.io/preview/base/app-shell-1).
-App Switcher: auth-portal `GET /api/v1/app-switcher`. Ids: `cfdm` · `vps` · `bgp` · `fw`. Admin: portal `/admin/apps`.
+App Switcher: auth-portal `GET /api/v1/app-switcher`. Ids: `cfdm` · `vps` · `bgp` · `fw` · `dns`. Admin: portal `/admin/apps`.
-QuickActionGrid / KPI icons: только semantic **text** (`text-info` / `text-primary` / …) на kit `bg-muted` — без solid fills.
+QuickActionGrid / KPI icons: только semantic **text** (`text-info` / `text-primary` / …) на `IconTile` `elevated` — без solid fills.
## System monitor
diff --git a/packages/ui/src/components/sheet.tsx b/packages/ui/src/components/sheet.tsx
index 6eb7dd2..9bc286b 100644
--- a/packages/ui/src/components/sheet.tsx
+++ b/packages/ui/src/components/sheet.tsx
@@ -70,9 +70,8 @@ function SheetContent({
/>
}
>
-
- Close
+
+ Закрыть
)}
diff --git a/packages/ui/src/components/sidebar.tsx b/packages/ui/src/components/sidebar.tsx
index c044115..c8635dc 100644
--- a/packages/ui/src/components/sidebar.tsx
+++ b/packages/ui/src/components/sidebar.tsx
@@ -194,8 +194,8 @@ function Sidebar({
side={side}
>
- Sidebar
- Displays the mobile sidebar.
+ Боковое меню
+ Навигация по приложению
{children}
@@ -254,7 +254,7 @@ function SidebarTrigger({
onClick,
...props
}: React.ComponentProps) {
- const { toggleSidebar } = useSidebar()
+ const { toggleSidebar, open, openMobile, isMobile } = useSidebar()
return (
)
}
@@ -282,10 +285,10 @@ function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {