ChecklistInput
Checkboxes under a progress line that says how many are done and fills as they are ticked; each posts with a form.
Demo
Nothing ticked.
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/checklist_input.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/checklist_input.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/checklist_input.qwe
component ChecklistInputDemoLive() { state done = [] <div.demo-stack> <ChecklistInput name="launch" label="Launch checklist" field="checklist-input-demo-live" items={["Write the announcement", "Tag the release", "Deploy to production"]} on_change={(vs) => { done = vs }}/> <p.demo-note>{len(done) == 0 ? "Nothing ticked." : `Ticked: ${join(done, ", ")}`}</p> </div>}view ChecklistInputDemo() = <div.demo-stack.demo-narrow> <ChecklistInput name="setup" label="Getting started" field="checklist-input-demo-setup" hint="Tick each step as you go." values={["project"]} items={[{ label: "Create a project", value: "project" }, { label: "Invite the team", value: "invite", checked: true, hint: "They get an email." }, { label: "Connect a domain", value: "domain" }, { label: "Add a payment method", value: "billing" }, { label: "Deploy", value: "deploy", disabled: true }]}/> <ChecklistInputDemoLive/> </div>Source
ui/checklist_input.qwe
// ChecklistInput: a list of checkboxes with a progress line over them that says// how many are done ("3 of 5 done") and fills as they are ticked. They are real// checkboxes, so a plain <form> sends each ticked value as `name`.//// <ChecklistInput name="setup" label="Getting started"// items={["Create a project", { label: "Invite the team", value: "invite", checked: true }, "Deploy"]}/>// <ChecklistInput name="todo" items={tasks} on_change={(done) => { done_ids = done }}/>//// items: strings (the text is the value), or maps of { label, value, checked?,// hint?, disabled? }. values: the values ticked to start with (as well as each// item's own checked). label: a heading for the list. done_text: what the line// says when everything is ticked ("All done!"). strike (true): ticked items are// struck through. hint and error (a line under the list) and field (the ids'// start, default "checklist-input-" + name) as in CheckboxGroup.// on_change(values): inside one of your components, called with the values// ticked, in the items' order, after each change.import ui/theme.qwe use ui_clslet checklist_input_css = css`.ui-checklist-input { display: grid; gap: 10px; min-width: 0; margin: 0; padding: 0; border: 0; font: 14px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-checklist-input-legend { padding: 0; font-size: 13px; font-weight: 500; }.ui-checklist-input-progress { display: grid; gap: 6px; }.ui-checklist-input-count { display: flex; justify-content: space-between; gap: 12px; font-size: 13px; color: var(--ui-muted); font-variant-numeric: tabular-nums; }.ui-checklist-input-all { color: var(--ui-success); font-weight: 500; }.ui-checklist-input-bar { height: 6px; border-radius: 3px; background: var(--ui-subtle); overflow: hidden; }.ui-checklist-input-bar span { display: block; height: 100%; width: var(--p); border-radius: 3px; background: var(--ui-accent); transition: width .25s, background .25s; }.ui-checklist-input-done .ui-checklist-input-bar span { background: var(--ui-success); }.ui-checklist-input-list { display: grid; gap: 8px; margin: 0; padding: 0; list-style: none; }.ui-checklist-input-item { display: flex; align-items: flex-start; gap: 10px; cursor: pointer; width: fit-content; }.ui-checklist-input-item input { position: absolute; opacity: 0; width: 1px; height: 1px; margin: 0; }.ui-checklist-input-box { flex: none; position: relative; width: 18px; height: 18px; margin-top: 1px; background: var(--ui-surface); border: 1px solid var(--ui-strong); border-radius: 4px; transition: background .15s, border-color .15s, box-shadow .15s;}.ui-checklist-input-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-checklist-input-item:hover .ui-checklist-input-box { border-color: var(--ui-accent); }.ui-checklist-input-item input:checked + .ui-checklist-input-box { background: var(--ui-accent); border-color: var(--ui-accent); }.ui-checklist-input-item input:checked + .ui-checklist-input-box::after { transform: rotate(45deg) scale(1); }.ui-checklist-input-item input:focus-visible + .ui-checklist-input-box { box-shadow: var(--ui-focus); }.ui-checklist-input-text { display: grid; gap: 2px; }.ui-checklist-input-strike input:checked ~ .ui-checklist-input-text > :first-child { text-decoration: line-through; color: var(--ui-muted); }.ui-checklist-input-hint-item { font-size: 13px; color: var(--ui-muted); }.ui-checklist-input-item:has(input:disabled) { opacity: .55; cursor: not-allowed; }.ui-checklist-input-hint, .ui-checklist-input-error { margin: 0; font-size: 13px; color: var(--ui-muted); }.ui-checklist-input-error { color: var(--ui-danger); }@media (prefers-reduced-motion: reduce) { .ui-checklist-input-bar span, .ui-checklist-input-box, .ui-checklist-input-box::after { transition: none; } }`// An item as { label, value, checked, hint, disabled }, from a string or a map.fn checklist_input_item(it) = type_of(it) == "map" ? { label: it.label ?? str(it.value), value: str(it.value ?? it.label), checked: it.checked ?? false, hint: it.hint, disabled: it.disabled ?? false } : { label: str(it), value: str(it), checked: false, hint: null, disabled: false }component ChecklistInput(name: str, items: list = [], values: list = [], label: str? = null, done_text: str = "All done!", strike: bool = true, hint: str? = null, error: str? = null, field: str? = null, on_change = null, class: str? = null) { style global checklist_input_css let list = items | map(checklist_input_item) state ticked = list | filter((it) => it.checked || contains(values | map((v) => str(v)), it.value)) | map((it) => it.value) let id = field ?? `checklist-input-${name}` let total = len(list) let done = len(list | filter((it) => contains(ticked, it.value))) let pct = total == 0 ? 0 : round(done * 100 / total) let described = ui_cls([`${id}-count`, hint != null && `${id}-hint`, error != null && `${id}-error`]) <fieldset class={ui_cls(["ui-checklist-input", done == total && total > 0 && "ui-checklist-input-done", strike && "ui-checklist-input-strike", class])} id={id} aria-describedby={described} aria-invalid={error != null ? "true" : null}> {label != null && <legend.ui-checklist-input-legend>{label}</legend>} <div.ui-checklist-input-progress> <div.ui-checklist-input-count id={`${id}-count`} aria-live="polite"> <span>{done} of {total} done</span> {done == total && total > 0 ? <span.ui-checklist-input-all>{done_text}</span> : <span>{pct}%</span>} </div> <div.ui-checklist-input-bar role="progressbar" aria-label={label ?? "Progress"} aria-valuemin="0" aria-valuemax={total} aria-valuenow={done} aria-valuetext={`${done} of ${total} done`}> <span style={`--p: ${pct}%`}></span> </div> </div> <ul.ui-checklist-input-list> {for [i, it] in enumerate(list) => <li key={it.value}> <label.ui-checklist-input-item for={`${id}-${i}`}> <input type="checkbox" id={`${id}-${i}`} name={name} value={it.value} checked={contains(ticked, it.value)} disabled={it.disabled} aria-describedby={it.hint != null ? `${id}-${i}-hint` : null} @change={(e) => { ticked = e.checked ? list | filter((x) => contains(ticked, x.value) || x.value == it.value) | map((x) => x.value) : ticked | filter((v) => v != it.value) on_change && on_change(ticked) }}/> <span.ui-checklist-input-box aria-hidden="true"></span> <span.ui-checklist-input-text> <span>{it.label}</span> {it.hint != null && <span.ui-checklist-input-hint-item id={`${id}-${i}-hint`}>{it.hint}</span>} </span> </label> </li>} </ul> {hint != null && <p.ui-checklist-input-hint id={`${id}-hint`}>{hint}</p>} {error != null && <p.ui-checklist-input-error id={`${id}-error`} role="alert">{error}</p>} </fieldset>}Related
AddressFormStreet, 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 downloadsCardInputA payment card as it is typed: the number grouped, the brand, MM/YY and the code, with typos said.6 views · 6 downloadsCharacterCounterA text box or textarea with a live 120 / 280 count that warns near the limit, as a hard stop or a soft one that blocks sending.5 views · 6 downloadsCheckboxA checkbox drawn by CSS on a real input, with an indeterminate state, and a group of them in a fieldset.5 views · 6 downloadsCheckboxCardsAny number of choices as cards with a title, a line of detail and a check, each a real checkbox a form sends.5 views · 6 downloads
Thread
Thread 0 posts
On ChecklistInput: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.