feat(ui): unified toolbar design + redesigned budget widgets + polish

Trip planner now has a consistent rounded toolbar across bookings, lists,
budget and files. Each panel shows title, inline filter pills (with
counts where useful) and an accent action button on the right. Moved
per-tab controls into the toolbar — lists import, todo add, budget
currency/add-category, files trash/filters — and dropped the redundant
in-panel headers.

Budget sidebar redesigned: total-budget card with indigo-ringed avatars
and coloured split bar; settlement flows as paired avatar cards;
by-category donut rebuilt in SVG with per-category gradients. Both cards
now follow dark/light mode via a widgetTheme helper.

Todo: add-new-task is a portalled modal on desktop, the add-task input
bar is gone; new SORT BY section in the sidebar; inline category
creation in the task editor.

Reservations: pending / confirmed sections remember their collapsed
state per trip (localStorage).

Misc: per-trip connections toggle moved into the day-plan sidebar,
booking endpoints fixed to show on map for trains/cruises/cars as well,
label localStorage persistence, RESMODAL test updated to the new
airport-select flow.

i18n: the new booking / map / todo / budget strings are translated into
all 15 supported languages.
This commit is contained in:
Maurice
2026-04-17 23:25:38 +02:00
parent 5e9c8d2c43
commit 530550455d
22 changed files with 894 additions and 316 deletions
+89 -23
View File
@@ -35,36 +35,102 @@ 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 } from 'lucide-react'
import { ListTodo, Upload, Plus } from 'lucide-react'
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 [addTodoSignal, setAddTodoSignal] = useState(0)
const { t } = useTranslation()
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 },
]
return (
<div>
<div style={{ display: 'flex', gap: 4, padding: '4px 16px 0', borderBottom: '1px solid var(--border-faint)', marginBottom: 8 }}>
{([
{ id: 'packing' as const, label: t('todo.subtab.packing'), icon: PackageCheck },
{ id: 'todo' as const, label: t('todo.subtab.todo'), icon: ListTodo },
]).map(tab => (
<button key={tab.id} onClick={() => setSubTabPersist(tab.id)}
style={{
display: 'flex', alignItems: 'center', gap: 5, fontSize: 12, fontWeight: 500, padding: '8px 14px',
border: 'none', cursor: 'pointer', fontFamily: 'inherit', background: 'none',
color: subTab === tab.id ? 'var(--text-primary)' : 'var(--text-faint)',
borderBottom: subTab === tab.id ? '2px solid var(--text-primary)' : '2px solid transparent',
marginBottom: -1, transition: 'color 0.15s',
}}>
<tab.icon size={14} />
{tab.label}
</button>
))}
<div style={{ padding: '24px 28px 0' }} className="max-md:!px-4 max-md:!pt-4">
<div style={{
background: 'var(--bg-tertiary)', borderRadius: 18,
padding: '14px 16px 14px 22px',
display: 'flex', alignItems: 'center', gap: 16, flexWrap: 'wrap',
}}>
<h2 style={{ margin: 0, fontSize: 18, fontWeight: 600, color: 'var(--text-primary)', letterSpacing: '-0.01em', flexShrink: 0 }}>
{t('trip.tabs.lists')}
</h2>
<div className="hidden md:block" style={{ width: 1, height: 22, background: 'var(--border-faint)', 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 key={tab.id} onClick={() => setSubTabPersist(tab.id)}
style={{
appearance: 'none', border: 'none', cursor: 'pointer', fontFamily: 'inherit',
display: 'inline-flex', alignItems: 'center', gap: 6,
padding: '6px 12px', borderRadius: 99, fontSize: 13, whiteSpace: 'nowrap',
background: active ? 'var(--bg-card)' : 'transparent',
color: active ? 'var(--text-primary)' : 'var(--text-muted)',
fontWeight: active ? 500 : 400,
boxShadow: active ? '0 1px 2px rgba(0,0,0,0.06)' : 'none',
transition: 'all 0.15s ease',
}}
>
<Icon size={13} style={{ color: active ? 'var(--text-primary)' : 'var(--text-faint)' }} />
<span className="hidden sm:inline">{tab.label}</span>
<span style={{
fontSize: 10, fontWeight: 600,
background: active ? 'var(--bg-tertiary)' : 'rgba(0,0,0,0.06)',
color: 'var(--text-faint)',
padding: '1px 6px', borderRadius: 99, minWidth: 16, textAlign: 'center',
}}>{tab.count}</span>
</button>
)
})}
</div>
{subTab === 'packing' && (
<button onClick={() => setImportPackingSignal(s => s + 1)} style={{
appearance: 'none', border: 'none', cursor: 'pointer', fontFamily: 'inherit',
display: 'inline-flex', alignItems: 'center', gap: 6,
padding: '9px 14px', borderRadius: 10, fontSize: 13, fontWeight: 500,
background: 'var(--accent)', color: 'var(--accent-text)', flexShrink: 0,
marginLeft: 'auto',
transition: 'opacity 0.15s ease',
}}
onMouseEnter={e => e.currentTarget.style.opacity = '0.88'}
onMouseLeave={e => e.currentTarget.style.opacity = '1'}
>
<Upload size={14} strokeWidth={2.5} />
<span className="hidden sm:inline">{t('packing.import')}</span>
</button>
)}
{subTab === 'todo' && (
<button onClick={() => setAddTodoSignal(s => s + 1)} style={{
appearance: 'none', border: 'none', cursor: 'pointer', fontFamily: 'inherit',
display: 'inline-flex', alignItems: 'center', gap: 6,
padding: '9px 14px', borderRadius: 10, fontSize: 13, fontWeight: 500,
background: 'var(--accent)', color: 'var(--accent-text)', flexShrink: 0,
marginLeft: 'auto',
transition: 'opacity 0.15s ease',
}}
onMouseEnter={e => e.currentTarget.style.opacity = '0.88'}
onMouseLeave={e => e.currentTarget.style.opacity = '1'}
>
<Plus size={14} strokeWidth={2.5} />
<span className="hidden sm:inline">{t('todo.addItem')}</span>
</button>
)}
</div>
</div>
<div style={{ padding: '16px 28px 0' }} className="max-md:!px-4">
{subTab === 'packing' && <PackingListPanel tripId={tripId} items={packingItems} openImportSignal={importPackingSignal} />}
{subTab === 'todo' && <TodoListPanel tripId={tripId} items={todoItems} addItemSignal={addTodoSignal} />}
</div>
{subTab === 'packing' && <PackingListPanel tripId={tripId} items={packingItems} />}
{subTab === 'todo' && <TodoListPanel tripId={tripId} items={todoItems} />}
</div>
)
}
@@ -940,7 +1006,7 @@ export default function TripPlannerPage(): React.ReactElement | null {
)}
{activeTab === 'buchungen' && (
<div style={{ height: '100%', maxWidth: 1800, margin: '0 auto', width: '100%', display: 'flex', flexDirection: 'column', overflowY: 'auto', overscrollBehavior: 'contain', paddingBottom: 'var(--bottom-nav-h)' }}>
<div style={{ height: '100%', width: '100%', display: 'flex', flexDirection: 'column', overflowY: 'auto', overscrollBehavior: 'contain', paddingBottom: 'var(--bottom-nav-h)' }}>
<ReservationsPanel
tripId={tripId}
reservations={reservations}
@@ -956,13 +1022,13 @@ export default function TripPlannerPage(): React.ReactElement | null {
)}
{activeTab === 'listen' && (
<div style={{ height: '100%', overflowY: 'auto', overscrollBehavior: 'contain', maxWidth: 1800, margin: '0 auto', width: '100%', padding: '8px 0', paddingBottom: 'calc(var(--bottom-nav-h) + 8px)' }}>
<div style={{ height: '100%', overflowY: 'auto', overscrollBehavior: 'contain', width: '100%', paddingBottom: 'var(--bottom-nav-h)' }}>
<ListsContainer tripId={tripId} packingItems={packingItems} todoItems={todoItems} />
</div>
)}
{activeTab === 'finanzplan' && (
<div style={{ height: '100%', overflowY: 'auto', overscrollBehavior: 'contain', maxWidth: 1800, margin: '0 auto', width: '100%', padding: '8px 0', paddingBottom: 'calc(var(--bottom-nav-h) + 8px)' }}>
<div style={{ height: '100%', overflowY: 'auto', overscrollBehavior: 'contain', width: '100%', paddingBottom: 'var(--bottom-nav-h)' }}>
<BudgetPanel tripId={tripId} tripMembers={tripMembers} />
</div>
)}