Mobile

SizeClassView

Tells compact, medium and expanded widths by its own box, so Split View, Slide Over and a turn are one case, and swaps slots without losing what is in them.

#mobile#tablet#interactive#layout#responsive#panes

Demo

Preview
unknown

Today

Delivery 1

Delivery 2

Delivery 3

Delivery 4

Delivery 5

Delivery 6

Delivery 7

Delivery 8

Delivery 9

Delivery 10

Delivery 11

Delivery 12

Delivery 13

Delivery 14

Delivery 15

Delivery 16

Delivery 17

Delivery 18

Delivery 19

Delivery 20

Delivery 21

Delivery 22

Delivery 23

Delivery 24

Delivery 25

Delivery 26

Delivery 27

Delivery 28

Delivery 29

Delivery 30

Delivery 31

Delivery 32

Delivery 33

Delivery 34

Delivery 35

Delivery 36

Delivery 37

Delivery 38

Delivery 39

Delivery 40

Today

Delivery 1

Delivery 2

Delivery 3

Delivery 4

Delivery 5

Delivery 6

Delivery 7

Delivery 8

Delivery 9

Delivery 10

Delivery 11

Delivery 12

Delivery 13

Delivery 14

Delivery 15

Delivery 16

Delivery 17

Delivery 18

Delivery 19

Delivery 20

Delivery 21

Delivery 22

Delivery 23

Delivery 24

Delivery 25

Delivery 26

Delivery 27

Delivery 28

Delivery 29

Delivery 30

Delivery 31

Delivery 32

Delivery 33

Delivery 34

Delivery 35

Delivery 36

Delivery 37

Delivery 38

Delivery 39

Delivery 40

Van

Today

Delivery 1

Delivery 2

Delivery 3

Delivery 4

Delivery 5

Delivery 6

Delivery 7

Delivery 8

Delivery 9

Delivery 10

Delivery 11

Delivery 12

Delivery 13

Delivery 14

Delivery 15

Delivery 16

Delivery 17

Delivery 18

Delivery 19

Delivery 20

Delivery 21

Delivery 22

Delivery 23

Delivery 24

Delivery 25

Delivery 26

Delivery 27

Delivery 28

Delivery 29

Delivery 30

Delivery 31

Delivery 32

Delivery 33

Delivery 34

Delivery 35

Delivery 36

Delivery 37

Delivery 38

Delivery 39

Delivery 40

Van

Driver

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/size_class_view.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/size_class_view.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/size_class_view.qwe
let size_class_view_demo_css = css`.demo-size-class { display: flex; flex-direction: column; height: 100%; }.demo-size-class-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding: 8px 12px; border-bottom: 1px solid var(--ui-border); background: var(--ui-subtle); font: 14px/1.4 var(--ui-font); }.demo-size-class-bar output { margin-left: auto; font-weight: 600; }.demo-size-class-room { flex: 1; min-height: 0; background: var(--ui-subtle); }.demo-size-class-box { height: 100%; margin: 0 auto; background: var(--ui-bg); border-inline: 1px solid var(--ui-border); }.demo-size-class-cols { display: grid; gap: 16px; height: 100%; padding: 16px; }.demo-size-class-col { display: flex; flex-direction: column; gap: 10px; min-width: 0; min-height: 0; }.demo-size-class-col h4 { margin: 0; font: 600 15px/1.3 var(--ui-font); }.demo-size-class-scroll { flex: 1 1 200px; min-height: 120px; overflow: auto; border: 1px solid var(--ui-border); border-radius: var(--ui-radius); }.demo-size-class-scroll p { margin: 0; padding: 10px 12px; border-bottom: 1px solid var(--ui-border); font: 14px/1.4 var(--ui-font); }.demo-size-class-col textarea { box-sizing: border-box; width: 100%; min-height: 72px; padding: 8px 10px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface); color: var(--ui-fg); font: 16px/1.4 var(--ui-font); }`let size_class_view_demo_rooms = [    { id: "full", label: "Whole screen", css: "width: 100%" },    { id: "two", label: "Two thirds", css: "width: 66.6%" },    { id: "half", label: "Half", css: "width: 50%" },    { id: "over", label: "Slide Over", css: "width: 320px" },]let size_class_view_demo_rows = range(1, 41) | map((n) => `Delivery ${n}`)component SizeClassViewDemoCount(name: str) {    state n = 0    <Button id={`size-demo-count-${name}`} size="lg" variant="soft" on_click={() => { n += 1 }}>Packed {n}</Button>}view SizeClassViewDemoList(name: str) =    <div.demo-size-class-scroll id={`size-demo-list-${name}`} data-keep="deliveries" tabindex="0" aria-label="Deliveries">        {for r in size_class_view_demo_rows => <p>{r}</p>}    </div>view SizeClassViewDemoNote(name: str) =    <textarea id={`size-demo-note-${name}`} data-keep="note" aria-label="Note for the driver" placeholder="Note for the driver"></textarea>component SizeClassViewDemoLive() {    style global size_class_view_demo_css    state room = "full"    state size = "unknown"    let shown = size_class_view_demo_rooms | filter((r) => r.id == room)    <div.demo-tablet>        <div.demo-size-class>            <div.demo-size-class-bar>                {for r in size_class_view_demo_rooms =>                    <Button key={r.id} id={`size-demo-room-${r.id}`} size="lg" variant={r.id == room ? "solid" : "outline"}                            on_click={() => { room = r.id }}>{r.label}</Button>}                <output id="size-demo-now">{size}</output>            </div>            <div.demo-size-class-room>                <div.demo-size-class-box id="size-demo-box" style={shown[0].css}>                    <SizeClassView fill={true} on_change={(c) => { size = c }}>                        <SizeClassSlot when="compact">                            <div.demo-size-class-cols>                                <div.demo-size-class-col>                                    <h4>Today</h4>                                    <SizeClassViewDemoCount name="compact"/>                                    <SizeClassViewDemoNote name="compact"/>                                    <SizeClassViewDemoList name="compact"/>                                </div>                            </div>                        </SizeClassSlot>                        <SizeClassSlot when="medium">                            <div.demo-size-class-cols style="grid-template-columns: 1fr 1fr">                                <div.demo-size-class-col><h4>Today</h4><SizeClassViewDemoList name="medium"/></div>                                <div.demo-size-class-col>                                    <h4>Van</h4>                                    <SizeClassViewDemoCount name="medium"/>                                    <SizeClassViewDemoNote name="medium"/>                                </div>                            </div>                        </SizeClassSlot>                        <SizeClassSlot when="expanded">                            <div.demo-size-class-cols style="grid-template-columns: 1fr 1fr 1fr">                                <div.demo-size-class-col><h4>Today</h4><SizeClassViewDemoList name="expanded"/></div>                                <div.demo-size-class-col><h4>Van</h4><SizeClassViewDemoCount name="expanded"/></div>                                <div.demo-size-class-col><h4>Driver</h4><SizeClassViewDemoNote name="expanded"/></div>                            </div>                        </SizeClassSlot>                    </SizeClassView>                </div>            </div>        </div>    </div>}view SizeClassViewDemo() = <SizeClassViewDemoLive/>

