Quarterly numbers
From Ana Silva · 09:41
Hi all, the report is attached. Revenue is up 12% on last quarter, mostly from the new plans.
Two panes with a divider to drag between them, across or stacked, with the keyboard too.
From Ana Silva · 09:41
Hi all, the report is attached. Revenue is up 12% on last quarter, mostly from the new plans.
select name, total from orders where placed_at > now() - interval '7 days' order by total desc
The outline takes 35% of the width.
Drag a divider, or focus it and use the arrow keys, Home and End; Enter or a double click folds a collapsible one.
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/splitter.qweimport ui/theme.qwe use theme_cssuse ui/splitter.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>The code of the preview above.
let splitter_demo_folders = ["Inbox", "Starred", "Sent", "Drafts", "Archive"]view SplitterDemoFolders() = <nav aria-label="Folders" style="padding: 12px"> {for f in splitter_demo_folders => <div style="padding: 6px 8px; border-radius: var(--ui-radius-sm)">{f}</div>} </nav>view SplitterDemoMessage() = <article style="padding: 16px 20px"> <h4 style="margin: 0 0 4px">Quarterly numbers</h4> <p style="margin: 0 0 12px; color: var(--ui-muted); font-size: 13px">From Ana Silva · 09:41</p> <p style="margin: 0">Hi all, the report is attached. Revenue is up 12% on last quarter, mostly from the new plans.</p> </article>view SplitterDemoQuery() = <pre style="margin: 0; padding: 14px 16px; font: 13px/1.6 var(--ui-mono)">{"select name, total\nfrom orders\nwhere placed_at > now() - interval '7 days'\norder by total desc"}</pre>view SplitterDemoResults() = <div style="padding: 12px 16px; font-size: 13px"> {for [n, t] in [["Ana", "€1,240"], ["Bruno", "€980"], ["Chen", "€815"], ["Dara", "€640"], ["Eli", "€512"]] => <div style="display: flex; justify-content: space-between; padding: 6px 0; border-bottom: 1px solid var(--ui-border)"> <span>{n}</span><span>{t}</span> </div>} </div>component SplitterDemoLive() { state pct = 35 <div.demo-stack> <Splitter id="splitter-demo-live" label="Resize the preview" size={35} min={20} max={80} collapsible={true} on_change={(p) => { pct = p }} first={<div style="padding: 14px">Outline</div>} second={<div style="padding: 14px">Preview</div>}/> <p.demo-note>The outline takes {pct == 0 ? "no room: it is folded away" : `${pct}% of the width`}.</p> </div>}view SplitterDemo() = <div.demo-stack> <Splitter id="splitter-demo-mail" label="Resize the folder list" size={30} min={20} max={60} collapsible={true} first={<SplitterDemoFolders/>} second={<SplitterDemoMessage/>}/> <Splitter id="splitter-demo-sql" label="Resize the query editor" vertical={true} height="300px" size={40} min={20} max={80} first={<SplitterDemoQuery/>} second={<SplitterDemoResults/>}/> <SplitterDemoLive/> <p.demo-note>Drag a divider, or focus it and use the arrow keys, Home and End; Enter or a double click folds a collapsible one.</p> </div>// Splitter: two panes side by side (or one above the other) with a divider// between them that people drag to share the space: a file list and an editor,// a list and its details. The divider is an ARIA window splitter: it takes focus,// the arrow keys move it, Home and End take it to its limits, and Enter folds the// first pane away when it is collapsible. The panes are any HTML from the server.//// <Splitter label="Resize the file list" size={30} min={15} max={60}// first={<FileTree/>} second={<Editor/>}/>// <Splitter vertical={true} height="420px" first={<Query/>} second={<Results/>}// collapsible={true} on_change={(pct) => { split = pct }}/>//// first, second: the two panes' HTML (left and right, or top and bottom).// size: how much of the room the first pane takes, in percent (50). min, max: the// limits it is dragged between (10, 90). step: how far one arrow key moves it (5).// vertical: one pane above the other. height: the splitter's height, as CSS// ("320px" by default when vertical; side by side, as tall as the panes).// collapsible: Enter or a double click on the divider folds the first pane to// nothing, and again brings it back. label: what screen readers call the divider// ("Resize panes"). id: the first pane's id, which the divider names as what it// controls ("ui-splitter"): give each splitter on a page its own.// on_change(size): inside one of your components, given the first pane's size in// percent (0 when folded) after each drag or key.// SplitterHandle is the divider itself, which Splitter places for you.import ui/theme.qwe use ui_clslet splitter_css = css`.ui-splitter { --ui-splitter-at: 50%; display: grid; min-width: 0; height: var(--ui-splitter-height, auto); overflow: hidden; grid-template-columns: minmax(0, var(--ui-splitter-at)) auto minmax(0, 1fr); grid-template-rows: minmax(0, 1fr); border: 1px solid var(--ui-border); border-radius: var(--ui-radius); background: var(--ui-bg); color: var(--ui-fg); font: 14px/1.5 var(--ui-font);}.ui-splitter-vertical { height: var(--ui-splitter-height, 320px); grid-template-columns: minmax(0, 1fr); grid-template-rows: minmax(0, var(--ui-splitter-at)) auto minmax(0, 1fr);}.ui-splitter-pane { min-width: 0; min-height: 0; overflow: auto; }.ui-splitter-collapsed > .ui-splitter-pane:first-child { visibility: hidden; }.ui-splitter-handle { position: relative; z-index: 1; display: grid; place-items: center; width: 9px; margin: 0 -4px; cursor: col-resize; touch-action: none; user-select: none; outline: none;}.ui-splitter-handle::before { content: ""; position: absolute; inset: 0 4px; background: var(--ui-border); transition: background .15s; }.ui-splitter-vertical .ui-splitter-handle { width: auto; height: 9px; margin: -4px 0; cursor: row-resize; }.ui-splitter-vertical .ui-splitter-handle::before { inset: 4px 0; }.ui-splitter-grip { position: relative; display: grid; gap: 3px; padding: 6px 3px; border: 1px solid var(--ui-border); border-radius: 999px; background: var(--ui-surface); pointer-events: none; transition: border-color .15s;}.ui-splitter-vertical .ui-splitter-grip { grid-auto-flow: column; padding: 3px 6px; }.ui-splitter-grip i { display: block; width: 3px; height: 3px; border-radius: 50%; background: var(--ui-muted); }.ui-splitter-handle:hover::before, .ui-splitter-dragging .ui-splitter-handle::before { background: var(--ui-accent); }.ui-splitter-handle:hover .ui-splitter-grip, .ui-splitter-dragging .ui-splitter-grip { border-color: var(--ui-accent); }.ui-splitter-handle:focus-visible::before { background: var(--ui-accent); }.ui-splitter-handle:focus-visible .ui-splitter-grip { border-color: var(--ui-accent); box-shadow: var(--ui-focus); }.ui-splitter-dragging { cursor: col-resize; user-select: none; }.ui-splitter-vertical.ui-splitter-dragging { cursor: row-resize; }.ui-splitter-dragging .ui-splitter-pane { pointer-events: none; }@media (prefers-reduced-motion: reduce) { .ui-splitter-handle::before, .ui-splitter-grip { transition: none; } }`fn splitter_clamp(v: num, lo: num, hi: num) -> num = v < lo ? lo : v > hi ? hi : vview Splitter(first = null, second = null, size: num = 50, min: num = 10, max: num = 90, step: num = 5, vertical: bool = false, height: str? = null, collapsible: bool = false, label: str = "Resize panes", id: str = "ui-splitter", on_change = null, class: str? = null) { style global splitter_css let pane = id let at = splitter_clamp(size, min, max) <div class={ui_cls(["ui-splitter", vertical && "ui-splitter-vertical", class])} style={`--ui-splitter-at: ${at}%${height != null ? `; --ui-splitter-height: ${height}` : ""}`}> <div.ui-splitter-pane id={pane}>{first}</div> <SplitterHandle size={at} min={min} max={max} step={step} vertical={vertical} collapsible={collapsible} label={label} controls={pane} on_change={on_change}/> <div.ui-splitter-pane>{second}</div> </div>}component SplitterHandle(size: num = 50, min: num = 10, max: num = 90, step: num = 5, vertical: bool = false, collapsible: bool = false, label: str = "Resize panes", controls: str? = null, on_change = null) { style global splitter_css state at = size state folded = false state dragging = false state box = null let shown = folded ? 0 : at if box != null { box.style.setProperty("--ui-splitter-at", `${shown}%`) box.classList.toggle("ui-splitter-collapsed", folded) box.classList.toggle("ui-splitter-dragging", dragging) } let tell = () => { on_change && on_change(round(folded ? 0 : at)) } let to = (v: num) => { folded = false at = splitter_clamp(v, min, max) } let fold = () => { if collapsible { folded = !folded tell() } } <div.ui-splitter-handle role="separator" tabindex="0" aria-label={label} aria-controls={controls} aria-orientation={vertical ? "horizontal" : "vertical"} aria-valuenow={round(shown)} aria-valuemin={folded ? 0 : min} aria-valuemax={max} aria-valuetext={folded ? "Collapsed" : `${round(shown)}%`} @mount={(el) => { box = el.closest(".ui-splitter") }} @pointerdown={(e) => { if e.button == 0 { e.preventDefault() e.target.setPointerCapture(e.pointerId) e.target.focus() dragging = true } }} @pointermove={(e) => { if dragging { let r = box.getBoundingClientRect() to(vertical ? (e.clientY - r.top) / r.height * 100 : (e.clientX - r.left) / r.width * 100) } }} @pointerup={() => { if dragging { dragging = false tell() } }} @pointercancel={() => { dragging = false }} @dblclick={fold} @keydown={(e) => { let goal = match e.key { "ArrowLeft" | "ArrowUp" => shown - step "ArrowRight" | "ArrowDown" => (folded ? min : shown + step) "Home" => min "End" => max _ => null } if goal != null { e.preventDefault() to(goal) tell() } else if e.key == "Enter" && collapsible { e.preventDefault() fold() } }}> <span.ui-splitter-grip aria-hidden="true"><i></i><i></i><i></i></span> </div>}On Splitter: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.