feat(client): show Dawarich stays, routes and marks where they belong

The trip planner and the journal get a "From Dawarich" list: the stays
recorded for those dates, each one a row that can be dismissed or turned
into a place or an entry after a review step that shows time, name, note
and day. A place that came from a recording carries the Dawarich mark
next to its name so its origin stays visible afterwards.

The recorded route lies over the trip and journal maps as an overlay,
coloured per day and cut along the reader's own timezone rather than
UTC. It is fetched for the window on screen and never stored.

The Atlas gets its own panel beside the map for the countries and cities
the recordings prove, and the wish list is ticked off there, on the day
of the visit, undoably. The phone has its own take on all of it, in the
mobile shell's colours and with native sheets.
This commit is contained in:
Maurice
2026-09-12 23:35:43 +02:00
parent 5678eed55c
commit 5b4a7e82ff
108 changed files with 8945 additions and 295 deletions
@@ -0,0 +1,14 @@
import React from 'react'
import BrandIcon, { type BrandIconProps } from './BrandIcon'
/**
* AirTrail's own mark, unmodified: the control tower in its blue.
*
* The file is `static/favicon.svg` from johanohly/AirTrail, served from
* `public/brands/`. Rendered as a file rather than as lucide's `TowerControl`
* recoloured: the two happen to be the same shape today, and the day AirTrail
* changes its logo TREK should follow the logo, not the coincidence.
*/
export default function AirTrailIcon(props: BrandIconProps): React.ReactElement {
return <BrandIcon src="/brands/airtrail.svg" {...props} />
}
@@ -0,0 +1,65 @@
import React from 'react'
export interface BrandIconProps {
size?: number
/**
* Fill the box it sits in, corner to corner.
*
* A brand mark that is itself a badge should BE the framed slot rather than
* float in the middle of one — which is what the lucide line icons beside it
* need, and it does not.
*/
fill?: boolean
/**
* Drain the colour, for a switched-off addon. Every other tile greys its glyph
* out; a full-colour badge among them reads as the one thing that is on.
*/
muted?: boolean
/**
* Sizing classes, so the mark can stand in wherever a lucide icon is expected
* (`w-5 h-5 …`). When set it wins over `size`.
*/
className?: string
}
/**
* A third party's own logo, served as a file and rendered unmodified.
*
* The files under `public/brands/` are the marks as each project ships them.
* They are deliberately NOT recoloured to `currentColor` the way the Immich and
* Synology glyphs in the addon manager are: those are single-colour shapes that
* happen to be brands, while these are the pictures people recognise, and
* redrawing one in the user's accent makes it a different logo.
*
* Served rather than inlined because one of them is 56 paths — inline it would
* be a kilobyte of markup at every call site, and this way the browser caches it.
*/
export default function BrandIcon({
src,
size = 20,
fill = false,
muted = false,
className,
}: BrandIconProps & { src: string }): React.ReactElement {
return (
<img
src={src}
alt=""
aria-hidden="true"
width={size}
height={size}
className={className}
// The files carry their own width/height attributes; the explicit style is
// what keeps them at icon size next to the lucide glyphs.
style={{
...(fill
? { width: '100%', height: '100%', objectFit: 'contain' as const }
: className
? { flexShrink: 0 }
: { width: size, height: size, flexShrink: 0 }),
display: 'block',
...(muted ? { filter: 'grayscale(1)', opacity: 0.55 } : {}),
}}
/>
)
}
@@ -0,0 +1,14 @@
import React from 'react'
import BrandIcon, { type BrandIconProps } from './BrandIcon'
/**
* Dawarich's own mark, unmodified: green and grey map blocks behind the blue
* road that loops back as a D, with the white roundabout at the junction.
*
* The file is `app/assets/images/logo.svg` from Freika/dawarich, served from
* `public/brands/`. See `BrandIcon` for why it is a file rather than inline
* markup, and why it keeps its own colours.
*/
export default function DawarichIcon(props: BrandIconProps): React.ReactElement {
return <BrandIcon src="/brands/dawarich.svg" {...props} />
}