Source

ui/size_class_view.qwe
// SizeClassView: says whether the room it has is compact, medium or expanded, and// shows the slots meant for that. It measures its own box, not the screen, so an iPad// in Split View, a Slide Over, a pane or a turn are all the same thing to it: a new// width. Every slot stays on the page and only the shown one is drawn, so what is in// the others keeps its state, its text and its scroll while it is away.////   import ui/size_class_view.qwe use SizeClassView, SizeClassSlot////   <SizeClassView on_change={(c) => { size = c }}>//       <SizeClassSlot when="compact"><Stacked/></SizeClassSlot>//       <SizeClassSlot when="medium expanded"><Columns/></SizeClassSlot>//   </SizeClassView>//// compact is under 600 px wide, medium under 840, expanded the rest: a phone, an iPad// upright, an iPad turned. They are fixed, as CSS has to know them before any script// runs and a custom property cannot reach a container query; that is why the right// slot shows on the first paint, with no flash.//// children: SizeClassSlots, or anything else, which shows at every size.// on_change(size): inside one of your components, called with "compact", "medium" or// "expanded" when the browser first measures and each time that changes.// fill: take the height of the box around it (false: as tall as what is in it).// class: added to its root. data-size-class on the root says the size, for your CSS.//// SizeClassSlot(when): shown at the sizes in `when`, separated by spaces.//// data-keep="name" on an element in two slots makes them one thing to a person: when// the size changes, the one coming into view takes the other's text, scroll and// caret, and the focus if it had it, so someone typing goes on typing. Nothing else// moves between slots: for a value both must show, hold it in your own state.import ui/theme.qwe use ui_clslet size_class_view_css = css`.ui-size-class { position: relative; container: ui-size-class / inline-size; min-width: 0; width: 100%; }.ui-size-class-fill { height: 100%; }/* Hidden with visibility, not display: an element that is not laid out forgets its   scroll, and these must come back as they were. Neither focus nor a screen reader   reaches it. */.ui-size-class-slot { position: absolute; inset: 0 0 auto 0; height: 0; overflow: hidden; visibility: hidden; pointer-events: none; }/* Chrome's scroll anchoring moves a list as its slot leaves, by the height it lost. */.ui-size-class-slot * { overflow-anchor: none; }@container ui-size-class (max-width: 599.98px) {  .ui-size-class-slot[data-when~="compact"] { position: relative; inset: auto; height: auto; overflow: visible; visibility: visible; pointer-events: auto; }}@container ui-size-class (min-width: 600px) and (max-width: 839.98px) {  .ui-size-class-slot[data-when~="medium"] { position: relative; inset: auto; height: auto; overflow: visible; visibility: visible; pointer-events: auto; }}@container ui-size-class (min-width: 840px) {  .ui-size-class-slot[data-when~="expanded"] { position: relative; inset: auto; height: auto; overflow: visible; visibility: visible; pointer-events: auto; }}.ui-size-class-fill > .ui-size-class-slot { min-height: 0; }@container ui-size-class (max-width: 599.98px) { .ui-size-class-fill > .ui-size-class-slot[data-when~="compact"] { height: 100%; } }@container ui-size-class (min-width: 600px) and (max-width: 839.98px) { .ui-size-class-fill > .ui-size-class-slot[data-when~="medium"] { height: 100%; } }@container ui-size-class (min-width: 840px) { .ui-size-class-fill > .ui-size-class-slot[data-when~="expanded"] { height: 100%; } }`// The one place that names the sizes, as the CSS above does. A width that is not a// number is the widest, where nothing is hidden for want of room.fn size_class_view_of(width) -> str {    let w = type_of(width) == "int" || type_of(width) == "float" ? width : 10000    w < 600 ? "compact" : w < 840 ? "medium" : "expanded"}// Whether a slot's `when` names a size: whole words, so "compact" never matches "not-compact".fn size_class_view_in(when, size: str) -> bool = contains(split(str(when ?? ""), " "), size)// Whether element e is in a slot shown at size s. A top-level function, as a component's// own may not be given to `filter` on every Quillo version.fn size_class_view_inside(e, s: str) -> bool {    let slot = e.closest(".ui-size-class-slot")    slot != null && size_class_view_in(slot.dataset.when, s)}// Every element with a data-keep of that name, in whichever slots.fn size_class_view_twins(root, name: str) {    let found = root.querySelectorAll("[data-keep]")    range(found.length) | map((i) => found[i]) | filter((e) => e.dataset.keep == name)}view SizeClassSlot(when: str = "compact medium expanded", class: str? = null, children) {    style global size_class_view_css    <div class={ui_cls(["ui-size-class-slot", class])} data-when={when}>{children}</div>}component SizeClassView(on_change = null, fill: bool = false, class: str? = null, children) {    style global size_class_view_css    state size = ""            // "" until measured: the server cannot know, and the CSS does not need to    state kept = ""            // the data-keep of what holds the focus    state tops = {}            // each data-keep's scroll, noted as it scrolls: a slot leaving is laid out anew before anyone can ask it    // What came into view takes over from what left. Each pair is looked at once, by    // name, so three slots sharing a name still give the text of the one that left.    let carry = (root, left: str, came: str) => {        let all = root.querySelectorAll("[data-keep]")        let names = range(all.length) | map((i) => all[i].dataset.keep) | unique()        for name in names {            let twins = size_class_view_twins(root, name)            let from = twins | filter((e) => size_class_view_inside(e, left) && !size_class_view_inside(e, came))            let to = twins | filter((e) => size_class_view_inside(e, came) && !size_class_view_inside(e, left))            if len(from) > 0 && len(to) > 0 {                let a = from[0]                let b = to[0]                if a.value != null && a.value != b.value {                    b.value = a.value                    b.dispatchEvent(js_new("Event", "input", { bubbles: true }))                }                let top = get(tops, name, 0)                a.scrollTop = top                b.scrollTop = top                if kept == name {                    b.focus({ preventScroll: true })                    if a.selectionStart != null && b.setSelectionRange != null { b.setSelectionRange(a.selectionStart, a.selectionEnd) }                }            }        }    }    <div class={ui_cls(["ui-size-class", fill && "ui-size-class-fill", class])} data-size-class={size == "" ? null : size}         @focusin={(e) => {             let holder = e.target.closest("[data-keep]")             kept = holder != null ? holder.dataset.keep : ""         }}         @mount={(el) => {             let look = (_e = null) => {                 let next = size_class_view_of(el.clientWidth)                 if next != size {                     let was = size                     size = next                     if was != "" { carry(el, was, next) }                     on_change && on_change(next)                 }             }             let scrolled = (e) => {                 let t = e.target                 if t.closest != null && t.dataset.keep != null && size_class_view_in(t.closest(".ui-size-class-slot").dataset.when, size) {                     tops = set(tops, t.dataset.keep, t.scrollTop)                 }             }             let seen = js_new("ResizeObserver", look)             seen.observe(el)             el.addEventListener("scroll", scrolled, { capture: true, passive: true })             () => {                 seen.disconnect()                 el.removeEventListener("scroll", scrolled, { capture: true })             }         }}>        {children}    </div>}
Thread
15Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.