ShippingOptions
Shipping methods as radio cards, each with its price and how long it takes; a plain form sends the choice.
Demo
Sending by standard (on_change).
Get it
It needs theme.qwe, as every component does, and price.qwe; 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/price.qwe$ curl -O https://quillolang.org/components/ui/shipping_options.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/shipping_options.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/shipping_options.qwe
let shipping_options_demo_ways = [ { value: "standard", label: "Standard", time: "3–5 business days", price: 0 }, { value: "express", label: "Express", time: "1–2 business days", price: 12.9 }, { value: "overnight", label: "Overnight", time: "Next business day, by 10 am", price: 29, hint: "Order by 4 pm" }, { value: "pickup", label: "Collect in store", time: "Ready tomorrow", price: 0, hint: "Oxford Street", disabled: true },]component ShippingOptionsDemoLive() { state way = "standard" <div.demo-stack.demo-narrow> <ShippingOptions name="shipping-demo-live" value={way} options={shipping_options_demo_ways} on_change={(v) => { way = v }}/> <p.demo-note>Sending by {way} (on_change).</p> </div>}view ShippingOptionsDemo() = <div.demo-row style="align-items: flex-start; gap: 32px"> <ShippingOptionsDemoLive/> <div.demo-narrow> <ShippingOptions name="shipping-demo-eur" label="Delivery" currency="EUR" locale="de-DE" required={true} error="Pick a delivery option" options={[{ value: "post", label: "Post", time: "4–6 days", price: 4.95 }, { value: "courier", label: "Courier", time: "1–2 days", price: 14.5 }]}/> </div> </div>Source
ui/shipping_options.qwe
// ShippingOptions: the ways an order can be sent, as cards to pick one from,// each with its price and how long it takes. They are real radio buttons, so a// plain <form> sends the chosen `value` as `name`, and the arrow keys move// between them. Prices are written as Price writes them (this file needs price.qwe).//// <ShippingOptions value="standard" options={[// { value: "standard", label: "Standard", time: "3–5 business days", price: 0 },// { value: "express", label: "Express", time: "1–2 business days", price: 12.9 },// { value: "pickup", label: "Collect in store", time: "Ready tomorrow", price: 0, hint: "Oxford Street" }]}/>//// options: a list of { value, label, price?, time?, hint?, disabled? }: price a// number (0 reads "Free") or text; time how long it takes; hint a quieter line.// value: the one chosen. name: the radios' name ("shipping"). label: the// group's legend ("Shipping"). currency, locale, cents as for Price. required,// disabled, error as for every field. on_change(value): inside one of your// components, given the value chosen; pass value from your state, so the choice// stays as the component redraws.import ui/price.qwe use price_textimport ui/theme.qwe use ui_clslet shipping_options_css = css`.ui-shipping { min-width: 0; margin: 0; padding: 0; border: 0; font: 14px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-shipping-legend { padding: 0; margin-bottom: 8px; font-size: 13px; font-weight: 500; }.ui-shipping-list { display: grid; gap: 8px; }.ui-shipping-opt { position: relative; display: grid; grid-template-columns: auto 1fr auto; gap: 12px; align-items: center; padding: 12px 14px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface); cursor: pointer; transition: border-color .15s, background .15s, box-shadow .15s; }.ui-shipping-opt input { position: absolute; opacity: 0; width: 1px; height: 1px; margin: 0; }.ui-shipping-dot { position: relative; width: 18px; height: 18px; border-radius: 50%; border: 1px solid var(--ui-strong); background: var(--ui-surface); transition: border-color .15s; }.ui-shipping-dot::after { content: ""; position: absolute; inset: 4px; border-radius: 50%; background: var(--ui-accent); transform: scale(0); transition: transform .12s; }.ui-shipping-opt:hover { border-color: var(--ui-strong); }.ui-shipping-opt: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-shipping-opt input:checked + .ui-shipping-dot { border-color: var(--ui-accent); border-width: 1.5px; }.ui-shipping-opt input:checked + .ui-shipping-dot::after { transform: scale(1); }.ui-shipping-opt:has(input:focus-visible) { box-shadow: var(--ui-focus); }.ui-shipping-opt:has(input:disabled) { opacity: .55; cursor: not-allowed; }.ui-shipping-text { display: grid; gap: 1px; min-width: 0; }.ui-shipping-name { font-weight: 500; }.ui-shipping-time, .ui-shipping-hint { font-size: 13px; color: var(--ui-muted); }.ui-shipping-price { font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; }.ui-shipping-free { color: var(--ui-success); }.ui-shipping-invalid .ui-shipping-opt { border-color: var(--ui-danger); }.ui-shipping-error { margin: 8px 0 0; font-size: 13px; color: var(--ui-danger); }@media (prefers-reduced-motion: reduce) { .ui-shipping-opt, .ui-shipping-dot, .ui-shipping-dot::after { transition: none; } }`view ShippingOptions(options: list = [], value: str? = null, name: str = "shipping", label: str? = "Shipping", currency: str = "USD", locale: str? = null, cents: bool = true, required: bool = false, disabled: bool = false, error: str? = null, on_change = null, class: str? = null) { style global shipping_options_css let id = `shipping-options-${name}` <fieldset class={ui_cls(["ui-shipping", error != null && "ui-shipping-invalid", class])} id={id} disabled={disabled} aria-describedby={error != null ? `${id}-error` : null}> {label != null && <legend.ui-shipping-legend>{label}</legend>} <div.ui-shipping-list> {for [i, o] in enumerate(options) => { let free = get(o, "price") != null && type_of(o.price) != "str" && dec(o.price) == 0 let oid = `${id}-${i}` <label.ui-shipping-opt for={oid} key={o.value}> <input type="radio" id={oid} name={name} value={o.value} checked={o.value == value} required={required} disabled={get(o, "disabled") == true} @change={(e) => { on_change && on_change(e.value) }}/> <span.ui-shipping-dot aria-hidden="true"></span> <span.ui-shipping-text> <span.ui-shipping-name>{o.label}</span> {get(o, "time") != null && <span.ui-shipping-time>{o.time}</span>} {get(o, "hint") != null && <span.ui-shipping-hint>{o.hint}</span>} </span> {get(o, "price") != null && <span class={ui_cls(["ui-shipping-price", free && "ui-shipping-free"])}> {free ? "Free" : price_text(o.price, currency, locale, cents)} </span>} </label> }} </div> {error != null && <p.ui-shipping-error id={`${id}-error`} role="alert">{error}</p>} </fieldset>}Related
PaymentMethodsWays to pay as radio cards with their marks: card (with the brands taken), PayPal, Apple Pay, Google Pay, bank, cash.6 views · 6 downloadsAddressCardA saved address with a Default badge and Edit and Remove, as submit buttons in a plain form or callbacks.6 views · 7 downloadsCouponInputA promo code box with Apply: on_apply(code) says whether it took, and an applied code shows with Remove.6 views · 5 downloadsPlanToggleMonthly or yearly billing as a pill of two radios, with the saving shown on the yearly side.6 views · 5 downloadsVariantPickerSizes as buttons and colours as swatches, sold-out ones crossed out: radio groups for the keyboard, hidden inputs for forms.6 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 ShippingOptions: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.