fix(briefing): 2-column layout with table-based weather forecast
Collapse briefing from 3-column to 2-column grid (chat + sidebar). Weather moves to top of news column, forecast uses an HTML table so rows align cleanly. Drop verbose condition text from forecast days (emoji already conveys it). Switch CI node_modules cache to npm download cache to avoid tar size error from onnxruntime-web. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -68,19 +68,14 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
# Cache node_modules directly (not the npm download cache).
|
- name: Cache npm download cache
|
||||||
# npm ci still has to extract + link every module even with a
|
|
||||||
# warm download cache, which is where the real time goes. Caching
|
|
||||||
# the output directory lets us skip npm ci entirely on hits.
|
|
||||||
- name: Cache node_modules
|
|
||||||
id: npm-cache
|
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: frontend/node_modules
|
path: ~/.npm
|
||||||
key: node-modules-${{ hashFiles('frontend/package-lock.json') }}
|
key: npm-cache-${{ hashFiles('frontend/package-lock.json') }}
|
||||||
|
restore-keys: npm-cache-
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
if: steps.npm-cache.outputs.cache-hit != 'true'
|
|
||||||
run: npm ci
|
run: npm ci
|
||||||
working-directory: frontend
|
working-directory: frontend
|
||||||
|
|
||||||
|
|||||||
@@ -85,22 +85,30 @@ const fetchedAtLabel = computed(() => {
|
|||||||
Today: {{ weather.today_high }}° / {{ weather.today_low }}°
|
Today: {{ weather.today_high }}° / {{ weather.today_low }}°
|
||||||
<span v-if="tempDelta" class="weather-delta"> · {{ tempDelta }}</span>
|
<span v-if="tempDelta" class="weather-delta"> · {{ tempDelta }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="weather-forecast" v-if="weather.forecast.length">
|
<table class="weather-forecast" v-if="weather.forecast.length">
|
||||||
<div v-for="day in weather.forecast" :key="day.day" class="weather-forecast-day">
|
<thead>
|
||||||
<span class="forecast-day-name">{{ day.day }}</span>
|
<tr>
|
||||||
<span class="forecast-icon">{{ weatherIcon(day.condition) }}</span>
|
<th></th>
|
||||||
<span class="forecast-condition">{{ day.condition }}</span>
|
<th></th>
|
||||||
<span class="forecast-temps">{{ day.high }}° / {{ day.low }}°</span>
|
<th>Hi / Lo</th>
|
||||||
<span v-if="day.precip_probability != null && day.precip_probability > 0" class="forecast-precip">
|
<th>💧</th>
|
||||||
💧 {{ day.precip_probability }}%
|
<th>💨 {{ weather.wind_unit ?? 'km/h' }}</th>
|
||||||
</span>
|
</tr>
|
||||||
<span v-else-if="day.precip_mm != null && day.precip_mm > 0" class="forecast-precip">
|
</thead>
|
||||||
💧 {{ day.precip_mm.toFixed(1) }}mm
|
<tbody>
|
||||||
</span>
|
<tr v-for="day in weather.forecast" :key="day.day">
|
||||||
<span v-else class="forecast-precip forecast-precip--dry">💧 —</span>
|
<td class="forecast-day-name">{{ day.day }}</td>
|
||||||
<span class="forecast-wind">💨 {{ day.windspeed_max }} {{ weather.wind_unit ?? 'km/h' }}</span>
|
<td class="forecast-icon">{{ weatherIcon(day.condition) }}</td>
|
||||||
</div>
|
<td class="forecast-temps">{{ day.high }}° / {{ day.low }}°</td>
|
||||||
</div>
|
<td class="forecast-precip" :class="{ 'forecast-precip--dry': !(day.precip_probability != null && day.precip_probability > 0) && !(day.precip_mm != null && day.precip_mm > 0) }">
|
||||||
|
<template v-if="day.precip_probability != null && day.precip_probability > 0">{{ day.precip_probability }}%</template>
|
||||||
|
<template v-else-if="day.precip_mm != null && day.precip_mm > 0">{{ day.precip_mm.toFixed(1) }}mm</template>
|
||||||
|
<template v-else>—</template>
|
||||||
|
</td>
|
||||||
|
<td class="forecast-wind">{{ day.windspeed_max }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="weather-card weather-unavailable">
|
<div v-else class="weather-card weather-unavailable">
|
||||||
Weather data unavailable — will retry at next slot.
|
Weather data unavailable — will retry at next slot.
|
||||||
@@ -169,20 +177,31 @@ const fetchedAtLabel = computed(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.weather-forecast {
|
.weather-forecast {
|
||||||
display: flex;
|
width: 100%;
|
||||||
gap: 0.75rem;
|
border-collapse: collapse;
|
||||||
overflow-x: auto;
|
margin-top: 0.75rem;
|
||||||
padding-top: 0.75rem;
|
|
||||||
border-top: 1px solid var(--color-border);
|
border-top: 1px solid var(--color-border);
|
||||||
|
font-size: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.weather-forecast-day {
|
.weather-forecast thead th {
|
||||||
display: flex;
|
font-size: 0.68rem;
|
||||||
flex-direction: column;
|
font-weight: 600;
|
||||||
align-items: center;
|
color: var(--color-text-muted);
|
||||||
gap: 0.25rem;
|
text-align: right;
|
||||||
min-width: 4.5rem;
|
padding: 0.5rem 0.4rem 0.25rem;
|
||||||
font-size: 0.8rem;
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-forecast thead th:first-child,
|
||||||
|
.weather-forecast thead th:nth-child(2) {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-forecast tbody td {
|
||||||
|
padding: 0.3rem 0.4rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.forecast-day-name {
|
.forecast-day-name {
|
||||||
@@ -190,26 +209,16 @@ const fetchedAtLabel = computed(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.forecast-icon {
|
.forecast-icon {
|
||||||
font-size: 1.2rem;
|
font-size: 1.1rem;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.forecast-condition {
|
|
||||||
font-size: 0.7rem;
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
text-align: center;
|
|
||||||
line-height: 1.2;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.forecast-temps {
|
.forecast-temps {
|
||||||
white-space: nowrap;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.forecast-precip,
|
.forecast-precip {
|
||||||
.forecast-wind {
|
text-align: right;
|
||||||
font-size: 0.72rem;
|
|
||||||
white-space: nowrap;
|
|
||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,6 +226,11 @@ const fetchedAtLabel = computed(() => {
|
|||||||
opacity: 0.35;
|
opacity: 0.35;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.forecast-wind {
|
||||||
|
text-align: right;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
.weather-unavailable {
|
.weather-unavailable {
|
||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
|
|||||||
@@ -261,22 +261,7 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- Left column: Weather -->
|
<!-- Left column: Chat -->
|
||||||
<div class="briefing-left">
|
|
||||||
<div class="panel-label">Weather</div>
|
|
||||||
<!-- Forecast card -->
|
|
||||||
<template v-if="weatherData.length">
|
|
||||||
<WeatherCard
|
|
||||||
v-for="loc in weatherData"
|
|
||||||
:key="(loc as WeatherData).location"
|
|
||||||
:weather="loc"
|
|
||||||
:temp-unit="tempUnit"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<div v-else class="panel-empty">No weather configured</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Center column: Chat -->
|
|
||||||
<div class="briefing-center">
|
<div class="briefing-center">
|
||||||
<ChatPanel
|
<ChatPanel
|
||||||
variant="full"
|
variant="full"
|
||||||
@@ -287,8 +272,19 @@ onMounted(async () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Right column: News -->
|
<!-- Right column: Weather + News -->
|
||||||
<div class="briefing-right">
|
<div class="briefing-right">
|
||||||
|
<!-- Weather section -->
|
||||||
|
<template v-if="weatherData.length">
|
||||||
|
<WeatherCard
|
||||||
|
v-for="loc in weatherData"
|
||||||
|
:key="(loc as WeatherData).location"
|
||||||
|
:weather="loc"
|
||||||
|
:temp-unit="tempUnit"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- News section -->
|
||||||
<div class="panel-label-row">
|
<div class="panel-label-row">
|
||||||
<div class="panel-label">Today's News</div>
|
<div class="panel-label">Today's News</div>
|
||||||
<span v-if="newsItems.length" class="news-count">{{ newsItems.length }} items</span>
|
<span v-if="newsItems.length" class="news-count">{{ newsItems.length }} items</span>
|
||||||
@@ -348,7 +344,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
.briefing-shell {
|
.briefing-shell {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 2fr 1fr;
|
grid-template-columns: 1fr minmax(320px, 420px);
|
||||||
grid-template-rows: auto 1fr;
|
grid-template-rows: auto 1fr;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
@@ -421,27 +417,10 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
.btn-trigger:disabled { opacity: 0.5; cursor: not-allowed; }
|
.btn-trigger:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||||
|
|
||||||
/* ─── Left column (Weather) ──────────────────────────────────────────────── */
|
/* ─── Left column (Chat) ─────────────────────────────────────────────────── */
|
||||||
|
|
||||||
.briefing-left {
|
|
||||||
grid-column: 1;
|
|
||||||
grid-row: 2;
|
|
||||||
border-right: 1px solid var(--color-border);
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: 1rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.briefing-left :deep(.weather-card) {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ─── Center column (Chat) ───────────────────────────────────────────────── */
|
|
||||||
|
|
||||||
.briefing-center {
|
.briefing-center {
|
||||||
grid-column: 2;
|
grid-column: 1;
|
||||||
grid-row: 2;
|
grid-row: 2;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -453,10 +432,10 @@ onMounted(async () => {
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ─── Right column (News) ────────────────────────────────────────────────── */
|
/* ─── Right column (Weather + News) ──────────────────────────────────────── */
|
||||||
|
|
||||||
.briefing-right {
|
.briefing-right {
|
||||||
grid-column: 3;
|
grid-column: 2;
|
||||||
grid-row: 2;
|
grid-row: 2;
|
||||||
border-left: 1px solid var(--color-border);
|
border-left: 1px solid var(--color-border);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
@@ -466,6 +445,10 @@ onMounted(async () => {
|
|||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.briefing-right :deep(.weather-card) {
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.panel-label {
|
.panel-label {
|
||||||
font-size: 0.72rem;
|
font-size: 0.72rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@@ -578,32 +561,21 @@ a.news-title:hover { text-decoration: underline; color: var(--color-primary); }
|
|||||||
|
|
||||||
/* ─── Responsive ─────────────────────────────────────────────────────────── */
|
/* ─── Responsive ─────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 700px) {
|
||||||
.briefing-shell {
|
.briefing-shell {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
grid-template-rows: auto auto 1fr auto;
|
grid-template-rows: auto 1fr auto;
|
||||||
}
|
|
||||||
.briefing-header {
|
|
||||||
grid-column: 1;
|
|
||||||
grid-row: 1;
|
|
||||||
}
|
|
||||||
.briefing-left {
|
|
||||||
grid-column: 1;
|
|
||||||
grid-row: 2;
|
|
||||||
border-right: none;
|
|
||||||
border-bottom: 1px solid var(--color-border);
|
|
||||||
max-height: 220px;
|
|
||||||
}
|
}
|
||||||
.briefing-center {
|
.briefing-center {
|
||||||
grid-column: 1;
|
grid-column: 1;
|
||||||
grid-row: 3;
|
grid-row: 2;
|
||||||
}
|
}
|
||||||
.briefing-right {
|
.briefing-right {
|
||||||
grid-column: 1;
|
grid-column: 1;
|
||||||
grid-row: 4;
|
grid-row: 3;
|
||||||
border-left: none;
|
border-left: none;
|
||||||
border-top: 1px solid var(--color-border);
|
border-top: 1px solid var(--color-border);
|
||||||
max-height: 260px;
|
max-height: 300px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user