# 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. Responsive Breakpoints

Tailwind defaults — `sm=640 / md=768 / lg=1024 / xl=1280 / 2xl=1536`. Ordinis
is an admin tool — desktop primary. Policy:

- **Target primary:** `lg` (1024) and up — full layout fidelity
- **Tolerate:** `sm`–`md` (640-1023) — degraded but functional (sidebar
  collapses to hamburger; tables horizontally scroll; tabs may wrap)
- **Minimum viable:** 375px (iPhone SE narrow). No layout breaks; some
  density compromises accepted.
- **Avoid breakpoint ping-pong:** don't redefine layout at every step.
  Standardize page padding: `px-4 sm:px-6 lg:px-8`. Big shifts only at `lg`
  (sidebar collapse) and `xl` (max-width caps).

Explicit non-goal: native mobile-app feel. Users on phones are doing
inspection / approval triage, not maker-flow data entry. See Section 19
disclosure.

---

## 13. Touch Targets

WCAG AAA recommends 44×44 minimum. Ordinis policy:

- **All interactive elements** (`button`, `a` with `href`, `[role=button]`,
  inputs) — `min-h-[44px]` enforced.
- **Icon-only buttons** (Bell, theme switch, language switch, hamburger) —
  `size-11` (44×44) wrapper instead of current `size-7`. Visual icon stays
  small; hit area is the padded wrapper.
- **Tabs** — `h-11` instead of `h-10`. Lose 4px header height, gain WCAG
  compliance.
- **Sidebar nav rows** — full row click area (already correct);
  `py-2.5` ensures ≥ 44px tall.
- **Table action links** ("Open →") — wrap in `<Button variant="ghost" size="sm">`
  which already has 44px hit area, despite small visual text.
