Support multi-warning Brīdinājumi selection with narrower, solid-color chips

This commit is contained in:
b0txec
2026-08-22 21:51:09 +03:00
parent c563012df7
commit 56b871606f
3 changed files with 55 additions and 27 deletions
+25 -9
View File
@@ -52,8 +52,13 @@ export function Warnings() {
const phenomenonMatches = phenomenon() === 'all' || warning.phenomenon === phenomenon(); const phenomenonMatches = phenomenon() === 'all' || warning.phenomenon === phenomenon();
return dateMatches && phenomenonMatches; return dateMatches && phenomenonMatches;
})); }));
const [selectedId, setSelectedId] = createSignal<string>(); const [selectedIds, setSelectedIds] = createSignal<Set<string>>(new Set());
const selectedWarning = createMemo(() => filteredWarnings().find(item => item.id === selectedId())); const selectedWarnings = createMemo(() => filteredWarnings().filter(item => selectedIds().has(item.id)));
const toggleSelected = (id: string) => setSelectedIds(prev => {
const next = new Set(prev);
if (next.has(id)) next.delete(id); else next.add(id);
return next;
});
const [detailId, setDetailId] = createSignal<string>(); const [detailId, setDetailId] = createSignal<string>();
const detailWarning = createMemo(() => filteredWarnings().find(item => item.id === detailId())); const detailWarning = createMemo(() => filteredWarnings().find(item => item.id === detailId()));
const closeDetail = () => setDetailId(undefined); const closeDetail = () => setDetailId(undefined);
@@ -84,22 +89,30 @@ export function Warnings() {
createEffect(() => { output(); loadArtwork(); }); createEffect(() => { output(); loadArtwork(); });
createEffect(() => { createEffect(() => {
const list = filteredWarnings(); const list = filteredWarnings();
if (!list.some(item => item.id === selectedId())) setSelectedId(list[0]?.id); const validIds = new Set(list.map(item => item.id));
setSelectedIds(prev => {
let changed = false;
const next = new Set<string>();
prev.forEach(id => { if (validIds.has(id)) next.add(id); else changed = true; });
if (next.size === 0 && list.length > 0) { next.add(list[0].id); changed = true; }
return changed ? next : prev;
}); });
});
const primarySelected = createMemo(() => selectedWarnings()[0]);
createEffect(() => { createEffect(() => {
const warning = selectedWarning(); const warning = primarySelected();
setTitle(warning ? defaultTitle(warning.phenomenon) : ''); setTitle(warning ? defaultTitle(warning.phenomenon) : '');
}); });
createEffect(() => { createEffect(() => {
const images = artwork(); const images = artwork();
const current = template(); const current = template();
const warning = selectedWarning(); const warnings = selectedWarnings();
const currentTitle = title(); const currentTitle = title();
if (!canvas || !images) return; if (!canvas || !images) return;
if (canvas.width !== current.width) canvas.width = current.width; if (canvas.width !== current.width) canvas.width = current.width;
if (canvas.height !== current.height) canvas.height = current.height; if (canvas.height !== current.height) canvas.height = current.height;
void document.fonts.load(`700 ${current.title.fontSize}px Monda`).then(() => void document.fonts.load(`700 ${current.title.fontSize}px Monda`).then(() =>
drawWarningMap(canvas.getContext('2d')!, images.background, images.border, current, warning, currentTitle), drawWarningMap(canvas.getContext('2d')!, images.background, images.border, current, warnings, currentTitle),
); );
}); });
@@ -107,7 +120,7 @@ export function Warnings() {
return <section class="pageWorkspace warningsPage"> return <section class="pageWorkspace warningsPage">
<div class="pageHeading warningsHeading"> <div class="pageHeading warningsHeading">
<div><span class="eyebrow">ĒTERA GRAFIKA</span><h1>Brīdinājumi</h1><p>Izvēlies vienu LVĢMC brīdinājumu, kas jārāda kartē; virsraksts aizpildās automātiski.</p></div> <div><span class="eyebrow">ĒTERA GRAFIKA</span><h1>Brīdinājumi</h1><p>Atzīmē LVĢMC brīdinājumus, kas jārāda kartē; virsraksts aizpildās automātiski.</p></div>
<span class="selectionCount">{filteredWarnings().length} {filteredWarnings().length === 1 ? 'brīdinājums' : 'brīdinājumi'}</span> <span class="selectionCount">{filteredWarnings().length} {filteredWarnings().length === 1 ? 'brīdinājums' : 'brīdinājumi'}</span>
</div> </div>
@@ -128,8 +141,11 @@ export function Warnings() {
<Show when={!data.loading && !data.error && filteredWarnings().length === 0}><div class="inlineNotice">Izvēlētajam periodam nav aktīvu brīdinājumu. Karti joprojām var priekšskatīt, bet tajā nebūs iekrāsotu teritoriju.</div></Show> <Show when={!data.loading && !data.error && filteredWarnings().length === 0}><div class="inlineNotice">Izvēlētajam periodam nav aktīvu brīdinājumu. Karti joprojām var priekšskatīt, bet tajā nebūs iekrāsotu teritoriju.</div></Show>
<Show when={filteredWarnings().length > 0}><div class="warningSummary"><For each={filteredWarnings()}>{warning => <Show when={filteredWarnings().length > 0}><div class="warningSummary"><For each={filteredWarnings()}>{warning =>
<div class={`warningCard ${warningSeverity(warning.intensity)}`} classList={{ active: warning.id === selectedId() }}> <div class={`warningCard ${warningSeverity(warning.intensity)}`} classList={{ active: selectedIds().has(warning.id) }}>
<input type="checkbox" class="warningCardCheck" checked={warning.id === selectedId()} title="Rādīt šo brīdinājumu kartē" aria-label={`Rādīt ${warning.phenomenon || 'šo brīdinājumu'} kartē`} onClick={() => setSelectedId(warning.id)}/> <label class="warningCheckboxLabel" title="Rādīt šo brīdinājumu kartē">
<input type="checkbox" class="warningCheckboxInput" checked={selectedIds().has(warning.id)} aria-label={`Rādīt ${warning.phenomenon || 'šo brīdinājumu'} kartē`} onChange={() => toggleSelected(warning.id)}/>
<span class="warningCheckmark"/>
</label>
<button type="button" class="warningCardOpen" onClick={() => setDetailId(warning.id)}> <button type="button" class="warningCardOpen" onClick={() => setDetailId(warning.id)}>
<span class="warningCardIcon"><PhenomenonIcon phenomenon={warning.phenomenon}/></span> <span class="warningCardIcon"><PhenomenonIcon phenomenon={warning.phenomenon}/></span>
<span class="warningCardText">{warning.phenomenon || 'Brīdinājums'}</span> <span class="warningCardText">{warning.phenomenon || 'Brīdinājums'}</span>
+11 -2
View File
@@ -35,7 +35,7 @@ export function drawWarningMap(
background: HTMLImageElement, background: HTMLImageElement,
border: HTMLImageElement, border: HTMLImageElement,
template: WarningTemplate, template: WarningTemplate,
warning: WeatherWarning | undefined, warnings: WeatherWarning[],
title: string, title: string,
) { ) {
ctx.clearRect(0, 0, template.width, template.height); ctx.clearRect(0, 0, template.width, template.height);
@@ -46,7 +46,12 @@ export function drawWarningMap(
warningLayer.height = template.height; warningLayer.height = template.height;
const warningContext = warningLayer.getContext('2d')!; const warningContext = warningLayer.getContext('2d')!;
if (warning) drawWarning(warningContext, template, warning); // Lower severities draw first so a higher one painting the same area on
// top reads as the dominant color, matching how newsroom combined maps
// (e.g. yellow + orange wind together) are meant to look.
[...warnings]
.sort((a, b) => severityRank(warningSeverity(a.intensity)) - severityRank(warningSeverity(b.intensity)))
.forEach(warning => drawWarning(warningContext, template, warning));
ctx.save(); ctx.save();
ctx.filter = `blur(${template.feather}px)`; ctx.filter = `blur(${template.feather}px)`;
@@ -60,6 +65,10 @@ export function drawWarningMap(
drawTitle(ctx, template, title); drawTitle(ctx, template, title);
} }
function severityRank(severity: Severity) {
return severity === 'red' ? 3 : severity === 'orange' ? 2 : 1;
}
function drawWarning(ctx: CanvasRenderingContext2D, template: WarningTemplate, warning: WeatherWarning) { function drawWarning(ctx: CanvasRenderingContext2D, template: WarningTemplate, warning: WeatherWarning) {
const severity = warningSeverity(warning.intensity); const severity = warningSeverity(warning.intensity);
const style = severityStyles[severity]; const style = severityStyles[severity];
+19 -16
View File
@@ -1,21 +1,24 @@
.warningsPage{max-width:1900px}.warningsHeading{margin-bottom:18px}.warningsSetup{display:grid;grid-template-columns:260px minmax(0,1fr);gap:28px;padding:18px 0 24px;border-top:1px solid var(--border);border-bottom:1px solid var(--border)}.warningsSetup h2{margin:0 0 14px;font-size:20px}.warningFilterGrid{display:grid;grid-template-columns:220px 260px minmax(320px,1fr);gap:12px}.warningFilterGrid label{display:grid;gap:5px}.warningFilterGrid label span{font-size:12px;color:var(--text-muted)}.warningTitle input{width:100%}.warningsPreview{padding-top:24px}.warningsPreview canvas{display:block;width:100%;height:auto}.inlineNotice button{margin-left:14px} .warningsPage{max-width:1900px}.warningsHeading{margin-bottom:18px}.warningsSetup{display:grid;grid-template-columns:260px minmax(0,1fr);gap:28px;padding:18px 0 24px;border-top:1px solid var(--border);border-bottom:1px solid var(--border)}.warningsSetup h2{margin:0 0 14px;font-size:20px}.warningFilterGrid{display:grid;grid-template-columns:220px 260px minmax(320px,1fr);gap:12px}.warningFilterGrid label{display:grid;gap:5px}.warningFilterGrid label span{font-size:12px;color:var(--text-muted)}.warningTitle input{width:100%}.warningsPreview{padding-top:24px}.warningsPreview canvas{display:block;width:100%;height:auto}.inlineNotice button{margin-left:14px}
.warningSummary{display:flex;flex-wrap:wrap;gap:10px;margin:18px 0} .warningSummary{display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin:18px 0}
.warningCard{position:relative;display:flex;align-items:center;height:44px;flex:0 1 220px;border:1.5px solid var(--border);border-radius:12px;background:rgba(255,255,255,.65);overflow:hidden;transition:border-color 200ms,box-shadow 200ms} .warningCard{position:relative;display:flex;align-items:center;gap:9px;height:44px;flex:0 0 auto;padding:0 12px 0 10px;border:1.5px solid var(--border);border-radius:12px;background:rgba(255,255,255,.65);transition:box-shadow 200ms}
.warningCardCheck{flex:0 0 auto;margin:0 0 0 12px} .warningCardOpen{position:relative;display:flex;align-items:center;justify-content:center;gap:8px;height:32px;padding:0 16px;border:none;border-radius:8px;appearance:none;font:inherit;color:#fff;cursor:pointer;transition:filter 150ms}
.warningCardOpen{position:relative;flex:1 1 auto;align-self:stretch;display:flex;align-items:center;padding:0 14px 0 40px;border:none;background:none;appearance:none;font:inherit;color:var(--text);text-align:left;cursor:pointer} .warningCardOpen:hover{filter:brightness(1.08)}
.warningCardIcon{position:absolute;left:0;top:0;width:36px;height:100%;display:flex;align-items:center;justify-content:center;color:#fff;transition:width 300ms ease} .warningCardOpen:active{filter:brightness(.92)}
.warningCardText{position:relative;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:14px;font-weight:600;transition:color 300ms ease} .warningCardIcon{display:flex;align-items:center}
.warningCard.yellow{border-color:#caa000} .warningCardText{font-size:14px;font-weight:600;white-space:nowrap}
.warningCard.orange{border-color:#c1580f} .warningCard.yellow .warningCardOpen{background:#caa000}
.warningCard.red{border-color:#b91c1c} .warningCard.orange .warningCardOpen{background:#c1580f}
.warningCard.yellow .warningCardIcon{background:#caa000} .warningCard.red .warningCardOpen{background:#b91c1c}
.warningCard.orange .warningCardIcon{background:#c1580f} .warningCard.active{box-shadow:0 0 0 3px var(--accent-pale)}
.warningCard.red .warningCardIcon{background:#b91c1c}
.warningCardOpen:hover .warningCardIcon{width:100%} .warningCheckboxLabel{position:relative;display:inline-flex;flex:0 0 auto;width:22px;height:22px;cursor:pointer}
.warningCardOpen:hover .warningCardText{color:transparent} .warningCheckboxLabel .warningCheckboxInput{position:absolute;opacity:0;height:0;width:0;margin:0}
.warningCardOpen:active .warningCardIcon{filter:brightness(.88)} .warningCheckmark{position:absolute;inset:0;background-color:#eee;border-radius:5px;transition:background-color .3s;box-shadow:0 2px 5px rgba(0,0,0,.2)}
.warningCard.active{background:rgba(255,255,255,.95);box-shadow:0 0 0 3px var(--accent-pale)} .warningCheckmark:after{content:"";position:absolute;display:none;left:7px;top:3px;width:5px;height:10px;border:solid #fff;border-width:0 3px 3px 0;transform:rotate(45deg)}
.warningCheckboxInput:checked ~ .warningCheckmark{background-color:#2196F3;box-shadow:0 3px 7px rgba(33,150,243,.3)}
.warningCheckboxInput:checked ~ .warningCheckmark:after{display:block;animation:warningCheckAnim .2s forwards}
@keyframes warningCheckAnim{0%{height:0}100%{height:10px}}
.warningBackdrop{position:fixed;inset:0;z-index:1000;display:flex;align-items:center;justify-content:center;padding:24px;background:rgba(10,20,35,.55);backdrop-filter:blur(6px);animation:warningFadeIn 180ms ease} .warningBackdrop{position:fixed;inset:0;z-index:1000;display:flex;align-items:center;justify-content:center;padding:24px;background:rgba(10,20,35,.55);backdrop-filter:blur(6px);animation:warningFadeIn 180ms ease}
.warningModal{position:relative;width:100%;max-width:900px;max-height:85vh;overflow-y:auto;padding:32px 36px;border-radius:20px;background:var(--surface);box-shadow:var(--shadow-md);animation:warningPopIn 200ms ease} .warningModal{position:relative;width:100%;max-width:900px;max-height:85vh;overflow-y:auto;padding:32px 36px;border-radius:20px;background:var(--surface);box-shadow:var(--shadow-md);animation:warningPopIn 200ms ease}