refactor(client): the lists tab moves out of the planner page

ListsContainer, the packing and to-do subtabs of the lists tab, moves
unchanged to pages/tripPlanner/ListsContainer.tsx, taking along the
imports only it used. The page renders it exactly as before.
This commit is contained in:
Maurice
2026-10-08 01:07:16 +00:00
parent 3b9e5606ce
commit 7d8dcb02c8
3 changed files with 279 additions and 149 deletions
+4 -149
View File
@@ -2,7 +2,6 @@ import React, { useState, Suspense } from 'react'
import { createPortal } from 'react-dom'
import { useParams, useNavigate, useSearchParams } from 'react-router'
import { useTripStore } from '../store/tripStore'
import { useCanDo } from '../store/permissionsStore'
import { useSettingsStore } from '../store/settingsStore'
import { MapViewAuto as MapView } from '../components/Map/MapViewAuto'
import { MapCompassPill, type CompassMap } from '../components/Map/MapCompassPill'
@@ -28,8 +27,6 @@ import { BookingDetailPopup } from '../components/Planner/bookings/BookingDetail
import BookingImportModal from '../components/Planner/BookingImportModal'
import AirTrailImportModal from '../components/Planner/AirTrailImportModal'
// MemoriesPanel moved to Journey addon
import ApplyTemplateButton from '../components/Packing/ApplyTemplateButton'
import PackingExportMenu from '../components/Packing/PackingExportMenu'
import type { ExpensePrefill } from '../components/Budget/CostsPanel'
import { expenseEditorFor } from '../components/Budget/CostsPanel.helpers'
import type { BookingExpenseRequest } from '../components/Planner/BookingCostsSection.types'
@@ -42,8 +39,7 @@ import Navbar from '../components/Layout/Navbar'
import HelpAnchor from '../components/Help/HelpAnchor'
import { getHelpContext } from '../help/registry'
import { useToast } from '../components/shared/Toast'
import { Map, X, PanelLeftClose, PanelLeftOpen, PanelRightClose, PanelRightOpen, Ticket, PackageCheck, Wallet, FolderOpen, Users, Train } from 'lucide-react'
import { useTranslation } from '../i18n'
import { Map, X, PanelLeftClose, PanelLeftOpen, PanelRightClose, PanelRightOpen, Ticket, Wallet, FolderOpen, Users, Train } from 'lucide-react'
import { addonsApi, accommodationsApi, authApi, tripsApi, assignmentsApi, mapsApi } from '../api/client'
import { accommodationRepo } from '../repo/accommodationRepo'
import { useAuthStore } from '../store/authStore'
@@ -53,161 +49,20 @@ import { useTripWebSocket } from '../hooks/useTripWebSocket'
import { useRouteCalculation } from '../hooks/useRouteCalculation'
import { usePlaceSelection } from '../hooks/usePlaceSelection'
import { usePlannerHistory } from '../hooks/usePlannerHistory'
import type { Accommodation, TripMember, Day, Place, Reservation, PackingItem, TodoItem } from '../types'
import { ListTodo, ListPlus, Download, Plus, FolderPlus } from 'lucide-react'
import type { Accommodation, TripMember, Day, Place, Reservation } from '../types'
import { useTripPlannerPage } from './tripPlanner/useTripPlannerPage'
import {
ReservationsPanel, PackingListPanel, TodoListPanel, FileManager, CostsPanel, ExpenseModal, CollabPanel,
ReservationsPanel, FileManager, CostsPanel, ExpenseModal, CollabPanel,
RoadtripSidebar, RoadtripCorridorPanel, RoadtripLimitsCard, RoadtripStopPopup, RoadtripStayModal, RoadtripTrackModal, RoadtripAlternativesBar,
TourPlannerRail, TourPlannerToursRail, TransportModal,
} from './tripPlanner/plannerLazy'
import { LazyPanel } from './tripPlanner/LazyPanel'
import { ListsContainer } from './tripPlanner/ListsContainer'
import { useMergedMapPois } from '../components/Map/useMergedMapPois'
import PoiCategoryPill from '../components/Map/PoiCategoryPill'
import { useTouchDragBridge } from '../hooks/useTouchDragBridge'
import PanelResizeHandle from '../components/Planner/PanelResizeHandle'
function ListsContainer({ tripId, packingItems, todoItems }: { tripId: number; packingItems: PackingItem[]; todoItems: TodoItem[] }) {
const [subTab, setSubTab] = useState<'packing' | 'todo'>(() => {
return (sessionStorage.getItem(`trip-lists-subtab-${tripId}`) as 'packing' | 'todo') || 'packing'
})
const setSubTabPersist = (tab: 'packing' | 'todo') => { setSubTab(tab); sessionStorage.setItem(`trip-lists-subtab-${tripId}`, tab) }
const [importPackingSignal, setImportPackingSignal] = useState(0)
const [addCategorySignal, setAddCategorySignal] = useState(0)
const [saveTemplateSignal, setSaveTemplateSignal] = useState(0)
const [addTodoSignal, setAddTodoSignal] = useState(0)
const [packingView, setPackingView] = useState<'common' | 'personal'>('common')
const { t } = useTranslation()
const isAdmin = useAuthStore(s => s.user?.role === 'admin')
const trip = useTripStore(s => s.trip)
const canEditPacking = useCanDo()('packing_edit', trip)
const tabs = [
{ id: 'packing' as const, label: t('todo.subtab.packing'), icon: PackageCheck, count: packingItems.length },
{ id: 'todo' as const, label: t('todo.subtab.todo'), icon: ListTodo, count: todoItems.length },
]
// The to-do view fills what is left under the bar, so its list and detail pane
// scroll inside the screen and the pane's buttons stay in sight.
const fill = subTab === 'todo'
return (
<div style={fill ? { display: 'flex', flexDirection: 'column', height: '100%' } : undefined}>
<div style={{ padding: '24px 28px 0', flexShrink: 0 }} className="max-md:!px-4 max-md:!pt-4">
<div className="bg-surface-tertiary" style={{
borderRadius: 18,
paddingBlock: 14, paddingInlineEnd: 16, paddingInlineStart: 22,
display: 'flex', alignItems: 'center', gap: 16, flexWrap: 'wrap',
}}>
<h2 className="text-content" style={{ margin: 0, fontSize: 'calc(18px * var(--fs-scale-subtitle, 1))', fontWeight: 600, letterSpacing: '-0.01em', flexShrink: 0 }}>
{t('trip.tabs.lists')}
</h2>
<div className="hidden md:block bg-edge-faint" style={{ width: 1, height: 22, flexShrink: 0 }} />
<div style={{ display: 'inline-flex', gap: 4, flexWrap: 'wrap', flex: 1, minWidth: 0 }}>
{tabs.map(tab => {
const active = subTab === tab.id
const Icon = tab.icon
return (
<button type="button" key={tab.id} onClick={() => setSubTabPersist(tab.id)}
className={active ? 'bg-surface-card text-content' : 'bg-transparent text-content-muted'}
style={{
appearance: 'none', border: 'none', cursor: 'pointer', fontFamily: 'inherit',
display: 'inline-flex', alignItems: 'center', gap: 6,
padding: '6px 12px', borderRadius: 99, fontSize: 'calc(13px * var(--fs-scale-body, 1))', whiteSpace: 'nowrap',
fontWeight: active ? 500 : 400,
boxShadow: active ? '0 1px 2px rgba(0,0,0,0.06)' : 'none',
transition: 'background 180ms cubic-bezier(0.23,1,0.32,1), color 180ms cubic-bezier(0.23,1,0.32,1), box-shadow 180ms cubic-bezier(0.23,1,0.32,1)',
}}
>
<Icon size={13} className={active ? 'text-content' : 'text-content-faint'} />
<span className="hidden sm:inline">{tab.label}</span>
<span className={`text-content-faint ${active ? 'bg-surface-tertiary' : 'bg-[rgba(0,0,0,0.06)]'}`} style={{
fontSize: 'calc(10px * var(--fs-scale-caption, 1))', fontWeight: 600,
padding: '1px 6px', borderRadius: 99, minWidth: 16, textAlign: 'center',
}}>{tab.count}</span>
</button>
)
})}
</div>
{subTab === 'packing' && (() => {
const sharedBtnClass = 'inline-flex items-center gap-1.5 px-2.5 sm:px-[14px] py-[7px] sm:py-[9px] hover:opacity-[0.88]'
// Export and Import carry only their icon, with the name as tooltip and label.
const iconBtnClass = 'inline-flex items-center justify-center px-2.5 py-[7px] sm:py-[9px] hover:opacity-[0.88] bg-accent text-accent-text'
const sharedBtnStyle: React.CSSProperties = {
appearance: 'none', border: 'none', cursor: 'pointer', fontFamily: 'inherit',
borderRadius: 10, fontSize: 'calc(13px * var(--fs-scale-body, 1))', fontWeight: 500,
}
return (
<div style={{ display: 'flex', gap: 6, flexShrink: 0, marginInlineStart: 'auto', flexWrap: 'wrap' }}>
{canEditPacking && (
<button type="button" onClick={() => setAddCategorySignal(s => s + 1)}
className={`${sharedBtnClass} bg-accent text-accent-text`}
style={sharedBtnStyle}
>
<ListPlus size={14} strokeWidth={2.5} />
<span className="hidden sm:inline">{t('packing.addCategory')}</span>
</button>
)}
<ApplyTemplateButton
tripId={tripId}
visibility={packingView}
className={`${sharedBtnClass} bg-accent text-accent-text`}
style={sharedBtnStyle}
/>
{isAdmin && packingItems.length > 0 && (
<button type="button" onClick={() => setSaveTemplateSignal(s => s + 1)}
className={`${sharedBtnClass} bg-accent text-accent-text`}
style={sharedBtnStyle}
>
<FolderPlus size={14} strokeWidth={2.5} />
<span className="hidden sm:inline">{t('packing.saveAsTemplate')}</span>
</button>
)}
<PackingExportMenu tripId={tripId} view={packingView} className={iconBtnClass} style={sharedBtnStyle} />
<button type="button" onClick={() => setImportPackingSignal(s => s + 1)}
className={iconBtnClass}
style={sharedBtnStyle}
aria-label={t('packing.import')}
title={t('packing.import')}
>
<Download size={14} strokeWidth={2.5} />
</button>
</div>
)
})()}
{subTab === 'todo' && (
<button type="button" onClick={() => setAddTodoSignal(s => s + 1)}
className="hover:opacity-[0.88] bg-accent text-accent-text"
style={{
appearance: 'none', border: 'none', cursor: 'pointer', fontFamily: 'inherit',
display: 'inline-flex', alignItems: 'center', gap: 6,
padding: '9px 14px', borderRadius: 10, fontSize: 'calc(13px * var(--fs-scale-body, 1))', fontWeight: 500,
flexShrink: 0,
marginInlineStart: 'auto',
}}
>
<Plus size={14} strokeWidth={2.5} />
<span className="hidden sm:inline">{t('todo.addItem')}</span>
</button>
)}
</div>
</div>
<div style={fill ? { padding: '16px 28px 16px', flex: 1, minHeight: 0 } : { padding: '16px 28px 0' }} className="max-md:!px-4">
{subTab === 'packing' && (
<LazyPanel id="packing">
<PackingListPanel tripId={tripId} items={packingItems} openImportSignal={importPackingSignal} addCategorySignal={addCategorySignal} saveTemplateSignal={saveTemplateSignal} inlineHeader={false} view={packingView} onViewChange={setPackingView} />
</LazyPanel>
)}
{subTab === 'todo' && (
<LazyPanel id="todo">
<TodoListPanel tripId={tripId} items={todoItems} addItemSignal={addTodoSignal} />
</LazyPanel>
)}
</div>
</div>
)
}
/** The tab ids are historical; the help screens carry the names the tabs show. */
const TRIP_TAB_HELP: Record<string, string> = {
transports: 'transports', buchungen: 'bookings', listen: 'lists', finanzplan: 'costs', dateien: 'files', collab: 'collab', roadtrip: 'roadtrip',
@@ -0,0 +1,123 @@
// FE-PAGE-PLANNER-LISTS-001 to FE-PAGE-PLANNER-LISTS-006
import { render, screen, fireEvent, act } from '../../../tests/helpers/render'
import { resetAllStores, seedStore } from '../../../tests/helpers/store'
import { buildUser, buildTrip, buildPackingItem, buildTodoItem } from '../../../tests/helpers/factories'
import { useAuthStore } from '../../store/authStore'
import { useTripStore } from '../../store/tripStore'
import { usePermissionsStore, type PermissionLevel } from '../../store/permissionsStore'
import { ListsContainer } from './ListsContainer'
// The panels and header tools are stubs that record what the container hands them.
const captured = vi.hoisted(() => ({} as Record<string, Record<string, unknown>>))
const stub = vi.hoisted(() => (name: string) => (props: Record<string, unknown>) => {
captured[name] = props
return null
})
vi.mock('../../components/Packing/PackingListPanel', () => ({ default: stub('packingPanel') }))
vi.mock('../../components/Todo/TodoListPanel', () => ({ default: stub('todoPanel') }))
vi.mock('../../components/Packing/ApplyTemplateButton', () => ({ default: stub('applyTemplate') }))
vi.mock('../../components/Packing/PackingExportMenu', () => ({ default: stub('exportMenu') }))
/** The props a lazily loaded panel was last rendered with, once its chunk is in. */
function loaded<T = Record<string, unknown>>(name: string): Promise<T> {
return vi.waitFor(() => {
if (!captured[name]) throw new Error(`${name} has not rendered yet`)
return captured[name] as T
})
}
const TRIP_ID = 31
function seed({ role = 'user', packingPermission }: { role?: 'user' | 'admin'; packingPermission?: PermissionLevel } = {}) {
seedStore(useAuthStore, { user: buildUser({ id: 9, role }) })
// A member, not the owner: owner-only rights then really are withheld.
seedStore(useTripStore, { trip: buildTrip({ id: TRIP_ID, user_id: 1 }) })
seedStore(usePermissionsStore, { permissions: packingPermission ? { packing_edit: packingPermission } : {} })
}
function renderLists(packing = [buildPackingItem(), buildPackingItem()], todos = [buildTodoItem()]) {
return render(<ListsContainer tripId={TRIP_ID} packingItems={packing} todoItems={todos} />)
}
beforeEach(() => {
resetAllStores()
sessionStorage.clear()
for (const key of Object.keys(captured)) delete captured[key]
})
describe('ListsContainer', () => {
it('FE-PAGE-PLANNER-LISTS-001: opens on the packing list and counts both lists', async () => {
seed()
renderLists()
expect(screen.getByRole('heading', { name: 'Lists' })).toBeInTheDocument()
expect(screen.getByRole('button', { name: /Packing List\s*2/ })).toBeInTheDocument()
expect(screen.getByRole('button', { name: /To-Do\s*1/ })).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'Add list' })).toBeInTheDocument()
expect(await loaded('packingPanel')).toMatchObject({ tripId: TRIP_ID, inlineHeader: false, view: 'common' })
expect(captured.todoPanel).toBeUndefined()
})
it('FE-PAGE-PLANNER-LISTS-002: the chosen subtab is remembered per trip for the session', async () => {
seed()
sessionStorage.setItem('trip-lists-subtab-99', 'todo')
renderLists()
// Another trip's choice does not carry over.
expect(screen.queryByRole('button', { name: 'Add new task' })).toBeNull()
fireEvent.click(screen.getByRole('button', { name: /To-Do/ }))
expect(sessionStorage.getItem(`trip-lists-subtab-${TRIP_ID}`)).toBe('todo')
expect(await loaded('todoPanel')).toMatchObject({ tripId: TRIP_ID, addItemSignal: 0 })
expect(screen.queryByRole('button', { name: 'Add list' })).toBeNull()
fireEvent.click(screen.getByRole('button', { name: /Packing List/ }))
expect(sessionStorage.getItem(`trip-lists-subtab-${TRIP_ID}`)).toBe('packing')
})
it('FE-PAGE-PLANNER-LISTS-003: each press of Add new task raises the to-do signal once more', async () => {
seed()
sessionStorage.setItem(`trip-lists-subtab-${TRIP_ID}`, 'todo')
renderLists()
const add = screen.getByRole('button', { name: 'Add new task' })
fireEvent.click(add)
fireEvent.click(add)
expect(await loaded('todoPanel')).toMatchObject({ addItemSignal: 2 })
})
it('FE-PAGE-PLANNER-LISTS-004: without the packing right there is no Add list, the other tools stay', async () => {
seed({ packingPermission: 'trip_owner' })
renderLists()
expect(screen.queryByRole('button', { name: 'Add list' })).toBeNull()
expect(screen.getByRole('button', { name: 'Import' })).toBeInTheDocument()
await loaded('packingPanel')
fireEvent.click(screen.getByRole('button', { name: 'Import' }))
expect(captured.packingPanel).toMatchObject({ openImportSignal: 1, addCategorySignal: 0 })
})
it('FE-PAGE-PLANNER-LISTS-005: an admin saves a template only when the list has items', async () => {
seed({ role: 'admin' })
const { unmount } = renderLists([])
expect(screen.queryByRole('button', { name: 'Save as template' })).toBeNull()
unmount()
renderLists()
fireEvent.click(screen.getByRole('button', { name: 'Save as template' }))
fireEvent.click(screen.getByRole('button', { name: 'Add list' }))
expect(await loaded('packingPanel')).toMatchObject({ saveTemplateSignal: 1, addCategorySignal: 1 })
})
it('FE-PAGE-PLANNER-LISTS-006: the panel switches the view the template and export tools work on', async () => {
seed()
renderLists()
const panel = await loaded<{ onViewChange: (v: string) => void }>('packingPanel')
expect(captured.applyTemplate).toMatchObject({ tripId: TRIP_ID, visibility: 'common' })
expect(captured.exportMenu).toMatchObject({ tripId: TRIP_ID, view: 'common' })
act(() => { panel.onViewChange('personal') })
expect(captured.applyTemplate).toMatchObject({ visibility: 'personal' })
expect(captured.exportMenu).toMatchObject({ view: 'personal' })
expect(captured.packingPanel).toMatchObject({ view: 'personal' })
})
})
@@ -0,0 +1,152 @@
import React, { useState } from 'react'
import { PackageCheck, ListTodo, ListPlus, Download, Plus, FolderPlus } from 'lucide-react'
import ApplyTemplateButton from '../../components/Packing/ApplyTemplateButton'
import PackingExportMenu from '../../components/Packing/PackingExportMenu'
import { useTranslation } from '../../i18n'
import { useAuthStore } from '../../store/authStore'
import { useTripStore } from '../../store/tripStore'
import { useCanDo } from '../../store/permissionsStore'
import type { PackingItem, TodoItem } from '../../types'
import { PackingListPanel, TodoListPanel } from './plannerLazy'
import { LazyPanel } from './LazyPanel'
export function ListsContainer({ tripId, packingItems, todoItems }: Readonly<{ tripId: number; packingItems: PackingItem[]; todoItems: TodoItem[] }>) {
const [subTab, setSubTab] = useState<'packing' | 'todo'>(() => {
return (sessionStorage.getItem(`trip-lists-subtab-${tripId}`) as 'packing' | 'todo') || 'packing'
})
const setSubTabPersist = (tab: 'packing' | 'todo') => { setSubTab(tab); sessionStorage.setItem(`trip-lists-subtab-${tripId}`, tab) }
const [importPackingSignal, setImportPackingSignal] = useState(0)
const [addCategorySignal, setAddCategorySignal] = useState(0)
const [saveTemplateSignal, setSaveTemplateSignal] = useState(0)
const [addTodoSignal, setAddTodoSignal] = useState(0)
const [packingView, setPackingView] = useState<'common' | 'personal'>('common')
const { t } = useTranslation()
const isAdmin = useAuthStore(s => s.user?.role === 'admin')
const trip = useTripStore(s => s.trip)
const canEditPacking = useCanDo()('packing_edit', trip)
const tabs = [
{ id: 'packing' as const, label: t('todo.subtab.packing'), icon: PackageCheck, count: packingItems.length },
{ id: 'todo' as const, label: t('todo.subtab.todo'), icon: ListTodo, count: todoItems.length },
]
// The to-do view fills what is left under the bar, so its list and detail pane
// scroll inside the screen and the pane's buttons stay in sight.
const fill = subTab === 'todo'
return (
<div style={fill ? { display: 'flex', flexDirection: 'column', height: '100%' } : undefined}>
<div style={{ padding: '24px 28px 0', flexShrink: 0 }} className="max-md:!px-4 max-md:!pt-4">
<div className="bg-surface-tertiary" style={{
borderRadius: 18,
paddingBlock: 14, paddingInlineEnd: 16, paddingInlineStart: 22,
display: 'flex', alignItems: 'center', gap: 16, flexWrap: 'wrap',
}}>
<h2 className="text-content" style={{ margin: 0, fontSize: 'calc(18px * var(--fs-scale-subtitle, 1))', fontWeight: 600, letterSpacing: '-0.01em', flexShrink: 0 }}>
{t('trip.tabs.lists')}
</h2>
<div className="hidden md:block bg-edge-faint" style={{ width: 1, height: 22, flexShrink: 0 }} />
<div style={{ display: 'inline-flex', gap: 4, flexWrap: 'wrap', flex: 1, minWidth: 0 }}>
{tabs.map(tab => {
const active = subTab === tab.id
const Icon = tab.icon
return (
<button type="button" key={tab.id} onClick={() => setSubTabPersist(tab.id)}
className={active ? 'bg-surface-card text-content' : 'bg-transparent text-content-muted'}
style={{
appearance: 'none', border: 'none', cursor: 'pointer', fontFamily: 'inherit',
display: 'inline-flex', alignItems: 'center', gap: 6,
padding: '6px 12px', borderRadius: 99, fontSize: 'calc(13px * var(--fs-scale-body, 1))', whiteSpace: 'nowrap',
fontWeight: active ? 500 : 400,
boxShadow: active ? '0 1px 2px rgba(0,0,0,0.06)' : 'none',
transition: 'background 180ms cubic-bezier(0.23,1,0.32,1), color 180ms cubic-bezier(0.23,1,0.32,1), box-shadow 180ms cubic-bezier(0.23,1,0.32,1)',
}}
>
<Icon size={13} className={active ? 'text-content' : 'text-content-faint'} />
<span className="hidden sm:inline">{tab.label}</span>
<span className={`text-content-faint ${active ? 'bg-surface-tertiary' : 'bg-[rgba(0,0,0,0.06)]'}`} style={{
fontSize: 'calc(10px * var(--fs-scale-caption, 1))', fontWeight: 600,
padding: '1px 6px', borderRadius: 99, minWidth: 16, textAlign: 'center',
}}>{tab.count}</span>
</button>
)
})}
</div>
{subTab === 'packing' && (() => {
const sharedBtnClass = 'inline-flex items-center gap-1.5 px-2.5 sm:px-[14px] py-[7px] sm:py-[9px] hover:opacity-[0.88]'
// Export and Import carry only their icon, with the name as tooltip and label.
const iconBtnClass = 'inline-flex items-center justify-center px-2.5 py-[7px] sm:py-[9px] hover:opacity-[0.88] bg-accent text-accent-text'
const sharedBtnStyle: React.CSSProperties = {
appearance: 'none', border: 'none', cursor: 'pointer', fontFamily: 'inherit',
borderRadius: 10, fontSize: 'calc(13px * var(--fs-scale-body, 1))', fontWeight: 500,
}
return (
<div style={{ display: 'flex', gap: 6, flexShrink: 0, marginInlineStart: 'auto', flexWrap: 'wrap' }}>
{canEditPacking && (
<button type="button" onClick={() => setAddCategorySignal(s => s + 1)}
className={`${sharedBtnClass} bg-accent text-accent-text`}
style={sharedBtnStyle}
>
<ListPlus size={14} strokeWidth={2.5} />
<span className="hidden sm:inline">{t('packing.addCategory')}</span>
</button>
)}
<ApplyTemplateButton
tripId={tripId}
visibility={packingView}
className={`${sharedBtnClass} bg-accent text-accent-text`}
style={sharedBtnStyle}
/>
{isAdmin && packingItems.length > 0 && (
<button type="button" onClick={() => setSaveTemplateSignal(s => s + 1)}
className={`${sharedBtnClass} bg-accent text-accent-text`}
style={sharedBtnStyle}
>
<FolderPlus size={14} strokeWidth={2.5} />
<span className="hidden sm:inline">{t('packing.saveAsTemplate')}</span>
</button>
)}
<PackingExportMenu tripId={tripId} view={packingView} className={iconBtnClass} style={sharedBtnStyle} />
<button type="button" onClick={() => setImportPackingSignal(s => s + 1)}
className={iconBtnClass}
style={sharedBtnStyle}
aria-label={t('packing.import')}
title={t('packing.import')}
>
<Download size={14} strokeWidth={2.5} />
</button>
</div>
)
})()}
{subTab === 'todo' && (
<button type="button" onClick={() => setAddTodoSignal(s => s + 1)}
className="hover:opacity-[0.88] bg-accent text-accent-text"
style={{
appearance: 'none', border: 'none', cursor: 'pointer', fontFamily: 'inherit',
display: 'inline-flex', alignItems: 'center', gap: 6,
padding: '9px 14px', borderRadius: 10, fontSize: 'calc(13px * var(--fs-scale-body, 1))', fontWeight: 500,
flexShrink: 0,
marginInlineStart: 'auto',
}}
>
<Plus size={14} strokeWidth={2.5} />
<span className="hidden sm:inline">{t('todo.addItem')}</span>
</button>
)}
</div>
</div>
<div style={fill ? { padding: '16px 28px 16px', flex: 1, minHeight: 0 } : { padding: '16px 28px 0' }} className="max-md:!px-4">
{subTab === 'packing' && (
<LazyPanel id="packing">
<PackingListPanel tripId={tripId} items={packingItems} openImportSignal={importPackingSignal} addCategorySignal={addCategorySignal} saveTemplateSignal={saveTemplateSignal} inlineHeader={false} view={packingView} onViewChange={setPackingView} />
</LazyPanel>
)}
{subTab === 'todo' && (
<LazyPanel id="todo">
<TodoListPanel tripId={tripId} items={todoItems} addItemSignal={addTodoSignal} />
</LazyPanel>
)}
</div>
</div>
)
}