From 155c70e23cc0e38deb10953c176ff3835712f415 Mon Sep 17 00:00:00 2001 From: "Zimin A.N." Date: Mon, 11 May 2026 16:37:38 +0300 Subject: [PATCH] =?UTF-8?q?feat(drawer):=20all-sections=20layout=20=D1=81?= =?UTF-8?q?=20chips=20strip=20per=20handoff=20prototype?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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, не разломан). --- .../src/components/form/SchemaDrivenForm.tsx | 70 +++++++++++++++++-- .../src/routes/dictionaries.$name.tsx | 2 + 2 files changed, 68 insertions(+), 4 deletions(-) diff --git a/ordinis-admin-ui/src/components/form/SchemaDrivenForm.tsx b/ordinis-admin-ui/src/components/form/SchemaDrivenForm.tsx index d114e3d..214757d 100644 --- a/ordinis-admin-ui/src/components/form/SchemaDrivenForm.tsx +++ b/ordinis-admin-ui/src/components/form/SchemaDrivenForm.tsx @@ -54,6 +54,11 @@ type Props = { formId?: string /** Notifies parent of dirty-field count (for "N полей" footer caption). */ onDirtyChange?: (dirtyCount: number) => void + /** Layout mode (default 'tabs' для backward compat): + * - 'tabs': single bucket visible at once, tab bar для switch (legacy) + * - 'sections': all buckets vertically scrollable + section chips strip + * на верху для quick jump. Per handoff Screen 3 RecordDrawer design. */ + layout?: 'tabs' | 'sections' } const ajv = new Ajv({ allErrors: true, strict: false }) @@ -98,6 +103,7 @@ export const SchemaDrivenForm = ({ onCancel, formId, onDirtyChange, + layout = 'tabs', }: Props) => { const { t } = useTranslation() const [activeTab, setActiveTab] = useState('identity') @@ -188,15 +194,55 @@ export const SchemaDrivenForm = ({ onDirtyChange?.(dirtyCount) }, [dirtyCount, onDirtyChange]) + // Section visibility — в 'sections' layout все buckets рендерятся подряд, + // в 'tabs' — только active. Sections layout per handoff Screen 3 prototype. + const showAllSections = layout === 'sections' + const sectionVisible = (id: TabId) => showAllSections || activeTab === id + + // Scroll-to-section для chip click (только в sections mode). + const scrollToSection = (id: TabId) => { + document.getElementById(`form-section-${formId ?? 'default'}-${id}`)?.scrollIntoView({ + behavior: 'smooth', + block: 'start', + }) + } + return (
- setActiveTab(id as TabId)} /> + {/* Section chips strip per handoff (sections mode) OR tab bar (legacy). */} + {showAllSections ? ( +
+ {visibleTabs.map((tab) => ( + + ))} +
+ ) : ( + setActiveTab(id as TabId)} /> + )} -
+
+ {showAllSections && ( +

+ {t('form.tabs.identity')} +

+ )}
{!idSource && (
@@ -258,7 +304,15 @@ export const SchemaDrivenForm = ({
-
+
+ {showAllSections && buckets.description.length > 0 && ( +

+ {t('form.tabs.description')} +

+ )}
{buckets.description.map((key) => (
-
+
+ {showAllSections && buckets.extra.length > 0 && ( +

+ {t('form.tabs.extra')} +

+ )}
{buckets.extra.map((key) => ( setEdit({ kind: 'closed' })} formId="record-form" + layout="sections" onDirtyChange={setRecordDirtyCount} /> )} @@ -1031,6 +1032,7 @@ function DictionaryDetail() { onSubmit={handleSubmit} onCancel={() => setEdit({ kind: 'closed' })} formId="record-form" + layout="sections" onDirtyChange={setRecordDirtyCount} /> )}