Files
mdm-ordinis/DESIGN.md
T
2026-05-14 15:21:58 +00:00

21 KiB
Raw Blame History

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):

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: smmd (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.
  • Tabsh-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 2832px 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 50100ms Hover, focus rings, toggle thumb, button press
short 150250ms Dropdown / popover open, success/error flash on form input, badge appearance
medium 300400ms Drawer slide-in, modal fade, ReviewDrawer decision flash, tab content fade
long 500700ms 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 1020% 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 1219 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.