QuickView
A Quick view button on a card that opens a preview of the item, its picture, price, text and actions, in a dialog.
Demo
The cart is empty.
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/quick_view.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/quick_view.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/quick_view.qwe
let quick_view_demo_items = [ { name: "Linen shirt", price: "€49", text: "Loose fit, washed linen, mother-of-pearl buttons.", hue: "accent" }, { name: "Wool scarf", price: "€35", text: "Merino, knitted in Porto, long enough to wrap twice.", hue: "success" }, { name: "Canvas tote", price: "€22", text: "Heavy cotton canvas with a leather strap.", hue: "warning" },]// A placeholder picture: a tinted square with the item's initial, as an SVG data URL.fn quick_view_demo_img(name: str) = `data:image/svg+xml,${url_encode(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400"><rect width="400" height="400" fill="#e8e6f7"/><text x="200" y="240" font-size="140" text-anchor="middle" fill="#5b4bdb" font-family="sans-serif">${slice(name, 0, 1)}</text></svg>`)}`component QuickViewDemoLive() { state cart = [] <div.demo-stack> <div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 14px"> {for it in quick_view_demo_items => <QuickView key={it.name} title={it.name} image={quick_view_demo_img(it.name)} alt={`${it.name}, a placeholder picture`} meta={it.price} text={it.text} href="#quick_view" action="Add to cart" on_action={() => { cart = cart | push(it.name) }}> <div style="overflow: hidden; border: 1px solid var(--ui-border); border-radius: var(--ui-radius); background: var(--ui-surface)"> <img src={quick_view_demo_img(it.name)} alt="" style="display: block; width: 100%; aspect-ratio: 1"/> <div style="padding: 10px 12px"><strong>{it.name}</strong><div style="color: var(--ui-muted)">{it.price}</div></div> </div> </QuickView>} </div> <p.demo-note>{len(cart) == 0 ? "The cart is empty." : `In the cart: ${join(cart, ", ")}.`}</p> </div>}view QuickViewDemo() = <QuickViewDemoLive/>Source
ui/quick_view.qwe
// QuickView: a card (yours, as children) with a "Quick view" button on it that opens a// preview of the item in a dialog: its image, title, a line such as the price, a// description, and the actions (a main button and a link to the full page). The// button shows when the card is hovered or has the focus, and always on touch// screens. The dialog is the browser's own <dialog>: focus stays in it, Escape or a// click outside closes it.//// <QuickView title="Linen shirt" image="/img/shirt.jpg" alt="A white linen shirt" meta="€49"// text="Loose fit, washed linen, mother-of-pearl buttons." href="/shop/linen-shirt">// <ProductCard product={p}/>// </QuickView>// <QuickView title={p.name} image={p.img} action="Add to cart" on_action={() => { add(p) }}> ... </QuickView>//// title: the item's name, the dialog's heading and name. image and alt: its picture// (alt says what it shows). meta: a line under the title (a price, a date). text: the// description. href: a link to the item's own page, "View details" (details: its text).// action: a main button's text; on_action(), inside one of your components, is what it// does (then the dialog closes). label ("Quick view"): the button's text.// children: the card the button sits on.import ui/theme.qwe use ui_clslet quick_view_css = css`.ui-quick-view { position: relative; display: block; }.ui-quick-view-open { position: absolute; left: 50%; bottom: 14px; z-index: 2; translate: -50% 0; display: inline-flex; align-items: center; gap: 6px; height: 34px; padding: 0 14px; border: 1px solid var(--ui-border); border-radius: 999px; background: var(--ui-surface); color: var(--ui-fg); box-shadow: var(--ui-shadow); font: 500 13px/1 var(--ui-font); white-space: nowrap; cursor: pointer; opacity: 0; transition: opacity .15s, translate .15s;}.ui-quick-view-open svg { width: 16px; height: 16px; }.ui-quick-view:hover .ui-quick-view-open, .ui-quick-view:focus-within .ui-quick-view-open { opacity: 1; translate: -50% -2px; }.ui-quick-view-open:hover { background: var(--ui-subtle); }.ui-quick-view-open:focus-visible { outline: none; box-shadow: var(--ui-shadow), var(--ui-focus); }@media (hover: none) { .ui-quick-view-open { opacity: 1; } }.ui-quick-view-dialog { width: min(820px, calc(100vw - 32px)); max-height: calc(100dvh - 48px); padding: 0; overflow: auto; 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: 15px/1.5 var(--ui-font);}.ui-quick-view-dialog[open] { animation: ui-quick-view-in .18s ease-out; }.ui-quick-view-dialog::backdrop { background: var(--ui-overlay); }@keyframes ui-quick-view-in { from { opacity: 0; transform: scale(.97); } }.ui-quick-view-box { position: relative; display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }.ui-quick-view-box.ui-quick-view-plain { grid-template-columns: minmax(0, 1fr); }.ui-quick-view-img { display: block; width: 100%; height: 100%; min-height: 260px; object-fit: cover; background: var(--ui-subtle); }.ui-quick-view-info { display: flex; flex-direction: column; gap: 10px; padding: 28px 26px 24px; }.ui-quick-view-info h2 { margin: 0; padding-right: 30px; font-size: 22px; font-weight: 650; line-height: 1.25; }.ui-quick-view-meta { margin: 0; font-size: 18px; font-weight: 600; color: var(--ui-accent); }.ui-quick-view-text { margin: 0; color: var(--ui-muted); }.ui-quick-view-acts { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; margin-top: auto; padding-top: 12px; }.ui-quick-view-act { height: 40px; padding: 0 18px; border: 0; border-radius: var(--ui-radius-sm); background: var(--ui-accent); color: var(--ui-accent-fg); font: 600 14px/1 var(--ui-font); cursor: pointer;}.ui-quick-view-act:hover { background: color-mix(in srgb, var(--ui-accent) 88%, var(--ui-fg)); }.ui-quick-view-link { color: var(--ui-accent); font-weight: 500; text-underline-offset: 3px; }.ui-quick-view-x { position: absolute; top: 12px; right: 12px; display: grid; place-items: center; width: 32px; height: 32px; padding: 0; border: 0; border-radius: 50%; background: var(--ui-surface); color: var(--ui-fg); box-shadow: var(--ui-shadow); cursor: pointer;}.ui-quick-view-x:hover { background: var(--ui-subtle); }.ui-quick-view-x svg { width: 18px; height: 18px; }.ui-quick-view-act:focus-visible, .ui-quick-view-x:focus-visible, .ui-quick-view-link:focus-visible { outline: none; box-shadow: var(--ui-focus); border-radius: var(--ui-radius-sm); }@media (max-width: 640px) { .ui-quick-view-box { grid-template-columns: minmax(0, 1fr); } .ui-quick-view-img { min-height: 0; height: 240px; }}@media (prefers-reduced-motion: reduce) { .ui-quick-view-dialog[open] { animation: none; } .ui-quick-view-open { transition: none; } }`component QuickView(title: str, image: str? = null, alt: str = "", meta: str? = null, text: str? = null, href: str? = null, details: str = "View details", action: str? = null, on_action = null, label: str = "Quick view", class: str? = null, children) { style global quick_view_css state dlg = null <div class={ui_cls(["ui-quick-view", class])}> {children} <button.ui-quick-view-open type="button" aria-haspopup="dialog" aria-label={`${label}: ${title}`} @click={() => dlg.showModal()}> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7S2 12 2 12Z"/><circle cx="12" cy="12" r="3"/></svg> <span aria-hidden="true">{label}</span> </button> <dialog.ui-quick-view-dialog aria-label={title} @mount={(el) => { dlg = el }} @click={(e) => { if e.target.tagName == "DIALOG" { dlg.close() } }}> <div class={ui_cls(["ui-quick-view-box", image == null && "ui-quick-view-plain"])}> {image != null && <img.ui-quick-view-img src={image} alt={alt} loading="lazy"/>} <div.ui-quick-view-info> <h2>{title}</h2> {meta != null && <p.ui-quick-view-meta>{meta}</p>} {text != null && <p.ui-quick-view-text>{text}</p>} {(action != null || href != null) && <div.ui-quick-view-acts> {action != null && <button.ui-quick-view-act type="button" @click={() => { on_action && on_action() dlg.close() }}>{action}</button>} {href != null && <a.ui-quick-view-link href={href}>{details} →</a>} </div>} </div> <button.ui-quick-view-x type="button" aria-label="Close" @click={() => dlg.close()}> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg> </button> </div> </dialog> </div>}Related
AlertDialogA modal that only informs, with one OK button, on the browser's own <dialog> and no JavaScript.6 views · 7 downloadsFullscreenModalA dialog taking the whole window, with a bar holding close, its title and a main action, on the browser's <dialog>.5 views · 4 downloadsModalA dialog over the page, on the browser's own <dialog>: focus, Escape and screen readers handled.6 views · 8 downloadsPromptDialogA modal asking for one text value with OK and Cancel, on the browser's own <dialog>; Enter submits, Escape cancels.4 views · 5 downloadsSearchOverlayA full-screen search with results as one types, grouped and marked, from a list or a server, opened with /.7 views · 5 downloadsShareSheetA bottom sheet with the ways to share a link: copy it, email, X, LinkedIn, WhatsApp and more, and the system's own.6 views · 4 downloads
Thread
Thread 0 posts
On QuickView: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.