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:
- Design system — earthy / woody light theme + Claude-style warm dark theme. Inter + JetBrains Mono + Tektur. Custom token palette.
- Dictionary catalog — bundle-grouped grid with FK chips, scope/bundle filters, search.
- 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.
- Modals — Create dictionary, Confirm delete, Toast notifications.
- 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 +
@themetokens inglobals.css. - Mock data (
SATS,DICTS,HISTORY) → real TanStack Query. - Hand-built
<button>/<div>chrome →@nstart/ui(short-term) orshadcn/uiprimitives (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, with0 14pxhorizontal padding - Page gutter:
18pxhorizontal
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 "Поиск по названию, коду…", searchesid + 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 forfk[];← N использ.+ chips forrefBy[](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-bgbackground, 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-2background. - 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
HEADmarked. - 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-navybackground. - "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/ui → shadcn/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)
pnpm patch @nstart/uito fix three things indist/index.js:Select/MultiSelect/DatePickerpopover →createPortal(node, document.body)withposition: fixed, computed top/left fromtriggerRef.getBoundingClientRect(),z-index: 9999, listens to scroll/resize.- Outside-click detection: check both
triggerRef.contains(target)andpopoverRef.contains(target). Checkbox: forward ref to inner<input>, syncindeterminateprop viauseEffect.
- Add
@themeblock toglobals.csswith both palettes (above). - Add
<ThemeSwitch>component → togglesdata-themeon<html>. - Wire
@nstart/uitheme 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'sprimary/destructiveetc. 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/uiButton / Alert / Badge / Tabs / TextInput / Drawer get migrated one at a time as touched. - End state:
@nstart/uiremoved frompackage.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/#fafbfcinonMouseEnter/Leavehandlers — those break dark mode (rows go white-on-white). Either use CSStr: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 butstopPropagationis 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/kmoves selected row;Enteropens drawer;Esccloses;nopens 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:00Zin 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-accent2px 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 isaria-pressed. - Tables:
<th scope="col">; sortable headers are<button>witharia-sort. - Status badges include text, never color-only.
- All FK chips have a
titlewith the full target id.
Open questions for the team
- JSON-schema FK convention — is it
x-ref,$ref, orx-dict?extractFksmust match. - Backend graph endpoint — willing to add
GET /api/v1/dictionaries/graphso the catalog doesn't recompute on every load? - Workflow state machine — confirm exact transitions (draft → review → live → archived?). Current banner assumes draft / review / live.
- Time-travel granularity — minute? second? day? Affects calendar UX.
- Sections in record drawer — confirm
x-sectionwill be added to schema metadata, or do we need a separate ordering file? - 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.