Skip to content

Commit dafad83

Browse files
committed
Merge branch 'claude/intelligent-elbakyan-a83924' with ui redesign
2 parents 3ae8134 + dbc69ee commit dafad83

14 files changed

Lines changed: 1046 additions & 428 deletions

index.html

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,18 @@
55
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
66
<title>MeteoCompare — multi-model weather forecast</title>
77
<meta name="description" content="Multi-model weather forecast comparison with a weighted aggregate and confidence score. Powered by open-meteo.com." />
8+
9+
<!-- Type system: a variable serif for display, a humanist sans for body,
10+
and a mono with slashed-zero for all data. Loaded via the Google Fonts
11+
CSS API so the hashed asset URLs are always current. -->
12+
<link rel="preconnect" href="https://fonts.googleapis.com" />
13+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
14+
<link
15+
rel="stylesheet"
16+
href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght,SOFT@9..144,200..900,30..100&family=Instrument+Sans:wght@400..700&family=JetBrains+Mono:wght@300..700&display=swap"
17+
/>
818
</head>
9-
<body class="bg-slate-950 text-slate-100">
19+
<body>
1020
<div id="app"></div>
1121
<script type="module" src="/src/main.ts"></script>
1222
</body>

src/components/AggregateSummary.vue

Lines changed: 103 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,15 @@ const props = defineProps<{
2020
const { current, isFavourite, toggleFavourite } = useLocation();
2121
const starred = computed(() => isFavourite(current.value));
2222
23-
const { formatTemp, formatPercent } = useUnits();
23+
const { temp, formatTemp, formatPercent } = useUnits();
2424
25-
const currentTemp = computed(() => props.raw.current.temperature_2m ?? null);
25+
const tempUnitLetter = computed(() => (temp.value === "f" ? "F" : "C"));
26+
27+
const currentTemp = computed(() => {
28+
const t = props.raw.current.temperature_2m;
29+
if (t == null) return null;
30+
return temp.value === "f" ? (t * 9) / 5 + 32 : t;
31+
});
2632
const currentCode = computed(() => Number(props.raw.current.weather_code ?? 0));
2733
const currentIsDay = computed(() => (props.raw.current.is_day ?? 1) === 1);
2834
const todayHigh = computed(() => props.daily.series.temperature_2m_max[0]?.value ?? null);
@@ -54,52 +60,111 @@ const dayLength = computed(() => {
5460
const m = totalMin % 60;
5561
return `${h}h ${m.toString().padStart(2, "0")}m`;
5662
});
63+
64+
// Decimal portion of the current temperature is rendered smaller so the
65+
// huge serif number reads as a typographic display rather than a flat number.
66+
const tempWhole = computed(() => {
67+
const t = currentTemp.value;
68+
if (t == null || !Number.isFinite(t)) return "";
69+
return `${Math.round(t)}`;
70+
});
5771
</script>
5872

5973
<template>
60-
<section class="rounded-2xl bg-slate-900/60 p-6 ring-1 ring-slate-800 sm:p-8">
61-
<div class="flex flex-col gap-6 sm:flex-row sm:items-center sm:justify-between">
62-
<div class="flex items-center gap-5">
63-
<WeatherIcon :code="currentCode" :is-day="currentIsDay" size="4.5rem" />
64-
<div>
65-
<div class="flex items-center gap-1.5">
66-
<div class="text-sm tracking-wider text-slate-400 uppercase">{{ locationName }}</div>
67-
<button
68-
type="button"
69-
class="-m-1 rounded-md p-1 text-base leading-none transition-colors"
70-
:class="starred ? 'text-amber-300 hover:text-amber-200' : 'text-slate-500 hover:text-amber-300'"
71-
:title="starred ? 'Remove from favourites' : 'Save as favourite'"
72-
:aria-pressed="starred"
73-
@click="toggleFavourite(current)"
74-
>
75-
{{ starred ? "★" : "☆" }}
76-
</button>
74+
<section class="border-ink-700 bg-ink-900/60 border p-4 sm:p-5">
75+
<!-- Header bar: same eyebrow + hairline-divider pattern as the chart
76+
card below, so the two stack as a single typographic system. -->
77+
<div class="border-ink-700 mb-3 flex items-center justify-between gap-3 border-b pb-3">
78+
<h2 class="eyebrow">Current conditions</h2>
79+
<span class="text-paper-400 font-mono text-[10px] tracking-[0.12em] whitespace-nowrap tabular-nums">
80+
<span class="text-sodium-300">●</span>
81+
{{ lastUpdated.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) }}
82+
<span class="hidden sm:inline">
83+
<span class="text-paper-500">·</span>
84+
{{ lastUpdated.toLocaleDateString([], { day: "2-digit", month: "short" }) }}
85+
</span>
86+
</span>
87+
</div>
88+
89+
<!-- Location ----------------------------------------------------- -->
90+
<div class="flex items-center gap-1.5">
91+
<span class="text-paper-100 text-sm font-medium tracking-wide">{{ locationName }}</span>
92+
<button
93+
type="button"
94+
class="-m-1 p-1 text-sm leading-none transition-colors"
95+
:class="starred ? 'text-sodium-300 hover:text-sodium-200' : 'text-paper-400 hover:text-sodium-300'"
96+
:title="starred ? 'Remove from favourites' : 'Save as favourite'"
97+
:aria-pressed="starred"
98+
@click="toggleFavourite(current)"
99+
>
100+
{{ starred ? "★" : "☆" }}
101+
</button>
102+
</div>
103+
104+
<!-- Reading + readouts ------------------------------------------ -->
105+
<div class="mt-2 grid gap-4 sm:mt-3 sm:grid-cols-[1fr_auto] sm:items-center sm:gap-8">
106+
<div class="flex items-center gap-4 sm:gap-5">
107+
<div class="relative shrink-0">
108+
<!-- A halo behind the weather icon so it reads as a "lit" panel
109+
element. -->
110+
<div class="bg-sodium-300/10 absolute inset-0 -m-2 rounded-full blur-2xl" aria-hidden="true" />
111+
<WeatherIcon :code="currentCode" :is-day="currentIsDay" size="2.75rem" class="relative" />
112+
</div>
113+
114+
<div class="min-w-0">
115+
<div class="flex items-baseline gap-1">
116+
<span class="sodium-glow text-paper-50 font-mono text-5xl leading-none font-light tabular-nums sm:text-6xl">
117+
{{ tempWhole }}
118+
</span>
119+
<span class="text-sodium-300 font-mono text-xl leading-none font-light tabular-nums sm:text-2xl"> °{{ tempUnitLetter }} </span>
77120
</div>
78-
<div class="mt-0.5 flex items-baseline gap-3">
79-
<div class="text-5xl font-semibold tabular-nums sm:text-6xl">{{ formatTemp(currentTemp, 0) }}</div>
80-
<div class="hidden text-slate-300 sm:block">{{ weatherLabel(currentCode) }}</div>
121+
<div class="text-paper-200 mt-1.5 font-mono text-xs tracking-wide">
122+
{{ weatherLabel(currentCode) }}
81123
</div>
82-
<div class="mt-1 text-sm text-slate-300 sm:hidden">{{ weatherLabel(currentCode) }}</div>
83124
</div>
84125
</div>
85126

