Demo
On change:
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/status_tint.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/status_tint.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/status_tint.qwe
let status_tint_demo_css = css`.status-tint-demo-keys { display: flex; flex-wrap: wrap; gap: 8px; }.status-tint-demo-field { display: grid; gap: 4px; max-width: 320px; color: var(--ui-muted); font: 13px/1.4 var(--ui-font); }.status-tint-demo-field input { box-sizing: border-box; min-height: 46px; padding: 0 12px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface); color: var(--ui-fg); font: 16px var(--ui-font); }`component StatusTintDemoLive() { style global status_tint_demo_css state page = "home" state typed = "" state over = false state base = true state said = "" state changes = 0 let color = match page { "alerts" => "#c62a2f" "typed" => typed _ => null } <div.demo-stack> {base && <StatusTint color={color} dark={page == "alerts" ? "#3a1618" : null} note={true} on_change={(c) => { said = c; changes += 1 }}/>} {over && <StatusTint color="#0b6bcb" background={true}/>} <div.status-tint-demo-keys> <Button size="lg" id="status-tint-demo-home" variant={page == "home" ? "solid" : "outline"} on_click={() => { page = "home" }}>Home</Button> <Button size="lg" id="status-tint-demo-alerts" variant={page == "alerts" ? "solid" : "outline"} on_click={() => { page = "alerts" }}>Alerts</Button> <Button size="lg" id="status-tint-demo-over" variant="soft" on_click={() => { over = !over }}>{over ? "Close the sheet" : "Open a sheet"}</Button> <Button size="lg" id="status-tint-demo-leave" variant="ghost" on_click={() => { base = !base }}>{base ? "Leave this page" : "Come back"}</Button> </div> <label.status-tint-demo-field>Any colour <input id="status-tint-demo-color" value={typed} placeholder="hsl(160 80% 25%)" @input={(e) => { typed = e.value; page = "typed" }}/> </label> <p.demo-note id="status-tint-demo-out" data-changes={changes}>On change: {said}</p> </div>}view StatusTintDemo() = <div><StatusTintDemoLive/></div>Source
ui/status_tint.qwe
// StatusTint: gives the browser's bar the colour of this page and theme. It writes a// `theme-color` meta tag ahead of any the page has, follows the theme as it changes,// whether by the system's dark mode or a class on the page, and takes the tag away when// it leaves, so whatever the page had before is back. Several may stand at once, a// sheet's over a page's: the newest wins, and leaving in any order leaves the right one.//// <StatusTint/> the theme's page colour// <StatusTint color="#0a7d55" dark="#0b3d2e"/> by theme// <StatusTint color={screen == "alerts" ? "var(--ui-danger)" : null}/>//// color: any CSS colour, or null (default) for the theme's --ui-bg. dark: the same, for// the dark theme; color is used there too when this is left out. A colour the browser// cannot read is the same as none. background: also paint the page's own <html>, which// is what shows above a page pulled past its end, and which recent Safaris are said to// take their bar from in place of theme-color (not tried here). note: say on the page// what was done, and what could not be.// on_change(color): inside one of your components, given the colour as it changes.//// It takes up no room. Where it does not work: a phone app's web view reads no// theme-color, and the shells have no call for the status bar's colour, so in the apps// it does nothing but say so under `note`; a desktop browser shows this only for an// installed app.let status_tint_css = css`.ui-status-tint { display: contents; }.ui-status-tint-probe { background-color: var(--ui-bg); }.ui-status-tint-note { display: block; margin: 0; color: var(--ui-muted); font: 13px/1.5 var(--ui-font); }`fn status_tint_byte(x) -> str { let n = min([max([round(x), 0]), 255]) let digits = split("0123456789abcdef", "") digits[floor(n / 16)] + digits[n % 16]}// A computed colour as #rrggbb, or as it came for a space other than sRGB, as theme-color// takes any CSS colour. Fully see-through is null.fn status_tint_hex(text) { if type_of(text) != "str" || text == "" || text == "transparent" { return null } let unit = starts_with(text, "color(srgb ") if !unit && !starts_with(text, "rgb") { return text } let nums = re.find_all(text, "[0-9]+[.]?[0-9]*") | map((m) => float(m.text)) if len(nums) < 3 { return text } if len(nums) > 3 && nums[3] == 0 { return null } let k = unit ? 255 : 1 "#" + status_tint_byte(nums[0] * k) + status_tint_byte(nums[1] * k) + status_tint_byte(nums[2] * k)}// The line of tints standing, kept on the meta tag itself so that it is the page's and no// global's: this one's colour, replaced where it stands or added as the newest.fn status_tint_put(stack: list, id, color) -> list { if any(stack, (s) => s.id == id) { return stack | map((s) => s.id == id ? { id, color } : s) } stack + [{ id, color }]}fn status_tint_drop(stack: list, id) -> list = stack | filter((s) => s.id != id)fn status_tint_top(stack: list) = len(stack) > 0 ? stack[len(stack) - 1].color : nullfn status_tint_read(tag) -> list { let text = tag.getAttribute("data-stack") if text == null || text == "" { return [] } let got = json.parse(text) is_err(got) || type_of(got) != "list" ? [] : got}// The tag, made first in <head>, as the browser takes the first that fits.fn status_tint_tag(doc) { let found = doc.querySelector("meta[data-ui-status-tint]") if found != null { return found } let tag = doc.createElement("meta") tag.setAttribute("name", "theme-color") tag.setAttribute("data-ui-status-tint", "") tag.setAttribute("data-was", doc.documentElement.style.backgroundColor) doc.head.prepend(tag) tag}fn status_tint_write(doc, tag, stack: list, paint: bool) { if len(stack) == 0 { doc.documentElement.style.backgroundColor = tag.getAttribute("data-was") ?? "" tag.remove() } else { let top = status_tint_top(stack) tag.setAttribute("data-stack", json.stringify(stack)) tag.setAttribute("content", top) if paint { doc.documentElement.style.backgroundColor = top } }}component StatusTint(color: str? = null, dark: str? = null, background: bool = false, note: bool = false, on_change = null) { style global status_tint_css state win = null state probe = null state me = null state shown = null state native = false let paint = (light, deep) => { let doc = win.document probe.style.backgroundColor = "" let deep_now = contains(str(win.getComputedStyle(probe).colorScheme), "dark") let wanted = deep_now && deep != null ? deep : light if wanted != null { probe.style.backgroundColor = wanted } let hex = status_tint_hex(win.getComputedStyle(probe).backgroundColor) // See-through, or unreadable: the theme's. if hex == null { probe.style.backgroundColor = "" hex = status_tint_hex(win.getComputedStyle(probe).backgroundColor) } if hex != null { let tag = status_tint_tag(doc) status_tint_write(doc, tag, status_tint_put(status_tint_read(tag), me, hex), background) if hex != shown { shown = hex on_change && on_change(hex) } } } if probe != null { paint(color, dark) } <span.ui-status-tint @mount={(el) => { let w = js("window") let doc = w.document let scheme = js("matchMedia", "(prefers-color-scheme: dark)") let id = uuid() let again = () => { paint(color, dark) } let changes = js_new("MutationObserver", again) changes.observe(doc.documentElement, { attributes: true, attributeFilter: ["class"] }) changes.observe(doc.body, { attributes: true, attributeFilter: ["class"] }) scheme.addEventListener("change", again) doc.addEventListener("ui-theme-change", again) win = w me = id native = w.quilloNative != null probe = el.querySelector(".ui-status-tint-probe") () => { changes.disconnect() scheme.removeEventListener("change", again) doc.removeEventListener("ui-theme-change", again) let tag = doc.querySelector("meta[data-ui-status-tint]") if tag != null { status_tint_write(doc, tag, status_tint_drop(status_tint_read(tag), id), background) } } }}> <span.ui-status-tint-probe hidden></span> {note && <span.ui-status-tint-note role="status">{native ? "This app's bar keeps its own colour: a web view reads no theme-color." : (shown != null ? `The browser's bar is asked for ${shown}.` : "")}</span>} </span>}Related
SplashScreenHolds the app's mark until ready, without a white flash, in both themes, and yields quickly.3 views · 6 downloadsThemeSheetLight, dark, system and accent in a sheet, with a live preview, kept, and no flash next time.2 views · 8 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 downloads
Thread
Thread 0 posts
On StatusTint: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.