ColorSwatches
Pick one colour from a set of round swatches: real radio buttons, so they post with a form and work with the keyboard.
Demo
Theme: ocean.
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/color_swatches.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/color_swatches.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/color_swatches.qwe
component ColorSwatchesDemoLive() { state theme = "ocean" <div.demo-stack> <ColorSwatches name="theme" label="Theme" value={theme} size="lg" names={true} field="color-swatches-demo-live" options={[{ label: "Ocean", value: "ocean", color: "#0b6bcb" }, { label: "Forest", value: "forest", color: "#18794e" }, { label: "Plum", value: "plum", color: "#5b4bdb" }, { label: "Ember", value: "ember", color: "#c62a2f" }, { label: "Sand", value: "sand", color: "#d9b26f", disabled: true }]} on_change={(v) => { theme = v }}/> <p.demo-note>Theme: {theme}.</p> </div>}view ColorSwatchesDemo() = <div.demo-stack> <ColorSwatches name="color" label="Label colour" value="#18794e" field="color-swatches-demo-hex" options={["#5b4bdb", "#18794e", "#c62a2f", "#a35200", "#0b6bcb", "#17161f"]} hint="The arrow keys move between them."/> <ColorSwatchesDemoLive/> <div.demo-row> <ColorSwatches name="tiny" label="Small" size="sm" options={["#5b4bdb", "#18794e", "#c62a2f"]} required={true} error="Pick a colour."/> </div> </div>Source
ui/color_swatches.qwe
// ColorSwatches: pick one colour from a set, shown as round swatches. They are// real radio buttons in a <fieldset>, so a plain <form> sends the chosen value as// `name`, the arrow keys move between them, and each has a name for screen readers.//// <ColorSwatches name="color" label="Colour" value="#5b4bdb" options={["#5b4bdb", "#18794e", "#c62a2f"]}/>// <ColorSwatches name="theme" label="Theme" size="lg" on_change={(v) => { theme = v }}// options={[{ label: "Ocean", value: "ocean", color: "#0b6bcb" },// { label: "Forest", value: "forest", color: "#18794e", disabled: true }]}/>//// options: colours as strings (the colour is the value and its name), or maps of// { label, value, color?, disabled? } (color: what is shown; the value by default).// value: the chosen one. size: "sm", "md" (default) or "lg". names (false): the// label written under each swatch. hint, error, required, disabled and field (the// ids' start, default "color-swatches-" + name) as in RadioGroup.// on_change(value): inside one of your components, given the value chosen.import ui/theme.qwe use ui_clslet color_swatches_css = css`.ui-color-swatches { min-width: 0; margin: 0; padding: 0; border: 0; font: 14px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-color-swatches-legend { padding: 0; margin-bottom: 8px; font-size: 13px; font-weight: 500; }.ui-color-swatches-req { margin-left: 2px; color: var(--ui-danger); }.ui-color-swatches-list { display: flex; flex-wrap: wrap; gap: 10px; }.ui-color-swatches-opt { position: relative; display: grid; justify-items: center; gap: 4px; cursor: pointer; }.ui-color-swatches-opt input { position: absolute; opacity: 0; width: 1px; height: 1px; margin: 0; }.ui-color-swatches-chip { display: inline-grid; place-items: center; width: 28px; height: 28px; border-radius: 50%; background: var(--c); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ui-fg) 18%, transparent); transition: transform .12s, box-shadow .15s;}.ui-color-swatches-chip svg { width: 14px; height: 14px; opacity: 0; color: #fff; filter: drop-shadow(0 0 1px rgba(0, 0, 0, .7)); transition: opacity .12s; }.ui-color-swatches-opt:hover .ui-color-swatches-chip { transform: scale(1.08); }.ui-color-swatches-opt input:checked + .ui-color-swatches-chip { box-shadow: 0 0 0 2px var(--ui-bg), 0 0 0 4px var(--ui-accent); }.ui-color-swatches-opt input:checked + .ui-color-swatches-chip svg { opacity: 1; }.ui-color-swatches-opt input:focus-visible + .ui-color-swatches-chip { box-shadow: 0 0 0 2px var(--ui-bg), 0 0 0 4px var(--ui-accent), var(--ui-focus); }.ui-color-swatches-opt:has(input:disabled) { opacity: .4; cursor: not-allowed; }.ui-color-swatches-opt:has(input:disabled):hover .ui-color-swatches-chip { transform: none; }.ui-color-swatches-opt:has(input:disabled) .ui-color-swatches-chip::after { content: ""; width: 30px; height: 2px; background: var(--ui-fg); transform: rotate(-45deg); border-radius: 1px;}.ui-color-swatches-name { font-size: 12px; color: var(--ui-muted); max-width: 72px; text-align: center; }.ui-color-swatches-opt input:checked ~ .ui-color-swatches-name { color: var(--ui-fg); font-weight: 500; }.ui-color-swatches-sm .ui-color-swatches-chip { width: 20px; height: 20px; }.ui-color-swatches-sm .ui-color-swatches-chip svg { width: 10px; height: 10px; }.ui-color-swatches-lg .ui-color-swatches-chip { width: 38px; height: 38px; }.ui-color-swatches-lg .ui-color-swatches-chip svg { width: 18px; height: 18px; }.ui-color-swatches-invalid .ui-color-swatches-chip { box-shadow: 0 0 0 2px var(--ui-danger); }.ui-color-swatches-hint, .ui-color-swatches-error { margin: 8px 0 0; font-size: 13px; color: var(--ui-muted); }.ui-color-swatches-error { color: var(--ui-danger); }@media (prefers-reduced-motion: reduce) { .ui-color-swatches-chip, .ui-color-swatches-chip svg { transition: none; } }`// An option as { label, value, color, disabled }, from a string or a map.fn color_swatches_option(o) = type_of(o) == "map" ? { label: o.label ?? str(o.value), value: str(o.value ?? o.label), color: o.color ?? str(o.value ?? o.label), disabled: o.disabled ?? false } : { label: str(o), value: str(o), color: str(o), disabled: false }// Only what a CSS colour can be made of: a word, #hex, or rgb(...) / hsl(...) with digits.fn color_swatches_safe(c: str) -> str = re.test(c, "^[#a-zA-Z0-9(),.% -]{1,60}$") ? c : "transparent"view ColorSwatches(name: str, label: str? = null, value: str? = null, options: list = [], size: str = "md", names: bool = false, hint: str? = null, error: str? = null, required: bool = false, disabled: bool = false, field: str? = null, on_change = null, class: str? = null) { style global color_swatches_css let id = field ?? `color-swatches-${name}` let described = ui_cls([hint != null && `${id}-hint`, error != null && `${id}-error`]) <fieldset class={ui_cls(["ui-color-swatches", size != "md" && `ui-color-swatches-${size}`, error != null && "ui-color-swatches-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-color-swatches-legend> {label}{required && <span.ui-color-swatches-req aria-hidden="true">*</span>} </legend>} <div.ui-color-swatches-list> {for [i, o] in enumerate(options) => { let it = color_swatches_option(o) let oid = `${id}-${i}` <label.ui-color-swatches-opt for={oid} title={it.label}> <input type="radio" id={oid} name={name} value={it.value} checked={it.value == value} required={required} disabled={it.disabled} @change={(e) => { on_change && on_change(e.value) }}/> <span.ui-color-swatches-chip style={`--c: ${color_swatches_safe(it.color)}`} aria-hidden="true"> <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 8.5l3 3 6-7"/></svg> </span> {names ? <span.ui-color-swatches-name>{it.label}</span> : <span.ui-sr>{it.label}</span>} </label> }} </div> {hint != null && <p.ui-color-swatches-hint id={`${id}-hint`}>{hint}</p>} {error != null && <p.ui-color-swatches-error id={`${id}-error`} role="alert">{error}</p>} </fieldset>}Related
ImagePickerOne of several pictures chosen by its thumbnail: real radios under the pictures, so a form sends the choice and the arrow keys move it.6 views · 7 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 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 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 downloads
Thread
Thread 0 posts
On ColorSwatches: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.