Components
shadcn/ui Components
The app uses shadcn/ui for standard UI components. These are installed directly into src/components/ui/ and can be customized:
| Component | Usage |
|---|---|
| Button | Primary actions, form submissions, destructive actions |
| Card | Content containers for dashboard cards, detail sections |
| Input | Text inputs with label support |
| Select | Dropdown selects for filters, form fields |
| Badge | Trip status badges (ACTIVE, COMPLETED, CANCELLED) |
| Checkbox | Toggle switches (GPS logging, auto-refresh) |
| Label | Form field labels |
| Textarea | Multi-line text input (descriptions, notes) |
| Separator | Visual dividers |
Theme
shadcn/ui uses CSS variables for theming with automatic dark mode support via the dark class on <html>. The theme is configured in globals.css with oklch colors.
Shared Components (@elcto/ui)
Custom components that are shared across apps via the @elcto/ui workspace package.
Map
File: shared/ui/src/components/map.tsx
Interactive MapLibre GL map with composable children components. Supports multiple tile providers with automatic dark/light theme switching.
Map Props
| Prop | Type | Default | Description |
|---|---|---|---|
latitude | number | — | Center latitude |
longitude | number | — | Center longitude |
center | [lng, lat] | — | Center as GeoJSON array (alternative to lat/lng) |
zoom | number | 13 | Initial zoom level |
tiles | MapTileProvider | 'carto' | Tile provider |
themeOverride | 'light' | 'dark' | — | Override auto theme detection |
children | ReactNode | — | Map children (markers, routes, etc.) |
Tile Providers
| Provider | Light | Dark |
|---|---|---|
carto | Carto Voyager | Carto Dark Matter |
openfreemap | OpenFreeMap Bright | Carto Dark Matter |
openfreemap3d | OpenFreeMap Liberty (60° pitch) | Carto Dark Matter |
positron | OpenFreeMap Positron | Carto Dark Matter |
liberty | OpenFreeMap Liberty (flat) | Carto Dark Matter |
fiord | OpenFreeMap Fiord | Carto Dark Matter |
Child Components
| Component | Description |
|---|---|
MapMarker | Position marker with React children (dot, arrow, badge) |
StaticMarker | Simple colored dot marker |
MapRoute | GeoJSON line layer for routes |
FitBounds | Auto-fit map to given points |
Current Position Marker
The GPS page uses MapMarker with a CurrentPositionMarker child that renders:
- Dot —
#14F5BFwith beacon pulse animation - Heading arrow — SVG chevron pointing in direction of travel (only when speed > 2 km/h)
- Speed badge — Spring-animated speed display with
useSpring, positioned opposite to heading arrow using 8-point anchoring - Dark mode — Adapts border/stroke colors for dark map tiles
Usage
import { Map, MapMarker, StaticMarker, MapRoute, FitBounds } from '@elcto/ui';
<Map latitude={52.52} longitude={13.40} zoom={14} tiles="carto">
<MapMarker longitude={13.40} latitude={52.52}>
<CurrentPositionMarker speed={12.5} heading={90} />
</MapMarker>
<StaticMarker latitude={52.51} longitude={13.39} color="#22c55e" />
<MapRoute points={gpsHistory} />
<FitBounds points={allPoints} />
</Map>
Spinner
Loading indicator with configurable size (sm, md, lg).
import { Spinner } from '@elcto/ui';
<Spinner size="md" />
ThemeToggle
Dark/light mode toggle with system preference detection. Persists to localStorage.
import { ThemeToggle } from '@elcto/ui';
<ThemeToggle />
Speed Overlay
File: src/app/(overlay)/overlay/speed/page.tsx
A minimal, embeddable speed display designed for OBS Studio browser source.
Features
- Real-time WebSocket — Subscribes to the
speedchannel via Rust API/ws - Spring animation — Speed transitions smoothly via custom
useSpringhook (stiffness: 0.05, damping: 0.65) - Connection indicator — Green pulsing dot (connected), red (disconnected)
- Three states — Loading (spinner), Active (animated speed), Error (message)
- OBS compatible — Transparent
html/bodybackground, dark card design
Visual Design (hardcoded, not themed)
| Property | Value |
|---|---|
| Card background | rgba(39, 44, 50, 0.95) |
| Accent color | #14F5BF |
| Text color | #EDEDF3 |
| Left border | 4px solid #14F5BF |
| Card height | 5.25rem |
URL
Access at /overlay/speed — uses the overlay layout group (no navigation).
Map Overlay
File: src/app/(overlay)/overlay/map/page.tsx
Fullpage live GPS map for OBS Studio browser source with heading arrow, speed badge, and compass.
URL Parameters
| Param | Default | Description |
|---|---|---|
tiles | openfreemap | Tile provider (carto, openfreemap, openfreemap3d, positron, liberty, fiord) |
theme | light | light or dark (manual override) |
zoom | 16 | Initial zoom level |
compass | off | 1 = show compass, 1.5 = larger, 0 or absent = hidden |
speed | on | false or 0 = hide speed badge |
ais | on | false or 0 = hide AIS vessel markers |
aisnames | on | false or 0 = hide vessel name labels |
aisinfo | on | false or 0 = hide vessel info badges (speed, length, beam, type) |
zoomcycle | off | Comma-separated zoom levels (e.g. 16,12,10) |
zoominterval | 30 | Seconds between zoom cycle steps |
autotime | off | true = auto dark/light based on Berlin time (20:00–06:00 = night) |
Example
/overlay/map?tiles=carto&theme=dark&zoom=15&compass=1&speed=true&ais=true&aisnames=true&zoomcycle=16,12,10&zoominterval=15
Compass Overlay
File: src/app/(overlay)/overlay/compass/page.tsx
Standalone compass widget for OBS. Shows heading needle, degree value, and 16-point cardinal direction.
| Param | Default | Description |
|---|---|---|
scale | 1 | Compass size multiplier |
URL
Access at /overlay/compass or /overlay/compass?scale=1.5.
AIS Vessel Markers
The map overlay displays AIS (Automatic Identification System) vessel data received via the vessels WebSocket channel. The Rust API connects to AISstream.io via tokio-tungstenite and broadcasts vessel positions.
Vessel Display
| Vessel State | Marker | Arrow | Badge |
|---|---|---|---|
| Moving (Unterwegs) | Orange dot | Orange heading arrow | Speed, name, info rotation |
| Stationary (Vor Anker, Festgemacht, Auf Grund) | Gray dot | None | Name only, no speed |
Badge Info Rotation
Moving vessels rotate through info badges every 10 seconds with a fade animation:
- Speed (knots)
- Length (meters, from ShipStaticData: A+B)
- Beam (meters, from ShipStaticData: C+D)
- Ship type
Distance-Based Opacity
Badge opacity scales with distance from own ship:
- ≤500m: 100% opacity
- 500m–5km: Linear fade
- ≥5km: 15% minimum opacity
z-index Layering
| Layer | z-index |
|---|---|
| Own ship marker | 100 |
| Moving vessels | 10 |
| Stationary vessels | 1 |
Nav Status (German Labels)
Unterwegs, Vor Anker, Festgemacht, Nicht unter Kommando, Tiefgangbeschränkt, Auf Grund, Fischend, Unter Segel, Manövrierunfähig.
Configuration
AIS is configured in the Rust API via the [ais] TOML section:
| Key | Description |
|---|---|
api_key | AISstream.io API key |
enabled | Enable/disable AIS tracking |
radius_km | Radius around GPS position for vessel subscription |
ignore_mmsi | Array of MMSI numbers to blacklist |
GPS Tracker Page
File: src/app/(app)/gps/page.tsx
Displays real-time GPS position via WebSocket with interactive map.
- Subscribes to
speedandgpsWebSocket channels for live updates - Dead reckoning interpolation for smooth marker movement between GPS updates
- Heading arrow + spring-animated speed badge on current position marker
- Beacon pulse animation on dot
- Tile provider selector (Carto / OpenStreetMap) with auto dark/light mode
- Map auto-centers on position via
LiveTracker - Manual refresh button as REST fallback
Trip Management
Full CRUD with filtering by status, client, category, date range, GPS tracking, and search.
Pages
| Page | Path | Description |
|---|---|---|
| Trip List | /trips | Filterable list with status badges |
| New Trip | /trips/new | Create with sender/receiver, cargo, GPS toggle |
| Trip Detail | /trips/[id] | Map with route, complete/delete actions |
| Edit Trip | /trips/[id]/edit | Update all trip fields |
| Categories | /trips/categories | Inline CRUD for cargo categories |
Client Management
Client directory with address management, contact persons, and trip associations.
Pages
| Page | Path | Description |
|---|---|---|
| Client List | /clients | Names, emails, trip counts |
| New Client | /clients/new | Company info, address, dynamic contacts |
| Client Detail | /clients/[id] | Full info with trips as sender/receiver |