86-
<div class="flex flex-col items-start gap-2 sm:items-end">
127+
<!-- Two instrument modules: temperature/precip and the solar block.
128+
They sit side by side when the card is wide enough and stack
129+
otherwise. -->
130+
<div class="flex flex-col gap-2.5 sm:items-end">
87131
<ConfidenceBadge :value="todayConfidence" />
88-
<div class="text-sm text-slate-300 tabular-nums">
89-
<span class="text-rose-300">{{ formatTemp(todayHigh, 0) }}</span>
90-
<span class="mx-2 text-slate-500">·</span>
91-
<span class="text-sky-300">{{ formatTemp(todayLow, 0) }}</span>
92-
<template v-if="todayPrecipProb != null && todayPrecipProb > 0">
93-
<span class="mx-2 text-slate-500">·</span>
94-
<span>💧 {{ formatPercent(todayPrecipProb) }}</span>
95-
</template>
96-
</div>
97-
<div v-if="sunrise || sunset" class="flex items-center gap-3 text-xs text-slate-400 tabular-nums">
98-
<span title="Sunrise"><span class="text-amber-300">↑</span> {{ formatClock(sunrise) }}</span>
99-
<span title="Sunset"><span class="text-orange-400">↓</span> {{ formatClock(sunset) }}</span>
100-
<span v-if="dayLength" class="text-slate-500" title="Day length">· {{ dayLength }}</span>
132+
133+
<div class="flex flex-col gap-2.5 sm:flex-row sm:gap-3">
134+
<!-- High / Low / Precip -->
135+
<div class="border-ink-700 bg-ink-950/60 grid grid-cols-3 gap-x-4 gap-y-1 border px-3 py-2 font-mono tabular-nums">
136+
<div class="flex flex-col gap-0.5">
137+
<span class="text-paper-400 text-[10px] tracking-wide">High</span>
138+
<span class="text-heat-300 text-sm">{{ formatTemp(todayHigh, 0) }}</span>
139+
</div>
140+
<div class="flex flex-col gap-0.5">
141+
<span class="text-paper-400 text-[10px] tracking-wide">Low</span>
142+
<span class="text-cold-300 text-sm">{{ formatTemp(todayLow, 0) }}</span>
143+
</div>
144+
<div class="flex flex-col gap-0.5">
145+
<span class="text-paper-400 text-[10px] tracking-wide">Precip</span>
146+
<span class="text-sm" :class="todayPrecipProb != null && todayPrecipProb > 0 ? 'text-rain-300' : 'text-paper-400'">
147+
{{ todayPrecipProb != null && todayPrecipProb > 0 ? formatPercent(todayPrecipProb) : "—" }}
148+
</span>
149+
</div>
150+
</div>
151+
152+
<!-- Sunrise / Sunset / Total sun -->
153+
<div v-if="sunrise || sunset" class="border-ink-700 bg-ink-950/60 grid grid-cols-3 gap-x-4 gap-y-1 border px-3 py-2 font-mono tabular-nums">
154+
<div class="flex flex-col gap-0.5">
155+
<span class="text-paper-400 text-[10px] tracking-wide">Sunrise</span>
156+
<span class="text-sodium-300 text-sm">{{ formatClock(sunrise) }}</span>
157+
</div>
158+
<div class="flex flex-col gap-0.5">
159+
<span class="text-paper-400 text-[10px] tracking-wide">Sunset</span>
160+
<span class="text-heat-300 text-sm">{{ formatClock(sunset) }}</span>
161+
</div>
162+
<div class="flex flex-col gap-0.5">
163+
<span class="text-paper-400 text-[10px] tracking-wide">Total sun</span>
164+
<span class="text-paper-200 text-sm">{{ dayLength ?? "—" }}</span>
165+
</div>
166+
</div>
101167
</div>
102-
<div class="text-xs text-slate-500" :title="lastUpdated.toString()">Updated {{ lastUpdated.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) }}</div>
103168
</div>
104169
</div>
105170
</section>

