feat(notifications): TODO 7 — draft decision toast + email + bell badge

This commit is contained in:
Александр Зимин
2026-05-14 14:40:35 +00:00
parent 50d263745a
commit 9050e2427e
21 changed files with 1992 additions and 82 deletions
+353
View File
@@ -0,0 +1,353 @@
# DESIGN.md — Ordinis Admin UI Design System
**Status:** v0 baseline (2026-05-14). Captured from observed patterns в
codebase after approval workflow saga (MR !179!190). To be refined через
`/design-consultation` skill in a dedicated sprint.
**Goal:** Lock the implicit design system как source of truth для будущих
features (Phase 4 webhook approvals, notifications surfaces, MDM expansion).
Document invariants (tokens, primitives, semantic rules) — NOT specific
components.
---
## 1. Voice & Tone
- **Russian-first.** UI primary language ru-RU; en-US is parallel mirror.
Every user-facing string registered in `src/i18n.ts`.
- **Operational, not aspirational.** Copy describes what user sees / does
("Очередь чистая", "Reject требует comment") — no marketing-speak.
- **Domain-precise.** Use exact role names (maker / reviewer / publisher)
and operation codes (CREATE / UPDATE / CLOSE) — no generic "user".
- **Apologize on failure.** Empty states + error messages name what went
wrong и what to do next ("Refresh page + retry", "Укажите причину").
---
## 2. Color Tokens
CSS variables defined в `src/styles.css`. Three themes: `default` (light
Anthropic warm), `nstart` (medium contrast brand), `dark` (Anthropic dark).
### Semantic roles
| Token | Light | Dark | Use |
|---|---|---|---|
| `--color-ink` | `#1a1714` | `#f0ece4` | Primary text |
| `--color-ink-2` | `#52483d` | `#c8c1b3` | Secondary text |
| `--color-mute` | `#8c8276` | `#8a8474` | Tertiary text / placeholders |
| `--color-bg` | `#faf9f5` | `#1c1a17` | App background |
| `--color-surface` | `#ffffff` | `#262421` | Card / panel surface |
| `--color-surface-2` | `#f5f3ec` | `#2e2b27` | Hover / nested surface |
| `--color-line` | `#e6e1d4` | `#3a3733` | Dividers / borders |
| `--color-line-2` | `#efeae0` | `#2d2a27` | Subtle dividers |
| `--color-accent` | `#b05a2e` | `#d97757` | Primary CTA / links |
| `--color-accent-bg` | accent @ 11% | accent @ 16% | Accent tint |
| `--color-on-accent` | `#ffffff` | `#1c1a17` | Text on accent |
| `--color-green` | `#4f7038` | `#4a7d4f` | Success / APPROVED |
| `--color-green-bg` | `#e4ead0` | `#dde9da` | Success tint |
| `--color-warn` | `#a8762a` | `#b97c2f` | Warning / CHANGES_REQUESTED |
| `--color-warn-bg` | `#f6e8bc` | `#fcecc7` | Warning tint |
| `--color-pink` | `#a64636` | `#b34d6a` | Danger / REJECTED |
| `--color-pink-bg` | `#f4d9cd` | `#f8d8de` | Danger tint |
### Rules
- **Never hard-code hex.** Use `bg-line`, `text-mute`, `border-accent` через
Tailwind utilities mapped to these tokens.
- **Status colors are stable across themes.** `success` always green-family;
`danger` always pink-family.
- **Tint backgrounds (`*-bg`) use 11-16% alpha** — for badge backgrounds
and subtle highlights. Never tint at higher opacity (loses contrast).
---
## 3. Typography
### Font families
| Token | Stack | Use |
|---|---|---|
| `--font-sans` | `Inter, -apple-system, BlinkMacSystemFont, sans-serif` | Body, UI |
| `--font-mono` | `JetBrains Mono, SF Mono, monospace` | Code, UUIDs, business keys, versions |
| `--font-display` | `Tektur, Inter, sans-serif` | Caps headers, brand mark |
### Type scale (Tailwind utilities)
| Utility | Use |
|---|---|
| `text-title-md` | Section headings (Panel title) |
| `text-body` | Body copy |
| `text-cell` | Table cell, dense info |
| `text-cap` | Caps tracking-wide labels (column headers) — uses `--font-display` |
| `text-mono` | Code / monospaced |
### Rules
- **Mono для identity:** UUIDs (truncated `slice(0,8)…`), business keys,
version strings (`v1.1.0`). Reads as "this is an exact identifier".
- **Display caps для column headers:** `text-cap text-mute font-display
uppercase tracking-wider` — visual hierarchy через caps, not size.
- **`tabular-nums` для timestamp + count columns** — prevents jitter on
refresh.
- **Never use system-ui / -apple-system as primary display font.** Inter
is fallback only.
---
## 4. Spacing & Layout
### Base scale
Tailwind default 4px scale (`gap-1` = 4px, `gap-2` = 8px, `gap-3` = 12px,
`gap-4` = 16px). Avoid arbitrary spacing values (`mt-[13px]`) — use scale
or extend tokens.
### Layout primitives
- **Page chrome:** `<PageHeader title description>` for top of every route
- **Content area:** `space-y-4` (16px) between top-level sections
- **Panel:** `<Panel>` wraps tables / forms with `bg-surface border border-line`
- **Card:** `<Card>` для individual content blocks (`bg-surface-2`)
- **Drawer:** Right-side slide-in for detail views (review draft, schema
draft, history)
- **Modal:** Centered overlay для destructive confirmations + create flows
### Drawer-vs-Modal rules
| Pattern | Use Drawer | Use Modal |
|---|---|---|
| Review existing entity | ✓ | |
| Read-only history | ✓ | |
| Diff side-by-side | ✓ (max-w-3xl) | |
| Create new entity | | ✓ |
| Destructive confirmation | | ✓ (size=sm) |
| Maker action requiring focus | | ✓ |
Drawer = "look at this thing, maybe act on it". Modal = "do this thing now,
then dismiss".
### Width caps
- Drawer: `max-w-md` (notifications), `max-w-xl` (schema draft), `max-w-3xl`
(record review with diff panes)
- Modal: `size="sm"` (confirm), `"md"` (form), `"xl"` (JSON editor)
- Body copy: implicit via Panel padding; for description text apply
`max-w-prose` если viewport wide
---
## 5. Component Primitives
Locations: `src/ui/components/*.tsx`. Re-exported via `@/ui`.
### Stable primitives (use freely)
- `<Button variant size>` — `primary | secondary | ghost | danger`, `sm | md | lg`
- `<Badge variant>` — `info | success | warning | danger | default | primary | outline`
- `<Panel>` — surface wrapper для table/form blocks
- `<Card>` — content block (lighter surface than Panel)
- `<Drawer isOpen onClose title side widthClassName>` — slide-in detail view
- `<Modal isOpen onClose title size>` — centered overlay
- `<Table TableHead TableRow TableHeaderCell TableBody TableCell>` — tabular data
- `<Tabs items value onValueChange>` — Radix-based tabs with badge slot
- `<EmptyState icon title description action>` — no-data fallback
- `<Alert variant title>` — banner with `info | success | warning | error`
- `<LoadingBlock size label>` — skeleton / spinner
- `<QueryErrorState error onRetry>` — generic 4xx/5xx state с retry
- `<UserCell uuid>` — resolves sub → display name (three-tier cache)
- `<TextInput TextArea Checkbox Select MultiSelect>` — form inputs
### Domain-specific (in `src/components/`)
- `<ReviewDrawer>` — record draft review (records tab + Мои tab)
- `<SchemaDraftDrawer>` — schema draft review (dict page + Мои tab redirect)
- `<DictionaryEditorDialog>` — schema-aware editor с approval flow
- `<MyDraftsPanel>` — Мои tab on /reviews
- `<NotificationsDrawer>` + `<NotificationsBell>` — TODO 7 notification surfaces
- `<ChangelogDiffModal>` — historical schema diff viewer
- `<TimeTravelModal>` — record at point-in-time
---
## 6. Badge Variant Mapping
Visual category по semantic role, applied везде через `<Badge variant=…>`:
### Operations (DraftOperation)
| Op | Variant | Color | Reason |
|---|---|---|---|
| CREATE | success | green | Net-new record — positive change |
| UPDATE | info | accent | Most common; neutral default |
| CLOSE | warning | warn | Soft-delete; destructive but reversible |
### Record draft status (DraftStatus)
| Status | Variant | Reason |
|---|---|---|
| PENDING | info | Awaiting reviewer action |
| APPROVED | success | Terminal positive |
| REJECTED | danger | Terminal negative |
| WITHDRAWN | default | Maker-initiated cancel, neutral |
### Schema draft status (SchemaDraftStatus)
| Status | Variant | Reason |
|---|---|---|
| draft | warning | Pre-submit, maker action pending |
| review_pending | info | Reviewer queue |
| approved | success | Awaiting publish |
| changes_requested | warning | Bounced to maker |
| rejected | danger | Terminal negative |
| published | primary | Terminal live (strongest signal) |
| withdrawn | default | Maker cancel |
### Notification event type
| Pattern in eventType | Variant |
|---|---|
| `*Approved`, `*Published` | success |
| `*Rejected`, `*Withdrawn` | danger |
| `*Submitted`, `*ChangesRequested` | info |
| other | default |
---
## 7. Code / Diff Visual Language
### Live vs proposed panes
- **Live (current state):** `bg-line/30` background — muted, "established"
- **Proposed (draft):** `bg-accent/4` background — accent-tinted, "your suggestion"
- **CLOSE op note:** `text-aurora italic` — call-out пустой proposed pane
### Diff summary chips (above panes)
- `+N added`: text-aurora (green-family) with `+` prefix
- `M removed`: text-danger with `` prefix
- `~K changed`: text-warn with `~` prefix
- Bucket details в `text-mute lowercase` parens
Rendered via `shallowDiffSummary()` from `src/lib/diff.ts`.
---
## 8. Interaction States
### Required for all interactive elements
- `hover:bg-line/30` или similar hover affordance
- `focus-visible:ring-2 focus-visible:ring-ring` focus ring (NEVER
`outline: none` без replacement)
- `disabled:opacity-50 disabled:cursor-not-allowed` для disabled
- Touch target ≥ 44×44 — use `min-h-[44px]` for icon buttons (current gap;
audit findings P1 in TODOS.md TODO 6)
### Success / error affordances
- **Mutation success:** прокачка через `toast.success()` (sonner) — auto-dismiss 4s
- **Mutation error:** `toast.error()` с structured message via
`extractReviewError()` / `humanizeBulkReason()` mappings
- **In-form validation:** inline error under input с `role="alert"` +
`aria-invalid` + `aria-describedby`
- **Approve/reject decision flash:** 600ms bg-green-bg/30 или bg-pink-bg/30
band на drawer footer перед `onClose()` — ack ack для reviewer
### Empty states (warmth required)
Every `<EmptyState>` MUST have:
- `icon` — phosphor duotone, size 28-32
- `title` — outcome-framed ("Очередь пуста", not "No results")
- `description` — actionable hint (what to do next, where data appears)
- `action` (optional) — Link / Button to relevant route
---
## 9. Term Glossary (canonical)
| Code | EN | RU | Definition |
|---|---|---|---|
| maker | maker | автор | User who creates / submits a draft |
| reviewer | reviewer | ревьюер | User who approves / rejects drafts |
| publisher | publisher | публикатор | User who publishes approved schema drafts |
| draft | draft | черновик | Pending change awaiting approval |
| pending | pending | на ревью | Status: awaiting reviewer decision |
| approved | approved | одобрено | Status: reviewer approved (record published OR schema awaits publish) |
| rejected | rejected | отклонён | Status: reviewer rejected (terminal) |
| withdrawn | withdrawn | отозван | Status: maker cancelled (terminal) |
| WIP | in progress | в работе | Schema draft pre-submit OR changes_requested |
| decided | decided | закрыты | Any terminal status — APPROVED + REJECTED + WITHDRAWN |
| changes requested | changes requested | запрошены правки | Reviewer bounced schema draft back to maker |
| published | published | опубликован | Schema draft → live (terminal positive) |
Source: backend domain types (DraftStatus, SchemaDraftStatus, DraftOperation).
NEVER use synonyms ("submitter" for maker, "validator" for reviewer) —
breaks consistency with audit log + RBAC roles.
---
## 10. RBAC & Role Visibility
Backend gates (single source of truth). Frontend hide UI elements when
roles missing — backend still enforces if frontend leaks.
| Composite role | Members | UI surface |
|---|---|---|
| `ordinis:reviewer` | `schema:reviewer` + `record:reviewer` | /reviews tabs (Records, Schemas) — approve/reject buttons |
| (maker is implicit) | any authenticated user | /reviews Мои tab, draft create modals, dict editor |
Identity check pattern (in components):
```ts
const auth = useAuth()
const currentSub = auth.user?.profile?.sub
const isMine = draft.makerId === currentSub
```
For role gating use `usePermissions()` (src/auth/usePermissions.ts).
---
## 11. AI Slop Anti-Patterns (banned)
Per /design-review gstack skill plus our own observations:
- ❌ Purple/violet/indigo gradient backgrounds
- ❌ 3-column feature grid with icon-in-circle headers
- ❌ Decorative blobs / wavy SVG dividers / floating circles
- ❌ Emoji as design elements (rocket in heading)
- ❌ Colored left-border-only cards
- ❌ Generic hero copy ("Welcome to Ordinis", "Unlock the power…")
- ❌ Bubbly uniform `rounded-full` on every element
- ❌ Centered everything (`text-align: center` on all body content)
- ❌ system-ui / -apple-system as PRIMARY display font
Pass test: every visual element should be there because it does a job, not
because empty space looks empty.
---
## 12. Open Items (gaps to address in /design-consultation sprint)
- Responsive breakpoint policy (mobile 375, tablet 768, desktop 1024,
wide 1440 — confirm + lint)
- Touch target enforcement (P1 in TODO 6 a11y findings)
- Loading state taxonomy (skeleton vs spinner vs LoadingBlock vs Suspense
— when to use which)
- Motion duration policy (currently ad-hoc — set 150ms quick / 300ms medium
/ 500ms entrance)
- Long-content overflow rules (truncate vs wrap vs ellipsis vs scroll —
per surface type)
- Dark mode parity audit (some tokens have alpha inconsistency between
themes)
- Notifications channel UI policy (in-app vs email vs both — currently
every event goes to all enabled channels; should be user-configurable)
---
## Changelog
- **2026-05-14 v0** — Initial document. Captured tokens, components, badge
semantics, term glossary, drawer-vs-modal rules from approval workflow
saga codebase. Marked open items для full `/design-consultation`
sprint. Authored as part of TODO 5 closeout.