- **Exception:** icons inside larger clickable containers (e.g. unread dot
  inside a notification row that's clickable as a whole) — hit area belongs
  to parent, not the icon itself.

Enforcement: a follow-up MR sweeps current 28–32px buttons → 44px wrappers.
Tracked in TODOS.md TODO 6 punch list.

---

## 14. Loading State Taxonomy

| Surface | Pattern | Rationale |
|---|---|---|
| Inline cell / quick value | `<LoadingBlock size="sm">` | Local replacement, no layout shift |
| Panel / table / drawer body | `<LoadingBlock size="md">` | Replaces whole section, matches Panel padding |
| Whole route mount | Skeleton matching real shape (TanStack Router pending state) | First paint should hint at content layout |
| Suspense fallback | `<LoadingBlock size="md">` | Same as panel — Suspense boundaries wrap panels |
| Inflight mutation | Button `loading` prop (replaces label with spinner) | Local feedback at the trigger |
| Optimistic updates | No loading affordance — use `useOptimistic` | Most modern pattern |

Never use a bare `<Spinner>` outside `<Button loading>`. Spinners detached
from context are anxiety-inducing — replace with a skeleton or LoadingBlock
sized to the missing content.

---

## 15. Motion Durations

Four-step scale. Implemented via Tailwind utility `duration-{step}` mapped
to CSS custom properties so the same scale applies to bespoke transitions.

| Step | Range | Use |
|---|---|---|
| `micro` | 50–100ms | Hover, focus rings, toggle thumb, button press |
| `short` | 150–250ms | Dropdown / popover open, success/error flash on form input, badge appearance |
| `medium` | 300–400ms | Drawer slide-in, modal fade, ReviewDrawer decision flash, tab content fade |
| `long` | 500–700ms | Route entrance choreography, hero reveals, complex sequence |

### Easing

- **Entrance** (hidden → visible, off-screen → on-screen): `ease-out` —
  start fast, settle in
- **Exit** (visible → hidden): `ease-in` — start gentle, accelerate away
- **Move** (element relocates / resizes in-place): `ease-in-out`
- **Linear**: only for indeterminate loops (spinner rotation, progress
  bar bounce)

### Rules

- Animate `transform` and `opacity` only — never width/height/top/left
  (triggers layout reflow on every frame)
- `transition: all` is banned — list properties explicitly
- Respect `prefers-reduced-motion`: replace entrance animation with instant
  state swap; spinner becomes pulsing dot
- No animation > 700ms without user-controlled trigger (clicking expand, etc.)

---

## 16. Long-Content Overflow Rules

| Surface | Pattern |
|---|---|
| Table cells (string columns) | `truncate` + native `title={fullValue}` tooltip |
| Drawer / Modal title | `truncate` (single line, no wrap) |
| Body description text | Wrap naturally; `max-w-prose` on wide screens |
| Code blocks, JSON diff panes | `overflow-x-auto whitespace-pre font-mono` |
| Comments (makerComment / reviewComment) | `line-clamp-2` on mobile, full on `md+` |
| Notification preview text | `line-clamp-2` always (drawer list is dense) |
| Search results / lists | Wrap; `break-words` for long URLs/identifiers |
| Tab labels | Never truncate — if label is long, label is wrong |

Tooltip + truncation rule: whenever applying `truncate`, ALWAYS pair with
native `title` attribute (or `<Tooltip>` primitive for rich content). Don't
let truncation hide information without a recovery path.

---

## 17. Dark Mode Parity

Audited 2026-05-14 — found `*-bg` alpha tokens differ between themes (11%
light vs 16% dark). Drift from independent token tweaks, not intentional.

### Policy

- **Surface elevation** via opacity / shadow, NOT lightness inversion. Light
  → dark = different palette, not flipped lightness. Already implemented;
  keep.
- **Accent saturation:** reduce 10–20% in dark mode to avoid vibration on
  dark surface. Already implemented (`--color-accent: #b05a2e` light vs
  `#d97757` dark which is brighter but compensated for darker bg).
- **`*-bg` alpha consistency:** normalize to 12% in both themes via new
  `--alpha-bg-tint` token. One-time MR. Visual diff acceptable (slight
  saturation shift on info badges).
- **Text on accent (`--color-on-accent`):** verified ≥ 4.5:1 ratio both
  themes. Light = white (#fff) on `#b05a2e` ≈ 4.8:1 OK. Dark = `#1c1a17`
  on `#d97757` ≈ 7.2:1 OK.
- **`text-mute` on `bg-surface`:** light ≈ 4.6:1 (just passes), dark ≈ 4.8:1.
  Borderline — re-verify after Section 13 button rework.

---

## 18. Notifications Channel UX

Phase A (current, MR !190): every fired event dispatches to all enabled
channels at backend level. EmailChannel is the only Phase A channel.
Reviewer-pool address hardcoded in `application.yml`. Bell + drawer + toast
already shipped on frontend.

### Phase B (deferred — TODO follow-up)

User-configurable preferences via new route `/me/notifications/preferences`.

Surface design when shipped:
- Table: rows = event types, columns = channels (Email / Express / Telegram /
  In-App). Toggle per cell.
- Save button bottom-right; optimistic update.
- "Mute all" + "Reset to defaults" quick actions in header.
- Defaults: all on; per-user state in `user_notification_prefs` table
  (migration 0021 already has it).

Anti-pattern: don't bury preferences inside profile settings. Notifications
preferences live one click from the bell — make the bell drawer footer have
a settings cog.

### Future channel ordering (Phase C, "Don't Disturb")

Per-channel priority for DND mode. Email always lowest (durable). Express/Slack
mid. Telegram highest urgency. Bell + in-app toast always on.

---

## 19. Disclosure: Known Tradeoffs

This admin UI **deliberately deprioritizes mobile-first design**. Affordances:

- Native touch targets bigger than 44px not enforced on data-cell click
  areas (would explode row heights). Cells with `<UserCell>` or row-level
  click handlers rely on the parent row hit area.
- Tables don't reformat into card-stacks on narrow screens — they scroll
  horizontally.
- Forms don't shift to one-column layouts under 768px — they compress.
- Server-side rendering not used; client-only SPA means full-app JS bundle
  required for any page.

Users on phones doing emergency approvals are supported. Users doing
maker-flow data entry on phones are not. Don't ship to a context where the
latter matters without prior architecture work.

---

## 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.
- **2026-05-14 v1** — Section 12 (open items) resolved via `/design-consultation`.
  Added Sections 12–19 with concrete policies: responsive breakpoints, touch
  targets, loading taxonomy, motion durations, overflow rules, dark mode
  parity, notifications channel UX, known-tradeoffs disclosure. Touch target
  follow-up MR pending; dark mode alpha normalization MR pending; Phase B
  notifications prefs TODO.
