Files
mdm-ordinis/ordinis-admin-ui/design_handoff_ordinis_mdm

Handoff: Ordinis MDM — UI redesign

Overview

Full UI redesign for the Ordinis MDM admin (ordinis-admin-ui — React 19 + TanStack Router + Vite + Tailwind v4 + currently @nstart/ui@0.1.3). Covers:

  1. Design system — earthy / woody light theme + Claude-style warm dark theme. Inter + JetBrains Mono + Tektur. Custom token palette.
  2. Dictionary catalog — bundle-grouped grid with FK chips, scope/bundle filters, search.
  3. Dictionary editor — tabs (Records / Relations / Fields / JSON / Events / History), keyboard-driven table, record drawer with sectioned form, time-travel viewer, status banner for draft/review/published flows.
  4. Modals — Create dictionary, Confirm delete, Toast notifications.
  5. Theme switcher — Light (earthy) / Dark (Claude-warm), persisted to localStorage.

About the design files

design/compact.html and design/ui-kit.html are single-file React prototypes using inline Babel + CDN React 18. They are design references, not production code.

Your task: recreate the screens in the existing ordinis-admin-ui codebase using its real patterns:

  • TanStack Router routes
  • TanStack Query for data
  • Tailwind v4 utility classes (project already uses @tailwindcss/vite)
  • Component primitives from @nstart/ui (or progressively replace with shadcn/ui behind a barrel — see "Stack migration" below)
  • React Hook Form + AJV for forms
  • Phosphor Icons

Do not ship the prototype HTML to production. Do not import the prototype as a module — it uses window.* globals and inline Babel that won't survive the Vite build.

Fidelity

High-fidelity. Colors, typography, spacing, and most interactions are decided. Pixel-perfect recreation is the goal, with these adaptations:

  • Inline styles → Tailwind classes + @theme tokens in globals.css.
  • Mock data (SATS, DICTS, HISTORY) → real TanStack Query.
  • Hand-built <button>/<div> chrome → @nstart/ui (short-term) or shadcn/ui primitives (long-term).

Files in this bundle

Path What it is
design/compact.html The main prototype — catalog, editor, drawer, modals, time-travel, theme switcher
design/ui-kit.html Design-system reference — tokens, type scale, components in isolation
screenshots/01-catalog-light.png Catalog (Earth theme)
screenshots/02-catalog-dark.png Catalog (Claude-warm dark)
screenshots/03-uikit-light.png Design-system reference (Earth)
screenshots/04-uikit-dark.png Design-system reference (Dark)
review.md Original code review of ordinis-admin-ui — bugs and improvements that drove this redesign

Open both HTML files in any modern browser. Toggle Earth / Dark in the top-right corner. Editor screen and record drawer are accessed by clicking any dictionary row in the catalog.


Design tokens

Both themes share token names, just different values. Apply in globals.css via Tailwind v4's @theme directive.

Light theme (Earth — default, :root)

@theme {
  /* text */
  --color-ink:        #1a1714;   /* body, headings */
  --color-ink-2:      #52483d;   /* secondary text, labels */
  --color-mute:       #8c8276;   /* placeholders, captions, mono labels */

  /* surfaces */
  --color-bg:         #fbf8ee;   /* page background */
  --color-surface:    #ffffff;   /* cards, modals, drawer */
  --color-surface-2:  #f4eedc;   /* hovers, table-row stripe */

  /* lines */
  --color-line:       #e3dccb;   /* hairlines, card borders */
  --color-line-2:     #efe9d8;   /* subtle dividers */

  /* brand + actions */
  --color-navy:       #3a2818;   /* primary button bg (deep coffee) */
  --color-accent:     #b05a2e;   /* terracotta — links, focus, FK chips */
  --color-accent-bg:  rgb(176 90 46 / 11%);
  --color-on-accent:  #ffffff;

  /* semantic */
  --color-green:      #4f7038;   /* "ok" / live */
  --color-green-bg:   #e4ead0;
  --color-warn:       #a8762a;   /* warning / draft */
  --color-warn-bg:    #f6e8bc;
  --color-pink:       #a64636;   /* error / destructive */
  --color-pink-bg:    #f4d9cd;
}

