Beta4
Fix mobile table issues
This commit is contained in:
+47
-15
@@ -82,7 +82,7 @@ body {
|
||||
color: #1e1208;
|
||||
background: #f5edd6;
|
||||
min-height: 100vh;
|
||||
padding: 52px 28px 32px;
|
||||
padding: 52px 10px 32px;
|
||||
position: relative;
|
||||
/* overflow-x: clip (not hidden) — `clip` clips horizontal overflow
|
||||
WITHOUT making this a scroll container, so `position: sticky` on
|
||||
@@ -564,6 +564,12 @@ body {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 14px;
|
||||
/* Fixed layout so the explicit per-cell widths set by the JS width-sync
|
||||
(see useLayoutEffect in sunscope.js) are actually enforced. Without
|
||||
this the browser's auto layout treats those widths as suggestions and
|
||||
redistributes space, which makes the header and body columns drift
|
||||
apart the further right you scroll. */
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
/* Column headings (Hour, Air, RH, Wind...).
|
||||
@@ -627,29 +633,40 @@ body {
|
||||
border-right: 1px solid #ede4cc;
|
||||
}
|
||||
|
||||
/* The Dir column is centered on both desktop and mobile so the
|
||||
compass vane sits with even padding either side — especially
|
||||
important on mobile where the compass label is hidden and there's
|
||||
no text to balance the right-aligned content. */
|
||||
.utci-table th.utci-dir-cell,
|
||||
.utci-table td.utci-dir-cell {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* "Tight" header — for columns whose body content is narrower than
|
||||
the default tracked-out header would suggest (Air, Pcpt). Reducing
|
||||
the letter-spacing shrinks the heading's natural width so it stops
|
||||
driving the column wider than the body needs. Right-aligned values
|
||||
then sit with balanced visual padding on either side. */
|
||||
.utci-table th.utci-tight-head {
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
|
||||
/* ── 8. NOW-ROW & NIGHT-ROW DECORATIONS ─────────────────────────────── */
|
||||
|
||||
/* "Currently this hour" — the highlighted row */
|
||||
/* "Currently this hour" — the highlighted row.
|
||||
The row gets a cream-yellow background and darker text. No left
|
||||
stripe or pip — the colour shift alone signals "this is now". */
|
||||
.utci-table tbody tr.is-now {
|
||||
background: #fff8e4 !important;
|
||||
box-shadow: inset 3px 0 #c8922a; /* brass stripe down the left */
|
||||
}
|
||||
|
||||
.utci-table tbody tr.is-now td {
|
||||
color: #1e1208;
|
||||
}
|
||||
|
||||
/* The little brass pip in the time cell of the current hour */
|
||||
.now-pip {
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: #c8922a;
|
||||
margin-right: 7px;
|
||||
vertical-align: middle;
|
||||
box-shadow: 0 0 8px #c8922a73;
|
||||
/* Brass bracket framing the current-hour row top and bottom — clean,
|
||||
definitive, no left stripe needed. */
|
||||
border-top: 2px solid #c8922a;
|
||||
border-bottom: 2px solid #c8922a;
|
||||
}
|
||||
|
||||
/* Night-time hours get a slightly muted text colour */
|
||||
@@ -843,4 +860,19 @@ body {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Halve the hourly-table cell padding on mobile so more columns fit
|
||||
in the viewport before the horizontal scrollbar kicks in. */
|
||||
.utci-table th {
|
||||
padding: 7px 6px;
|
||||
}
|
||||
.utci-table td {
|
||||
padding: 5px 6px;
|
||||
}
|
||||
|
||||
/* Drop the compass-label text ("N", "NW"...) next to the wind vane on
|
||||
mobile — the vane arrow alone is enough at small sizes. */
|
||||
.wind-dir-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+100
-62
@@ -854,12 +854,12 @@ function ScopeReticle({ value, cat, loading, elev = 0, dt = new Date() }) {
|
||||
font-family="Fraunces, serif" font-weight="700">
|
||||
${value.toFixed(1)}°
|
||||
</text>
|
||||
<text x=${cx} y=${cy + 7} text-anchor="middle"
|
||||
<text x=${cx} y=${cy + 15} text-anchor="middle"
|
||||
fill=${readoutMutedColor} font-size="6.5"
|
||||
font-family="JetBrains Mono, monospace" letter-spacing="2">
|
||||
UTCI NOW
|
||||
</text>
|
||||
<text x=${cx} y=${cy + 19} text-anchor="middle"
|
||||
<text x=${cx} y=${cy + 23} text-anchor="middle"
|
||||
fill=${glowColor} font-size="7"
|
||||
font-family="JetBrains Mono, monospace" letter-spacing="0.8">
|
||||
${cat.label.toUpperCase()}
|
||||
@@ -974,8 +974,8 @@ function UTCIForecast() {
|
||||
// true = visible on first load (and the only ones free users see)
|
||||
// false = hidden by default (Pro users can toggle these on)
|
||||
const [visibleCols, setVisibleCols] = useState({
|
||||
hour: true, air: true, rh: true, dew: true,
|
||||
wind: true, dir: true,
|
||||
hour: true, air: true, rh: false, dew: false,
|
||||
wind: true, dir: false,
|
||||
cloud: false, sun: false, direct: false, diffuse: false,
|
||||
tmrt: false, delta: false, utci: false,
|
||||
uvA: false, uvB: false, burn: false,
|
||||
@@ -1015,47 +1015,84 @@ function UTCIForecast() {
|
||||
};
|
||||
|
||||
useLayoutEffect(() => {
|
||||
// Synchronise the head and body table column widths.
|
||||
// Each column is sized to max(60px, header's natural width, body's
|
||||
// natural width) so neither header labels nor body data ever feel
|
||||
// squashed, and header text never overflows into the next column.
|
||||
const MIN_COL = 60;
|
||||
// Synchronise the head and body table column widths with a
|
||||
// "shrink-to-fit then distribute" strategy:
|
||||
// • Measure each column's true natural (content-fit) width by
|
||||
// temporarily switching both tables to table-layout: auto +
|
||||
// width: max-content. White-space: nowrap on cells stops content
|
||||
// from wrapping, so the measurement is the smallest width that
|
||||
// won't clip the content.
|
||||
// • If the body scroller has spare horizontal space (natural total
|
||||
// < container width), scale every column up proportionally to
|
||||
// fill it — so toggling columns off makes the remaining ones fan
|
||||
// out instead of leaving an awkward gap.
|
||||
// • Otherwise apply the natural widths as-is and let the body
|
||||
// scroller's overflow-x: auto produce a horizontal scrollbar.
|
||||
const sync = () => {
|
||||
const headTable = headTableRef.current;
|
||||
const bodyTable = bodyTableRef.current;
|
||||
if (!headTable || !bodyTable) return;
|
||||
const headTable = headTableRef.current;
|
||||
const bodyTable = bodyTableRef.current;
|
||||
const bodyScroll = bodyScrollRef.current;
|
||||
if (!headTable || !bodyTable || !bodyScroll) return;
|
||||
const bodyRow = bodyTable.querySelector('tbody tr');
|
||||
const headRow = headTable.querySelector('thead tr');
|
||||
if (!bodyRow || !headRow) return;
|
||||
const headCells = Array.from(headRow.children);
|
||||
const bodyCells = Array.from(bodyRow.children);
|
||||
const n = Math.min(headCells.length, bodyCells.length);
|
||||
// Step 1: clear any previously-forced widths so we can read each
|
||||
// cell's *natural* width (the width it'd take with just min-width
|
||||
// and content driving it).
|
||||
headCells.forEach(c => {
|
||||
c.style.width = '';
|
||||
c.style.minWidth = '';
|
||||
c.style.maxWidth = '';
|
||||
});
|
||||
bodyCells.forEach(c => {
|
||||
c.style.width = '';
|
||||
c.style.minWidth = '';
|
||||
c.style.maxWidth = '';
|
||||
});
|
||||
headTable.style.width = '';
|
||||
// Reading getBoundingClientRect forces layout — that's what we want.
|
||||
// Step 2: compute final widths as max(MIN_COL, headNatural, bodyNatural).
|
||||
const finalW = new Array(n);
|
||||
let totalWidth = 0;
|
||||
if (n === 0) return;
|
||||
|
||||
// Step 1: clear any previously-forced cell widths and switch the
|
||||
// tables to natural sizing so the measurement reflects the true
|
||||
// content-fit width — independent of how wide the container is.
|
||||
headCells.forEach(c => { c.style.width = ''; c.style.minWidth = ''; c.style.maxWidth = ''; });
|
||||
bodyCells.forEach(c => { c.style.width = ''; c.style.minWidth = ''; c.style.maxWidth = ''; });
|
||||
headTable.style.width = 'max-content';
|
||||
bodyTable.style.width = 'max-content';
|
||||
headTable.style.tableLayout = 'auto';
|
||||
bodyTable.style.tableLayout = 'auto';
|
||||
|
||||
// Step 2: read each cell's natural width. getBoundingClientRect
|
||||
// forces synchronous layout — that's what we want.
|
||||
const naturalW = new Array(n);
|
||||
let naturalTotal = 0;
|
||||
for (let i = 0; i < n; i++) {
|
||||
const headW = headCells[i].getBoundingClientRect().width;
|
||||
const bodyW = bodyCells[i].getBoundingClientRect().width;
|
||||
const w = Math.max(MIN_COL, Math.ceil(headW), Math.ceil(bodyW));
|
||||
finalW[i] = w;
|
||||
totalWidth += w;
|
||||
const w = Math.max(Math.ceil(headW), Math.ceil(bodyW));
|
||||
naturalW[i] = w;
|
||||
naturalTotal += w;
|
||||
}
|
||||
// Step 3: apply the final width to both head and body cells.
|
||||
|
||||
// Step 3: decide final widths based on available container width.
|
||||
const containerW = bodyScroll.clientWidth;
|
||||
const finalW = new Array(n);
|
||||
let totalWidth;
|
||||
if (naturalTotal > 0 && naturalTotal < containerW) {
|
||||
// Spare space — distribute proportionally across columns so they
|
||||
// fan out to fill the scroller (no awkward right-hand gap).
|
||||
const scale = containerW / naturalTotal;
|
||||
let running = 0;
|
||||
for (let i = 0; i < n - 1; i++) {
|
||||
finalW[i] = Math.floor(naturalW[i] * scale);
|
||||
running += finalW[i];
|
||||
}
|
||||
// Absorb sub-pixel rounding into the last column so the total
|
||||
// exactly matches the container width.
|
||||
finalW[n - 1] = containerW - running;
|
||||
totalWidth = containerW;
|
||||
} else {
|
||||
// Naturals don't fit — use them as-is and let the body scroll.
|
||||
for (let i = 0; i < n; i++) finalW[i] = naturalW[i];
|
||||
totalWidth = naturalTotal;
|
||||
}
|
||||
|
||||
// Step 4: restore the CSS-defined table-layout: fixed so the
|
||||
// explicit cell widths we apply below are honoured by the browser
|
||||
// (not redistributed by the auto-layout algorithm).
|
||||
headTable.style.tableLayout = '';
|
||||
bodyTable.style.tableLayout = '';
|
||||
|
||||
// Step 5: apply the final width to both head and body cells.
|
||||
for (let i = 0; i < n; i++) {
|
||||
const px = `${finalW[i]}px`;
|
||||
headCells[i].style.width = px;
|
||||
@@ -1065,19 +1102,24 @@ function UTCIForecast() {
|
||||
bodyCells[i].style.minWidth = px;
|
||||
bodyCells[i].style.maxWidth = px;
|
||||
}
|
||||
// Make the header table the same total width as the body table
|
||||
// so the inner track has somewhere to translate to.
|
||||
// Make both tables exactly totalWidth wide so they share the same
|
||||
// horizontal extent — column N in the header sits directly above
|
||||
// column N in the body, no drift as you scroll right.
|
||||
headTable.style.width = `${totalWidth}px`;
|
||||
bodyTable.style.width = `${totalWidth}px`;
|
||||
// Re-apply current horizontal offset so column alignment survives.
|
||||
handleBodyScroll();
|
||||
};
|
||||
// Run once after layout
|
||||
sync();
|
||||
// Re-sync if the body table reflows (columns toggle, content changes)
|
||||
// Re-sync when the scroll container's width changes (window resize,
|
||||
// sidebar opens, etc). We observe the scroller — not the body table —
|
||||
// because the body table's width is now driven by sync itself, which
|
||||
// would otherwise create a feedback loop.
|
||||
let ro = null;
|
||||
if (typeof ResizeObserver !== 'undefined' && bodyTableRef.current) {
|
||||
if (typeof ResizeObserver !== 'undefined' && bodyScrollRef.current) {
|
||||
ro = new ResizeObserver(sync);
|
||||
ro.observe(bodyTableRef.current);
|
||||
ro.observe(bodyScrollRef.current);
|
||||
}
|
||||
window.addEventListener('resize', sync);
|
||||
return () => {
|
||||
@@ -1500,7 +1542,7 @@ function UTCIForecast() {
|
||||
<div class="col-toggles">
|
||||
<span class="col-toggles-label">Columns:</span>
|
||||
${[
|
||||
{ key: 'hour', label: 'Hour' },
|
||||
// Hour and UTCI+P are always-on — no toggle button for them.
|
||||
{ key: 'air', label: 'Air' },
|
||||
{ key: 'rh', label: 'RH' },
|
||||
{ key: 'dew', label: 'Dew' },
|
||||
@@ -1520,7 +1562,6 @@ function UTCIForecast() {
|
||||
{ key: 'uvB', label: 'UV-B' },
|
||||
{ key: 'burn', label: 'Burn' },
|
||||
{ key: 'precip', label: 'Precip' },
|
||||
{ key: 'utciP', label: 'UTCI+P' },
|
||||
].map(c => html`
|
||||
<button
|
||||
key=${c.key}
|
||||
@@ -1564,15 +1605,15 @@ function UTCIForecast() {
|
||||
<table class="utci-table utci-table-head" ref=${headTableRef}>
|
||||
<thead>
|
||||
<tr>
|
||||
${visibleCols.hour && html`<th>Hour</th>`}
|
||||
${visibleCols.air && html`<th>Air <span class="col-unit">°C</span></th>`}
|
||||
<th>Hour</th>
|
||||
${visibleCols.air && html`<th class="utci-tight-head">Air <span class="col-unit">°C</span></th>`}
|
||||
${visibleCols.rh && html`<th>RH <span class="col-unit">%</span></th>`}
|
||||
${visibleCols.dew && html`<th>Dew <span class="col-unit">°C</span></th>`}
|
||||
${visibleCols.soilT && html`<th>Soil °C <span class="col-unit">surface</span></th>`}
|
||||
${visibleCols.soilT6 && html`<th>Soil 6cm <span class="col-unit">°C root</span></th>`}
|
||||
${visibleCols.soilM && html`<th>Soil moist <span class="col-unit">m³/m³</span></th>`}
|
||||
${visibleCols.wind && html`<th>Wind <span class="col-unit">m/s (gust)</span></th>`}
|
||||
${visibleCols.dir && html`<th>Dir <span class="col-unit">compass</span></th>`}
|
||||
${visibleCols.dir && html`<th class="utci-dir-cell">Dir <span class="col-unit">-</span></th>`}
|
||||
${visibleCols.cloud && html`<th>Cloud <span class="col-unit">%</span></th>`}
|
||||
${visibleCols.sun && html`<th>Sun <span class="col-unit">elev°</span></th>`}
|
||||
${visibleCols.direct && html`<th>Direct <span class="col-unit">W/m²</span></th>`}
|
||||
@@ -1583,8 +1624,8 @@ function UTCIForecast() {
|
||||
${visibleCols.uvA && html`<th>UV-A <span class="col-unit">est. idx</span></th>`}
|
||||
${visibleCols.uvB && html`<th>UV-B <span class="col-unit">est. idx</span></th>`}
|
||||
${visibleCols.burn && html`<th>Burn <span class="col-unit">to MED</span></th>`}
|
||||
${visibleCols.precip && html`<th>Precip <span class="col-unit">mm/h</span></th>`}
|
||||
${visibleCols.utciP && html`<th>UTCI+P <span class="col-unit">°C adj.</span></th>`}
|
||||
${visibleCols.precip && html`<th class="utci-tight-head">Pcpt <span class="col-unit">mm/h</span></th>`}
|
||||
<th>UTCI+P <span class="col-unit">°C adj.</span></th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
@@ -1607,14 +1648,12 @@ function UTCIForecast() {
|
||||
return html`
|
||||
<tr key=${r.iso}
|
||||
class=${`${isNight ? 'is-night' : ''} ${isNow ? 'is-now' : ''}`.trim()}>
|
||||
${visibleCols.hour && html`
|
||||
<td class="utci-time">
|
||||
${isNow && html`<span class="now-pip"></span>`}
|
||||
<span style=${{ display: 'inline-flex', alignItems: 'center', gap: '7px', verticalAlign: 'middle' }}>
|
||||
<${SkyScope} elev=${r.elev} dt=${r.dt} size=${30} />
|
||||
<span>${r.dt.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' })}</span>
|
||||
</span>
|
||||
</td>`}
|
||||
<td class="utci-time">
|
||||
<span style=${{ display: 'inline-flex', alignItems: 'center', gap: '7px', verticalAlign: 'middle' }}>
|
||||
<${SkyScope} elev=${r.elev} dt=${r.dt} size=${30} />
|
||||
<span>${r.dt.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' })}</span>
|
||||
</span>
|
||||
</td>
|
||||
${visibleCols.air && html`<td>${r.Ta.toFixed(1)}</td>`}
|
||||
${visibleCols.rh && html`<td>${Math.round(r.RH)}</td>`}
|
||||
${visibleCols.dew && html`<td>${r.dew != null ? r.dew.toFixed(1) : '—'}</td>`}
|
||||
@@ -1629,10 +1668,10 @@ function UTCIForecast() {
|
||||
? html`<span style=${{ opacity: 0.65, marginLeft: '4px' }}>(${r.gust.toFixed(1)})</span>`
|
||||
: ''}
|
||||
</td>`}
|
||||
${visibleCols.dir && html`<td>
|
||||
${visibleCols.dir && html`<td class="utci-dir-cell">
|
||||
<span style=${{ display: 'inline-flex', alignItems: 'center', gap: '6px', verticalAlign: 'middle' }}>
|
||||
<${WindVane} bearing=${r.wd} size=${28} />
|
||||
<span style=${{ fontFamily: 'JetBrains Mono, monospace', fontSize: '11px' }}>${r.compass.label}</span>
|
||||
<span class="wind-dir-label" style=${{ fontFamily: 'JetBrains Mono, monospace', fontSize: '11px' }}>${r.compass.label}</span>
|
||||
</span>
|
||||
</td>`}
|
||||
${visibleCols.cloud && html`<td>
|
||||
@@ -1674,12 +1713,11 @@ function UTCIForecast() {
|
||||
<td style=${{ color: r.snow > 0 ? '#6090c8' : r.precip > 0 ? '#5090b0' : '#c0a880' }}>
|
||||
${r.snow > 0 ? '❅ ' + r.snow.toFixed(1) + 'cm' : r.precip > 0 ? r.precip.toFixed(1) : '—'}
|
||||
</td>`}
|
||||
${visibleCols.utciP && html`
|
||||
<td style=${{ background: 'rgba(180,215,250,0.10)' }}>
|
||||
<span class="utci-cell utci-cell-hero" style=${{ background: adjCat.bg, color: adjCat.fg }}>
|
||||
${r.utciAdj.toFixed(1)}
|
||||
</span>
|
||||
</td>`}
|
||||
<td style=${{ background: 'rgba(180,215,250,0.10)' }}>
|
||||
<span class="utci-cell utci-cell-hero" style=${{ background: adjCat.bg, color: adjCat.fg }}>
|
||||
${r.utciAdj.toFixed(1)}
|
||||
</span>
|
||||
</td>
|
||||
</tr>`;
|
||||
})}
|
||||
</tbody>
|
||||
|
||||
Reference in New Issue
Block a user