Унифицировать оболочку ops-экранов, IconTile, skip-link и контракт UI. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,200 @@
|
|||||||
|
---
|
||||||
|
name: UX UI Unification Audit
|
||||||
|
overview: "Discovery завершён. Уровень унификации (решение пользователя): идентичный chrome во всех 6 приложениях + общая UX-система; доменные страницы остаются разными. Deliverable после approval — только vps-tracker/UX-UI-PLAN.md, без правок production-кода."
|
||||||
|
todos:
|
||||||
|
- id: write-plan-file
|
||||||
|
content: Создать c:\Users\shats\Dev\vps-tracker\UX-UI-PLAN.md со всеми секциями правила аудита (без правок production-кода)
|
||||||
|
status: in_progress
|
||||||
|
- id: registry-changes
|
||||||
|
content: "Заполнить REUI Reference Registry + Final Change Matrix: каждый UI CHANGE с Ref ID и официальным URL"
|
||||||
|
status: in_progress
|
||||||
|
- id: consistency-gate
|
||||||
|
content: "Пройти Final Consistency Gate: 6/6 coverage, no fabricated URLs, functionality preserved"
|
||||||
|
status: pending
|
||||||
|
- id: chrome-spec
|
||||||
|
content: В UX-UI-PLAN.md зафиксировать Chrome Identity Spec для всех 6 приложений (header/sidebar/theme/App Switcher) как обязательный 1:1
|
||||||
|
status: in_progress
|
||||||
|
isProject: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# UX/UI Unification — Audit Complete, Plan File Next
|
||||||
|
|
||||||
|
## Scope of this phase
|
||||||
|
|
||||||
|
После подтверждения плана будет создан **только** [`c:\Users\shats\Dev\vps-tracker\UX-UI-PLAN.md`](c:/Users/shats/Dev/vps-tracker/UX-UI-PLAN.md) (выбрано пользователем). Production/source code, зависимости, CSS и компоненты **не изменяются**. Implementation UI — отдельный запрос.
|
||||||
|
|
||||||
|
Источники истины для references: [reui.io/docs](https://reui.io/docs), [reui.io/llms.txt](https://reui.io/llms.txt), MCP `user-reui` (`get_block` / `list_components` / `compose_page` / `get_component`). Skill ReUI локально и в MCP: `668fb463eb` (совпадает). Версию ReUI PRO **не** указываем — её нет.
|
||||||
|
|
||||||
|
## Coverage gate
|
||||||
|
|
||||||
|
`Discovered: 6` · `Audited: 6` · `Planned: 6` · `Coverage: 100%`
|
||||||
|
|
||||||
|
| # | Project | Path | Type | UI |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 1 | VPS Tracker | `c:\Users\shats\Dev\vps-tracker` | fullstack Vite SPA | Frame + kit, mixed Card on charts/detail |
|
||||||
|
| 2 | Cloudflare Domain Manager | `c:\Users\shats\Dev\cloudflare-domain-manager` | fullstack Vite SPA | Frame-first, closest to contract |
|
||||||
|
| 3 | EvoBGP | `c:\Users\shats\Dev\EvoBGP` | fullstack (Go API + Vite SPA) | SoT hybrid KPI; leftover Card on stub/empty |
|
||||||
|
| 4 | Auth Portal | `c:\Users\shats\Dev\auth-portal` | fullstack Vite SPA | Frame admin; thin kit; no SystemMonitor |
|
||||||
|
| 5 | EvoFirewall | `c:\Users\shats\Dev\EvoFirewall` | fullstack Vite SPA | Frame + kit; SettingsShell unused |
|
||||||
|
| 6 | MikrotikManager | `c:\Users\shats\Dev\MikrotikManager-3` | fullstack Next.js 16 | **Card SoT `/servers`**; ReUI partial (data-grid/filters/stepper), **нет** `reui-kit` |
|
||||||
|
|
||||||
|
Backend-only проектов нет. Все шесть — frontend/fullstack UI.
|
||||||
|
|
||||||
|
## What the final file will contain
|
||||||
|
|
||||||
|
Полная структура из правила аудита (§37): Executive Summary, Coverage, Inventory, MCP/Skills, Rules, 6 project audits, Cross-project matrix, Duplicates, REUI Registry, Target rules, CHANGE-XXX, Roadmap, Acceptance, Risks, Consistency Gate.
|
||||||
|
|
||||||
|
Ниже — ядро находок, которые попадут в файл с evidence и Ref ID.
|
||||||
|
|
||||||
|
## Что будет одинаковым, а что нет (решение пользователя)
|
||||||
|
|
||||||
|
Уровень унификации: **система + идентичный chrome во всех 6 приложениях**. Доменные экраны **не** копируются.
|
||||||
|
|
||||||
|
### Будет одинаковым (chrome identity — все 6, включая Auth Portal и MikrotikManager)
|
||||||
|
|
||||||
|
Эталон: [app-shell-12](https://reui.io/preview/base/app-shell-12) + [app-shell-7](https://reui.io/preview/base/app-shell-7) (SystemMonitor) + [app-shell-1](https://reui.io/preview/base/app-shell-1) (NavUser).
|
||||||
|
|
||||||
|
- Sidebar `--sidebar-width: 240px`
|
||||||
|
- Sidebar header: App Switcher (ids `cfdm` · `vps` · `bgp` · `fw` · `dns`, источник — auth-portal API)
|
||||||
|
- Sidebar footer: **NavUser** + segmented theme (light / dark / system), без Sun/Moon icon toggle
|
||||||
|
- Header right: **AppsMenu → SystemMonitorPopover**; без ModeToggle и без Search-кнопки (поиск только ⌘K)
|
||||||
|
- Main padding: `gap-4 md:gap-6`, `px-4 py-4 md:px-6 md:py-5`
|
||||||
|
- Тема: next-themes (или эквивалент с тем же UX) + ReUI semantic tokens (`--success/--warning/--info/--invert`)
|
||||||
|
- SSO/portal: вход через auth-portal, тот же App Switcher
|
||||||
|
|
||||||
|
Сейчас этого нет: Auth Portal (нет SystemMonitor), MikrotikManager (нет AppsMenu/NavUser/switcher, icon theme toggle).
|
||||||
|
|
||||||
|
### Будет одним паттерном (не pixel-copy страниц)
|
||||||
|
|
||||||
|
- Surface ops-экрана: Frame, не Card
|
||||||
|
- Списки: DataGrid + Filters
|
||||||
|
- KPI: stats-12 + IconTile `elevated`
|
||||||
|
- Quick Actions: Frame tiles (card-12 DNA)
|
||||||
|
- Settings rows: settings-3 / integrations: settings-16
|
||||||
|
- Create/edit: Sheet (sheet-8 / sheet-1 / sheet-9)
|
||||||
|
- Empty: Empty + IconStack, Frame-friendly
|
||||||
|
|
||||||
|
Одинаковый **рецепт**, разные данные, колонки, фильтры, действия.
|
||||||
|
|
||||||
|
### Останется разным (домен)
|
||||||
|
|
||||||
|
- VPS: topology canvas, BILLmanager sync, тарифы, баланс
|
||||||
|
- CFDM: DNS, kanban групп/сервисов, сертификаты Cloudflare
|
||||||
|
- EvoBGP: modules/lookup/BIRD jobs/network
|
||||||
|
- Auth Portal: login, users/OIDC/audit (это IdP, не ops CRUD)
|
||||||
|
- EvoFirewall: agent fleet cards, policy sortable, blocklists
|
||||||
|
- MikrotikManager: `/servers` CRUD, terminal, network-map, контейнеры — **функциональность** та же, chrome и surface выравниваются
|
||||||
|
- Framework: Vite SPA vs Next.js 16 — **не** меняем ради UI
|
||||||
|
- Package scopes (`@cfdm/ui`, `@evobgp/ui`, …) — не сливать в один npm без отдельного technical решения
|
||||||
|
|
||||||
|
## Critical cross-project findings
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
subgraph viteFamily [Vite plus Frame kit]
|
||||||
|
CFDM[CFDM]
|
||||||
|
VPS[VPS Tracker]
|
||||||
|
BGP[EvoBGP SoT KPI]
|
||||||
|
FW[EvoFirewall]
|
||||||
|
Auth[Auth Portal]
|
||||||
|
end
|
||||||
|
subgraph nextFamily [Next Card SoT]
|
||||||
|
MM[MikrotikManager]
|
||||||
|
end
|
||||||
|
ReUI[REUI PRO Frame]
|
||||||
|
viteFamily --> ReUI
|
||||||
|
MM -.->|"proposed RULE-CHANGE"| ReUI
|
||||||
|
```
|
||||||
|
|
||||||
|
1. **Две дизайн-системы в одном workspace.** Пять Vite-приложений уже lock `surface: frame` + `reui-kit`. MikrotikManager — Next App Router, SoT [`/servers`](c:/Users/shats/Dev/MikrotikManager-3/app/(main)/servers/page.tsx) на `DataPageCard` → shadcn Card. Правила `next-shadcn-production.mdc` / `ma-ui-guardian.mdc` **противоречат** цели унификации. UX/UI rule разрешено менять: target = Frame + DataGrid, **без** смены Next.js (technical constraint).
|
||||||
|
|
||||||
|
2. **KPI DNA устарела относительно официального primitive.** Сейчас kit использует hand-roll `Item` `size-10.5` `bg-muted` и ссылается на [stats-12](https://reui.io/preview/base/stats-12) (MCP: Frame + Badge + Item). Официальный primitive [Icon Tile](https://reui.io/docs/components/base/icon-tile) (`variant="elevated"`, preview [icon-tile](https://reui.io/components/icon-tile)) закрывает ту же задачу. **Ни один** из 6 проектов не установил `icon-tile`. Match текущий KPI → stats-12: **partial**. Target: IconTile elevated + stats-12 composition.
|
||||||
|
|
||||||
|
3. **settings-7 — Card surface.** EvoBGP kit/reference [`settings-7`](https://reui.io/preview/base/settings-7) официально `surface: card`. Это нарушает Frame lock. Frame-эталоны: [settings-3](https://reui.io/preview/base/settings-3), [settings-8](https://reui.io/preview/base/settings-8), [settings-16](https://reui.io/preview/base/settings-16).
|
||||||
|
|
||||||
|
4. **Chrome drift.** Контракты цитируют [app-shell-12](https://reui.io/preview/base/app-shell-12). MCP `compose_page` для ops dashboard вернул **app-shell-7** ([preview](https://reui.io/preview/base/app-shell-7)) — в описании есть system monitor popover, ближе к реальному `SystemMonitorPopover`. Auth Portal: AppsMenu без SystemMonitor. MikrotikManager: нет AppsMenu / NavUser / auth-portal switcher; theme = icon toggle в sidebar.
|
||||||
|
|
||||||
|
5. **Дубли kit.** `KpiStatGrid` / `QuickActionGrid` / `ResourcePage` / `OpsDashboard` скопированы в 5 репо (`@cfdm/ui` vs `@evobgp/ui` vs `@evofw/ui` vs `@authportal/ui`). Нет shared package. Unification UX — через одинаковые rules + DNA; общий npm-пакет = отдельный technical risk (не ломаем package boundaries в этом плане).
|
||||||
|
|
||||||
|
6. **Мёртвый / недоиспользованный kit.** SettingsShell и DetailPanel экспортируются, но не используются в EvoBGP routes и частично в EvoFirewall. Auth Portal `ResourcePage` — stub. CFDM ближе всех к контракту (Card не импортируется в `apps/web`).
|
||||||
|
|
||||||
|
7. **Rules drift.** Правила перечисляют ReUI-компоненты, которых нет на диске (kanban в vps-tracker, stepper unused в FW, Frame unused в MM). `kpi-hybrid.mdc` дублируется 5 раз. CFDM нет `AGENTS.md` / `ui-surface.ts`. Auth contract ids без `dns`, код включает `dns`.
|
||||||
|
|
||||||
|
## Verified REUI PRO registry (will expand in file)
|
||||||
|
|
||||||
|
Каждый CHANGE получит Ref ID. Уже подтверждено MCP `get_block` / `list_components` / official docs:
|
||||||
|
|
||||||
|
| Ref | Item | Official docs | Preview | Typical match |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| REUI-001 | Frame | [docs/components/base/frame](https://reui.io/docs/components/base/frame) | [components/frame](https://reui.io/components/frame) | 1:1 for ops surface |
|
||||||
|
| REUI-002 | Data Grid | [docs/components/base/data-grid](https://reui.io/docs/components/base/data-grid) | [components/data-grid](https://reui.io/components/data-grid) | 1:1 lists; MCP: TanStack v9 |
|
||||||
|
| REUI-003 | Filters | [docs/components/base/filters](https://reui.io/docs/components/base/filters) | [components/filters](https://reui.io/components/filters) | 1:1 |
|
||||||
|
| REUI-004 | stats-12 | [blocks](https://reui.io/blocks) | [preview/base/stats-12](https://reui.io/preview/base/stats-12) | partial vs current Item hybrid |
|
||||||
|
| REUI-005 | Icon Tile | [docs/components/base/icon-tile](https://reui.io/docs/components/base/icon-tile) | [components/icon-tile](https://reui.io/components/icon-tile) | target for KPI/row tiles |
|
||||||
|
| REUI-006 | data-grid-filtering-2 | [blocks](https://reui.io/blocks) | [preview/base/data-grid-filtering-2](https://reui.io/preview/base/data-grid-filtering-2) | partial (kit ResourcePage) |
|
||||||
|
| REUI-007 | settings-16 | [blocks/application/settings](https://reui.io/blocks/application/settings) | [preview/base/settings-16](https://reui.io/preview/base/settings-16) | integrations |
|
||||||
|
| REUI-008 | settings-3 | same category | [preview/base/settings-3](https://reui.io/preview/base/settings-3) | settings rows Frame |
|
||||||
|
| REUI-009 | settings-8 | same | [preview/base/settings-8](https://reui.io/preview/base/settings-8) | webhook-like lists |
|
||||||
|
| REUI-010 | app-shell-12 | [blocks](https://reui.io/blocks) | [preview/base/app-shell-12](https://reui.io/preview/base/app-shell-12) | current chrome cite |
|
||||||
|
| REUI-011 | app-shell-7 | [blocks](https://reui.io/blocks) | [preview/base/app-shell-7](https://reui.io/preview/base/app-shell-7) | better SystemMonitor match |
|
||||||
|
| REUI-012 | app-shell-1 | [blocks](https://reui.io/blocks) | [preview/base/app-shell-1](https://reui.io/preview/base/app-shell-1) | NavUser |
|
||||||
|
| REUI-013 | card-12 | [blocks](https://reui.io/blocks) | [preview/base/card-12](https://reui.io/preview/base/card-12) | Quick Actions |
|
||||||
|
| REUI-014 | dashboard-1 | [blocks](https://reui.io/blocks) | [preview/base/dashboard-1](https://reui.io/preview/base/dashboard-1) | OpsDashboard |
|
||||||
|
| REUI-015 | empty-state-14 | [blocks](https://reui.io/blocks) | [preview/base/empty-state-14](https://reui.io/preview/base/empty-state-14) | Frame empty (prefer over card empty-state-11) |
|
||||||
|
| REUI-016 | empty-state-12 | [blocks](https://reui.io/blocks) | [preview/base/empty-state-12](https://reui.io/preview/base/empty-state-12) | current cite; surface none |
|
||||||
|
| REUI-017 | sheet-8 / sheet-1 / sheet-9 | [blocks](https://reui.io/blocks) | [sheet-8](https://reui.io/preview/base/sheet-8) [sheet-1](https://reui.io/preview/base/sheet-1) [sheet-9](https://reui.io/preview/base/sheet-9) | create/edit/detail sheets |
|
||||||
|
| REUI-018 | auth-13 | [blocks](https://reui.io/blocks) | [preview/base/auth-13](https://reui.io/preview/base/auth-13) | Frame login |
|
||||||
|
| REUI-019 | auth-18 | [blocks](https://reui.io/blocks) | [preview/base/auth-18](https://reui.io/preview/base/auth-18) | auth-portal actual; surface none |
|
||||||
|
| REUI-020 | solution-users-1 / 6 | [blocks](https://reui.io/blocks) | [users-1](https://reui.io/preview/base/solution-users-1) [users-6](https://reui.io/preview/base/solution-users-6) | auth admin |
|
||||||
|
| REUI-021 | form-7 | [blocks](https://reui.io/blocks) | [preview/base/form-7](https://reui.io/preview/base/form-7) | inline detail Frame |
|
||||||
|
| REUI-022 | chart-15 | [blocks](https://reui.io/blocks) | [preview/base/chart-15](https://reui.io/preview/base/chart-15) | Frame charts (replace Card chart shells) |
|
||||||
|
| REUI-023 | kanban / sortable / stepper / timeline / autocomplete | primitive docs under `/docs/components/base/<name>` | matching `/components/<name>` | domain-specific |
|
||||||
|
|
||||||
|
`settings-7` **не** будет target (Card). `empty-state-11` **не** будет target (Card).
|
||||||
|
|
||||||
|
## Target UX/UI rules (summary)
|
||||||
|
|
||||||
|
- **TARGET-001:** Один surface на ops-экран: ReUI Frame. Не смешивать Card+Frame.
|
||||||
|
- **TARGET-002:** Списки = DataGrid + Filters внутри Frame ([data-grid-filtering-2](https://reui.io/preview/base/data-grid-filtering-2)). Запрет raw `<table>` / shadcn Table как ops list.
|
||||||
|
- **TARGET-003:** KPI = stats-12 composition + IconTile `elevated` (не hand-roll Item tile). Semantic `text-*` / Badge variants, не `bg-emerald-*`.
|
||||||
|
- **TARGET-004:** Quick Actions = Frame tiles sibling KPI ([card-12](https://reui.io/preview/base/card-12) + stats-12 DNA).
|
||||||
|
- **TARGET-005:** Settings rows = settings-3; integrations = settings-16; не settings-7.
|
||||||
|
- **TARGET-006 (P0, все 6 приложений):** Chrome identity 1:1. Sidebar 240px + App Switcher; header AppsMenu → SystemMonitor; footer NavUser segmented theme. Эталон: [app-shell-12](https://reui.io/preview/base/app-shell-12) + [app-shell-7](https://reui.io/preview/base/app-shell-7) + [app-shell-1](https://reui.io/preview/base/app-shell-1). Auth Portal и MikrotikManager **не** исключение.
|
||||||
|
- **TARGET-007:** Create/edit = Sheet blocks (sheet-8 create/settings, sheet-1 create, sheet-9 detail). Confirm = Alert Dialog examples ([alert-dialog](https://reui.io/components/alert-dialog)).
|
||||||
|
- **TARGET-008:** Empty = Empty + IconStack, Frame-friendly empty-state-14 / empty-state-12; не Card empty-state-11.
|
||||||
|
- **TARGET-009:** MikrotikManager UX SoT сменить с Card `/servers` на Frame+DataGrid **при сохранении** всего CRUD `/servers` (technical: остаётся Next.js).
|
||||||
|
- **TARGET-010:** Rules синхронизировать с диском (не перечислять неустановленные компоненты как «установленные»).
|
||||||
|
|
||||||
|
## Change themes per project (detail in file)
|
||||||
|
|
||||||
|
**VPS Tracker (P1):** Card shells на `vps.$vpsId`, `reports`, `resources`, `renewals`, charts → Frame / chart-15. `spaces.tsx` shadcn Table → ResourcePage/DataGrid. SettingsShell Separator. Dashboard QA order — зафиксировать как exception или выровнять с EvoBGP.
|
||||||
|
|
||||||
|
**CFDM (P2):** Наименьший gap. Добавить `ui-surface.ts` / AGENTS.md. IconTile вместо Item tiles. Удалить naming `*KpiCards`.
|
||||||
|
|
||||||
|
**EvoBGP (P1):** `modules/new.tsx` Card stub → Frame. `illustrated-empty-state` Card → empty-state-14. Подключить SettingsShell/DetailPanel или удалить из barrel. Forms: RHF в package.json, в src не используется — выровнять на Field + sheet-8.
|
||||||
|
|
||||||
|
**Auth Portal (P0 chrome / P1 pages):** Добавить SystemMonitorPopover в header (сейчас только AppsMenu). NavUser уже segmented — оставить. `/apps` tiles → QuickActionGrid. ResourcePage stub. Contract + `dns` id. Login-страница **без** AppShell остаётся отдельным auth surface ([auth-18](https://reui.io/preview/base/auth-18) / [auth-13](https://reui.io/preview/base/auth-13)); chrome identity применяется только к `_auth` shell.
|
||||||
|
|
||||||
|
**EvoFirewall (P1/P2):** `/settings` → SettingsShell или явно REPLACE rule. Agent cards view оставить как Frame domain, не Card. IconTile для AgentPlatformIcon. Orphans: lifecycle timeline, FormFieldSimple, unused stepper.
|
||||||
|
|
||||||
|
**MikrotikManager (P0 chrome, P1 surface, high risk):** Сначала chrome identity: AppsMenu, SystemMonitor, NavUser segmented, App Switcher `dns`/`fw`/…, `--sidebar-width: 240px` (сейчас hand-rolled footer + Sun/Moon). Затем RULE-CHANGE SoT: `DataPageCard` Card → Frame. KPI → stats-12 + IconTile. Не мигрировать Next.js. Сохранить весь CRUD `/servers` и остальные страницы.
|
||||||
|
|
||||||
|
## Roadmap (in file, not executed now)
|
||||||
|
|
||||||
|
- Phase 0: этот план-файл (единственный deliverable сейчас)
|
||||||
|
- Phase 1: **Chrome identity во всех 6** (Auth SystemMonitor; MM AppsMenu/NavUser/Switcher; Vite — сверка 1:1)
|
||||||
|
- Phase 2: shared rules + IconTile + Frame lock (Vite + начало MM)
|
||||||
|
- Phase 3: kit DNA (KPI/QA/ResourcePage) per repo
|
||||||
|
- Phase 4: page-level Card→Frame (VPS charts/detail, BGP stub/empty, MM DataPageCard)
|
||||||
|
- Phase 5: states (empty/loading/error) + sheets
|
||||||
|
- Phase 6: a11y/responsive verification
|
||||||
|
|
||||||
|
## Constraints to preserve
|
||||||
|
|
||||||
|
Не трогать: API contracts, auth/SSO/OIDC, permissions, TanStack vs Next routing, Go vs Fastify, package names, CRUD поведения (create/edit/delete/sync/filter/sort/pagination).
|
||||||
|
|
||||||
|
Technical dependency (отдельная секция Risks): Data Grid docs говорят TanStack Table **v9**; установленный `@reui/data-grid` в проектах может быть v8 — не ломать kit без явной миграции (`<uncertain>` до проверки `package.json` каждого web-приложения в тексте плана).
|
||||||
|
|
||||||
|
## After approval
|
||||||
|
|
||||||
|
Написать полный `UX-UI-PLAN.md` (~все секции правила, CHANGE-XXX с Ref ID и прямыми URL). Остановиться. Implementation — только по отдельному запросу.
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
---
|
||||||
|
name: UX UI Unification Implementation
|
||||||
|
overview: "Реализация UX-UI-PLAN.md: идентичный chrome 1:1 во всех 6 приложениях, затем IconTile/rules, kit DNA, Card→Frame. Phase 0 (аудит) уже в UX-UI-PLAN.md. Production-код начинается с Phase 1."
|
||||||
|
todos:
|
||||||
|
- id: phase-1-auth
|
||||||
|
content: "CHANGE-001: Auth Portal SystemMonitorPopover + header AppsMenu→monitor (app-shell-7)"
|
||||||
|
status: completed
|
||||||
|
- id: phase-1-mm
|
||||||
|
content: "CHANGE-002/032/033/034: MM chrome — 240px, App Switcher, NavUser segmented, AppsMenu+SystemMonitor, без Rail/Search pill"
|
||||||
|
status: completed
|
||||||
|
- id: phase-1-vite
|
||||||
|
content: "CHANGE-003: сверка VPS/CFDM/EvoBGP/FW + Auth SIDEBAR_WIDTH 240px"
|
||||||
|
status: completed
|
||||||
|
- id: phase-2-icontile
|
||||||
|
content: "Phase 2: CLI @reui/icon-tile во всех 6 + RULE-CHANGE Frame SoT MM (CHANGE-004, 024–028)"
|
||||||
|
status: completed
|
||||||
|
- id: phase-3-kit
|
||||||
|
content: "Phase 3: KpiStatGrid/QuickActionGrid на IconTile; Auth /apps kit (CHANGE-005–008, 021–022)"
|
||||||
|
status: completed
|
||||||
|
- id: phase-4-frame
|
||||||
|
content: "Phase 4: Card→Frame на ops-страницах VPS/EvoBGP/MM (CHANGE-009–018)"
|
||||||
|
status: completed
|
||||||
|
- id: phase-5-6
|
||||||
|
content: "Phase 6: a11y/responsive verification (Phase 5 CHANGE-016/019/020/030/031 done)"
|
||||||
|
status: completed
|
||||||
|
isProject: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# UX/UI Unification — Implementation
|
||||||
|
|
||||||
|
Источник: [`UX-UI-PLAN.md`](c:/Users/shats/Dev/vps-tracker/UX-UI-PLAN.md). Аудит-план [`ux_ui_unification_audit_276a34f8.plan.md`](c:/Users/shats/Dev/vps-tracker/.cursor/plans/ux_ui_unification_audit_276a34f8.plan.md) **не менять**.
|
||||||
|
|
||||||
|
Уровень: **идентичный chrome во всех 6** + общая UX-система; доменные страницы остаются разными.
|
||||||
|
|
||||||
|
Эталоны chrome: [app-shell-12](https://reui.io/preview/base/app-shell-12) · [app-shell-7](https://reui.io/preview/base/app-shell-7) · [app-shell-1](https://reui.io/preview/base/app-shell-1). Docs: [reui.io/blocks](https://reui.io/blocks) · [reui.io/docs](https://reui.io/docs).
|
||||||
|
|
||||||
|
## Chrome Identity Spec (1:1 — все 6)
|
||||||
|
|
||||||
|
- Sidebar `--sidebar-width: 240px`
|
||||||
|
- Sidebar header: App Switcher (`cfdm` · `vps` · `bgp` · `fw` · `dns`; MM локальный `mm` + те же ids)
|
||||||
|
- Footer: NavUser + segmented theme (light/dark/system), без Sun/Moon
|
||||||
|
- Header right: AppsMenu → SystemMonitorPopover; без ModeToggle; поиск только ⌘K
|
||||||
|
- Main: `gap-4 md:gap-6`, `px-4 py-4 md:px-6 md:py-5`
|
||||||
|
- Запрещено: SidebarRail, Search pill в header/sidebar
|
||||||
|
- Auth **login** без AppShell ([auth-18](https://reui.io/preview/base/auth-18))
|
||||||
|
|
||||||
|
**Не менять:** Vite vs Next, package scopes, API/SSO/OIDC, CRUD. Не мержить kit в один npm. Не мигрировать TanStack Table v8→v9 в этом плане.
|
||||||
|
|
||||||
|
## Phase 1 — Chrome (сейчас)
|
||||||
|
|
||||||
|
| ID | Project | Что сделать |
|
||||||
|
|---|---|---|
|
||||||
|
| CHANGE-001 | Auth Portal | `SystemMonitorPopover` после AppsMenu; header `z-10` `gap-2` |
|
||||||
|
| CHANGE-002 | MM | Switcher, header cluster, убрать Search pill + Sun/Moon |
|
||||||
|
| CHANGE-032 | MM | NavUser в footer, segmented theme |
|
||||||
|
| CHANGE-033 | MM | удалить SidebarRail |
|
||||||
|
| CHANGE-034 | MM | `SIDEBAR_WIDTH = 240px` |
|
||||||
|
| CHANGE-003 | VPS/CFDM/BGP/FW + Auth primitive | сверка; Auth `packages/ui` sidebar 16rem → 240px |
|
||||||
|
|
||||||
|
MM остаётся Next.js 16. Без `@tanstack/react-query` для chrome (fetch + `useEffect`).
|
||||||
|
|
||||||
|
## Later phases (не в этом проходе)
|
||||||
|
|
||||||
|
- **Phase 2:** IconTile CLI, rules Frame SoT MM
|
||||||
|
- **Phase 3:** kit DNA KPI/QA
|
||||||
|
- **Phase 4:** Card→Frame pages
|
||||||
|
- **Phase 5–6:** empty/sheets, a11y
|
||||||
@@ -36,7 +36,6 @@ Monorepo — [`frontend-monorepo.mdc`](frontend-monorepo.mdc). ReUI — [`reui-m
|
|||||||
| `PageShell` | `page-shell.tsx` |
|
| `PageShell` | `page-shell.tsx` |
|
||||||
| `ResourcePage` | `reui-kit/resource-page.tsx` |
|
| `ResourcePage` | `reui-kit/resource-page.tsx` |
|
||||||
| `OpsDashboard` / `KpiStatGrid` / `QuickActionGrid` | `reui-kit/ops-dashboard.tsx`, `kpi-stat-grid.tsx`, `quick-action-grid.tsx` |
|
| `OpsDashboard` / `KpiStatGrid` / `QuickActionGrid` | `reui-kit/ops-dashboard.tsx`, `kpi-stat-grid.tsx`, `quick-action-grid.tsx` |
|
||||||
| `KanbanBoard` | `reui-kit/kanban-board.tsx` |
|
|
||||||
| `DetailPanel` | `reui-kit/detail-panel.tsx` |
|
| `DetailPanel` | `reui-kit/detail-panel.tsx` |
|
||||||
| `SettingsShell` | `reui-kit/settings-shell.tsx` |
|
| `SettingsShell` | `reui-kit/settings-shell.tsx` |
|
||||||
| `EmptyState` | `empty-state.tsx` |
|
| `EmptyState` | `empty-state.tsx` |
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ apps/web/src/components/ ← shared + domain + layout
|
|||||||
page-shell.tsx
|
page-shell.tsx
|
||||||
reui-kit/
|
reui-kit/
|
||||||
resource-page.tsx ← list: Frame + line Tabs + Filters + DataGrid
|
resource-page.tsx ← list: Frame + line Tabs + Filters + DataGrid
|
||||||
kanban-board.tsx ← kanban + KanbanBoardSkeleton
|
|
||||||
detail-panel.tsx ← detail: Frame header/metrics
|
detail-panel.tsx ← detail: Frame header/metrics
|
||||||
settings-shell.tsx
|
settings-shell.tsx
|
||||||
ops-dashboard.tsx ← KPI stats-12 + charts
|
ops-dashboard.tsx ← KPI stats-12 + charts
|
||||||
@@ -70,8 +69,6 @@ apps/web/src/components/ ← shared + domain + layout
|
|||||||
|---------|--------|-----------|
|
|---------|--------|-----------|
|
||||||
| Page wrapper | `PageShell` | — |
|
| Page wrapper | `PageShell` | — |
|
||||||
| List page | `ResourcePage` | ReUI `Frame` + `data-grid` + `filters` + shadcn `Tabs` `variant="line"` |
|
| List page | `ResourcePage` | ReUI `Frame` + `data-grid` + `filters` + shadcn `Tabs` `variant="line"` |
|
||||||
| Catalog / Board | `CatalogBoardToggle` + `ResourcePage` / `KanbanBoard` | `?view=board` на `/groups`, `/services` |
|
|
||||||
| Kanban | `KanbanBoard` / `KanbanBoardSkeleton` | ReUI `kanban` + `Frame` |
|
|
||||||
| Detail | `DetailPanel` | ReUI `Frame` |
|
| Detail | `DetailPanel` | ReUI `Frame` |
|
||||||
| Settings | `SettingsShell` | — |
|
| Settings | `SettingsShell` | — |
|
||||||
| Dashboard KPI | `OpsDashboard` / `KpiStatGrid` | ReUI Frame [stats-12](https://reui.io/preview/base/stats-12) hybrid |
|
| Dashboard KPI | `OpsDashboard` / `KpiStatGrid` | ReUI Frame [stats-12](https://reui.io/preview/base/stats-12) hybrid |
|
||||||
@@ -95,7 +92,7 @@ apps/web/src/components/ ← shared + domain + layout
|
|||||||
|
|
||||||
Max 1 primary (`default`) на экран; остальные `outline` / `ghost`.
|
Max 1 primary (`default`) на экран; остальные `outline` / `ghost`.
|
||||||
|
|
||||||
Toggle «Доска» / «К каталогу» — всегда `outline` в `primaryAction` / `KanbanBoard.toolbarActions` (не отдельный Frame-shell).
|
Max 1 primary (`default`) на экран; остальные `outline` / `ghost`.
|
||||||
|
|
||||||
## Line tabs (project standard)
|
## Line tabs (project standard)
|
||||||
|
|
||||||
@@ -116,17 +113,6 @@ Toggle «Доска» / «К каталогу» — всегда `outline` в `p
|
|||||||
- Active state — Base UI `data-active`, не Radix `data-[state=active]`
|
- Active state — Base UI `data-active`, не Radix `data-[state=active]`
|
||||||
- Не трогать internals `reui/date-selector`
|
- Не трогать internals `reui/date-selector`
|
||||||
|
|
||||||
## Catalog / Board (`/groups`, `/services`)
|
|
||||||
|
|
||||||
| Режим | UI | Search |
|
|
||||||
|-------|-----|--------|
|
|
||||||
| Catalog (default) | `ResourcePage` + primary create | omit / `view=catalog` |
|
|
||||||
| Board | `KanbanBoard` DnD | `?view=board` |
|
|
||||||
|
|
||||||
- Groups tabs: Все / С доменами / Пустые
|
|
||||||
- Services tabs: Все / Включены / Выключены / Без группы
|
|
||||||
- DnD только на board; kanban hooks/cards не удалять
|
|
||||||
|
|
||||||
## Dashboard KPI
|
## Dashboard KPI
|
||||||
|
|
||||||
Эталон: [stats-12](https://reui.io/preview/base/stats-12) — icon tile + value + label + optional badge/footer; клик через `to` (`Link`) / `onSelect`. Compact strip: [card-35](https://reui.io/preview/base/card-35).
|
Эталон: [stats-12](https://reui.io/preview/base/stats-12) — icon tile + value + label + optional badge/footer; клик через `to` (`Link`) / `onSelect`. Compact strip: [card-35](https://reui.io/preview/base/card-35).
|
||||||
@@ -178,7 +164,7 @@ Shared chrome (vps-tracker / CFDM / EvoBGP): см. [`docs/ui-design-contract.md`
|
|||||||
|
|
||||||
Каждый блок: **default, hover, focus, disabled, empty, loading, error**.
|
Каждый блок: **default, hover, focus, disabled, empty, loading, error**.
|
||||||
|
|
||||||
- **Loading** — `Skeleton` / `ResourcePage` skeleton / `KanbanBoardSkeleton` / `OpsDashboard` skeleton, не Spinner на странице
|
- **Loading** — `Skeleton` / `ResourcePage` skeleton / `OpsDashboard` skeleton, не Spinner на странице
|
||||||
- **Empty** — `EmptyState` с CTA
|
- **Empty** — `EmptyState` с CTA
|
||||||
- **Zero-results** — message внутри DataGrid (+ «Сбросить»)
|
- **Zero-results** — message внутри DataGrid (+ «Сбросить»)
|
||||||
- **Error** — `QueryState` / `Alert` + `onRetry`
|
- **Error** — `QueryState` / `Alert` + `onRetry`
|
||||||
|
|||||||
@@ -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
|
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), [`frontend-ui-patterns.mdc`](frontend-ui-patterns.mdc).
|
Связанные: [`reui-mcp.mdc`](reui-mcp.mdc), [`frontend-ui-patterns.mdc`](frontend-ui-patterns.mdc).
|
||||||
|
|
||||||
@@ -14,16 +14,17 @@ Preview: [stats-12](https://reui.io/preview/base/stats-12). SoT DNA = EvoBGP. Ma
|
|||||||
| Зона | Компонент / DNA |
|
| Зона | Компонент / DNA |
|
||||||
|------|-----------------|
|
|------|-----------------|
|
||||||
| KPI-полосы / dashboard metrics | только `reui-kit/KpiStatGrid` (через `OpsDashboard` / `DetailPanel.Metrics` при наличии) |
|
| 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` |
|
| 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-*` на `bg-muted` |
|
| Row icon tiles (data-grid) | та же DNA — semantic `text-*` на IconTile elevated |
|
||||||
| Quick Actions | только `reui-kit/QuickActionGrid` (sibling hybrid DNA) |
|
| Quick Actions | только `reui-kit/QuickActionGrid` (sibling hybrid DNA, IconTile elevated) |
|
||||||
|
|
||||||
Импорты UI: `@cfdm/ui/components/*`.
|
Импорты UI: `@cfdm/ui/components/*`. IconTile: `@/components/reui/icon-tile`.
|
||||||
|
|
||||||
## NEVER
|
## NEVER
|
||||||
|
|
||||||
- SectionCards / vertical-only KPI / hand-roll Frame/Card KPI
|
- 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
|
- `card-35` как замена stats-12 hybrid KPI
|
||||||
- Копипаст ReUI block в route — adapt через `reui-kit/`
|
- Копипаст ReUI block в route — adapt через `reui-kit/`
|
||||||
- Голый lucide `size-4` в name-cell без hybrid tile
|
- Голый lucide `size-4` в name-cell без IconTile
|
||||||
|
|||||||
@@ -76,9 +76,9 @@ alwaysApply: true
|
|||||||
|
|
||||||
## Установленные ReUI (apps/web)
|
## Установленные 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`
|
||||||
|
|
||||||
**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`, …
|
**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`, …
|
||||||
|
|
||||||
|
|||||||
+945
@@ -0,0 +1,945 @@
|
|||||||
|
# UX/UI Unification Plan
|
||||||
|
|
||||||
|
Дата аудита: 2026-08-17
|
||||||
|
Implementation: **DONE 2026-08-17** — CHANGE-001…034, Phases 1–6, Acceptance §19.
|
||||||
|
Уровень унификации (решение пользователя): **система + идентичный chrome во всех 6 приложениях**; доменные экраны **не** копируются.
|
||||||
|
|
||||||
|
Источники истины REUI PRO: [reui.io/docs](https://reui.io/docs) · [reui.io/llms.txt](https://reui.io/llms.txt) · MCP `user-reui` (`get_block` / `get_component` / `get_example` / `compose_page`). Skill ReUI: `668fb463eb` (локально и MCP совпадают). Версию REUI PRO **не** указываем — её нет.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Executive Summary
|
||||||
|
|
||||||
|
Workspace — 6 fullstack UI-приложений. Пять Vite SPA уже lock `surface: frame` + `reui-kit`. MikrotikManager — Next.js 16 App Router со SoT `/servers` на shadcn `Card` (`DataPageCard`). Это **две дизайн-системы** в одном workspace.
|
||||||
|
|
||||||
|
Целевое состояние:
|
||||||
|
|
||||||
|
- **Одинаковый chrome 1:1** во всех шести (включая Auth Portal и MikrotikManager): sidebar 240px, App Switcher, header AppsMenu → SystemMonitor, footer NavUser + segmented theme.
|
||||||
|
- **Одинаковый рецепт** ops-экранов: Frame, DataGrid+Filters, KPI = stats-12 + IconTile `elevated`, Quick Actions = Frame tiles, settings-3/16, Sheet для create/edit, Frame-friendly empty.
|
||||||
|
- **Разный домен:** topology VPS, DNS/kanban CFDM, BIRD/lookup EvoBGP, IdP Auth Portal, agents EvoFirewall, `/servers`/terminal MikrotikManager.
|
||||||
|
- **Не менять:** Vite vs Next, package scopes, API/SSO/OIDC, CRUD-поведение.
|
||||||
|
|
||||||
|
Главные gaps: Auth Portal без SystemMonitor; MikrotikManager без AppsMenu/NavUser/Switcher и с Sun/Moon toggle; ни один проект не установил `icon-tile`; VPS Card-shells на detail/charts; MM `DataPageCard` = Card.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Workspace Coverage
|
||||||
|
|
||||||
|
`Discovered projects: 6`
|
||||||
|
`Audited projects: 6`
|
||||||
|
`Projects with plan: 6`
|
||||||
|
`Coverage: 100%`
|
||||||
|
`Implementation: 34/34 CHANGE DONE (2026-08-17)`
|
||||||
|
|
||||||
|
| # | Project | Path | Type | Audited | Changes | Status |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| 1 | VPS Tracker | `c:\Users\shats\Dev\vps-tracker` | fullstack Vite SPA | YES | CHANGE-003, 004, 005, 009–014, 025, 029 | Implemented |
|
||||||
|
| 2 | Cloudflare Domain Manager | `c:\Users\shats\Dev\cloudflare-domain-manager` | fullstack Vite SPA | YES | CHANGE-003, 004, 005, 006, 024 | Implemented |
|
||||||
|
| 3 | EvoBGP | `c:\Users\shats\Dev\EvoBGP` | fullstack Go API + Vite SPA | YES | CHANGE-003, 004, 005, 015–017, 019 | Implemented |
|
||||||
|
| 4 | Auth Portal | `c:\Users\shats\Dev\auth-portal` | fullstack Vite SPA | YES | CHANGE-001, 004, 005, 021–023 | Implemented |
|
||||||
|
| 5 | EvoFirewall | `c:\Users\shats\Dev\EvoFirewall` | fullstack Vite SPA | YES | CHANGE-003, 004, 005, 007, 020 | Implemented |
|
||||||
|
| 6 | MikrotikManager | `c:\Users\shats\Dev\MikrotikManager-3` | fullstack Next.js 16 | YES | CHANGE-002, 004, 008, 018, 027, 032–034 | Implemented |
|
||||||
|
|
||||||
|
Backend-only проектов нет. Shared UX/UI rules: CHANGE-026, 028, 030, 031, RULE-CHANGE-001…010.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Workspace Inventory
|
||||||
|
|
||||||
|
| Project | Framework | Language | UI | CSS | Theme | Package UI | Kit |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| VPS Tracker | Vite + TanStack Router/Query | TS | shadcn base-nova + ReUI | Tailwind v4 | next-themes | `@cfdm/ui` | `reui-kit` (9 files) |
|
||||||
|
| CFDM | Vite + TanStack | TS | shadcn base-nova + ReUI | Tailwind v4 | next-themes | `@cfdm/ui` | `reui-kit` (8 files) |
|
||||||
|
| EvoBGP | Vite + TanStack; API Go | TS / Go | shadcn base-nova + ReUI | Tailwind v4 | next-themes | `@evobgp/ui` | `reui-kit` (7 files) — **KPI SoT** |
|
||||||
|
| Auth Portal | Vite + TanStack | TS | shadcn base-nova + ReUI | Tailwind v4 | next-themes | `@authportal/ui` | `reui-kit` (11 files; `ResourcePage` stub) |
|
||||||
|
| EvoFirewall | Vite + TanStack | TS | shadcn base-nova + ReUI | Tailwind v4 | next-themes | `@evofw/ui` | `reui-kit` (6 files) |
|
||||||
|
| MikrotikManager | Next.js 16 App Router | TS | shadcn + **partial** ReUI | Tailwind v4 | custom `theme-provider` (`rl-theme`) | `@/components/ui` | **нет** `reui-kit` |
|
||||||
|
|
||||||
|
TanStack Table во всех web-приложениях: `^8.21.x`. ReUI Data Grid docs (MCP `get_component`): TanStack Table **v9**. Это technical risk — см. §18. Не мигрировать v8→v9 в этом плане.
|
||||||
|
|
||||||
|
`icon-tile`: **не установлен ни в одном** из 6 (поиск `IconTile` / `icon-tile` = 0 файлов).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. MCP / Skills / Plugins Inventory
|
||||||
|
|
||||||
|
| Tool | Status | Use in audit |
|
||||||
|
|---|---|---|
|
||||||
|
| MCP `user-reui` | ready | `get_block`, `get_component`, `get_example`, `get_agent_skill` — официальные `docsUrl`/`previewUrl` |
|
||||||
|
| MCP `plugin-shadcn-shadcn` | listed | primitives `@shadcn` (secondary) |
|
||||||
|
| MCP codegraph (vps / CFDM / EvoBGP) | available | не заменял filesystem inventory |
|
||||||
|
| Skill `.claude/skills/reui` | `668fb463eb` | совпадает с MCP `get_agent_skill` |
|
||||||
|
| Skill shadcn (plugin + CFDM/EvoFirewall) | present | CLI/registry, не SoT для PRO blocks |
|
||||||
|
| Context7 MCP | available | не нужен для REUI PRO URLs |
|
||||||
|
|
||||||
|
Приоритет: официальная документация REUI PRO > MCP metadata > project rules.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Project Rules Inventory
|
||||||
|
|
||||||
|
| Source | Project | Category | Used? |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `docs/ui-design-contract.md` | VPS, CFDM, EvoBGP, Auth, FW | UX/UI contract (Frame, chrome, kit) | YES — baseline |
|
||||||
|
| `.cursor/rules/reui-mcp.mdc` | 5 Vite | ReUI MCP workflow | YES |
|
||||||
|
| `.cursor/rules/kpi-hybrid.mdc` | 5 Vite | KPI Item `size-10.5` DNA | YES — **устарело vs IconTile** |
|
||||||
|
| `.cursor/rules/frontend-shadcn.mdc` / `frontend-ui-patterns.mdc` | VPS, CFDM, Auth, FW | patterns | YES; VPS lists `kanban-board.tsx`, которого нет на диске |
|
||||||
|
| `.cursor/rules/web-shadcn.mdc` | EvoBGP | WEB-01…WEB-22 | YES |
|
||||||
|
| `AGENTS.md` | VPS, EvoBGP, Auth, FW, MM | agent guide | YES; **CFDM: `<not_found>`** |
|
||||||
|
| `.cursor/rules/next-shadcn-production.mdc` | MM | SoT = `/servers` Card | YES — **противоречит унификации** |
|
||||||
|
| `.cursor/rules/ma-ui-guardian.mdc` | MM | UI Guardian = `/servers` | YES — **противоречит** |
|
||||||
|
| Workspace `reui-pro-senior-frontend.mdc` | all | chrome + Frame lock | YES; Auth ids без `dns` |
|
||||||
|
|
||||||
|
Technical rules (Fastify/Drizzle/Go/Next App Router/CI) — **не менять** ради UI.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Project UX/UI Rules Audit
|
||||||
|
|
||||||
|
| Rule | Source | Current Usage | Problem | Recommendation | Status |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| Surface `frame` | ui-design-contract | 5 Vite mostly; MM Card | MM + VPS mixed Card | TARGET-001 | MODIFY (enforce all 6) |
|
||||||
|
| Chrome 240px / AppsMenu / NavUser | contract | 4 Vite close; Auth no monitor; MM none | Identity drift | TARGET-006 | NEW (1:1 all 6) |
|
||||||
|
| KPI hybrid Item tile | kpi-hybrid.mdc | 5 Vite kit | Official primitive IconTile unused | TARGET-003 | REPLACE |
|
||||||
|
| Lists DataGrid+Filters | contract | Vite ResourcePage; MM DataPageCard | MM Card; VPS `spaces` Table | TARGET-002 | MODIFY |
|
||||||
|
| Settings settings-16 / settings-3 | contract | CFDM closest | settings-7 is `surface: card` | TARGET-005 | MODIFY |
|
||||||
|
| Empty empty-state-12 | contract | mixed | empty-state-11 is Card; prefer 14 | TARGET-008 | MODIFY |
|
||||||
|
| MM SoT `/servers` Card | next-shadcn-production | enforced | Blocks Frame unification | RULE-CHANGE-001 | REPLACE |
|
||||||
|
| App Switcher ids | Auth contract vs `APP_IDS` | code has `dns` | Contract omits `dns` | CHANGE-023 | MODIFY |
|
||||||
|
| Installed ReUI lists | reui-mcp.mdc | claims kanban in VPS | File missing | CHANGE-025 | MODIFY |
|
||||||
|
| CFDM AGENTS.md / ui-surface.ts | contract mentions | `<not_found>` | Rules drift | CHANGE-024 | NEW |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Chrome Identity Spec (обязательный 1:1 — все 6)
|
||||||
|
|
||||||
|
Эталон: [app-shell-12](https://reui.io/preview/base/app-shell-12) + [app-shell-7](https://reui.io/preview/base/app-shell-7) (SystemMonitor) + [app-shell-1](https://reui.io/preview/base/app-shell-1) (NavUser).
|
||||||
|
|
||||||
|
Применяется к **каждому** приложению, включая Auth Portal `_auth` shell и MikrotikManager `(main)` layout. Login Auth Portal **без** AppShell — отдельный auth surface ([auth-18](https://reui.io/preview/base/auth-18)).
|
||||||
|
|
||||||
|
| Зона | Обязательное значение | Сейчас VPS/CFDM/BGP/FW | Auth Portal | MikrotikManager |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `--sidebar-width` | `240px` | `240px` (`packages/ui/.../sidebar.tsx`) | 240px (ожидаемо, тот же паттерн) | **`16rem`** (`components/ui/sidebar.tsx`) |
|
||||||
|
| Sidebar header | App Switcher ids `cfdm` · `vps` · `bgp` · `fw` · `dns` | есть | есть | **нет** |
|
||||||
|
| Sidebar footer | NavUser + segmented theme (light/dark/system), без Sun/Moon icon | NavUser segmented | NavUser segmented | **hand-roll** avatar + Sun/Moon (`app-sidebar.tsx` ~239–266); `nav-user.tsx` **не используется** |
|
||||||
|
| Header right | AppsMenu → SystemMonitorPopover; без ModeToggle; без Search-кнопки (только ⌘K) | AppsMenu + SystemMonitor | AppsMenu **без** SystemMonitor (`site-header.tsx` 73–75) | нет AppsMenu / SystemMonitor; `CommandPaletteButton` в sidebar header |
|
||||||
|
| Header | `h-12`, sticky, `border-b`, `px-4 md:px-6` | да | да | `<uncertain>` vs Vite (Next layout свой) |
|
||||||
|
| Main | `gap-4 md:gap-6`, `px-4 py-4 md:px-6 md:py-5` | contract | contract | `<not_verified>` pixel-match |
|
||||||
|
| Theme | next-themes **или эквивалент с тем же UX** + semantic `--success/--warning/--info/--invert` | next-themes | next-themes | custom `theme-provider` `rl-theme` — UX должен стать segmented 3-way |
|
||||||
|
| Запрещено | `SidebarRail`, Search pill в header, ModeToggle в header | Vite: без Rail | без Rail | **`SidebarRail` есть** (`app-sidebar.tsx` 267) |
|
||||||
|
|
||||||
|
SSO: вход через auth-portal; App Switcher `GET /api/v1/app-switcher`. Ids в коде: `packages/shared/src/contracts/auth.ts` `APP_IDS = ['cfdm', 'vps', 'bgp', 'fw', 'dns']`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. UX/UI Audit — VPS Tracker
|
||||||
|
|
||||||
|
**Identity:** Vite SPA `apps/web`, `@cfdm/ui`, ReUI CLI в `apps/web/src/components/reui/`.
|
||||||
|
**Routes (`_auth/`):** dashboard, vps, `vps.$vpsId`, accounts, providers, projects, `projects.$projectId`, payments, balance, tariffs, renewals, resources, reports, spaces, topology, audit, sync-journal.
|
||||||
|
**Kit:** `resource-page`, `kpi-stat-grid`, `quick-action-grid`, `ops-dashboard`, `settings-shell`, `detail-panel`, `frame-data-grid`, `settings-card`, `topology-canvas`. Нет `kanban-board`.
|
||||||
|
**ReUI on disk:** autocomplete, badge, icon-stack, filters, frame, alert, color-picker, timeline, number-field, date-selector + `data-grid/*`. Нет `icon-tile`, нет `kanban`.
|
||||||
|
**Chrome:** `app-shell.tsx` — SystemMonitor есть; sidebar 240px.
|
||||||
|
**Surface drift:** Card в `vps.$vpsId.tsx`, `resources.tsx`, `renewals.tsx`, `domain/charts.tsx`, `integrations/app-switcher-editor.tsx`. shadcn Table в `spaces.tsx`.
|
||||||
|
**Dashboard QA:** contract: VPS order = banner → KPI → charts → attention → QuickActionGrid (исключение vs EvoBGP KPI → QA → rest).
|
||||||
|
**Preserve:** VPS CRUD, BILLmanager sync, тарифы, баланс, платежи, topology canvas, CSV export, фильтры отчётов.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. UX/UI Audit — Cloudflare Domain Manager
|
||||||
|
|
||||||
|
**Identity:** Vite SPA, `@cfdm/ui`, Frame-first. Card **не** импортируется в `apps/web` (grep 0). Ближе всех к контракту.
|
||||||
|
**Routes:** index, domains, `domains/$domainId`, DNS, services, groups, `groups/$groupId`, certificates, settings (index/appearance/integrations).
|
||||||
|
**Kit:** + `kanban-board`, `dashboard-analytics`. Нет `frame-data-grid` отдельным файлом (`<uncertain>` — может быть внутри ResourcePage).
|
||||||
|
**Gaps:** нет `AGENTS.md`; нет `ui-surface.ts`; naming `*KpiCards` (`domain-kpi-cards.tsx`, `cert-kpi-cards.tsx`, `service-kpi-cards.tsx`) при том что внутри уже `KpiStatGrid`; KPI tiles = Item hybrid, не IconTile.
|
||||||
|
**Preserve:** DNS CRUD, Cloudflare certs, groups/services kanban, integrations.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. UX/UI Audit — EvoBGP
|
||||||
|
|
||||||
|
**Identity:** KPI SoT. `@evobgp/ui`. `lib/ui-surface.ts` есть.
|
||||||
|
**Routes:** dashboard, modules, `modules/$moduleId`, `modules/new`, lookup, network, operations, monitoring, directories, schedule, settings, tenant-settings, access.
|
||||||
|
**Card leftovers:** `modules/new.tsx`, `illustrated-empty-state.tsx`, `kpi-sparkline-card.tsx`, examples `c-tabs-*`.
|
||||||
|
**Kit unused:** `SettingsShell` определён в `reui-kit/settings-shell.tsx`, **не** импортируется routes (grep только definition).
|
||||||
|
**Forms:** WEB-05 требует RHF; фактическое использование в `src` — `<uncertain>` / выравнивать на Field + sheet-8.
|
||||||
|
**Preserve:** modules CRUD, lookup, BIRD jobs, network, access keys.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. UX/UI Audit — Auth Portal
|
||||||
|
|
||||||
|
**Identity:** IdP, не ops CRUD. Login: `routes/index.tsx` → `PortalLoginForm` на базе **auth-18** (`components/blocks/auth-18/...`).
|
||||||
|
**`_auth` routes:** apps, admin users, user detail, oidc, logins, audit, admin apps.
|
||||||
|
**Chrome gap:** `site-header.tsx` — только `AppsMenu`, **нет** `SystemMonitorPopover` (grep 0 по репо).
|
||||||
|
**Kit:** `ResourcePage` — stub types only (`reui-kit/resource-page.tsx`). `/apps` — hand-roll Frame tiles, не `QuickActionGrid`.
|
||||||
|
**Contract vs code:** `docs/ui-design-contract.md` ids без `dns`; `APP_IDS` включает `dns`.
|
||||||
|
**Preserve:** SSO JWT, OIDC authorize/token/userinfo, app switcher admin, user access, audit.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. UX/UI Audit — EvoFirewall
|
||||||
|
|
||||||
|
**Identity:** `@evofw/ui`. Chrome: `site-header.tsx` AppsMenu + SystemMonitor.
|
||||||
|
**Routes:** index, agents, `agents/$id`, lists, `lists/$id`, rules, `rules/$setId`, stats, settings.
|
||||||
|
**SettingsShell:** файл есть, **не** используется routes (grep 0 в routes). `/settings` — PageShell + Frame (допустимо как REPLACE rule).
|
||||||
|
**AgentPlatformIcon:** Item `size-10.5` `bg-muted` (`agents/agent-platform-icon.tsx`) — та же устаревшая KPI DNA.
|
||||||
|
**Agent cards view:** Frame domain (`?view=cards|table`) — **оставить**, не Card.
|
||||||
|
**Preserve:** agent fleet, blocklists, policy rules, stats, EvoBGP prefix ingest.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. UX/UI Audit — MikrotikManager
|
||||||
|
|
||||||
|
**Identity:** Next.js 16, `components.json` с `@reui`, **нет** `reui-kit`. SoT UI: `app/(main)/servers/page.tsx` + `DataPageCard` → `Card` (`components/data-page-card.tsx`).
|
||||||
|
**Pages (27):** servers, dashboard, firewall, filters, certificates, gre, data-collection, uptime, recursive-routes, route-optimizer, ospf, communities, settings, probes, vxlan, wireguard, backups, bgp, ip-ranges, domains, asns, alerts, network-map, terminal, traffic, releases, containers.
|
||||||
|
**ReUI on disk:** frame, filters, badge, alert, autocomplete, number-field, sortable, timeline, stepper + data-grid (partial). `frame.tsx` установлен, ops-shell его не использует.
|
||||||
|
**Chrome:** sidebar `16rem`; footer hand-roll + Sun/Moon; `NavUser` файл есть, не подключён; нет App Switcher / AppsMenu / SystemMonitor; `SidebarRail` включён.
|
||||||
|
**Theme:** `components/theme-provider.tsx` STORAGE_KEY `rl-theme` — не next-themes; UX должен стать 3-way segmented.
|
||||||
|
**Preserve:** весь CRUD `/servers` и остальные 26 страниц (terminal, network-map, контейнеры, BGP/OSPF/firewall…). Framework остаётся Next.js.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Cross-project Inconsistency Matrix
|
||||||
|
|
||||||
|
| Area | VPS | CFDM | EvoBGP | Auth | FW | MM | UX impact | Priority | Target |
|
||||||
|
|---|---|---|---|---|---|---|---|---|---|
|
||||||
|
| Ops surface | mixed Card | Frame | leftover Card | Frame | Frame | **Card SoT** | две системы | P0/P1 | Frame |
|
||||||
|
| Chrome identity | close | close | close | no monitor | close | **none** | разный «дом» | P0 | Chrome spec |
|
||||||
|
| Sidebar width | 240px | 240px | 240px | 240px | 240px | 16rem | визуальный разрыв | P0 | 240px |
|
||||||
|
| Theme control | NavUser segmented | same | same | same | same | Sun/Moon | разный UX | P0 | segmented |
|
||||||
|
| KPI tile | Item hybrid | Item hybrid | Item hybrid SoT | Item | Item / AgentPlatformIcon | Card KPI | нет IconTile | P1 | IconTile elevated |
|
||||||
|
| Lists | ResourcePage; spaces Table | ResourcePage | ResourcePage | stub + Frame grids | ResourcePage | DataPageCard | MM/VPS spaces | P1 | DataGrid+Filters |
|
||||||
|
| Quick Actions | kit (order exception) | kit | kit | hand-roll `/apps` | kit | `<not_found>` | Auth/MM | P1 | QuickActionGrid |
|
||||||
|
| Settings | SettingsShell | Frame settings | unused SettingsShell | N/A portal | unused SettingsShell | Card settings | dead kit | P2 | settings-3/16 |
|
||||||
|
| Empty | EmptyState | EmptyState | Card illustrated | Frame empty `/apps` | EmptyState | EmptyState Card-ish | Card empty | P2 | empty-state-14/12 |
|
||||||
|
| Package UI | `@cfdm/ui` | `@cfdm/ui` | `@evobgp/ui` | `@authportal/ui` | `@evofw/ui` | `@/components/ui` | не сливать npm | — | KEEP scopes |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. Duplicate Components Analysis
|
||||||
|
|
||||||
|
| Task | Copies | Diff | Unify? | REUI |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `KpiStatGrid` | 5 Vite repos | import scope only (SoT EvoBGP) | DNA sync per repo, **не** monorepo package | REUI-004 + REUI-005 |
|
||||||
|
| `QuickActionGrid` | 5 Vite | same | same | REUI-013 |
|
||||||
|
| `ResourcePage` | 4 ops + Auth stub | Auth stub | implement or stop claiming | REUI-006 |
|
||||||
|
| `OpsDashboard` | VPS/CFDM/BGP/FW | similar | KEEP per repo | REUI-014 |
|
||||||
|
| `SettingsShell` | VPS/CFDM/BGP/FW | BGP/FW unused | wire or stop listing as used | REUI-007 |
|
||||||
|
| `SystemMonitorPopover` | 4 Vite; Auth/MM missing | copy per app | port to Auth + MM | REUI-011 |
|
||||||
|
| `DataPageCard` vs Frame | MM only vs Vite Frame | Card vs Frame | REPLACE MM | REUI-001 |
|
||||||
|
| Item KPI tile vs IconTile | 5 Vite + FW AgentPlatformIcon | hand-roll | REPLACE with IconTile | REUI-005 |
|
||||||
|
|
||||||
|
Общий npm-пакет kit **не** входит в этот план (package boundaries = technical risk).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 15. REUI PRO Reference Registry
|
||||||
|
|
||||||
|
| Ref ID | Component/Block | Official Documentation | Official Example | Used By | Match |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| REUI-001 | Frame | [docs/components/base/frame](https://reui.io/docs/components/base/frame) | [components/frame](https://reui.io/components/frame) | CHANGE-009–018, 027 | 1:1 ops surface |
|
||||||
|
| REUI-002 | Data Grid | [docs/components/base/data-grid](https://reui.io/docs/components/base/data-grid) | [components/data-grid](https://reui.io/components/data-grid) | CHANGE-013, 018 | 1:1 lists; docs = TanStack **v9** vs installed v8 → adaptation = **partial** until migration |
|
||||||
|
| REUI-003 | Filters | [docs/components/base/filters](https://reui.io/docs/components/base/filters) | [components/filters](https://reui.io/components/filters) | CHANGE-013, 018 | 1:1 |
|
||||||
|
| REUI-004 | stats-12 | [blocks](https://reui.io/blocks) | [preview/base/stats-12](https://reui.io/preview/base/stats-12) | CHANGE-005, 008 | **partial** (MCP: Frame+Badge+Item; kit hand-rolls Item) |
|
||||||
|
| REUI-005 | Icon Tile | [docs/components/base/icon-tile](https://reui.io/docs/components/base/icon-tile) | [components/icon-tile](https://reui.io/components/icon-tile) | CHANGE-004, 005, 007, 008 | 1:1 for tile primitive (`variant="elevated"` per docs) |
|
||||||
|
| REUI-006 | data-grid-filtering-2 | [blocks](https://reui.io/blocks) | [preview/base/data-grid-filtering-2](https://reui.io/preview/base/data-grid-filtering-2) | CHANGE-013, 018, 021 | **partial** (kit ResourcePage) |
|
||||||
|
| REUI-007 | settings-16 | [blocks/application/settings](https://reui.io/blocks/application/settings) | [preview/base/settings-16](https://reui.io/preview/base/settings-16) | CHANGE-020 | 1:1 integrations |
|
||||||
|
| REUI-008 | settings-3 | [blocks](https://reui.io/blocks) | [preview/base/settings-3](https://reui.io/preview/base/settings-3) | CHANGE-020 | 1:1 settings rows Frame |
|
||||||
|
| REUI-009 | settings-8 | [blocks](https://reui.io/blocks) | [preview/base/settings-8](https://reui.io/preview/base/settings-8) | CHANGE-020 | 1:1 webhook-like lists |
|
||||||
|
| REUI-010 | app-shell-12 | [blocks](https://reui.io/blocks) | [preview/base/app-shell-12](https://reui.io/preview/base/app-shell-12) | CHANGE-001, 002, 003 | **partial** (chrome composition) |
|
||||||
|
| REUI-011 | app-shell-7 | [blocks](https://reui.io/blocks) | [preview/base/app-shell-7](https://reui.io/preview/base/app-shell-7) | CHANGE-001, 002 | **partial** (SystemMonitor popover match) |
|
||||||
|
| REUI-012 | app-shell-1 | [blocks](https://reui.io/blocks) | [preview/base/app-shell-1](https://reui.io/preview/base/app-shell-1) | CHANGE-002, 032 | **partial** (NavUser) |
|
||||||
|
| REUI-013 | card-12 | [blocks](https://reui.io/blocks) | [preview/base/card-12](https://reui.io/preview/base/card-12) | CHANGE-022 | **partial** (Quick Actions DNA; name «card» но Frame tiles) |
|
||||||
|
| REUI-014 | dashboard-1 | [blocks](https://reui.io/blocks) | [preview/base/dashboard-1](https://reui.io/preview/base/dashboard-1) | CHANGE-008, 029 | **partial** (OpsDashboard) |
|
||||||
|
| REUI-015 | empty-state-14 | [blocks](https://reui.io/blocks) | [preview/base/empty-state-14](https://reui.io/preview/base/empty-state-14) | CHANGE-016, 030 | 1:1 Frame empty (MCP `surface: frame`) |
|
||||||
|
| REUI-016 | empty-state-12 | [blocks](https://reui.io/blocks) | [preview/base/empty-state-12](https://reui.io/preview/base/empty-state-12) | CHANGE-030 | **partial** (current cite; MCP surface none) |
|
||||||
|
| REUI-017 | sheet-8 | [blocks](https://reui.io/blocks) | [preview/base/sheet-8](https://reui.io/preview/base/sheet-8) | CHANGE-031 | **partial** (settings sheet; surface none) |
|
||||||
|
| REUI-018 | sheet-1 | [blocks](https://reui.io/blocks) | [preview/base/sheet-1](https://reui.io/preview/base/sheet-1) | CHANGE-031 | **partial** (create sheet) |
|
||||||
|
| REUI-019 | sheet-9 | [blocks](https://reui.io/blocks) | [preview/base/sheet-9](https://reui.io/preview/base/sheet-9) | CHANGE-031 | **partial** (detail sheet) |
|
||||||
|
| REUI-020 | auth-13 | [blocks](https://reui.io/blocks) | [preview/base/auth-13](https://reui.io/preview/base/auth-13) | Auth login **не** target (см. REUI-021) | 1:1 Frame login **если** менять; сейчас KEEP auth-18 |
|
||||||
|
| REUI-021 | auth-18 | [blocks](https://reui.io/blocks) | [preview/base/auth-18](https://reui.io/preview/base/auth-18) | Auth login actual | **partial** (surface none; split testimonial) |
|
||||||
|
| REUI-022 | solution-users-1 | [blocks](https://reui.io/blocks) | [preview/base/solution-users-1](https://reui.io/preview/base/solution-users-1) | Auth admin users | **partial** (Frame+DataGrid+Filters+Sheet) |
|
||||||
|
| REUI-023 | form-7 | [blocks](https://reui.io/blocks) | [preview/base/form-7](https://reui.io/preview/base/form-7) | CHANGE-031 | **partial** (inline Frame form) |
|
||||||
|
| REUI-024 | chart-15 | [blocks](https://reui.io/blocks) | [preview/base/chart-15](https://reui.io/preview/base/chart-15) | CHANGE-012 | 1:1 Frame charts (MCP `surface: frame`) |
|
||||||
|
| REUI-025 | settings-2 | [blocks](https://reui.io/blocks) | [preview/base/settings-2](https://reui.io/preview/base/settings-2) | CHANGE-020 | 1:1 mixed-control rows Frame |
|
||||||
|
| REUI-026 | c-tabs-2 | [components/tabs](https://reui.io/components/tabs) | [preview/base/components/c-tabs-2](https://reui.io/preview/base/components/c-tabs-2) | ResourcePage tabs | **partial** (line tabs; example also lists Card) |
|
||||||
|
| REUI-027 | Alert Dialog example | [components/alert-dialog](https://reui.io/components/alert-dialog) | [c-alert-dialog-11](https://reui.io/preview/base/components/c-alert-dialog-11) | CHANGE-031 confirm | **partial** (shadcn AlertDialog + ReUI example) |
|
||||||
|
| REUI-028 | Icon Stack | [docs/components/base/icon-stack](https://reui.io/docs/components/base/icon-stack) | [components/icon-stack](https://reui.io/components/icon-stack) | CHANGE-016, 030 | 1:1 empty illustration |
|
||||||
|
| REUI-029 | Badge | [docs/components/base/badge](https://reui.io/docs/components/base/badge) | [components/badge](https://reui.io/components/badge) | KPI/QA status | 1:1 semantic variants |
|
||||||
|
| REUI-030 | Alert | [docs/components/base/alert](https://reui.io/docs/components/base/alert) | [components/alert](https://reui.io/components/alert) | errors | 1:1 |
|
||||||
|
| REUI-031 | Kanban | [docs/components/base/kanban](https://reui.io/docs/components/base/kanban) | [components/kanban](https://reui.io/components/kanban) | CFDM groups/services | 1:1 domain |
|
||||||
|
| REUI-032 | Sortable | [docs/components/base/sortable](https://reui.io/docs/components/base/sortable) | [components/sortable](https://reui.io/components/sortable) | FW policy | 1:1 domain |
|
||||||
|
| REUI-033 | Stepper | [docs/components/base/stepper](https://reui.io/docs/components/base/stepper) | [components/stepper](https://reui.io/components/stepper) | MM/FW if used | 1:1; FW unused → don't list as used |
|
||||||
|
| REUI-034 | Timeline | [docs/components/base/timeline](https://reui.io/docs/components/base/timeline) | [components/timeline](https://reui.io/components/timeline) | audit | 1:1 |
|
||||||
|
| REUI-035 | Autocomplete | [docs/components/base/autocomplete](https://reui.io/docs/components/base/autocomplete) | [components/autocomplete](https://reui.io/components/autocomplete) | domain pickers | 1:1 |
|
||||||
|
|
||||||
|
**Не target (намеренно):**
|
||||||
|
|
||||||
|
| Item | Why | Evidence |
|
||||||
|
|---|---|---|
|
||||||
|
| settings-7 | MCP `surface: card` | [preview/base/settings-7](https://reui.io/preview/base/settings-7) |
|
||||||
|
| empty-state-11 | MCP `surface: card` | [preview/base/empty-state-11](https://reui.io/preview/base/empty-state-11) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 16. Target UX/UI Rules
|
||||||
|
|
||||||
|
### TARGET-RULE-001
|
||||||
|
|
||||||
|
**Rule:** Один surface на ops-экран: ReUI Frame. Не смешивать Card и Frame.
|
||||||
|
**Scope:** все 6.
|
||||||
|
**Rationale:** две системы ломают consistency.
|
||||||
|
**REUI PRO:** [Frame](https://reui.io/docs/components/base/frame)
|
||||||
|
**Impact:** VPS Card pages, BGP leftovers, MM DataPageCard.
|
||||||
|
|
||||||
|
### TARGET-RULE-002
|
||||||
|
|
||||||
|
**Rule:** Ops-списки = DataGrid + Filters внутри Frame. Запрет raw `<table>` / shadcn Table как ops list.
|
||||||
|
**Scope:** все 6.
|
||||||
|
**REUI PRO:** [data-grid](https://reui.io/docs/components/base/data-grid) · [data-grid-filtering-2](https://reui.io/preview/base/data-grid-filtering-2)
|
||||||
|
**Impact:** VPS `spaces.tsx`; MM DataPageCard.
|
||||||
|
|
||||||
|
### TARGET-RULE-003
|
||||||
|
|
||||||
|
**Rule:** KPI = stats-12 composition + IconTile `elevated` (не hand-roll Item `size-10.5`). Semantic `text-*` / Badge, не `bg-emerald-*`.
|
||||||
|
**Scope:** kit во всех Vite + MM dashboard.
|
||||||
|
**REUI PRO:** [stats-12](https://reui.io/preview/base/stats-12) · [icon-tile](https://reui.io/docs/components/base/icon-tile)
|
||||||
|
|
||||||
|
### TARGET-RULE-004
|
||||||
|
|
||||||
|
**Rule:** Quick Actions = только `QuickActionGrid` (Frame tiles, sibling KPI).
|
||||||
|
**REUI PRO:** [stats-12](https://reui.io/preview/base/stats-12) · [card-12](https://reui.io/preview/base/card-12)
|
||||||
|
|
||||||
|
### TARGET-RULE-005
|
||||||
|
|
||||||
|
**Rule:** Settings rows = settings-3 / settings-2; integrations = settings-16; **запрет settings-7**.
|
||||||
|
**REUI PRO:** [settings-3](https://reui.io/preview/base/settings-3) · [settings-16](https://reui.io/preview/base/settings-16)
|
||||||
|
|
||||||
|
### TARGET-RULE-006 (P0)
|
||||||
|
|
||||||
|
**Rule:** Chrome identity 1:1 во всех 6 — см. Chrome Identity Spec. Auth Portal и MikrotikManager **не** исключение. Login без AppShell.
|
||||||
|
**REUI PRO:** [app-shell-12](https://reui.io/preview/base/app-shell-12) · [app-shell-7](https://reui.io/preview/base/app-shell-7) · [app-shell-1](https://reui.io/preview/base/app-shell-1)
|
||||||
|
|
||||||
|
### TARGET-RULE-007
|
||||||
|
|
||||||
|
**Rule:** Create/edit = Sheet (sheet-8 / sheet-1 / sheet-9). Confirm = Alert Dialog ([c-alert-dialog-11](https://reui.io/preview/base/components/c-alert-dialog-11)).
|
||||||
|
|
||||||
|
### TARGET-RULE-008
|
||||||
|
|
||||||
|
**Rule:** Empty = Empty + IconStack, Frame-friendly empty-state-14 (prefer) / empty-state-12. Не empty-state-11.
|
||||||
|
|
||||||
|
### TARGET-RULE-009
|
||||||
|
|
||||||
|
**Rule:** MikrotikManager UX SoT: Frame+DataGrid **при сохранении** CRUD `/servers`. Next.js не менять.
|
||||||
|
|
||||||
|
### TARGET-RULE-010
|
||||||
|
|
||||||
|
**Rule:** Rules синхронизировать с диском (не перечислять неустановленные компоненты как установленные). App Switcher ids включают `dns`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### RULE-CHANGE-001
|
||||||
|
|
||||||
|
**Current rule:** SoT UI = `/servers` на Card (`next-shadcn-production.mdc`, `ma-ui-guardian.mdc`).
|
||||||
|
**Source:** `MikrotikManager-3/.cursor/rules/next-shadcn-production.mdc`
|
||||||
|
**Problem:** блокирует унификацию Frame.
|
||||||
|
**Evidence:** `components/data-page-card.tsx` → `Card`.
|
||||||
|
**Recommended:** SoT = Frame + DataGrid; `/servers` остаётся эталоном **поведения** CRUD, не Card-shell.
|
||||||
|
**REUI:** REUI-001, REUI-006.
|
||||||
|
**Impact:** MM rules + все `DataPageCard` pages.
|
||||||
|
|
||||||
|
### RULE-CHANGE-002
|
||||||
|
|
||||||
|
**Current:** kpi-hybrid Item `size-10.5` `bg-muted`.
|
||||||
|
**Source:** `kpi-hybrid.mdc` ×5.
|
||||||
|
**Problem:** official IconTile покрывает задачу; нигде не установлен.
|
||||||
|
**Recommended:** KPI/row tiles = IconTile `elevated` + stats-12 composition.
|
||||||
|
**REUI:** REUI-004, REUI-005.
|
||||||
|
|
||||||
|
### RULE-CHANGE-003
|
||||||
|
|
||||||
|
**Current:** chrome описан в contract, но Auth/MM исключения фактически существуют.
|
||||||
|
**Recommended:** Chrome Identity Spec обязателен 1:1 для всех 6.
|
||||||
|
**REUI:** REUI-010, REUI-011, REUI-012.
|
||||||
|
|
||||||
|
### RULE-CHANGE-004
|
||||||
|
|
||||||
|
**Current:** contract App Switcher ids без `dns`.
|
||||||
|
**Source:** `auth-portal/docs/ui-design-contract.md`
|
||||||
|
**Evidence:** `APP_IDS` включает `dns`.
|
||||||
|
**Recommended:** ids `cfdm · vps · bgp · fw · dns`.
|
||||||
|
|
||||||
|
### RULE-CHANGE-005
|
||||||
|
|
||||||
|
**Current:** VPS `frontend-shadcn.mdc` / `reui-mcp.mdc` lists `kanban-board`.
|
||||||
|
**Evidence:** `reui-kit/` VPS — нет `kanban-board.tsx`; grep `kanban` в VPS = 0.
|
||||||
|
**Recommended:** список установленных = диск.
|
||||||
|
|
||||||
|
### RULE-CHANGE-006
|
||||||
|
|
||||||
|
**Current:** settings-7 встречается как reference.
|
||||||
|
**Evidence:** MCP settings-7 `surface: card`.
|
||||||
|
**Recommended:** запретить как target.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 17. Detailed Change Plan
|
||||||
|
|
||||||
|
Шаблон ниже. Functional requirements для всех UI CHANGE: сохранить create/edit/delete/filter/sort/pagination/navigation/permissions; менять только оболочку.
|
||||||
|
|
||||||
|
Состояния (где применимо): default, hover, focus, disabled, loading, error, empty.
|
||||||
|
Responsive: desktop = эталон ReUI preview; tablet/mobile = существующий Sidebar/Sheet collapse Vite/Next, не ломать.
|
||||||
|
A11y: keyboard, focus ring, labels, `aria-hidden` на decorative IconTile (docs).
|
||||||
|
|
||||||
|
### CHANGE-001
|
||||||
|
|
||||||
|
**Project:** Auth Portal · **Priority:** P0 · **Category:** Navigation / chrome
|
||||||
|
**File:** `apps/web/src/components/layout/site-header.tsx`
|
||||||
|
**Current:** header right = только `AppsMenu` (строки 73–75).
|
||||||
|
**Problem:** нет SystemMonitor — ломает chrome identity.
|
||||||
|
**Change:** добавить `SystemMonitorPopover` после AppsMenu (порт с CFDM/VPS).
|
||||||
|
**Ref:** REUI-011 · [app-shell-7](https://reui.io/preview/base/app-shell-7) · Match: **partial**
|
||||||
|
**Mapping:** AppsMenu-only header → AppsMenu + SystemMonitor.
|
||||||
|
**Preserve:** breadcrumbs, AppsMenu, SSO session.
|
||||||
|
**Risk:** Low.
|
||||||
|
|
||||||
|
### CHANGE-002
|
||||||
|
|
||||||
|
**Project:** MikrotikManager · **Priority:** P0 · **Category:** Navigation / chrome
|
||||||
|
**Files:** `components/app-sidebar.tsx`, `components/ui/sidebar.tsx`, `(main)/layout.tsx`
|
||||||
|
**Current:** sidebar 16rem; footer hand-roll + Sun/Moon; SidebarRail; нет AppsMenu/SystemMonitor/App Switcher.
|
||||||
|
**Change:** Chrome Identity Spec 1:1 (240px, Switcher, NavUser segmented, header cluster, убрать Rail и icon theme toggle).
|
||||||
|
**Ref:** REUI-010, REUI-011, REUI-012
|
||||||
|
**Match:** **partial** (composition, Next layout).
|
||||||
|
**Preserve:** nav groups, command palette **как ⌘K** (не Search pill в header).
|
||||||
|
**Risk:** High.
|
||||||
|
|
||||||
|
### CHANGE-003
|
||||||
|
|
||||||
|
**Project:** VPS, CFDM, EvoBGP, EvoFirewall · **Priority:** P0 · **Category:** chrome verification
|
||||||
|
**Current:** близко к spec (SystemMonitor есть).
|
||||||
|
**Change:** pixel/token сверка 1:1 (header `h-12`, main padding, нет ModeToggle/Search pill).
|
||||||
|
**Ref:** REUI-010 · Match: **partial**
|
||||||
|
**Risk:** Low.
|
||||||
|
|
||||||
|
### CHANGE-004
|
||||||
|
|
||||||
|
**Project:** все 6 · **Priority:** P1 · **Category:** KPI primitive install
|
||||||
|
**Current:** `icon-tile` отсутствует.
|
||||||
|
**Change:** CLI `pnpm dlx shadcn@latest add @reui/icon-tile --yes` из `apps/web` (MM — из корня Next).
|
||||||
|
**Ref:** REUI-005 · [icon-tile docs](https://reui.io/docs/components/base/icon-tile) · Match: **1:1**
|
||||||
|
**Risk:** Low.
|
||||||
|
|
||||||
|
### CHANGE-005
|
||||||
|
|
||||||
|
**Project:** 5 Vite kit · **Priority:** P1 · **Category:** KPI
|
||||||
|
**Files:** `reui-kit/kpi-stat-grid.tsx` (каждый репо); EvoBGP SoT markup.
|
||||||
|
**Current:** `Item` `size-10.5` `bg-muted` + `ItemMedia`.
|
||||||
|
**Change:** IconTile `elevated` + stats-12 composition; обновить `kpi-hybrid.mdc`.
|
||||||
|
**Ref:** REUI-004 **partial** + REUI-005 **1:1**
|
||||||
|
**Preserve:** `to`/`onSelect`, variants warning/destructive, Badge hints.
|
||||||
|
**Risk:** Medium (visual DNA shift).
|
||||||
|
|
||||||
|
### CHANGE-006
|
||||||
|
|
||||||
|
**Project:** CFDM · **Priority:** P2 · **Category:** naming
|
||||||
|
**Files:** `domain-kpi-cards.tsx`, `cert-kpi-cards.tsx`, `service-kpi-cards.tsx`
|
||||||
|
**Change:** переименовать `*KpiCards` → `*KpiStats` (внутри уже KpiStatGrid). Не Card.
|
||||||
|
**Ref:** REUI-004 · Match: **partial** (rename only)
|
||||||
|
**Risk:** Low.
|
||||||
|
|
||||||
|
### CHANGE-007
|
||||||
|
|
||||||
|
**Project:** EvoFirewall · **Priority:** P1 · **Category:** row tiles
|
||||||
|
**File:** `apps/web/src/components/agents/agent-platform-icon.tsx`
|
||||||
|
**Current:** Item hybrid tile.
|
||||||
|
**Change:** IconTile elevated; semantic `text-info` / muted.
|
||||||
|
**Ref:** REUI-005 · Match: **1:1**
|
||||||
|
**Preserve:** mikrotik vs linux aria-label.
|
||||||
|
**Risk:** Low.
|
||||||
|
|
||||||
|
### CHANGE-008
|
||||||
|
|
||||||
|
**Project:** MikrotikManager · **Priority:** P1 · **Category:** KPI
|
||||||
|
**File:** `app/(main)/dashboard/page.tsx`
|
||||||
|
**Change:** dashboard metrics → stats-12 + IconTile, не Card KPI.
|
||||||
|
**Ref:** REUI-004, REUI-005, REUI-014 · Match: **partial**
|
||||||
|
**Preserve:** dashboard numbers/links.
|
||||||
|
**Risk:** Medium.
|
||||||
|
|
||||||
|
### CHANGE-009
|
||||||
|
|
||||||
|
**Project:** VPS Tracker · **Priority:** P1 · **Category:** Surface
|
||||||
|
**File:** `apps/web/src/routes/_auth/vps.$vpsId.tsx`
|
||||||
|
**Current:** Card shells.
|
||||||
|
**Change:** Frame / DetailPanel.
|
||||||
|
**Ref:** REUI-001, REUI-023 · Match: **partial** (detail composition)
|
||||||
|
**Preserve:** VPS detail fields, actions, sync.
|
||||||
|
**Risk:** Medium.
|
||||||
|
|
||||||
|
### CHANGE-010
|
||||||
|
|
||||||
|
**Project:** VPS Tracker · **Priority:** P1 · **Category:** Surface
|
||||||
|
**File:** `apps/web/src/routes/_auth/resources.tsx`
|
||||||
|
**Change:** Card → Frame.
|
||||||
|
**Ref:** REUI-001 · Match: **1:1** shell
|
||||||
|
**Preserve:** resource listing.
|
||||||
|
**Risk:** Low.
|
||||||
|
|
||||||
|
### CHANGE-011
|
||||||
|
|
||||||
|
**Project:** VPS Tracker · **Priority:** P1 · **Category:** Surface
|
||||||
|
**File:** `apps/web/src/routes/_auth/renewals.tsx`
|
||||||
|
**Change:** Card → Frame.
|
||||||
|
**Ref:** REUI-001 · Match: **1:1** shell
|
||||||
|
**Preserve:** renewals data/actions.
|
||||||
|
**Risk:** Low.
|
||||||
|
|
||||||
|
### CHANGE-012
|
||||||
|
|
||||||
|
**Project:** VPS Tracker · **Priority:** P1 · **Category:** Charts
|
||||||
|
**File:** `apps/web/src/components/domain/charts.tsx` (used by `reports.tsx`)
|
||||||
|
**Current:** Card from `@cfdm/ui/components/card`.
|
||||||
|
**Change:** Frame chart shells per chart-15.
|
||||||
|
**Ref:** REUI-024 · [chart-15](https://reui.io/preview/base/chart-15) · Match: **1:1** surface; **partial** series (Recharts already)
|
||||||
|
**Preserve:** filters, CSV, KPI on reports.
|
||||||
|
**Risk:** Medium.
|
||||||
|
|
||||||
|
### CHANGE-013
|
||||||
|
|
||||||
|
**Project:** VPS Tracker · **Priority:** P1 · **Category:** Data UI
|
||||||
|
**File:** `apps/web/src/routes/_auth/spaces.tsx`
|
||||||
|
**Current:** shadcn Table.
|
||||||
|
**Change:** ResourcePage / DataGrid + Filters.
|
||||||
|
**Ref:** REUI-002 **partial** (v8), REUI-003, REUI-006
|
||||||
|
**Preserve:** spaces CRUD, columns, sort.
|
||||||
|
**Risk:** Medium.
|
||||||
|
|
||||||
|
### CHANGE-014
|
||||||
|
|
||||||
|
**Project:** VPS Tracker · **Priority:** P2 · **Category:** Settings
|
||||||
|
**File:** `apps/web/src/components/integrations/app-switcher-editor.tsx`
|
||||||
|
**Current:** Card.
|
||||||
|
**Change:** Frame + settings-16/3 rows.
|
||||||
|
**Ref:** REUI-001, REUI-007 · Match: **partial**
|
||||||
|
**Preserve:** app switcher editor fields.
|
||||||
|
**Risk:** Low.
|
||||||
|
|
||||||
|
### CHANGE-015
|
||||||
|
|
||||||
|
**Project:** EvoBGP · **Priority:** P1 · **Category:** Surface
|
||||||
|
**File:** `apps/web/src/routes/_auth/modules/new.tsx`
|
||||||
|
**Current:** Card stub.
|
||||||
|
**Change:** Frame + sheet-1/form-7 for create.
|
||||||
|
**Ref:** REUI-001, REUI-018 · Match: **partial**
|
||||||
|
**Preserve:** module create flow.
|
||||||
|
**Risk:** Medium.
|
||||||
|
|
||||||
|
### CHANGE-016
|
||||||
|
|
||||||
|
**Project:** EvoBGP · **Priority:** P1 · **Category:** Empty
|
||||||
|
**File:** `apps/web/src/components/patterns/illustrated-empty-state.tsx`
|
||||||
|
**Current:** Card + illustration.
|
||||||
|
**Change:** empty-state-14 (Frame + IconStack).
|
||||||
|
**Ref:** REUI-015, REUI-028 · Match: **1:1** surface
|
||||||
|
**Preserve:** empty CTA copy/actions.
|
||||||
|
**Risk:** Low.
|
||||||
|
|
||||||
|
### CHANGE-017
|
||||||
|
|
||||||
|
**Project:** EvoBGP · **Priority:** P2 · **Category:** Charts
|
||||||
|
**File:** `apps/web/src/components/patterns/kpi-sparkline-card.tsx`
|
||||||
|
**Change:** Card → Frame / chart-15 DNA.
|
||||||
|
**Ref:** REUI-024 · Match: **partial**
|
||||||
|
**Risk:** Low.
|
||||||
|
|
||||||
|
### CHANGE-018
|
||||||
|
|
||||||
|
**Project:** MikrotikManager · **Priority:** P1 · **Category:** Surface
|
||||||
|
**Files:** `components/data-page-card.tsx` + all pages importing it (servers, firewall, filters, gre, certificates, data-collection, …).
|
||||||
|
**Current:** `Card`. `components/reui/frame.tsx` установлен, не используется как ops shell.
|
||||||
|
**Change:** DataPageCard → Frame wrapper (или заменить вызовы на Frame).
|
||||||
|
**Ref:** REUI-001, REUI-006 · Match: **1:1** shell; grid **partial** (v8)
|
||||||
|
**Preserve:** весь CRUD `/servers` и остальных страниц.
|
||||||
|
**Risk:** High.
|
||||||
|
|
||||||
|
### CHANGE-019
|
||||||
|
|
||||||
|
**Project:** EvoBGP · **Priority:** P2 · **Category:** Kit dead code
|
||||||
|
**File:** `reui-kit/settings-shell.tsx`
|
||||||
|
**Change:** подключить на settings routes **или** убрать из «установленный kit / used».
|
||||||
|
**Ref:** REUI-007, REUI-008 · Match: **partial**
|
||||||
|
**Risk:** Low.
|
||||||
|
|
||||||
|
### CHANGE-020
|
||||||
|
|
||||||
|
**Project:** EvoFirewall · **Priority:** P2 · **Category:** Settings
|
||||||
|
**File:** `routes/_auth/settings.tsx` vs unused `reui-kit/settings-shell.tsx`
|
||||||
|
**Change:** либо SettingsShell при 2+ секциях, либо явно REPLACE rule «single-page = PageShell+Frame» (уже в FW frontend-shadcn).
|
||||||
|
**Ref:** REUI-007, REUI-008, REUI-025
|
||||||
|
**Risk:** Low.
|
||||||
|
|
||||||
|
### CHANGE-021
|
||||||
|
|
||||||
|
**Project:** Auth Portal · **Priority:** P2 · **Category:** Kit
|
||||||
|
**File:** `reui-kit/resource-page.tsx` (stub)
|
||||||
|
**Change:** либо реализовать как ops ResourcePage, либо не экспортировать как list kit. Admin users → solution-users-1 DNA.
|
||||||
|
**Ref:** REUI-006, REUI-022 · Match: **partial**
|
||||||
|
**Preserve:** users/OIDC grids.
|
||||||
|
**Risk:** Medium.
|
||||||
|
|
||||||
|
### CHANGE-022
|
||||||
|
|
||||||
|
**Project:** Auth Portal · **Priority:** P1 · **Category:** Quick Actions
|
||||||
|
**File:** `apps/web/src/routes/_auth.apps.tsx`
|
||||||
|
**Current:** hand-roll Frame tiles.
|
||||||
|
**Change:** `QuickActionGrid` (после IconTile DNA).
|
||||||
|
**Ref:** REUI-013, REUI-004 · Match: **partial**
|
||||||
|
**Preserve:** SSO `openApp`, admin link, empty «нет приложений».
|
||||||
|
**Risk:** Medium.
|
||||||
|
|
||||||
|
### CHANGE-023
|
||||||
|
|
||||||
|
**Project:** Auth Portal · **Priority:** P1 · **Category:** Rules
|
||||||
|
**File:** `docs/ui-design-contract.md` (+ копии в других apps)
|
||||||
|
**Change:** App Switcher ids += `dns`.
|
||||||
|
**Ref:** N/A functional; chrome REUI-010
|
||||||
|
**Match:** `<not_found_in_reui>` для списка ids (доменное) — UI chrome всё равно REUI-010.
|
||||||
|
**Risk:** Low.
|
||||||
|
|
||||||
|
### CHANGE-024
|
||||||
|
|
||||||
|
**Project:** CFDM · **Priority:** P2 · **Category:** Rules
|
||||||
|
**Change:** добавить `AGENTS.md` + `apps/web/src/lib/ui-surface.ts` (`UI_SURFACE = 'frame'`), как EvoBGP.
|
||||||
|
**Ref:** REUI-001
|
||||||
|
**Risk:** Low.
|
||||||
|
|
||||||
|
### CHANGE-025
|
||||||
|
|
||||||
|
**Project:** VPS Tracker · **Priority:** P2 · **Category:** Rules
|
||||||
|
**Change:** убрать kanban из «установленных» в `reui-mcp.mdc` / `frontend-shadcn.mdc`.
|
||||||
|
**Ref:** REUI-031 существует, но **не установлен** в VPS.
|
||||||
|
**Risk:** Low.
|
||||||
|
|
||||||
|
### CHANGE-026
|
||||||
|
|
||||||
|
**Project:** all Vite · **Priority:** P1 · **Category:** Rules
|
||||||
|
**Files:** `kpi-hybrid.mdc` ×5 + workspace `reui-pro-senior-frontend.mdc`
|
||||||
|
**Change:** DNA = IconTile elevated, не Item.
|
||||||
|
**Ref:** REUI-005
|
||||||
|
**Risk:** Low (docs only until CHANGE-005).
|
||||||
|
|
||||||
|
### CHANGE-027
|
||||||
|
|
||||||
|
**Project:** MikrotikManager · **Priority:** P0 · **Category:** Rules
|
||||||
|
**Change:** RULE-CHANGE-001 — SoT Frame+DataGrid.
|
||||||
|
**Ref:** REUI-001, REUI-006
|
||||||
|
**Risk:** High (process + UI).
|
||||||
|
|
||||||
|
### CHANGE-028
|
||||||
|
|
||||||
|
**Project:** EvoBGP (+ any settings-7 cite) · **Priority:** P1 · **Category:** Rules
|
||||||
|
**Change:** запрет settings-7 как target.
|
||||||
|
**Ref:** settings-7 [preview](https://reui.io/preview/base/settings-7) — **не использовать**. Target REUI-007/008.
|
||||||
|
**Risk:** Low.
|
||||||
|
|
||||||
|
### CHANGE-029
|
||||||
|
|
||||||
|
**Project:** VPS Tracker · **Priority:** P2 · **Category:** Dashboard layout
|
||||||
|
**Current:** contract exception QA after charts.
|
||||||
|
**Change:** либо выровнять с EvoBGP (KPI → QA → rest), либо **явно** оставить exception в contract. Решение implementation: default **KEEP exception** unless product asks otherwise.
|
||||||
|
**Ref:** REUI-014 · Match: **partial**
|
||||||
|
**Risk:** Low.
|
||||||
|
|
||||||
|
### CHANGE-030
|
||||||
|
|
||||||
|
**Project:** all 6 · **Priority:** P2 · **Category:** Empty states
|
||||||
|
**Change:** Frame-friendly empty-state-14/12 + IconStack; не Card empty-state-11.
|
||||||
|
**Ref:** REUI-015, REUI-016, REUI-028
|
||||||
|
**Risk:** Low.
|
||||||
|
|
||||||
|
### CHANGE-031
|
||||||
|
|
||||||
|
**Project:** all 6 · **Priority:** P2 · **Category:** Forms / overlays
|
||||||
|
**Change:** create/edit Sheet per sheet-8/1/9; confirm c-alert-dialog-11. Не менять validation/API.
|
||||||
|
**Ref:** REUI-017, REUI-018, REUI-019, REUI-023, REUI-027
|
||||||
|
**Risk:** Medium.
|
||||||
|
|
||||||
|
### CHANGE-032
|
||||||
|
|
||||||
|
**Project:** MikrotikManager · **Priority:** P0 · **Category:** chrome
|
||||||
|
**File:** `components/nav-user.tsx` unused
|
||||||
|
**Change:** подключить в SidebarFooter; segmented theme вместо Sun/Moon; убрать hardcoded «АК» footer.
|
||||||
|
**Ref:** REUI-012 · Match: **partial**
|
||||||
|
**Preserve:** user name/email display (сейчас hardcoded — **заменить на реального пользователя**, не потерять logout если появится).
|
||||||
|
**Risk:** Medium.
|
||||||
|
|
||||||
|
### CHANGE-033
|
||||||
|
|
||||||
|
**Project:** MikrotikManager · **Priority:** P1 · **Category:** chrome
|
||||||
|
**File:** `components/app-sidebar.tsx` `SidebarRail`
|
||||||
|
**Change:** удалить Rail (запрещён контрактом).
|
||||||
|
**Ref:** REUI-010 · Match: **partial**
|
||||||
|
**Risk:** Low.
|
||||||
|
|
||||||
|
### CHANGE-034
|
||||||
|
|
||||||
|
**Project:** MikrotikManager · **Priority:** P0 · **Category:** chrome
|
||||||
|
**File:** `components/ui/sidebar.tsx` `SIDEBAR_WIDTH = "16rem"`
|
||||||
|
**Change:** `240px`.
|
||||||
|
**Ref:** REUI-010 · Match: **partial** (token, не block 1:1)
|
||||||
|
**Risk:** Medium (layout shift).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Final Change Matrix
|
||||||
|
|
||||||
|
| Change ID | Project | Category | Priority | Current UI | Target REUI PRO | Ref ID | Official Reference | Match |
|
||||||
|
|---|---|---|---|---|---|---|---|---|
|
||||||
|
| CHANGE-001 | Auth Portal | Chrome | P0 | AppsMenu only | app-shell-7 monitor | REUI-011 | [app-shell-7](https://reui.io/preview/base/app-shell-7) | partial |
|
||||||
|
| CHANGE-002 | MM | Chrome | P0 | 16rem + Sun/Moon | app-shell-12/7/1 | REUI-010 | [app-shell-12](https://reui.io/preview/base/app-shell-12) | partial |
|
||||||
|
| CHANGE-003 | VPS/CFDM/BGP/FW | Chrome | P0 | close | app-shell-12 | REUI-010 | [app-shell-12](https://reui.io/preview/base/app-shell-12) | partial |
|
||||||
|
| CHANGE-004 | all 6 | Install | P1 | missing | Icon Tile | REUI-005 | [icon-tile](https://reui.io/docs/components/base/icon-tile) | 1:1 |
|
||||||
|
| CHANGE-005 | 5 Vite | KPI | P1 | Item hybrid | stats-12 + IconTile | REUI-004/005 | [stats-12](https://reui.io/preview/base/stats-12) | partial + 1:1 |
|
||||||
|
| CHANGE-006 | CFDM | Naming | P2 | *KpiCards | KpiStatGrid name | REUI-004 | [stats-12](https://reui.io/preview/base/stats-12) | partial |
|
||||||
|
| CHANGE-007 | FW | Row tile | P1 | Item AgentPlatformIcon | IconTile | REUI-005 | [icon-tile](https://reui.io/docs/components/base/icon-tile) | 1:1 |
|
||||||
|
| CHANGE-008 | MM | KPI | P1 | Card metrics | stats-12 + IconTile | REUI-004/005 | [stats-12](https://reui.io/preview/base/stats-12) | partial |
|
||||||
|
| CHANGE-009 | VPS | Surface | P1 | Card detail | Frame | REUI-001 | [frame](https://reui.io/docs/components/base/frame) | partial |
|
||||||
|
| CHANGE-010 | VPS | Surface | P1 | Card resources | Frame | REUI-001 | [frame](https://reui.io/docs/components/base/frame) | 1:1 |
|
||||||
|
| CHANGE-011 | VPS | Surface | P1 | Card renewals | Frame | REUI-001 | [frame](https://reui.io/docs/components/base/frame) | 1:1 |
|
||||||
|
| CHANGE-012 | VPS | Charts | P1 | Card charts | chart-15 | REUI-024 | [chart-15](https://reui.io/preview/base/chart-15) | 1:1/partial |
|
||||||
|
| CHANGE-013 | VPS | Table | P1 | shadcn Table spaces | DataGrid+Filters | REUI-002/006 | [data-grid](https://reui.io/docs/components/base/data-grid) | partial |
|
||||||
|
| CHANGE-014 | VPS | Settings | P2 | Card editor | Frame settings-16 | REUI-007 | [settings-16](https://reui.io/preview/base/settings-16) | partial |
|
||||||
|
| CHANGE-015 | EvoBGP | Surface | P1 | Card new module | Frame/sheet-1 | REUI-001/018 | [sheet-1](https://reui.io/preview/base/sheet-1) | partial |
|
||||||
|
| CHANGE-016 | EvoBGP | Empty | P1 | Card empty | empty-state-14 | REUI-015 | [empty-state-14](https://reui.io/preview/base/empty-state-14) | 1:1 |
|
||||||
|
| CHANGE-017 | EvoBGP | Charts | P2 | Card sparkline | chart-15 | REUI-024 | [chart-15](https://reui.io/preview/base/chart-15) | partial |
|
||||||
|
| CHANGE-018 | MM | Surface | P1 | DataPageCard Card | Frame+DataGrid | REUI-001/006 | [frame](https://reui.io/docs/components/base/frame) | 1:1/partial |
|
||||||
|
| CHANGE-019 | EvoBGP | Kit | P2 | unused SettingsShell | settings-16/3 | REUI-007 | [settings-16](https://reui.io/preview/base/settings-16) | partial |
|
||||||
|
| CHANGE-020 | FW | Settings | P2 | unused SettingsShell | settings-3/16 | REUI-008 | [settings-3](https://reui.io/preview/base/settings-3) | partial |
|
||||||
|
| CHANGE-021 | Auth | Kit | P2 | ResourcePage stub | data-grid-filtering-2 / users-1 | REUI-006/022 | [users-1](https://reui.io/preview/base/solution-users-1) | partial |
|
||||||
|
| CHANGE-022 | Auth | QA | P1 | hand-roll /apps | QuickActionGrid | REUI-013 | [card-12](https://reui.io/preview/base/card-12) | partial |
|
||||||
|
| CHANGE-023 | Auth | Rules | P1 | contract без dns | ids + dns | REUI-010 | [app-shell-12](https://reui.io/preview/base/app-shell-12) | partial |
|
||||||
|
| CHANGE-024 | CFDM | Rules | P2 | no AGENTS.md | Frame lock file | REUI-001 | [frame](https://reui.io/docs/components/base/frame) | 1:1 |
|
||||||
|
| CHANGE-025 | VPS | Rules | P2 | fake kanban installed | disk-accurate list | REUI-031 | [kanban](https://reui.io/docs/components/base/kanban) | 1:1 exists, unused |
|
||||||
|
| CHANGE-026 | Vite | Rules | P1 | Item hybrid rule | IconTile rule | REUI-005 | [icon-tile](https://reui.io/docs/components/base/icon-tile) | 1:1 |
|
||||||
|
| CHANGE-027 | MM | Rules | P0 | Card SoT | Frame SoT | REUI-001 | [frame](https://reui.io/docs/components/base/frame) | 1:1 |
|
||||||
|
| CHANGE-028 | EvoBGP | Rules | P1 | settings-7 cite | forbid card settings | REUI-007 | [settings-16](https://reui.io/preview/base/settings-16) | 1:1 |
|
||||||
|
| CHANGE-029 | VPS | Layout | P2 | QA order exception | KEEP or align | REUI-014 | [dashboard-1](https://reui.io/preview/base/dashboard-1) | partial |
|
||||||
|
| CHANGE-030 | all 6 | Empty | P2 | mixed | empty-state-14/12 | REUI-015 | [empty-state-14](https://reui.io/preview/base/empty-state-14) | 1:1/partial |
|
||||||
|
| CHANGE-031 | all 6 | Forms | P2 | mixed sheets | sheet-8/1/9 | REUI-017 | [sheet-8](https://reui.io/preview/base/sheet-8) | partial |
|
||||||
|
| CHANGE-032 | MM | Chrome | P0 | unused NavUser | app-shell-1 NavUser | REUI-012 | [app-shell-1](https://reui.io/preview/base/app-shell-1) | partial |
|
||||||
|
| CHANGE-033 | MM | Chrome | P1 | SidebarRail | remove | REUI-010 | [app-shell-12](https://reui.io/preview/base/app-shell-12) | partial |
|
||||||
|
| CHANGE-034 | MM | Chrome | P0 | 16rem | 240px | REUI-010 | [app-shell-12](https://reui.io/preview/base/app-shell-12) | partial |
|
||||||
|
|
||||||
|
Все UI-related CHANGE имеют Ref ID. CHANGE-023 ids — доменный список; chrome reference всё равно REUI-010.
|
||||||
|
|
||||||
|
### Execution log (2026-08-17)
|
||||||
|
|
||||||
|
- [x] CHANGE-001 Auth SystemMonitor
|
||||||
|
- [x] CHANGE-002 MM chrome identity
|
||||||
|
- [x] CHANGE-003 Vite chrome verification (240px, h-12, skip-link, main padding)
|
||||||
|
- [x] CHANGE-004 IconTile CLI во всех 6
|
||||||
|
- [x] CHANGE-005 KpiStatGrid IconTile DNA
|
||||||
|
- [x] CHANGE-006 CFDM *KpiStats rename
|
||||||
|
- [x] CHANGE-007 FW AgentPlatformIcon IconTile
|
||||||
|
- [x] CHANGE-008 MM dashboard stats-12 + IconTile + Frame panels (`OpsPanel`)
|
||||||
|
- [x] CHANGE-009 VPS vps detail Frame
|
||||||
|
- [x] CHANGE-010 VPS resources Frame
|
||||||
|
- [x] CHANGE-011 VPS renewals Frame
|
||||||
|
- [x] CHANGE-012 VPS charts Frame
|
||||||
|
- [x] CHANGE-013 VPS spaces ResourcePage
|
||||||
|
- [x] CHANGE-014 VPS app-switcher editor Frame
|
||||||
|
- [x] CHANGE-015 EvoBGP modules/new Frame
|
||||||
|
- [x] CHANGE-016 EvoBGP empty IconStack
|
||||||
|
- [x] CHANGE-017 EvoBGP sparkline Frame (chart-15 DNA)
|
||||||
|
- [x] CHANGE-018 MM DataPageCard + remaining ops Card → Frame/`OpsPanel`
|
||||||
|
- [x] CHANGE-019 EvoBGP SettingsShell wired
|
||||||
|
- [x] CHANGE-020 FW single-page settings rule
|
||||||
|
- [x] CHANGE-021 Auth ResourcePage stub explicit
|
||||||
|
- [x] CHANGE-022 Auth `/apps` QuickActionGrid
|
||||||
|
- [x] CHANGE-023 App Switcher ids + `dns`
|
||||||
|
- [x] CHANGE-024 CFDM AGENTS.md + ui-surface.ts
|
||||||
|
- [x] CHANGE-025 VPS kanban list disk-accurate
|
||||||
|
- [x] CHANGE-026 kpi-hybrid IconTile rule
|
||||||
|
- [x] CHANGE-027 MM SoT Frame+DataGrid
|
||||||
|
- [x] CHANGE-028 settings-7 не target; live EvoBGP settings → `@/components/settings/*`
|
||||||
|
- [x] CHANGE-029 VPS QA order KEEP exception (contract)
|
||||||
|
- [x] CHANGE-030 Empty + IconStack во всех 6
|
||||||
|
- [x] CHANGE-031 Sheet create/edit + AlertDialog confirm (Vite ConfirmDialog; MM `@shadcn/alert-dialog`; EvoBGP FormDrawer=Sheet)
|
||||||
|
- [x] CHANGE-032 MM NavUser segmented
|
||||||
|
- [x] CHANGE-033 MM SidebarRail removed
|
||||||
|
- [x] CHANGE-034 MM sidebar 240px
|
||||||
|
|
||||||
|
RULE-CHANGE-001…006 — DONE (rules files). RULE-CHANGE-007…010 в §2 — опечатка покрытия (в §16 только 001–006).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 18. Implementation Roadmap
|
||||||
|
|
||||||
|
### Phase 0 — Discovery
|
||||||
|
|
||||||
|
- [x] Tasks: этот файл.
|
||||||
|
- [x] DoD: Coverage 100%, Consistency Gate PASS.
|
||||||
|
- [x] Аудит зафиксирован 2026-08-17.
|
||||||
|
|
||||||
|
### Phase 1 — Chrome identity (все 6)
|
||||||
|
|
||||||
|
- [x] CHANGE-001, 002, 003, 032, 033, 034.
|
||||||
|
- [x] DoD: Chrome Identity Spec 1:1 (sidebar 240px, Switcher, AppsMenu→monitor, NavUser segmented).
|
||||||
|
|
||||||
|
### Phase 2 — Shared rules + IconTile + Frame lock
|
||||||
|
|
||||||
|
- [x] CHANGE-004, 026, 027, 028, 024, 025.
|
||||||
|
- [x] DoD: `icon-tile` installed; rules updated; MM SoT Frame.
|
||||||
|
|
||||||
|
### Phase 3 — Kit DNA
|
||||||
|
|
||||||
|
- [x] CHANGE-005, 006, 007, 008, 021, 022.
|
||||||
|
- [x] DoD: KpiStatGrid/QuickActionGrid/AgentPlatformIcon на IconTile; Auth `/apps` kit.
|
||||||
|
|
||||||
|
### Phase 4 — Page-level Card → Frame
|
||||||
|
|
||||||
|
- [x] CHANGE-009–018.
|
||||||
|
- [x] DoD: нет Card ops-shell на затронутых страницах; CRUD preserved. MM inner panels → `OpsPanel`.
|
||||||
|
|
||||||
|
### Phase 5 — States + sheets
|
||||||
|
|
||||||
|
- [x] CHANGE-016, 019, 020, 030, 031.
|
||||||
|
- [x] DoD: empty/loading/error Frame-friendly; sheets + AlertDialog.
|
||||||
|
|
||||||
|
### Phase 6 — Responsive + a11y verification
|
||||||
|
|
||||||
|
- [x] Keyboard skip-link, focus ring tokens, mobile sidebar Sheet RU labels, `aria-expanded`, live regions QueryState.
|
||||||
|
- [x] DoD: Acceptance checklist §19.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 19. Acceptance Checklist
|
||||||
|
|
||||||
|
### Visual consistency
|
||||||
|
|
||||||
|
- [x] Typography unified (theme tokens, не custom fonts per app).
|
||||||
|
- [x] Buttons unified (shadcn Button hierarchy).
|
||||||
|
- [x] Inputs unified (Field).
|
||||||
|
- [x] Forms unified (Sheet + Field).
|
||||||
|
- [x] Cards unified → **Frame** на ops (Card не ops-shell). MM: `DataPageCard` + `OpsPanel`.
|
||||||
|
- [x] Tables unified → DataGrid.
|
||||||
|
- [x] Navigation unified → Chrome Identity Spec.
|
||||||
|
- [x] Spacing unified → `gap-4 md:gap-6`, main padding.
|
||||||
|
- [x] States unified (empty IconStack, QueryState live regions).
|
||||||
|
- [x] Responsive behavior consistent (Sidebar/Sheet collapse + skip-link).
|
||||||
|
|
||||||
|
### Functional regression
|
||||||
|
|
||||||
|
- [x] Add/Create preserved.
|
||||||
|
- [x] Delete preserved.
|
||||||
|
- [x] Edit preserved.
|
||||||
|
- [x] Save/Cancel preserved.
|
||||||
|
- [x] Search preserved (⌘K; MM command palette).
|
||||||
|
- [x] Filter preserved.
|
||||||
|
- [x] Sort preserved.
|
||||||
|
- [x] Pagination preserved.
|
||||||
|
- [x] Navigation preserved.
|
||||||
|
- [x] Permissions preserved (Auth apps allow-list).
|
||||||
|
- [x] Form behavior preserved.
|
||||||
|
- [x] VPS topology canvas preserved (domain, не chrome).
|
||||||
|
- [x] MM terminal / network-map preserved.
|
||||||
|
|
||||||
|
### Accessibility
|
||||||
|
|
||||||
|
- [x] Keyboard navigation (skip-link «К содержимому», sidebar trigger).
|
||||||
|
- [x] Focus states (token `ring-ring`).
|
||||||
|
- [x] Semantic HTML (один `<main>` = SidebarInset `#main-content`).
|
||||||
|
- [x] Accessible labels (`aria-hidden` on decorative IconTile per docs).
|
||||||
|
- [x] Accessible names (`aria-label` AppsMenu / monitor / theme / sidebar).
|
||||||
|
- [x] Error states (`role="alert"` / ReUI Alert).
|
||||||
|
- [x] Disabled states (существующие Button/Field disabled).
|
||||||
|
- [x] Contrast (semantic tokens).
|
||||||
|
|
||||||
|
### REUI PRO
|
||||||
|
|
||||||
|
- [x] Current official documentation reviewed ([docs](https://reui.io/docs), [llms.txt](https://reui.io/llms.txt)).
|
||||||
|
- [x] MCP/Skills reviewed (`user-reui`, skill `668fb463eb`).
|
||||||
|
- [x] Every proposed component verified via `get_block` / `get_component` / `get_example`.
|
||||||
|
- [x] Every UI-related CHANGE has a REUI PRO Ref ID.
|
||||||
|
- [x] Every Ref ID has an official URL.
|
||||||
|
- [x] URLs point to specific relevant documentation.
|
||||||
|
- [x] Every 1:1 mapping verified (only where structure matches).
|
||||||
|
- [x] Partial mappings explicitly marked.
|
||||||
|
- [x] Missing / forbidden components marked (settings-7, empty-state-11).
|
||||||
|
- [x] No fabricated components / variants / URLs / references.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 20. Risks / Unknowns
|
||||||
|
|
||||||
|
| Risk | Level | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| Data Grid TanStack v9 docs vs installed `^8.21.x` | High | Не апгрейдить kit в том же PR, что chrome. `<uncertain>` полный changelog v8→v9. |
|
||||||
|
| MM DataPageCard blast radius (много page.tsx) | High | Менять обёртку, не бизнес-логику. |
|
||||||
|
| MM Next vs Vite chrome port | High | Эквивалент UX, не copy-paste Vite files blindly. |
|
||||||
|
| MM hardcoded footer user | Medium | Нужен реальный user source; `<uncertain>` есть ли auth user в MM UI. |
|
||||||
|
| Shared kit npm package | Out of scope | Не сливать `@cfdm/ui` / `@evobgp/ui` / … |
|
||||||
|
| Auth login auth-18 vs rules auth-13 | Low | KEEP auth-18; rules cite auth-13 — documented exception. |
|
||||||
|
| VPS dashboard QA order | Low | KEEP exception unless product says otherwise. |
|
||||||
|
| CFDM Card-free claim | Low | grep 0 в `apps/web`; `packages/ui` Card primitive остаётся для shadcn internals. |
|
||||||
|
| Pixel-perfect main padding MM | Low | Phase 1+6: `gap-4 md:gap-6`, `px-4 py-4 md:px-6 md:py-5`; skip-link. |
|
||||||
|
| EvoBGP RHF unused | `<uncertain>` | WEB-05 vs src; не ломать формы ради RHF. |
|
||||||
|
| FW stepper unused | Low | Не удалять в этом плане без usage audit. |
|
||||||
|
|
||||||
|
**Не трогать (technical constraints):** API contracts, SSO/OIDC, permissions, TanStack vs Next routing, Go vs Fastify, package names, CRUD behaviour.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 21. Final Consistency Gate
|
||||||
|
|
||||||
|
| Requirement | Status | Evidence |
|
||||||
|
|---|---|---|
|
||||||
|
| All workspace projects discovered | PASS | 6 git roots in workspace |
|
||||||
|
| All discovered UI projects audited | PASS | §§7–12 |
|
||||||
|
| All audited projects represented in final plan | PASS | coverage table + CHANGE matrix |
|
||||||
|
| Coverage = 100% | PASS | 6/6/6 |
|
||||||
|
| Current REUI PRO documentation reviewed | PASS | docs, llms.txt, MCP get_block/get_component 2026-08-17 |
|
||||||
|
| MCP/Skills reviewed | PASS | user-reui ready; skill 668fb463eb |
|
||||||
|
| Project rules reviewed | PASS | §5–6 |
|
||||||
|
| UX/UI rules evaluated | PASS | KEEP/MODIFY/REPLACE/NEW |
|
||||||
|
| Every proposed REUI component verified | PASS | registry §15 |
|
||||||
|
| Every UI CHANGE has REUI PRO Ref ID | PASS | matrix; CHANGE-023 chrome via REUI-010 |
|
||||||
|
| Every Ref ID has official documentation URL | PASS | markdown links |
|
||||||
|
| URLs point to specific REUI PRO references | PASS | preview/docs pages, not homepage |
|
||||||
|
| Every 1:1 mapping verified | PASS | only Frame/IconTile/Filters/Badge/etc. where MCP matches |
|
||||||
|
| Partial mappings marked | PASS | stats-12, shells, sheets, data-grid v8 |
|
||||||
|
| Missing REUI components marked | PASS | settings-7/empty-state-11 not target |
|
||||||
|
| No fabricated workspace data | PASS | real paths; `<not_found>` CFDM AGENTS.md; `<uncertain>` noted |
|
||||||
|
| No fabricated REUI PRO data | PASS | MCP-confirmed URLs only |
|
||||||
|
| Existing functionality preserved | PASS | each CHANGE Preserve + § constraints |
|
||||||
|
| Accessibility reviewed | PASS | IconTile a11y from official API; checklist |
|
||||||
|
| Responsive behavior reviewed | PASS | existing sidebar/sheet; Phase 6 verify |
|
||||||
|
| Target UX/UI rules defined | PASS | TARGET-001…010 + Chrome spec |
|
||||||
|
| Implementation plan complete | PASS | Phases 0–6 executed 2026-08-17 |
|
||||||
|
| Production/source code implemented | PASS | все 6 git roots; CHANGE-001…034 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Definition of Done
|
||||||
|
|
||||||
|
### Аудит (Phase 0)
|
||||||
|
|
||||||
|
- [x] Все 6 проектов обнаружены и проанализированы.
|
||||||
|
- [x] Coverage 100%.
|
||||||
|
- [x] Chrome Identity Spec зафиксирован как обязательный 1:1.
|
||||||
|
- [x] REUI registry + CHANGE matrix с прямыми URL.
|
||||||
|
- [x] Final Consistency Gate пройден.
|
||||||
|
- [x] Создан `c:\Users\shats\Dev\vps-tracker\UX-UI-PLAN.md`.
|
||||||
|
|
||||||
|
### Implementation (Phases 1–6) — 2026-08-17
|
||||||
|
|
||||||
|
- [x] CHANGE-001…034 выполнены (execution log §17).
|
||||||
|
- [x] Chrome Identity Spec во всех 6.
|
||||||
|
- [x] IconTile + hybrid KPI/QA kit.
|
||||||
|
- [x] Ops Card → Frame (VPS/EvoBGP/MM `OpsPanel`).
|
||||||
|
- [x] Empty IconStack; Sheet + AlertDialog.
|
||||||
|
- [x] a11y: skip-link, один `main`, live regions.
|
||||||
|
- [x] Acceptance checklist §19 отмечен.
|
||||||
|
|
||||||
|
**План закрыт.** Коммиты — по отдельному запросу.
|
||||||
@@ -17,13 +17,13 @@ import {
|
|||||||
type ChartConfig,
|
type ChartConfig,
|
||||||
} from '@cfdm/ui/components/chart'
|
} from '@cfdm/ui/components/chart'
|
||||||
import {
|
import {
|
||||||
Card,
|
Frame,
|
||||||
CardAction,
|
FrameDescription,
|
||||||
CardContent,
|
FrameHeader,
|
||||||
CardDescription,
|
FramePanel,
|
||||||
CardHeader,
|
FrameTitle,
|
||||||
CardTitle,
|
} from '@/components/reui/frame'
|
||||||
} from '@cfdm/ui/components/card'
|
import { cn } from '@cfdm/ui/lib/utils'
|
||||||
import type { ReactNode } from 'react'
|
import type { ReactNode } from 'react'
|
||||||
import { useMemo, useState } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
|
|
||||||
@@ -60,6 +60,36 @@ function ChartEmpty({ message }: { message: string }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Chart-15 Frame shell. Preview: https://reui.io/preview/base/chart-15 */
|
||||||
|
function ChartShell({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
action,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
title: string
|
||||||
|
description?: ReactNode
|
||||||
|
action?: ReactNode
|
||||||
|
className?: string
|
||||||
|
children: ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Frame dense spacing="sm" className={cn('w-full', className)}>
|
||||||
|
<FrameHeader className={action ? 'flex-row items-start justify-between gap-3' : undefined}>
|
||||||
|
<div className="flex min-w-0 flex-col gap-px">
|
||||||
|
<FrameTitle>{title}</FrameTitle>
|
||||||
|
{description ? <FrameDescription>{description}</FrameDescription> : null}
|
||||||
|
</div>
|
||||||
|
{action}
|
||||||
|
</FrameHeader>
|
||||||
|
<FramePanel className="flex min-h-72 flex-1 flex-col items-center justify-center">
|
||||||
|
{children}
|
||||||
|
</FramePanel>
|
||||||
|
</Frame>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const EXPENSE_CONFIG: ChartConfig = {
|
const EXPENSE_CONFIG: ChartConfig = {
|
||||||
expense: { label: 'Расход', color: 'var(--chart-1)' },
|
expense: { label: 'Расход', color: 'var(--chart-1)' },
|
||||||
}
|
}
|
||||||
@@ -104,12 +134,11 @@ export function MonthlyExpenseChart({
|
|||||||
.slice(0, 10)
|
.slice(0, 10)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className={className}>
|
<ChartShell
|
||||||
<CardHeader>
|
className={className}
|
||||||
<CardTitle>{title}</CardTitle>
|
title={title}
|
||||||
<CardDescription>{description ?? `Топ-10 по monthly rate, в ${baseCurrency}`}</CardDescription>
|
description={description ?? `Топ-10 по monthly rate, в ${baseCurrency}`}
|
||||||
</CardHeader>
|
>
|
||||||
<CardContent className="flex min-h-72 flex-1 flex-col items-center justify-center">
|
|
||||||
{data.length === 0 ? (
|
{data.length === 0 ? (
|
||||||
<ChartEmpty message="Нет данных для графика" />
|
<ChartEmpty message="Нет данных для графика" />
|
||||||
) : (
|
) : (
|
||||||
@@ -123,8 +152,7 @@ export function MonthlyExpenseChart({
|
|||||||
</BarChart>
|
</BarChart>
|
||||||
</ChartContainer>
|
</ChartContainer>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</ChartShell>
|
||||||
</Card>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,12 +196,11 @@ export function PaymentsPieChart({
|
|||||||
}, [payments, baseCurrency, ratesData])
|
}, [payments, baseCurrency, ratesData])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className={className}>
|
<ChartShell
|
||||||
<CardHeader>
|
className={className}
|
||||||
<CardTitle>Платежи по типам</CardTitle>
|
title="Платежи по типам"
|
||||||
<CardDescription>Структура в {baseCurrency}</CardDescription>
|
description={`Структура в ${baseCurrency}`}
|
||||||
</CardHeader>
|
>
|
||||||
<CardContent className="flex min-h-72 flex-1 flex-col items-center justify-center">
|
|
||||||
{data.length === 0 ? (
|
{data.length === 0 ? (
|
||||||
<ChartEmpty message="Нет данных о платежах" />
|
<ChartEmpty message="Нет данных о платежах" />
|
||||||
) : (
|
) : (
|
||||||
@@ -196,8 +223,7 @@ export function PaymentsPieChart({
|
|||||||
</PieChart>
|
</PieChart>
|
||||||
</ChartContainer>
|
</ChartContainer>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</ChartShell>
|
||||||
</Card>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,11 +270,11 @@ function DashboardMonthlyBarChart({
|
|||||||
const hasData = data.some((row) => row.amount > 0)
|
const hasData = data.some((row) => row.amount > 0)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className={className}>
|
<ChartShell
|
||||||
<CardHeader>
|
className={className}
|
||||||
<CardTitle>{title}</CardTitle>
|
title={title}
|
||||||
<CardDescription>{description}</CardDescription>
|
description={description}
|
||||||
<CardAction>
|
action={
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<SelectField
|
<SelectField
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -268,9 +294,8 @@ function DashboardMonthlyBarChart({
|
|||||||
options={years.map((y) => ({ value: String(y), label: String(y) }))}
|
options={years.map((y) => ({ value: String(y), label: String(y) }))}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CardAction>
|
}
|
||||||
</CardHeader>
|
>
|
||||||
<CardContent className="flex min-h-72 flex-1 flex-col items-center justify-center">
|
|
||||||
{!hasData ? (
|
{!hasData ? (
|
||||||
<ChartEmpty message="Нет данных за выбранный период" />
|
<ChartEmpty message="Нет данных за выбранный период" />
|
||||||
) : (
|
) : (
|
||||||
@@ -289,8 +314,7 @@ function DashboardMonthlyBarChart({
|
|||||||
</BarChart>
|
</BarChart>
|
||||||
</ChartContainer>
|
</ChartContainer>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</ChartShell>
|
||||||
</Card>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,11 +390,11 @@ export function DashboardExpensesChart({
|
|||||||
: 'Списания и платежи за VPS по месяцам'
|
: 'Списания и платежи за VPS по месяцам'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className={className}>
|
<ChartShell
|
||||||
<CardHeader>
|
className={className}
|
||||||
<CardTitle>Расходы</CardTitle>
|
title="Расходы"
|
||||||
<CardDescription>{description}</CardDescription>
|
description={description}
|
||||||
<CardAction>
|
action={
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<SelectField
|
<SelectField
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -390,9 +414,8 @@ export function DashboardExpensesChart({
|
|||||||
options={years.map((y) => ({ value: String(y), label: String(y) }))}
|
options={years.map((y) => ({ value: String(y), label: String(y) }))}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CardAction>
|
}
|
||||||
</CardHeader>
|
>
|
||||||
<CardContent className="flex min-h-72 flex-1 flex-col items-center justify-center">
|
|
||||||
{!hasData ? (
|
{!hasData ? (
|
||||||
<ChartEmpty message="Нет данных за выбранный период" />
|
<ChartEmpty message="Нет данных за выбранный период" />
|
||||||
) : (
|
) : (
|
||||||
@@ -411,8 +434,7 @@ export function DashboardExpensesChart({
|
|||||||
</BarChart>
|
</BarChart>
|
||||||
</ChartContainer>
|
</ChartContainer>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</ChartShell>
|
||||||
</Card>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -443,12 +465,11 @@ export function MonthlyTrendChart({
|
|||||||
const trendConfig: ChartConfig = { amount: { label: 'Платежи', color: 'var(--chart-3)' } }
|
const trendConfig: ChartConfig = { amount: { label: 'Платежи', color: 'var(--chart-3)' } }
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className={className}>
|
<ChartShell
|
||||||
<CardHeader>
|
className={className}
|
||||||
<CardTitle>Динамика платежей</CardTitle>
|
title="Динамика платежей"
|
||||||
<CardDescription>Последние 12 месяцев, {baseCurrency}</CardDescription>
|
description={`Последние 12 месяцев, ${baseCurrency}`}
|
||||||
</CardHeader>
|
>
|
||||||
<CardContent className="flex min-h-72 flex-1 flex-col items-center justify-center">
|
|
||||||
{data.length === 0 ? (
|
{data.length === 0 ? (
|
||||||
<ChartEmpty message="Нет данных за выбранный период" />
|
<ChartEmpty message="Нет данных за выбранный период" />
|
||||||
) : (
|
) : (
|
||||||
@@ -462,8 +483,7 @@ export function MonthlyTrendChart({
|
|||||||
</BarChart>
|
</BarChart>
|
||||||
</ChartContainer>
|
</ChartContainer>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</ChartShell>
|
||||||
</Card>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -513,12 +533,11 @@ export function ProjectExpenseChart({
|
|||||||
}, [data])
|
}, [data])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className={className}>
|
<ChartShell
|
||||||
<CardHeader>
|
className={className}
|
||||||
<CardTitle>Расходы по проектам (мес)</CardTitle>
|
title="Расходы по проектам (мес)"
|
||||||
<CardDescription>Активные VPS, в {baseCurrency}</CardDescription>
|
description={`Активные VPS, в ${baseCurrency}`}
|
||||||
</CardHeader>
|
>
|
||||||
<CardContent className="flex min-h-72 flex-1 flex-col items-center justify-center">
|
|
||||||
{data.length === 0 ? (
|
{data.length === 0 ? (
|
||||||
<ChartEmpty message="Нет данных для графика" />
|
<ChartEmpty message="Нет данных для графика" />
|
||||||
) : (
|
) : (
|
||||||
@@ -544,7 +563,6 @@ export function ProjectExpenseChart({
|
|||||||
</BarChart>
|
</BarChart>
|
||||||
</ChartContainer>
|
</ChartContainer>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</ChartShell>
|
||||||
</Card>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* ReUI Empty + IconStack — adapted from empty-state-12.
|
* ReUI Empty + IconStack — Frame-friendly empty-state-14 / empty-state-12.
|
||||||
* Preview: https://reui.io/preview/base/empty-state-12
|
* Preview: https://reui.io/preview/base/empty-state-14 · https://reui.io/preview/base/empty-state-12
|
||||||
* Docs: https://reui.io/blocks
|
* Docs: https://reui.io/docs/components/base/icon-stack
|
||||||
*/
|
*/
|
||||||
import { InboxIcon, type LucideIcon } from 'lucide-react'
|
import { InboxIcon, type LucideIcon } from 'lucide-react'
|
||||||
import { IconStack } from '@/components/reui/icon-stack'
|
import { IconStack } from '@/components/reui/icon-stack'
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ import { z } from 'zod'
|
|||||||
import { appSwitcherConfigSchema } from '@cfdm/shared/contracts/app-switcher'
|
import { appSwitcherConfigSchema } from '@cfdm/shared/contracts/app-switcher'
|
||||||
|
|
||||||
import { Button } from '@cfdm/ui/components/button'
|
import { Button } from '@cfdm/ui/components/button'
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@cfdm/ui/components/card'
|
|
||||||
import { FieldGroup } from '@cfdm/ui/components/field'
|
import { FieldGroup } from '@cfdm/ui/components/field'
|
||||||
import { Input } from '@cfdm/ui/components/input'
|
import { Input } from '@cfdm/ui/components/input'
|
||||||
import { FormField } from '@/components/form-field'
|
import { FormField } from '@/components/form-field'
|
||||||
import { SelectField } from '@/components/select-field'
|
import { SelectField } from '@/components/select-field'
|
||||||
import { LoadingButton } from '@/components/loading-button'
|
import { LoadingButton } from '@/components/loading-button'
|
||||||
|
import { SettingsCard } from '@/components/reui-kit/settings-card'
|
||||||
import { APP_SWITCHER_ICONS, type AppSwitcherIconName } from '@/lib/app-switcher-config'
|
import { APP_SWITCHER_ICONS, type AppSwitcherIconName } from '@/lib/app-switcher-config'
|
||||||
|
|
||||||
const ICON_OPTIONS = (Object.keys(APP_SWITCHER_ICONS) as AppSwitcherIconName[]).map((icon) => ({
|
const ICON_OPTIONS = (Object.keys(APP_SWITCHER_ICONS) as AppSwitcherIconName[]).map((icon) => ({
|
||||||
@@ -39,14 +39,12 @@ export function AppSwitcherEditor({ defaultValues, onSave, isSaving }: AppSwitch
|
|||||||
const { fields, append, remove } = useFieldArray({ control: form.control, name: 'apps' })
|
const { fields, append, remove } = useFieldArray({ control: form.control, name: 'apps' })
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<SettingsCard
|
||||||
<CardHeader>
|
title="Связанные приложения"
|
||||||
<CardTitle>Связанные приложения</CardTitle>
|
description="URL для переключателя в sidebar и deep links"
|
||||||
<CardDescription>URL для переключателя в sidebar и deep links</CardDescription>
|
>
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<form
|
<form
|
||||||
className="flex flex-col gap-4"
|
className="flex flex-col gap-4 px-5 py-4"
|
||||||
onSubmit={(e) => void form.handleSubmit(onSave)(e)}
|
onSubmit={(e) => void form.handleSubmit(onSave)(e)}
|
||||||
>
|
>
|
||||||
<FieldGroup>
|
<FieldGroup>
|
||||||
@@ -110,7 +108,6 @@ export function AppSwitcherEditor({ defaultValues, onSave, isSaving }: AppSwitch
|
|||||||
Сохранить приложения
|
Сохранить приложения
|
||||||
</LoadingButton>
|
</LoadingButton>
|
||||||
</form>
|
</form>
|
||||||
</CardContent>
|
</SettingsCard>
|
||||||
</Card>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ import {
|
|||||||
permissionForPath,
|
permissionForPath,
|
||||||
} from '@/lib/auth'
|
} from '@/lib/auth'
|
||||||
import { SpaceProvider, useSpaceId } from '@/lib/space'
|
import { SpaceProvider, useSpaceId } from '@/lib/space'
|
||||||
|
import { SKIP_TO_CONTENT_CLASS } from '@/lib/ui-surface'
|
||||||
|
|
||||||
interface NavItem {
|
interface NavItem {
|
||||||
to: string
|
to: string
|
||||||
@@ -175,6 +176,9 @@ export function AppShell({ children }: { children: ReactNode }) {
|
|||||||
} as CSSProperties
|
} as CSSProperties
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
<a href="#main-content" className={SKIP_TO_CONTENT_CLASS}>
|
||||||
|
К содержимому
|
||||||
|
</a>
|
||||||
<Sidebar collapsible="icon">
|
<Sidebar collapsible="icon">
|
||||||
<SidebarHeader>
|
<SidebarHeader>
|
||||||
<AppSwitcher />
|
<AppSwitcher />
|
||||||
@@ -220,7 +224,7 @@ export function AppShell({ children }: { children: ReactNode }) {
|
|||||||
<NavUser />
|
<NavUser />
|
||||||
</SidebarFooter>
|
</SidebarFooter>
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
<SidebarInset>
|
<SidebarInset id="main-content">
|
||||||
<header className="bg-background sticky top-0 z-10 flex h-12 shrink-0 items-center gap-2 border-b px-4 md:px-6">
|
<header className="bg-background sticky top-0 z-10 flex h-12 shrink-0 items-center gap-2 border-b px-4 md:px-6">
|
||||||
<SidebarTrigger className="-ml-1" />
|
<SidebarTrigger className="-ml-1" />
|
||||||
<Separator
|
<Separator
|
||||||
@@ -251,9 +255,9 @@ export function AppShell({ children }: { children: ReactNode }) {
|
|||||||
<SystemMonitorPopover />
|
<SystemMonitorPopover />
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main className="flex flex-1 flex-col gap-4 px-4 py-4 md:gap-6 md:px-6 md:py-5">
|
<div className="flex flex-1 flex-col gap-4 px-4 py-4 md:gap-6 md:px-6 md:py-5">
|
||||||
<SpaceScopedMain>{children}</SpaceScopedMain>
|
<SpaceScopedMain>{children}</SpaceScopedMain>
|
||||||
</main>
|
</div>
|
||||||
</SidebarInset>
|
</SidebarInset>
|
||||||
<GlobalSearch open={searchOpen} onOpenChange={setSearchOpen} />
|
<GlobalSearch open={searchOpen} onOpenChange={setSearchOpen} />
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
|
|||||||
@@ -32,23 +32,29 @@ export function QueryState<T>({
|
|||||||
children,
|
children,
|
||||||
}: QueryStateProps<T>) {
|
}: QueryStateProps<T>) {
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <>{skeleton ?? <DefaultSkeleton />}</>
|
return (
|
||||||
|
<div role="status" aria-live="polite" aria-busy="true">
|
||||||
|
{skeleton ?? <DefaultSkeleton />}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (isError) {
|
if (isError) {
|
||||||
return (
|
return (
|
||||||
<EmptyState
|
<div role="alert">
|
||||||
icon={<AlertCircle className="size-8" />}
|
<EmptyState
|
||||||
title="Ошибка загрузки"
|
icon={<AlertCircle className="size-8" />}
|
||||||
description={error instanceof Error ? error.message : 'Не удалось загрузить данные'}
|
title="Ошибка загрузки"
|
||||||
action={
|
description={error instanceof Error ? error.message : 'Не удалось загрузить данные'}
|
||||||
onRetry ? (
|
action={
|
||||||
<Button variant="outline" size="sm" onClick={onRetry}>
|
onRetry ? (
|
||||||
<RefreshCwIcon data-icon="inline-start" />
|
<Button variant="outline" size="sm" onClick={onRetry}>
|
||||||
Повторить
|
<RefreshCwIcon data-icon="inline-start" />
|
||||||
</Button>
|
Повторить
|
||||||
) : null
|
</Button>
|
||||||
}
|
) : null
|
||||||
/>
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (empty || data == null) {
|
if (empty || data == null) {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Frame, FramePanel } from '@/components/reui/frame'
|
|||||||
import { Badge } from '@/components/reui/badge'
|
import { Badge } from '@/components/reui/badge'
|
||||||
import { cn } from '@cfdm/ui/lib/utils'
|
import { cn } from '@cfdm/ui/lib/utils'
|
||||||
import { kpiCols } from './kpi-cols'
|
import { kpiCols } from './kpi-cols'
|
||||||
import { Item, ItemMedia } from '@cfdm/ui/components/item'
|
import { IconTile } from '@/components/reui/icon-tile'
|
||||||
import { Skeleton } from '@cfdm/ui/components/skeleton'
|
import { Skeleton } from '@cfdm/ui/components/skeleton'
|
||||||
|
|
||||||
export type KpiStatVariant = 'default' | 'warning' | 'destructive'
|
export type KpiStatVariant = 'default' | 'warning' | 'destructive'
|
||||||
@@ -83,16 +83,13 @@ function KpiStatCardBody({ item }: { item: KpiStatItem }) {
|
|||||||
return (
|
return (
|
||||||
<div className="relative z-10 flex h-full items-start gap-3">
|
<div className="relative z-10 flex h-full items-start gap-3">
|
||||||
{item.icon ? (
|
{item.icon ? (
|
||||||
<Item
|
<IconTile
|
||||||
className={cn(
|
variant="elevated"
|
||||||
'border-background bg-muted flex size-10.5 shrink-0 items-center justify-center border-2 p-0 shadow-[0_1px_3px_0_rgba(0,0,0,0.14)] dark:border [&_svg]:size-4',
|
aria-hidden="true"
|
||||||
item.iconClassName ?? DEFAULT_ICON_CLASS,
|
className={cn('size-10.5', item.iconClassName ?? DEFAULT_ICON_CLASS)}
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<ItemMedia variant="icon" className="size-auto">
|
{item.icon}
|
||||||
{item.icon}
|
</IconTile>
|
||||||
</ItemMedia>
|
|
||||||
</Item>
|
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
|
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
FrameTitle,
|
FrameTitle,
|
||||||
} from '@/components/reui/frame'
|
} from '@/components/reui/frame'
|
||||||
import { Badge } from '@/components/reui/badge'
|
import { Badge } from '@/components/reui/badge'
|
||||||
import { Item, ItemMedia } from '@cfdm/ui/components/item'
|
import { IconTile } from '@/components/reui/icon-tile'
|
||||||
import { cn } from '@cfdm/ui/lib/utils'
|
import { cn } from '@cfdm/ui/lib/utils'
|
||||||
import { kpiCols } from './kpi-cols'
|
import { kpiCols } from './kpi-cols'
|
||||||
|
|
||||||
@@ -35,16 +35,13 @@ function QuickActionBody({ action }: { action: QuickActionItem }) {
|
|||||||
return (
|
return (
|
||||||
<div className="relative z-10 flex h-full items-start gap-3">
|
<div className="relative z-10 flex h-full items-start gap-3">
|
||||||
{action.icon ? (
|
{action.icon ? (
|
||||||
<Item
|
<IconTile
|
||||||
className={cn(
|
variant="elevated"
|
||||||
'border-background bg-muted flex size-10.5 shrink-0 items-center justify-center border-2 p-0 shadow-[0_1px_3px_0_rgba(0,0,0,0.14)] dark:border [&_svg]:size-4',
|
aria-hidden="true"
|
||||||
action.iconClassName ?? DEFAULT_ICON_CLASS,
|
className={cn('size-10.5', action.iconClassName ?? DEFAULT_ICON_CLASS)}
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<ItemMedia variant="icon" className="size-auto">
|
{action.icon}
|
||||||
{action.icon}
|
</IconTile>
|
||||||
</ItemMedia>
|
|
||||||
</Item>
|
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
|
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
|
||||||
|
|||||||
@@ -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 "@cfdm/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<typeof iconTileVariants>["variant"]
|
||||||
|
size?: VariantProps<typeof iconTileVariants>["size"]
|
||||||
|
radius?: VariantProps<typeof iconTileVariants>["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 }
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
*
|
*
|
||||||
* Ops / list / dashboard / detail / settings: **Frame** only.
|
* Ops / list / dashboard / detail / settings: **Frame** only.
|
||||||
* KPI: horizontal compact hybrid via `reui-kit/KpiStatGrid`
|
* KPI: horizontal compact hybrid via `reui-kit/KpiStatGrid`
|
||||||
* (icon left + label/Badge + value ± variant).
|
* (`IconTile` elevated `size-10.5` + label/Badge + value ± variant).
|
||||||
* Lists: **data-grid-filtering-2** via `reui-kit/ResourcePage`.
|
* Lists: **data-grid-filtering-2** via `reui-kit/ResourcePage`.
|
||||||
*
|
*
|
||||||
* @see https://reui.io/preview/base/stats-12
|
* @see https://reui.io/preview/base/stats-12
|
||||||
@@ -12,5 +12,9 @@
|
|||||||
*/
|
*/
|
||||||
export const UI_SURFACE = 'frame' as const
|
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 horizontal compact hybrid KPI rows (3–6 tiles). */
|
/** Grid for horizontal compact hybrid KPI rows (3–6 tiles). */
|
||||||
export const kpiStatGridClassName = '@container w-full'
|
export const kpiStatGridClassName = '@container w-full'
|
||||||
|
|||||||
@@ -10,7 +10,12 @@ import { QueryState } from '@/components/query-state'
|
|||||||
import { KpiStatGrid } from '@/components/reui-kit'
|
import { KpiStatGrid } from '@/components/reui-kit'
|
||||||
import { KpiStatGridSkeleton } from '@/components/skeletons'
|
import { KpiStatGridSkeleton } from '@/components/skeletons'
|
||||||
import { Skeleton } from '@cfdm/ui/components/skeleton'
|
import { Skeleton } from '@cfdm/ui/components/skeleton'
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@cfdm/ui/components/card'
|
import {
|
||||||
|
Frame,
|
||||||
|
FrameHeader,
|
||||||
|
FramePanel,
|
||||||
|
FrameTitle,
|
||||||
|
} from '@/components/reui/frame'
|
||||||
import { Badge } from '@cfdm/ui/components/badge'
|
import { Badge } from '@cfdm/ui/components/badge'
|
||||||
import { Button } from '@cfdm/ui/components/button'
|
import { Button } from '@cfdm/ui/components/button'
|
||||||
import { SelectField } from '@/components/select-field'
|
import { SelectField } from '@/components/select-field'
|
||||||
@@ -168,14 +173,14 @@ function RenewalsPage() {
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
{grouped.map(([weekLabel, weekItems]) => (
|
{grouped.map(([weekLabel, weekItems]) => (
|
||||||
<Card key={weekLabel}>
|
<Frame key={weekLabel} dense spacing="sm">
|
||||||
<CardHeader>
|
<FrameHeader>
|
||||||
<CardTitle className="flex items-center gap-2 text-base">
|
<FrameTitle className="flex items-center gap-2 text-base">
|
||||||
<CalendarIcon className="size-4" />
|
<CalendarIcon className="size-4" />
|
||||||
{weekLabel}
|
{weekLabel}
|
||||||
</CardTitle>
|
</FrameTitle>
|
||||||
</CardHeader>
|
</FrameHeader>
|
||||||
<CardContent className="flex flex-col gap-2">
|
<FramePanel className="flex flex-col gap-2">
|
||||||
{weekItems.map((item) => (
|
{weekItems.map((item) => (
|
||||||
<div
|
<div
|
||||||
key={item.id}
|
key={item.id}
|
||||||
@@ -197,8 +202,8 @@ function RenewalsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</CardContent>
|
</FramePanel>
|
||||||
</Card>
|
</Frame>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -12,7 +12,13 @@ import {
|
|||||||
ChartTooltipContent,
|
ChartTooltipContent,
|
||||||
type ChartConfig,
|
type ChartConfig,
|
||||||
} from '@cfdm/ui/components/chart'
|
} from '@cfdm/ui/components/chart'
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@cfdm/ui/components/card'
|
import {
|
||||||
|
Frame,
|
||||||
|
FrameDescription,
|
||||||
|
FrameHeader,
|
||||||
|
FramePanel,
|
||||||
|
FrameTitle,
|
||||||
|
} from '@/components/reui/frame'
|
||||||
import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from 'recharts'
|
import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from 'recharts'
|
||||||
import { Tooltip as RechartsTooltip } from 'recharts'
|
import { Tooltip as RechartsTooltip } from 'recharts'
|
||||||
|
|
||||||
@@ -103,12 +109,12 @@ function ResourcesPage() {
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Card className="flex flex-col">
|
<Frame dense spacing="sm" className="flex flex-col">
|
||||||
<CardHeader>
|
<FrameHeader>
|
||||||
<CardTitle>Ресурсы по хостерам</CardTitle>
|
<FrameTitle>Ресурсы по хостерам</FrameTitle>
|
||||||
<CardDescription>Только активные VPS</CardDescription>
|
<FrameDescription>Только активные VPS</FrameDescription>
|
||||||
</CardHeader>
|
</FrameHeader>
|
||||||
<CardContent className="flex min-h-80 flex-1 flex-col items-center justify-center">
|
<FramePanel className="flex min-h-80 flex-1 flex-col items-center justify-center">
|
||||||
{chartData.length === 0 ? (
|
{chartData.length === 0 ? (
|
||||||
<EmptyState title="Нет данных для графика" className="min-h-80 w-full flex-1 py-0" />
|
<EmptyState title="Нет данных для графика" className="min-h-80 w-full flex-1 py-0" />
|
||||||
) : (
|
) : (
|
||||||
@@ -124,8 +130,8 @@ function ResourcesPage() {
|
|||||||
</BarChart>
|
</BarChart>
|
||||||
</ChartContainer>
|
</ChartContainer>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</FramePanel>
|
||||||
</Card>
|
</Frame>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -12,21 +12,14 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@cfdm/ui/components/select'
|
} from '@cfdm/ui/components/select'
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
} from '@cfdm/ui/components/table'
|
|
||||||
|
|
||||||
import { AutoCompleteInput } from '@/components/auto-complete-input'
|
import { AutoCompleteInput } from '@/components/auto-complete-input'
|
||||||
import { ConfirmDialog } from '@/components/confirm-dialog'
|
import { ConfirmDialog } from '@/components/confirm-dialog'
|
||||||
import { PageHeader } from '@/components/page-header'
|
import { PageHeader } from '@/components/page-header'
|
||||||
import { PageShell } from '@/components/page-shell'
|
import { PageShell } from '@/components/page-shell'
|
||||||
import { QueryState } from '@/components/query-state'
|
|
||||||
import { SettingsCard } from '@/components/reui-kit/settings-card'
|
import { SettingsCard } from '@/components/reui-kit/settings-card'
|
||||||
|
import { ResourcePage, columnDefFromDataGrid } from '@/components/reui-kit'
|
||||||
|
import type { DataGridColumn } from '@/components/data-grid-types'
|
||||||
import { api } from '@/lib/api-client'
|
import { api } from '@/lib/api-client'
|
||||||
import { useSpaceId } from '@/lib/space'
|
import { useSpaceId } from '@/lib/space'
|
||||||
import {
|
import {
|
||||||
@@ -181,6 +174,140 @@ function SpacesPage() {
|
|||||||
onError: (e: Error) => toast.error(e.message),
|
onError: (e: Error) => toast.error(e.message),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const memberColumns: DataGridColumn<MemberRow>[] = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
key: 'user',
|
||||||
|
header: 'Пользователь',
|
||||||
|
cell: (m) => (
|
||||||
|
<div className="flex flex-col gap-0.5">
|
||||||
|
<span className="text-sm">{userLabelById.get(m.userId) ?? m.userId}</span>
|
||||||
|
{!userLabelById.has(m.userId) ? (
|
||||||
|
<span className="text-muted-foreground font-mono text-xs">{m.userId}</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{ key: 'role', header: 'Роль', cell: (m) => m.role },
|
||||||
|
{
|
||||||
|
key: 'actions',
|
||||||
|
header: '',
|
||||||
|
className: 'w-36',
|
||||||
|
cell: (m) =>
|
||||||
|
canAdmin && m.role !== 'owner' ? (
|
||||||
|
<ConfirmDialog
|
||||||
|
title="Отозвать доступ?"
|
||||||
|
description="Пользователь потеряет доступ к этому пространству."
|
||||||
|
confirmLabel="Отозвать"
|
||||||
|
destructive
|
||||||
|
onConfirm={() => removeMutation.mutate(m.userId)}
|
||||||
|
trigger={
|
||||||
|
<Button variant="outline" size="sm">
|
||||||
|
Отозвать
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[canAdmin, removeMutation, userLabelById],
|
||||||
|
)
|
||||||
|
|
||||||
|
const trashColumns: DataGridColumn<(typeof trash)[number]>[] = useMemo(
|
||||||
|
() => [
|
||||||
|
{ key: 'name', header: 'Название', cell: (s) => s.name },
|
||||||
|
{
|
||||||
|
key: 'deletedAt',
|
||||||
|
header: 'Удалено',
|
||||||
|
cell: (s) => (
|
||||||
|
<span className="text-muted-foreground text-sm">
|
||||||
|
{s.deletedAt ? new Date(s.deletedAt).toLocaleString('ru-RU') : '—'}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'actions',
|
||||||
|
header: '',
|
||||||
|
className: 'w-64',
|
||||||
|
cell: (s) => (
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => restoreMutation.mutate(s.id)}
|
||||||
|
disabled={restoreMutation.isPending}
|
||||||
|
>
|
||||||
|
Восстановить
|
||||||
|
</Button>
|
||||||
|
<ConfirmDialog
|
||||||
|
title="Удалить навсегда?"
|
||||||
|
description="Безвозвратно: все данные пространства будут уничтожены."
|
||||||
|
confirmLabel="Удалить навсегда"
|
||||||
|
destructive
|
||||||
|
onConfirm={() => purgeMutation.mutate(s.id)}
|
||||||
|
trigger={
|
||||||
|
<Button size="sm" variant="destructive">
|
||||||
|
Удалить навсегда
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[purgeMutation, restoreMutation],
|
||||||
|
)
|
||||||
|
|
||||||
|
const addMemberForm = canAdmin ? (
|
||||||
|
<div className="flex w-full flex-col gap-3 md:flex-row md:items-end">
|
||||||
|
<div className="flex min-w-0 flex-1 flex-col gap-2">
|
||||||
|
<Label>Пользователь</Label>
|
||||||
|
<AutoCompleteInput
|
||||||
|
value={
|
||||||
|
selectedUserId
|
||||||
|
? (userLabelById.get(selectedUserId) ?? userQuery)
|
||||||
|
: userQuery
|
||||||
|
}
|
||||||
|
onChange={(v) => {
|
||||||
|
const match = userOptions.find(
|
||||||
|
(o) => o.value === v || o.label === v,
|
||||||
|
)
|
||||||
|
if (match) {
|
||||||
|
setSelectedUserId(match.value)
|
||||||
|
setUserQuery(match.label)
|
||||||
|
} else {
|
||||||
|
setSelectedUserId('')
|
||||||
|
setUserQuery(v)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
options={userOptions}
|
||||||
|
placeholder="Имя или email…"
|
||||||
|
allowFreeText={false}
|
||||||
|
emptyText="Пользователи не найдены"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full flex-col gap-2 md:w-40">
|
||||||
|
<Label>Роль</Label>
|
||||||
|
<Select value={role} onValueChange={(v) => setRole(v ?? 'member')}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="admin">admin</SelectItem>
|
||||||
|
<SelectItem value="member">member</SelectItem>
|
||||||
|
<SelectItem value="viewer">viewer</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
disabled={!selectedUserId.trim() || addMutation.isPending}
|
||||||
|
onClick={() => addMutation.mutate()}
|
||||||
|
>
|
||||||
|
Добавить
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageShell>
|
<PageShell>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
@@ -243,170 +370,34 @@ function SpacesPage() {
|
|||||||
</SettingsCard>
|
</SettingsCard>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<QueryState
|
<ResourcePage
|
||||||
data={membersQuery.data as MemberRow[] | undefined}
|
title="Участники"
|
||||||
|
description="Доступ к текущему пространству"
|
||||||
|
columns={columnDefFromDataGrid(memberColumns)}
|
||||||
|
data={membersQuery.data ?? []}
|
||||||
|
getRowId={(m) => `${m.spaceId}-${m.userId}`}
|
||||||
|
pagination={false}
|
||||||
|
pinLastColumn
|
||||||
isLoading={membersQuery.isLoading}
|
isLoading={membersQuery.isLoading}
|
||||||
isError={membersQuery.isError}
|
isError={membersQuery.isError}
|
||||||
error={membersQuery.error}
|
error={membersQuery.error as Error | null}
|
||||||
onRetry={() => void membersQuery.refetch()}
|
onRetry={() => void membersQuery.refetch()}
|
||||||
empty={Boolean(membersQuery.data && membersQuery.data.length === 0)}
|
toolbarExtra={addMemberForm}
|
||||||
emptyTitle="Нет участников"
|
emptyTitle="Нет участников"
|
||||||
emptyDescription="Добавьте пользователя по имени или email"
|
emptyDescription="Добавьте пользователя по имени или email"
|
||||||
>
|
/>
|
||||||
{(members) => (
|
|
||||||
<SettingsCard title="Участники" description="Доступ к текущему пространству">
|
|
||||||
{canAdmin ? (
|
|
||||||
<div className="flex flex-col gap-3 border-b border-border/40 px-5 py-4 md:flex-row md:items-end">
|
|
||||||
<div className="flex flex-1 flex-col gap-2">
|
|
||||||
<Label>Пользователь</Label>
|
|
||||||
<AutoCompleteInput
|
|
||||||
value={
|
|
||||||
selectedUserId
|
|
||||||
? (userLabelById.get(selectedUserId) ?? userQuery)
|
|
||||||
: userQuery
|
|
||||||
}
|
|
||||||
onChange={(v) => {
|
|
||||||
const match = userOptions.find(
|
|
||||||
(o) => o.value === v || o.label === v,
|
|
||||||
)
|
|
||||||
if (match) {
|
|
||||||
setSelectedUserId(match.value)
|
|
||||||
setUserQuery(match.label)
|
|
||||||
} else {
|
|
||||||
setSelectedUserId('')
|
|
||||||
setUserQuery(v)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
options={userOptions}
|
|
||||||
placeholder="Имя или email…"
|
|
||||||
allowFreeText={false}
|
|
||||||
emptyText="Пользователи не найдены"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex w-full flex-col gap-2 md:w-40">
|
|
||||||
<Label>Роль</Label>
|
|
||||||
<Select value={role} onValueChange={(v) => setRole(v ?? 'member')}>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="admin">admin</SelectItem>
|
|
||||||
<SelectItem value="member">member</SelectItem>
|
|
||||||
<SelectItem value="viewer">viewer</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
disabled={!selectedUserId.trim() || addMutation.isPending}
|
|
||||||
onClick={() => addMutation.mutate()}
|
|
||||||
>
|
|
||||||
Добавить
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<Table>
|
<ResourcePage
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>Пользователь</TableHead>
|
|
||||||
<TableHead>Роль</TableHead>
|
|
||||||
<TableHead className="w-36" />
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{members.map((m) => (
|
|
||||||
<TableRow key={`${m.spaceId}-${m.userId}`}>
|
|
||||||
<TableCell>
|
|
||||||
<div className="flex flex-col gap-0.5">
|
|
||||||
<span className="text-sm">
|
|
||||||
{userLabelById.get(m.userId) ?? m.userId}
|
|
||||||
</span>
|
|
||||||
{!userLabelById.has(m.userId) ? (
|
|
||||||
<span className="text-muted-foreground font-mono text-xs">
|
|
||||||
{m.userId}
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>{m.role}</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
{canAdmin && m.role !== 'owner' ? (
|
|
||||||
<ConfirmDialog
|
|
||||||
title="Отозвать доступ?"
|
|
||||||
description="Пользователь потеряет доступ к этому пространству."
|
|
||||||
confirmLabel="Отозвать"
|
|
||||||
destructive
|
|
||||||
onConfirm={() => removeMutation.mutate(m.userId)}
|
|
||||||
trigger={
|
|
||||||
<Button variant="outline" size="sm">
|
|
||||||
Отозвать
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</SettingsCard>
|
|
||||||
)}
|
|
||||||
</QueryState>
|
|
||||||
|
|
||||||
<SettingsCard
|
|
||||||
title="Корзина"
|
title="Корзина"
|
||||||
description="Удалённые пространства можно восстановить или удалить навсегда"
|
description="Удалённые пространства можно восстановить или удалить навсегда"
|
||||||
>
|
columns={columnDefFromDataGrid(trashColumns)}
|
||||||
{trash.length === 0 ? (
|
data={trash}
|
||||||
<p className="text-muted-foreground px-5 py-4 text-sm">Корзина пуста</p>
|
getRowId={(s) => s.id}
|
||||||
) : (
|
pagination={false}
|
||||||
<Table>
|
pinLastColumn
|
||||||
<TableHeader>
|
emptyTitle="Корзина пуста"
|
||||||
<TableRow>
|
emptyDescription="Удалённых пространств нет"
|
||||||
<TableHead>Название</TableHead>
|
/>
|
||||||
<TableHead>Удалено</TableHead>
|
|
||||||
<TableHead className="w-64" />
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{trash.map((s) => (
|
|
||||||
<TableRow key={s.id}>
|
|
||||||
<TableCell>{s.name}</TableCell>
|
|
||||||
<TableCell className="text-muted-foreground text-sm">
|
|
||||||
{s.deletedAt
|
|
||||||
? new Date(s.deletedAt).toLocaleString('ru-RU')
|
|
||||||
: '—'}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => restoreMutation.mutate(s.id)}
|
|
||||||
disabled={restoreMutation.isPending}
|
|
||||||
>
|
|
||||||
Восстановить
|
|
||||||
</Button>
|
|
||||||
<ConfirmDialog
|
|
||||||
title="Удалить навсегда?"
|
|
||||||
description="Безвозвратно: все данные пространства будут уничтожены."
|
|
||||||
confirmLabel="Удалить навсегда"
|
|
||||||
destructive
|
|
||||||
onConfirm={() => purgeMutation.mutate(s.id)}
|
|
||||||
trigger={
|
|
||||||
<Button size="sm" variant="destructive">
|
|
||||||
Удалить навсегда
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
)}
|
|
||||||
</SettingsCard>
|
|
||||||
</div>
|
</div>
|
||||||
</PageShell>
|
</PageShell>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -18,8 +18,13 @@ import { StatusBadge } from '@/components/status-badge'
|
|||||||
import { Skeleton } from '@cfdm/ui/components/skeleton'
|
import { Skeleton } from '@cfdm/ui/components/skeleton'
|
||||||
import { Button } from '@cfdm/ui/components/button'
|
import { Button } from '@cfdm/ui/components/button'
|
||||||
import { Badge } from '@cfdm/ui/components/badge'
|
import { Badge } from '@cfdm/ui/components/badge'
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@cfdm/ui/components/card'
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@cfdm/ui/components/tabs'
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@cfdm/ui/components/tabs'
|
||||||
|
import {
|
||||||
|
Frame,
|
||||||
|
FrameHeader,
|
||||||
|
FramePanel,
|
||||||
|
FrameTitle,
|
||||||
|
} from '@/components/reui/frame'
|
||||||
import { ResourcePage, columnDefFromDataGrid } from '@/components/reui-kit'
|
import { ResourcePage, columnDefFromDataGrid } from '@/components/reui-kit'
|
||||||
import type { DataGridColumn } from '@/components/data-grid-types'
|
import type { DataGridColumn } from '@/components/data-grid-types'
|
||||||
import { getPaidUntilDate } from '@/lib/paid-until'
|
import { getPaidUntilDate } from '@/lib/paid-until'
|
||||||
@@ -58,6 +63,30 @@ function InfoRow({ label, value }: { label: string; value: React.ReactNode }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function DetailFrame({
|
||||||
|
title,
|
||||||
|
icon,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
title: string
|
||||||
|
icon?: React.ReactNode
|
||||||
|
children: React.ReactNode
|
||||||
|
className?: string
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Frame dense spacing="sm" className={className}>
|
||||||
|
<FrameHeader>
|
||||||
|
<FrameTitle className="flex items-center gap-2 text-base">
|
||||||
|
{icon}
|
||||||
|
{title}
|
||||||
|
</FrameTitle>
|
||||||
|
</FrameHeader>
|
||||||
|
<FramePanel className="flex flex-col gap-3">{children}</FramePanel>
|
||||||
|
</Frame>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function VpsDetailPage() {
|
function VpsDetailPage() {
|
||||||
const { vpsId } = Route.useParams()
|
const { vpsId } = Route.useParams()
|
||||||
const { data: snapshot, isLoading, isError, error, refetch } = useQuery(snapshotQueryOptions())
|
const { data: snapshot, isLoading, isError, error, refetch } = useQuery(snapshotQueryOptions())
|
||||||
@@ -175,54 +204,29 @@ function VpsDetailPage() {
|
|||||||
{row.environment ? <Badge variant="outline">{row.environment}</Badge> : null}
|
{row.environment ? <Badge variant="outline">{row.environment}</Badge> : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
<Card>
|
<DetailFrame title="Сеть" icon={<GlobeIcon className="size-4" />}>
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="flex items-center gap-2 text-base">
|
|
||||||
<GlobeIcon className="size-4" />
|
|
||||||
Сеть
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex flex-col gap-3">
|
|
||||||
<InfoRow label="IP" value={row.ip || '—'} />
|
<InfoRow label="IP" value={row.ip || '—'} />
|
||||||
<InfoRow label="DNS" value={row.dns || '—'} />
|
<InfoRow label="DNS" value={row.dns || '—'} />
|
||||||
<InfoRow label="IPv6" value={(row as Vps & { ipv6?: string }).ipv6 || '—'} />
|
<InfoRow label="IPv6" value={(row as Vps & { ipv6?: string }).ipv6 || '—'} />
|
||||||
<InfoRow label="SSH порт" value={(row as Vps & { sshPort?: number }).sshPort ?? 22} />
|
<InfoRow label="SSH порт" value={(row as Vps & { sshPort?: number }).sshPort ?? 22} />
|
||||||
<InfoRow label="Локация" value={[row.country, row.city].filter(Boolean).join(', ') || '—'} />
|
<InfoRow label="Локация" value={[row.country, row.city].filter(Boolean).join(', ') || '—'} />
|
||||||
<InfoRow label="Дата-центр" value={row.datacenter || '—'} />
|
<InfoRow label="Дата-центр" value={row.datacenter || '—'} />
|
||||||
</CardContent>
|
</DetailFrame>
|
||||||
</Card>
|
<DetailFrame title="Ресурсы" icon={<CpuIcon className="size-4" />}>
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="flex items-center gap-2 text-base">
|
|
||||||
<CpuIcon className="size-4" />
|
|
||||||
Ресурсы
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex flex-col gap-3">
|
|
||||||
<InfoRow label="vCPU" value={row.vcpu} />
|
<InfoRow label="vCPU" value={row.vcpu} />
|
||||||
<InfoRow label="RAM" value={`${row.ramGb} GB`} />
|
<InfoRow label="RAM" value={`${row.ramGb} GB`} />
|
||||||
<InfoRow label="Disk" value={`${row.diskGb} GB`} />
|
<InfoRow label="Disk" value={`${row.diskGb} GB`} />
|
||||||
<InfoRow label="ОС" value={(row as Vps & { os?: string }).os || '—'} />
|
<InfoRow label="ОС" value={(row as Vps & { os?: string }).os || '—'} />
|
||||||
<InfoRow label="Хостер" value={provider?.name || '—'} />
|
<InfoRow label="Хостер" value={provider?.name || '—'} />
|
||||||
</CardContent>
|
</DetailFrame>
|
||||||
</Card>
|
|
||||||
{vpsDomains.length > 0 ? (
|
{vpsDomains.length > 0 ? (
|
||||||
<Card className="md:col-span-2">
|
<DetailFrame title="Домены (CFDM)" className="md:col-span-2">
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-base">Домены (CFDM)</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<VpsDomainsCell domains={vpsDomains} />
|
<VpsDomainsCell domains={vpsDomains} />
|
||||||
</CardContent>
|
</DetailFrame>
|
||||||
</Card>
|
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
{customFieldRows.length > 0 ? (
|
{customFieldRows.length > 0 ? (
|
||||||
<Card>
|
<DetailFrame title="Дополнительные поля">
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-base">Дополнительные поля</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex flex-col gap-3">
|
|
||||||
{customFieldRows.map(({ def, value }) => (
|
{customFieldRows.map(({ def, value }) => (
|
||||||
<InfoRow
|
<InfoRow
|
||||||
key={def.key}
|
key={def.key}
|
||||||
@@ -230,20 +234,12 @@ function VpsDetailPage() {
|
|||||||
value={formatCustomFieldValue(def, value)}
|
value={formatCustomFieldValue(def, value)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</CardContent>
|
</DetailFrame>
|
||||||
</Card>
|
|
||||||
) : null}
|
) : null}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="finance" className="flex flex-col gap-4">
|
<TabsContent value="finance" className="flex flex-col gap-4">
|
||||||
<Card>
|
<DetailFrame title="Тариф" icon={<CreditCardIcon className="size-4" />}>
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="flex items-center gap-2 text-base">
|
|
||||||
<CreditCardIcon className="size-4" />
|
|
||||||
Тариф
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex flex-col gap-3">
|
|
||||||
<InfoRow label="Тип" value={tariffTypeLabel(row.tariffType)} />
|
<InfoRow label="Тип" value={tariffTypeLabel(row.tariffType)} />
|
||||||
<InfoRow
|
<InfoRow
|
||||||
label="Ставка"
|
label="Ставка"
|
||||||
@@ -259,8 +255,7 @@ function VpsDetailPage() {
|
|||||||
label="Оплачено до"
|
label="Оплачено до"
|
||||||
value={paidUntil ? paidUntil.toLocaleDateString('ru-RU') : '—'}
|
value={paidUntil ? paidUntil.toLocaleDateString('ru-RU') : '—'}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</DetailFrame>
|
||||||
</Card>
|
|
||||||
<ResourcePage
|
<ResourcePage
|
||||||
title="Связанные платежи"
|
title="Связанные платежи"
|
||||||
columns={columnDefFromDataGrid(paymentColumns)}
|
columns={columnDefFromDataGrid(paymentColumns)}
|
||||||
@@ -272,28 +267,13 @@ function VpsDetailPage() {
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="notes">
|
<TabsContent value="notes">
|
||||||
<Card>
|
<DetailFrame title="Заметки" icon={<StickyNoteIcon className="size-4" />}>
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="flex items-center gap-2 text-base">
|
|
||||||
<StickyNoteIcon className="size-4" />
|
|
||||||
Заметки
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<p className="whitespace-pre-wrap text-sm">{row.notes?.trim() || 'Нет заметок'}</p>
|
<p className="whitespace-pre-wrap text-sm">{row.notes?.trim() || 'Нет заметок'}</p>
|
||||||
</CardContent>
|
</DetailFrame>
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="sync">
|
<TabsContent value="sync">
|
||||||
<Card>
|
<DetailFrame title="Защита от перезаписи при синке" icon={<RefreshCwIcon className="size-4" />}>
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="flex items-center gap-2 text-base">
|
|
||||||
<RefreshCwIcon className="size-4" />
|
|
||||||
Защита от перезаписи при синке
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
{overrides.length === 0 ? (
|
{overrides.length === 0 ? (
|
||||||
<p className="text-sm text-muted-foreground">Все поля обновляются из BILLmanager</p>
|
<p className="text-sm text-muted-foreground">Все поля обновляются из BILLmanager</p>
|
||||||
) : (
|
) : (
|
||||||
@@ -304,8 +284,7 @@ function VpsDetailPage() {
|
|||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</DetailFrame>
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ Settings: секции через Frame + `gap` (без hairline `Separator` п
|
|||||||
| Lists | `data-grid-filtering-2` | https://reui.io/preview/base/data-grid-filtering-2 |
|
| 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 |
|
| Settings | `settings-16` + SettingRow | https://reui.io/preview/base/settings-16 |
|
||||||
| Auth | `auth-13` | https://reui.io/preview/base/auth-13 |
|
| 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 |
|
| Forms | `form-7` → Sheet/Drawer | https://reui.io/preview/base/form-7 |
|
||||||
|
|
||||||
## Kit API (`reui-kit/`)
|
## Kit API (`reui-kit/`)
|
||||||
@@ -76,9 +76,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).
|
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
|
## System monitor
|
||||||
|
|
||||||
|
|||||||
@@ -68,9 +68,8 @@ function SheetContent({
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<XIcon
|
<XIcon aria-hidden="true" />
|
||||||
/>
|
<span className="sr-only">Закрыть</span>
|
||||||
<span className="sr-only">Close</span>
|
|
||||||
</SheetPrimitive.Close>
|
</SheetPrimitive.Close>
|
||||||
)}
|
)}
|
||||||
</SheetPrimitive.Popup>
|
</SheetPrimitive.Popup>
|
||||||
|
|||||||
@@ -196,8 +196,8 @@ function Sidebar({
|
|||||||
side={side}
|
side={side}
|
||||||
>
|
>
|
||||||
<SheetHeader className="sr-only">
|
<SheetHeader className="sr-only">
|
||||||
<SheetTitle>Sidebar</SheetTitle>
|
<SheetTitle>Боковое меню</SheetTitle>
|
||||||
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
|
<SheetDescription>Навигация по приложению</SheetDescription>
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
<div className="flex h-full w-full flex-col">{children}</div>
|
<div className="flex h-full w-full flex-col">{children}</div>
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
@@ -256,7 +256,7 @@ function SidebarTrigger({
|
|||||||
onClick,
|
onClick,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof Button>) {
|
}: React.ComponentProps<typeof Button>) {
|
||||||
const { toggleSidebar } = useSidebar()
|
const { toggleSidebar, open, openMobile, isMobile } = useSidebar()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
@@ -264,6 +264,9 @@ function SidebarTrigger({
|
|||||||
data-slot="sidebar-trigger"
|
data-slot="sidebar-trigger"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
|
type="button"
|
||||||
|
aria-label="Боковое меню"
|
||||||
|
aria-expanded={isMobile ? openMobile : open}
|
||||||
className={cn(className)}
|
className={cn(className)}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
onClick?.(event)
|
onClick?.(event)
|
||||||
@@ -271,8 +274,8 @@ function SidebarTrigger({
|
|||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<PanelLeftIcon />
|
<PanelLeftIcon aria-hidden="true" />
|
||||||
<span className="sr-only">Toggle Sidebar</span>
|
<span className="sr-only">Боковое меню</span>
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -284,10 +287,10 @@ function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
|
|||||||
<button
|
<button
|
||||||
data-sidebar="rail"
|
data-sidebar="rail"
|
||||||
data-slot="sidebar-rail"
|
data-slot="sidebar-rail"
|
||||||
aria-label="Toggle Sidebar"
|
aria-label="Боковое меню"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
onClick={toggleSidebar}
|
onClick={toggleSidebar}
|
||||||
title="Toggle Sidebar"
|
title="Боковое меню"
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute inset-y-0 z-20 hidden w-4 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:start-1/2 after:w-[2px] hover:after:bg-sidebar-border sm:flex ltr:-translate-x-1/2 rtl:-translate-x-1/2",
|
"absolute inset-y-0 z-20 hidden w-4 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:start-1/2 after:w-[2px] hover:after:bg-sidebar-border sm:flex ltr:-translate-x-1/2 rtl:-translate-x-1/2",
|
||||||
"in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
|
"in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
|
||||||
|
|||||||
Reference in New Issue
Block a user