CheckboxCards
Any number of choices as cards with a title, a line of detail and a check, each a real checkbox a form sends.
Demo
Notifying by email.
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/checkbox_cards.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/checkbox_cards.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/checkbox_cards.qwe
component CheckboxCardsDemoLive() { state picked = ["email"] <div.demo-stack> <CheckboxCards name="cc-demo-notify" label="Notify me by" values={picked} columns={3} size="sm" options={[{ label: "Email", value: "email", icon: "✉️" }, { label: "Text message", value: "sms", icon: "💬" }, { label: "Push", value: "push", icon: "🔔" }]} on_change={(v, on) => { picked = on ? picked + [v] : picked | filter((x) => x != v) }}/> <p.demo-note>{len(picked) == 0 ? "No channel picked." : `Notifying by ${join(picked, ", ")}.`}</p> </div>}view CheckboxCardsDemo() = <div.demo-stack> <CheckboxCards name="cc-demo-addons" label="Add-ons" values={["backups"]} hint="Billed monthly with the plan." options={[{ label: "Backups", value: "backups", description: "Daily, kept for 30 days.", meta: "$5" }, { label: "Monitoring", value: "monitoring", description: "Alerts by email when a check fails.", meta: "$3" }, { label: "Priority support", value: "support", description: "Sold out this month.", meta: "$20", disabled: true }]}/> <CheckboxCardsDemoLive/> <CheckboxCards name="cc-demo-days" label="Open on" columns={1} required={true} error="Pick at least one day." options={["Saturday", "Sunday"]}/> </div>Source
ui/checkbox_cards.qwe
// CheckboxCards: several choices as cards, each with a title, a line of detail// and a check in the corner, for picking any number (add-ons, interests, the// channels to notify). Each card is a real checkbox, so a plain <form> sends// `name` once per card checked, and Space toggles the focused one.//// <CheckboxCards name="addons" label="Add-ons" values={["backups"]}// options={[{ label: "Backups", value: "backups", description: "Daily, kept 30 days", meta: "$5" },// { label: "Monitoring", value: "monitoring", description: "Alerts by email" },// { label: "Support", value: "support", description: "Sold out", disabled: true }]}/>//// options: a list of strings, or of { label, value, description?, meta?, icon?,// disabled? }: meta is short text at the right of the title (a price); icon, HTML// or an emoji before it. values: the ones checked to start with. columns: how many// cards across (as many as fit, 200px each, by default); 1 stacks them.// label (the legend), hint, error, required (at least one; the browser does not// check it, your route does), disabled and field (the ids' start, "checkbox-cards-"// + name by default) as for every field. size: "sm" or "md".// on_change(value, checked): inside one of your components, given the card's value// and whether it is now checked.import ui/theme.qwe use ui_clslet checkbox_cards_css = css`.ui-checkbox-cards { min-width: 0; margin: 0; padding: 0; border: 0; font: 14px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-checkbox-cards-legend { padding: 0; margin-bottom: 8px; font-size: 13px; font-weight: 500; }.ui-checkbox-cards-req { margin-left: 2px; color: var(--ui-danger); }.ui-checkbox-cards-list { display: grid; grid-template-columns: repeat(var(--ui-checkbox-cards-cols, auto-fill), minmax(min(200px, 100%), 1fr)); gap: 10px; }.ui-checkbox-cards-card { position: relative; display: flex; align-items: flex-start; gap: 12px; min-width: 0; padding: 12px 14px; cursor: pointer; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface); transition: border-color .15s, background .15s, box-shadow .15s;}.ui-checkbox-cards-card input { position: absolute; opacity: 0; width: 1px; height: 1px; margin: 0; }.ui-checkbox-cards-card:hover { border-color: var(--ui-strong); }.ui-checkbox-cards-card:has(input:checked) { border-color: var(--ui-accent); background: color-mix(in srgb, var(--ui-accent-soft) 60%, var(--ui-surface)); box-shadow: inset 0 0 0 1px var(--ui-accent);}.ui-checkbox-cards-card:has(input:focus-visible) { box-shadow: var(--ui-focus); }.ui-checkbox-cards-card:has(input:checked):has(input:focus-visible) { box-shadow: inset 0 0 0 1px var(--ui-accent), var(--ui-focus); }.ui-checkbox-cards-card:has(input:disabled) { opacity: .55; cursor: not-allowed; }.ui-checkbox-cards-card:has(input:disabled):hover { border-color: var(--ui-border); }.ui-checkbox-cards-box { flex: none; position: relative; width: 18px; height: 18px; margin-top: 1px; border-radius: 4px; border: 1px solid var(--ui-strong); background: var(--ui-surface); transition: background .15s, border-color .15s;}.ui-checkbox-cards-box::after { content: ""; position: absolute; left: 5px; top: 1.5px; width: 5px; height: 10px; border: solid var(--ui-accent-fg); border-width: 0 2px 2px 0; transform: rotate(45deg) scale(0); transition: transform .12s;}.ui-checkbox-cards-card input:checked ~ .ui-checkbox-cards-box { background: var(--ui-accent); border-color: var(--ui-accent); }.ui-checkbox-cards-card input:checked ~ .ui-checkbox-cards-box::after { transform: rotate(45deg) scale(1); }.ui-checkbox-cards-icon { flex: none; display: grid; place-items: center; width: 32px; height: 32px; border-radius: var(--ui-radius-sm); background: var(--ui-subtle); color: var(--ui-accent); font-size: 18px; }.ui-checkbox-cards-icon svg { width: 18px; height: 18px; }.ui-checkbox-cards-text { display: grid; flex: 1; gap: 2px; min-width: 0; }.ui-checkbox-cards-title { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; font-weight: 500; }.ui-checkbox-cards-meta { flex: none; color: var(--ui-muted); font-size: 13px; font-variant-numeric: tabular-nums; }.ui-checkbox-cards-desc { color: var(--ui-muted); font-size: 13px; }.ui-checkbox-cards-sm .ui-checkbox-cards-card { padding: 9px 11px; gap: 10px; }.ui-checkbox-cards-sm .ui-checkbox-cards-title { font-size: 13px; }.ui-checkbox-cards-sm .ui-checkbox-cards-desc, .ui-checkbox-cards-sm .ui-checkbox-cards-meta { font-size: 12px; }.ui-checkbox-cards-invalid .ui-checkbox-cards-card { border-color: var(--ui-danger); }.ui-checkbox-cards-hint, .ui-checkbox-cards-error { margin: 8px 0 0; font-size: 13px; color: var(--ui-muted); }.ui-checkbox-cards-error { color: var(--ui-danger); }@media (prefers-reduced-motion: reduce) { .ui-checkbox-cards-card, .ui-checkbox-cards-box, .ui-checkbox-cards-box::after { transition: none; } }`// An option as { label, value, description, meta, icon, disabled }, from a string or a map.fn checkbox_cards_option(o) = type_of(o) == "map" ? { label: o.label ?? str(o.value), value: str(o.value ?? o.label), description: o.description, meta: o.meta, icon: o.icon, disabled: o.disabled ?? false } : { label: str(o), value: str(o), description: null, meta: null, icon: null, disabled: false }view CheckboxCards(name: str, label: str? = null, options: list = [], values: list = [], columns: int? = null, hint: str? = null, error: str? = null, required: bool = false, disabled: bool = false, field: str? = null, size: str = "md", on_change = null, class: str? = null) { style global checkbox_cards_css let id = field ?? `checkbox-cards-${name}` let described = ui_cls([hint != null && `${id}-hint`, error != null && `${id}-error`]) let chosen = values | map((v) => str(v)) <fieldset class={ui_cls(["ui-checkbox-cards", size == "sm" && "ui-checkbox-cards-sm", error != null && "ui-checkbox-cards-invalid", class])} id={id} disabled={disabled} aria-required={required ? "true" : null} aria-invalid={error != null ? "true" : null} aria-describedby={described != "" ? described : null}> {label != null && <legend.ui-checkbox-cards-legend> {label}{required && <span.ui-checkbox-cards-req aria-hidden="true">*</span>} </legend>} <div.ui-checkbox-cards-list style={columns != null ? `--ui-checkbox-cards-cols: ${columns}` : null}> {for [i, o] in enumerate(options) => { let it = checkbox_cards_option(o) let oid = `${id}-${i}` <label.ui-checkbox-cards-card for={oid}> <input type="checkbox" id={oid} name={name} value={it.value} checked={contains(chosen, it.value)} disabled={it.disabled} aria-describedby={it.description != null ? `${oid}-desc` : null} @change={(e) => { on_change && on_change(it.value, e.checked) }}/> {it.icon != null && <span.ui-checkbox-cards-icon aria-hidden="true">{it.icon}</span>} <span.ui-checkbox-cards-text> <span.ui-checkbox-cards-title> <span>{it.label}</span> {it.meta != null && <span.ui-checkbox-cards-meta>{it.meta}</span>} </span> {it.description != null && <span.ui-checkbox-cards-desc id={`${oid}-desc`}>{it.description}</span>} </span> <span.ui-checkbox-cards-box aria-hidden="true"></span> </label> }} </div> {hint != null && <p.ui-checkbox-cards-hint id={`${id}-hint`}>{hint}</p>} {error != null && <p.ui-checkbox-cards-error id={`${id}-error`} role="alert">{error}</p>} </fieldset>}Related
RadioCardsOne choice of several as cards with a title, a line of detail and a dot, each a real radio a form sends.5 views · 7 downloadsComboboxA text box that filters a list of options as you type, or asks for them (a server, a map service), with the arrow keys, Enter and Escape.5 views · 6 downloadsDependentSelectTwo drop-downs where the second's options depend on the first, both real selects a form sends.5 views · 8 downloadsPhoneInputA country code select and a number box, sent together in international form (+351912345678) with the country's ISO code.6 views · 6 downloadsRadioGroupOne choice of several: real radio buttons in a fieldset, in a column, a row or as cards.5 views · 6 downloadsAddressFormStreet, city, region, postcode and country in the usual layout, with the autocomplete names browsers fill from.5 views · 7 downloads
Thread
Thread 0 posts
On CheckboxCards: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.