Replace Daira glyphs with normalized weather icons
@@ -7,7 +7,6 @@ import { Cities } from './pages/cities/Cities';
|
||||
import { Database } from './pages/database/Database';
|
||||
import { Station } from './pages/station/Station';
|
||||
import { apiHost } from './consts';
|
||||
import { WeatherIconStub } from './components/weatherIcons/WeatherIconStub';
|
||||
import { Harmonie } from './pages/harmonie/Harmonie'
|
||||
import { LvgmcForecast } from './pages/lvgmc-forecast/LvgmcForecast'
|
||||
import { Home } from './pages/home/Home'
|
||||
@@ -47,7 +46,6 @@ const AppLayout: ParentComponent = (props) => {
|
||||
})
|
||||
return (
|
||||
<div class="appShell">
|
||||
<WeatherIconStub />
|
||||
<header class={styles.header} classList={{[styles.scrolled]: isScrolled()}}>
|
||||
<A class={styles.brand} href="/"><span class={styles.brandMark}>W</span><span><strong>WeatherTool</strong><small>Weather data workspace</small></span></A>
|
||||
<nav aria-label="Main navigation"><ul class={styles.menu}>{pages.map(([path, label]) => <li><A href={path} activeClass={styles.active}>{label}</A></li>)}</ul></nav>
|
||||
|
||||
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 24 KiB |
@@ -8,12 +8,14 @@ import { cityCoords } from "../cityCoords";
|
||||
import { MapResolution, resolutionProps, windProps, WindProps } from "./mapConsts";
|
||||
import { CityData, drawOnMap } from "./canvasDraw";
|
||||
import { IconInputs } from "../weatherIcons/IconInputs";
|
||||
import { preloadWeatherIcons } from "../weatherIcons/weatherIconAssets";
|
||||
import { download } from '../../helpers/download'
|
||||
|
||||
export const MapView: Component<{ type: MapResolution, data: () => ResultKeyVal[], mode?: "temperature" | "faktiska", productionTemplate?: boolean }> = ({ type, data, mode = "temperature", productionTemplate = false }) => {
|
||||
const [getCanvas, setCanvas] = createSignal<HTMLCanvasElement>();
|
||||
const [getImg, setImg] = createSignal(new Image());
|
||||
const [fontReady, setFontReady] = createSignal(false);
|
||||
const [iconsReady, setIconsReady] = createSignal(mode !== "faktiska");
|
||||
const weatherIconsSignal = createSignal<{[key: string]: string;}>({});
|
||||
const [getWeatherIcons] = weatherIconsSignal;
|
||||
const [getTitle, setTitle] = createSignal("");
|
||||
@@ -44,6 +46,7 @@ export const MapView: Component<{ type: MapResolution, data: () => ResultKeyVal[
|
||||
|
||||
onMount(() => {
|
||||
void document.fonts.load(`700 ${props.fontSize}px Monda`).then(() => setFontReady(true));
|
||||
if (mode === "faktiska") void preloadWeatherIcons().then(() => setIconsReady(true));
|
||||
const ctx = getCanvas()!.getContext("2d")!;
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
@@ -63,6 +66,7 @@ export const MapView: Component<{ type: MapResolution, data: () => ResultKeyVal[
|
||||
|
||||
createEffect(() => {
|
||||
fontReady();
|
||||
iconsReady();
|
||||
const ctx = getCanvas()!.getContext("2d")!;
|
||||
const weatherIcons = getWeatherIcons();
|
||||
const coordsAndData: CityData[] = data()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ResolutionPropsValue, WindProps, windProps } from "./mapConsts";
|
||||
import { drawRotatedImage, getAngleFromString } from "./windAngles";
|
||||
import { faktiskaMarkerLayout, FaktiskaStation } from "../../pages/map-graphics/faktiskaConfig";
|
||||
import { getLoadedWeatherIcon } from "../weatherIcons/weatherIconAssets";
|
||||
|
||||
// cityName, {x, y}, weatherValue, icon
|
||||
export type CityData = [cityName: string, {x: number, y: number }, weatherValue: number, icon: string | undefined]
|
||||
@@ -48,11 +49,10 @@ export function drawOnMap(
|
||||
ctx.fillText(stringValue, localX, localY);
|
||||
|
||||
if (icon) {
|
||||
ctx.fillStyle = "#FFFFFF";
|
||||
ctx.font = `normal ${props.fontSize * (productionTemplate ? 4.2 : 5)}px Daira by LTV grafika`;
|
||||
const iconMetrics = ctx.measureText(icon);
|
||||
const glyphWidth = iconMetrics.actualBoundingBoxLeft + iconMetrics.actualBoundingBoxRight;
|
||||
const badgeWidth = getBadgeWidth(ctx, stringValue, props, true);
|
||||
const iconImage = getLoadedWeatherIcon(icon);
|
||||
if (!iconImage) return;
|
||||
const iconSize = productionTemplate ? 190 : props.boxSize * 1.35;
|
||||
const badgeWidth = getBadgeWidth(ctx, stringValue, props);
|
||||
|
||||
if (productionTemplate) {
|
||||
const layout = faktiskaMarkerLayout[city as FaktiskaStation];
|
||||
@@ -61,11 +61,11 @@ export function drawOnMap(
|
||||
const offsetX = layout?.iconOffsetX ?? 0;
|
||||
const offsetY = layout?.iconOffsetY ?? 0;
|
||||
const iconCenterX = side === "right"
|
||||
? localX + badgeWidth / 2 + gap + glyphWidth / 2 + offsetX
|
||||
: localX - badgeWidth / 2 - gap - glyphWidth / 2 + offsetX;
|
||||
drawOpticallyCenteredGlyph(ctx, icon, iconCenterX, localY + offsetY);
|
||||
? localX + badgeWidth / 2 + gap + iconSize / 2 + offsetX
|
||||
: localX - badgeWidth / 2 - gap - iconSize / 2 + offsetX;
|
||||
drawWeatherIcon(ctx, iconImage, iconCenterX, localY + offsetY, iconSize);
|
||||
} else {
|
||||
drawOpticallyCenteredGlyph(ctx, icon, localX + props.boxSize / 2 + 10 + iconMetrics.width / 2, localY);
|
||||
drawWeatherIcon(ctx, iconImage, localX + badgeWidth / 2 + 10 + iconSize / 2, localY, iconSize);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -98,20 +98,13 @@ export function drawOnMap(
|
||||
ctx.fillText("M/S", windProp.offsetX + boxMiddleX + gustsWidth / 2 - 22, (windProp.offsetY + 805)*windProp.scaleY);
|
||||
}
|
||||
|
||||
function getBadgeWidth(ctx: CanvasRenderingContext2D, value: string, props: ResolutionPropsValue, restoreValueFont = false): number {
|
||||
if (restoreValueFont) ctx.font = `bold ${props.fontSize}px Monda`;
|
||||
const width = Math.max(props.boxSize, Math.ceil(ctx.measureText(value).width + props.fontSize * 0.8));
|
||||
if (restoreValueFont) ctx.font = `normal ${props.fontSize * 4.2}px Daira by LTV grafika`;
|
||||
return width;
|
||||
function getBadgeWidth(ctx: CanvasRenderingContext2D, value: string, props: ResolutionPropsValue): number {
|
||||
ctx.font = `bold ${props.fontSize}px Monda`;
|
||||
return Math.max(props.boxSize, Math.ceil(ctx.measureText(value).width + props.fontSize * 0.8));
|
||||
}
|
||||
|
||||
function drawOpticallyCenteredGlyph(ctx: CanvasRenderingContext2D, glyph: string, centerX: number, centerY: number): void {
|
||||
const metrics = ctx.measureText(glyph);
|
||||
ctx.textAlign = "left";
|
||||
ctx.textBaseline = "alphabetic";
|
||||
const x = centerX + (metrics.actualBoundingBoxLeft - metrics.actualBoundingBoxRight) / 2;
|
||||
const y = centerY + (metrics.actualBoundingBoxAscent - metrics.actualBoundingBoxDescent) / 2;
|
||||
ctx.fillText(glyph, x, y);
|
||||
export function drawWeatherIcon(ctx: CanvasRenderingContext2D, image: HTMLImageElement, centerX: number, centerY: number, size: number): void {
|
||||
ctx.drawImage(image, centerX - size / 2, centerY - size / 2, size, size);
|
||||
}
|
||||
|
||||
function drawTitleOverlay(ctx: CanvasRenderingContext2D, title: string, source: string, canvasWidth: number): void {
|
||||
|
||||
@@ -3,6 +3,8 @@ import { codeToIcon } from '../weatherIcons/iconConsts'
|
||||
import rigaBgUrl from '../../assets/bg-riga.webp'
|
||||
import arrowUrl from '../../assets/arrow.webp'
|
||||
import { drawRotatedImage, getAngleFromString } from '../map/windAngles'
|
||||
import { getLoadedWeatherIcon, preloadWeatherIcons } from '../weatherIcons/weatherIconAssets'
|
||||
import { drawWeatherIcon } from '../map/canvasDraw'
|
||||
|
||||
type TableWeather = {
|
||||
hour: string,
|
||||
@@ -44,7 +46,7 @@ export const TableFour: Component<{
|
||||
|
||||
return { hour, temperature, windDirection, windSpeed, icon }
|
||||
})
|
||||
drawTable(canvas!, rigaBg, arrowImg, data, weekDayLV)
|
||||
void preloadWeatherIcons(data.map(item => item.icon)).then(() => drawTable(canvas!, rigaBg, arrowImg, data, weekDayLV))
|
||||
})
|
||||
|
||||
return <>
|
||||
@@ -82,8 +84,8 @@ function drawTable(
|
||||
ctx.fillStyle = '#FFFFFF'
|
||||
ctx.fillText(weather.hour, midX, rigaBg.height * 0.2)
|
||||
|
||||
ctx.font = `normal 240px Daira by LTV grafika`
|
||||
ctx.fillText(weather.icon, midX, rigaBg.height * 0.5)
|
||||
const iconImage = getLoadedWeatherIcon(weather.icon)
|
||||
if (iconImage) drawWeatherIcon(ctx, iconImage, midX, rigaBg.height * 0.5, 220)
|
||||
|
||||
ctx.font = 'normal 50px Monda'
|
||||
ctx.fillText(weather.temperature, midX, rigaBg.height * 0.75)
|
||||
|
||||
@@ -3,6 +3,8 @@ import { codeToIcon } from '../weatherIcons/iconConsts'
|
||||
import table2BgUrl from '../../assets/bg-table2-left.webp'
|
||||
import arrowUrl from '../../assets/arrow.webp'
|
||||
import { drawRotatedImage, getAngleFromString } from '../map/windAngles'
|
||||
import { getLoadedWeatherIcon, preloadWeatherIcons } from '../weatherIcons/weatherIconAssets'
|
||||
import { drawWeatherIcon } from '../map/canvasDraw'
|
||||
|
||||
type TableWeather = {
|
||||
temperature: string,
|
||||
@@ -50,7 +52,7 @@ export const TableTwo: Component<{
|
||||
},
|
||||
]
|
||||
|
||||
drawTable(canvas!, table2Bg, arrowImg, data, weekDayLV)
|
||||
void preloadWeatherIcons(data.map(item => item.icon)).then(() => drawTable(canvas!, table2Bg, arrowImg, data, weekDayLV))
|
||||
})
|
||||
|
||||
return <>
|
||||
@@ -86,8 +88,8 @@ function drawTable(
|
||||
ctx.textAlign = 'center'
|
||||
ctx.fillStyle = '#FFFFFF'
|
||||
|
||||
ctx.font = `normal 240px Daira by LTV grafika`
|
||||
ctx.fillText(weather.icon, midX, rigaBg.height * 0.4)
|
||||
const iconImage = getLoadedWeatherIcon(weather.icon)
|
||||
if (iconImage) drawWeatherIcon(ctx, iconImage, midX, rigaBg.height * 0.4, 220)
|
||||
|
||||
ctx.font = 'normal 50px Monda'
|
||||
ctx.fillText(weather.temperature, midX, rigaBg.height * 0.7)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component, createSignal, For, Show, Signal } from "solid-js";
|
||||
import { weatherIcons } from "./iconConsts";
|
||||
import "../../css/weatherIcons.css";
|
||||
import { WeatherGlyph } from "./WeatherGlyph";
|
||||
import { WeatherIcon } from "./WeatherIcon";
|
||||
|
||||
export const IconInputs: Component<{cities: string[]; weatherIconsSignal: Signal<Record<string, string>>}> = (props) => {
|
||||
const [icons, setIcons] = props.weatherIconsSignal;
|
||||
@@ -11,9 +11,9 @@ export const IconInputs: Component<{cities: string[]; weatherIconsSignal: Signal
|
||||
const applyAll = () => setIcons(Object.fromEntries(props.cities.map(city => [city, selected()])));
|
||||
|
||||
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"><WeatherGlyph glyph={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)}><WeatherGlyph glyph={icon} size={40} tone={selected() === icon ? "inverse" : "default"}/></button>}</For></div>
|
||||
<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 => <WeatherGlyph glyph={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="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>
|
||||
</section>;
|
||||
};
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import { Component, createEffect, onMount } from "solid-js";
|
||||
|
||||
export const WeatherGlyph: Component<{glyph: string; size?: number; tone?: "default" | "inverse"}> = (props) => {
|
||||
let canvas!: HTMLCanvasElement;
|
||||
const boxSize = () => props.size ?? 40;
|
||||
const draw = async () => {
|
||||
const size = boxSize();
|
||||
await document.fonts.load(`${Math.round(size * 0.82)}px "Daira by LTV grafika"`);
|
||||
canvas.width = size; canvas.height = size;
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
ctx.clearRect(0, 0, size, size);
|
||||
ctx.font = `normal ${Math.round(size * 0.82)}px "Daira by LTV grafika"`;
|
||||
ctx.fillStyle = props.tone === "inverse" ? "#ffffff" : "#17212d";
|
||||
ctx.textAlign = "left"; ctx.textBaseline = "alphabetic";
|
||||
const metrics = ctx.measureText(props.glyph);
|
||||
const x = size / 2 + (metrics.actualBoundingBoxLeft - metrics.actualBoundingBoxRight) / 2;
|
||||
const y = size / 2 + (metrics.actualBoundingBoxAscent - metrics.actualBoundingBoxDescent) / 2;
|
||||
ctx.fillText(props.glyph, x, y);
|
||||
};
|
||||
onMount(draw);
|
||||
createEffect(() => { props.glyph; props.tone; boxSize(); if (canvas) void draw(); });
|
||||
return <canvas ref={canvas} class="weatherGlyph" style={{width: `${boxSize()}px`, height: `${boxSize()}px`}} aria-hidden="true"/>;
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Component } from "solid-js";
|
||||
import { getWeatherIconUrl } from "./weatherIconAssets";
|
||||
|
||||
export const WeatherIcon: Component<{code: string; size?: number; tone?: "default" | "inverse"}> = (props) => {
|
||||
const boxSize = () => props.size ?? 40;
|
||||
return <img
|
||||
src={getWeatherIconUrl(props.code)}
|
||||
class="weatherIconAsset"
|
||||
classList={{inverse: props.tone === "inverse"}}
|
||||
style={{width: `${boxSize()}px`, height: `${boxSize()}px`}}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
/>;
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
import { Component } from "solid-js";
|
||||
|
||||
/* Put this in page so browser actually downloads ltv font */
|
||||
export const WeatherIconStub: Component<{}> = () => {
|
||||
return (
|
||||
<div class="weatherIconStub">ABCDEF</div>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
/* font-family: 'Daira by LTV grafika'; */
|
||||
export const weatherIcons = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "a", "b", "c", "d", "e", "f", "g", "h", "i"]
|
||||
export const weatherIconCities = ['Ainaži', 'Alūksne', 'Bauska', 'Daugavpils', 'Liepāja', 'Madona', 'Priekuļi', 'Rēzekne', 'Rīga', 'Saldus', 'Stende', 'Ventspils', 'Zīlāni']
|
||||
|
||||
@@ -179,4 +178,4 @@ export const codeIconMap: { [key: number]: string } = {
|
||||
2617: 'd',
|
||||
2618: 'd',
|
||||
2619: 'f',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { weatherIcons } from "./iconConsts";
|
||||
|
||||
const iconModules = import.meta.glob("../../assets/weather-icons/*.png", {
|
||||
eager: true,
|
||||
query: "?url",
|
||||
import: "default",
|
||||
}) as Record<string, string>;
|
||||
|
||||
const imageCache = new Map<string, HTMLImageElement>();
|
||||
|
||||
function assetFilename(code: string): string {
|
||||
return code === code.toLowerCase() ? `${code}${code}.png` : `${code}.png`;
|
||||
}
|
||||
|
||||
export function getWeatherIconUrl(code: string): string {
|
||||
const filename = assetFilename(code);
|
||||
const entry = Object.entries(iconModules).find(([path]) => path.endsWith(`/${filename}`));
|
||||
if (!entry) throw new Error(`Missing weather icon asset: ${filename}`);
|
||||
return entry[1];
|
||||
}
|
||||
|
||||
export function loadWeatherIcon(code: string): Promise<HTMLImageElement> {
|
||||
const cached = imageCache.get(code);
|
||||
if (cached?.complete && cached.naturalWidth > 0) return Promise.resolve(cached);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const image = cached ?? new Image();
|
||||
imageCache.set(code, image);
|
||||
image.onload = () => resolve(image);
|
||||
image.onerror = () => reject(new Error(`Failed to load weather icon: ${code}`));
|
||||
if (!image.src) image.src = getWeatherIconUrl(code);
|
||||
});
|
||||
}
|
||||
|
||||
export function preloadWeatherIcons(codes: readonly string[] = weatherIcons): Promise<HTMLImageElement[]> {
|
||||
return Promise.all(codes.map(loadWeatherIcon));
|
||||
}
|
||||
|
||||
export function getLoadedWeatherIcon(code: string): HTMLImageElement | undefined {
|
||||
const image = imageCache.get(code);
|
||||
return image?.complete && image.naturalWidth > 0 ? image : undefined;
|
||||
}
|
||||
|
||||
@@ -66,14 +66,6 @@ table tr:hover {
|
||||
background: #c0c0c0;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Daira by LTV grafika';
|
||||
src: url('../assets/ltv_meteo.otf') format('truetype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
@@ -93,4 +85,3 @@ table tr:hover {
|
||||
grid-template-columns: 1fr 2fr;
|
||||
gap: 10px;
|
||||
}
|
||||
.weatherIconStub{position:fixed!important;width:1px;height:1px;overflow:hidden;opacity:0;pointer-events:none}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
.weatherIconButton {
|
||||
font-family: 'Daira by LTV grafika';
|
||||
font-size: 30px;
|
||||
display:inline-flex;align-items:center;justify-content:center;
|
||||
width:50px;height:50px;min-height:50px;
|
||||
background:#f4f6f8;border:1px solid #d9e0e8;border-radius:10px;
|
||||
@@ -12,17 +10,8 @@
|
||||
background:#e9eef4;border-color:#aebccc;color:#17212d;
|
||||
}
|
||||
|
||||
.weatherIconInput {
|
||||
font-family: 'Daira by LTV grafika';
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.weatherIconStub {
|
||||
font-family: 'Daira by LTV grafika';
|
||||
font-size: 20px;
|
||||
color: white;
|
||||
}
|
||||
.weatherIconButton.selected{background:#17212d;border-color:#17212d;color:#fff;box-shadow:0 0 0 3px var(--accent-pale)}
|
||||
.weatherIconButton:focus-visible{outline:3px solid var(--accent-soft);outline-offset:2px}
|
||||
.weatherGlyph{display:block;flex:none;color:inherit;pointer-events:none}
|
||||
.weatherIconAsset{display:block;flex:none;object-fit:contain;pointer-events:none;filter:brightness(0) saturate(100%) invert(12%) sepia(13%) saturate(1127%) hue-rotate(173deg) brightness(93%) contrast(92%)}
|
||||
.weatherIconAsset.inverse,.weatherIconButton.selected .weatherIconAsset{filter:none}
|
||||
.symbolPalette{grid-template-columns:repeat(auto-fill,50px)!important}.selectedPreview{width:46px!important;height:46px!important}.selectedPreview,.currentSymbol{font-family:inherit!important}
|
||||
|
||||
@@ -14,15 +14,13 @@ export type FaktiskaMarkerLayout = {
|
||||
|
||||
const rightMarker = (): FaktiskaMarkerLayout => ({
|
||||
iconSide: "right",
|
||||
iconGap: 22,
|
||||
// Daira's canvas advance box contains substantial empty space before the
|
||||
// visible artwork. Pull the optically measured glyph back toward its badge.
|
||||
iconOffsetX: -50,
|
||||
iconOffsetY: 90,
|
||||
iconGap: 12,
|
||||
iconOffsetX: 0,
|
||||
iconOffsetY: 0,
|
||||
});
|
||||
|
||||
// These placements are part of the fixed newsroom template. Operators select
|
||||
// the glyph; the renderer owns its position relative to the temperature badge.
|
||||
// the symbol; the renderer owns its position relative to the temperature badge.
|
||||
export const faktiskaMarkerLayout: Record<FaktiskaStation, FaktiskaMarkerLayout> = {
|
||||
"Liepāja": rightMarker(),
|
||||
"Ventspils": rightMarker(),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.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{font-family:'Daira by LTV grafika';font-size:28px;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;font-family:'Daira by LTV grafika';font-size:30px}.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}}
|
||||
.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}}
|
||||
|
||||
/* Production layout: a compact control band above one uninterrupted workspace. */
|
||||
.graphicsSetup{grid-template-columns:230px minmax(0,1fr);column-gap:28px}
|
||||
|
||||