feat(ui): Costs bar like the other tabs, desktop packing list in the phone's look

The Costs tab opens with the same bar as Transports, Bookings, Lists and
Files: the tab's name, the trip's dates and days in one pill, the
travelers, and Settle up and Add expense on the right.

The desktop packing list takes the phone's look without losing anything
it shows. Category cards get a tinted head band, rows get hairlines and
filled checkboxes, owner, quantity and weight read as small badges, and
the bin and a menu with Move to List, Sharing, Rename and Delete replace
the row of icons. What a row does not use stays dimmed until hover or
focus, so nothing leaves a gap. A click on the name renames, ticked
items too. Add item opens a composer shaped like the next row, Add list
moved into the Lists bar, Add list and Save as template ask for the name
in a dialog, and Remove checked sits beside the progress bar. The Bags
panel becomes a card, and the item menu, the bag picker, the list menu
and the bag member picker share one popover look and close on a click
beside them.

Help center pictures for Costs and Lists are re-rendered, the Lists
guide texts follow the new controls in all 27 languages, and the wiki
pages are updated. The phone screens are unchanged.
This commit is contained in:
Maurice
2026-09-25 12:57:15 +02:00
parent 5c312a8497
commit dc60f39554
141 changed files with 1574 additions and 960 deletions
@@ -3,61 +3,57 @@ import type { PackingState } from './usePackingListPanel'
/**
* One tab row: the three-tier view switch (Gemeinsam / Meine Liste, #858) on the
* left, and the all/open/done filter on the right, separated by a vertical rule
* and sharing the same height. Left-aligned with the list content.
* left, and the all/open/done filter on the right. Each sits in its own tinted
* track, the way the phone shows them, so the row reads as two questions
* (whose list, which state) rather than five pills of equal weight.
*/
export function PackingViewTabs(S: PackingState) {
const { view, setView, filter, setFilter, t, items } = S
const commonCount = items.filter(i => !i.is_private).length
const personalCount = items.filter(i => !!i.is_private).length
const pillBase: React.CSSProperties = {
display: 'inline-flex', alignItems: 'center', gap: 6, height: 30, padding: '0 12px', borderRadius: 999,
border: '1px solid', cursor: 'pointer', fontFamily: 'inherit',
fontSize: 'calc(12.5px * var(--fs-scale-body, 1))', fontWeight: 600, transition: 'all 0.12s',
const track: React.CSSProperties = {
display: 'inline-flex', alignItems: 'center', gap: 2, padding: 3, borderRadius: 999, background: 'var(--bg-tertiary)',
}
const segment = (active: boolean): React.CSSProperties => ({
display: 'inline-flex', alignItems: 'center', gap: 6, height: 28, padding: '0 13px', borderRadius: 999,
border: 'none', cursor: 'pointer', fontFamily: 'inherit',
fontSize: 'calc(12.5px * var(--fs-scale-body, 1))', fontWeight: 600, transition: 'all 0.12s',
background: active ? 'var(--text-primary)' : 'transparent',
color: active ? 'var(--bg-primary)' : 'var(--text-muted)',
boxShadow: active ? '0 1px 2px rgba(0,0,0,0.12)' : 'none',
})
const viewPill = (id: 'common' | 'personal', icon: React.ReactNode, label: string, count: number) => {
const active = view === id
return (
<button type="button" onClick={() => setView(id)} style={{
...pillBase,
background: active ? 'var(--text-primary)' : 'transparent',
borderColor: active ? 'var(--text-primary)' : 'var(--border-primary)',
color: active ? 'var(--bg-primary)' : 'var(--text-secondary)',
}}>
<button type="button" onClick={() => setView(id)} style={segment(active)}>
{icon}{label}
<span style={{
fontSize: 'calc(10px * var(--fs-scale-caption, 1))', fontWeight: 700, borderRadius: 99, padding: '0 6px',
background: active ? 'var(--bg-primary)' : 'var(--bg-tertiary)',
background: active ? 'var(--bg-primary)' : 'var(--bg-card)',
color: active ? 'var(--text-primary)' : 'var(--text-faint)',
}}>{count}</span>
</button>
)
}
const filterPill = (id: string, label: string) => {
const active = filter === id
return (
<button type="button" key={id} onClick={() => setFilter(id)} style={{
...pillBase, gap: 0, border: '1px solid transparent', fontWeight: active ? 600 : 400,
background: active ? 'var(--text-primary)' : 'transparent',
color: active ? 'var(--bg-primary)' : 'var(--text-muted)',
}}>{label}</button>
)
}
const filterPill = (id: string, label: string) => (
<button type="button" key={id} onClick={() => setFilter(id)} style={segment(filter === id)}>{label}</button>
)
return (
<div style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '10px 0 0', flexShrink: 0, flexWrap: 'wrap' }}>
{viewPill('common', <Users size={14} />, t('packing.viewCommon'), commonCount)}
{viewPill('personal', <UserRound size={14} />, t('packing.viewPersonal'), personalCount)}
<div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '10px 0 0', flexShrink: 0, flexWrap: 'wrap' }}>
<div style={track}>
{viewPill('common', <Users size={14} />, t('packing.viewCommon'), commonCount)}
{viewPill('personal', <UserRound size={14} />, t('packing.viewPersonal'), personalCount)}
</div>
{items.length > 0 && (
<>
<span style={{ alignSelf: 'stretch', width: 1, background: 'var(--border-primary)', margin: '3px 4px' }} />
<div style={track}>
{filterPill('alle', t('packing.filterAll'))}
{filterPill('offen', t('packing.filterOpen'))}
{filterPill('erledigt', t('packing.filterDone'))}
</>
</div>
)}
</div>
)