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.
This commit is contained in:
Zimin A.N.
2026-05-11 16:33:40 +03:00
parent f43ce5563a
commit a57cf520a4
2 changed files with 184 additions and 8 deletions
@@ -42,6 +42,7 @@ import { CascadeConfirmDialog } from '@/components/lineage/CascadeConfirmDialog'
import { RecordHistoryDrawer } from '@/components/record/RecordHistoryDrawer'
import { RecordDrawer } from '@/components/record/RecordDrawer'
import { WorkflowBanner } from '@/components/editor/WorkflowBanner'
import { EditorInfoPanel } from '@/components/editor/EditorInfoPanel'
import { AoiPickerDialog, type AoiResult } from '@/components/geo/AoiPickerDialog'
import { TimeTravelPicker } from '@/components/timetravel/TimeTravelPicker'
import { nowIsoLocal } from '@/lib/dates'
@@ -621,15 +622,23 @@ function DictionaryDetail() {
}
/>
{/* WorkflowBanner — status (live/review/draft) above tab bar per handoff Screen 2. */}
{detailQuery.data && (
<WorkflowBanner detail={detailQuery.data} pendingCount={pendingByKey.size} />
)}
{/* 3-col layout per handoff prototype design/compact.html:
[Sidebar (220px global)] [InfoPanel 220px] [main content]
Info panel — sticky слева с dict metadata + relations.
На <lg viewport schedule переключается в stacked (info above content). */}
<div className="grid grid-cols-1 lg:grid-cols-[220px_1fr] gap-4 lg:gap-6 mt-2">
{detailQuery.data && (
<EditorInfoPanel detail={detailQuery.data} recordCount={totalRecords} />
)}
{/* Editor tabs per handoff Stage 3.4:
Records | Relations | Fields | JSON | Events | History.
URL-synced через ?tab=. Default — records (records table + filters). */}
{detailQuery.data && (
<div className="min-w-0 space-y-4">
{/* WorkflowBanner — status (live/review/draft) above tab bar per handoff Screen 2. */}
{detailQuery.data && (
<WorkflowBanner detail={detailQuery.data} pendingCount={pendingByKey.size} />
)}
{/* Editor tabs per handoff Stage 3.4 */}
{detailQuery.data && (
<EditorTabBar
active={urlSearch.tab ?? 'records'}
onChange={(tab) =>
@@ -945,6 +954,9 @@ function DictionaryDetail() {
</>
)}
{/* === end records tab content === */}
</div>
</div>
{/* === end 3-col grid === */}
<RecordDrawer
open={edit.kind === 'create' || edit.kind === 'edit'}