diff --git a/src/components/atoms/LineBadge.tsx b/src/components/atoms/LineBadge.tsx index 35ec678..a47b6ad 100644 --- a/src/components/atoms/LineBadge.tsx +++ b/src/components/atoms/LineBadge.tsx @@ -1,22 +1,9 @@ -/** SL line badge. `size` is the pixel height of the badge. */ -export const LineBadge = ({ - designation, - size = 36, -}: { +export const LineBadge = ({ designation }: { designation: string; mode?: string; groupOfLines?: string; - size?: number; }) => ( - + {designation} ); diff --git a/src/components/atoms/SBBLineBadge.tsx b/src/components/atoms/SBBLineBadge.tsx index 47a2ae2..5dcd783 100644 --- a/src/components/atoms/SBBLineBadge.tsx +++ b/src/components/atoms/SBBLineBadge.tsx @@ -1,21 +1,5 @@ -/** SBB line badge. `size` is the pixel height of the badge. */ -export const SBBLineBadge = ({ - name, - size = 36, -}: { - name: string; - category?: string; - size?: number; -}) => ( - +export const SBBLineBadge = ({ name }: { name: string; category?: string }) => ( + {name} ); diff --git a/src/components/molecules/Clock.tsx b/src/components/molecules/Clock.tsx index bbe04de..bfa4626 100644 --- a/src/components/molecules/Clock.tsx +++ b/src/components/molecules/Clock.tsx @@ -4,7 +4,7 @@ import { useSize } from '../../hooks/useSize'; export const Clock = () => { const alwaysOn = useAlwaysOn(); - const { w, h, ref } = useSize(); + const { h, ref } = useSize(); const [time, setTime] = useState(new Date()); useEffect(() => { @@ -12,7 +12,6 @@ export const Clock = () => { const id = setInterval(() => setTime(new Date()), 1000); return () => clearInterval(id); } - // In alwaysOn mode: sync to the next full minute, then tick every 60 s let interval: ReturnType; const now = new Date(); const msUntilNext = (60 - now.getSeconds()) * 1000 - now.getMilliseconds(); @@ -24,11 +23,13 @@ export const Clock = () => { }, [alwaysOn]); const effectiveH = h || 80; - const effectiveW = w || 400; - const showSeconds = !alwaysOn && effectiveH > 100; - const showDate = effectiveH > 55 || effectiveW > 280; - const longDate = effectiveH > 130; + // Show seconds when not in alwaysOn and there is enough vertical room + const showSeconds = !alwaysOn && effectiveH > 90; + // Stack time and date vertically when tall enough; otherwise inline + const stacked = effectiveH > 110; + // Show full weekday + month name when stacked, short otherwise + const longDate = stacked; const timeStr = time.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', @@ -39,28 +40,24 @@ export const Clock = () => { weekday: longDate ? 'long' : 'short', day: 'numeric', month: longDate ? 'long' : 'short', - ...(effectiveH > 200 ? { year: 'numeric' } : {}), + ...(effectiveH > 160 ? { year: 'numeric' } : {}), }); - // Fluid font size: fill width or height, whichever is tighter. - // Each character is ~0.60 em wide in tabular-nums. - const charCount = timeStr.length; - const fontByW = (effectiveW * 0.90) / (charCount * 0.60); - const fontByH = showDate ? effectiveH * 0.50 : effectiveH * 0.72; - const fontSize = Math.max(16, Math.min(fontByW, fontByH, 220)); - const dateFontSize = Math.max(12, Math.min(fontSize * 0.22, 32)); - return (
-
- {timeStr} -
- {showDate && ( -
- {dateStr} + {stacked ? ( + <> +
+ {timeStr} +
+
{dateStr}
+ + ) : ( +
+
+ {timeStr} +
+
{dateStr}
)}
diff --git a/src/components/molecules/DepartureItem.tsx b/src/components/molecules/DepartureItem.tsx index 1f43d94..5680b93 100644 --- a/src/components/molecules/DepartureItem.tsx +++ b/src/components/molecules/DepartureItem.tsx @@ -2,33 +2,23 @@ import { LineBadge } from '../atoms/LineBadge'; import { Departure } from '../../types'; import { useAlwaysOn } from '../../contexts/AlwaysOnContext'; -export const DepartureItem = ({ - departure, - rowHeight = 48, -}: { - departure: Departure; - rowHeight?: number; -}) => { +export const DepartureItem = ({ departure }: { departure: Departure }) => { const alwaysOn = useAlwaysOn(); const depTime = new Date(departure.scheduled); const diffMin = Math.round((depTime.getTime() - Date.now()) / 60000); const timeText = diffMin <= 0 ? 'Now' : `${diffMin} min`; return ( -
-
+
+
{departure.destination}
- {timeText} + {timeText}
); }; diff --git a/src/components/molecules/SBBDepartureItem.tsx b/src/components/molecules/SBBDepartureItem.tsx index ea2fa5d..f66d71a 100644 --- a/src/components/molecules/SBBDepartureItem.tsx +++ b/src/components/molecules/SBBDepartureItem.tsx @@ -4,11 +4,9 @@ import { useAlwaysOn } from '../../contexts/AlwaysOnContext'; export const SBBDepartureItem = ({ departure, - rowHeight = 48, showPlatform = false, }: { departure: SBBDeparture; - rowHeight?: number; showPlatform?: boolean; }) => { const alwaysOn = useAlwaysOn(); @@ -16,31 +14,19 @@ export const SBBDepartureItem = ({ const diffMin = dep ? Math.round((dep.getTime() - Date.now()) / 60000) : null; const timeText = diffMin === null ? '—' : diffMin <= 0 ? 'Now' : `${diffMin} min`; const delay = departure.stop.delay; - const fontSize = Math.max(11, rowHeight * 0.40); return ( -
-
- +
+
+ {departure.to}
-
+
{showPlatform && departure.stop.platform && ( - - Pl.{departure.stop.platform} - + Pl.{departure.stop.platform} )} {delay != null && delay > 0 && ( - - +{delay}' - + +{delay}' )} {timeText}
diff --git a/src/components/organisms/DepartureList.tsx b/src/components/organisms/DepartureList.tsx index 5f9e7c8..b336434 100644 --- a/src/components/organisms/DepartureList.tsx +++ b/src/components/organisms/DepartureList.tsx @@ -1,6 +1,12 @@ import { useSize } from '../../hooks/useSize'; import { DepartureItem } from '../molecules/DepartureItem'; import { Departure } from '../../types'; +import { useAlwaysOn } from '../../contexts/AlwaysOnContext'; + +// text-2xl (line-height 2rem = 32px) + py-3 (2 × 12px) = 56 px per row +const ROW_H = 56; +// text-xl header + pb-1 + a little breathing room +const HEADER_H = 38; export const DepartureList = ({ departures, @@ -11,46 +17,28 @@ export const DepartureList = ({ loading: boolean; stationName?: string; }) => { - const { h, w, ref } = useSize(); - const effectiveH = h || 200; + const alwaysOn = useAlwaysOn(); + const { h, ref } = useSize(); + const effectiveH = h || 300; - // Header sizing - const headerH = stationName ? Math.max(24, Math.min(effectiveH * 0.17, 44)) : 0; - const headerFontSz = Math.max(11, headerH * 0.55); - - // Row sizing: taller containers get bigger rows - const listH = effectiveH - headerH - (headerH ? 6 : 0); - const rowH = listH > 500 ? 68 : listH > 360 ? 54 : listH > 240 ? 42 : listH > 150 ? 34 : 27; - const maxRows = Math.max(1, Math.floor(listH / rowH)); + const listH = effectiveH - (stationName ? HEADER_H : 0); + const maxRows = Math.max(1, Math.floor(listH / ROW_H)); const visible = departures.slice(0, maxRows); - if (loading && departures.length === 0) { - return ( -
- Loading… -
- ); - } - return (
{stationName && ( -
+

{stationName} -

+ )} - {visible.length === 0 ? ( - No departures + {loading && departures.length === 0 ? ( +

Loading…

+ ) : visible.length === 0 ? ( +

No departures

) : ( visible.map((dep, i) => ( - + )) )}
diff --git a/src/components/organisms/SBBDepartureList.tsx b/src/components/organisms/SBBDepartureList.tsx index 6df3330..20e6db8 100644 --- a/src/components/organisms/SBBDepartureList.tsx +++ b/src/components/organisms/SBBDepartureList.tsx @@ -1,6 +1,10 @@ import { useSize } from '../../hooks/useSize'; import { SBBDepartureItem } from '../molecules/SBBDepartureItem'; import { SBBDeparture } from '../../types'; +import { useAlwaysOn } from '../../contexts/AlwaysOnContext'; + +const ROW_H = 56; +const HEADER_H = 38; export const SBBDepartureList = ({ departures, @@ -11,46 +15,32 @@ export const SBBDepartureList = ({ loading: boolean; stationName?: string; }) => { + const alwaysOn = useAlwaysOn(); const { h, w, ref } = useSize(); - const effectiveH = h || 200; + const effectiveH = h || 300; - // Header sizing - const headerH = stationName ? Math.max(24, Math.min(effectiveH * 0.17, 44)) : 0; - const headerFontSz = Math.max(11, headerH * 0.55); - - // Row sizing: taller containers get bigger rows and platform info - const listH = effectiveH - headerH - (headerH ? 6 : 0); - const rowH = listH > 500 ? 68 : listH > 360 ? 54 : listH > 240 ? 42 : listH > 150 ? 34 : 27; - const maxRows = Math.max(1, Math.floor(listH / rowH)); - const showPlatform = rowH >= 54 && (w || 0) > 300; + const listH = effectiveH - (stationName ? HEADER_H : 0); + const maxRows = Math.max(1, Math.floor(listH / ROW_H)); + // Platform column only when the slot is wide enough to show it comfortably + const showPlatform = (w || 0) > 320; const visible = departures.slice(0, maxRows); - if (loading && departures.length === 0) { - return ( -
- Loading… -
- ); - } - return (
{stationName && ( -
+

{stationName} -

+ )} - {visible.length === 0 ? ( - No departures + {loading && departures.length === 0 ? ( +

Loading…

+ ) : visible.length === 0 ? ( +

No departures

) : ( visible.map((dep, i) => ( )) diff --git a/src/components/organisms/SpotifyWidget.tsx b/src/components/organisms/SpotifyWidget.tsx index 1d109c3..e5bb330 100644 --- a/src/components/organisms/SpotifyWidget.tsx +++ b/src/components/organisms/SpotifyWidget.tsx @@ -20,60 +20,38 @@ export const SpotifyWidget = () => { const effectiveH = h || 100; const effectiveW = w || 300; - // Progressive disclosure - const showLabel = effectiveH > 80; - const showArtist = effectiveH > 65 || effectiveW > 240; - const showAlbum = effectiveH > 120 && effectiveW > 260; - const showProgress = !alwaysOn && effectiveH > 95 && effectiveW > 200; - const showProgressTime = showProgress && effectiveW > 280; - - // Font sizes derived from container - const trackFS = Math.min(effectiveH * 0.28, effectiveW * 0.10, 40); - const artistFS = Math.max(11, trackFS * 0.70); - const albumFS = Math.max(10, trackFS * 0.55); - const labelFS = Math.max(9, trackFS * 0.45); - const barH = Math.max(2, effectiveH * 0.018); + // Progressive disclosure — text sizes are fixed, only visibility changes + const showAlbum = effectiveH > 110 && effectiveW > 260; + const showProgress = !alwaysOn && effectiveH > 90 && effectiveW > 200; + const showTimes = showProgress && effectiveW > 280; return (
- {showLabel && ( -
- Now Playing -
- )} +

Now Playing

{data?.playing && data.track ? ( <> -
+

{data.track.name} -

- {showArtist && ( -
- {data.track.artist} -
- )} +

+

{data.track.artist}

{showAlbum && ( -
- {data.track.album} -
+

{data.track.album}

)} {showProgress && ( -
- {showProgressTime && ( - +
+ {showTimes && ( + {fmtMs(data.track.progressMs)} )} -
+
- {showProgressTime && ( - + {showTimes && ( + {fmtMs(data.track.durationMs)} )} @@ -81,7 +59,7 @@ export const SpotifyWidget = () => { )} ) : ( -
Nothing playing
+

Nothing playing

)}
); diff --git a/src/components/organisms/SystemWidget.tsx b/src/components/organisms/SystemWidget.tsx index 41cd446..fafe15a 100644 --- a/src/components/organisms/SystemWidget.tsx +++ b/src/components/organisms/SystemWidget.tsx @@ -10,17 +10,17 @@ interface SystemData { hostname: string; } -const MetricRow = ({ - label, percent, detail, rowH, barH, fontSize, -}: { - label: string; percent: number; detail: string; - rowH: number; barH: number; fontSize: number; -}) => ( -
- {label} -
-
-
+const Bar = ({ percent }: { percent: number }) => ( +
+
+
+); + +const Row = ({ label, percent, detail }: { label: string; percent: number; detail: string }) => ( +
+ {label} + + {percent}% {detail}
); @@ -39,52 +39,27 @@ export const SystemWidget = () => { const effectiveH = h || 120; const effectiveW = w || 300; - // Progressive disclosure - const showHostname = effectiveH > 55; - const showDisk = effectiveH > 95; - const showUptime = showHostname && (effectiveH > 120 || effectiveW > 450); - const showLoadDetail = effectiveH > 150 && effectiveW > 380; - - // Row sizing - const metricCount = 2 + (showDisk ? 1 : 0); - const hostH = showHostname ? Math.min(effectiveH * 0.22, 32) : 0; - const availH = effectiveH - hostH - (metricCount - 1) * 5; - const rowH = Math.max(14, Math.min(availH / metricCount, 52)); - const fontSize = Math.max(10, rowH * 0.52); - const barH = Math.max(2, rowH * 0.22); - const hostFontSz = Math.max(12, Math.min(effectiveH * 0.14, 28)); + // Progressive disclosure — fixed text-sm rows, just show more when taller/wider + const showDisk = effectiveH > 95; + const showUptime = effectiveH > 110 || effectiveW > 450; + const showLoadDetail = effectiveH > 140 && effectiveW > 380; return ( -
- {showHostname && ( -
- {data.hostname} - {showUptime && ( - - up {data.uptime} - - )} -
- )} - +
+ {data.hostname} + {showUptime && ( + up {data.uptime} + )} +
+ - + {showDisk && ( - + )}
); diff --git a/src/components/organisms/WeatherWidget.tsx b/src/components/organisms/WeatherWidget.tsx index 4fc3fd2..252310e 100644 --- a/src/components/organisms/WeatherWidget.tsx +++ b/src/components/organisms/WeatherWidget.tsx @@ -22,18 +22,17 @@ const MOON_CHAR: Record = { function getAsciiKey(desc: string): string { const d = desc.toLowerCase(); - if (d.includes('thunder')) return 'thunder'; - if (d.includes('snow')) return 'snow'; - if (d.includes('sleet')) return 'sleet'; - if (d.includes('drizzle')) return 'drizzle'; - if (d.includes('rain')) return 'rain'; - if (d.includes('fog') || d.includes('mist')) return 'fog'; - if (d.includes('overcast')) return 'overcast'; - if (d.includes('mostly cloudy') || d.includes('very')) return 'cloudy'; + if (d.includes('thunder')) return 'thunder'; + if (d.includes('snow')) return 'snow'; + if (d.includes('sleet')) return 'sleet'; + if (d.includes('drizzle')) return 'drizzle'; + if (d.includes('rain')) return 'rain'; + if (d.includes('fog') || d.includes('mist')) return 'fog'; + if (d.includes('overcast')) return 'overcast'; + if (d.includes('mostly cloudy') || d.includes('very')) return 'cloudy'; if (d.includes('partly') || d.includes('mostly sunny')) return 'partlycloudy'; - if (d.includes('cloudy')) return 'cloudy'; - if (d.includes('sunny') || d.includes('clear')) return 'sunny'; - return 'cloudy'; + if (d.includes('cloudy')) return 'cloudy'; + return 'sunny'; } export const WeatherWidget = () => { @@ -44,9 +43,6 @@ export const WeatherWidget = () => { refetchInterval: 10 * 60 * 1000, }); - const effectiveH = h || 100; - const effectiveW = w || 350; - if (isError || !data || (data as any).error) { return (
@@ -55,49 +51,43 @@ export const WeatherWidget = () => { ); } - // Progressive disclosure based on available space - const showAscii = effectiveH > 200 && effectiveW > 380; - const showHourly = effectiveH > 140 && effectiveW > 260; - const showSunMoon = effectiveH > 95 || effectiveW > 460; + const effectiveH = h || 80; + const effectiveW = w || 350; - // Font sizes derived from container - const tempFontSize = Math.min(effectiveH * (showAscii ? 0.34 : 0.42), 84); - const descFontSize = Math.max(12, tempFontSize * 0.36); - const metaFontSize = Math.max(10, tempFontSize * 0.27); - const asciiFontSize = Math.max(9, effectiveH / 8); - const maxHours = effectiveW > 500 ? 6 : effectiveW > 380 ? 4 : 3; + // Progressive disclosure — everything is the same size, just more/less shown + const showAscii = effectiveH > 180 && effectiveW > 380; + const showHourly = effectiveH > 120 && effectiveW > 260; + const showSunMoon = effectiveH > 80 || effectiveW > 460; + const maxHours = effectiveW > 500 ? 6 : effectiveW > 380 ? 4 : 3; const art = ASCII[getAsciiKey(data.description)] ?? ASCII.cloudy; const moon = MOON_CHAR[data.moonPhase] ?? data.moonPhase; return ( -
+
{showAscii && ( -
+        
           {art.join('\n')}
         
)} -
+
- {data.temperature}°C - {data.description} + {data.temperature}°C + {data.description}
{showSunMoon && ( -
+
↑ {data.sunrise} ↓ {data.sunset} {moon} {data.moonPhase}
)} {showHourly && data.hourly.length > 0 && ( -
+
{data.hourly.slice(0, maxHours).map((entry, i) => (
-
{entry.time}
-
{entry.temp}°
+
{entry.time}
+
{entry.temp}°
))}