Dark theme (Claude-warm, [data-theme="dark"])

[data-theme="dark"] {
  --color-ink:        #f0ece4;
  --color-ink-2:      #c8c1b3;
  --color-mute:       #8a8474;
  --color-bg:         #1c1a17;
  --color-surface:    #262421;
  --color-surface-2:  #2e2b27;
  --color-line:       #3a3733;
  --color-line-2:     #2d2a27;
  --color-navy:       #d97757;   /* primary becomes Claude-orange in dark */
  --color-accent:     #d97757;
  --color-accent-bg:  rgb(217 119 87 / 16%);
  --color-on-accent:  #1c1a17;
  --color-green:      #6fa97d;
  --color-green-bg:   rgb(111 169 125 / 18%);
  --color-warn:       #d4a653;
  --color-warn-bg:    rgb(212 166 83 / 18%);
  --color-pink:       #d18272;
  --color-pink-bg:    rgb(209 130 114 / 20%);
}

Scope colors (dictionary visibility)

Scope Light fg Light bg Dark behavior
PUBLIC --color-accent (#b05a2e) --color-accent-bg inherits accent
INTERNAL --color-warn (#a8762a) --color-warn-bg inherits warn
TENANT --color-pink (#a64636) --color-pink-bg inherits pink

Always pair with a 6-8px square swatch prefix (<span class="scope-dot">).

Typography

Font Source Used for
Inter 400/500/600/700 Google Fonts All UI text, headings, body
JetBrains Mono 400/500/600 Google Fonts IDs, FK refs, dates, numerics
Tektur 500/600/700 Google Fonts UPPERCASE captions (.cap), logo, theme switch labels

font-feature-settings: "ss01","cv11","cv02","cv03","cv04" on body for Inter's stylistic alternates.

Scale:

  • 22px / 600 — section header (rare, e.g. modal title)
  • 17px / 600 — page title in editor
  • 15px / 600 — dictionary card title
  • 13px / 400-600 — body text, buttons, tabs (this is the workhorse)
  • 12.5px / 500 — table cell text
  • 11px Mono / 500 — IDs, dates
  • 10.5px Tektur uppercase, letter-spacing: .10em — captions / section labels

Spacing

Tight, density-first. The whole UI optimizes for showing many records at once.

  • Card padding: 12px 14px
  • Drawer body padding: 4px 18px 24px
  • Drawer section grid gap: 14px
  • Modal padding: 20px 22px
  • Toolbar height: 40px, with 0 14px horizontal padding
  • Page gutter: 18px horizontal

Radius / shadow

Element Value
Buttons, inputs border-radius: 6px
Cards border-radius: 8px
Modal, drawer border-radius: 8px (drawer only on inner edge)
Toast border-radius: 8px
Chip (badge, FK) border-radius: 4-5px

Shadows are minimal in light, almost absent in dark. Drawer: -20px 0 60px -20px rgba(15,17,21,.35). Modal: 0 24px 48px -16px rgba(15,17,21,.30). Toast: 0 12px 30px -10px rgba(40,25,10,.38).


Screens

The prototype is one page that toggles between catalog and editor via a state machine (view: "catalog" | "editor"). In production these become two TanStack Router routes.

Screen 1 — Catalog (/dictionaries)

Layout (1280px+):

┌─ sidebar 220px ──┬─ main ─────────────────────────────────────┐
│ logo (ORDINIS)   │ header (h=56): breadcrumb · ⌕ search · usr │
│ nav rail         ├────────────────────────────────────────────┤
│ ─ Главная        │ toolbar (h=44): filter chips · "+ Создать" │
│ ▣ Справочники 40 ├────────────────────────────────────────────┤
│   Мои черновики  │ bundle group: cuod (10)                    │
│   На ревью    7  │   ┌── card ──┐ ┌── card ──┐                │
│   Outbox      2  │   │ title    │ │ title    │  …             │
│   Webhooks       │   │ desc     │ │ desc     │                │
│   Аудит          │   │ FK chips │ │ FK chips │                │
│   Поиск          │   └──────────┘ └──────────┘                │
│                  │ bundle group: geo (5)                      │
│ user · profile   │   …                                        │
└──────────────────┴────────────────────────────────────────────┘

Toolbar:

  • Search input — width: 280px, placeholder "Поиск по названию, коду…", searches id + title + desc.
  • Scope chip group — three toggleable buttons with colored dot prefix.
  • Bundle filter — single-select chips after a small "BUNDLE" Tektur label.
  • Right side: "+ Создать справочник" primary button → opens modal.

Card:

  • 3px left strip in scope color.
  • Title (15px / 600 / --color-navy) + id below in Mono 11px gray.
  • Description (12px) clamped to 2 lines.
  • Relations row — → ссылается label + chips for fk[]; ← N использ. + chips for refBy[] (dimmed dashed border style).
  • Right column: ScopeBadge + version mono.
  • Hover: translateY(-1px) + soft shadow; entire card is the click target → opens editor.

FK chip: clickable button, padding: 2px 7px, border-radius: 4px, mono 11px. Active variant: bg: --color-accent-bg, color: --color-accent. Dimmed: transparent bg + dashed border. onClick stops propagation and navigates to that dictionary.

Screen 2 — Editor (/dictionaries/$name)

Layout:

┌─ sidebar (collapsed to 56px in editor) ─┬─ main ─────────────────┐
│ ⌂  ▣ ✎ ◐ ↗ ⇆ ☷ ⌕                       │ breadcrumb + actions   │
│                                          │ title row              │
│                                          │ status banner          │
│                                          ├────────────────────────┤
│                                          │ tabs (h=40)            │
│                                          ├────────────────────────┤
│                                          │ tab body               │
│                                          │   - records (table)    │
│                                          │   - relations (FK list)│
│                                          │   - fields (schema)    │
│                                          │   - json (raw schema)  │
│                                          │   - events (audit)     │
│                                          │   - history (timeline) │
│                                          ├────────────────────────┤
│                                          │ footer toolbar         │
└──────────────────────────────────────────┴────────────────────────┘

Breadcrumb row: "Справочники / Космические аппараты" + right-aligned: [Export] [Compare] [⋯ menu].

Title row: large title (17px / 600) + scope badge + version mono + edit pencil.

Status banner (sits above tabs): 1-line card showing current workflow state.

  • Draft: --color-warn-bg background, left edge --color-warn, copy "Черновик · v1.8.0+draft · 3 правки · открыт {user}".
  • Review: --color-accent-bg, copy "На ревью · ждёт {reviewer} · открыто {time}".
  • Live: --color-green-bg, "Опубликовано · v1.8.0 · {time}".
  • Buttons on the right adapt to state: Запросить ревью (draft), Approve / Request changes (review), Создать черновик (live).

Tab bar: horizontal scrollable on narrow viewports; active tab has --color-accent bottom border, weight 600. Right side of bar has utility buttons (Time-travel ◷ opens a popover with date picker).

Records tab — the main view:

  • Compact table, row height 32px, monospace IDs in first column.
  • Sticky header with column sort arrows and a search icon per column (filter).
  • Row hover: --color-surface-2 background.
  • Click anywhere in a row → opens RecordDrawer on the right.
  • Bulk-select via leftmost checkbox column.
  • Footer toolbar: "N из M записей" · paging · "+ Запись" (right-aligned, primary).

Relations tab — two sections:

  • "Исходящие FK" — table: field · target dict · cardinality · critical / soft.
  • "Входящие ссылки" — table: source dict · field · count of records using it.

Fields tab — full JSON-schema view as an editable grid:

  • Columns: name · type · required · unique · i18n · index · description.
  • Inline edit; "+ Поле" at bottom.

JSON tab — Monaco editor with the raw schema.json; read-only on Live records.

Events tab — audit log filtered to this dict:

  • Filter pills: webhook · publish · review · import · retried.
  • One row per event with mono timestamp, actor avatar, target record, status badge.

History tab — vertical timeline:

  • One node per commit, current HEAD marked.
  • Each node shows: short SHA, message, author, diff counters (+N -M).
  • Click → opens diff in a slide-over.

Screen 3 — RecordDrawer (right slide-over)

Opens on row click. Two widths: 520px (default) and 880px (toggle via / button).

Header (sticky): breadcrumb "редактирование записи · satellites" caption, then id mono + · name, then [/] [История] [×].

Toolbar (sticky): search field for filtering schema fields by name + "только заполненные" toggle + counter "N / M".

Section anchor chips: horizontally scrolling row of section names with field-count badge; click to scroll-to-section. Active chip is filled navy/accent.

Body: sections in order with sticky section headers (.cap style). Each section is a 2-column grid (or 4 in wide mode). Section list — see Record schema below.

Wide mode (880px): adds a left ToC rail (180px) with the same sections as a vertical list, mirroring the chip strip.

Footer (sticky): [Удалить] (pink) · "не сохранено · N полей" caption · [Отмена] · [Сохранить] (primary).

Screen 4 — Modals

Create dictionary — centered dialog, 560px:

  • "НОВЫЙ СПРАВОЧНИК" cap header.
  • Fields: Название (text), id (mono, slug-validated, auto-derived from title), Bundle (select), Scope (radio with colored dots), Поддерживаемые локали (multi-chip — ru / en / zh / es / fr / de / ar).
  • Footer: [Отмена] · [Создать].

Confirm delete — centered dialog, 440px:

  • Pink left-accent stripe (4px).
  • "ПОДТВЕРЖДЕНИЕ" cap header.
  • Copy explains consequences (e.g. "12 справочников ссылаются на эту запись через FK").
  • Footer: [Отмена] · [Удалить] (pink danger button).

Toast — bottom-right slide-up:

  • Navy (or Claude-orange in dark) --color-navy background.
  • "OK" cap header in toast-cap color (orange-cream), then short message, then ×.
  • Auto-dismiss after 4s.

Screen 5 — Time-travel

A popover from the time-travel button in the editor tab bar. Two parts:

  • Date picker — calendar with 30 day range; selecting a date snapshots the dict and editor to that point.
  • Quick presets — "сейчас" · "1ч назад" · "вчера 18:00" · "перед последним публикованием".
  • "Применить" / "Сбросить" footer.

When active, all editor surfaces gain a thin amber top border and a banner: "Просмотр на {timestamp} · только чтение · [Сбросить]".


Record schema (sections in drawer)

The drawer's section list is the canonical "what fields a record can have" definition. The prototype encodes this in RECORD_SCHEMA (see design/compact.html ~line 220).

Section Fields Notes
Основное id, active toggle, name_ru/en/zh/es, description name_ru is the only required loc; others optional
Связи (FK) operator, status (enum), orbit_type, mission, launch_site, launch_vehicle, manufacturer, data_center Each is a FK to another dict
Геометрия орбиты altitude_km, inclination, period_min, eccentricity, raan, arg_perigee numerics with units
Физические параметры mass_kg, power_w, design_life, dimensions
Инструменты и продукты instruments[], products[], ground_stations[] tag inputs
Внешние идентификаторы cospar_id, norad_id, sat_cat, itu mono text
Даты и события launch_date, decom_date, first_light, last_contact
Флаги public, deprecated, sensitive, draft_only toggles
Произвольные данные raw_json JSON editor (Monaco)
Системные (readonly) created_at, updated_at, created_by, version

In production these sections must come from the dictionary's JSON-schema metadata (x-section per property), not be hard-coded.


Components inventory

Map each prototype piece to a target component file. Where possible reuse @nstart/ui; where it's broken, replace.

Atomic

Prototype Target Notes
Cap (uppercase Tektur label) <Cap> in components/ui/Cap.tsx Plain <span class="cap">, Tektur 10.5px
ScopeBadge components/catalog/ScopeBadge.tsx Pill with colored dot + uppercase label
ScopeDot components/catalog/ScopeDot.tsx 8×8 colored square
FkChip components/catalog/FkChip.tsx Active + dimmed variants
StatusBadge (DRAFT / LIVE / DEPRECATED) components/ui/StatusBadge.tsx Background = --color-warn-bg / --color-green-bg / --color-pink-bg
Btn / BtnPrimary / BtnDanger / BtnGhost @nstart/ui Button (variant) Patch theme tokens; danger uses --color-pink
IconBtn (28x28 with icon glyph) components/ui/IconButton.tsx
Toggle (switch) @nstart/ui Toggle or shadcn Switch Native indeterminate not needed
TagInput replace @nstart/ui MultiSelect → shadcn Combobox + cmdk See "Stack migration"

Composite

Prototype piece Target component File
Sidebar (nav rail + collapse) <Sidebar> components/layout/Sidebar.tsx
Header (breadcrumb + search + user) <TopBar> components/layout/TopBar.tsx
Catalog toolbar <CatalogToolbar> components/catalog/CatalogToolbar.tsx
Catalog card <DictCard> components/catalog/DictCard.tsx
Catalog bundle group <BundleGroup> components/catalog/BundleGroup.tsx
Editor tab bar <EditorTabs> components/editor/EditorTabs.tsx
Editor status banner <WorkflowBanner> components/editor/WorkflowBanner.tsx (variant = draft/review/live)
Records table <RecordsTable> components/editor/RecordsTable.tsx (TanStack Table)
Record drawer <RecordDrawer> components/editor/RecordDrawer.tsx
Drawer field renderers <DrawerField> components/editor/DrawerField.tsx (kind-switch: text / mono / num / date / fk / enum / toggle / tags / textarea / json)
Create-dict modal <CreateDictModal> components/modals/CreateDictModal.tsx
Confirm modal <ConfirmModal> components/modals/ConfirmModal.tsx
Toast <Toast> use sonner (recommended) or @nstart/ui Toast
Time-travel popover <TimeTravelPopover> components/editor/TimeTravelPopover.tsx
Theme switcher <ThemeSwitch> components/layout/ThemeSwitch.tsx, writes data-theme attr + localStorage

Stack migration plan (@nstart/uishadcn/ui)

Three-phase migration. Don't do it all at once. Phase 1 unblocks the redesign; phases 2-3 happen over the following weeks.

Phase 1 — Patch & ship (1 day)

  1. pnpm patch @nstart/ui to fix three things in dist/index.js:
    • Select / MultiSelect / DatePicker popover → createPortal(node, document.body) with position: fixed, computed top/left from triggerRef.getBoundingClientRect(), z-index: 9999, listens to scroll/resize.
    • Outside-click detection: check both triggerRef.contains(target) and popoverRef.contains(target).
    • Checkbox: forward ref to inner <input>, sync indeterminate prop via useEffect.
  2. Add @theme block to globals.css with both palettes (above).
  3. Add <ThemeSwitch> component → toggles data-theme on <html>.
  4. Wire @nstart/ui theme tokens to our --color-* variables (one CSS file, override their CSS vars).

Phase 2 — Barrel + Radix replacements for broken components (3-4 days)

Create a barrel at src/ui/index.ts:

// re-export anything from @nstart/ui that works
export { Button, Alert, Badge, Tabs, TextInput, Drawer } from '@nstart/ui';

// override the broken ones with our wrappers
export { Select, MultiSelect } from './Select';     // Radix Select + cmdk
export { Modal } from './Modal';                    // Radix Dialog
export { Checkbox } from './Checkbox';              // Radix Checkbox
export { Combobox } from './Combobox';              // cmdk-based

Then a codemod / find-and-replace across the codebase: from '@nstart/ui'from '@/ui'. Adopt shadcn/ui primitives inside the wrappers:

  • npx shadcn@latest init (Tailwind v4 mode, slate base, CSS variables = yes).
  • npx shadcn@latest add select combobox dialog checkbox dropdown-menu popover tooltip command.
  • Edit the generated files to use our --color-* tokens directly (replace shadcn's primary / destructive etc. with our names).

Phase 3 — Full shadcn for new screens, gradual replace for old (2-3 weeks, parallel to feature work)

  • All new components use shadcn directly.
  • Old @nstart/ui Button / Alert / Badge / Tabs / TextInput / Drawer get migrated one at a time as touched.
  • End state: @nstart/ui removed from package.json, patch deleted.

Interactions & behavior

Tables (all 5 — catalog list, records, fields, events, history-preview)

  • Row hover background uses --color-surface-2; default is --color-surface. Never hardcode #fff / #fafbfc in onMouseEnter/Leave handlers — those break dark mode (rows go white-on-white). Either use CSS tr:hover { background: var(--color-surface-2) } or pass theme tokens to the inline handler.
  • Row height: 32px in production; 28px in compact density mode.
  • IDs in the first column are mono + --color-accent; clickable but stopPropagation is not needed (row click and id click both navigate to the same place).

Catalog

  • Search debounced 150ms.
  • Filter chips are URL-synced via TanStack Router validateSearch (?q&scopes&bundle&onlyWithRefs).
  • Card click → navigate({ to: '/dictionaries/$name', params: { name: id } }).

Editor

  • Tab change → ?tab=records|relations|fields|json|events|history.
  • Status banner → mutations (request-review, approve, publish, create-draft) hit existing API and invalidate the dict query.
  • Records table sort/filter are URL-synced.
  • Pressing j/k moves selected row; Enter opens drawer; Esc closes; n opens new-record drawer; / focuses search.

Drawer

  • Width toggle persists per-user to localStorage (ord-drawer-wide).
  • "Только заполненные" filter only hides fields with no value (excluding active toggles set to false — those still show because the field is "configured").
  • "История" button opens the History tab of the current dict filtered to this record.
  • Save → optimistic update + toast; conflict → modal with diff.

Time-travel

  • Opens via tab-bar button.
  • Sets ?asOf=2026-04-30T09:00:00Z in search.
  • All API queries scope to that timestamp; mutations disabled (UI goes read-only with amber border).
  • "Сбросить" clears asOf.

Theme

  • Click switch → document.documentElement.setAttribute('data-theme', val) + localStorage.setItem('ord-theme', val).
  • Initial load reads localStorage; falls back to prefers-color-scheme.

Responsive

Desktop-first (1280px+ baseline). Three breakpoints:

Breakpoint Changes
≤ 1024px Sidebar becomes overlay drawer; hamburger in TopBar; main grid 0 1fr
≤ 880px Drawer and modals go fullscreen (100vw, 100dvh, border-radius: 0); editor body is single column
≤ 560px Table columns 7+ hidden via nth-child; toolbar wraps; sidebar always overlay

Accessibility checklist

  • All buttons have visible focus rings — use --color-accent 2px outline-offset 2px.
  • Drawer + modal are focus-trapped (shadcn Dialog gives this free).
  • Drawer + modal close on Esc.
  • Theme switch has aria-label; current value is aria-pressed.
  • Tables: <th scope="col">; sortable headers are <button> with aria-sort.
  • Status badges include text, never color-only.
  • All FK chips have a title with the full target id.

Open questions for the team

  1. JSON-schema FK convention — is it x-ref, $ref, or x-dict? extractFks must match.
  2. Backend graph endpoint — willing to add GET /api/v1/dictionaries/graph so the catalog doesn't recompute on every load?
  3. Workflow state machine — confirm exact transitions (draft → review → live → archived?). Current banner assumes draft / review / live.
  4. Time-travel granularity — minute? second? day? Affects calendar UX.
  5. Sections in record drawer — confirm x-section will be added to schema metadata, or do we need a separate ordering file?
  6. Are sensitive fields really record-level or are they field-level (mask in UI based on user permission)?

Out of scope

  • Mobile-first redesign — admin remains desktop-primary.
  • Bulk import / export UX — separate work.
  • Permission management UI — separate work.
  • Webhook subscription manager — separate work.
  • The form-validation bugs in review.md (#4 AJV errors, #12 token storage) — covered in Phase 1 patch + ongoing.