feat(ST-1116): mobile auto-switch tabs + back button in BacklogSplitPane

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Janpeter Visser 2026-04-30 17:53:38 +02:00
parent 6052aa81fb
commit 3e86a8d5c9
5 changed files with 218 additions and 5 deletions

View file

@ -0,0 +1,34 @@
'use client'
import { useState } from 'react'
import { useSelectionStore } from '@/stores/selection-store'
import { SplitPane, type SplitPaneProps } from '@/components/split-pane/split-pane'
type Props = Omit<SplitPaneProps, 'activeTab' | 'onActiveTabChange'>
export function BacklogSplitPane(props: Props) {
const { selectedPbiId, selectedStoryId } = useSelectionStore()
const [activeTab, setActiveTab] = useState(0)
// React-recommended "derived state from props" pattern: update state during render
// instead of useEffect to avoid cascading renders.
const [prevPbiId, setPrevPbiId] = useState(selectedPbiId)
const [prevStoryId, setPrevStoryId] = useState(selectedStoryId)
if (selectedStoryId !== prevStoryId) {
setPrevStoryId(selectedStoryId)
if (selectedStoryId) setActiveTab(2)
}
if (selectedPbiId !== prevPbiId) {
setPrevPbiId(selectedPbiId)
if (selectedPbiId && !selectedStoryId) setActiveTab(1)
}
return (
<SplitPane
{...props}
activeTab={activeTab}
onActiveTabChange={setActiveTab}
/>
)
}

View file

@ -39,6 +39,8 @@ export interface SplitPaneProps {
tabLabels?: string[] // mobile tab labels, defaults to "Pane N"
minSize?: number // minimum px per pane, default 200
mobileBreakpoint?: number // default 1024
activeTab?: number // controlled: parent manages which tab is active
onActiveTabChange?: (index: number) => void
}
export function SplitPane({
@ -48,7 +50,10 @@ export function SplitPane({
tabLabels,
minSize = 200,
mobileBreakpoint = 1024,
activeTab: activeTabProp,
onActiveTabChange,
}: SplitPaneProps) {
const isControlled = activeTabProp !== undefined
const n = panes.length
const containerRef = useRef<HTMLDivElement>(null)
const splitsRef = useRef<number[]>(defaultSplit)
@ -58,7 +63,13 @@ export function SplitPane({
})
const [dragging, setDragging] = useState<number | null>(null) // divider index (0..n-2)
const [isMobile, setIsMobile] = useState(false)
const [activeTab, setActiveTab] = useState(0)
const [internalTab, setInternalTab] = useState(0)
const activeTab = isControlled ? activeTabProp : internalTab
const handleTabChange = (i: number) => {
if (!isControlled) setInternalTab(i)
onActiveTabChange?.(i)
}
useEffect(() => { splitsRef.current = splits }, [splits])
@ -113,11 +124,20 @@ export function SplitPane({
if (isMobile) {
return (
<div className="flex flex-col h-full">
<div className="flex border-b border-border shrink-0">
<div className="flex items-center border-b border-border shrink-0">
{activeTab > 0 && (
<button
onClick={() => handleTabChange(activeTab - 1)}
className="px-3 py-2 text-sm text-muted-foreground hover:text-foreground transition-colors shrink-0"
aria-label="Terug"
>
</button>
)}
{panes.map((_, i) => (
<button
key={i}
onClick={() => setActiveTab(i)}
onClick={() => handleTabChange(i)}
className={cn(
'flex-1 py-2 text-sm font-medium transition-colors',
activeTab === i