Display: fixed-viewport CSS grid so the kiosk screen always fills, no scroll
Previous layout used flex + clamp()-based intrinsic sizing, which worked
until the rain%/wind additions made hourly cards tall enough to push total
content past 100vh -- overflow was silently clipped rather than scrolling
(the container has overflow:hidden by design for a kiosk screen), so the
bottom of the page (daily row / "updated" footer) could vanish depending on
viewport height.
Now .wx-display is a CSS grid with explicit rows (auto for header/now/label
rows, minmax(0, 1fr) for the two content rows) that always sum to exactly
100vh. Each 1fr row's direct child gets min-height:0 + overflow:hidden so it
actually respects its shrunk allotment instead of forcing the track to grow
-- the standard CSS grid overflow gotcha. Wrapped the next-2-days + daily
row together in a new .wx-coming-up flex column so they share one grid
track instead of each claiming independent space.
Also: rain% + wind now render on one line ("12% · SW 8mph") instead of two
stacked lines, trimming card height further.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
7f3ff4495c
commit
3beac76ae6
2 changed files with 137 additions and 82 deletions
|
|
@ -34,47 +34,56 @@ html, body, #root { height: 100%; margin: 0; font-size: 14px; }
|
|||
::-webkit-scrollbar-track { background: transparent; }
|
||||
::-webkit-scrollbar-thumb { background: var(--card-border); border-radius: 2px; }
|
||||
|
||||
/* ── Reception display (full-screen, public, no chrome) ─────────────────── */
|
||||
/* ── Reception display (full-screen, public, no chrome) ───────────────────
|
||||
Fixed-viewport CSS grid, not intrinsic/flowing content — this is a kiosk
|
||||
screen, not a responsive page, so every row is sized to guarantee the whole
|
||||
thing always sums to exactly 100vh with no scrolling: header/now/label rows
|
||||
are `auto` (their own intrinsic height), the two content rows are
|
||||
`minmax(0, 1fr)` (share whatever's left, and — critically — `minmax(0, …)`
|
||||
lets them shrink below their content's natural size instead of overflowing
|
||||
the grid). Each `1fr` row's direct child then has `min-height: 0` +
|
||||
`overflow: hidden` so it actually respects that shrunk size rather than
|
||||
forcing the track to grow (the #1 CSS grid gotcha this pattern exists to
|
||||
avoid). Landscape-first: designed to be wide and comfortably short. */
|
||||
.wx-display {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
background: linear-gradient(160deg, var(--navy-dark) 0%, var(--navy) 60%, #14304a 100%);
|
||||
color: var(--text);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: clamp(16px, 3vh, 40px) clamp(20px, 4vw, 56px);
|
||||
font-size: clamp(14px, 1.1vw, 18px);
|
||||
display: grid;
|
||||
grid-template-rows: auto auto minmax(0, 1fr) auto minmax(0, 1fr) auto;
|
||||
row-gap: clamp(8px, 1.6vh, 20px);
|
||||
padding: clamp(14px, 2.2vh, 32px) clamp(20px, 3vw, 48px);
|
||||
font-size: clamp(12px, 0.95vw, 16px);
|
||||
}
|
||||
|
||||
.wx-display-header {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
margin-bottom: clamp(12px, 2vh, 28px);
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
.wx-clock { font-size: clamp(2.2rem, 5vw, 4rem); font-weight: 700; line-height: 1; letter-spacing: -0.02em; }
|
||||
.wx-date { color: var(--text-muted); font-size: clamp(1rem, 1.6vw, 1.4rem); margin-top: 4px; }
|
||||
.wx-hotel-name { color: var(--gold); font-size: clamp(1.1rem, 1.8vw, 1.6rem); font-weight: 600; letter-spacing: .03em; text-transform: uppercase; }
|
||||
.wx-clock { font-size: clamp(1.8rem, 4.2vw, 3.4rem); font-weight: 700; line-height: 1; letter-spacing: -0.02em; }
|
||||
.wx-date { color: var(--text-muted); font-size: clamp(0.9rem, 1.4vw, 1.25rem); margin-top: 4px; }
|
||||
.wx-hotel-name { color: var(--gold); font-size: clamp(1rem, 1.6vw, 1.4rem); font-weight: 600; letter-spacing: .03em; text-transform: uppercase; }
|
||||
|
||||
.wx-now {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: clamp(12px, 2vw, 28px);
|
||||
margin-bottom: clamp(16px, 3vh, 32px);
|
||||
gap: clamp(10px, 1.8vw, 24px);
|
||||
}
|
||||
.wx-now-temp { font-size: clamp(3.2rem, 7vw, 6rem); font-weight: 700; line-height: 1; }
|
||||
.wx-now-meta { display: flex; flex-direction: column; gap: 4px; color: var(--text-muted); font-size: clamp(0.9rem, 1.2vw, 1.15rem); }
|
||||
.wx-now-temp { font-size: clamp(2.4rem, 5.5vw, 4.6rem); font-weight: 700; line-height: 1; }
|
||||
.wx-now-meta { display: flex; flex-direction: column; gap: 4px; color: var(--text-muted); font-size: clamp(0.8rem, 1.05vw, 1.05rem); }
|
||||
|
||||
.wx-section-label {
|
||||
font-size: clamp(0.75rem, 1vw, 0.95rem);
|
||||
font-size: clamp(0.7rem, 0.95vw, 0.9rem);
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .08em;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: clamp(8px, 1.2vh, 14px);
|
||||
}
|
||||
.wx-section-label-row {
|
||||
display: flex;
|
||||
|
|
@ -82,73 +91,110 @@ html, body, #root { height: 100%; margin: 0; font-size: 14px; }
|
|||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: clamp(8px, 1.2vh, 14px);
|
||||
}
|
||||
.wx-section-label-row .wx-section-label { margin-bottom: 0; }
|
||||
|
||||
/* Sunrise/sunset, shown next to a day's label */
|
||||
.wx-astro { display: flex; gap: clamp(10px, 1.2vw, 18px); color: var(--text-muted); font-size: clamp(0.75rem, 0.9vw, 0.95rem); }
|
||||
.wx-astro { display: flex; gap: clamp(10px, 1.2vw, 18px); color: var(--text-muted); font-size: clamp(0.7rem, 0.85vw, 0.9rem); }
|
||||
.wx-astro-item { display: inline-flex; align-items: center; gap: 4px; }
|
||||
|
||||
/* Rain%/wind, shown on hourly and 3-hourly point cards */
|
||||
.wx-meta-precip { font-size: clamp(0.62rem, 0.75vw, 0.75rem); color: #7dd3fc; }
|
||||
.wx-meta-wind { font-size: clamp(0.62rem, 0.75vw, 0.75rem); color: var(--text-muted); white-space: nowrap; }
|
||||
/* Rain% + wind, one line, shown on hourly and 3-hourly point cards */
|
||||
.wx-meta { display: flex; align-items: center; gap: 4px; font-size: clamp(0.6rem, 0.72vw, 0.72rem); white-space: nowrap; }
|
||||
.wx-meta-precip { color: #7dd3fc; }
|
||||
.wx-meta-wind { color: var(--text-muted); }
|
||||
.wx-meta-sep { color: var(--text-muted); opacity: .5; }
|
||||
|
||||
/* Today — wrapped in its own block so its hourly cards read as one distinct
|
||||
group, not a continuation of the "Coming up" cards below it. */
|
||||
group, not a continuation of the "Coming up" cards below it. Fills its grid
|
||||
row exactly (see the .wx-display comment above). */
|
||||
.wx-today-block {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--surface-2);
|
||||
border-radius: var(--radius);
|
||||
padding: clamp(10px, 1.6vh, 18px);
|
||||
margin-bottom: clamp(16px, 2.5vh, 28px);
|
||||
padding: clamp(8px, 1.4vh, 16px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: clamp(6px, 1vh, 12px);
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.wx-hourly-row {
|
||||
display: flex;
|
||||
gap: clamp(6px, 1vw, 14px);
|
||||
align-items: stretch;
|
||||
gap: clamp(5px, 0.8vw, 12px);
|
||||
overflow-x: auto;
|
||||
padding-bottom: 4px;
|
||||
flex: 0 0 auto;
|
||||
overflow-y: hidden;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
.wx-hour-card {
|
||||
flex: 1 0 68px;
|
||||
flex: 1 0 clamp(52px, 5.2vw, 76px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: clamp(8px, 1vh, 14px) 4px;
|
||||
justify-content: space-between;
|
||||
gap: 4px;
|
||||
padding: clamp(6px, 1vh, 10px) 4px;
|
||||
border-radius: var(--radius);
|
||||
background: var(--surface-2);
|
||||
}
|
||||
.wx-hour-card.now { background: rgba(201,168,76,0.16); border: 1px solid rgba(201,168,76,0.4); }
|
||||
.wx-hour-time { font-size: clamp(0.75rem, 0.9vw, 0.95rem); color: var(--text-muted); }
|
||||
.wx-hour-temp { font-size: clamp(0.95rem, 1.1vw, 1.2rem); font-weight: 600; }
|
||||
.wx-hour-time { font-size: clamp(0.68rem, 0.82vw, 0.88rem); color: var(--text-muted); }
|
||||
.wx-hour-temp { font-size: clamp(0.85rem, 1vw, 1.1rem); font-weight: 600; }
|
||||
|
||||
/* Next 2 days — 3-hourly, both under the "Coming up" label */
|
||||
/* "Coming up" — tomorrow/day-after (3-hourly) above, days 4-7 (daily) below,
|
||||
sharing one grid row between them so the pair always fits. */
|
||||
.wx-coming-up {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: clamp(8px, 1.4vh, 18px);
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.wx-next-days {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: clamp(12px, 1.6vw, 24px);
|
||||
margin-bottom: clamp(16px, 2.5vh, 28px);
|
||||
gap: clamp(10px, 1.4vw, 20px);
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
.wx-next-day-card {
|
||||
background: var(--surface);
|
||||
border-radius: var(--radius);
|
||||
padding: clamp(10px, 1.4vh, 18px);
|
||||
padding: clamp(8px, 1.2vh, 14px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.wx-next-day-title-row { display: flex; align-items: baseline; justify-content: space-between; flex-wrap: wrap; gap: 6px; margin-bottom: 8px; }
|
||||
.wx-next-day-title { font-weight: 700; font-size: clamp(0.95rem, 1.2vw, 1.2rem); }
|
||||
.wx-next-day-points { display: flex; gap: clamp(4px, 0.8vw, 10px); overflow-x: auto; }
|
||||
.wx-point { display: flex; flex-direction: column; align-items: center; gap: 4px; flex: 1 0 56px; }
|
||||
.wx-point-time { font-size: clamp(0.65rem, 0.8vw, 0.8rem); color: var(--text-muted); }
|
||||
.wx-point-temp { font-size: clamp(0.85rem, 1vw, 1.05rem); font-weight: 600; }
|
||||
.wx-next-day-title-row { display: flex; align-items: baseline; justify-content: space-between; flex-wrap: wrap; gap: 6px; }
|
||||
.wx-next-day-title { font-weight: 700; font-size: clamp(0.85rem, 1.1vw, 1.1rem); }
|
||||
.wx-next-day-points {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: clamp(4px, 0.6vw, 8px);
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
.wx-point {
|
||||
flex: 1 0 clamp(46px, 4.4vw, 60px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 3px;
|
||||
}
|
||||
.wx-point-time { font-size: clamp(0.6rem, 0.75vw, 0.75rem); color: var(--text-muted); }
|
||||
.wx-point-temp { font-size: clamp(0.78rem, 0.95vw, 1rem); font-weight: 600; }
|
||||
|
||||
/* Days 4-7 — plain daily row, still under the "Coming up" label above */
|
||||
/* Days 4-7 — plain daily row, fixed (short) height, doesn't need to stretch */
|
||||
.wx-daily-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: clamp(10px, 1.4vw, 20px);
|
||||
margin-top: auto;
|
||||
gap: clamp(8px, 1.2vw, 16px);
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.wx-day-card {
|
||||
|
|
@ -156,19 +202,20 @@ html, body, #root { height: 100%; margin: 0; font-size: 14px; }
|
|||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: clamp(10px, 1.4vh, 16px) 6px;
|
||||
padding: clamp(8px, 1.2vh, 14px) 6px;
|
||||
border-radius: var(--radius);
|
||||
background: var(--surface);
|
||||
}
|
||||
.wx-day-name { font-weight: 700; font-size: clamp(0.85rem, 1vw, 1.05rem); }
|
||||
.wx-day-temps { display: flex; gap: 8px; font-size: clamp(0.8rem, 0.95vw, 1rem); }
|
||||
.wx-day-name { font-weight: 700; font-size: clamp(0.78rem, 0.92vw, 0.98rem); }
|
||||
.wx-day-temps { display: flex; gap: 8px; font-size: clamp(0.75rem, 0.88vw, 0.95rem); }
|
||||
.wx-day-max { font-weight: 600; }
|
||||
.wx-day-min { color: var(--text-muted); }
|
||||
|
||||
.wx-updated { text-align: right; color: var(--text-muted); font-size: clamp(0.7rem, 0.8vw, 0.85rem); margin-top: 10px; }
|
||||
.wx-updated { text-align: right; color: var(--text-muted); font-size: clamp(0.65rem, 0.75vw, 0.8rem); }
|
||||
|
||||
.wx-fallback-screen {
|
||||
height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
height: 100vh; width: 100vw; box-sizing: border-box;
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
gap: 12px; background: var(--navy-dark); color: var(--text-muted);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -51,17 +51,21 @@ function AstroRow({ astro }: { astro?: Astro }) {
|
|||
)
|
||||
}
|
||||
|
||||
// Rain% + wind on one line ("12% · SW 8mph") — was two separate lines, which was
|
||||
// part of what pushed the hourly strip past a single viewport's worth of height.
|
||||
function PointMeta({ point }: { point: ForecastPoint }) {
|
||||
if (point.precip_prob == null && point.wind_speed_mph == null) return null
|
||||
return (
|
||||
<>
|
||||
<span className="wx-meta">
|
||||
{point.precip_prob != null && <span className="wx-meta-precip">{Math.round(point.precip_prob)}%</span>}
|
||||
{point.precip_prob != null && point.wind_speed_mph != null && <span className="wx-meta-sep">·</span>}
|
||||
{point.wind_speed_mph != null && (
|
||||
<span className="wx-meta-wind">
|
||||
{point.wind_dir_deg != null ? `${compassFromDeg(point.wind_dir_deg)} ` : ''}
|
||||
{Math.round(point.wind_speed_mph)}mph
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -173,40 +177,44 @@ export default function Display() {
|
|||
|
||||
<div className="wx-section-label">Coming up</div>
|
||||
|
||||
{nextDayGroups.length > 0 && (
|
||||
<div className="wx-next-days">
|
||||
{nextDayGroups.map(g => (
|
||||
<div className="wx-next-day-card" key={g.key}>
|
||||
<div className="wx-next-day-title-row">
|
||||
<span className="wx-next-day-title">{formatDayName(g.points[0].ts)}</span>
|
||||
<AstroRow astro={data.astro[g.key]} />
|
||||
</div>
|
||||
<div className="wx-next-day-points">
|
||||
{g.points.map(p => (
|
||||
<div className="wx-point" key={p.ts}>
|
||||
<span className="wx-point-time">{formatHour(p.ts)}</span>
|
||||
<WeatherIcon icon={p.icon} size={26} />
|
||||
<span className="wx-point-temp">{roundTemp(p.temp_c)}</span>
|
||||
<PointMeta point={p} />
|
||||
</div>
|
||||
))}
|
||||
{/* One grid track shared by both — sized to fit whatever's left of the
|
||||
viewport, so this can never push the page past 100vh. */}
|
||||
<div className="wx-coming-up">
|
||||
{nextDayGroups.length > 0 && (
|
||||
<div className="wx-next-days">
|
||||
{nextDayGroups.map(g => (
|
||||
<div className="wx-next-day-card" key={g.key}>
|
||||
<div className="wx-next-day-title-row">
|
||||
<span className="wx-next-day-title">{formatDayName(g.points[0].ts)}</span>
|
||||
<AstroRow astro={data.astro[g.key]} />
|
||||
</div>
|
||||
<div className="wx-next-day-points">
|
||||
{g.points.map(p => (
|
||||
<div className="wx-point" key={p.ts}>
|
||||
<span className="wx-point-time">{formatHour(p.ts)}</span>
|
||||
<WeatherIcon icon={p.icon} size={22} />
|
||||
<span className="wx-point-temp">{roundTemp(p.temp_c)}</span>
|
||||
<PointMeta point={p} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="wx-daily-row">
|
||||
{data.daily.map(p => (
|
||||
<div className="wx-day-card" key={p.ts}>
|
||||
<span className="wx-day-name">{formatDayName(p.ts)}</span>
|
||||
<WeatherIcon icon={p.icon} size={28} />
|
||||
<span className="wx-day-temps">
|
||||
<span className="wx-day-max">{roundTemp(p.temp_c)}</span>
|
||||
{p.temp_min_c != null && <span className="wx-day-min">{roundTemp(p.temp_min_c)}</span>}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="wx-daily-row">
|
||||
{data.daily.map(p => (
|
||||
<div className="wx-day-card" key={p.ts}>
|
||||
<span className="wx-day-name">{formatDayName(p.ts)}</span>
|
||||
<WeatherIcon icon={p.icon} size={32} />
|
||||
<span className="wx-day-temps">
|
||||
<span className="wx-day-max">{roundTemp(p.temp_c)}</span>
|
||||
{p.temp_min_c != null && <span className="wx-day-min">{roundTemp(p.temp_min_c)}</span>}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{updatedAgo != null && (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue