MiniCart
A bag icon with the count that opens a small panel of the items, the subtotal and the way to the cart and checkout.
Demo
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/mini_cart.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/mini_cart.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/mini_cart.qwe
let mini_cart_demo_items = [ { id: "runner", name: "Trail runner", price: 89, quantity: 1, image: "https://picsum.photos/seed/runner/100/100", options: ["Size 42", "Black"] }, { id: "beanie", name: "Wool beanie", price: 24, quantity: 2, image: "https://picsum.photos/seed/beanie/100/100" }, { id: "socks", name: "Merino socks, 3 pairs", price: 29, quantity: 1 },]component MiniCartDemoLive() { state removed = [] <div.demo-row> <MiniCart id="mini-cart-demo-live" items={mini_cart_demo_items} cart_href="#cart_item" checkout_href="#checkout_steps" on_remove={(id) => { removed = removed | push(id) }}/> <p.demo-note>{len(removed) == 0 ? "Open the bag; × removes an item (on_remove)." : `Removed: ${join(removed, ", ")}.`}</p> </div>}view MiniCartDemo() = <div.demo-stack> <MiniCartDemoLive/> <div.demo-row> <MiniCart id="mini-cart-demo-plain" items={mini_cart_demo_items} currency="EUR" locale="de-DE" cart_href="#cart_item" checkout_href="#checkout_steps" align="start"/> <MiniCart id="mini-cart-demo-empty" items={[]} cart_href="#cart_item"/> <p.demo-note>Without on_remove the items are read-only; an empty bag says so.</p> </div> </div>Source
ui/mini_cart.qwe
// MiniCart: the cart in a navbar: a bag with how many are in it, which opens a// small panel of the items, the subtotal and the way to the cart and checkout.// The panel is the browser's `popover`, so Escape or a click elsewhere closes// it; it opens under the button where the browser can anchor it. Prices are// written as Price writes them (this file needs price.qwe).//// <MiniCart id="cart" items={cart} cart_href="/cart" checkout_href="/checkout"/>// <MiniCart id="cart" items={cart} on_remove={(id) => { cart = cart | filter((i) => i.id != id) }}/>//// id (required): unique on the page; the button opens the panel by it.// items: a list of { id, name, price, quantity, image?, options? } (options:// strings, "Size 42"). currency, locale, cents as for Price. label: the button's// name ("Cart"). cart_href, checkout_href: where the two buttons go ("/cart",// "/checkout"; null hides one). empty: what the panel says with nothing in it.// align: "end" (default: the panel's right edge under the button's) or "start".// on_remove(id): inside one of your components, given the id of the item whose// × was clicked; the panel drops the item itself. Without it, there is no ×.import ui/price.qwe use price_textimport ui/theme.qwe use ui_clslet mini_cart_css = css`.ui-mini-cart-btn { position: relative; display: inline-grid; place-items: center; width: 40px; height: 40px; padding: 0; border: 1px solid transparent; border-radius: var(--ui-radius-sm); background: transparent; color: var(--ui-fg); cursor: pointer; transition: background .15s; }.ui-mini-cart-btn:hover { background: var(--ui-subtle); }.ui-mini-cart-btn:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-mini-cart-btn svg { width: 22px; height: 22px; }.ui-mini-cart-count { position: absolute; top: 2px; right: 2px; min-width: 18px; height: 18px; padding: 0 5px; border-radius: 999px; background: var(--ui-accent); color: var(--ui-accent-fg); font: 700 11px/18px var(--ui-font); text-align: center; font-variant-numeric: tabular-nums; }.ui-mini-cart { width: min(340px, calc(100vw - 16px)); margin: auto; padding: 0; border: 1px solid var(--ui-border); border-radius: var(--ui-radius); background: var(--ui-surface); color: var(--ui-fg); box-shadow: var(--ui-shadow-lg); font: 14px/1.4 var(--ui-font); }.ui-mini-cart-head { display: flex; justify-content: space-between; align-items: center; padding: 12px 16px; border-bottom: 1px solid var(--ui-border); font-weight: 600; }.ui-mini-cart-head span { font-weight: 400; color: var(--ui-muted); font-size: 13px; }.ui-mini-cart-list { max-height: 300px; margin: 0; padding: 6px 0; overflow: auto; list-style: none; }.ui-mini-cart-item { display: grid; grid-template-columns: 44px 1fr auto; gap: 10px; align-items: center; padding: 8px 16px; }.ui-mini-cart-item img, .ui-mini-cart-ph { display: block; width: 44px; height: 44px; border-radius: var(--ui-radius-sm); background: var(--ui-subtle); object-fit: cover; }.ui-mini-cart-text { display: grid; gap: 1px; min-width: 0; }.ui-mini-cart-name { font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }.ui-mini-cart-meta { font-size: 12px; color: var(--ui-muted); }.ui-mini-cart-right { display: flex; align-items: center; gap: 6px; font-variant-numeric: tabular-nums; }.ui-mini-cart-x { width: 24px; height: 24px; padding: 0; border: 0; border-radius: 50%; background: transparent; color: var(--ui-muted); font-size: 16px; line-height: 1; cursor: pointer; }.ui-mini-cart-x:hover { background: var(--ui-danger-soft); color: var(--ui-danger); }.ui-mini-cart-x:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-mini-cart-empty { padding: 28px 16px; text-align: center; color: var(--ui-muted); }.ui-mini-cart-foot { display: grid; gap: 8px; padding: 12px 16px 16px; border-top: 1px solid var(--ui-border); }.ui-mini-cart-sub { display: flex; justify-content: space-between; font-weight: 600; }.ui-mini-cart-sub span:first-child { color: var(--ui-muted); font-weight: 400; }.ui-mini-cart-go { display: flex; align-items: center; justify-content: center; height: 38px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface); color: var(--ui-fg); font: 500 14px/1 var(--ui-font); text-decoration: none; }.ui-mini-cart-go:hover { border-color: var(--ui-strong); background: var(--ui-subtle); }.ui-mini-cart-go:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-mini-cart-go.ui-mini-cart-main { background: var(--ui-accent); color: var(--ui-accent-fg); border-color: transparent; }.ui-mini-cart-go.ui-mini-cart-main:hover { background: color-mix(in srgb, var(--ui-accent) 88%, var(--ui-fg)); }.ui-mini-cart[popover]:popover-open { animation: ui-mini-cart-in .12s ease-out; }@keyframes ui-mini-cart-in { from { opacity: 0; transform: translateY(-4px); } }@supports (position-area: bottom) { .ui-mini-cart { inset: auto; margin: 6px 0; position-area: bottom span-left; position-try-fallbacks: flip-block, flip-inline; } .ui-mini-cart-start { position-area: bottom span-right; }}@supports not (position-area: bottom) { .ui-mini-cart::backdrop { background: var(--ui-overlay); } }@media (prefers-reduced-motion: reduce) { .ui-mini-cart[popover]:popover-open { animation: none; } .ui-mini-cart-btn { transition: none; } }`component MiniCart(id: str, items: list = [], currency: str = "USD", locale: str? = null, cents: bool = true, label: str = "Cart", cart_href: str? = "/cart", checkout_href: str? = "/checkout", empty: str = "Your cart is empty", align: str = "end", on_remove = null, class: str? = null) { style global mini_cart_css state kept = items let count = sum(kept | map((i) => i.quantity ?? 1)) let subtotal = sum(kept | map((i) => dec(i.price) * (i.quantity ?? 1))) <span class={class}> <button.ui-mini-cart-btn type="button" popovertarget={id} aria-label={`${label}, ${count} ${count == 1 ? "item" : "items"}`} style={`anchor-name: --${id}`}> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"> <path d="M5 8h14l-1 12H6zM9 8V6a3 3 0 0 1 6 0v2"/> </svg> {count > 0 && <span.ui-mini-cart-count aria-hidden="true">{count > 99 ? "99+" : str(count)}</span>} </button> <div id={id} popover="auto" class={ui_cls(["ui-mini-cart", align == "start" && "ui-mini-cart-start"])} style={`position-anchor: --${id}`} role="dialog" aria-label={label}> <div.ui-mini-cart-head>{label} <span>{count} {count == 1 ? "item" : "items"}</span></div> {len(kept) == 0 ? <p.ui-mini-cart-empty>{empty}</p> : <ul.ui-mini-cart-list> {for it in kept => <li.ui-mini-cart-item key={it.id}> {get(it, "image") != null ? <img src={it.image} alt="" loading="lazy"/> : <span.ui-mini-cart-ph aria-hidden="true"></span>} <span.ui-mini-cart-text> <span.ui-mini-cart-name>{it.name}</span> <span.ui-mini-cart-meta>{it.quantity ?? 1} × {price_text(it.price, currency, locale, cents)}{len(get(it, "options") ?? []) > 0 && ` · ${join(it.options, ", ")}`}</span> </span> <span.ui-mini-cart-right> <span>{price_text(dec(it.price) * (it.quantity ?? 1), currency, locale, cents)}</span> {on_remove != null && <button.ui-mini-cart-x type="button" aria-label={`Remove ${it.name}`} @click={() => { kept = kept | filter((k) => k.id != it.id) on_remove(it.id) }}>×</button>} </span> </li>} </ul>} <div.ui-mini-cart-foot> {len(kept) > 0 && <div.ui-mini-cart-sub><span>Subtotal</span><span>{price_text(subtotal, currency, locale, cents)}</span></div>} {cart_href != null && <a.ui-mini-cart-go href={cart_href}>View cart</a>} {checkout_href != null && len(kept) > 0 && <a.ui-mini-cart-go.ui-mini-cart-main href={checkout_href}>Checkout</a>} </div> </div> </span>}Related
CartItemOne line of a cart: picture, name and options, a quantity control, the line's total and Remove, as inputs a form sends.6 views · 6 downloadsCartSummarySubtotal, shipping, tax, a discount and the total added up, with a Checkout button as a link, a form or a callback.6 views · 7 downloadsCheckoutStepsCart → Details → Payment → Done: the steps of a checkout, those done as links back, the current one marked.5 views · 7 downloadsFreeShippingMeterHow far a cart is from free shipping or the next reward, and the cheapest item or set that gets it there, with tiers per currency, exact when reached.3 views · 5 downloadsProductCardA product in a shop's grid: picture, name, price with the discount, stars, a badge and Add to cart, as a plain form or a callback.6 views · 6 downloadsQuantityInputA compact count with − and + either side, kept to its limits and step, with the arrow keys, PageUp/PageDown, Home and End.5 views · 7 downloads
Thread
Thread 0 posts
On MiniCart: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.