Mobile

SplashScreen

Holds the app's mark until ready, without a white flash, in both themes, and yields quickly.

#mobile#interactive#tablet#splash#startup#theme

Demo

Preview

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.qwe
main.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>}
Thread
36Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.