diff --git a/assets/css/table.css b/assets/css/table.css index e4ccb30..2871146 100644 --- a/assets/css/table.css +++ b/assets/css/table.css @@ -358,6 +358,13 @@ box-shadow: 0 2px 5px rgba(74, 52, 32, 0.10); } +/* On mobile the nav bar is fixed at 72px - sticky header must clear it */ +@media (max-width: 640px) { + .utci-thead-sticky { + top: 72px; + } +} + /* The shifted track inside the sticky header — JS sets transform:translate3d to keep its columns aligned with the body scroller's scrollLeft. */ .utci-thead-track { @@ -721,7 +728,9 @@ /* Plain profile buttons: fixed ~31% so 3 fit per row */ .profile-btn { - flex: 0 0 calc(33.33% - 4px); + flex: 0 0 calc((100% - 12px) / 3); + min-width: 0; + box-sizing: border-box; text-align: center; padding: 7px 2px; font-size: 11px; @@ -733,7 +742,8 @@ /* CustomSelect (Activities / Places dropdowns) — same width as buttons */ .filter-profiles .cs-wrap { - flex: 0 0 calc(33.33% - 4px); + flex: 0 0 calc((100% - 12px) / 3); + min-width: 0; } .filter-profiles .cs-btn { width: 100%; @@ -759,38 +769,151 @@ } .col-toggle { - flex: 0 0 calc(33.33% - 4px); + flex: 0 0 calc((100% - 12px) / 3); + min-width: 0; + box-sizing: border-box; text-align: center; padding: 7px 2px; font-size: 11px; letter-spacing: 0.03em; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } /* Pollen CustomSelect in col-toggles — match toggle width */ .col-toggles .cs-wrap { - flex: 0 0 calc(33.33% - 4px); + flex: 0 0 calc((100% - 12px) / 3); + min-width: 0; } .col-toggles .cs-btn { width: 100%; + min-width: 0; padding: 7px 6px; font-size: 11px; letter-spacing: 0.03em; justify-content: center; + overflow: hidden; } - /* Fused vehicle group (select + vent pill): spans 2 slots = ~65% */ + /* Fused group: 1 slot by default, 2 slots when VentPill is active */ .col-toggle-group { - flex: 0 0 calc(66.66% - 1px); + flex: 0 0 calc((100% - 12px) / 3); + min-width: 0; display: inline-flex; align-items: stretch; } + .col-toggle-group--expanded { + flex: 0 0 calc((100% - 12px) / 3 * 2 + 6px); + } .col-toggle-group .cs-wrap { - flex: 1; + flex: 1 1 0; + min-width: 0; + width: 100%; } .col-toggle-group .cs-btn { width: 100%; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; font-size: 11px; padding: 7px 6px; justify-content: center; } + +} + +/* ── MOBILE TABLE DENSITY — squeeze all basic profile columns in ──────── + Target: 7 cols (hour air wind cloud utciP precip vis) on a 390px screen. + Worst-case minimum width at these values: 366px - fits iPhone SE (375px). + Levers: tighter cell padding, smaller min-width, reduced font sizes. + The existing 800px and 640px blocks handle tablets and phablets above. + DENSITY-BREAKPOINT - change 480px here to adjust where dense layout kicks in. + -------------------------------------------------------------------- */ + +/* -- MOBILE TABLE DENSITY - squeeze all basic profile columns in -------- + Target: 7 cols (hour air wind cloud utciP precip vis) on a 390px screen. + Worst-case minimum width: 366px - fits iPhone SE (375px) with room to spare. + DENSITY-BREAKPOINT - change 480px to adjust where dense layout kicks in. + -------------------------------------------------------------------- */ +@media (max-width: 480px) { + + /* -- Table header cells ----------------------------------------------- + Reduce padding so headers do not drive columns wider than data needs. */ + .utci-table th { + font-size: 9px; + padding: 10px 4px; + letter-spacing: 0.04em; + } + + .utci-table th .col-unit { + font-size: 8px; + } + + /* -- Table body cells ------------------------------------------------- + Drop side padding 12px to 4px, shrink min-width 60px to 44px. + JetBrains Mono at 11px reads cleanly on retina screens. */ + .utci-table td { + padding: 8px 4px; + font-size: 11px; + min-width: 44px; + } + + /* -- Time (Hour) column ----------------------------------------------- + Drop Fraunces from 16px to 13px and tighten the hour cell min-width. */ + .utci-time { + font-size: 13px; + } + + .utci-table td:first-child { + min-width: 42px; + } + + /* -- SkyScope moon-sun icon in the hour cell --------------------------- + JS renders at size=30. Clamp to 20px via CSS to stop it ballooning + the hour column width and driving up row height. */ + .utci-time svg { + width: 20px !important; + height: 20px !important; + flex-shrink: 0; + } + + /* -- Cloud, wind-vane and precip icons in body cells ------------------- + Clamp the 28px SVGs down to 20px on mobile to recover column width + without touching the JS size prop. */ + .utci-table td svg { + width: 20px !important; + height: 20px !important; + flex-shrink: 0; + } + + /* -- Hero UTCI+P pill ------------------------------------------------- + Shrink min-width and font - still the visual anchor but no longer + forcing the column to be the widest. */ + .utci-cell-hero { + font-size: 14px; + min-width: 50px; + padding: 5px 2px; + } + + /* -- Vis column ----------------------------------------------------------- + Short values (8-99km) need less room than the default 44px min-width. */ + .utci-vis-cell { + min-width: 30px; + } + + /* -- Standard UTCI pill (non-hero) ------------------------------------*/ + .utci-cell { + font-size: 12px; + min-width: 44px; + padding: 3px 5px; + } + + /* -- Band label inside pills ------------------------------------------ + Tighten letter-spacing to save a little width. */ + .utci-band-label { + font-size: 9px; + letter-spacing: 0.03em; + } } diff --git a/assets/css/ui.css b/assets/css/ui.css index 7852d21..51d2b39 100644 --- a/assets/css/ui.css +++ b/assets/css/ui.css @@ -715,3 +715,36 @@ } } + +/* -- CUSTOM SELECT BUTTON - mobile label clipping ---------------------- + On narrow screens buttons are fixed ~33% width. The .cs-btn-label span + wraps the text node so ellipsis can work (text-overflow needs a block + context - bare text nodes inside flex containers cannot clip directly). + The cs-arrow is flex-shrink-0 so the chevron always stays visible. + DENSITY-BREAKPOINT - matches the table.css 480px breakpoint. + -------------------------------------------------------------------- */ +@media (max-width: 480px) { + .cs-btn { + display: block !important; + position: relative !important; + overflow: hidden; + padding: 7px 6px !important; + white-space: nowrap; + } + + .cs-btn-label { + display: block; + width: calc(100% - 15px); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .cs-btn .cs-arrow { + position: absolute !important; + right: 6px; + top: 50%; + margin: 0; + transform: translateY(-50%) rotate(45deg); + } +} diff --git a/assets/js/app.js b/assets/js/app.js index 9bd4dd8..784a80d 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -469,6 +469,15 @@ export function UTCIForecast() { // activeEvents — today's events → drives banner & lens overlay. // selectedDayEvents — viewed day's events → drives cell tags. const [dismissedEventIds, setDismissedEventIds] = useState([]); + + // ------------------------------------------------------------------ + // BANNER_SNOOZE_HOURS - how long to suppress a banner after dismissal. + // Change this value to adjust the snooze duration. Future builds can + // expose this as a user-facing setting and pass the value in here. + // ------------------------------------------------------------------ + const BANNER_SNOOZE_HOURS = 6; + const BANNER_SNOOZE_MS = BANNER_SNOOZE_HOURS * 60 * 60 * 1000; + const BANNER_SNOOZE_KEY = id => `sunscope.banner.snoozed.${id}`; const [bannerIndex, setBannerIndex] = useState(0); const [bannerTransition, setBannerTransition] = useState(null); // 'entering' | 'exiting' | null // bannerVisible drives the .visible class — kept true until the collapse @@ -477,9 +486,16 @@ export function UTCIForecast() { const bannerIndexRef = useRef(0); useEffect(() => { bannerIndexRef.current = bannerIndex; }, [bannerIndex]); + const isSnoozed = id => { + try { + const ts = localStorage.getItem(BANNER_SNOOZE_KEY(id)); + return ts && (Date.now() - Number(ts)) < BANNER_SNOOZE_MS; + } catch (e) { return false; } + }; + const todayRows = days[0]?.rows || []; const activeEvents = getActiveEvents(todayRows, location) - .filter(ev => !dismissedEventIds.includes(ev.id)); + .filter(ev => !dismissedEventIds.includes(ev.id) && !isSnoozed(ev.id)); const lensEvent = getLensEvent(activeEvents); const selectedDayEvents = getActiveEvents(visible, location); @@ -500,9 +516,11 @@ export function UTCIForecast() { // Dismiss with animation: fade stage → collapse wrap → remove event const dismissBanner = useCallback((evId) => { - // 1. fade the stage out (CSS handles this via :not(.visible)) + // 1. Record the dismissal time so we can suppress for BANNER_SNOOZE_HOURS + try { localStorage.setItem(BANNER_SNOOZE_KEY(evId), String(Date.now())); } catch (e) {} + // 2. Fade the stage out (CSS handles this via .visible class removal) setBannerVisible(false); - // 2. after wrap has collapsed (grid transition ~450ms), remove the event + // 3. After wrap has collapsed (grid transition ~450ms), remove the event setTimeout(() => { setDismissedEventIds(ids => [...ids, evId]); setBannerIndex(0); @@ -545,7 +563,6 @@ export function UTCIForecast() {
`; } diff --git a/assets/js/components.js b/assets/js/components.js index e4cc19f..ae54fba 100644 --- a/assets/js/components.js +++ b/assets/js/components.js @@ -80,7 +80,7 @@ export function CustomSelect({ value, options, onChange, hideLabel, hidingLabel, onClick=${(e) => { e.stopPropagation(); setOpen(o => !o); }} type="button" > - ${btnLabel} + ${btnLabel} ${open && html`