Commit Graph

110 Commits

Author SHA1 Message Date
Zimin A.N. 889207f629 feat: wire drawer toolbar filters + records table responsive hiding
Closing 2 handoff gaps:

1) DRAWER TOOLBAR FILTERS (handoff Screen 3 line 279)
SchemaDrivenForm получил три new optional props:
  - searchFilter (string) — case-insensitive substring против field key + label
  - onlyFilled (boolean) — hide fields с empty data[key]
    (null/undefined/''/false/0/empty-array/empty-object)
  - onCounterChange (visible, total) — surface field counter parent'у

Filter применяется поверх bucketing — все три buckets (identity/description/
extra) фильтруются. UI рендерит filteredBuckets.X.map(...) вместо raw bucket'ов.

В sections layout (drawer use case) если section после filter empty —
sticky header не рендерится. Tabs layout backward-compat (filter применяется
но visually invisible если user в другом tab'е).

dictionaries.$name.tsx callsite:
  - State: drawerSearch, drawerOnlyFilled, drawerCount {visible, total}
  - Reset on drawer close
  - RecordDrawer.toolbar prop wired только в edit mode (create — все поля
    empty, 'только заполненные' даст 0/0)
  - SchemaDrivenForm receives все три filter props

Result: user может typing 'mass' в drawer search → видит только mass_kg /
data_mass / etc.; toggle 'только заполненные' → hide unused optional fields.

2) RECORDS TABLE RESPONSIVE COLUMN HIDING (handoff line 466)
Records table в dictionaries.$name.tsx применяет hideBelow API из MR !59:
  - businessKey: всегда visible
  - name/code: hideBelow='md' (768px)
  - extra schema columns: hideBelow='lg' (1024px)
  - scope badge: hideBelow='sm' (640px)
  - validFrom: hideBelow='lg'
  - actions: всегда visible

На mobile 375px только businessKey + actions visible — chrome остаётся
usable. Tablet 768+ adds name/scope. Desktop 1024+ adds schema columns.

Tests: 116 pass.
2026-05-11 17:10:26 +03:00
Zimin A.N. e5572d5346 fix(theme): de-saturate light bg — убрать жёлтый Earth-cream tone
Пользователь: 'этот желтый фон давай уберем'. Светлый bg #fbf8ee
выглядел слишком cream/yellow на staging — особенно на больших areas
страницы (список справочников, hover states).

Color token changes (light theme только; dark не трогаем):
  --color-bg:        #fbf8ee → #fafaf8  (off-white neutral)
  --color-surface-2: #f4eedc → #f2f1ee  (subtle warm grey hover/stripe)
  --color-line:      #e3dccb → #e4e1dc  (de-saturated border)
  --color-line-2:    #efe9d8 → #ecebe7  (subtle inner border)

Brand accents preserved:
  --color-accent: #b05a2e (warm orange-brown) — unchanged
  --color-navy:   #3a2818 (deep brown) — unchanged
  --color-warn / pink / green BG — unchanged (semantic states)

Result: cleaner light theme, professional reading surface. Accent
color по-прежнему даёт warmth для brand identity, но page bg
больше не tints всё жёлтым.
2026-05-11 17:01:54 +03:00
Zimin A.N. 155c70e23c feat(drawer): all-sections layout с chips strip per handoff prototype
Handoff Screen 3 RecordDrawer показывает sections подряд с chips strip
для quick jump, не tabs (currently SchemaDrivenForm has 3 tabs).

SchemaDrivenForm:
  - New optional prop layout: 'tabs' (default, legacy) | 'sections'
  - В sections mode:
    * Section chips strip на верху (sticky top-0) вместо Tabs
    * Chip = bucket name + field count в Mono
    * Click chip → scrollIntoView к section
    * Все 3 buckets рендерятся подряд (visible одновременно)
    * Каждая section получает sticky h3 cap header + scroll-mt-12
    * z-indexed sticky: chips strip = 10, section headers = 5
  - В tabs mode: backward-compat без изменений

RecordDrawer callsite (dictionaries.$name.tsx):
  - Передаёт layout='sections' в SchemaDrivenForm (create + edit)
  - Существующий tabs API остаётся для test'ов которые используют default

Layout matches handoff Screen 3 prototype:
  [chips strip sticky]
  [section: Основное]
   field grid 2-col
  [section: Описание (i18n)]
   localized fields
  [section: Дополнительно]
   optional fields grid

