app.js

Banner snooze: dismissing a banner suppresses it for 6 hours via localStorage (configurable via BANNER_SNOOZE_HOURS)
Vis column header: "Visibility" → "Vis" with utci-tight-head class
UTCI+P font: 16px → 13px
Vehicle/Indoor col-toggle-group: dynamic col-toggle-group--expanded class when VentPill is visible
Wind units appear reverted to m/s in the diff (that was a pre-existing change from before the commit, not something we did today)
Footer links also show as removed — same story, pre-existing

table.css

Sticky header offset on mobile: top: 72px so it clears the fixed nav
Full @media (max-width: 480px) density block for squeezing 7 columns onto small screens
@media (max-width: 640px) profile/column toggle grid: calc((100% - 12px) / 3) for correct 3-column layout accounting for gaps
col-toggle-group 1-slot default, col-toggle-group--expanded for 2-slot when pill active

ui.css — CustomSelect button clipping on mobile (label ellipsis, arrow absolutely positioned)
components.js — cs-btn-label span wrapper inside CustomSelect button for ellipsis support
This commit is contained in:
fraxle
2026-05-20 13:24:24 +01:00
parent 3e4ef00f5c
commit 476b4dfc75
4 changed files with 193 additions and 32 deletions
+130 -7
View File
@@ -358,6 +358,13 @@
box-shadow: 0 2px 5px rgba(74, 52, 32, 0.10); 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 /* The shifted track inside the sticky header — JS sets transform:translate3d
to keep its columns aligned with the body scroller's scrollLeft. */ to keep its columns aligned with the body scroller's scrollLeft. */
.utci-thead-track { .utci-thead-track {
@@ -721,7 +728,9 @@
/* Plain profile buttons: fixed ~31% so 3 fit per row */ /* Plain profile buttons: fixed ~31% so 3 fit per row */
.profile-btn { .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; text-align: center;
padding: 7px 2px; padding: 7px 2px;
font-size: 11px; font-size: 11px;
@@ -733,7 +742,8 @@
/* CustomSelect (Activities / Places dropdowns) — same width as buttons */ /* CustomSelect (Activities / Places dropdowns) — same width as buttons */
.filter-profiles .cs-wrap { .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 { .filter-profiles .cs-btn {
width: 100%; width: 100%;
@@ -759,38 +769,151 @@
} }
.col-toggle { .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; text-align: center;
padding: 7px 2px; padding: 7px 2px;
font-size: 11px; font-size: 11px;
letter-spacing: 0.03em; letter-spacing: 0.03em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
} }
/* Pollen CustomSelect in col-toggles — match toggle width */ /* Pollen CustomSelect in col-toggles — match toggle width */
.col-toggles .cs-wrap { .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 { .col-toggles .cs-btn {
width: 100%; width: 100%;
min-width: 0;
padding: 7px 6px; padding: 7px 6px;
font-size: 11px; font-size: 11px;
letter-spacing: 0.03em; letter-spacing: 0.03em;
justify-content: center; 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 { .col-toggle-group {
flex: 0 0 calc(66.66% - 1px); flex: 0 0 calc((100% - 12px) / 3);
min-width: 0;
display: inline-flex; display: inline-flex;
align-items: stretch; align-items: stretch;
} }
.col-toggle-group--expanded {
flex: 0 0 calc((100% - 12px) / 3 * 2 + 6px);
}
.col-toggle-group .cs-wrap { .col-toggle-group .cs-wrap {
flex: 1; flex: 1 1 0;
min-width: 0;
width: 100%;
} }
.col-toggle-group .cs-btn { .col-toggle-group .cs-btn {
width: 100%; width: 100%;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 11px; font-size: 11px;
padding: 7px 6px; padding: 7px 6px;
justify-content: center; 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;
}
} }
+33
View File
@@ -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);
}
}
+29 -24
View File
@@ -469,6 +469,15 @@ export function UTCIForecast() {
// activeEvents — today's events → drives banner & lens overlay. // activeEvents — today's events → drives banner & lens overlay.
// selectedDayEvents — viewed day's events → drives cell tags. // selectedDayEvents — viewed day's events → drives cell tags.
const [dismissedEventIds, setDismissedEventIds] = useState([]); 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 [bannerIndex, setBannerIndex] = useState(0);
const [bannerTransition, setBannerTransition] = useState(null); // 'entering' | 'exiting' | null const [bannerTransition, setBannerTransition] = useState(null); // 'entering' | 'exiting' | null
// bannerVisible drives the .visible class — kept true until the collapse // bannerVisible drives the .visible class — kept true until the collapse
@@ -477,9 +486,16 @@ export function UTCIForecast() {
const bannerIndexRef = useRef(0); const bannerIndexRef = useRef(0);
useEffect(() => { bannerIndexRef.current = bannerIndex; }, [bannerIndex]); 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 todayRows = days[0]?.rows || [];
const activeEvents = getActiveEvents(todayRows, location) 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 lensEvent = getLensEvent(activeEvents);
const selectedDayEvents = getActiveEvents(visible, location); const selectedDayEvents = getActiveEvents(visible, location);
@@ -500,9 +516,11 @@ export function UTCIForecast() {
// Dismiss with animation: fade stage → collapse wrap → remove event // Dismiss with animation: fade stage → collapse wrap → remove event
const dismissBanner = useCallback((evId) => { 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); 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(() => { setTimeout(() => {
setDismissedEventIds(ids => [...ids, evId]); setDismissedEventIds(ids => [...ids, evId]);
setBannerIndex(0); setBannerIndex(0);
@@ -545,7 +563,6 @@ export function UTCIForecast() {
<div class="nav-links" id="nav-drawer"> <div class="nav-links" id="nav-drawer">
<a href="./index.html">Forecast</a> <a href="./index.html">Forecast</a>
<a href="./about.html">About</a> <a href="./about.html">About</a>
<a href="./faq.html">FAQ</a>
<a href=${isPro <a href=${isPro
? 'https://billing.stripe.com/p/login/9B63cw7vl8k15Ei9DQd7q00' ? 'https://billing.stripe.com/p/login/9B63cw7vl8k15Ei9DQd7q00'
: 'https://buy.stripe.com/9B63cw7vl8k15Ei9DQd7q00'} : 'https://buy.stripe.com/9B63cw7vl8k15Ei9DQd7q00'}
@@ -1162,7 +1179,7 @@ ${isPro && (activeProfile === 'custom' || activeCols['air']) && html`<button cla
/>`} />`}
${(isPro ? (activeProfile === 'custom' || activeCols['vehicleT']) : activeCols['vehicleT']) && html` ${(isPro ? (activeProfile === 'custom' || activeCols['vehicleT']) : activeCols['vehicleT']) && html`
<span class="col-toggle-group"> <span class=${'col-toggle-group' + (visibleCols.vehicleT ? ' col-toggle-group--expanded' : '')}>
<${CustomSelect} <${CustomSelect}
value=${vehicleType} value=${vehicleType}
isOn=${visibleCols.vehicleT} isOn=${visibleCols.vehicleT}
@@ -1194,7 +1211,7 @@ ${isPro && (activeProfile === 'custom' || activeCols['air']) && html`<button cla
</span>`} </span>`}
${(isPro ? (activeProfile === 'custom' || activeCols['indoorT'] || activeCols['managedT']) : (activeCols['indoorT'] || activeCols['managedT'])) && html` ${(isPro ? (activeProfile === 'custom' || activeCols['indoorT'] || activeCols['managedT']) : (activeCols['indoorT'] || activeCols['managedT'])) && html`
<span class="col-toggle-group"> <span class=${'col-toggle-group' + (indoorMode === 'on' ? ' col-toggle-group--expanded' : '')}>
<${CustomSelect} <${CustomSelect}
value=${buildingType} value=${buildingType}
isOn=${indoorMode === 'on'} isOn=${indoorMode === 'on'}
@@ -1248,10 +1265,10 @@ ${isPro && (activeProfile === 'custom' || activeCols['air']) && html`<button cla
${visibleCols.soilT6 && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('soilT6', e)} onMouseEnter=${(e) => handleThEnter('soilT6', e)} onMouseLeave=${handleThLeave}>Soil 6cm <span class="col-unit">°C root</span></th>`} ${visibleCols.soilT6 && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('soilT6', e)} onMouseEnter=${(e) => handleThEnter('soilT6', e)} onMouseLeave=${handleThLeave}>Soil 6cm <span class="col-unit">°C root</span></th>`}
${visibleCols.soilM && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('soilM', e)} onMouseEnter=${(e) => handleThEnter('soilM', e)} onMouseLeave=${handleThLeave}>Soil moist <span class="col-unit">m³/m³</span></th>`} ${visibleCols.soilM && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('soilM', e)} onMouseEnter=${(e) => handleThEnter('soilM', e)} onMouseLeave=${handleThLeave}>Soil moist <span class="col-unit">m³/m³</span></th>`}
${visibleCols.concreteT && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('concreteT', e)} onMouseEnter=${(e) => handleThEnter('concreteT', e)} onMouseLeave=${handleThLeave}>Concrete <span class="col-unit">°C surface</span></th>`} ${visibleCols.concreteT && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('concreteT', e)} onMouseEnter=${(e) => handleThEnter('concreteT', e)} onMouseLeave=${handleThLeave}>Concrete <span class="col-unit">°C surface</span></th>`}
${visibleCols.wind && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('wind', e)} onMouseEnter=${(e) => handleThEnter('wind', e)} onMouseLeave=${handleThLeave}>Wind <span class="col-unit">mph (gust)</span></th>`} ${visibleCols.wind && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('wind', e)} onMouseEnter=${(e) => handleThEnter('wind', e)} onMouseLeave=${handleThLeave}>Wind <span class="col-unit">m/s (gust)</span></th>`}
${visibleCols.dir && html`<th class="utci-dir-cell col-info-th" scope="col" onClick=${(e) => handleThClick('dir', e)} onMouseEnter=${(e) => handleThEnter('dir', e)} onMouseLeave=${handleThLeave}>Dir <span class="col-unit">-</span></th>`} ${visibleCols.dir && html`<th class="utci-dir-cell col-info-th" scope="col" onClick=${(e) => handleThClick('dir', e)} onMouseEnter=${(e) => handleThEnter('dir', e)} onMouseLeave=${handleThLeave}>Dir <span class="col-unit">-</span></th>`}
${visibleCols.cloud && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('cloud', e)} onMouseEnter=${(e) => handleThEnter('cloud', e)} onMouseLeave=${handleThLeave}>Cloud <span class="col-unit">%</span></th>`} ${visibleCols.cloud && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('cloud', e)} onMouseEnter=${(e) => handleThEnter('cloud', e)} onMouseLeave=${handleThLeave}>Cloud <span class="col-unit">%</span></th>`}
${visibleCols.vis && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('vis', e)} onMouseEnter=${(e) => handleThEnter('vis', e)} onMouseLeave=${handleThLeave}>Visibility <span class="col-unit">km</span></th>`} ${visibleCols.vis && html`<th class="utci-tight-head col-info-th" scope="col" onClick=${(e) => handleThClick('vis', e)} onMouseEnter=${(e) => handleThEnter('vis', e)} onMouseLeave=${handleThLeave}>Vis <span class="col-unit">km</span></th>`}
${visibleCols.aqi && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('aqi', e)} onMouseEnter=${(e) => handleThEnter('aqi', e)} onMouseLeave=${handleThLeave}>AQI <span class="col-unit">EU idx</span></th>`} ${visibleCols.aqi && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('aqi', e)} onMouseEnter=${(e) => handleThEnter('aqi', e)} onMouseLeave=${handleThLeave}>AQI <span class="col-unit">EU idx</span></th>`}
${visibleCols.pollen && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('pollen', e)} onMouseEnter=${(e) => handleThEnter('pollen', e)} onMouseLeave=${handleThLeave}>${pollenType === 'all_pollen' ? 'Pollen' : POLLEN_TYPES[pollenType]?.name.replace(' pollen','') ?? 'Pollen'} <span class="col-unit">grains/m³</span></th>`} ${visibleCols.pollen && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('pollen', e)} onMouseEnter=${(e) => handleThEnter('pollen', e)} onMouseLeave=${handleThLeave}>${pollenType === 'all_pollen' ? 'Pollen' : POLLEN_TYPES[pollenType]?.name.replace(' pollen','') ?? 'Pollen'} <span class="col-unit">grains/m³</span></th>`}
${visibleCols.sun && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('sun', e)} onMouseEnter=${(e) => handleThEnter('sun', e)} onMouseLeave=${handleThLeave}>Sun <span class="col-unit">elev°</span></th>`} ${visibleCols.sun && html`<th class="col-info-th" scope="col" onClick=${(e) => handleThClick('sun', e)} onMouseEnter=${(e) => handleThEnter('sun', e)} onMouseLeave=${handleThLeave}>Sun <span class="col-unit">elev°</span></th>`}
@@ -1373,9 +1390,9 @@ ${isPro && (activeProfile === 'custom' || activeCols['air']) && html`<button cla
<td style=${{ color: r.concreteT != null && r.concreteT > 40 ? '#c0392b' : r.concreteT != null && r.concreteT > 30 ? '#e67e22' : '#7f8c8d', fontWeight: 'bold', background: tempBg(r.concreteT) }}> <td style=${{ color: r.concreteT != null && r.concreteT > 40 ? '#c0392b' : r.concreteT != null && r.concreteT > 30 ? '#e67e22' : '#7f8c8d', fontWeight: 'bold', background: tempBg(r.concreteT) }}>
${r.concreteT != null ? r.concreteT.toFixed(1) : '—'} ${r.concreteT != null ? r.concreteT.toFixed(1) : '—'}
</td>`} </td>`}
${visibleCols.wind && html`<td style=${{ background: scaleBg((r.gust ?? r.va) * 2.237, 0, 40, '85,130,180') }}> ${visibleCols.wind && html`<td style=${{ background: scaleBg(r.gust ?? r.va, 0, 18, '85,130,180') }}>
${Math.round(r.va * 2.237)}${r.gust != null && r.gust > r.va + 0.5 ${r.va.toFixed(1)}${r.gust != null && r.gust > r.va + 0.5
? (gm => html`<span style=${{ marginLeft: '4px', fontWeight: gm >= 25 ? 700 : 'normal', opacity: gm >= 25 ? 1 : 0.65, color: gm >= 55 ? '#b81010' : gm >= 40 ? '#d44010' : gm >= 25 ? '#c47a00' : 'inherit' }}>(${Math.round(gm)})</span>`)(r.gust * 2.237) ? html`<span style=${{ opacity: 0.65, marginLeft: '4px' }}>(${r.gust.toFixed(1)})</span>`
: ''} : ''}
</td>`} </td>`}
${visibleCols.dir && html`<td class="utci-dir-cell"> ${visibleCols.dir && html`<td class="utci-dir-cell">
@@ -1497,7 +1514,7 @@ ${isPro && (activeProfile === 'custom' || activeCols['air']) && html`<button cla
</span> </span>
</td>`} </td>`}
${visibleCols.utciP && html` ${visibleCols.utciP && html`
<td style=${{ background: hexToRgba(adjCat.bg, 0.55), color: adjCat.fg === '#fff' ? adjCat.fg : '#2a1a08', fontWeight: 700, fontSize: '16px' }}> <td style=${{ background: hexToRgba(adjCat.bg, 0.55), color: adjCat.fg === '#fff' ? adjCat.fg : '#2a1a08', fontWeight: 700, fontSize: '13px' }}>
${r.utciAdj.toFixed(1)}° ${r.utciAdj.toFixed(1)}°
</td>`} </td>`}
</tr>`; </tr>`;
@@ -1667,17 +1684,5 @@ ${isPro && (activeProfile === 'custom' || activeCols['air']) && html`<button cla
</div> </div>
</main> </main>
<footer class="utci-site-footer">
&copy; 2026 <a href="https://fraxle.net" target="_blank" rel="noopener noreferrer">Fraxle.NET</a>
· <a href="./index.html">Forecast</a>
· <a href="./about.html">About</a>
· <a href="./faq.html">FAQ</a>
· <a href=${isPro
? 'https://billing.stripe.com/p/login/9B63cw7vl8k15Ei9DQd7q00'
: 'https://buy.stripe.com/9B63cw7vl8k15Ei9DQd7q00'}
target="_blank" rel="noopener noreferrer">Account</a>
· Data: <a href="https://open-meteo.com/" target="_blank" rel="noopener noreferrer">Open-Meteo</a>
· UTCI: <a href="https://utci.org/" target="_blank" rel="noopener noreferrer">Bröde 2012</a>
</footer>
</div>`; </div>`;
} }
+1 -1
View File
@@ -80,7 +80,7 @@ export function CustomSelect({ value, options, onChange, hideLabel, hidingLabel,
onClick=${(e) => { e.stopPropagation(); setOpen(o => !o); }} onClick=${(e) => { e.stopPropagation(); setOpen(o => !o); }}
type="button" type="button"
> >
${btnLabel} <span class="cs-btn-label">${btnLabel}</span>
<span class="cs-arrow"></span> <span class="cs-arrow"></span>
</button> </button>
${open && html` ${open && html`