feat(tours): plan ski and winter tours, snowshoeing and winter hiking among them

Ski becomes "Ski & winter", a type the planner plans with six disciplines
(alpine, snowboard, cross-country, ski touring, snowshoeing, winter hiking),
nothing preselected; saving needs a discipline, and a technique for
cross-country. Every leg is drawn straight between the points through
planner/skiRouting.ts, the one place the piste router will plug in, with its
heights from the elevation source and no ski time worked out.

The hike's winter label goes entirely: its migration, column, field, marks,
GPX rule, MCP field and strings. A hike is summer only; a ski tour with the
snowshoe discipline exports as snowshoeing. The unreleased ski migration takes
the six disciplines and the collection migration loses the winter column.

The pistes are offered for a ski tour, shown until switched off, and with no
tour open, never for a hike. Export, copy, collections, stages (untimed),
actual tracks, the atlas, list and day marks (one glyph per discipline), 3D,
planning over a ski import and list_tours/get_tour_plan know the planned ski
tour.
This commit is contained in:
Maurice
2026-10-20 18:27:53 +02:00
parent 204ee2ef86
commit 164a3633ac
116 changed files with 1675 additions and 1576 deletions
+9 -3
View File
@@ -57,8 +57,11 @@ export interface SegmentedOption<T extends string> {
icon?: ReactNode
}
/** The track laid out as a grid of this many columns, for more choices than one row holds. */
const SEGMENTED_COLUMNS = { 2: 'grid w-full grid-cols-2', 3: 'grid w-full grid-cols-3' } as const
/** A row of choices on a grey track, the chosen one raised like the status filter (or filled like the view switcher). */
export function Segmented<T extends string>({ value, options, onChange, label, accent = false, fill = false }: {
export function Segmented<T extends string>({ value, options, onChange, label, accent = false, fill = false, columns }: {
value: T
options: readonly SegmentedOption<T>[]
onChange: (value: T) => void
@@ -68,15 +71,18 @@ export function Segmented<T extends string>({ value, options, onChange, label, a
accent?: boolean
/** Stretch across the row, each option taking an equal share. */
fill?: boolean
/** Wrap the options into a grid of this many equal columns on the same track. */
columns?: keyof typeof SEGMENTED_COLUMNS
}) {
const track = columns ? SEGMENTED_COLUMNS[columns] : fill ? 'flex w-full' : 'inline-flex'
return (
<div role="group" aria-label={label} className={`${fill ? 'flex w-full' : 'inline-flex'} flex-none gap-0.5 rounded-[10px] bg-surface-tertiary p-[3px]`}>
<div role="group" aria-label={label} className={`${track} flex-none gap-0.5 rounded-[10px] bg-surface-tertiary p-[3px]`}>
{options.map(o => {
const on = o.value === value
const look = on ? (accent ? 'bg-accent text-accent-text' : 'bg-surface-card text-content shadow-sm') : 'text-content-muted hover:text-content'
return (
<button key={o.value} type="button" onClick={() => onChange(o.value)} aria-pressed={on}
className={`${fill ? 'flex-1' : ''} inline-flex items-center justify-center gap-1.5 whitespace-nowrap rounded-[8px] px-3 py-1.5 font-medium ${look}`}
className={`${fill ? 'flex-1' : ''} ${columns ? 'min-w-0' : ''} inline-flex items-center justify-center gap-1.5 whitespace-nowrap rounded-[8px] px-3 py-1.5 font-medium ${look}`}
style={fs(12.5, 'body')}>
{o.icon}{o.label}
</button>