Future Phase B refactor: backend x-section schema metadata (open question
#5) разблокирует per-property sections (вместо bucketing). Текущий
fallback bucketing (identity/description/extra) достаточен для MVP.

Tests: 116 pass (test использует default 'tabs' layout, не разломан).
2026-05-11 16:37:38 +03:00
Zimin A.N. a57cf520a4 feat(editor): left info panel + 3-col layout per handoff prototype
Handoff prototype design/compact.html показывает editor как 3-col layout:
[Sidebar 220 (global nav)] [InfoPanel 220 (dict metadata)] [main content].
Текущая implementation была 2-col (nav + everything).

NEW src/components/editor/EditorInfoPanel.tsx:
  - Scope badge (PUBLIC/INTERNAL/RESTRICTED) — top
  - ОПИСАНИЕ section — caption + dict.description body
  - Metadata 2x2 grid: Bundle / Версия / Записей / Локали
    (mono text-ink-2, cap labels)
  - СВЯЗИ section с двумя группами:
    → ссылается  outgoing FK list (name links к /dictionaries/$name)
    ← используют incoming refBy list (deduplicated sourceDict names)
  - sticky lg:top-2 self-start чтобы panel оставалась видна при scroll
  - lg:max-h-[calc(100vh-7rem)] overflow-y-auto если metadata высокий

src/routes/dictionaries.$name.tsx:
  - Обернул содержимое (banner + tabs + tab content) в:
    <div className='grid grid-cols-1 lg:grid-cols-[220px_1fr] gap-4 lg:gap-6'>
      <EditorInfoPanel ... />
      <div>{banner + tabs + content}</div>
    </div>
  - На <lg переключается в stacked (info above content) — single column
    благодаря grid-cols-1.

PageHeader остаётся выше grid (full-width breadcrumb + title + actions).
Action buttons (AOI / Time-travel / Schema edit / Создать) живут в
PageHeader.actions slot — handoff prototype показывает их в info panel,
но рядом с title лучше discoverable. Может уйти в info panel в future
refactor.

Tests: 116 pass. TS strict clean.
2026-05-11 16:33:40 +03:00
Zimin A.N. f43ce5563a feat(catalog): row-based table per handoff prototype design/compact.html
Refactor каталога с card grid → compact row table. Handoff README line
46 явно говорит 'clicking any dictionary row in the catalog' — наша cards
implementation diverged от прототипа.

What's new:
- DictionaryListTable — single flat table (bundle стал column, не группа)
- DictRow — compact tr с scope dot prefix + title + truncated desc
- 8 columns per prototype: name / id / bundle / scope / records / → / ← / updated
- Responsive column hiding via hideBelow-style breakpoints:
  - id: hidden <md
  - bundle: hidden <lg
  - scope: hidden <sm
  - records: hidden <md
  - → ←: hidden <lg
  - updated: hidden <xl
- На mobile <640px показано только name column (с inline scope dot и desc)
- Row click → navigate к editor; Enter/Space keyboard accessible (role=link)

Removed:
- DictCard component (card layout)
- FkChip component (FK chips на cards — теперь just counts)
- SCOPE_STRIP (vertical 3px strip — заменён на inline scope dot)
- groupByBundle usage (bundle стал column, не group section)
- MAX_REFBY_CHIPS const

Note: outgoing FK count (→) пока показывает '—' — требует schemaJson per-dict
fetch (40+ requests). Batch endpoint GET /dictionaries/graph (handoff open
question #2) разблокирует это. Текущий incoming refBy через useDictionary-
Dependents работает per row.

Tests: 116 pass. TS strict clean.
2026-05-11 16:30:31 +03:00
Zimin A.N. d4000ddd3d feat(drawer): sticky toolbar API per handoff Screen 3 line 279
RecordDrawer теперь принимает optional toolbar prop:
  - search (string) — field name filter
  - onlyFilled (boolean) — show only filled fields toggle
  - counterVisible/Total (number) — 'N / M полей' caption

Sticky position между header и body, h=44 per handoff style. Toolbar
rendered только если caller передал. SchemaDrivenForm callsite (записать
filter в каждое FieldRenderer) — separate follow-up, требует refactor
form internals (currently 3 buckets identity/description/extra).

Section anchor chips + wide-mode ToC rail — deferred до backend
x-section schema metadata (CEO open question #5 в handoff).
2026-05-11 16:02:11 +03:00
Zimin A.N. a09c4113c1 feat(editor): keyboard shortcuts j/k/Enter/n per handoff Screen 2
Handoff line 438: 'Pressing j/k moves selected row; Enter opens drawer;
Esc closes; n opens new-record drawer; / focuses search.'

Implementation:
  - rowCursor state — index в visibleRecords (-1 = no focus)
  - j или ArrowDown → cursor++ (clamped)
  - k или ArrowUp → cursor-- (clamped)
  - Enter → setEdit({kind:'edit', record}) когда cursor valid
  - n → setEdit({kind:'create'}) когда canMutate
  - Esc — handled Radix Dialog natively через RecordDrawer
  - / — handled TopBar (⌘K equivalent, focuses global search input)

Guards:
  - Skip когда target inside input/textarea/contentEditable (no conflict с typing)
  - Skip когда drawer/modal open (edit.kind !== 'closed' — focus в portal)
  - Cursor reset на page/filter change (stale index безопасности)

Visual: active row получает bg-accent-bg/40 outline accent для clarity.
2026-05-11 16:00:42 +03:00
Zimin A.N. 0acba4c073 feat(handoff): WorkflowBanner + table a11y + responsive + toast styling
Closing handoff gaps выявленных при полном audit'е README.md vs current
implementation. См. screen-by-screen mapping ниже.

== Screen 2 — Editor: WorkflowBanner (NEW) ==
Required by handoff (line 238-242) но не существовал. Inline status row выше
tab bar — live/review/info variants. Backend не имеет explicit dict-level
workflow state (draft → review → live transition), используем proxy:
  - approvalRequired=false      → 'Live' (green-bg + green left edge)
  - approvalRequired=true + 0   → 'Approval enabled' (accent-bg + accent edge)
  - approvalRequired=true + N   → 'На ревью N' (warn-bg + warn edge), link к /reviews

Backend extension future: добавить explicit workflowState к DictionaryDetail
+ buttons для transitions (request-review / approve / publish). Сейчас banner
дает минимум — visual workflow signal без UI mutation buttons.

== Table accessibility per handoff line 476 ==
TableHeaderCell:
  - scope='col' (a11y screen reader requirement)
  - aria-sort='ascending|descending|none' когда sortable prop true
  - New sortable + sortDirection props (API ready, не используется пока)

== 560px responsive per handoff line 466 ==
TableHeaderCell + TableCell получили optional hideBelow='sm|md|lg' prop:
  hideBelow='sm' → hidden <640px (table-cell ≥sm)
  hideBelow='md' → hidden <768px
  hideBelow='lg' → hidden <1024px

Callsites могут пометить secondary columns (created_by, updated_at, etc.)
hideBelow='md' для clean mobile experience. Текущие routes ещё не используют
prop — API ready для incremental adoption.

== Toast styling per handoff line 302-305 ==
Sonner toast — раньше surface bg + ink text. Handoff:
  - Navy bg + on-accent text (warm cream-orange feeling)
  - 4s auto-dismiss
  - Variant accent через left border 4px:
    success → green / error → pink / info → accent / warning → warn
  - cap-style title (но используем text-body для нативности, не overkill)
  - 13px font-sans

richColors disabled — наши tokens вместо sonner defaults.

Files:
  - NEW src/components/editor/WorkflowBanner.tsx
  - src/routes/dictionaries.$name.tsx (wire banner above tab bar)
  - src/ui/components/table.tsx (a11y + hideBelow API)
  - src/ui/components/toaster.tsx (navy variant + variant borders)
  - src/ui/index.ts (TableCellProps export)

Tests: 116 pass. TS strict clean.
2026-05-11 15:58:12 +03:00
Zimin A.N. 787cea9db1 fix(table+layout): TableHead alias to THEAD wrapper + kill horizontal scroll
CRITICAL bugs reported by user — table выглядела кривой, контент уезжал
за экран с горизонтальным скроллом.

Bug 1 — invalid HTML в Table:
  Callsites используют <TableHead><TableRow><TableHeaderCell>... ожидая
  нstart-style THEAD wrapper. Но мой alias делал TableHead = TableHeaderCell
  (TH cell). Получалось <th><tr><th>...</th></tr></th> — invalid nested,
  browser parser hoist'ил TableRow наружу, создавая phantom 509px column
  слева от данных. Колонки выглядели misaligned, content смещён вправо.

  Fix: TableHead → THEAD wrapper (alias TableHeader). TableHeaderCell
  остаётся отдельным TH cell. Source semantically correct, browser parses
  валидно, phantom column исчез.

Bug 2 — horizontal overflow:
  Scope strip border на верху detail-страницы использовал
  '-mt-6 -mx-4 sm:-mx-6 lg:-mx-8'. На lg viewport main padding только
  sm:px-6 (24px), strip negative margins -mx-8 (32px) → 8px overflow each
  side → body.scrollWidth > viewport → horizontal scrollbar.

  Fix:
  - Strip negative margins только до -mx-6 (соответствует main padding)
  - main получил overflow-x-clip + max-w-7xl wrapper получил min-w-0
    (защита от любых future overflow children)

Verified в preview viewport 1440 + 375: body.scrollWidth === viewport,
no h-scroll.
2026-05-11 15:49:27 +03:00
Zimin A.N. 9e2d985485 fix(ui): tailwind-merge стрипал text-body — buttons ушли в 16px inherited
CRITICAL bug — на staging buttons рендерились в 16px вместо handoff
workhorse 13px. UpdateBanner secondary buttons показывали invisible
текст (text-ink overrode by inherited text-on-accent от parent banner).

Root cause:
  buttonVariants base = 'rounded-md text-body font-medium ...'
  primary variant     = 'bg-navy text-on-accent ...'

tailwind-merge default config pattern-matches `text-*` как ОДНУ группу
(color). Видя `text-body text-on-accent` он считал это конфликтом двух
colors → keep last → text-on-accent остаётся, text-body стрипается.
Result: button теряет font-size → inherit body 16px.

Fix: extendTailwindMerge регистрирует наши custom semantic typography
utilities (text-body/cell/mono/cap/title-*) в группе font-size (отдельной
от colors). twMerge теперь знает `text-body text-on-accent` это size +
color, обе сохраняются.

Also: text-mono/text-cap в font-family группе (они baked Mono/Tektur)
— fix конфликт с font-mono/font-display tokens если кто-то их случайно
combined.

Verified в preview: 'Sign in' button class теперь содержит text-body,
computed fontSize: 13px (вместо 16px).

Plus CI: release job image node:22-alpine → repo.nstart.cloud/library/
node:22-alpine (Docker Hub rate-limited, mirror используется во всех
других jobs).
2026-05-11 15:45:27 +03:00
Zimin A.N. 7a3b31b957 feat(mobile): Sidebar drawer <1024px + Modal/Sheet fullscreen <880px
Per design_handoff_ordinis_mdm/README.md responsive table (line 464-465):
- ≤1024px: Sidebar becomes overlay drawer (hamburger в TopBar)
- ≤880px:  Drawers и modals fullscreen (100vw × 100dvh, no border-radius)

Implementation:

src/components/layout/Sidebar.tsx
  - Extracted SidebarContent (logo + nav + user block) для DRY
  - Desktop <Sidebar /> wraps SidebarContent в hidden lg:flex aside
  - New <MobileSidebar open onClose /> — Sheet с тем же содержимым,
    260px wide, slides from left
  - SidebarLink принимает onClick → drawer auto-close при navigate

src/components/layout/TopBar.tsx
  - Optional onMenuClick prop — рендерит hamburger button (Menu icon
    из lucide-react) с lg:hidden
  - VersionBadge скрыт <md (768px) — diagnostic info, не critical UX

src/routes/__root.tsx
  - useState mobileNavOpen, передаём в MobileSidebar + TopBar

src/ui/components/dialog.tsx
  - SIZE_CLASSES получили  prefix (arbitrary Tailwind v4
    breakpoint per handoff exact 880px). На mobile <880px modal становится
    fullscreen (inset-0 w-full h-full no rounded), ≥880px — centered с
    translate + max-w из SIZE_CLASSES.

src/ui/components/sheet.tsx
  - size variants: sm:max-w-* → min-[880px]:max-w-*. На mobile <880px
    sheet берёт w-full (fullscreen drawer per handoff).

src/components/record/RecordDrawer.tsx
  - max-width переключатель 520/880 теперь применяется только ≥880px.
    На mobile drawer всегда fullscreen.

src/ui/components/language-switch.tsx
  - Display fallback: opt.short → opt.label → uppercase id.
    Previously показывал id ('ru-RU', 'en-US') когда short не был задан —
    теперь использует label ('RU', 'EN') как fallback.

Browser-verified в preview viewport 375×812:
  - Hamburger ☰ visible, click открывает Sheet с nav items
  - Backdrop fade + slide-from-left animation
  - TopBar fits в 375px (RU/EN compact, Sign in visible)
  - LoadingBlock + Alert text-wrap correctly
2026-05-11 15:39:23 +03:00
Zimin A.N. f0392af0e3 feat(editor): Stage 3.4 polish — Events embedded + JSON syntax highlight
Per handoff Stage 3.4 polish list (Records/Relations/Fields/JSON/Events/
History tabs).

Events tab — раньше был redirect к /audit?dict=name. Теперь embedded:
- useAudit({dictionaryName, size: 20}) → last 20 entries
- 4-col compact table: time / action / businessKey / user
- Empty state когда нет событий
- Bottom link → /audit с filter для full diff view

JSON tab — раньше был plain <pre>. Теперь:
- Regex-based syntax highlight (keys=accent, strings=green, numbers=warn,
  booleans=pink, null=mute italic)
- Copy-to-clipboard button с visual feedback
- Bundle cost: 0 (нет Monaco). Если потребуется EDIT — Monaco lazy
  через React.lazy в Stage 3.x followup.

Fields tab — пока read-only table остаётся. Inline edit требует backend
PATCH /dictionaries/{name}/schema endpoint; deferred.

History tab — placeholder remains. Требует backend changelog endpoint;
deferred.

TS strict + 116 tests pass.
2026-05-11 15:28:29 +03:00
Zimin A.N. 5007e0f4eb feat(ui): drop @nstart/ui dependency (Stage 3.9)
Все font cascade bugs из последних MR были вызваны @nstart/ui pollution
(Tektur inheritance от Card parent'ов, Onest font в Button, font-primary
class overrides). Решили обрезать раньше plan'а.

What changed:

New primitives (8 файлов в src/ui/components/):
  - panel.tsx           — container с border/surface
  - field.tsx           — FieldLabel/Hint/Error typography helpers
  - text-input.tsx      — Input + label/hint/error composite
  - table.tsx           — Table/TableHeader/Body/Row/HeaderCell/Cell/Empty
  - date-picker.tsx     — native <input type=date> wrapper с label/hint/error
  - multi-select.tsx    — Radix Popover + inline checkboxes
  - single-select.tsx   — native <select> с label/hint/error chrome
  - language-switch.tsx — segmented control в Tektur uppercase

Compatibility shims (existing components extended):
  - checkbox.tsx        — + label/description/error/onChange (nstart-compat
                          synthetic event), wrap в <label> когда есть chrome
  - modal.tsx           — + panelClassName / bodyClassName props
  - form-actions.tsx    — + align prop (start/end/between)
  - tabs-simple.tsx     — TabItem.count alias for .trailing (nstart-compat)
  - table TableHead     — alias TableHeaderCell с align prop

Barrel rewrite (src/ui/index.ts):
  - Удалён 'export * from @nstart/ui'
  - Explicit exports для всех 45+ нужных символов
  - SelectOption / MultiSelectOption / LanguageOption теперь {id, label}
    с optional 'value' alias — nstart-compat без массового codemod call-sites

styles.css cleanup:
  - Removed @import @nstart/ui/styles/{fonts,theme}.css
  - Removed @source nstart/dist directive (Tailwind v4 scan)
  - Removed --text-sm: 13px override (был костыль для @nstart/ui dist)
  - Legacy color tokens (--color-carbon/ultramarain/orbit/regolith/asteroid)
    sсодержали fallback mappings — оставлены, но callsites больше не используют

Codemod: 9 файлов конвертированы legacy colors → handoff tokens
  carbon → ink, ultramarain → accent, orbit → warn, regolith → line, asteroid → mute

main.tsx:
  - NStartUiProvider → TooltipProvider (Radix)
  - DatePickerProvider убран (наш DatePicker native, no provider needed)

routes/dictionaries.$name.tsx:
  - Removed useRef-based <input>.indeterminate hack
  - Radix Checkbox принимает checked='indeterminate' declaratively

package.json: -@nstart/ui (was 0.1.3)

Bundle impact:
  before: vendor-nstart-ui chunk = 1228 KB
  after:  no nstart chunk, всё в vendor-misc (+~70 KB Radix wrappers)
  net savings: ~1.15 MB minified, ~440 KB gzip

Tests: 116 pass (1 fixed: tab role from 'button' → 'tab', Radix semantically correct).
TS strict: clean.
Browser-verified (preview): 7 utilities computed correctly, nstart bundle absent.
2026-05-11 15:17:37 +03:00
Zimin A.N. f0dbc20f62 fix(fonts): explicit font-family on title utilities + cascade conflicts
Реальные измерения через DevTools на staging выявили 3 бага после MR !47:

Bug 1: text-title-md/lg/xl inherited Tektur от @nstart/ui Card parent.
  Handoff: 'Inter for all headings'. Computed: Tektur.
  Fix: добавил font-family: var(--font-sans) explicit на text-title-*
  и text-body/text-cell. После Stage 3.9 (nstart gone) можно убрать —
  body inherit будет достаточно.

Bug 2: text-cap + font-mono cohabit (catalog scope chips, time-travel
  presets) → Tailwind v4 cascade order: font-mono идёт после text-cap
  в bundled CSS → JetBrains Mono перебивает Tektur. text-cap baked
  font-display, font-mono token redundant.
  Fix: codemod strip font-mono где есть text-cap (4 sites). Computed
  font-family теперь правильно Tektur.

Bug 3: @nstart/ui PageHeader рендерил h1 'DICTIONARIES' в Tektur
  36px/700 uppercase (font-primary text-4xl). Handoff page title:
  17px/600 Inter normal case. Наш src/ui/components/page-header.tsx
  написан но не экспортирован через barrel — все routes резолвили
  PageHeader к nstart версии.
  Fix: добавил 'export { PageHeader }' override в src/ui/index.ts.
  Все 7 PageHeader usages (catalog/search/reviews/webhooks/outbox/
  my-drafts/graph) теперь получат handoff-conformant heading.

Bug 4 (small): catalog counter '37/37' использовал text-cap но это
  numeric value — handoff role text-mono (11px JetBrains Mono).
  Fix: text-cap → text-mono + tabular-nums для grid alignment.

Verified via injected test elements + CDP computed styles:
  text-title-xl: 22/600 Inter
  text-title-lg: 17/600 Inter
  text-title-md: 15/600 Inter
  text-body:     13/400 Inter
  text-cell:     12.5/500 Inter
  text-mono:     11/500 JetBrains Mono
  text-cap:      10.5/600 Tektur

Все 7 utilities точно matches handoff scale.

Files: 4 modified. TS strict + 116 tests pass.
2026-05-11 14:58:32 +03:00
Zimin A.N. 3223a5ced7 fix(version): channel-based label, no semver fallback for branch builds
Stale package.json создавал ложное впечатление rollback'а: staging
показывал "v1.2.0" хотя последний tag — v1.2.2 (package.json не bump'ился
со времён v1.2.0 release).

Vote A — branch builds НЕ используют package.json как version label.
Vite определяет channel:

  serverTag != none → что прод/staging RUNS
  clientTag         → что build'ил CI tag pipeline (CI_COMMIT_TAG set)
  CI без tag        → 'dev' literal (staging / branch CI build)
  локально          → 'local' literal (pnpm dev)

Detection через новый __APP_CI__ define из process.env.CI (GitLab сетает
CI=true). Local pnpm dev: CI undefined → channel='local'.

package.json.version всё ещё инжектится как __APP_VERSION__ и видна в
tooltip badge'а как 'floor vX.Y.Z' — не как primary label.

Files:
  - vite.config.ts: + APP_CI from process.env.CI
  - src/vite-env.d.ts: + __APP_CI__ declare
  - src/components/version/useAppVersion.ts: + clientCI
  - src/components/version/VersionBadge.tsx: channel logic вместо fallback

Verify all three modes:
  local         → 'local · {commit}'
  CI branch     → 'dev · {commit}'
  CI tag v1.3.1 → 'v1.3.1 · {commit}'

Tests: 116 pass.
2026-05-11 14:45:04 +03:00
Zimin A.N. 750395da09 feat(record): RecordDrawer slide-over per Stage 3.5 handoff (Phase A)
Заменяет Modal-based record create/edit на right slide-over drawer per
design_handoff_ordinis_mdm/README.md Screen 3.

What's в Phase A (drawer shell + sticky chrome):

- src/components/record/RecordDrawer.tsx (NEW)
  - Two widths: 520px default / 880px wide, toggle через ‹/› button.
    Persists в localStorage 'ord-drawer-wide'.
  - Sticky header: caption (cap style) + recordId mono + width-toggle +
    history button (edit mode) + ×.
  - Sticky footer: [Удалить] (danger, edit mode) · 'не сохранено · N полей'
    caption · [Отмена] · [Сохранить] (drives form submit через form attr).
  - Built на shadcn Sheet primitive + Radix Dialog. Override sheetVariants
    inline для transition-able max-width (variant size не toggle'ится
    smoothly без animate prop).
  - aria-labelledby wires DialogTitle к содержимому header.
  - Mobile <880px: w-full fullscreen (per handoff responsive table).

- src/components/form/SchemaDrivenForm.tsx
  - New optional props: formId, onDirtyChange.
  - Когда formId передан — внутренний FormActions footer прячется (drawer
    footer driving submit через <button form={formId} type=submit>).
  - Dirty-count считается из RHF formState.dirtyFields (top-level + data.*).
  - useEffect surfaces count к parent — drawer footer caption updates live.

- src/routes/dictionaries.$name.tsx
  - Modal create/edit -> RecordDrawer.
  - State: + recordDirtyCount (reset на close).
  - onDelete wires к existing close-confirm flow (kind: 'close-confirm').
  - onHistory wires к existing setHistoryKey RecordHistoryDrawer.

- src/i18n.ts
  - + form.delete, form.unsavedNFields_one/few/many/other (i18next plural rules).
  - + drawer.{collapse,expand,close,captionEdit,captionCreate}.

Phase B (TODO Stage 3.5b — separate MR):
- Section anchor chips (sticky strip)
- Wide-mode left ToC rail (180px)
- Sticky toolbar: search + 'только заполненные' toggle + counter
- Requires x-section schema metadata в backend properties (CEO open question #5 в handoff)

Tests: 8 files, 116 tests pass. TS strict clean.
2026-05-11 14:39:38 +03:00
Zimin A.N. b94912789f fix(fonts): semantic typography utilities per handoff (7 roles)
Заменяет blanket override из MR !45 на типизированную type scale per
design_handoff_ordinis_mdm/README.md "Scale" section.

Семь semantic @utility в styles.css:
  text-title-xl  22/600   — modal title, section header
  text-title-lg  17/600   — page title в editor
  text-title-md  15/600   — dictionary card title
  text-body      13/400   — workhorse: body, buttons, tabs, inputs
  text-cell      12.5/500 — table cell text
  text-mono      11/500   — Mono: IDs, dates, FK refs (font-mono baked in)
  text-cap       10.5/600 — Tektur UPPERCASE caption (font/uppercase baked in)

Audit phases:
  P1: добавил 7 утилит, font/uppercase/tracking baked где надо
  P2: 43 deterministic codemods (font-mono+text-2xs/[11px] → text-mono,
      [15/17/22]px+font-semibold → text-title-*, [12.5]px → text-cell,
      [10.5]px+uppercase+tracking → text-cap)
  P3: 64 text-sm → text-body (handoff workhorse), 84 text-2xs context-aware
      (TableCell → text-cell, bare → text-cell, плюс cleanup caps в backticks
      template literals который Phase 2 пропустил), 25 text-xs (6 → text-mono
      когда с font-mono, 19 → text-cell), 8 titles text-base/lg → text-title-*
  P4: убрал --text-2xs override (no users), оставил --text-sm: 13px scoped
      к @nstart/ui passthrough (см. comment в styles.css — убирается в Stage 3.9)

Stats:
  text-body: 69 | text-cell: 99 | text-mono: 50 | text-cap: 42
  text-title-xl: 5 | text-title-lg: 5 | text-title-md: 7
  text-sm/text-2xs/text-xs в src/: 0 (только в styles.css комментариях)

Поведение всех 277 typography usages теперь явно соответствует handoff —
каждое место осознанно выбрано под роль, не плажирующий override.
2026-05-11 14:31:35 +03:00
Zimin A.N. e62633d903 fix(version): read CI_COMMIT_TAG as source of truth, package.json as fallback
Previously: vite read package.json.version only, requiring manual bump
before each tag pipeline (manual sync ломалось — MR !44 пример костыля).

Now: vite reads CI_COMMIT_TAG (passed via Dockerfile ARG GIT_TAG from
.gitlab-ci.yml .docker_base) → strips 'v' prefix → exposes как __APP_TAG__.
'none' sentinel filtered → empty string → fallback к package.json.

VersionBadge priority chain:
  1. serverTag (что прод RUNS, через /api/v1/version)
  2. clientTag (что build'илось на tag pipeline)
  3. v${clientVersion} (package.json fallback floor для local / branch)

Dockerfile: add ARG GIT_TAG=none, ENV CI_COMMIT_TAG=$GIT_TAG (mirrors
существующий pattern с GIT_COMMIT → CI_COMMIT_SHORT_SHA).

Revert package.json 1.3.1 → 1.2.0 (manual bump из MR !44). Версия теперь
follows tag, не наоборот.
2026-05-11 14:19:50 +03:00
Zimin A.N. 71432e9ae7 fix(fonts): align all text sizes to handoff spec via Tailwind theme overrides
Глобальные правки через @theme в styles.css вместо 200+ точечных правок:

- text-sm: 14px -> 13px (handoff workhorse — body, buttons, tabs)
- text-2xs: undefined -> 11px (handoff IDs, mono meta — было невидимо)
- @utility text-cap: 10.5px Tektur uppercase 0.10em (handoff caps)

Codemod (30 sites): text-2xs + uppercase + tracking-[0.10em] -> text-cap.
Sidebar section labels consolidated с explicit text-[10.5px] на text-cap.

Покрывает 64 text-sm + 148 text-2xs автоматически. Table cells (12.5px)
и page/modal titles (17px/22px) остаются explicit text-[Npx] arbitrary
values по месту — нет slots в Tailwind scale.
2026-05-11 14:12:57 +03:00
Zimin A.N. 780de70fc5 fix(version): bump to 1.3.1 and show 8-char commit hash
VersionBadge показывал 1.2.0 (stale package.json) и truncate коммита на
7 символов. Пользователь ожидал v1.3.1 · 4c7fa8d7 (full 8-char short).

- package.json: 1.2.0 -> 1.3.1
- VersionBadge: substring(0, 7) -> substring(0, 8)
- vite.config: git rev-parse --short=8 для consistency с CI_COMMIT_SHORT_SHA
2026-05-11 14:09:03 +03:00
Zimin A.N. f72aac6398 fix(admin-ui): sidebar font sizes per handoff spec
- Section labels (WORKFLOW / АДМИНИСТРИРОВАНИЕ): text-2xs (12px) → 10.5px
  Tektur per handoff caption spec. "Администрирование" не вылазит за
  220px sidebar.
- Nav items: text-sm (14px) → text-[13px] handoff body workhorse size.
- Badge counters: text-2xs (12px) → text-[11px] (mono IDs spec).

Partial — full font audit pending (Stage 3.x font sweep). См. nsi-hot.md.
2026-05-11 13:56:38 +03:00
Zimin A.N. 0ddba871f7 feat(admin-ui): editor 6-tab layout (Stage 3.4)
Dictionary editor route теперь имеет TabBar (handoff design Stage 3.4):
  Records | Relations | Fields | JSON | Events | History

URL-synced через ?tab= (default: records). Backward-compat: старый
?view=hub auto-remap'нется в ?tab=relations при mount'е.

Tab contents:
- records (default): существующий records table + filters + edit modal
  flow — без изменений, обёрнут в conditional render
- relations: DictionaryHubView (был раньше при view=hub)
- fields: schema properties summary — name/type/required/unique/i18n/FK/desc
- json: pretty-printed schemaJson read-only
- events: link к /audit с pre-filled dict filter
- history: placeholder (schema version timeline — backend endpoint pending)

EditorTabBar — inline component с handoff styling (border-b border-line +
active accent border + font-semibold), -mb-px overlap для clean look.

Hub view's neighbor cards теперь search={tab:'relations'} вместо
view='hub' (consistent с new param).

DictionaryDependentsPanel рендерится только в Records tab — auto Hub view
в Relations tab уже включает dependents через свой layout.

Tests: 116/116 PASS, build green, TS clean.
2026-05-11 13:54:26 +03:00
Александр Зимин dc0002f379 fix(admin-ui): UpdateBanner generic message 2026-05-11 10:39:35 +00:00
Александр Зимин 4122aa1fc6 fix(admin-ui): UpdateBanner none-version + tokens 2026-05-11 10:29:45 +00:00
Александр Зимин 580529b977 fix(admin-ui): version display + global search submit 2026-05-11 10:18:22 +00:00
Александр Зимин 649b7c4244 feat(admin-ui): shadcn TextArea (Stage 3.8a) 2026-05-11 09:13:10 +00:00
Александр Зимин e2f1c3527b feat(admin-ui): sonner Toast + TimeTravel polish (Stage 3.6+3.7) 2026-05-11 09:11:21 +00:00
Александр Зимин 6db2a0c345 feat(admin-ui): catalog refresh + token migration (Stage 3.2) 2026-05-11 09:08:38 +00:00
Александр Зимин ec713f9704 feat(admin-ui): Graph view с d3-force (Stage 3.3) 2026-05-11 08:55:53 +00:00
Александр Зимин b2228c2a29 feat(admin-ui): Sidebar 220px + TopBar (Stage 3.1) 2026-05-11 08:51:37 +00:00
Александр Зимин b7d2fbd7ce feat(admin-ui): codemod @nstart/ui → @/ui (Stage 2.2) 2026-05-10 22:17:06 +00:00
Александр Зимин 5d12c89c4f feat(admin-ui): shadcn primitives + @/ui barrel (Stage 2.1) 2026-05-10 21:47:24 +00:00
Александр Зимин c57003cd73 fix(admin-ui): guest nav filter + suppress drafts 401 2026-05-10 21:19:07 +00:00
Александр Зимин 2902730b1e fix(admin-ui): RequireAuth не блокирует UI пока silent SSO loading 2026-05-10 21:15:14 +00:00
Александр Зимин 94da10f487 fix(admin-ui): TimeTravelPicker → visual SVG timeline 2026-05-10 19:50:17 +00:00
Александр Зимин 69f1a05a1f fix(admin-ui): v1.1.1 patch — version routing + guest mode + timeline UX 2026-05-10 19:34:47 +00:00
Александр Зимин 48dd8a487a fix(admin-ui): backlog sweep — 7 review items 2026-05-10 19:21:33 +00:00
Александр Зимин 0918820661 fix(admin-ui): Checkbox indeterminate stale state 2026-05-10 18:53:07 +00:00
Александр Зимин 1df232679d feat(admin-ui): Hub view (B) per design handoff — dict relations 3-col layout 2026-05-10 18:41:37 +00:00
Александр Зимин 5afc233a4b fix(admin-ui): compact catalog toolbar (single row) 2026-05-10 18:30:49 +00:00
Александр Зимин 49ab6c9340 feat(admin-ui): catalog FK chips на cards (← N использ.) 2026-05-10 18:26:30 +00:00
Александр Зимин 75e844eb98 feat(admin-ui): catalog list view per design handoff (D-list-view) 2026-05-10 18:05:21 +00:00
Александр Зимин 7e460ebff7 feat(admin-ui): Dictionary Catalog v2 (URL filters + sort + density) 2026-05-10 17:37:47 +00:00
Александр Зимин f5fb2b70a6 fix(admin-ui): dict detail toolbar — wrap buttons вместо ломки текста 2026-05-10 17:30:38 +00:00
Александр Зимин e8f98230f2 feat: version banner UI + GET /api/v1/version backend endpoint 2026-05-10 16:50:50 +00:00
Александр Зимин fc2fb35717 fix(admin-ui): top-5 critical review bugs (auth + AJV + TZ + idempotency + storage) 2026-05-10 16:22:35 +00:00
Александр Зимин 02167e29f6 chore(admin-ui): strip "НСИ ЦУОД" from logo 2026-05-10 15:20:56 +00:00
Zimin A.N. 361f3b431c feat(approval): bulk approve/reject в /reviews queue (Phase 4 polish)
Phase 4 last optional UX из design doc — bulk operations:

- Multi-select column в reviewer queue (Checkbox per row + select-all
  header). Set<draftId> selection state, persisted между refetch'ами.
- "Approve выбранные" — confirm dialog → parallel approveDraft mutations
  через Promise.all. Каждая mutation independent: если одна fail
  (409 self_approve_forbidden / draft_not_pending race), остальные
  продолжают.
- "Reject выбранные" — Modal с TextArea для shared reason (required,
  matches backend reject_reason_required validation). Reason одинаковый
  для всех — bulk обычно "duplicate registration" / "wrong scope" / etc.
- Bulk result Alert: approved count / rejected count / failed list с
  per-draft reason'ами (err code из 409). User видит точно что прошло
  и что нет.
- Auto-clear selection после bulk operation. Queue refetch'ит через
  refetchInterval=30s, плюс mutation onSuccess invalidate'ит ['drafts'].

i18n RU/EN: 22 keys (selectAll/selectRow/selectedCount/approveSelected/
rejectSelected/clear/confirmApprove/rejectModal*/confirmReject/cancel/
resultTitle/approvedCount/rejectedCount/failedCount). Pluralization
через i18next plurals.

Why ship without soak data:
- Implementation не зависит от queue depth — это просто loop'ит над
  выбранными drafts.
- Reviewer на любом dict с queue >5 уже выгадывает время.
- Если queue staying small после soak — feature не bothering нас (UI
  toolbar показывается только когда anySelected).

Tests: vitest 89/89, tsc clean, prod build clean.
2026-05-10 12:12:57 +03:00
Zimin A.N. aedeafc84e feat(approval): /my-drafts maker self-tracking view + /api/v1/drafts/me
Approval Workflow v2 Phase 4 — UX:

Backend:
- DraftController.myDrafts: GET /api/v1/drafts/me?page&size — все статусы
  (PENDING/APPROVED/REJECTED/WITHDRAWN) текущего authenticated user, sorted
  submitted_at DESC. Resolve maker_id из JWT subject (fallback "anonymous"
  если auth off).
- Reuses existing DraftService.listByMaker (Phase 1 method, был не exposed).

Frontend:
- routes/my-drafts.tsx: новый маршрут с табличным view.
  Колонки: businessKey / operation / status / submittedAt / reviewedAt /
  reviewerId / comment / actions. Status badge tinted (warning/success/
  error/neutral). PENDING строки имеют Withdraw кнопку (calls existing
  useWithdrawDraft с confirm).
- api/queries.ts: useMyDrafts (refetch 30s — maker увидит когда reviewer
  decide).
- api/mutations.ts: useWithdrawDraft теперь invalidate'ит ['drafts'] —
  /me + by-dict обе видят изменение.
- routes/__root.tsx: nav link "Мои черновики" / "My drafts" (после Reviews).
- i18n RU/EN: 18 keys (nav.myDrafts, myDrafts.* col/action/status).
  Pluralization для total через i18next plurals.

Why это нужно:
- До этого maker submit'нул draft и пропадал — единственный feedback был
  badge "На review" в records list (только для текущего dict). Здесь — все
  его submissions с history + withdraw для PENDING.
- Reviewer pool (D3=A) маленький, /reviews queue только для них. Maker
  без role'а не видит queue, но должен tracking своих submissions.

Tests: ordinis-rest-api unit 119/119, ordinis-app e2e 28/28, vitest 89/89.
2026-05-10 12:07:35 +03:00
Zimin A.N. b2e7fd84c9 feat(approval): Approval Workflow v2 Phase 4 monitoring + pending-review badges
Approval Workflow v2 — Phase 4: monitoring + UX visibility.

Backend metrics (Micrometer → Prometheus):
- Gauge nsi_draft_pending_total — global queue depth (polled на scrape).
- Counter nsi_draft_submit_total{dictionary,operation,outcome=ok|already_pending|error}
  — submit attempts с разбивкой по outcome. Phase 3 lessons: error outcome
  выявит schema bugs которые ранее маскировались.
- Counter nsi_draft_decision_total{dictionary,operation,outcome=approved|rejected|withdrawn}
  — counts decisions, разбит по типу.
- Timer nsi_draft_review_duration_seconds — time-to-decide histogram (от
  submit до approve/reject/withdraw). publishPercentileHistogram() — для
  Prometheus histogram_quantile p95/p99.

DraftService:
- Optional<MeterRegistry> — null-safe в test ctor'ах.
- @PostConstruct registerGauges() — Gauge.builder polled от
  RecordDraftRepository.countPending().
- recordDecision() helper в approve/reject/withdraw — записывает Timer +
  Counter одной точкой.
- 4-arg ctor compat для Phase 1 unit tests.

UI (admin):
- useDictPendingDrafts — per-dict pending list, refetch 30s, gated на
  detailQuery.data.approvalRequired (no-op для не-approval dicts).
- "На review" badge в businessKey cell records table — моментальная
  видимость что есть pending draft. Open /reviews для act'а.
- i18n RU/EN: dict.pendingReview.label + tooltip.

Domain:
- RecordDraftRepository.countPending() — для Gauge polled count.

Tests: ordinis-rest-api unit 119/119, ordinis-app e2e 28/28, vitest 89/89.
2026-05-08 15:11:25 +03:00