Declutter Faktiskā: collapse stations and map settings by default

Both "Jaunākās temperatūras" and "Kartes noformējums" dumped their
full content on load, forcing a scroll past both just to reach the
map/export on every visit. Both now collapse by default (Faktiskā
only; the older non-production Kartes overlay panel keeps its current
always-open behavior) behind a toggle that still surfaces a manual-
override count so nothing is silently hidden.

Also fixed a real bug found in the process: Faktiskā's resolution
buttons carried both "resolutionChoices" and "faktiskaResolutionChoices"
classes, and the former (defined in waterTemperature.css for a narrow
sidebar layout) was winning the cascade in this wide-content context,
forcing full-width, left-aligned buttons instead of the intended
compact pair.

Restructured "Kartes noformējums" into distinct bordered cards instead
of one continuous flow, translated the still-English "Weather symbols"
/"City assignments" block to Latvian ("Laikapstākļu simboli"/"Mākoņi
pilsētām"), and collapsed the city list to show only active exceptions
by default with a "Rādīt pārējās pilsētas" button to reveal the rest.
This commit is contained in:
b0txec
2026-08-23 13:31:12 +03:00
parent 744d706c83
commit a1c2fd5d7d
5 changed files with 50 additions and 19 deletions
+1 -1
View File
@@ -106,7 +106,7 @@ export const MapView: Component<{ type: MapResolution, data: () => ResultKeyVal[
return (
<div class="mapWorkspace">
<div class="mapToolbar"><div><strong>{productionTemplate ? "Kartes priekšskatījums" : "Map preview"}</strong><span>{props.width} × {props.height} px {productionTemplate ? "eksports" : "export"}</span></div><button class="primary" onClick={downloadPng}>{productionTemplate ? "Lejupielādēt PNG" : "Download PNG"}</button></div>
<details class="broadcastControls" open>
<details class="broadcastControls" open={!productionTemplate}>
<summary><span><strong>{productionTemplate ? "Kartes noformējums" : "Broadcast overlay controls"}</strong>{!productionTemplate && <small>Optional manual wind and weather-symbol overrides</small>}</span></summary>
{!productionTemplate && <p class="controlHelp">These settings do not change the queried city values. Use them only when preparing a finished broadcast graphic.</p>}
{!productionTemplate && <div class="overlayFields">
+22 -4
View File
@@ -6,14 +6,32 @@ import { WeatherIcon } from "./WeatherIcon";
export const IconInputs: Component<{cities: string[]; weatherIconsSignal: Signal<Record<string, string>>}> = (props) => {
const [icons, setIcons] = props.weatherIconsSignal;
const [selected, setSelected] = createSignal(weatherIcons[0]);
const [showAllCities, setShowAllCities] = createSignal(false);
const assign = (city: string) => setIcons({...icons(), [city]: selected()});
const clear = (city: string) => { const next = {...icons()}; delete next[city]; setIcons(next); };
const applyAll = () => setIcons(Object.fromEntries(props.cities.map(city => [city, selected()])));
const exceptedCities = () => props.cities.filter(city => icons()[city]);
const remainingCities = () => props.cities.filter(city => !icons()[city]);
return <section class="symbolEditor">
<div class="symbolEditorHeading"><div><strong>Weather symbols</strong><p class="fieldHint">Choose one symbol, apply it broadly, then update the exceptions below.</p></div><div class="bulkSymbol"><span class="selectedPreview"><WeatherIcon code={selected()} size={40} tone="inverse"/></span><button type="button" onClick={applyAll}>Apply to all cities</button></div></div>
<div class="symbolPalette" role="radiogroup" aria-label="Choose weather symbol"><For each={weatherIcons}>{icon => <button type="button" role="radio" aria-checked={selected() === icon} aria-label={`Weather symbol ${icon}`} class="weatherIconButton" classList={{selected: selected() === icon}} onClick={() => setSelected(icon)}><WeatherIcon code={icon} size={40} tone={selected() === icon ? "inverse" : "default"}/></button>}</For></div>
<div class="assignmentHeading"><strong>City assignments</strong><span>Use the selected symbol for each exception.</span></div>
<div class="citySymbols"><For each={props.cities}>{city => <div class="citySymbolRow"><span class="cityName">{city}</span><span class="currentSymbol" classList={{empty: !icons()[city]}}><Show when={icons()[city]} fallback="—">{icon => <WeatherIcon code={icon()} size={40}/>}</Show></span><button type="button" class="assignAction" onClick={() => assign(city)}>Use selected</button><button type="button" class="clearSymbol" disabled={!icons()[city]} onClick={() => clear(city)} aria-label={`Clear ${city} symbol`}>×</button></div>}</For></div>
<div class="symbolCard">
<div class="symbolEditorHeading"><div><strong>Laikapstākļu simboli</strong><p class="fieldHint">Izvēlies simbolu un pielieto to visām pilsētām, tad pielāgo atsevišķus izņēmumus zemāk.</p></div><div class="bulkSymbol"><span class="selectedPreview"><WeatherIcon code={selected()} size={40} tone="inverse"/></span><button type="button" onClick={applyAll}>Pielietot visām</button></div></div>
<div class="symbolPalette" role="radiogroup" aria-label="Izvēlies laikapstākļu simbolu"><For each={weatherIcons}>{icon => <button type="button" role="radio" aria-checked={selected() === icon} aria-label={`Laikapstākļu simbols ${icon}`} class="weatherIconButton" classList={{selected: selected() === icon}} onClick={() => setSelected(icon)}><WeatherIcon code={icon} size={40} tone={selected() === icon ? "inverse" : "default"}/></button>}</For></div>
</div>
<div class="symbolCard">
<div class="assignmentHeading"><strong>Mākoņi pilsētām</strong>{exceptedCities().length > 0 && <span>{exceptedCities().length} no {props.cities.length} pilsētām ar atsevišķu simbolu</span>}</div>
<Show when={exceptedCities().length > 0}>
<div class="citySymbols"><For each={exceptedCities()}>{city => <div class="citySymbolRow"><span class="cityName">{city}</span><span class="currentSymbol"><WeatherIcon code={icons()[city]} size={40}/></span><button type="button" class="assignAction" onClick={() => assign(city)}>Lietot</button><button type="button" class="clearSymbol" onClick={() => clear(city)} aria-label={`Notīrīt ${city} simbolu`}>×</button></div>}</For></div>
</Show>
<Show when={remainingCities().length > 0}>
<button type="button" class="secondary showAllCitiesToggle" onClick={() => setShowAllCities(v => !v)}>
{showAllCities() ? "Slēpt pārējās pilsētas" : `Rādīt pārējās pilsētas (${remainingCities().length})`}
</button>
<Show when={showAllCities()}>
<div class="citySymbols"><For each={remainingCities()}>{city => <div class="citySymbolRow"><span class="cityName">{city}</span><span class="currentSymbol empty"></span><button type="button" class="assignAction" onClick={() => assign(city)}>Lietot</button><button type="button" class="clearSymbol" disabled aria-label={`Notīrīt ${city} simbolu`}>×</button></div>}</For></div>
</Show>
</Show>
</div>
</section>;
};
+1 -1
View File
@@ -13,7 +13,7 @@
font-size: 11px;
}
.overlayFields{display:grid;grid-template-columns:minmax(260px,2fr) minmax(160px,1fr);gap:12px;padding:16px 16px 0}.overlayFields label,.overlayFields span{display:block}.overlayFields span{margin-bottom:6px;font-size:12px;font-weight:700}.overlayFields input{width:100%}
.mapControlSections{display:grid;gap:18px;padding:16px}.valueControls{padding-bottom:16px;border-bottom:1px solid var(--border)}.windControlRow{display:grid;grid-template-columns:minmax(170px,.8fr) repeat(3,minmax(180px,1fr));gap:14px;align-items:end;margin-top:10px}.roundControl{display:flex;align-items:center;gap:8px;min-height:40px}.windField{display:grid;gap:6px}.windField span{font-size:12px;font-weight:700;color:var(--text-muted)}.windField input{width:100%;min-width:0}@media(max-width:900px){.windControlRow{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:560px){.windControlRow{grid-template-columns:1fr}}
.mapControlSections{display:grid;gap:16px;padding:16px;background:var(--surface-soft)}.valueControls{padding:16px;border:1px solid var(--border);border-radius:14px;background:var(--surface)}.valueControls>strong{display:block;margin-bottom:2px}.windControlRow{display:grid;grid-template-columns:minmax(170px,.8fr) repeat(3,minmax(180px,1fr));gap:14px;align-items:end;margin-top:10px}.roundControl{display:flex;align-items:center;gap:8px;min-height:40px}.windField{display:grid;gap:6px}.windField span{font-size:12px;font-weight:700;color:var(--text-muted)}.windField input{width:100%;min-width:0}@media(max-width:900px){.windControlRow{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:560px){.windControlRow{grid-template-columns:1fr}}
.pageWorkspace{max-width:1800px;margin:auto;padding:34px 28px 70px}.pageHeading{display:flex;align-items:end;justify-content:space-between;gap:24px;margin-bottom:24px}.pageHeading h1{margin:5px 0 6px;font-size:34px}.pageHeading p{margin:0;color:var(--text-muted)}.selectionCount{padding:10px 14px;border:1px solid var(--border);border-radius:10px;background:var(--surface);font-weight:700}.aggregator>.cityWorkspace{display:grid;grid-template-columns:240px 300px minmax(0,1fr);gap:18px;align-items:start}.panel{padding:20px;border:1px solid var(--border);border-radius:16px;background:var(--surface);box-shadow:var(--shadow-sm)}.panel h2{margin:0 0 18px;font-size:16px}.cityPanel{max-height:calc(100vh - 160px);overflow:auto}.cityPanel ul{margin:12px 0;padding:0;columns:1}.cityPanel li{padding:3px 0}.queryPanel h4{margin:22px 0 9px}.queryPanel p{margin:10px 0}.resultsPanel{min-width:0}.emptyState{display:flex;min-height:260px;align-items:center;justify-content:center;flex-direction:column;gap:8px;margin-top:18px;padding:30px;border:1px dashed #bac6d4;border-radius:16px;background:rgba(255,255,255,.55);color:var(--text-muted);text-align:center}.emptyState strong{color:var(--text);font-size:18px}.mapWorkspace{margin-top:14px}.mapToolbar{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px}.mapToolbar strong,.mapToolbar span{display:block}.mapToolbar span{margin-top:3px;color:var(--text-muted);font-size:12px}.broadcastControls{margin-bottom:14px;border:1px solid var(--border);border-radius:12px;background:var(--surface)}.broadcastControls summary{padding:14px 16px;cursor:pointer}.broadcastControls summary span,.broadcastControls summary strong,.broadcastControls summary small{display:block}.broadcastControls summary small,.controlHelp,.fieldHint{color:var(--text-muted);font-size:12px}.broadcastControls[open] summary{border-bottom:1px solid var(--border)}.broadcastControls .grid-1-2{padding:16px}.controlHelp{margin:16px 16px 0}.canvasFrame{overflow:hidden;border:1px solid var(--border);border-radius:14px;background:#d8dee6;box-shadow:var(--shadow-sm)}.canvasFrame canvas{display:block}.weatherIconButton{border-radius:7px}.fieldHint{margin:4px 0 10px}@media(max-width:1200px){.aggregator>.cityWorkspace{grid-template-columns:220px 1fr}.resultsPanel{grid-column:1/-1}.cityPanel{max-height:none}}@media(max-width:700px){.pageWorkspace{padding:24px 14px}.pageHeading{align-items:start;flex-direction:column}.aggregator>.cityWorkspace{grid-template-columns:1fr}.resultsPanel{grid-column:auto}}
+21 -12
View File
@@ -15,6 +15,7 @@ export function Faktiska() {
const [values, setValues] = createSignal(emptyValues());
const [overrides, setOverrides] = createSignal<Set<FaktiskaStation>>(new Set());
const [template, setTemplate] = createSignal<FaktiskaTemplate>(faktiskaExport.defaultTemplate);
const [showStations, setShowStations] = createSignal(false);
const productionSize = () => template() === "faktiska_1920x1080"
? { width: 1920, height: 1080 }
: { width: 3840, height: 1440 };
@@ -74,25 +75,33 @@ export function Faktiska() {
<section class="faktiskaDataBand">
<div class="faktiskaStatus">
<div><span class="stepLabel">1. Jaunākās temperatūras</span><strong>{newestTimestamp() ? newestTimestamp()!.toLocaleString("lv-LV") : "Gaida novērojumus"}</strong><small>Jaunākie pieejamie staciju novērojumi</small></div>
<button class="secondary" disabled={latest.loading} onClick={() => refetch()}>Atjaunot datus</button>
<div class="faktiskaStatusActions">
<button class="secondary" disabled={latest.loading} onClick={() => refetch()}>Atjaunot datus</button>
<button type="button" class="secondary stationsToggle" onClick={() => setShowStations(v => !v)}>
{showStations() ? "Slēpt stacijas" : "Rādīt stacijas"}
<Show when={overrides().size > 0}><span class="overrideBadge">{overrides().size}</span></Show>
</button>
</div>
</div>
<Show when={latest.loading}><LoadingSpinner text="Ielādē jaunākās temperatūras"/></Show>
<Show when={latest.error}><div class="inlineNotice error"><strong>Novērojumus neizdevās ielādēt.</strong> Manuālie lauki joprojām ir pieejami.</div></Show>
<div class="temperatureEditor">
<For each={faktiskaStations}>{city => {
const observation = () => observationsByCity().get(city);
return <label class="temperatureField" classList={{manual: overrides().has(city), stale: isStale(city), missing: observation()?.value == null}}>
<span class="temperatureFieldHeading"><strong>{city}</strong><small>{overrides().has(city) ? "Manuāli" : isStale(city) ? "Vecāks novērojums" : observation()?.observedAt ? new Date(observation()!.observedAt).toLocaleTimeString("lv-LV", {hour:"2-digit", minute:"2-digit"}) : "Nav datu"}</small></span>
<span class="temperatureInput"><input type="text" inputmode="decimal" value={values()[city]} onInput={event => updateValue(city, event.currentTarget.value)} aria-label={`${city} temperatūra`}/><span>°C</span></span>
<button type="button" class="resetTemperature" disabled={!overrides().has(city)} onClick={() => resetValue(city)}>Atiestatīt</button>
</label>;
}}</For>
</div>
<Show when={showStations()}>
<div class="temperatureEditor">
<For each={faktiskaStations}>{city => {
const observation = () => observationsByCity().get(city);
return <label class="temperatureField" classList={{manual: overrides().has(city), stale: isStale(city), missing: observation()?.value == null}}>
<span class="temperatureFieldHeading"><strong>{city}</strong><small>{overrides().has(city) ? "Manuāli" : isStale(city) ? "Vecāks novērojums" : observation()?.observedAt ? new Date(observation()!.observedAt).toLocaleTimeString("lv-LV", {hour:"2-digit", minute:"2-digit"}) : "Nav datu"}</small></span>
<span class="temperatureInput"><input type="text" inputmode="decimal" value={values()[city]} onInput={event => updateValue(city, event.currentTarget.value)} aria-label={`${city} temperatūra`}/><span>°C</span></span>
<button type="button" class="resetTemperature" disabled={!overrides().has(city)} onClick={() => resetValue(city)}>Atiestatīt</button>
</label>;
}}</For>
</div>
</Show>
</section>
<section class="mapProduction faktiskaProduction">
<h2>2. Pievieno laikapstākļu simbolus un vēju, tad eksportē</h2>
<div class="resolutionChoices faktiskaResolutionChoices" aria-label="Faktiskā eksporta izmērs">
<div class="faktiskaResolutionChoices" aria-label="Faktiskā eksporta izmērs">
<button classList={{ active: template() === "faktiska_1920x1080" }} onClick={() => setTemplate("faktiska_1920x1080")}>1920 × 1080</button>
<button classList={{ active: template() === "faktiska_3840x1440" }} onClick={() => setTemplate("faktiska_3840x1440")}>3840 × 1440</button>
</div>
+5 -1
View File
@@ -1,5 +1,6 @@
.graphicsSetup{display:grid;grid-template-columns:240px minmax(0,1fr);gap:18px;align-items:start}.graphicsMain{display:grid;gap:18px}.productionOptions,.dataOptions{padding:20px}.choiceGroup{display:grid;grid-template-columns:130px 1fr;gap:16px;align-items:start;margin-top:16px}.choiceGroup>span,.dataOptions label>span{display:block;font-size:12px;font-weight:700}.choiceGroup>div{display:flex;flex-wrap:wrap;gap:9px}.choiceGroup button{min-width:150px;text-align:left}.choiceGroup button small{display:block;margin-top:3px;color:var(--text-muted);font-family:Inter,sans-serif}.choiceGroup button.active{border-color:var(--accent);background:var(--accent-pale);color:var(--accent-strong)}.windChoice{display:flex;align-items:center;gap:7px;padding:9px 12px}.dataOptionsGrid{display:grid;grid-template-columns:minmax(250px,1.4fr) 1fr 1fr;gap:18px;align-items:end;margin-bottom:16px}.dataOptions select{width:100%;margin-top:6px}.mapProduction>h2{margin:0 0 10px}.symbolEditor{min-width:0}.symbolEditorHeading{display:flex;align-items:start;justify-content:space-between;gap:12px}.symbolPalette{display:flex;flex-wrap:wrap;gap:5px;margin:10px 0 16px}.citySymbols{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:8px}.citySymbolRow{display:grid;grid-template-columns:1fr 52px 54px;gap:6px;align-items:center;padding:7px;border:1px solid var(--border);border-radius:9px}.assignedSymbol{padding:2px}.clearSymbol{padding:5px;font-size:11px}@media(max-width:900px){.graphicsSetup{grid-template-columns:1fr}.dataOptionsGrid{grid-template-columns:1fr}.choiceGroup{grid-template-columns:1fr}}
.symbolEditorHeading{gap:16px}.bulkSymbol{display:flex;align-items:center;gap:8px}.selectedPreview,.currentSymbol{display:flex;align-items:center;justify-content:center}.selectedPreview{width:42px;height:42px;border-radius:9px;background:#17212d;color:#fff}.symbolPalette{display:grid;grid-template-columns:repeat(auto-fill,46px);gap:8px;margin:14px 0 22px;padding:14px;border:1px solid var(--border);border-radius:14px;background:var(--surface-soft)}.assignmentHeading{display:flex;align-items:baseline;gap:10px;margin-bottom:10px}.assignmentHeading span{color:var(--text-muted);font-size:12px}.citySymbols{grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:10px}.citySymbolRow{grid-template-columns:minmax(90px,1fr) 48px 100px 36px;gap:8px;min-height:62px;padding:8px 10px;border-radius:12px;background:#fff}.cityName{overflow:hidden;text-overflow:ellipsis;font-weight:700}.currentSymbol{width:44px;height:44px;border:1px solid #d7dfe8;border-radius:9px;background:#f6f8fa}.currentSymbol.empty{font-family:Inter,sans-serif;font-size:18px;color:#98a2b3}.assignAction{padding:7px 9px;font-size:12px}.clearSymbol{width:36px;padding:0;font-size:20px;color:var(--text-muted)}@media(max-width:900px){.symbolEditorHeading{flex-direction:column}.citySymbols{grid-template-columns:1fr}}
.symbolEditorHeading{gap:16px}.bulkSymbol{display:flex;align-items:center;gap:8px}.selectedPreview,.currentSymbol{display:flex;align-items:center;justify-content:center}.selectedPreview{width:42px;height:42px;border-radius:9px;background:#17212d;color:#fff}.symbolPalette{display:grid;grid-template-columns:repeat(auto-fill,46px);gap:8px;margin:14px 0 0;padding:14px;border:1px solid var(--border);border-radius:14px;background:var(--surface-soft)}.assignmentHeading{display:flex;align-items:baseline;gap:10px;margin-bottom:10px}.assignmentHeading span{color:var(--text-muted);font-size:12px}.citySymbols{grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:10px}.citySymbolRow{grid-template-columns:minmax(90px,1fr) 48px 100px 36px;gap:8px;min-height:62px;padding:8px 10px;border-radius:12px;background:#fff}.cityName{overflow:hidden;text-overflow:ellipsis;font-weight:700}.currentSymbol{width:44px;height:44px;border:1px solid #d7dfe8;border-radius:9px;background:#f6f8fa}.currentSymbol.empty{font-family:Inter,sans-serif;font-size:18px;color:#98a2b3}.assignAction{padding:7px 9px;font-size:12px}.clearSymbol{width:36px;padding:0;font-size:20px;color:var(--text-muted)}@media(max-width:900px){.symbolEditorHeading{flex-direction:column}.citySymbols{grid-template-columns:1fr}}
.symbolEditor{display:grid;gap:16px}.symbolCard{padding:16px;border:1px solid var(--border);border-radius:14px;background:var(--surface)}.showAllCitiesToggle{margin-top:4px}
/* Production layout: a compact control band above one uninterrupted workspace. */
.graphicsSetup{grid-template-columns:230px minmax(0,1fr);column-gap:28px}
@@ -39,6 +40,9 @@
.faktiskaDataBand{padding:2px 0 22px;border-bottom:1px solid rgba(90,126,166,.22)}
.faktiskaStatus{display:flex;align-items:center;justify-content:space-between;gap:24px;margin-bottom:18px}
.faktiskaStatus>div,.faktiskaStatus strong,.faktiskaStatus small{display:block}
.faktiskaStatus .faktiskaStatusActions{display:flex;align-items:center;gap:16px}
.stationsToggle{display:flex;align-items:center;gap:7px}
.overrideBadge{display:inline-flex;align-items:center;justify-content:center;min-width:18px;height:18px;padding:0 5px;border-radius:9px;background:var(--accent);color:#fff;font-size:11px;font-weight:700}
.faktiskaStatus strong{margin-top:5px;font-size:18px}
.faktiskaStatus small{margin-top:3px;color:var(--text-muted)}
.stepLabel{font-family:Monda,sans-serif;font-size:20px;font-weight:700}