src/components/ConfidenceBadge.vue

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,28 +15,57 @@ const props = withDefaults(
1515
1616
const tier = computed(() => confidenceTier(props.value));
1717
18+
// Each tier gets its own typographic register: sodium for "mid" (the neutral
19+
// instrument-default), sage for "high", coral for "low".
1820
const TONE_BY_TIER = {
19-
high: "bg-emerald-500/15 text-emerald-300 ring-emerald-500/30",
20-
mid: "bg-amber-500/15 text-amber-200 ring-amber-500/30",
21-
low: "bg-rose-500/15 text-rose-200 ring-rose-500/30",
21+
high: {
22+
bar: "bg-confidence-high",
23+
text: "text-confidence-high",
24+
ring: "border-confidence-high/40",
25+
glow: "shadow-[0_0_12px_rgba(155,184,122,0.25)]",
26+
},
27+
mid: {
28+
bar: "bg-sodium-300",
29+
text: "text-sodium-200",
30+
ring: "border-sodium-300/40",
31+
glow: "shadow-[0_0_12px_rgba(245,185,66,0.25)]",
32+
},
33+
low: {
34+
bar: "bg-heat-400",
35+
text: "text-heat-300",
36+
ring: "border-heat-400/40",
37+
glow: "shadow-[0_0_12px_rgba(232,130,107,0.25)]",
38+
},
2239
} as const;
2340
2441
const tone = computed(() => TONE_BY_TIER[tier.value]);
2542
2643
const percent = computed(() => Math.round(props.value * 100));
27-
const sizing = computed(() => (props.size === "sm" ? "text-xs px-2 py-0.5" : "text-sm px-2.5 py-1"));
44+
const sizing = computed(() => (props.size === "sm" ? "text-[10px] px-2 py-0.5 gap-1.5" : "text-[11px] px-2.5 py-1 gap-2"));
45+
46+
// 12-segment dial: the meter reads like a strip from a vintage signal-strength
47+
// indicator. Each segment is either "lit" or not.
48+
const SEGMENTS = 12;
49+
const litSegments = computed(() => Math.max(0, Math.min(SEGMENTS, Math.round(props.value * SEGMENTS))));
50+
51+
// Default label: the md size (used in the hero) carries the word "Agreement"
52+
// so the segmented meter is introduced as a known UI element. The sm size
53+
// (used in the daily outlook cards) drops the word and shows just the percent
54+
// — by the time a user reaches the outlook they've seen the badge labelled.
55+
const defaultLabel = computed(() => (props.size === "sm" ? `${percent.value}%` : `Agreement ${percent.value}%`));
2856
</script>
2957

3058
<template>
31-
<span class="inline-flex items-center gap-1 rounded-full font-medium tabular-nums ring-1" :class="[tone, sizing]" :title="`Model agreement: ${percent}%`">
32-
<span
33-
class="size-1.5 rounded-full"
34-
:class="{
35-
'bg-emerald-400': tier === 'high',
36-
'bg-amber-400': tier === 'mid',
37-
'bg-rose-400': tier === 'low',
38-
}"
39-
/>
40-
{{ label ?? `${percent}% confidence` }}
59+
<span class="bg-ink-950/60 inline-flex items-center border font-mono tracking-wide tabular-nums" :class="[tone.ring, sizing]" :title="`Model agreement: ${percent}%`">
60+
<!-- Segmented meter -->
61+
<span class="flex items-center gap-px" aria-hidden="true">
62+
<span
63+
v-for="i in SEGMENTS"
64+
:key="i"
65+
class="block w-[3px]"
66+
:class="[size === 'sm' ? 'h-2' : 'h-2.5', i <= litSegments ? `${tone.bar} ${i === litSegments ? tone.glow : ''}` : 'bg-ink-700']"
67+
/>
68+
</span>
69+
<span :class="tone.text">{{ label ?? defaultLabel }}</span>
4170
</span>
4271
</template>

src/components/DailyStrip.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ const days = computed<DayRow[]>(() =>
4949

5050
<template>
5151
<section>
52-
<h2 class="mb-3 text-sm font-medium tracking-wider text-slate-300 uppercase">10-day outlook</h2>
53-
<div class="-mx-2 flex snap-x gap-3 overflow-x-auto px-2 py-1">
52+
<h2 class="eyebrow mb-3">10-day outlook</h2>
53+
<div class="-mx-2 flex snap-x gap-2 overflow-x-auto px-2 pt-1 pb-3">
5454
<DayCard
5555
v-for="(d, i) in days"
5656
:key="d.date"

0 commit comments

Comments
 (0)