diff --git a/static/app.js b/static/app.js index 8ea1965..19fc983 100644 --- a/static/app.js +++ b/static/app.js @@ -170,10 +170,7 @@ function render(data) { - - Check historical data - The last 2 years, graded day by day - + Check historical data
${data.target_date} vs a typical day — tap a metric for the full breakdown
@@ -263,6 +260,14 @@ window.Thermograph.onUnitChange(() => { if (recentData) render(recentData); }); +// Rebuild the chart when the window is resized so its drawing width keeps +// tracking the container (debounced — resize fires continuously while dragging). +let resizeTimer = null; +window.addEventListener("resize", () => { + clearTimeout(resizeTimer); + resizeTimer = setTimeout(() => { if (recentData) renderSeries(); }, 160); +}); + function renderSeries() { const host = document.getElementById("series"); if (!host || !recentData) return; @@ -369,9 +374,13 @@ function chartPalette() { }; } -const W = 720, H = 340; +// W/PW are recomputed per build from the container width (see buildChart), so a +// wide monitor gets a genuinely wider drawing — more room between days — instead +// of the same 720-unit canvas magnified. +let W = 720; +const H = 340; const PL = 46, PR = 62, plotTop = 48, plotBot = 288, xLabY = 308; -const PW = W - PL - PR; +let PW = W - PL - PR; const ord = (n) => { const r = Math.round(n), s = ["th", "st", "nd", "rd"], v = r % 100; return r + (s[(v - 20) % 10] || s[v] || s[0]); @@ -397,6 +406,11 @@ let chartMetric = localStorage.getItem("thermograph:chartMetric") || "tmax"; function buildChart(data) { const wrap = document.getElementById("chart-wrap"); const C = chartPalette(); + // Drawing width tracks the on-screen box at a steady ~1.5 CSS px per SVG unit, + // so text renders the same size everywhere while wide monitors gain plot room. + // Phones keep the 720-unit floor (the SVG just scales down). + W = Math.max(720, Math.min(1400, Math.round((wrap.clientWidth || 720) / 1.5))); + PW = W - PL - PR; lastGraded = data; // the series currently drawn (for PNG export) chartDays = [...data.recent].reverse(); // oldest -> newest const days = chartDays, n = days.length; diff --git a/static/calendar.js b/static/calendar.js index aba08fd..70e870c 100644 --- a/static/calendar.js +++ b/static/calendar.js @@ -412,6 +412,13 @@ function markRangeDirty() { refreshBtn.hidden = false; } startInput.addEventListener("change", markRangeDirty); endInput.addEventListener("change", markRangeDirty); +// Clicking anywhere in a From/To field opens the native month/year picker. +// Desktop Chrome otherwise only opens it from the tiny calendar glyph and just +// focuses a segment. showPicker isn't everywhere (Safari/Firefox) — ignore there. +for (const el of [startInput, endInput]) { + el.addEventListener("click", () => { try { el.showPicker(); } catch (e) {} }); +} + // Confirm: read the pickers into the active range and (re)load. function applyRange() { let s = startInput.value ? monthStart(startInput.value) : null; diff --git a/static/compare.js b/static/compare.js index 6374e89..15b719e 100644 --- a/static/compare.js +++ b/static/compare.js @@ -383,6 +383,11 @@ basisToggle.addEventListener("click", (e) => { startInput.max = endInput.max = `${new Date().getFullYear()}-12`; startInput.addEventListener("change", renderAll); endInput.addEventListener("change", renderAll); +// Clicking anywhere in a From/To field opens the native month/year picker +// (mirrors the Calendar range; showPicker is missing in some browsers — ignore). +for (const el of [startInput, endInput]) { + el.addEventListener("click", () => { try { el.showPicker(); } catch (e) {} }); +} // ---- init ---- (function restore() { diff --git a/static/style.css b/static/style.css index c7345c1..f2584e3 100644 --- a/static/style.css +++ b/static/style.css @@ -58,7 +58,10 @@ header { border-bottom: 1px solid var(--border); background: var(--surface); } -.brand { display: flex; align-items: center; gap: 14px; } +/* Header content shares the main column's width so the logo and tabs line up + with the content edges instead of hugging the monitor corners. (1152 = main's + 1200 max minus its 24px side paddings; the header carries its own padding.) */ +.brand { display: flex; align-items: center; gap: 14px; max-width: 1152px; margin: 0 auto; } /* The title block (logo's neighbour) grows and may shrink, so the unit toggle is pushed to the top-right on the logo's row and the long tagline wraps inside it instead of bumping the toggle onto its own line. */ @@ -79,6 +82,22 @@ header.compact h1 { font-size: 18px; } main { max-width: 1200px; margin: 0 auto; padding: 20px 24px 60px; } +/* Larger monitors: widen the content column in steps (1440p, 2K, 4K) so pages + use the screen instead of a 1200px strip. Each page's own grids (normals, + calendar months, day cards) flow into the extra room. */ +@media (min-width: 1680px) { + main { max-width: 1440px; } + .brand { max-width: 1392px; } +} +@media (min-width: 2400px) { + main { max-width: 1640px; } + .brand { max-width: 1592px; } +} +@media (min-width: 3400px) { + main { max-width: 1880px; } + .brand { max-width: 1832px; } +} + .controls { margin-bottom: 18px; display: flex; flex-wrap: wrap; gap: 16px; align-items: flex-start; } .search { position: relative; flex: 1 1 340px; display: flex; gap: 8px; } .search input { @@ -159,24 +178,23 @@ main { max-width: 1200px; margin: 0 auto; padding: 20px 24px 60px; } .wx { width: 1.15em; height: 1.15em; vertical-align: -0.2em; } .share button:hover, .share .share-btn:hover { border-color: var(--accent); } -/* Prominent call-to-action into the 2-year calendar — the primary next step, - so it reads unmistakably as a tappable button (full width, big touch target). */ +/* Call-to-action into the 2-year calendar — a compact button-sized link, so the + graded results stay the page's focal point. */ .cta-cal { - display: flex; align-items: center; gap: 13px; - width: 100%; margin: 0 0 20px; padding: 14px 16px; min-height: 60px; - border: none; border-radius: 13px; text-decoration: none; + display: inline-flex; align-items: center; gap: 9px; + margin: 0 0 20px; padding: 10px 16px; min-height: 44px; + border: none; border-radius: 10px; text-decoration: none; + font-size: 14px; font-weight: 700; color: #1a1206; background: linear-gradient(135deg, #f79556, #ea722c); - box-shadow: 0 4px 16px rgba(240, 128, 60, .3); + box-shadow: 0 2px 8px rgba(240, 128, 60, .25); transition: transform .12s ease, box-shadow .12s ease, filter .12s ease; } -.cta-cal:hover { transform: translateY(-1px); box-shadow: 0 7px 22px rgba(240, 128, 60, .42); filter: brightness(1.04); } -.cta-cal:active { transform: translateY(0); box-shadow: 0 3px 10px rgba(240, 128, 60, .34); } +.cta-cal .ic-lg { width: 18px; height: 18px; } +.cta-cal:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(240, 128, 60, .35); filter: brightness(1.04); } +.cta-cal:active { transform: translateY(0); box-shadow: 0 2px 6px rgba(240, 128, 60, .3); } .cta-cal:focus-visible { outline: 3px solid var(--text); outline-offset: 2px; } -.cta-cal-icon { font-size: 26px; line-height: 1; flex-shrink: 0; } -.cta-cal-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; } -.cta-cal-title { font-size: 16px; font-weight: 800; letter-spacing: .01em; } -.cta-cal-sub { font-size: 12.5px; font-weight: 600; opacity: .8; } -.cta-cal-arrow { margin-left: auto; font-size: 22px; font-weight: 800; flex-shrink: 0; } +.cta-cal-icon { line-height: 1; flex-shrink: 0; display: inline-flex; } +.cta-cal-arrow { font-size: 16px; font-weight: 800; flex-shrink: 0; } #chart-wrap { position: relative; margin-bottom: 22px; } #chart-wrap svg { width: 100%; height: auto; display: block; border-radius: 10px; touch-action: pan-y; } @@ -332,7 +350,7 @@ main { max-width: 1200px; margin: 0 auto; padding: 20px 24px 60px; } .range-refresh:hover { filter: brightness(1.05); } /* Time-filter panel: month/year range + season + year checklists, stacked in a - tidy column so nothing balloons on wide screens or leaves gaps on phones. */ + tidy column on phones. */ .cal-filters { display: flex; flex-direction: column; gap: 16px; max-width: 560px; margin: 0 0 18px; padding: 14px 16px; @@ -340,6 +358,25 @@ main { max-width: 1200px; margin: 0 auto; padding: 20px 24px 60px; } } /* Season + year dropdowns fill the row evenly — no dead space to their right. */ .cal-seasonyear { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; } +/* Desktop: the panel spans the content width as one row — the date range on the + left, the season/year/weather dropdowns filling the rest — instead of a narrow + stacked box with dead space beside it. */ +@media (min-width: 960px) { + .cal-filters { max-width: none; flex-direction: row; align-items: flex-start; gap: 14px 28px; } + .cal-range { flex: 0 1 400px; } + .cal-seasonyear { flex: 1 1 500px; grid-template-columns: 1fr 1fr 1.6fr; align-content: start; } + /* A head row mirroring the Range label, so both halves read as label + controls. */ + .cal-seasonyear::before { + content: "Filter by"; grid-column: 1 / -1; + font-size: 11px; text-transform: uppercase; letter-spacing: .04em; + color: var(--muted); font-weight: 700; line-height: 1.45; + } + /* Weather joins the same row as Seasons + Years (it only spans the full width + in the stacked phone layout — the extra .cal-seasonyear qualifier outranks + that base rule); its panel keeps a usable two-column width, anchored right. */ + .cal-seasonyear .cal-weather { grid-column: auto; } + .cal-seasonyear .cal-weather .cal-dd-panel { left: auto; right: 0; min-width: 420px; } +} .cal-filter-group { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; } .cal-filter-label { font-size: 11px; text-transform: uppercase; letter-spacing: .04em; @@ -370,11 +407,14 @@ main { max-width: 1200px; margin: 0 auto; padding: 20px 24px 60px; } border-radius: 999px; } -/* Months stacked vertically (one per row), with roomier cells. */ -.calendar { display: flex; flex-direction: column; gap: 18px; margin-bottom: 22px; align-items: stretch; } -/* Each month fills the available width (capped on wide desktops so the squares - don't balloon); the 7 columns split it evenly and cells stay square. */ -.cal-month { width: 100%; max-width: 560px; } +/* Months flow into as many columns as fit (one on phones, several on monitors), + so a multi-year span reads as a year-at-a-glance wall instead of one long + thin strip. Cells stay square via aspect-ratio. */ +.calendar { + display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 20px 24px; margin-bottom: 22px; align-items: start; +} +.cal-month { width: 100%; } .cal-month h4 { margin: 0 0 6px; font-size: 14px; color: var(--text); font-weight: 700; } .cal-dows, .cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 5px; } .cal-dows { margin-bottom: 3px; } @@ -444,9 +484,15 @@ main { max-width: 1200px; margin: 0 auto; padding: 20px 24px 60px; } .day-step:hover:not(:disabled) { border-color: var(--accent); } .day-step:disabled { opacity: .4; cursor: default; } +/* Metric cards: one column on phones, side by side on monitors so the page + doesn't run as a single long strip of full-width ladders. */ +#day-body { display: grid; grid-template-columns: 1fr; gap: 16px; align-items: start; } +@media (min-width: 1000px) { #day-body { grid-template-columns: repeat(2, 1fr); } } +@media (min-width: 3400px) { #day-body { grid-template-columns: repeat(3, 1fr); } } + .ladder-card { background: var(--surface); border: 1px solid var(--border); - border-radius: 13px; padding: 16px 16px 12px; margin-bottom: 16px; + border-radius: 13px; padding: 16px 16px 12px; } .ladder-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; flex-wrap: wrap; } .ladder-head h3 { margin: 0; font-size: 15px; } @@ -787,7 +833,7 @@ main { max-width: 1200px; margin: 0 auto; padding: 20px 24px 60px; } "Show count" is ticked, in that category's own status color) above a thin status- colored line rising from a shared baseline (height ∝ share). Every category draws a line — including the neutral "Normal" center of the temperature scale. */ -.cal-totals { max-width: 560px; margin: 0 0 18px; } +.cal-totals { max-width: 720px; margin: 0 0 18px; } /* Header row: the "N days shown — by X" title, with the share/count toggle at the far end. Wraps to its own line on narrow screens rather than crushing the title. */ .ct-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px 16px; flex-wrap: wrap; } @@ -911,9 +957,11 @@ main { max-width: 1200px; margin: 0 auto; padding: 20px 24px 60px; } .cmp-bar { display: flex; height: 14px; border-radius: 7px; overflow: hidden; margin: 12px 0 10px; background: var(--surface-2); } .cmp-seg { display: block; height: 100%; } -.cmp-below { background: var(--cold); } -.cmp-comfort { background: var(--normal); } -.cmp-above { background: var(--hot); } +/* Scoped to the bar segments — .cmp-comfort is also the comfort-slider param's + class, which must not inherit the green fill. */ +.cmp-seg.cmp-below { background: var(--cold); } +.cmp-seg.cmp-comfort { background: var(--normal); } +.cmp-seg.cmp-above { background: var(--hot); } .cmp-stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; } .cmp-stat { text-align: center; padding: 8px 4px; border-radius: 8px; background: var(--surface-2); }