News

WeatherStrip

The weather for a news page: the temperature now with the sky drawn, the high and low, the hours or days ahead with rain chances, and a °C/°F switch.

#news#weather#forecast#widget#interactive

Demo

Preview
LisbonNow 18° CelsiusPartly cloudy · H 21° L 12° · Feels like 17°
Forecast at 14:00
  1. NowPartly cloudy, 18°
  2. 3 PMSunny, 20°
  3. 4 PMShowers, 19°, chance of rain 40%
  4. 5 PMRain, 17°, chance of rain 80%
  5. 6 PMThunderstorms, 16°, chance of rain 70%
  6. 7 PMCloudy, 15°
  7. 8 PMPartly cloudy, 13°
  8. 9 PMClear, 12°

Unit not switched yet.

ChicagoNow -2° CelsiusLight snow · H -1° L -7°
  1. ThuSnow, -1° -7°, chance of rain 60%
  2. FriSleet, 2° -4°, chance of rain 30%
  3. SatFog, 5° -1°
  4. SunWindy, 7° 1°
  5. MonSunny, 4° -2°

Get it

It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.

$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/weather_strip.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/weather_strip.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/weather_strip.qwe
component WeatherStripDemoLive() {    state note = "Unit not switched yet."    <div.demo-stack>        <WeatherStrip id="weather-strip-demo-lisbon" place="Lisbon" temp={18} condition="partly" high={21} low={12} feels={17} unit="C"                      updated="Forecast at 14:00" href="#weather-lisbon" on_unit={(u) => { note = `on_unit gave ${u}` }}                      hours={[                          { label: "Now", temp: 18, condition: "partly" },                          { label: "3 PM", temp: 20, condition: "sun" },                          { label: "4 PM", temp: 19, condition: "showers", rain: 40 },                          { label: "5 PM", temp: 17, condition: "rain", rain: 80 },                          { label: "6 PM", temp: 16, condition: "storm", rain: 70 },                          { label: "7 PM", temp: 15, condition: "cloud", rain: 5 },                          { label: "8 PM", temp: 13, condition: "partly_night" },                          { label: "9 PM", temp: 12, condition: "clear_night" },                      ]}/>        <p.demo-note id="weather-strip-demo-note">{note}</p>    </div>}view WeatherStripDemo() =    <div.demo-stack>        <WeatherStripDemoLive/>        <WeatherStrip id="weather-strip-demo-chicago" place="Chicago" temp={28} condition="snow" summary="Light snow" high={31} low={19} unit="F"                      show="C" remember={true} hours={[                          { label: "Thu", high: 31, low: 19, condition: "snow", rain: 60 },                          { label: "Fri", high: 35, low: 24, condition: "sleet", rain: 30 },                          { label: "Sat", high: 41, low: 30, condition: "fog" },                          { label: "Sun", high: 44, low: 33, condition: "wind" },                          { label: "Mon", high: 39, low: 28, condition: "sun" },                      ]}/>    </div>

Source

