SplashScreen
Holds the app's mark until ready, without a white flash, in both themes, and yields quickly.
Demo
The app, light.
The app, dark.
Held.
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/splash_screen.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/splash_screen.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/splash_screen.qwe
view SplashScreenDemoMark() = <svg viewBox="0 0 64 64" fill="none" aria-hidden="true"><rect x="6" y="6" width="52" height="52" rx="14" fill="var(--ui-accent)"/><path d="M20 44V20h14a8 8 0 0 1 0 16H20M34 36l10 8" stroke="var(--ui-accent-fg)" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/></svg>component SplashScreenDemoLive() { state run = 0 state ready = false state hold = 0 state limit = 60000 state said = "Held." let again = (r: bool, h: int, m: int) => { ready = r hold = h limit = m said = "Held." run = run + 1 } <div.demo-stack> <div.demo-stack> <div.demo-phone.ui-light id="splash-screen-demo-light" style="height: 260px"> <p style="padding: 16px">The app, light.</p> <SplashScreen key={run} name="Fieldbook" fixed={true} ready={ready} min_ms={hold} max_ms={limit} show_name={true} on_gone={(w) => { said = `Gone: ${w}.` }}><SplashScreenDemoMark/></SplashScreen> </div> <div.demo-phone.ui-dark id="splash-screen-demo-dark" style="height: 260px"> <p style="padding: 16px; color: var(--ui-fg)">The app, dark.</p> <SplashScreen key={run} name="Fieldbook" fixed={true} ready={ready} min_ms={hold} max_ms={limit}><SplashScreenDemoMark/></SplashScreen> </div> </div> <div.demo-row> <Button size="lg" id="splash-screen-demo-ready" on_click={() => { ready = true }}>The app is ready</Button> <Button size="lg" variant="outline" id="splash-screen-demo-again" on_click={() => { again(false, 0, 60000) }}>Hold it again</Button> <Button size="lg" variant="outline" id="splash-screen-demo-min" on_click={() => { again(true, 600, 60000) }}>Ready at once, holds 0.6 s</Button> <Button size="lg" variant="outline" id="splash-screen-demo-max" on_click={() => { again(false, 0, 500) }}>Never ready, gives up at 0.5 s</Button> <Button size="lg" variant="ghost" id="splash-screen-demo-odd" on_click={() => { again(false, 900, 300) }}>Least past the most</Button> </div> <p.demo-note id="splash-screen-demo-said">{said}</p> </div>}view SplashScreenDemo() = <SplashScreenDemoLive/>Source
ui/splash_screen.qwe
// SplashScreen: holds the app's mark over the page until the app is ready, then gets out of// the way. It is in the page the server sends, in the theme's own colours, so the first// thing painted is the mark on the right background, light or dark, with no white flash, and it// needs no script to go: past max_ms it leaves by CSS alone, whatever else has failed.//// component App() {// state notes = null// <div @mount={() => { notes = load_notes() }}>...</div>// <SplashScreen name="Fieldbook" ready={notes != null}><img src="/mark.svg" alt=""/></SplashScreen>// }//// children: the mark. name: the app's name, for screen readers ("Loading Fieldbook"), and shown under// the mark if show_name. ready: true lets it go; null waits for the page to have loaded.// min_ms: the least it stays, for a mark that should not blink (0). max_ms: the most (3000); it// goes then even if never told, as a splash must not become a wall. fixed: fills the screen (true);// false fills its box. on_gone(why): inside one of your components, once it has left, with// "ready" or "timeout".//// For the moments before the first paint, put these in <head>, with the theme's colours://// <meta name="color-scheme" content="light dark"/>// <meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff"/>// <meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0f0e17"/>//// In a phone app what shows before the web view paints is the app's own launch screen,// which a program does not set.import ui/theme.qwe use ui_clslet splash_screen_css = css`.ui-splash-screen-host { display: contents; }.ui-splash-screen { position: absolute; inset: 0; z-index: 80; display: grid; align-content: center; justify-items: center; gap: 14px; color-scheme: light dark; container: ui-splash-screen / size; background: var(--ui-bg, Canvas); color: var(--ui-fg, CanvasText); font: 500 17px/1.3 var(--ui-font, system-ui, sans-serif); animation: ui-splash-screen-yield .18s linear var(--ui-splash-screen-max, 3s) forwards; transition: opacity .18s linear;}.ui-splash-screen-fixed { position: fixed; }.ui-splash-screen-leaving { opacity: 0; pointer-events: none; }@keyframes ui-splash-screen-yield { to { opacity: 0; visibility: hidden; pointer-events: none; } }.ui-splash-screen-mark { display: grid; place-items: center; width: min(32cqmin, 120px); aspect-ratio: 1; }.ui-splash-screen-mark > *, .ui-splash-screen-mark > quillo-raw > * { display: block; width: 100%; height: 100%; object-fit: contain; }/* Past a second something is wrong enough to say so: a thin bar, and no sooner, as most starts are faster. */.ui-splash-screen-slow { width: 72px; height: 3px; overflow: hidden; border-radius: 999px; background: var(--ui-subtle, transparent); opacity: 0; animation: ui-splash-screen-show .2s linear 1s forwards; }.ui-splash-screen-slow span { display: block; width: 40%; height: 100%; border-radius: inherit; background: var(--ui-accent, currentColor); animation: ui-splash-screen-run 1s ease-in-out infinite alternate; }@keyframes ui-splash-screen-show { to { opacity: 1; } }@keyframes ui-splash-screen-run { to { transform: translateX(150%); } }@container ui-splash-screen (min-width: 600px) { .ui-splash-screen-mark { width: min(40cqh, 180px); } }@media (prefers-reduced-motion: reduce) { .ui-splash-screen { transition: none; } .ui-splash-screen-slow span { animation: none; width: 100%; }}`// What it should do `elapsed` ms after it came up: "hold" or "leave". max wins over min, so// props the wrong way round still end.fn splash_screen_phase(elapsed: num, ready: bool, min_ms: num, max_ms: num) -> str { if elapsed >= max_ms { return "leave" } ready && elapsed >= min_ms ? "leave" : "hold"}component SplashScreen(name: str = "the app", ready: bool? = null, min_ms: int = 0, max_ms: int = 3000, fixed: bool = true, show_name: bool = false, on_gone = null, class: str? = null, children) { style global splash_screen_css state phase = "hold" // then "leaving", then "gone" state why = "" state born = 0 state left_at = 0 state fade = 180 state page_loaded = false let check = () => { let t = js("Date.now") let set_for = ready ?? page_loaded if phase == "hold" { let elapsed = t - born if splash_screen_phase(elapsed, set_for, min_ms, max_ms) == "leave" { why = elapsed >= max_ms && !set_for ? "timeout" : "ready" left_at = t phase = "leaving" } } else if phase == "leaving" && t - left_at >= fade { phase = "gone" on_gone && on_gone(why) } } // The only clock a component has. Once a tick has seen `born` it runs until it is gone. if phase != "gone" && born > 0 { every(40, check) } <div.ui-splash-screen-host @mount={() => { if js("matchMedia", "(prefers-reduced-motion: reduce)").matches { fade = 0 } page_loaded = js("document.readyState") == "complete" if !page_loaded { js("window.addEventListener", "load", (_e) => { page_loaded = true }, { once: true }) } born = js("Date.now") }}> {phase != "gone" && <div class={ui_cls(["ui-splash-screen", fixed && "ui-splash-screen-fixed", phase == "leaving" && "ui-splash-screen-leaving", class])} style={`--ui-splash-screen-max: ${max_ms}ms`} role="status" aria-label={`Loading ${name}`} aria-busy={phase == "hold" ? "true" : null} data-phase={phase}> <div.ui-splash-screen-mark aria-hidden="true">{children}</div> {show_name && <div aria-hidden="true">{name}</div>} <div.ui-splash-screen-slow aria-hidden="true"><span></span></div> </div>} </div>}Related
ThemeSheetLight, dark, system and accent in a sheet, with a live preview, kept, and no flash next time.2 views · 7 downloadsActionSheetA list of actions rising from the bottom, with a destructive style and Cancel: swipe down or the phone's back gesture closes it; on a tablet, a card by what opened it.3 views · 9 downloadsActivityRingsProgress rings inside each other that go round again past 100%, drawn once, with a legend and a sentence each for screen readers.3 views · 6 downloadsAdaptiveDialogA sheet on a phone and a centred dialog on a tablet or desktop: the same content, chosen by width, changing live on rotation.3 views · 5 downloadsAlertInboxAlerts by severity, swiped to acknowledge or snooze, with buttons and keys to the same, and a badge that stays right as snoozes run out.3 views · 8 downloadsAlphaRailListA long list with sticky letter headers and an A to Z rail scrubbed by finger, with a bubble showing the letter and a haptic per step.1 view · 6 downloads
Thread
Thread 0 posts
On SplashScreen: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.