fix: widgets show more content in larger slots, not bigger text
This commit is contained in:
parent
42353145e0
commit
56e58c6c08
10 changed files with 137 additions and 272 deletions
|
|
@ -1,22 +1,9 @@
|
||||||
/** SL line badge. `size` is the pixel height of the badge. */
|
export const LineBadge = ({ designation }: {
|
||||||
export const LineBadge = ({
|
|
||||||
designation,
|
|
||||||
size = 36,
|
|
||||||
}: {
|
|
||||||
designation: string;
|
designation: string;
|
||||||
mode?: string;
|
mode?: string;
|
||||||
groupOfLines?: string;
|
groupOfLines?: string;
|
||||||
size?: number;
|
|
||||||
}) => (
|
}) => (
|
||||||
<span
|
<span className="border border-white text-white px-2 py-0.5 font-bold min-w-[56px] text-center text-xl inline-block shrink-0">
|
||||||
className="border border-white text-white font-bold text-center tabular-nums shrink-0 inline-flex items-center justify-center"
|
|
||||||
style={{
|
|
||||||
height: size,
|
|
||||||
minWidth: size * 1.6,
|
|
||||||
fontSize: Math.max(10, size * 0.44),
|
|
||||||
padding: `0 ${size * 0.18}px`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{designation}
|
{designation}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,5 @@
|
||||||
/** SBB line badge. `size` is the pixel height of the badge. */
|
export const SBBLineBadge = ({ name }: { name: string; category?: string }) => (
|
||||||
export const SBBLineBadge = ({
|
<span className="border border-white text-white px-2 py-0.5 font-bold min-w-[56px] text-center text-xl inline-block shrink-0">
|
||||||
name,
|
|
||||||
size = 36,
|
|
||||||
}: {
|
|
||||||
name: string;
|
|
||||||
category?: string;
|
|
||||||
size?: number;
|
|
||||||
}) => (
|
|
||||||
<span
|
|
||||||
className="border border-white text-white font-bold text-center tabular-nums shrink-0 inline-flex items-center justify-center"
|
|
||||||
style={{
|
|
||||||
height: size,
|
|
||||||
minWidth: size * 1.6,
|
|
||||||
fontSize: Math.max(10, size * 0.44),
|
|
||||||
padding: `0 ${size * 0.18}px`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{name}
|
{name}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { useSize } from '../../hooks/useSize';
|
||||||
|
|
||||||
export const Clock = () => {
|
export const Clock = () => {
|
||||||
const alwaysOn = useAlwaysOn();
|
const alwaysOn = useAlwaysOn();
|
||||||
const { w, h, ref } = useSize();
|
const { h, ref } = useSize();
|
||||||
const [time, setTime] = useState(new Date());
|
const [time, setTime] = useState(new Date());
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -12,7 +12,6 @@ export const Clock = () => {
|
||||||
const id = setInterval(() => setTime(new Date()), 1000);
|
const id = setInterval(() => setTime(new Date()), 1000);
|
||||||
return () => clearInterval(id);
|
return () => clearInterval(id);
|
||||||
}
|
}
|
||||||
// In alwaysOn mode: sync to the next full minute, then tick every 60 s
|
|
||||||
let interval: ReturnType<typeof setInterval>;
|
let interval: ReturnType<typeof setInterval>;
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const msUntilNext = (60 - now.getSeconds()) * 1000 - now.getMilliseconds();
|
const msUntilNext = (60 - now.getSeconds()) * 1000 - now.getMilliseconds();
|
||||||
|
|
@ -24,11 +23,13 @@ export const Clock = () => {
|
||||||
}, [alwaysOn]);
|
}, [alwaysOn]);
|
||||||
|
|
||||||
const effectiveH = h || 80;
|
const effectiveH = h || 80;
|
||||||
const effectiveW = w || 400;
|
|
||||||
|
|
||||||
const showSeconds = !alwaysOn && effectiveH > 100;
|
// Show seconds when not in alwaysOn and there is enough vertical room
|
||||||
const showDate = effectiveH > 55 || effectiveW > 280;
|
const showSeconds = !alwaysOn && effectiveH > 90;
|
||||||
const longDate = effectiveH > 130;
|
// 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', {
|
const timeStr = time.toLocaleTimeString('en-GB', {
|
||||||
hour: '2-digit', minute: '2-digit',
|
hour: '2-digit', minute: '2-digit',
|
||||||
|
|
@ -39,28 +40,24 @@ export const Clock = () => {
|
||||||
weekday: longDate ? 'long' : 'short',
|
weekday: longDate ? 'long' : 'short',
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
month: longDate ? 'long' : 'short',
|
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 (
|
return (
|
||||||
<div ref={ref} className="w-full h-full flex flex-col justify-center overflow-hidden">
|
<div ref={ref} className="w-full h-full flex flex-col justify-center overflow-hidden">
|
||||||
<div
|
{stacked ? (
|
||||||
className="tabular-nums leading-none"
|
<>
|
||||||
style={{ fontSize, fontWeight: alwaysOn ? 300 : 700, letterSpacing: '-0.02em' }}
|
<div className={`text-7xl tabular-nums leading-none ${alwaysOn ? 'font-light' : 'font-bold'}`}>
|
||||||
>
|
{timeStr}
|
||||||
{timeStr}
|
</div>
|
||||||
</div>
|
<div className="text-2xl text-gray-400 mt-2">{dateStr}</div>
|
||||||
{showDate && (
|
</>
|
||||||
<div className="text-gray-400" style={{ fontSize: dateFontSize, marginTop: `${fontSize * 0.08}px` }}>
|
) : (
|
||||||
{dateStr}
|
<div className="flex items-baseline gap-6 flex-wrap">
|
||||||
|
<div className={`text-7xl tabular-nums ${alwaysOn ? 'font-light' : 'font-bold'}`}>
|
||||||
|
{timeStr}
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl text-gray-400">{dateStr}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -2,33 +2,23 @@ import { LineBadge } from '../atoms/LineBadge';
|
||||||
import { Departure } from '../../types';
|
import { Departure } from '../../types';
|
||||||
import { useAlwaysOn } from '../../contexts/AlwaysOnContext';
|
import { useAlwaysOn } from '../../contexts/AlwaysOnContext';
|
||||||
|
|
||||||
export const DepartureItem = ({
|
export const DepartureItem = ({ departure }: { departure: Departure }) => {
|
||||||
departure,
|
|
||||||
rowHeight = 48,
|
|
||||||
}: {
|
|
||||||
departure: Departure;
|
|
||||||
rowHeight?: number;
|
|
||||||
}) => {
|
|
||||||
const alwaysOn = useAlwaysOn();
|
const alwaysOn = useAlwaysOn();
|
||||||
const depTime = new Date(departure.scheduled);
|
const depTime = new Date(departure.scheduled);
|
||||||
const diffMin = Math.round((depTime.getTime() - Date.now()) / 60000);
|
const diffMin = Math.round((depTime.getTime() - Date.now()) / 60000);
|
||||||
const timeText = diffMin <= 0 ? 'Now' : `${diffMin} min`;
|
const timeText = diffMin <= 0 ? 'Now' : `${diffMin} min`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={`flex justify-between items-center py-3 border-b border-zinc-700 text-2xl ${alwaysOn ? 'font-light' : ''}`}>
|
||||||
className="flex justify-between items-center border-b border-zinc-700 shrink-0"
|
<div className="flex items-center gap-4 min-w-0 overflow-hidden">
|
||||||
style={{ height: rowHeight, fontSize: Math.max(11, rowHeight * 0.40) }}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-3 min-w-0 overflow-hidden">
|
|
||||||
<LineBadge
|
<LineBadge
|
||||||
designation={departure.line.designation}
|
designation={departure.line.designation}
|
||||||
mode={departure.line.transport_mode}
|
mode={departure.line.transport_mode}
|
||||||
groupOfLines={departure.line.group_of_lines}
|
groupOfLines={departure.line.group_of_lines}
|
||||||
size={rowHeight * 0.65}
|
|
||||||
/>
|
/>
|
||||||
<span className="truncate">{departure.destination}</span>
|
<span className="truncate">{departure.destination}</span>
|
||||||
</div>
|
</div>
|
||||||
<span className={`shrink-0 ml-3 ${alwaysOn ? '' : 'font-bold'}`}>{timeText}</span>
|
<span className={`shrink-0 ml-4 ${alwaysOn ? 'font-normal' : 'font-bold'}`}>{timeText}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,9 @@ import { useAlwaysOn } from '../../contexts/AlwaysOnContext';
|
||||||
|
|
||||||
export const SBBDepartureItem = ({
|
export const SBBDepartureItem = ({
|
||||||
departure,
|
departure,
|
||||||
rowHeight = 48,
|
|
||||||
showPlatform = false,
|
showPlatform = false,
|
||||||
}: {
|
}: {
|
||||||
departure: SBBDeparture;
|
departure: SBBDeparture;
|
||||||
rowHeight?: number;
|
|
||||||
showPlatform?: boolean;
|
showPlatform?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const alwaysOn = useAlwaysOn();
|
const alwaysOn = useAlwaysOn();
|
||||||
|
|
@ -16,31 +14,19 @@ export const SBBDepartureItem = ({
|
||||||
const diffMin = dep ? Math.round((dep.getTime() - Date.now()) / 60000) : null;
|
const diffMin = dep ? Math.round((dep.getTime() - Date.now()) / 60000) : null;
|
||||||
const timeText = diffMin === null ? '—' : diffMin <= 0 ? 'Now' : `${diffMin} min`;
|
const timeText = diffMin === null ? '—' : diffMin <= 0 ? 'Now' : `${diffMin} min`;
|
||||||
const delay = departure.stop.delay;
|
const delay = departure.stop.delay;
|
||||||
const fontSize = Math.max(11, rowHeight * 0.40);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={`flex justify-between items-center py-3 border-b border-zinc-700 text-2xl ${alwaysOn ? 'font-light' : ''}`}>
|
||||||
className="flex justify-between items-center border-b border-zinc-700 shrink-0"
|
<div className="flex items-center gap-4 min-w-0 overflow-hidden">
|
||||||
style={{ height: rowHeight, fontSize }}
|
<SBBLineBadge name={`${departure.category} ${departure.number}`} category={departure.category} />
|
||||||
>
|
|
||||||
<div className="flex items-center gap-3 min-w-0 overflow-hidden">
|
|
||||||
<SBBLineBadge
|
|
||||||
name={`${departure.category} ${departure.number}`}
|
|
||||||
category={departure.category}
|
|
||||||
size={rowHeight * 0.65}
|
|
||||||
/>
|
|
||||||
<span className="truncate">{departure.to}</span>
|
<span className="truncate">{departure.to}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={`flex items-center gap-2 shrink-0 ml-3 ${alwaysOn ? '' : 'font-bold'}`}>
|
<div className={`flex items-center gap-2 shrink-0 ml-4 ${alwaysOn ? 'font-normal' : 'font-bold'}`}>
|
||||||
{showPlatform && departure.stop.platform && (
|
{showPlatform && departure.stop.platform && (
|
||||||
<span className="text-gray-400 font-normal" style={{ fontSize: fontSize * 0.75 }}>
|
<span className="text-sm text-gray-400 font-normal">Pl.{departure.stop.platform}</span>
|
||||||
Pl.{departure.stop.platform}
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
{delay != null && delay > 0 && (
|
{delay != null && delay > 0 && (
|
||||||
<span className="text-orange-400 font-normal" style={{ fontSize: fontSize * 0.75 }}>
|
<span className="text-lg text-orange-400 font-normal">+{delay}'</span>
|
||||||
+{delay}'
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
<span>{timeText}</span>
|
<span>{timeText}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,12 @@
|
||||||
import { useSize } from '../../hooks/useSize';
|
import { useSize } from '../../hooks/useSize';
|
||||||
import { DepartureItem } from '../molecules/DepartureItem';
|
import { DepartureItem } from '../molecules/DepartureItem';
|
||||||
import { Departure } from '../../types';
|
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 = ({
|
export const DepartureList = ({
|
||||||
departures,
|
departures,
|
||||||
|
|
@ -11,46 +17,28 @@ export const DepartureList = ({
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
stationName?: string;
|
stationName?: string;
|
||||||
}) => {
|
}) => {
|
||||||
const { h, w, ref } = useSize();
|
const alwaysOn = useAlwaysOn();
|
||||||
const effectiveH = h || 200;
|
const { h, ref } = useSize();
|
||||||
|
const effectiveH = h || 300;
|
||||||
|
|
||||||
// Header sizing
|
const listH = effectiveH - (stationName ? HEADER_H : 0);
|
||||||
const headerH = stationName ? Math.max(24, Math.min(effectiveH * 0.17, 44)) : 0;
|
const maxRows = Math.max(1, Math.floor(listH / ROW_H));
|
||||||
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 visible = departures.slice(0, maxRows);
|
const visible = departures.slice(0, maxRows);
|
||||||
|
|
||||||
if (loading && departures.length === 0) {
|
|
||||||
return (
|
|
||||||
<div ref={ref} className="w-full h-full flex items-center">
|
|
||||||
<span className="text-gray-500 text-sm">Loading…</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref} className="w-full h-full flex flex-col overflow-hidden">
|
<div ref={ref} className="w-full h-full flex flex-col overflow-hidden">
|
||||||
{stationName && (
|
{stationName && (
|
||||||
<div
|
<h2 className={`text-xl uppercase tracking-wider shrink-0 pb-1 ${alwaysOn ? 'font-normal' : 'font-bold'}`}>
|
||||||
className="text-gray-200 uppercase tracking-wider font-bold shrink-0 flex items-center border-b border-zinc-600"
|
|
||||||
style={{ height: headerH, fontSize: headerFontSz }}
|
|
||||||
>
|
|
||||||
{stationName}
|
{stationName}
|
||||||
</div>
|
</h2>
|
||||||
)}
|
)}
|
||||||
{visible.length === 0 ? (
|
{loading && departures.length === 0 ? (
|
||||||
<span className="text-gray-500 mt-2 text-sm">No departures</span>
|
<p className="text-gray-500 text-xl mt-4">Loading…</p>
|
||||||
|
) : visible.length === 0 ? (
|
||||||
|
<p className="text-gray-500 text-xl mt-4">No departures</p>
|
||||||
) : (
|
) : (
|
||||||
visible.map((dep, i) => (
|
visible.map((dep, i) => (
|
||||||
<DepartureItem
|
<DepartureItem key={`${dep.line.designation}-${dep.scheduled}-${i}`} departure={dep} />
|
||||||
key={`${dep.line.designation}-${dep.scheduled}-${i}`}
|
|
||||||
departure={dep}
|
|
||||||
rowHeight={rowH}
|
|
||||||
/>
|
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
import { useSize } from '../../hooks/useSize';
|
import { useSize } from '../../hooks/useSize';
|
||||||
import { SBBDepartureItem } from '../molecules/SBBDepartureItem';
|
import { SBBDepartureItem } from '../molecules/SBBDepartureItem';
|
||||||
import { SBBDeparture } from '../../types';
|
import { SBBDeparture } from '../../types';
|
||||||
|
import { useAlwaysOn } from '../../contexts/AlwaysOnContext';
|
||||||
|
|
||||||
|
const ROW_H = 56;
|
||||||
|
const HEADER_H = 38;
|
||||||
|
|
||||||
export const SBBDepartureList = ({
|
export const SBBDepartureList = ({
|
||||||
departures,
|
departures,
|
||||||
|
|
@ -11,46 +15,32 @@ export const SBBDepartureList = ({
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
stationName?: string;
|
stationName?: string;
|
||||||
}) => {
|
}) => {
|
||||||
|
const alwaysOn = useAlwaysOn();
|
||||||
const { h, w, ref } = useSize();
|
const { h, w, ref } = useSize();
|
||||||
const effectiveH = h || 200;
|
const effectiveH = h || 300;
|
||||||
|
|
||||||
// Header sizing
|
const listH = effectiveH - (stationName ? HEADER_H : 0);
|
||||||
const headerH = stationName ? Math.max(24, Math.min(effectiveH * 0.17, 44)) : 0;
|
const maxRows = Math.max(1, Math.floor(listH / ROW_H));
|
||||||
const headerFontSz = Math.max(11, headerH * 0.55);
|
// Platform column only when the slot is wide enough to show it comfortably
|
||||||
|
const showPlatform = (w || 0) > 320;
|
||||||
// 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 visible = departures.slice(0, maxRows);
|
const visible = departures.slice(0, maxRows);
|
||||||
|
|
||||||
if (loading && departures.length === 0) {
|
|
||||||
return (
|
|
||||||
<div ref={ref} className="w-full h-full flex items-center">
|
|
||||||
<span className="text-gray-500 text-sm">Loading…</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref} className="w-full h-full flex flex-col overflow-hidden">
|
<div ref={ref} className="w-full h-full flex flex-col overflow-hidden">
|
||||||
{stationName && (
|
{stationName && (
|
||||||
<div
|
<h2 className={`text-xl uppercase tracking-wider shrink-0 pb-1 ${alwaysOn ? 'font-normal' : 'font-bold'}`}>
|
||||||
className="text-gray-200 uppercase tracking-wider font-bold shrink-0 flex items-center border-b border-zinc-600"
|
|
||||||
style={{ height: headerH, fontSize: headerFontSz }}
|
|
||||||
>
|
|
||||||
{stationName}
|
{stationName}
|
||||||
</div>
|
</h2>
|
||||||
)}
|
)}
|
||||||
{visible.length === 0 ? (
|
{loading && departures.length === 0 ? (
|
||||||
<span className="text-gray-500 mt-2 text-sm">No departures</span>
|
<p className="text-gray-500 text-xl mt-4">Loading…</p>
|
||||||
|
) : visible.length === 0 ? (
|
||||||
|
<p className="text-gray-500 text-xl mt-4">No departures</p>
|
||||||
) : (
|
) : (
|
||||||
visible.map((dep, i) => (
|
visible.map((dep, i) => (
|
||||||
<SBBDepartureItem
|
<SBBDepartureItem
|
||||||
key={`${dep.name}-${dep.stop.departure}-${i}`}
|
key={`${dep.name}-${dep.stop.departure}-${i}`}
|
||||||
departure={dep}
|
departure={dep}
|
||||||
rowHeight={rowH}
|
|
||||||
showPlatform={showPlatform}
|
showPlatform={showPlatform}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
|
|
|
||||||
|
|
@ -20,60 +20,38 @@ export const SpotifyWidget = () => {
|
||||||
const effectiveH = h || 100;
|
const effectiveH = h || 100;
|
||||||
const effectiveW = w || 300;
|
const effectiveW = w || 300;
|
||||||
|
|
||||||
// Progressive disclosure
|
// Progressive disclosure — text sizes are fixed, only visibility changes
|
||||||
const showLabel = effectiveH > 80;
|
const showAlbum = effectiveH > 110 && effectiveW > 260;
|
||||||
const showArtist = effectiveH > 65 || effectiveW > 240;
|
const showProgress = !alwaysOn && effectiveH > 90 && effectiveW > 200;
|
||||||
const showAlbum = effectiveH > 120 && effectiveW > 260;
|
const showTimes = showProgress && effectiveW > 280;
|
||||||
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);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref} className="w-full h-full flex flex-col justify-center overflow-hidden">
|
<div ref={ref} className="w-full h-full flex flex-col justify-center overflow-hidden">
|
||||||
{showLabel && (
|
<h2 className="text-sm uppercase tracking-wider text-gray-400 mb-1 font-normal">Now Playing</h2>
|
||||||
<div className="text-gray-400 uppercase tracking-wider" style={{ fontSize: labelFS, marginBottom: '0.3em' }}>
|
|
||||||
Now Playing
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{data?.playing && data.track ? (
|
{data?.playing && data.track ? (
|
||||||
<>
|
<>
|
||||||
<div
|
<p className={`text-2xl leading-tight truncate ${alwaysOn ? 'font-normal' : 'font-bold'}`}>
|
||||||
className="leading-tight truncate"
|
|
||||||
style={{ fontSize: trackFS, fontWeight: alwaysOn ? 400 : 700 }}
|
|
||||||
>
|
|
||||||
{data.track.name}
|
{data.track.name}
|
||||||
</div>
|
</p>
|
||||||
{showArtist && (
|
<p className="text-lg text-gray-300 mt-0.5 truncate">{data.track.artist}</p>
|
||||||
<div className="text-gray-300 truncate" style={{ fontSize: artistFS, marginTop: '0.15em' }}>
|
|
||||||
{data.track.artist}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{showAlbum && (
|
{showAlbum && (
|
||||||
<div className="text-gray-500 truncate" style={{ fontSize: albumFS, marginTop: '0.1em' }}>
|
<p className="text-sm text-gray-500 truncate">{data.track.album}</p>
|
||||||
{data.track.album}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
{showProgress && (
|
{showProgress && (
|
||||||
<div className="flex items-center gap-2" style={{ marginTop: '0.4em' }}>
|
<div className="mt-1.5 flex items-center gap-2">
|
||||||
{showProgressTime && (
|
{showTimes && (
|
||||||
<span className="text-gray-500 font-mono shrink-0" style={{ fontSize: albumFS }}>
|
<span className="text-xs text-gray-500 font-mono w-10 shrink-0">
|
||||||
{fmtMs(data.track.progressMs)}
|
{fmtMs(data.track.progressMs)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<div className="flex-1 bg-zinc-800 relative" style={{ height: barH }}>
|
<div className="flex-1 h-1.5 bg-zinc-800 relative">
|
||||||
<div
|
<div
|
||||||
className="absolute inset-y-0 left-0 bg-white"
|
className="absolute inset-y-0 left-0 bg-white"
|
||||||
style={{ width: `${(data.track.progressMs / data.track.durationMs) * 100}%` }}
|
style={{ width: `${(data.track.progressMs / data.track.durationMs) * 100}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{showProgressTime && (
|
{showTimes && (
|
||||||
<span className="text-gray-500 font-mono shrink-0 text-right" style={{ fontSize: albumFS }}>
|
<span className="text-xs text-gray-500 font-mono w-10 shrink-0 text-right">
|
||||||
{fmtMs(data.track.durationMs)}
|
{fmtMs(data.track.durationMs)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
@ -81,7 +59,7 @@ export const SpotifyWidget = () => {
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-gray-500" style={{ fontSize: artistFS }}>Nothing playing</div>
|
<p className="text-gray-500 text-lg">Nothing playing</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -10,17 +10,17 @@ interface SystemData {
|
||||||
hostname: string;
|
hostname: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MetricRow = ({
|
const Bar = ({ percent }: { percent: number }) => (
|
||||||
label, percent, detail, rowH, barH, fontSize,
|
<div className="flex-1 h-1.5 bg-zinc-800 relative">
|
||||||
}: {
|
<div className="absolute inset-y-0 left-0 bg-white" style={{ width: `${percent}%` }} />
|
||||||
label: string; percent: number; detail: string;
|
</div>
|
||||||
rowH: number; barH: number; fontSize: number;
|
);
|
||||||
}) => (
|
|
||||||
<div className="flex items-center gap-2 font-mono shrink-0" style={{ height: rowH, fontSize }}>
|
const Row = ({ label, percent, detail }: { label: string; percent: number; detail: string }) => (
|
||||||
<span className="text-gray-400 shrink-0" style={{ width: '3.2em' }}>{label}</span>
|
<div className="flex items-center gap-3 text-sm font-mono">
|
||||||
<div className="flex-1 min-w-0 bg-zinc-800 relative" style={{ height: barH }}>
|
<span className="w-10 text-gray-400 shrink-0">{label}</span>
|
||||||
<div className="absolute inset-y-0 left-0 bg-white" style={{ width: `${percent}%` }} />
|
<Bar percent={percent} />
|
||||||
</div>
|
<span className="w-8 text-right shrink-0">{percent}%</span>
|
||||||
<span className="text-gray-500 shrink-0">{detail}</span>
|
<span className="text-gray-500 shrink-0">{detail}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -39,52 +39,27 @@ export const SystemWidget = () => {
|
||||||
const effectiveH = h || 120;
|
const effectiveH = h || 120;
|
||||||
const effectiveW = w || 300;
|
const effectiveW = w || 300;
|
||||||
|
|
||||||
// Progressive disclosure
|
// Progressive disclosure — fixed text-sm rows, just show more when taller/wider
|
||||||
const showHostname = effectiveH > 55;
|
const showDisk = effectiveH > 95;
|
||||||
const showDisk = effectiveH > 95;
|
const showUptime = effectiveH > 110 || effectiveW > 450;
|
||||||
const showUptime = showHostname && (effectiveH > 120 || effectiveW > 450);
|
const showLoadDetail = effectiveH > 140 && effectiveW > 380;
|
||||||
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));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref} className="w-full h-full flex flex-col justify-center overflow-hidden" style={{ gap: 5 }}>
|
<div ref={ref} className="w-full h-full flex flex-col justify-center gap-3 overflow-hidden">
|
||||||
{showHostname && (
|
<div className="flex justify-between items-baseline">
|
||||||
<div className="flex justify-between items-baseline shrink-0" style={{ height: hostH }}>
|
<span className="font-bold text-lg">{data.hostname}</span>
|
||||||
<span className="font-bold truncate" style={{ fontSize: hostFontSz }}>{data.hostname}</span>
|
{showUptime && (
|
||||||
{showUptime && (
|
<span className="text-gray-500 text-sm font-mono">up {data.uptime}</span>
|
||||||
<span className="text-gray-500 font-mono shrink-0 ml-2" style={{ fontSize: hostFontSz * 0.65 }}>
|
)}
|
||||||
up {data.uptime}
|
</div>
|
||||||
</span>
|
<Row
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<MetricRow
|
|
||||||
label="CPU"
|
label="CPU"
|
||||||
percent={data.cpu.percent}
|
percent={data.cpu.percent}
|
||||||
detail={showLoadDetail ? data.cpu.load.join(' ') : `${data.cpu.percent}%`}
|
detail={showLoadDetail ? `load ${data.cpu.load.join(' ')}` : `load ${data.cpu.load[0]}`}
|
||||||
rowH={rowH} barH={barH} fontSize={fontSize}
|
|
||||||
/>
|
|
||||||
<MetricRow
|
|
||||||
label="MEM"
|
|
||||||
percent={data.memory.percent}
|
|
||||||
detail={`${data.memory.usedGb}/${data.memory.totalGb}G`}
|
|
||||||
rowH={rowH} barH={barH} fontSize={fontSize}
|
|
||||||
/>
|
/>
|
||||||
|
<Row label="MEM" percent={data.memory.percent} detail={`${data.memory.usedGb} / ${data.memory.totalGb} GB`} />
|
||||||
{showDisk && (
|
{showDisk && (
|
||||||
<MetricRow
|
<Row label="DISK" percent={data.disk.percent} detail={`${data.disk.usedGb} / ${data.disk.totalGb} GB`} />
|
||||||
label="DISK"
|
|
||||||
percent={data.disk.percent}
|
|
||||||
detail={`${data.disk.usedGb}/${data.disk.totalGb}G`}
|
|
||||||
rowH={rowH} barH={barH} fontSize={fontSize}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -22,18 +22,17 @@ const MOON_CHAR: Record<string, string> = {
|
||||||
|
|
||||||
function getAsciiKey(desc: string): string {
|
function getAsciiKey(desc: string): string {
|
||||||
const d = desc.toLowerCase();
|
const d = desc.toLowerCase();
|
||||||
if (d.includes('thunder')) return 'thunder';
|
if (d.includes('thunder')) return 'thunder';
|
||||||
if (d.includes('snow')) return 'snow';
|
if (d.includes('snow')) return 'snow';
|
||||||
if (d.includes('sleet')) return 'sleet';
|
if (d.includes('sleet')) return 'sleet';
|
||||||
if (d.includes('drizzle')) return 'drizzle';
|
if (d.includes('drizzle')) return 'drizzle';
|
||||||
if (d.includes('rain')) return 'rain';
|
if (d.includes('rain')) return 'rain';
|
||||||
if (d.includes('fog') || d.includes('mist')) return 'fog';
|
if (d.includes('fog') || d.includes('mist')) return 'fog';
|
||||||
if (d.includes('overcast')) return 'overcast';
|
if (d.includes('overcast')) return 'overcast';
|
||||||
if (d.includes('mostly cloudy') || d.includes('very')) return 'cloudy';
|
if (d.includes('mostly cloudy') || d.includes('very')) return 'cloudy';
|
||||||
if (d.includes('partly') || d.includes('mostly sunny')) return 'partlycloudy';
|
if (d.includes('partly') || d.includes('mostly sunny')) return 'partlycloudy';
|
||||||
if (d.includes('cloudy')) return 'cloudy';
|
if (d.includes('cloudy')) return 'cloudy';
|
||||||
if (d.includes('sunny') || d.includes('clear')) return 'sunny';
|
return 'sunny';
|
||||||
return 'cloudy';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WeatherWidget = () => {
|
export const WeatherWidget = () => {
|
||||||
|
|
@ -44,9 +43,6 @@ export const WeatherWidget = () => {
|
||||||
refetchInterval: 10 * 60 * 1000,
|
refetchInterval: 10 * 60 * 1000,
|
||||||
});
|
});
|
||||||
|
|
||||||
const effectiveH = h || 100;
|
|
||||||
const effectiveW = w || 350;
|
|
||||||
|
|
||||||
if (isError || !data || (data as any).error) {
|
if (isError || !data || (data as any).error) {
|
||||||
return (
|
return (
|
||||||
<div ref={ref} className="w-full h-full flex items-center">
|
<div ref={ref} className="w-full h-full flex items-center">
|
||||||
|
|
@ -55,49 +51,43 @@ export const WeatherWidget = () => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Progressive disclosure based on available space
|
const effectiveH = h || 80;
|
||||||
const showAscii = effectiveH > 200 && effectiveW > 380;
|
const effectiveW = w || 350;
|
||||||
const showHourly = effectiveH > 140 && effectiveW > 260;
|
|
||||||
const showSunMoon = effectiveH > 95 || effectiveW > 460;
|
|
||||||
|
|
||||||
// Font sizes derived from container
|
// Progressive disclosure — everything is the same size, just more/less shown
|
||||||
const tempFontSize = Math.min(effectiveH * (showAscii ? 0.34 : 0.42), 84);
|
const showAscii = effectiveH > 180 && effectiveW > 380;
|
||||||
const descFontSize = Math.max(12, tempFontSize * 0.36);
|
const showHourly = effectiveH > 120 && effectiveW > 260;
|
||||||
const metaFontSize = Math.max(10, tempFontSize * 0.27);
|
const showSunMoon = effectiveH > 80 || effectiveW > 460;
|
||||||
const asciiFontSize = Math.max(9, effectiveH / 8);
|
const maxHours = effectiveW > 500 ? 6 : effectiveW > 380 ? 4 : 3;
|
||||||
const maxHours = effectiveW > 500 ? 6 : effectiveW > 380 ? 4 : 3;
|
|
||||||
|
|
||||||
const art = ASCII[getAsciiKey(data.description)] ?? ASCII.cloudy;
|
const art = ASCII[getAsciiKey(data.description)] ?? ASCII.cloudy;
|
||||||
const moon = MOON_CHAR[data.moonPhase] ?? data.moonPhase;
|
const moon = MOON_CHAR[data.moonPhase] ?? data.moonPhase;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref} className="w-full h-full flex items-center gap-5 overflow-hidden">
|
<div ref={ref} className="w-full h-full flex items-center gap-4 overflow-hidden">
|
||||||
{showAscii && (
|
{showAscii && (
|
||||||
<pre
|
<pre className="text-gray-300 text-[11px] leading-tight select-none font-mono shrink-0">
|
||||||
className="text-gray-300 shrink-0 select-none font-mono leading-tight"
|
|
||||||
style={{ fontSize: asciiFontSize }}
|
|
||||||
>
|
|
||||||
{art.join('\n')}
|
{art.join('\n')}
|
||||||
</pre>
|
</pre>
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-col gap-1 min-w-0 overflow-hidden">
|
<div className="flex flex-col gap-1 min-w-0">
|
||||||
<div className="flex items-baseline gap-3 flex-wrap">
|
<div className="flex items-baseline gap-3 flex-wrap">
|
||||||
<span style={{ fontSize: tempFontSize, fontWeight: 700, lineHeight: 1 }}>{data.temperature}°C</span>
|
<span className="text-4xl font-bold">{data.temperature}°C</span>
|
||||||
<span style={{ fontSize: descFontSize }} className="text-gray-300">{data.description}</span>
|
<span className="text-lg text-gray-300">{data.description}</span>
|
||||||
</div>
|
</div>
|
||||||
{showSunMoon && (
|
{showSunMoon && (
|
||||||
<div className="flex flex-wrap gap-3 text-gray-400 font-mono" style={{ fontSize: metaFontSize }}>
|
<div className="flex flex-wrap gap-4 text-sm text-gray-400 font-mono">
|
||||||
<span>↑ {data.sunrise}</span>
|
<span>↑ {data.sunrise}</span>
|
||||||
<span>↓ {data.sunset}</span>
|
<span>↓ {data.sunset}</span>
|
||||||
<span title={data.moonPhase}>{moon} {data.moonPhase}</span>
|
<span title={data.moonPhase}>{moon} {data.moonPhase}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{showHourly && data.hourly.length > 0 && (
|
{showHourly && data.hourly.length > 0 && (
|
||||||
<div className="flex gap-4 mt-1">
|
<div className="flex gap-3 mt-1">
|
||||||
{data.hourly.slice(0, maxHours).map((entry, i) => (
|
{data.hourly.slice(0, maxHours).map((entry, i) => (
|
||||||
<div key={i} className="text-center shrink-0">
|
<div key={i} className="text-center shrink-0">
|
||||||
<div style={{ fontSize: metaFontSize * 0.85 }} className="text-gray-500">{entry.time}</div>
|
<div className="text-xs text-gray-500">{entry.time}</div>
|
||||||
<div style={{ fontSize: metaFontSize }} className="font-bold">{entry.temp}°</div>
|
<div className="text-sm font-bold">{entry.temp}°</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue