Merge branch 'feat/drawer-sections-layout' into 'main'
feat(drawer): all-sections layout + chips strip per handoff See merge request 2-6/2-6-4/terravault/ordinis!64
This commit is contained in:
@@ -54,6 +54,11 @@ type Props = {
|
|||||||
formId?: string
|
formId?: string
|
||||||
/** Notifies parent of dirty-field count (for "N полей" footer caption). */
|
/** Notifies parent of dirty-field count (for "N полей" footer caption). */
|
||||||
onDirtyChange?: (dirtyCount: number) => void
|
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 })
|
const ajv = new Ajv({ allErrors: true, strict: false })
|
||||||
@@ -98,6 +103,7 @@ export const SchemaDrivenForm = ({
|
|||||||
onCancel,
|
onCancel,
|
||||||
formId,
|
formId,
|
||||||
onDirtyChange,
|
onDirtyChange,
|
||||||
|
layout = 'tabs',
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [activeTab, setActiveTab] = useState<TabId>('identity')
|
const [activeTab, setActiveTab] = useState<TabId>('identity')
|
||||||
@@ -188,15 +194,55 @@ export const SchemaDrivenForm = ({
|
|||||||
onDirtyChange?.(dirtyCount)
|
onDirtyChange?.(dirtyCount)
|
||||||
}, [dirtyCount, onDirtyChange])
|
}, [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 (
|
return (
|
||||||
<form
|
<form
|
||||||
id={formId}
|
id={formId}
|
||||||
onSubmit={handleSubmit(submit)}
|
onSubmit={handleSubmit(submit)}
|
||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
>
|
>
|
||||||
<Tabs items={visibleTabs} value={activeTab} onValueChange={(id) => setActiveTab(id as TabId)} />
|
{/* Section chips strip per handoff (sections mode) OR tab bar (legacy). */}
|
||||||
|
{showAllSections ? (
|
||||||
|
<div className="flex flex-wrap items-center gap-1.5 sticky top-0 bg-surface pb-2 -mt-1 z-10">
|
||||||
|
{visibleTabs.map((tab) => (
|
||||||
|
<button
|
||||||
|
key={tab.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => scrollToSection(tab.id as TabId)}
|
||||||
|
className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-sm bg-surface-2 border border-line hover:bg-accent-bg hover:border-accent text-cap text-ink-2 hover:text-accent transition-colors"
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
<span className="text-mono text-mute">
|
||||||
|
{buckets[tab.id as TabId].length}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Tabs items={visibleTabs} value={activeTab} onValueChange={(id) => setActiveTab(id as TabId)} />
|
||||||
|
)}
|
||||||
|
|
||||||
<div className={activeTab === 'identity' ? 'block' : 'hidden'}>
|
<div
|
||||||
|
id={`form-section-${formId ?? 'default'}-identity`}
|
||||||
|
className={sectionVisible('identity') ? 'block scroll-mt-12' : 'hidden'}
|
||||||
|
>
|
||||||
|
{showAllSections && (
|
||||||
|
<h3 className="text-cap text-mute mb-2 sticky top-10 bg-surface py-1 z-[5]">
|
||||||
|
{t('form.tabs.identity')}
|
||||||
|
</h3>
|
||||||
|
)}
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||||
{!idSource && (
|
{!idSource && (
|
||||||
<div className="sm:col-span-2">
|
<div className="sm:col-span-2">
|
||||||
@@ -258,7 +304,15 @@ export const SchemaDrivenForm = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={activeTab === 'description' ? 'block' : 'hidden'}>
|
<div
|
||||||
|
id={`form-section-${formId ?? 'default'}-description`}
|
||||||
|
className={sectionVisible('description') ? 'block scroll-mt-12' : 'hidden'}
|
||||||
|
>
|
||||||
|
{showAllSections && buckets.description.length > 0 && (
|
||||||
|
<h3 className="text-cap text-mute mb-2 sticky top-10 bg-surface py-1 z-[5]">
|
||||||
|
{t('form.tabs.description')}
|
||||||
|
</h3>
|
||||||
|
)}
|
||||||
<div className="grid grid-cols-1 gap-3">
|
<div className="grid grid-cols-1 gap-3">
|
||||||
{buckets.description.map((key) => (
|
{buckets.description.map((key) => (
|
||||||
<FieldRenderer
|
<FieldRenderer
|
||||||
@@ -276,7 +330,15 @@ export const SchemaDrivenForm = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={activeTab === 'extra' ? 'block' : 'hidden'}>
|
<div
|
||||||
|
id={`form-section-${formId ?? 'default'}-extra`}
|
||||||
|
className={sectionVisible('extra') ? 'block scroll-mt-12' : 'hidden'}
|
||||||
|
>
|
||||||
|
{showAllSections && buckets.extra.length > 0 && (
|
||||||
|
<h3 className="text-cap text-mute mb-2 sticky top-10 bg-surface py-1 z-[5]">
|
||||||
|
{t('form.tabs.extra')}
|
||||||
|
</h3>
|
||||||
|
)}
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||||
{buckets.extra.map((key) => (
|
{buckets.extra.map((key) => (
|
||||||
<FieldRenderer
|
<FieldRenderer
|
||||||
|
|||||||
@@ -995,6 +995,7 @@ function DictionaryDetail() {
|
|||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
onCancel={() => setEdit({ kind: 'closed' })}
|
onCancel={() => setEdit({ kind: 'closed' })}
|
||||||
formId="record-form"
|
formId="record-form"
|
||||||
|
layout="sections"
|
||||||
onDirtyChange={setRecordDirtyCount}
|
onDirtyChange={setRecordDirtyCount}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -1031,6 +1032,7 @@ function DictionaryDetail() {
|
|||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
onCancel={() => setEdit({ kind: 'closed' })}
|
onCancel={() => setEdit({ kind: 'closed' })}
|
||||||
formId="record-form"
|
formId="record-form"
|
||||||
|
layout="sections"
|
||||||
onDirtyChange={setRecordDirtyCount}
|
onDirtyChange={setRecordDirtyCount}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user