diff --git a/docs/changelog.json b/docs/changelog.json index 6d9f7f6..4f0bfe0 100644 --- a/docs/changelog.json +++ b/docs/changelog.json @@ -1,4 +1,10 @@ [ + { + "date": "2026-10-13", + "area": "Kiosk", + "title": "New kiosk wallpapers: eight landscapes in place of the old schematics", + "summary": "Kiosk mode's wallpapers are now drawn scenes: mountains at dusk (the new default), a moonlit forest, dawn at sea, a kelp forest with a whale, desert dusk, the northern lights, the Lascaux frieze on a torchlit cave wall, and plain graphite. They keep the horizon in view on any screen shape, the picker shows each wallpaper itself as its thumbnail, and desktop icon labels stay white and readable on every wallpaper and theme. A wallpaper you'd picked from the old set switches to the new default." + }, { "date": "2026-10-13", "area": "Agents", diff --git a/ui/scripts/wallpapers.py b/ui/scripts/wallpapers.py new file mode 100644 index 0000000..8c502af --- /dev/null +++ b/ui/scripts/wallpapers.py @@ -0,0 +1,377 @@ +#!/usr/bin/env python3 +"""Regenerate the kiosk desktop wallpapers in src/kiosk/wallpapers/ (run from ui/; no dependencies). + +Each wallpaper is a layered landscape drawn at 2560×1600 with preserveAspectRatio "xMidYMax slice": the horizon and +the ground stay anchored to the bottom edge and the middle stays in frame, so the same file fills a 16:9 monitor, +an ultrawide or a portrait tablet without anything important being cut off. Nothing in them is text, and the top +third (behind the top bar and the desktop icons) is kept calm and fairly dark so labels stay readable. + +Ridges, trees and stars come from seeded noise, so a re-run gives the same files. A fine grain over every scene keeps +the long dark gradients from banding on 8-bit panels. +""" +import math +import pathlib +import random +import re + +W, H = 2560, 1600 +OUT = pathlib.Path(__file__).resolve().parent.parent / 'src' / 'kiosk' / 'wallpapers' +SCENERY = pathlib.Path(__file__).resolve().parent.parent / 'src' / 'themes' / 'scenery' + + +def f(v): + return f'{v:.1f}'.rstrip('0').rstrip('.') + + +def mix(a, b, t): + a, b = a.lstrip('#'), b.lstrip('#') + ca = [int(a[i:i + 2], 16) for i in (0, 2, 4)] + cb = [int(b[i:i + 2], 16) for i in (0, 2, 4)] + return '#' + ''.join(f'{round(x + (y - x) * t):02x}' for x, y in zip(ca, cb)) + + +def noise(seed, octaves=5, base=2.0, persistence=0.5): + """A smooth 1-D function of x in [0, W], roughly in [-1, 1]: summed sines with seeded phases.""" + rnd = random.Random(seed) + waves = [] + amp, freq, total = 1.0, base, 0.0 + for _ in range(octaves): + waves.append((amp, freq * rnd.uniform(0.8, 1.25), rnd.uniform(0, math.tau))) + total += amp + amp *= persistence + freq *= 2.1 + return lambda x: sum(a * math.sin(math.tau * fr * x / W + ph) for a, fr, ph in waves) / total + + +def ridge(base_y, amp, seed, *, octaves=6, base=1.6, peaky=False, step=6, bottom=H + 2): + n = noise(seed, octaves, base) + pts = [] + for x in range(-step * 2, W + step * 3, step): + v = n(x) + if peaky: # ridged noise: sharp summits, rounded valleys + v = 1 - 2 * abs(v) + pts.append((x, base_y - amp * v)) + d = f'M{f(pts[0][0])} {bottom} ' + ' '.join(f'L{f(x)} {f(y)}' for x, y in pts) + f' L{f(pts[-1][0])} {bottom}Z' + return d, lambda x: base_y - amp * ((1 - 2 * abs(n(x))) if peaky else n(x)) + + +def stars(seed, count, top, bottom, color='#ffffff', max_r=1.7): + rnd = random.Random(seed) + out = [] + for _ in range(count): + x, y = rnd.uniform(0, W), top + (bottom - top) * rnd.random() ** 1.6 # thinner toward the horizon + r = max_r * rnd.random() ** 2.5 + 0.5 + o = 0.25 + 0.65 * rnd.random() * (1 - (y - top) / (bottom - top)) ** 0.5 + out.append(f'') + return '' + ''.join(out) + '' + + +def pines(seed, base_y, amp, *, min_h, max_h, gap, wobble_seed=None, ground=None, floor=True): + """A treeline: one path of tiered conifers standing on a gentle ridge, plus the ground under it.""" + rnd = random.Random(seed) + if ground is None: + n = noise(wobble_seed if wobble_seed is not None else seed + 1, 4, 1.2) + ground = lambda x: base_y - amp * n(x) + parts = [] + x = -20.0 + while x < W + 20: + h = rnd.uniform(min_h, max_h) * (0.75 + 0.5 * rnd.random() ** 2) + w = h * rnd.uniform(0.28, 0.36) + y0 = ground(x) + 4 + tiers = 4 if h > 60 else 3 + left, right = [], [] + for i in range(tiers + 1): + t = i / tiers + ty = y0 - h + h * t + tw = w * (0.12 + 0.88 * t) / 2 + left.append((x - tw, ty)) + right.append((x + tw, ty)) + if 0 < i < tiers: # notch back in under each tier + left.append((x - tw * 0.55, ty + h * 0.04)) + right.append((x + tw * 0.55, ty + h * 0.04)) + pts = [(x, y0 - h - h * 0.06)] + right + [(x + w * 0.06, y0 + 6), (x - w * 0.06, y0 + 6)] + left[::-1] + parts.append('M' + ' L'.join(f'{f(px)} {f(py)}' for px, py in pts) + 'Z') + x += gap * rnd.uniform(0.35, 1.3) + if not floor: + return ''.join(parts) + return ''.join(parts) + 'M-20 ' + f(H + 2) + ' ' + ' '.join(f'L{x} {f(ground(x) + 2)}' for x in range(-20, W + 40, 12)) + f' L{W + 40} {H + 2}Z' + + +GRAIN = ( + '' + '' + '' +) +GRAIN_RECT = f'' + + +def svg(defs, body): + return ( + f'{GRAIN}{defs}{body}{GRAIN_RECT}\n' + ) + + +def sky(id_, stops): + s = ''.join(f'' for o, c in stops) + return f'{s}' + + +def glow(id_, color, stops=((0, 0.9), (0.25, 0.45), (0.6, 0.12), (1, 0))): + s = ''.join(f'' for o, a in stops) + return f'{s}' + + +def haze_band(y, height, color, opacity): + """Mist lying in a valley: transparent above, `color` at y, fading out again below.""" + return (f'') + + +MIST = ('' + '' + '') + + +# --------------------------------------------------------------------------------------------------------------------- + +def alpenglow(): + """Dusk over a mountain range: ridge after ridge fading into the haze, the sun just gone behind them.""" + defs = sky('sky', [(0, '#10173a'), (0.3, '#262a5c'), (0.52, '#5b3f74'), (0.66, '#b0627a'), (0.75, '#ee9a7c'), (0.82, '#ffd2a0')]) + defs += glow('sun', '#fff1d6') + MIST + body = f'' + body += stars(11, 140, 0, H * 0.4, max_r=1.5) + body += f'' + far, near = '#d78a8a', '#130f24' + layers = [(0.68, 150, 3), (0.73, 130, 5), (0.79, 120, 8), (0.86, 100, 13), (0.94, 70, 21)] + for i, (by, amp, seed) in enumerate(layers): + t = i / (len(layers) - 1) + d, _ = ridge(H * by, amp, seed, peaky=True, base=0.55 + 0.25 * i, octaves=5) + body += f'' + if i < len(layers) - 1: + body += haze_band(H * by + amp * 0.6, 160, mix('#ffc9a6', '#6d4a78', t), 0.35 - 0.05 * i) + return svg(defs, body) + + +def night_forest(): + """A clear moonlit night over forested hills, mist in the valleys.""" + defs = sky('sky', [(0, '#040914'), (0.4, '#0a1730'), (0.7, '#16305a'), (0.86, '#2a4c78')]) + defs += glow('moon', '#e8f0ff', ((0, 0.5), (0.18, 0.22), (0.5, 0.06), (1, 0))) + MIST + body = f'' + body += stars(3, 520, 0, H * 0.72, max_r=1.9) + mx, my = W * 0.74, H * 0.24 + body += f'' + body += f'' + body += f'' + d, _ = ridge(H * 0.7, 70, 31, base=1.1, octaves=4) + body += f'' + body += haze_band(H * 0.74, 140, '#5f7fa8', 0.35) + for i, (by, amp, hmin, hmax, gap, col) in enumerate([ + (0.76, 40, 26, 46, 14, '#15294a'), + (0.83, 50, 42, 78, 22, '#0d1c36'), + (0.93, 46, 80, 150, 40, '#050c1c'), + ]): + body += f'' + if i < 2: + body += haze_band(H * by + 30, 110, '#5f7fa8', 0.28 - 0.08 * i) + return svg(defs, body) + + +def sea_dawn(): + """Calm open sea at sunrise, a path of light on the water.""" + hy = H * 0.6 + defs = sky('sky', [(0, '#141f45'), (0.25, '#2f3c76'), (0.42, '#6c5f97'), (0.52, '#c4809a'), (0.57, '#f3a98e'), (0.6, '#ffd6a8')]) + defs += '' + defs += sky('sea', [(0, '#6d7fa6'), (0.06, '#3f5683'), (0.35, '#1d3360'), (1, '#0a1530')]) + defs += glow('sun', '#fff0d0', ((0, 0.95), (0.12, 0.6), (0.4, 0.15), (1, 0))) + defs += f'' + defs += ('' + '' + '' + f'') + body = f'' + body += stars(5, 70, 0, H * 0.3, max_r=1.2) + # thin clouds catching the light + rnd = random.Random(8) + clouds = '' + for _ in range(7): + cy = rnd.uniform(H * 0.32, H * 0.55) + t = (cy - H * 0.32) / (H * 0.23) + clouds += (f'') + body += f'{clouds}' + body += f'' + body += f'' + # swell lines, closer together near the horizon + rnd = random.Random(9) + lines = [] + for i in range(60): + t = (i / 60) ** 1.9 + y = hy + 6 + t * (H - hy) + for _ in range(int(6 + 10 * (1 - t))): + x = rnd.uniform(-100, W) + lines.append(f'') + body += f'{"".join(lines)}' + # glitter on the water under the sun + glints = [] + for i in range(240): + t = rnd.random() ** 1.4 + y = hy + 4 + t * (H - hy) + spread = 60 + t * 520 + x = W / 2 + rnd.gauss(0, spread / 2.2) + glints.append(f'') + body += f'{"".join(glints)}' + body += f'' + return svg(defs, body) + + +def kelp_forest(): + """Under the sea: sun rays through blue water, swaying kelp and a whale passing in the distance.""" + defs = sky('sea', [(0, '#1b7a9c'), (0.18, '#0f5878'), (0.5, '#073654'), (1, '#021428')]) + defs += glow('surface', '#bff2ff', ((0, 0.5), (0.4, 0.15), (1, 0))) + defs += '' + defs += ('' + '' + f'') + defs += '' + body = f'' + body += f'' + rnd = random.Random(21) + rays = [] + for _ in range(9): + x = rnd.uniform(W * 0.05, W * 0.95) + w = rnd.uniform(40, 140) + sk = rnd.uniform(160, 420) + rays.append(f'') + body += f'{"".join(rays)}' + # the whale, far off and blue with distance + whale = open(SCENERY / 'whale.svg').read() + paths = ''.join(f'' for d in re.findall(r"d='([^']+)'", whale)) + body += f'{paths}' + # distant then near seafloor + d, _ = ridge(H * 0.87, 46, 23, base=2.4, octaves=5) + body += f'' + + def kelp(x0, height, width, color, seed): + r = random.Random(seed) + ph, sway = r.uniform(0, math.tau), r.uniform(40, 90) + pts = [(x0 + sway * math.sin(ph + t * 2.6) * t, H + 10 - height * t) for t in [i / 48 for i in range(49)]] + stem = 'M' + ' L'.join(f'{f(x)} {f(y)}' for x, y in pts) + out = f'' + for i in range(3, 47, 2): # long blades trailing off the stipe, drifting the same way + x, y = pts[i] + side = 1 if i % 4 == 1 else -1 + ln = width * r.uniform(5, 8) * (1 - 0.4 * i / 48) + tipx, tipy = x + side * ln, y - ln * r.uniform(0.25, 0.6) + out += (f'') + return out + + for x0, hgt, wd, col, s in [(W * 0.07, H * 0.66, 9, '#0b4560', 1), (W * 0.17, H * 0.5, 8, '#0b4560', 2), + (W * 0.26, H * 0.36, 7, '#0b4560', 9), (W * 0.36, H * 0.26, 6, '#0b4560', 11), + (W * 0.66, H * 0.3, 6, '#0b4560', 12), (W * 0.76, H * 0.4, 7, '#0b4560', 10), + (W * 0.84, H * 0.6, 9, '#0b4560', 3), (W * 0.93, H * 0.46, 8, '#0b4560', 4)]: + body += kelp(x0, hgt, wd, col, s) + d, _ = ridge(H * 0.93, 34, 27, base=2.2, octaves=5) + body += f'' + for x0, hgt, wd, col, s in [(W * 0.02, H * 0.86, 14, '#032a40', 5), (W * 0.12, H * 0.72, 12, '#032a40', 6), + (W * 0.88, H * 0.8, 14, '#032a40', 7), (W * 0.98, H * 0.68, 12, '#032a40', 8)]: + body += kelp(x0, hgt, wd, col, s) + bubbles = ''.join(f'' for _ in range(70)) + body += bubbles + return svg(defs, body) + + +def desert_dusk(): + """Sand dunes after sunset under a violet sky, the first star out.""" + defs = sky('sky', [(0, '#1c1436'), (0.35, '#3e2652'), (0.58, '#87405a'), (0.7, '#d9785a'), (0.78, '#f4b27c')]) + defs += glow('sun', '#ffd8a0', ((0, 0.8), (0.3, 0.35), (0.7, 0.08), (1, 0))) + body = f'' + body += stars(17, 120, 0, H * 0.45, max_r=1.4) + body += f'' + body += f'' + layers = [(0.74, 50, 51, '#b86a5c', '#8a4a4e'), (0.8, 90, 52, '#c27a58', '#6e3a40'), (0.88, 120, 53, '#a85e44', '#4f2a32'), + (0.97, 140, 54, '#7e4432', '#2a1620')] + for i, (by, amp, seed, top, bot) in enumerate(layers): + d, _ = ridge(H * by, amp, seed, base=0.7 + 0.15 * i, octaves=3) + defs += sky(f'd{i}', [(0, top), (1, bot)]) + body += f'' + # the crest catching the last light + body += f'' + return svg(defs, body) + + +def aurora(): + """Northern lights over snowy hills and a dark line of pines.""" + defs = sky('sky', [(0, '#030713'), (0.45, '#081a33'), (0.8, '#123358'), (0.9, '#1d4568')]) + defs += '' + defs += ('' + '' + '') + defs += sky('snow', [(0, '#7d93b4'), (1, '#3b5274')]) + defs += sky('snow2', [(0, '#93a8c6'), (1, '#3e5478')]) + body = f'' + body += stars(7, 420, 0, H * 0.7, max_r=1.7) + bands = [] + for i, (cy, amp, height, seed) in enumerate([(0.5, 110, 330, 61), (0.4, 80, 260, 62), (0.58, 60, 200, 63)]): + n = noise(seed, 3, 0.8) + top = [(x, H * cy - amp * n(x) - height * (0.7 + 0.3 * n(x * 1.7 + 400))) for x in range(-100, W + 120, 20)] + bot = [(x, H * cy - amp * n(x)) for x in range(W + 100, -120, -20)] + dpath = 'M' + ' L'.join(f'{f(x)} {f(y)}' for x, y in top + bot) + 'Z' + bands.append(f'') + body += f'{"".join(bands)}' + d, g = ridge(H * 0.78, 80, 71, base=0.8, octaves=4) + body += f'' + body += f'' + d, g = ridge(H * 0.9, 60, 73, base=0.7, octaves=4) + body += f'' + body += f'' + return svg(defs, body) + + +def cave_wall(): + """The painted wall of a cave by torchlight: Lascaux's horses, aurochs and hands on rough limestone.""" + frieze = open(SCENERY / 'lascaux-frieze.svg').read() + frieze = re.sub(r"id='(\w+)'", r"id='lx-\1'", frieze) + frieze = re.sub(r"url\(#(\w+)\)", r"url(#lx-\1)", frieze) + fw = W * 0.86 + fh = fw * 420 / 1600 + frieze = frieze.replace("", + f"") + assert frieze.count('' + '' + '' + '' + '') + defs += glow('torch', '#ffb766', ((0, 0.45), (0.45, 0.18), (1, 0))) + defs += ('' + '') + body = f'' + body += f'' + body += frieze.replace('', '', 1) + body += f'' + return svg(defs, body) + + +def graphite(): + """No scene at all: soft graphite with a little light from above.""" + defs = ('' + '') + return svg(defs, f'') + + +SCENES = { + 'alpenglow': alpenglow, 'night-forest': night_forest, 'sea-dawn': sea_dawn, 'kelp-forest': kelp_forest, + 'desert-dusk': desert_dusk, 'aurora': aurora, 'cave-wall': cave_wall, 'graphite': graphite, +} + +if __name__ == '__main__': + OUT.mkdir(parents=True, exist_ok=True) + for name, draw in SCENES.items(): + path = OUT / f'{name}.svg' + path.write_text(draw()) + print(f'{path.relative_to(OUT.parent.parent.parent)} {path.stat().st_size // 1024} KB') diff --git a/ui/src/i18n/fr.json b/ui/src/i18n/fr.json index 8b3db6f..9925651 100644 --- a/ui/src/i18n/fr.json +++ b/ui/src/i18n/fr.json @@ -4094,5 +4094,26 @@ "Asset Scout": "Éclaireur de ressources", "Worlds & Cartography": "Mondes et cartographie", "Marine Biology": "Biologie marine", - "Studio Architect": "Architecte du Studio" + "Studio Architect": "Architecte du Studio", + "Kiosk Desktop Wallpaper": "Fond d'écran du bureau kiosque", + "Choose a scene, or use an image of your own": "Choisissez une scène, ou une image à vous", + "Scenes": "Scènes", + "Custom Image URL": "URL d'image personnalisée", + "Wallpaper Overlay Dimming": "Assombrissement du fond d'écran", + "Darkens the wallpaper background to maximize contrast for desktop shortcuts and open windows.": "Assombrit le fond d'écran pour mieux faire ressortir les raccourcis du bureau et les fenêtres ouvertes.", + "Mountain ridges fading into a rose and violet dusk.": "Des crêtes de montagnes qui se fondent dans un crépuscule rose et violet.", + "Moonlit Forest": "Forêt au clair de lune", + "A clear night over pine hills, mist lying in the valleys.": "Une nuit claire sur des collines de pins, la brume couchée dans les vallées.", + "Dawn at Sea": "Aube en mer", + "The sun rising over a calm sea, a path of light on the water.": "Le soleil se lève sur une mer calme, un chemin de lumière sur l'eau.", + "Kelp Forest": "Forêt de varech", + "Sunbeams through blue water, kelp swaying and a whale passing far off.": "Des rayons de soleil dans l'eau bleue, le varech qui ondule et une baleine qui passe au loin.", + "Desert Dusk": "Crépuscule dans le désert", + "Sand dunes after sunset, the first star out.": "Des dunes de sable après le coucher du soleil, la première étoile levée.", + "Northern Lights": "Aurore boréale", + "An aurora over snowy hills and a line of pines.": "Une aurore au-dessus de collines enneigées et d’une rangée de pins.", + "Painted Cave": "Grotte ornée", + "Horses, aurochs and hands from Lascaux on a torchlit limestone wall.": "Chevaux, aurochs et mains de Lascaux sur une paroi calcaire éclairée à la torche.", + "Plain soft graphite with a little light from above.": "Un graphite doux et uni, avec un peu de lumière venue d'en haut.", + "Load any high-resolution wallpaper image via HTTP/HTTPS URL.": "Chargez n'importe quelle image haute résolution par une URL HTTP/HTTPS." } diff --git a/ui/src/kiosk/DesktopIcons.tsx b/ui/src/kiosk/DesktopIcons.tsx index c13efd5..c327eec 100644 --- a/ui/src/kiosk/DesktopIcons.tsx +++ b/ui/src/kiosk/DesktopIcons.tsx @@ -128,7 +128,7 @@ export function DesktopIcons({ onOpenApp, status }: Props) { 'text-[11px] font-medium tracking-tight mt-1.5 px-1.5 py-0.5 rounded max-w-[90px] truncate drop-shadow', isSel ? 'bg-[var(--accent)] text-[var(--accent-fg)]' - : 'text-[var(--text-primary)] group-hover:text-white' + : 'text-white [text-shadow:0_1px_2px_rgba(0,0,0,0.9),0_0_8px_rgba(0,0,0,0.6)]' )} > {t(app.label)} diff --git a/ui/src/kiosk/WallpaperModal.tsx b/ui/src/kiosk/WallpaperModal.tsx index 15004f7..b976b67 100644 --- a/ui/src/kiosk/WallpaperModal.tsx +++ b/ui/src/kiosk/WallpaperModal.tsx @@ -1,5 +1,6 @@ import { useState } from 'react'; import { Check, Image, Sliders, X, Sparkles, ExternalLink } from 'lucide-react'; +import { t } from '../i18n'; import { WALLPAPERS, type WallpaperDef, @@ -54,10 +55,10 @@ export function WallpaperModal({ open, onClose, onChanged }: Props) {

- Kiosk Desktop Wallpaper + {t('Kiosk Desktop Wallpaper')}

- Choose a tactical schematic, ambient cosmic backdrop, or custom image URL + {t('Choose a scene, or use an image of your own')}

@@ -74,7 +75,7 @@ export function WallpaperModal({ open, onClose, onChanged }: Props) { {/* Wallpapers Grid */}
{WALLPAPERS.filter(w => w.id !== 'custom').map(w => { @@ -91,26 +92,23 @@ export function WallpaperModal({ open, onClose, onChanged }: Props) { > {/* Thumbnail preview */}
{isSelected && (
)} - - {w.category} -
{/* Metadata */}
- {w.name} + {t(w.name)}
- {w.description} + {t(w.description)}
@@ -124,11 +122,11 @@ export function WallpaperModal({ open, onClose, onChanged }: Props) {
{selectedId === 'custom' && ( - Active + {t('Active')} )}
@@ -145,7 +143,7 @@ export function WallpaperModal({ open, onClose, onChanged }: Props) { disabled={!customUrl.trim()} className="px-4 py-2 rounded-lg bg-[var(--accent)] text-[var(--accent-fg)] text-xs font-semibold hover:opacity-90 transition disabled:opacity-40" > - Apply + {t('Apply')}
@@ -155,7 +153,7 @@ export function WallpaperModal({ open, onClose, onChanged }: Props) {
{dim}%
@@ -169,7 +167,7 @@ export function WallpaperModal({ open, onClose, onChanged }: Props) { className="w-full h-1.5 bg-[var(--bg-secondary)] rounded-lg appearance-none cursor-pointer accent-[var(--accent)]" />

- Darkens the wallpaper background to maximize contrast for desktop shortcuts and open windows. + {t('Darkens the wallpaper background to maximize contrast for desktop shortcuts and open windows.')}

@@ -180,7 +178,7 @@ export function WallpaperModal({ open, onClose, onChanged }: Props) { onClick={onClose} className="px-4 py-1.5 rounded-lg text-xs font-semibold text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-hover)] transition" > - Done + {t('Done')} diff --git a/ui/src/kiosk/wallpapers.ts b/ui/src/kiosk/wallpapers.ts index 9c95f9c..15aca33 100644 --- a/ui/src/kiosk/wallpapers.ts +++ b/ui/src/kiosk/wallpapers.ts @@ -1,15 +1,25 @@ /** * Kiosk Mode Wallpapers - * Provides high-resolution vector and procedural wallpapers tailored to the Shell:B aesthetic, - * as well as custom user-specified image URL support. + * Layered landscapes drawn by scripts/wallpapers.py into ./wallpapers/*.svg (re-run it rather than editing the files), + * plus a custom image URL. Each one is anchored to the bottom edge, so the horizon survives any screen shape, and keeps + * its top third calm for the desktop icons. */ +import alpenglow from './wallpapers/alpenglow.svg?url'; +import nightForest from './wallpapers/night-forest.svg?url'; +import seaDawn from './wallpapers/sea-dawn.svg?url'; +import kelpForest from './wallpapers/kelp-forest.svg?url'; +import desertDusk from './wallpapers/desert-dusk.svg?url'; +import auroraUrl from './wallpapers/aurora.svg?url'; +import caveWall from './wallpapers/cave-wall.svg?url'; +import graphite from './wallpapers/graphite.svg?url'; export interface WallpaperDef { id: string; name: string; - category: 'tactical' | 'ambient' | 'custom'; + category: 'landscape' | 'plain' | 'custom'; description: string; - previewGradient: string; + /** CSS background for the picker's thumbnail. */ + preview: string; cssBackground: (customUrl?: string) => string; } @@ -17,348 +27,40 @@ export const WALLPAPER_KEY = 'shellb.kiosk.wallpaper'; export const WALLPAPER_CUSTOM_KEY = 'shellb.kiosk.wallpaper.custom'; export const WALLPAPER_DIM_KEY = 'shellb.kiosk.wallpaper.dim'; -// SVG Tactical Blueprint Data URI -const SVG_BLUEPRINT = `data:image/svg+xml;utf8,${encodeURIComponent(` - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - SYS: SPARK-GB10 // UNIFIED MEMORY BUS 256-BIT - LOC: 37.7749° N, 122.4194° W · HOST: LOCAL - SCHEMA REV 4.2 · TACTICAL - -`)}`; - -// SVG Tactical Topography Data URI -const SVG_TOPOGRAPHY = `data:image/svg+xml;utf8,${encodeURIComponent(` - - - - - - - - - - - - - - - - EL. 2,450 M // SECTOR 7-B - EL. 2,200 M - - -`)}`; - -// SVG Cyber Hex Matrix Data URI -const SVG_CYBERHEX = `data:image/svg+xml;utf8,${encodeURIComponent(` - - - - - - - -`)}`; - -// SVG Starfield / Synthwave (Synthfield) Data URI -const SVG_SYNTHFIELD = `data:image/svg+xml;utf8,${encodeURIComponent(` - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - CONSTELLATION // STARFIELD SHIP CONSOLE - NAV CHRONO · WARP 0.92c · SECTOR 04-CYGNUS - SYNTHFIELD HORIZON // NASA-PUNK SYNTHWAVE - - - - - - -`)}` +const scene = (id: string, name: string, description: string, url: string, base: string, category: WallpaperDef['category'] = 'landscape'): WallpaperDef => { + const bg = `url("${url}") center bottom / cover no-repeat, ${base}`; + return { id, name, category, description, preview: bg, cssBackground: () => bg }; +}; export const WALLPAPERS: WallpaperDef[] = [ - { - id: 'synthfield', - name: 'Starfield Synthwave', - category: 'ambient', - description: 'Starfield deep space constellation meets Synthwave: glowing sliced sun, wireframe grid floor, and 4-color stripe.', - previewGradient: 'linear-gradient(180deg, #09041a 0%, #2a0747 45%, #ff2e97 70%, #ff8a3d 85%, #05010b 100%)', - cssBackground: () => `url("${SVG_SYNTHFIELD}") center/cover no-repeat fixed, #04010a`, - }, - { - id: 'blueprint', - name: 'Tactical Blueprint', - category: 'tactical', - description: 'NVIDIA DGX Spark board layout with micro-vias, chip dies, bus traces, and coordinate crosshairs.', - previewGradient: 'linear-gradient(135deg, #070a12 0%, #0d1a2d 60%, #152c4a 100%)', - cssBackground: () => `url("${SVG_BLUEPRINT}") center/cover no-repeat fixed, #070a12`, - }, - { - id: 'deepspace', - name: 'Cosmic Nebula', - category: 'ambient', - description: 'Deep celestial void with luminous violet-indigo nebula clouds and star dust clusters.', - previewGradient: 'radial-gradient(ellipse at 70% 30%, #4c1d95 0%, #1e1b4b 45%, #050510 100%)', - cssBackground: () => - `radial-gradient(circle at 80% 20%, rgba(139, 92, 246, 0.22) 0%, transparent 45%), ` + - `radial-gradient(circle at 20% 80%, rgba(6, 182, 212, 0.18) 0%, transparent 50%), ` + - `radial-gradient(ellipse at 50% 50%, rgba(30, 27, 75, 0.8) 0%, #05060f 100%)`, - }, - { - id: 'topography', - name: 'Tactical Topography', - category: 'tactical', - description: 'Precision elevation contour lines and alpine topographic survey curves on dark slate.', - previewGradient: 'linear-gradient(135deg, #080c14 0%, #0f172a 60%, #1e293b 100%)', - cssBackground: () => `url("${SVG_TOPOGRAPHY}") center/cover no-repeat fixed, #080c14`, - }, - { - id: 'synthwave', - name: 'Synthwave Horizon', - category: 'ambient', - description: 'Radiant retro-futuristic sunburst on deep indigo with perspective grid floor.', - previewGradient: 'linear-gradient(180deg, #18092e 0%, #3b0764 50%, #f43f5e 80%, #090214 100%)', - cssBackground: () => - `radial-gradient(ellipse at 50% 65%, rgba(244, 63, 94, 0.35) 0%, rgba(59, 7, 100, 0.45) 35%, #0a0316 85%)`, - }, - { - id: 'cyberhex', - name: 'Neural Honeycomb', - category: 'tactical', - description: 'Hexagonal matrix grid with subtle neural node connections and indigo glow.', - previewGradient: 'linear-gradient(135deg, #090b12 0%, #17182e 60%, #1f2347 100%)', - cssBackground: () => `url("${SVG_CYBERHEX}") repeat, #090b12`, - }, - { - id: 'minimal', - name: 'Obsidian Minimal', - category: 'ambient', - description: 'Understated Swiss graphite matte with subtle radial focal lighting and clean corners.', - previewGradient: 'radial-gradient(circle at 50% 45%, #181920 0%, #0b0c10 100%)', - cssBackground: () => - `radial-gradient(circle at 50% 50%, rgba(255, 255, 255, 0.04) 0%, transparent 60%), #0c0e14`, - }, + scene('alpenglow', 'Alpenglow', 'Mountain ridges fading into a rose and violet dusk.', alpenglow, '#10173a'), + scene('night-forest', 'Moonlit Forest', 'A clear night over pine hills, mist lying in the valleys.', nightForest, '#040914'), + scene('sea-dawn', 'Dawn at Sea', 'The sun rising over a calm sea, a path of light on the water.', seaDawn, '#141f45'), + scene('kelp-forest', 'Kelp Forest', 'Sunbeams through blue water, kelp swaying and a whale passing far off.', kelpForest, '#073654'), + scene('desert-dusk', 'Desert Dusk', 'Sand dunes after sunset, the first star out.', desertDusk, '#1c1436'), + scene('aurora', 'Northern Lights', 'An aurora over snowy hills and a line of pines.', auroraUrl, '#030713'), + scene('cave-wall', 'Painted Cave', 'Horses, aurochs and hands from Lascaux on a torchlit limestone wall.', caveWall, '#4a2e1c'), + scene('graphite', 'Graphite', 'Plain soft graphite with a little light from above.', graphite, '#0b0c10', 'plain'), { id: 'custom', name: 'Custom Image URL', category: 'custom', description: 'Load any high-resolution wallpaper image via HTTP/HTTPS URL.', - previewGradient: 'linear-gradient(135deg, #27272a 0%, #3f3f46 50%, #52525b 100%)', + preview: 'linear-gradient(135deg, #27272a 0%, #3f3f46 50%, #52525b 100%)', cssBackground: customUrl => - customUrl ? `url("${customUrl}") center/cover no-repeat fixed, #070a12` : '#070a12', + customUrl ? `url("${customUrl}") center / cover no-repeat, #070a12` : '#070a12', }, ]; -export const DEFAULT_WALLPAPER = 'synthfield'; +export const DEFAULT_WALLPAPER = 'alpenglow'; +// The old vector set (until 2026-10): the plain one keeps its place, the rest fall back to the default. +const RENAMED: Record = { minimal: 'graphite' }; export function getStoredWallpaperId(): string { try { - return localStorage.getItem(WALLPAPER_KEY) || DEFAULT_WALLPAPER; + const id = localStorage.getItem(WALLPAPER_KEY) || DEFAULT_WALLPAPER; + const now = RENAMED[id] ?? id; + return WALLPAPERS.some(w => w.id === now) ? now : DEFAULT_WALLPAPER; } catch { return DEFAULT_WALLPAPER; } @@ -375,9 +77,9 @@ export function getStoredCustomUrl(): string { export function getStoredWallpaperDim(): number { try { const val = localStorage.getItem(WALLPAPER_DIM_KEY); - return val !== null ? Number(val) : 25; // default 25% dim for contrast + return val !== null ? Number(val) : 10; // the scenes are drawn dark enough up top; a touch more for contrast } catch { - return 25; + return 10; } } diff --git a/ui/src/kiosk/wallpapers/alpenglow.svg b/ui/src/kiosk/wallpapers/alpenglow.svg new file mode 100644 index 0000000..27a4067 --- /dev/null +++ b/ui/src/kiosk/wallpapers/alpenglow.svg @@ -0,0 +1 @@ + diff --git a/ui/src/kiosk/wallpapers/aurora.svg b/ui/src/kiosk/wallpapers/aurora.svg new file mode 100644 index 0000000..d9fd5f1 --- /dev/null +++ b/ui/src/kiosk/wallpapers/aurora.svg @@ -0,0 +1 @@ + diff --git a/ui/src/kiosk/wallpapers/cave-wall.svg b/ui/src/kiosk/wallpapers/cave-wall.svg new file mode 100644 index 0000000..d5a7a1f --- /dev/null +++ b/ui/src/kiosk/wallpapers/cave-wall.svg @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui/src/kiosk/wallpapers/desert-dusk.svg b/ui/src/kiosk/wallpapers/desert-dusk.svg new file mode 100644 index 0000000..2228521 --- /dev/null +++ b/ui/src/kiosk/wallpapers/desert-dusk.svg @@ -0,0 +1 @@ + diff --git a/ui/src/kiosk/wallpapers/graphite.svg b/ui/src/kiosk/wallpapers/graphite.svg new file mode 100644 index 0000000..e252cc9 --- /dev/null +++ b/ui/src/kiosk/wallpapers/graphite.svg @@ -0,0 +1 @@ + diff --git a/ui/src/kiosk/wallpapers/kelp-forest.svg b/ui/src/kiosk/wallpapers/kelp-forest.svg new file mode 100644 index 0000000..b981402 --- /dev/null +++ b/ui/src/kiosk/wallpapers/kelp-forest.svg @@ -0,0 +1 @@ + diff --git a/ui/src/kiosk/wallpapers/night-forest.svg b/ui/src/kiosk/wallpapers/night-forest.svg new file mode 100644 index 0000000..57aa131 --- /dev/null +++ b/ui/src/kiosk/wallpapers/night-forest.svg @@ -0,0 +1 @@ + diff --git a/ui/src/kiosk/wallpapers/sea-dawn.svg b/ui/src/kiosk/wallpapers/sea-dawn.svg new file mode 100644 index 0000000..22b7f0a --- /dev/null +++ b/ui/src/kiosk/wallpapers/sea-dawn.svg @@ -0,0 +1 @@ +