ui/weather_strip.qwe
// WeatherStrip: the weather in a news site's header or on its front: the place, the// temperature now with a picture of the sky, the day's high and low, and the hours// (or days) ahead in a row that scrolls sideways where it is narrow, each with its// chance of rain. °C and °F switch every temperature at once, and the choice can be// remembered in this browser. The forecast is data given to it: it fetches nothing.////   <WeatherStrip place="Lisbon" temp={18} condition="partly" high={21} low={12} unit="C" href="/weather/lisbon"//                 hours={[{ label: "3 PM", temp: 20, condition: "sun" }, { label: "4 PM", temp: 19, condition: "showers", rain: 40 }]}/>//// place: where. temp: the temperature now, in unit; high and low: the day's. unit:// "C" (default) or "F", that of the numbers given; show: the one to show first (unit).// condition: the sky now: "sun", "clear_night", "partly", "partly_night", "cloud",// "rain", "showers", "storm", "snow", "sleet", "fog" or "wind"; summary: how it reads// ("Sunny intervals"; each condition has its own words). hours: maps of { label, temp,// condition, rain? } (rain: the chance, 0 to 100), hours or days ("Tue"); for days,// give high and low instead of temp. feels: the "feels like" temperature. updated:// when the forecast is from. href: the full forecast, a link on the place. toggle: the// °C/°F switch (true). remember: keep the unit picked in this browser (localStorage),// for every strip on the site. on_unit(unit): inside one of your components, given// "C" or "F" when it is switched. id: the element's id.import ui/theme.qwe use ui_clslet weather_strip_css = css`.ui-wx { display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: center; gap: 12px 20px; min-width: 0; padding: 14px 16px;  border: 1px solid var(--ui-border); border-radius: var(--ui-radius); background: var(--ui-surface); color: var(--ui-fg);  font: 14px/1.35 var(--ui-font); font-variant-numeric: tabular-nums; }.ui-wx-now { display: grid; grid-template-columns: auto auto; align-items: center; gap: 0 12px; }.ui-wx-now > .ui-wx-icon { grid-row: span 3; width: 52px; height: 52px; }.ui-wx-place { display: flex; align-items: baseline; gap: 8px; font-size: 13px; font-weight: 650; }.ui-wx-place a { color: inherit; text-decoration: none; }.ui-wx-place a:hover { color: var(--ui-accent); text-decoration: underline; }.ui-wx-temp { font-size: 34px; line-height: 1.05; font-weight: 750; letter-spacing: -.02em; }.ui-wx-desc { color: var(--ui-muted); font-size: 13px; }.ui-wx-desc b { color: var(--ui-fg); font-weight: 600; }.ui-wx-side { display: grid; gap: 8px; min-width: 0; }.ui-wx-bar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 6px 12px; color: var(--ui-muted); font-size: 12px; }.ui-wx-units { display: inline-flex; padding: 2px; border-radius: 999px; background: var(--ui-subtle); }.ui-wx-units button { min-width: 38px; height: 26px; padding: 0 8px; border: 0; border-radius: 999px; background: transparent; color: var(--ui-muted);  font: 650 12px var(--ui-font); cursor: pointer; }.ui-wx-units button[aria-pressed="true"] { background: var(--ui-surface); color: var(--ui-fg); box-shadow: 0 1px 2px rgb(0 0 0 / .12); }.ui-wx button:focus-visible, .ui-wx a:focus-visible { outline: none; box-shadow: var(--ui-focus); border-radius: 999px; }.ui-wx-hours { display: flex; gap: 4px; margin: 0; padding: 0 0 2px; list-style: none; overflow-x: auto; scroll-snap-type: x proximity; scrollbar-width: thin; }.ui-wx-hours li { flex: 1 0 58px; display: grid; justify-items: center; gap: 3px; padding: 6px 4px; border-radius: var(--ui-radius-sm);  scroll-snap-align: start; text-align: center; }.ui-wx-hours li:first-child { background: var(--ui-subtle); }.ui-wx-when { color: var(--ui-muted); font-size: 12px; white-space: nowrap; }.ui-wx-hours .ui-wx-icon { width: 26px; height: 26px; }.ui-wx-t { font-weight: 650; white-space: nowrap; }.ui-wx-t small { color: var(--ui-muted); font-weight: 500; }.ui-wx-rain { color: var(--ui-info); font-size: 11px; font-weight: 600; }.ui-wx-rain-none { visibility: hidden; }.ui-wx-icon { display: block; overflow: visible; }.ui-wx-icon .ui-wx-sun { fill: var(--ui-warning); }.ui-wx-icon .ui-wx-ray { stroke: var(--ui-warning); stroke-width: 1.6; stroke-linecap: round; }.ui-wx-icon .ui-wx-moon { fill: color-mix(in srgb, var(--ui-warning) 45%, var(--ui-muted)); }.ui-wx-icon .ui-wx-cloud { fill: var(--ui-surface); stroke: var(--ui-muted); stroke-width: 1.4; }.ui-wx-icon .ui-wx-dark { fill: color-mix(in srgb, var(--ui-muted) 35%, var(--ui-surface)); }.ui-wx-icon .ui-wx-drop { stroke: var(--ui-info); stroke-width: 1.6; stroke-linecap: round; }.ui-wx-icon .ui-wx-flake { fill: var(--ui-info); }.ui-wx-icon .ui-wx-bolt { fill: var(--ui-warning); }.ui-wx-icon .ui-wx-line { stroke: var(--ui-muted); stroke-width: 1.6; stroke-linecap: round; fill: none; }@media (max-width: 560px) { .ui-wx { grid-template-columns: minmax(0, 1fr); } }`// The words for a condition.fn weather_strip_words(c: str?) -> str = match c {    "sun" => "Sunny"    "clear_night" => "Clear"    "partly" => "Partly cloudy"    "partly_night" => "Partly cloudy"    "cloud" => "Cloudy"    "rain" => "Rain"    "showers" => "Showers"    "storm" => "Thunderstorms"    "snow" => "Snow"    "sleet" => "Sleet"    "fog" => "Fog"    "wind" => "Windy"    _ => ""}// A temperature given in `from` ("C" or "F") as a whole number in `to`.fn weather_strip_temp(t, from: str, to: str) -> int =    from == to ? round(t) : (to == "F" ? round(t * 9 / 5 + 32) : round((t - 32) * 5 / 9))// A coordinate as text, to one decimal, written alike on the server and in the browser: 9, 12.5.fn weather_strip_n(n: num) -> str {    let r = round(n * 10)    r % 10 == 0 ? str(floor(r / 10)) : str(r / 10)}// A picture of the sky for a condition, in a 24 by 24 box.fn weather_strip_icon(c: str?) {    let cloud = (dark: bool) => <path class={dark ? "ui-wx-cloud ui-wx-dark" : "ui-wx-cloud"} d="M7 18.5h10.5a3.5 3.5 0 0 0 .3-7 5 5 0 0 0-9.6-1.2A4 4 0 0 0 7 18.5z"/>    let sun = (x: num, y: num, r: num) => [        <g>{for i in range(8) => {            let a = i * math.pi / 4            <line.ui-wx-ray x1={weather_strip_n(x + (r + 2) * math.cos(a))} y1={weather_strip_n(y + (r + 2) * math.sin(a))}                            x2={weather_strip_n(x + (r + 4) * math.cos(a))} y2={weather_strip_n(y + (r + 4) * math.sin(a))}/>        }}</g>,        <circle.ui-wx-sun cx={weather_strip_n(x)} cy={weather_strip_n(y)} r={weather_strip_n(r)}/>,    ]    let moon = <path.ui-wx-moon d="M10 3.5a6.5 6.5 0 1 0 7.5 9A5.5 5.5 0 0 1 10 3.5z"/>    let drops = (n: int) => for i in range(n) => <line.ui-wx-drop x1={weather_strip_n(9 + i * 3.5)} y1="20.5" x2={weather_strip_n(8 + i * 3.5)} y2="23"/>    <svg class="ui-wx-icon" viewBox="0 0 24 24" aria-hidden="true">        {match c {            "sun" => sun(12, 12, 4.5)            "clear_night" => moon            "partly" => [sun(9, 8, 3.5), cloud(false)]            "partly_night" => [<g transform="translate(-2 -2) scale(.8)">{moon}</g>, cloud(false)]            "rain" => [cloud(true), drops(3)]            "showers" => [sun(9, 7, 3), cloud(false), drops(2)]            "storm" => [cloud(true), <path.ui-wx-bolt d="M12.5 17l-2.5 4h2.5l-1 3 3.5-4.5h-2.5l1-2.5z"/>]            "snow" => [cloud(false), for i in range(3) => <circle.ui-wx-flake cx={weather_strip_n(9 + i * 3.5)} cy="21.5" r="1.1"/>]            "sleet" => [cloud(false), <line.ui-wx-drop x1="9.5" y1="20.5" x2="8.5" y2="23"/>, <circle.ui-wx-flake cx="14" cy="21.5" r="1.1"/>]            "fog" => <g>{for i in range(4) => <line.ui-wx-line x1={weather_strip_n(4 + (i % 2) * 2)} y1={weather_strip_n(7 + i * 3.5)} x2={weather_strip_n(20 - (i % 2) * 2)} y2={weather_strip_n(7 + i * 3.5)}/>}</g>            "wind" => <path.ui-wx-line d="M3 9h11a2.5 2.5 0 1 0-2.5-2.5M3 13h15a2.5 2.5 0 1 1-2.5 2.5M3 17h8"/>            _ => cloud(false)        }}    </svg>}component WeatherStrip(place: str, temp, condition: str = "cloud", summary: str? = null, high = null, low = null, feels = null,                       hours: list = [], unit: str = "C", show: str? = null, updated: str? = null, href: str? = null,                       toggle: bool = true, remember: bool = false, on_unit = null, id: str? = null, class: str? = null) {    style global weather_strip_css    state shown = show ?? unit    let deg = (v) => `${weather_strip_temp(v, unit, shown)}°`    let pick = (u: str) => {        if u != shown {            shown = u            if remember { js("localStorage.setItem", "ui-weather-unit", u) }            if on_unit != null { on_unit(u) }        }    }    let words = summary ?? weather_strip_words(condition)    <section class={ui_cls(["ui-wx", class])} id={id} aria-label={`Weather in ${place}`} data-unit={shown} @mount={(_el) => {        if remember {            let saved = js("localStorage.getItem", "ui-weather-unit")            if saved == "C" || saved == "F" { shown = saved }        }    }}>        <div.ui-wx-now>            {weather_strip_icon(condition)}            <span.ui-wx-place>{href != null ? <a href={href}>{place}</a> : place}</span>            <span.ui-wx-temp><span.ui-sr>Now </span>{deg(temp)}<span.ui-sr>{shown == "F" ? " Fahrenheit" : " Celsius"}</span></span>            <span.ui-wx-desc>                <b>{words}</b>                {high != null && low != null && [" · ", <span>H {deg(high)} L {deg(low)}</span>]}                {feels != null && [" · ", <span>Feels like {deg(feels)}</span>]}            </span>        </div>        <div.ui-wx-side>            {(toggle || updated != null) && <div.ui-wx-bar>                <span>{updated}</span>                {toggle && <div.ui-wx-units role="group" aria-label="Temperature unit">                    <button type="button" aria-pressed={shown == "C" ? "true" : "false"} @click={() => pick("C")}><span aria-hidden="true">°C</span><span.ui-sr>Celsius</span></button>                    <button type="button" aria-pressed={shown == "F" ? "true" : "false"} @click={() => pick("F")}><span aria-hidden="true">°F</span><span.ui-sr>Fahrenheit</span></button>                </div>}            </div>}            {len(hours) > 0 && <ol.ui-wx-hours aria-label="Forecast">                {for h in hours => <li>                    <span.ui-wx-when>{h.label}</span>                    {weather_strip_icon(h.condition)}                    <span.ui-sr>{weather_strip_words(h.condition)}, </span>                    <span.ui-wx-t>{h.temp != null ? deg(h.temp) : [deg(h.high), " ", <small>{deg(h.low)}</small>]}</span>                    <span class={ui_cls(["ui-wx-rain", (h.rain == null || h.rain < 10) && "ui-wx-rain-none"])}>                        {h.rain != null && h.rain >= 10 && [<span.ui-sr>, chance of rain </span>, `${h.rain}%`]}                    </span>                </li>}            </ol>}        </div>    </section>}
Thread
98Useful? Like it, or add to its thread below.

Thread 0 posts

On WeatherStrip: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.