fix: Vertical tabs visibility and add mobile horizontal tabs

Fixed two critical issues with VerticalTabForm:

Issue #1: All sections showing at once
- Problem: className override was removing original classes
- Fix: Preserve originalClassName and append 'hidden' when inactive
- Now only active section is visible
- Inactive sections get 'hidden' class added

Issue #2: No horizontal tabs on mobile
- Added mobile horizontal tabs (lg:hidden)
- Scrollable tab bar with overflow-x-auto
- Active tab highlighted with bg-primary
- Icons + labels for each tab
- Separate mobile content area

Changes to VerticalTabForm.tsx:
1. Fixed className merging logic
   - Get originalClassName from child.props
   - Active: use originalClassName as-is
   - Inactive: append ' hidden' to originalClassName
   - Prevents className override issue

2. Added mobile layout
   - Horizontal tabs at top (lg:hidden)
   - Flex with gap-2, overflow-x-auto
   - flex-shrink-0 prevents tab squishing
   - Active state: bg-primary text-primary-foreground
   - Inactive state: bg-muted text-muted-foreground

3. Desktop layout (hidden lg:flex)
   - Vertical sidebar (w-56)
   - Content area (flex-1)
   - Scroll spy for desktop only

4. Mobile content area (lg:hidden)
   - No scroll spy (simpler)
   - Direct tab switching
   - Same visibility logic (hidden class)

Result:
 Only active section visible (desktop + mobile)
 Mobile has horizontal tabs
 Desktop has vertical sidebar
 Proper responsive behavior
 Tab switching works correctly
This commit is contained in:
dwindown
2025-11-20 21:00:30 +07:00
parent c8bba9a91b
commit 7136b01be4

View File

@@ -66,20 +66,20 @@ export function VerticalTabForm({ tabs, children, className }: VerticalTabFormPr
}; };
return ( return (
<div className={cn('flex gap-6', className)}> <div className={cn('space-y-4', className)}>
{/* Vertical Tabs Sidebar */} {/* Mobile: Horizontal Tabs */}
<div className="hidden lg:block w-56 flex-shrink-0"> <div className="lg:hidden">
<div className="sticky top-4 space-y-1"> <div className="flex gap-2 overflow-x-auto pb-2">
{tabs.map((tab) => ( {tabs.map((tab) => (
<button <button
key={tab.id} key={tab.id}
onClick={() => scrollToSection(tab.id)} onClick={() => scrollToSection(tab.id)}
className={cn( className={cn(
'w-full text-left px-4 py-2.5 rounded-md text-sm font-medium transition-colors', 'flex-shrink-0 px-4 py-2 rounded-md text-sm font-medium transition-colors',
'flex items-center gap-3', 'flex items-center gap-2',
activeTab === tab.id activeTab === tab.id
? 'bg-primary text-primary-foreground' ? 'bg-primary text-primary-foreground'
: 'text-muted-foreground hover:bg-muted hover:text-foreground' : 'bg-muted text-muted-foreground'
)} )}
> >
{tab.icon && <span className="w-4 h-4">{tab.icon}</span>} {tab.icon && <span className="w-4 h-4">{tab.icon}</span>}
@@ -89,18 +89,59 @@ export function VerticalTabForm({ tabs, children, className }: VerticalTabFormPr
</div> </div>
</div> </div>
{/* Content Area */} {/* Desktop: Vertical Layout */}
<div <div className="hidden lg:flex gap-6">
ref={contentRef} {/* Vertical Tabs Sidebar */}
className="flex-1 overflow-y-auto pr-2" <div className="w-56 flex-shrink-0">
> <div className="sticky top-4 space-y-1">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => scrollToSection(tab.id)}
className={cn(
'w-full text-left px-4 py-2.5 rounded-md text-sm font-medium transition-colors',
'flex items-center gap-3',
activeTab === tab.id
? 'bg-primary text-primary-foreground'
: 'text-muted-foreground hover:bg-muted hover:text-foreground'
)}
>
{tab.icon && <span className="w-4 h-4">{tab.icon}</span>}
{tab.label}
</button>
))}
</div>
</div>
{/* Content Area - Desktop */}
<div
ref={contentRef}
className="flex-1 overflow-y-auto pr-2"
>
{React.Children.map(children, (child) => {
if (React.isValidElement(child) && child.props['data-section-id']) {
const sectionId = child.props['data-section-id'];
const isActive = sectionId === activeTab;
const originalClassName = child.props.className || '';
return React.cloneElement(child as React.ReactElement<any>, {
ref: (el: HTMLElement) => registerSection(sectionId, el),
className: isActive ? originalClassName : `${originalClassName} hidden`.trim(),
});
}
return child;
})}
</div>
</div>
{/* Mobile: Content Area */}
<div className="lg:hidden">
{React.Children.map(children, (child) => { {React.Children.map(children, (child) => {
if (React.isValidElement(child) && child.props['data-section-id']) { if (React.isValidElement(child) && child.props['data-section-id']) {
const sectionId = child.props['data-section-id']; const sectionId = child.props['data-section-id'];
const isActive = sectionId === activeTab; const isActive = sectionId === activeTab;
const originalClassName = child.props.className || '';
return React.cloneElement(child as React.ReactElement<any>, { return React.cloneElement(child as React.ReactElement<any>, {
ref: (el: HTMLElement) => registerSection(sectionId, el), className: isActive ? originalClassName : `${originalClassName} hidden`.trim(),
className: isActive ? '' : 'hidden',
}); });
} }
return child; return child;