Tweaks and new pulldowns
This commit is contained in:
fraxle
2026-05-15 18:09:45 +01:00
parent f57be65f95
commit ebd4b7d7b7
5 changed files with 422 additions and 90 deletions
+108
View File
@@ -14,6 +14,7 @@
// ════════════════════════════════════════════════════════════════════════
import { h, Fragment } from '../vendor/preact.js';
import { useState, useEffect, useRef } from '../vendor/preact-hooks.js';
import htm from '../vendor/htm.js';
import {
skyGradientForElev, skyFillForElev, grassFillForElev,
@@ -22,6 +23,113 @@ import {
const html = htm.bind(h);
// ═══════════════════════════════════════════════════════════════════
// CUSTOMSELECT — cross-platform styled dropdown pill.
// ───────────────────────────────────────────────────────────────────
// props:
// value — current value string
// options — array of { value, label, disabled }
// onChange — fn(newValue)
// hideLabel — label shown when value === 'off' (e.g. 'Burn')
// hidingLabel — label shown for the 'off' option when active (e.g. 'Hide Burn')
// isOn — bool — whether the pill shows as active (brass)
// groupedLeft — bool — fuse right side with a VentPill
// isLastChild — bool — restore right border-radius when no sibling follows
// ═══════════════════════════════════════════════════════════════════
export function CustomSelect({ value, options, onChange, hideLabel, hidingLabel, isOn, groupedLeft, isLastChild }) {
const [open, setOpen] = useState(false);
const wrapRef = useRef(null);
// Close on outside click or Escape
useEffect(() => {
if (!open) return;
const onDown = (e) => {
if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false);
};
const onKey = (e) => { if (e.key === 'Escape') setOpen(false); };
document.addEventListener('mousedown', onDown);
document.addEventListener('touchstart', onDown);
document.addEventListener('keydown', onKey);
return () => {
document.removeEventListener('mousedown', onDown);
document.removeEventListener('touchstart', onDown);
document.removeEventListener('keydown', onKey);
};
}, [open]);
// Build the label shown on the button
const activeOpt = options.find(o => o.value === value);
const btnLabel = isOn
? (activeOpt ? activeOpt.label : hideLabel)
: hideLabel;
const btnClass = [
'cs-btn',
isOn ? 'on' : '',
groupedLeft ? 'grouped-left' : '',
(groupedLeft && isLastChild) ? 'last-child' : '',
].filter(Boolean).join(' ');
return html`
<span class=${`cs-wrap${open ? ' open' : ''}`} ref=${wrapRef}>
<button
class=${btnClass}
onClick=${(e) => { e.stopPropagation(); setOpen(o => !o); }}
type="button"
>
${btnLabel}
<span class="cs-arrow">▾</span>
</button>
${open && html`
<div class="cs-panel" role="listbox">
${isOn && html`
<span
class="cs-option"
role="option"
onClick=${() => { onChange('off'); setOpen(false); }}
>${hidingLabel || ('Hide ' + hideLabel)}</span>
<div class="cs-divider"></div>
`}
${options.map(opt => html`
<span
key=${opt.value}
class=${`cs-option${opt.value === value && isOn ? ' selected' : ''}${opt.disabled ? ' disabled' : ''}`}
role="option"
onClick=${() => {
if (opt.disabled) return;
onChange(opt.value);
setOpen(false);
}}
>${opt.label}</span>
`)}
</div>
`}
</span>`;
}
// ═══════════════════════════════════════════════════════════════════
// VENTPILL — the checkbox pill fused to the right of a CustomSelect.
// ───────────────────────────────────────────────────────────────────
// props:
// checked — bool
// onChange — fn()
// label — string
// title — tooltip string
// ═══════════════════════════════════════════════════════════════════
export function VentPill({ checked, onChange, label, title }) {
return html`
<label class=${`cs-vent${checked ? ' on' : ''}`} title=${title}>
<input
type="checkbox"
checked=${checked}
onChange=${onChange}
style=${{ position: 'absolute', opacity: 0, width: 0, height: 0, pointerEvents: 'none' }}
/>
<span class=${`cs-checkbox${checked ? ' checked' : ''}`}></span>
${label}
</label>`;
}
// ═══════════════════════════════════════════════════════════════════
// SKYSCOPE — the little porthole circle next to each hour.
// ───────────────────────────────────────────────────────────────────