ResizableBox
A box the user resizes by dragging its corner or an edge, within limits, with the keyboard too.
Demo
Drag the corner, or focus it and use the arrow keys (Shift for bigger steps), Home and End.
320 × 200 pixels.
Only the width changes (axis="x"): a column of text to try line lengths with. The handle is along the right edge.
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/resizable_box.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/resizable_box.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/resizable_box.qwe
component ResizableBoxDemoLive() { state w = 320 state h = 200 <div.demo-stack> <ResizableBox id="resizable-box-demo-live" width={320} height={200} min_width={160} max_width={600} min_height={100} max_height={400} label="Resize the preview" on_resize={(nw, nh) => { w = nw; h = nh }}> <div style="display: grid; place-items: center; height: 100%; padding: 16px; text-align: center; color: var(--ui-muted); font-size: 13px"> Drag the corner, or focus it and use the arrow keys (Shift for bigger steps), Home and End. </div> </ResizableBox> <p.demo-note>{w} × {h} pixels.</p> </div>}view ResizableBoxDemo() = <div.demo-stack> <ResizableBoxDemoLive/> <ResizableBox id="resizable-box-demo-wide" axis="x" width={420} min_width={200} max_width={800} label="Resize the column"> <p style="margin: 0; padding: 16px; font-size: 13px">Only the width changes (axis="x"): a column of text to try line lengths with. The handle is along the right edge.</p> </ResizableBox> </div>Source
ui/resizable_box.qwe
// ResizableBox: a box whose size the user sets by dragging its corner (or an// edge), within limits: a preview pane, a textarea-like editor, a demo of how// something behaves at different widths. The handle takes focus: the arrow keys// change the size by `step` pixels (Shift: by ten times that), Home and End go to// the smallest and the largest, and screen readers hear the size.//// <ResizableBox width={360} height={220} min_width={200} max_width={720}> <Preview/> </ResizableBox>// <ResizableBox axis="x" width={300} on_resize={(w, h) => { px = w }}> ... </ResizableBox>//// width, height: the starting size, in pixels (320 by 200). min_width, max_width,// min_height, max_height: the limits (120 to 960, 80 to 640; max_width is also// capped at the container's width while dragging).// axis: "both" (the corner handle, the default), "x" (a right-edge handle, the width// only) or "y" (a bottom-edge handle, the height only). step: pixels per arrow key (10).// label: the handle's name for screen readers ("Resize"). show_size: the size shows// in the corner while it is dragged (true).// on_resize(width, height): inside one of your components, called after each drag or key.import ui/theme.qwe use ui_clslet resizable_box_css = css`.ui-resizable { position: relative; box-sizing: border-box; width: var(--ui-resizable-w, 320px); height: var(--ui-resizable-h, 200px); max-width: 100%; min-width: 0; overflow: hidden; border: 1px solid var(--ui-border); border-radius: var(--ui-radius); background: var(--ui-surface); color: var(--ui-fg); font: 14px/1.5 var(--ui-font);}.ui-resizable-x { height: auto; }.ui-resizable-y { width: auto; }.ui-resizable-body { height: 100%; min-height: 0; overflow: auto; }.ui-resizable-handle { position: absolute; z-index: 2; display: grid; place-items: center; margin: 0; padding: 0; border: 0; background: transparent; color: var(--ui-muted); cursor: nwse-resize; touch-action: none; user-select: none;}.ui-resizable-handle svg { width: 14px; height: 14px; }.ui-resizable-corner { right: 0; bottom: 0; width: 22px; height: 22px; }.ui-resizable-right { top: 0; bottom: 0; right: 0; width: 12px; cursor: ew-resize; }.ui-resizable-bottom { left: 0; right: 0; bottom: 0; height: 12px; cursor: ns-resize; }.ui-resizable-right::before, .ui-resizable-bottom::before { content: ""; border-radius: 999px; background: var(--ui-strong); }.ui-resizable-right::before { width: 3px; height: 28px; }.ui-resizable-bottom::before { width: 28px; height: 3px; }.ui-resizable-handle:hover, .ui-resizable-dragging .ui-resizable-handle { color: var(--ui-accent); }.ui-resizable-handle:hover::before, .ui-resizable-dragging .ui-resizable-handle::before { background: var(--ui-accent); }.ui-resizable-handle:focus-visible { outline: none; color: var(--ui-accent); }.ui-resizable-handle:focus-visible::after, .ui-resizable-dragging::after { content: ""; position: absolute; inset: 0; pointer-events: none; border-radius: inherit; box-shadow: var(--ui-focus);}.ui-resizable:has(.ui-resizable-handle:focus-visible) { box-shadow: var(--ui-focus); }.ui-resizable-dragging { box-shadow: var(--ui-focus); }.ui-resizable-dragging .ui-resizable-body { pointer-events: none; }.ui-resizable-size { position: absolute; right: 8px; bottom: 8px; z-index: 1; padding: 2px 7px; border-radius: var(--ui-radius-sm); background: var(--ui-fg); color: var(--ui-bg); font: 500 11px/1.4 var(--ui-mono); pointer-events: none; opacity: 0; transition: opacity .15s;}.ui-resizable-dragging .ui-resizable-size { opacity: .9; }.ui-resizable-corner + .ui-resizable-size { right: 24px; }@media (prefers-reduced-motion: reduce) { .ui-resizable-size { transition: none; } }`fn resizable_box_clamp(v: num, lo: num, hi: num) -> num = v < lo ? lo : v > hi ? hi : vcomponent ResizableBox(width: int = 320, height: int = 200, min_width: int = 120, max_width: int = 960, min_height: int = 80, max_height: int = 640, axis: str = "both", step: int = 10, label: str = "Resize", show_size: bool = true, on_resize = null, id: str? = null, class: str? = null, children) { style global resizable_box_css state w = resizable_box_clamp(width, min_width, max_width) state h = resizable_box_clamp(height, min_height, max_height) state dragging = false state from = null let across = axis != "y" let down = axis != "x" let tell = () => { on_resize && on_resize(round(w), round(h)) } let to = (nw: num, nh: num) => { if across { w = resizable_box_clamp(nw, min_width, max_width) } if down { h = resizable_box_clamp(nh, min_height, max_height) } } let said = across && down ? `${round(w)} by ${round(h)} pixels` : across ? `${round(w)} pixels wide` : `${round(h)} pixels tall` <div class={ui_cls(["ui-resizable", axis == "x" && "ui-resizable-x", axis == "y" && "ui-resizable-y", dragging && "ui-resizable-dragging", class])} id={id} style={`--ui-resizable-w: ${w}px; --ui-resizable-h: ${h}px`}> <div.ui-resizable-body>{children}</div> <button type="button" class={ui_cls(["ui-resizable-handle", axis == "both" ? "ui-resizable-corner" : axis == "x" ? "ui-resizable-right" : "ui-resizable-bottom"])} aria-label={label} aria-roledescription="resize handle" aria-describedby={id != null ? `${id}-size` : null} title={label} @pointerdown={(e) => { if e.button == 0 { e.preventDefault() e.target.setPointerCapture(e.pointerId) let box = e.target.closest(".ui-resizable") // (The box's own parent may be a component's island, which has no box: take the first that does.) let holder = box.parentElement.closest(":not(quillo-island):not(quillo-raw)") let room = holder.getBoundingClientRect().right - box.getBoundingClientRect().left from = { x: e.clientX, y: e.clientY, w, h, room } dragging = true } }} @pointermove={(e) => { if dragging { let nw = from.w + e.clientX - from.x to(nw > from.room ? from.room : nw, from.h + e.clientY - from.y) } }} @pointerup={() => { if dragging { dragging = false tell() } }} @pointercancel={() => { dragging = false }} @keydown={(e) => { let by = e.shiftKey ? step * 10 : step let moved = match e.key { "ArrowRight" => across ? { nw: w + by, nh: h } : null "ArrowLeft" => across ? { nw: w - by, nh: h } : null "ArrowDown" => down ? { nw: w, nh: h + by } : null "ArrowUp" => down ? { nw: w, nh: h - by } : null "Home" => { nw: min_width, nh: min_height } "End" => { nw: max_width, nh: max_height } _ => null } if moved != null { e.preventDefault() to(moved.nw, moved.nh) tell() } }}> {axis == "both" && <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" aria-hidden="true"><path d="M14 2 2 14M14 8l-6 6M14 14h0"/></svg>} </button> {show_size && <span.ui-resizable-size aria-hidden="true">{round(w)} × {round(h)}</span>} <span.ui-sr id={id != null ? `${id}-size` : null} aria-live="polite">{said}</span> </div>}Related
CenterPuts its content in the middle: a column capped in width, a thing centred by its own width, or centred text.7 views · 7 downloadsInsetRoom around what it holds, in the kit's sizes or pixels, on every side or only across or down.4 views · 7 downloadsMasterDetailA list on the left and the item picked in detail on the right; one at a time with a Back button when narrow.6 views · 6 downloadsScrollAreaA box with its own scrollbars and a height limit, fading at the edges where there is more to see.6 views · 7 downloadsSpacerEmpty room between things, down a column or along a row, in the kit's sizes or pixels, or growing to push things apart.5 views · 6 downloadsSplitterTwo panes with a divider to drag between them, across or stacked, with the keyboard too.6 views · 6 downloads
Thread
Thread 0 posts
On ResizableBox: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.