ImagePicker
One of several pictures chosen by its thumbnail: real radios under the pictures, so a form sends the choice and the arrow keys move it.
Demo
The dawn theme is on.
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/image_picker.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/image_picker.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/image_picker.qwe
// A picture as a data URL: a coloured square with a letter on it.fn image_picker_demo_img(color: str, letter: str) -> str = `data:image/svg+xml,${url_encode(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect width="100" height="100" fill="${color}"/><text x="50" y="64" font-family="sans-serif" font-size="44" font-weight="700" fill="#fff" text-anchor="middle">${letter}</text></svg>`)}`let image_picker_demo_avatars = [ { value: "fox", src: image_picker_demo_img("#e5484d", "F"), label: "Fox" }, { value: "owl", src: image_picker_demo_img("#5b4bdb", "O"), label: "Owl" }, { value: "bear", src: image_picker_demo_img("#18794e", "B"), label: "Bear" }, { value: "cat", src: image_picker_demo_img("#a35200", "C"), label: "Cat", disabled: true },]component ImagePickerDemoLive() { state theme = "dawn" <div.demo-stack> <ImagePicker name="ip-demo-theme" label="Theme" value={theme} shape="wide" size={140} options={[{ value: "dawn", src: image_picker_demo_img("#f76b15", "Dawn"), label: "Dawn" }, { value: "sea", src: image_picker_demo_img("#0b6bcb", "Sea"), label: "Sea" }, { value: "night", src: image_picker_demo_img("#1c2024", "Night"), label: "Night" }]} on_change={(v) => { theme = v }}/> <p.demo-note>The {theme} theme is on.</p> </div>}view ImagePickerDemo() = <div.demo-stack> <ImagePicker name="ip-demo-avatar" label="Avatar" value="owl" shape="circle" options={image_picker_demo_avatars} hint="The arrow keys move the choice."/> <ImagePickerDemoLive/> <ImagePicker name="ip-demo-cover" label="Cover" columns={3} captions={false} required={true} error="Choose a cover." options={[{ value: "a", src: image_picker_demo_img("#8e4ec6", "1"), label: "Purple" }, { value: "b", src: image_picker_demo_img("#12a594", "2"), label: "Teal" }, { value: "c", src: image_picker_demo_img("#d6409f", "3"), label: "Pink" }]}/> </div>Source
ui/image_picker.qwe
// ImagePicker: one of several pictures (an avatar, a theme, a cover, a layout),// chosen by clicking its thumbnail. Each is a real radio under the picture, so a// plain <form> sends the chosen `value` as `name`, the arrow keys move the choice,// and screen readers hear each picture's label.//// <ImagePicker name="avatar" label="Avatar" value="fox"// options={[{ value: "fox", src: "/img/fox.png", label: "Fox" }, { value: "owl", src: "/img/owl.png", label: "Owl" }]}/>// <ImagePicker name="theme" label="Theme" shape="wide" size={160} options={themes} on_change={(v) => { theme = v }}/>//// options: a list of { value, src, label, disabled? }: src the picture, label what// it is called (shown under it unless captions={false}; always read to screen readers).// value: the one chosen to start with. size: each picture's width in pixels (96).// shape: "square" (default), "circle" or "wide" (16:9). captions: the labels// under the pictures (true). columns: a fixed number across (as many as fit by default).// label (the legend), hint, error, required, disabled and field (the ids' start,// "image-picker-" + name by default) as for every field.// on_change(value): inside one of your components, given the value chosen.import ui/theme.qwe use ui_clslet image_picker_css = css`.ui-image-picker { min-width: 0; margin: 0; padding: 0; border: 0; font: 14px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-image-picker-legend { padding: 0; margin-bottom: 8px; font-size: 13px; font-weight: 500; }.ui-image-picker-req { margin-left: 2px; color: var(--ui-danger); }.ui-image-picker-list { display: flex; flex-wrap: wrap; gap: 12px; }.ui-image-picker-cols { display: grid; grid-template-columns: repeat(var(--ui-image-picker-cols), minmax(0, 1fr)); }.ui-image-picker-opt { position: relative; display: grid; gap: 6px; width: var(--ui-image-picker-size, 96px); max-width: 100%; cursor: pointer; text-align: center; }.ui-image-picker-cols .ui-image-picker-opt { width: auto; }.ui-image-picker-opt input { position: absolute; opacity: 0; width: 1px; height: 1px; margin: 0; }.ui-image-picker-frame { position: relative; display: block; width: 100%; aspect-ratio: 1; overflow: hidden; border-radius: var(--ui-radius-sm); background: var(--ui-subtle); box-shadow: 0 0 0 1px var(--ui-border); transition: box-shadow .15s, transform .15s;}.ui-image-picker-frame img { display: block; width: 100%; height: 100%; object-fit: cover; }.ui-image-picker-circle .ui-image-picker-frame { border-radius: 50%; }.ui-image-picker-wide .ui-image-picker-frame { aspect-ratio: 16 / 9; }.ui-image-picker-opt:hover .ui-image-picker-frame { box-shadow: 0 0 0 1px var(--ui-strong); }.ui-image-picker-opt input:checked ~ .ui-image-picker-frame { box-shadow: 0 0 0 2px var(--ui-accent), 0 0 0 4px var(--ui-accent-soft); }.ui-image-picker-opt input:focus-visible ~ .ui-image-picker-frame { box-shadow: 0 0 0 1px var(--ui-border), var(--ui-focus); }.ui-image-picker-opt input:checked:focus-visible ~ .ui-image-picker-frame { box-shadow: 0 0 0 2px var(--ui-accent), var(--ui-focus); }.ui-image-picker-check { position: absolute; right: 6px; top: 6px; display: grid; place-items: center; width: 22px; height: 22px; border-radius: 50%; background: var(--ui-accent); color: var(--ui-accent-fg); box-shadow: 0 1px 3px rgba(0, 0, 0, .3); opacity: 0; transform: scale(.6); transition: opacity .12s, transform .12s;}.ui-image-picker-check svg { width: 12px; height: 12px; }.ui-image-picker-circle .ui-image-picker-check { right: 2px; top: 2px; }.ui-image-picker-opt input:checked ~ .ui-image-picker-frame .ui-image-picker-check { opacity: 1; transform: scale(1); }.ui-image-picker-caption { font-size: 13px; color: var(--ui-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }.ui-image-picker-opt input:checked ~ .ui-image-picker-caption { color: var(--ui-fg); font-weight: 500; }.ui-image-picker-opt:has(input:disabled) { opacity: .5; cursor: not-allowed; }.ui-image-picker-opt:has(input:disabled):hover .ui-image-picker-frame { box-shadow: 0 0 0 1px var(--ui-border); }.ui-image-picker-invalid .ui-image-picker-frame { box-shadow: 0 0 0 1px var(--ui-danger); }.ui-image-picker-hint, .ui-image-picker-error { margin: 8px 0 0; font-size: 13px; color: var(--ui-muted); }.ui-image-picker-error { color: var(--ui-danger); }@media (prefers-reduced-motion: reduce) { .ui-image-picker-frame, .ui-image-picker-check { transition: none; } }`view ImagePicker(name: str, label: str? = null, options: list = [], value: str | num | null = null, size: int = 96, shape: str = "square", captions: bool = true, columns: int? = null, hint: str? = null, error: str? = null, required: bool = false, disabled: bool = false, field: str? = null, on_change = null, class: str? = null) { style global image_picker_css let id = field ?? `image-picker-${name}` let described = ui_cls([hint != null && `${id}-hint`, error != null && `${id}-error`]) let chosen = value != null ? str(value) : null <fieldset class={ui_cls(["ui-image-picker", shape != "square" && `ui-image-picker-${shape}`, error != null && "ui-image-picker-invalid", class])} id={id} disabled={disabled} role="radiogroup" aria-required={required ? "true" : null} aria-invalid={error != null ? "true" : null} aria-describedby={described != "" ? described : null}> {label != null && <legend.ui-image-picker-legend> {label}{required && <span.ui-image-picker-req aria-hidden="true">*</span>} </legend>} <div class={ui_cls(["ui-image-picker-list", columns != null && "ui-image-picker-cols"])} style={`--ui-image-picker-size: ${size}px${columns != null ? `; --ui-image-picker-cols: ${columns}` : ""}`}> {for [i, o] in enumerate(options) => { let v = str(o.value ?? o.label) let oid = `${id}-${i}` <label.ui-image-picker-opt for={oid} title={o.label}> <input type="radio" id={oid} name={name} value={v} checked={v == chosen} required={required} disabled={o.disabled ?? false} aria-label={o.label} @change={(e) => { on_change && on_change(e.value) }}/> <span.ui-image-picker-frame> <img src={o.src} alt="" loading="lazy"/> <span.ui-image-picker-check aria-hidden="true"> <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 8.5l3 3 6-7"/></svg> </span> </span> {captions && <span.ui-image-picker-caption aria-hidden="true">{o.label}</span>} </label> }} </div> {hint != null && <p.ui-image-picker-hint id={`${id}-hint`}>{hint}</p>} {error != null && <p.ui-image-picker-error id={`${id}-error`} role="alert">{error}</p>} </fieldset>}Related
ColorSwatchesPick one colour from a set of round swatches: real radio buttons, so they post with a form and work with the keyboard.5 views · 5 downloadsAddressFormStreet, city, region, postcode and country in the usual layout, with the autocomplete names browsers fill from.5 views · 7 downloadsAutocompleteA free-text box that offers matches as one types, with the arrow keys and Enter; whatever is typed may stay.5 views · 7 downloadsAvatarUploadA round picture with Change and Remove, showing the picture picked before it is sent, on a real file input.6 views · 6 downloadsBirthdayInputDay, month and year drop-downs that give one date in a hidden input, the days following the month and the years the age allowed.5 views · 7 downloadsCardInputA payment card as it is typed: the number grouped, the brand, MM/YY and the code, with typos said.6 views · 6 downloads
Thread
Thread 0 posts
On ImagePicker: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.