Data

RecordCard

A record's fields as labelled values in a card, with a title, status, formatted values and actions in a footer.

#data#card#record#no-js

Demo

Preview

Office printer

HP LaserJet M404

Offline
Serial
PHB8K21734
Toner left
18%
Pages printed
48,210

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/record_card.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/record_card.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/record_card.qwe
view RecordCardDemo() =    <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 14px">        <RecordCard id="record-card-demo-customer" title="Northwind Traders" subtitle="Customer since 2019" avatar={true}                    status="Active" status_tone="success" href="#northwind"                    fields={[                        { label: "Email", value: "buy@northwind.example", href: "mailto:buy@northwind.example" },                        { label: "Phone", value: null },                        { label: "Balance", value: 1240.5, format: "money" },                        { label: "Renews", value: "2027-01-31", format: "date" },                        { label: "Tax exempt", value: false, format: "bool" },                        { label: "Customer id", value: "cus_8f2a91", mono: true },                        { label: "Notes", value: "Prefers invoices by post, to the Hamburg office.", wide: true },                    ]}>            <Button size="sm" variant="ghost" href="#history">History</Button>            <Button size="sm" variant="outline" href="#edit">Edit</Button>        </RecordCard>        <RecordCard id="record-card-demo-device" title="Office printer" subtitle="HP LaserJet M404" status="Offline" status_tone="danger"                    columns={1} level={4}                    fields={[                        { label: "Serial", value: "PHB8K21734", mono: true },                        { label: "Toner left", value: 0.18, format: "percent" },                        { label: "Pages printed", value: 48210, format: "number" },                    ]}/>    </div>

Source

ui/record_card.qwe
// RecordCard: one record's fields as labelled values in a card: a customer, an// order, a device, a contract. A title row with an optional picture or initials,// a status and a subtitle; then the fields, in one or two columns; then any// actions you put in as children.////   <RecordCard title="Northwind Traders" subtitle="Customer since 2019" status="Active" status_tone="success"//               fields={[//                   { label: "Email", value: "buy@northwind.example", href: "mailto:buy@northwind.example" },//                   { label: "Balance", value: 1240.5, format: "money" },//                   { label: "Renews", value: "2027-01-31", format: "date" },//                   { label: "Notes", value: "Prefers invoices by post.", wide: true },//               ]}>//       <Button size="sm" variant="outline" href="/customers/7/edit">Edit</Button>//   </RecordCard>//// title, subtitle: the record's name and a line under it. avatar: a picture's URL,// or initials: true for the title's initials (false, default: none). status: a// label on the right, in status_tone ("neutral" default, "accent", "success",// "warning", "danger", "info"). fields: maps of { label, value, format?,// currency?, href?, mono?, wide? }: format "number", "money", "percent", "date",// or "bool" (Yes / No); a null or "" shows as a dash; href makes the value a// link; mono sets it in the code font (ids, keys); wide gives it the whole row.// columns: 1 or 2 (default; one where it is narrow). children: actions, in a// footer. href: the title links to the record. level: the title's heading level// (3). id: the element's id. The fields are a <dl>.import ui/theme.qwe use ui_clslet record_card_css = css`.ui-recordcard { display: flex; flex-direction: column; min-width: 0; border: 1px solid var(--ui-border); border-radius: var(--ui-radius); background: var(--ui-surface);  font: 14px/1.45 var(--ui-font); color: var(--ui-fg); container-type: inline-size; }.ui-recordcard-head { display: flex; align-items: center; gap: 12px; padding: 14px 18px; border-bottom: 1px solid var(--ui-border); }.ui-recordcard-pic { position: relative; flex: none; display: grid; place-items: center; width: 40px; height: 40px; overflow: hidden;  border-radius: 50%; background: var(--ui-accent-soft); color: var(--ui-accent); font-size: 14px; font-weight: 600; }.ui-recordcard-pic img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }.ui-recordcard-titles { flex: 1; min-width: 0; }.ui-recordcard-title { margin: 0; font-size: 16px; font-weight: 600; line-height: 1.3; overflow-wrap: anywhere; }.ui-recordcard-title a { color: inherit; text-decoration: none; }.ui-recordcard-title a:hover { color: var(--ui-accent); text-decoration: underline; }.ui-recordcard-title a:focus-visible { outline: none; box-shadow: var(--ui-focus); border-radius: 3px; }.ui-recordcard-sub { margin: 1px 0 0; color: var(--ui-muted); font-size: 13px; }.ui-recordcard-status { flex: none; padding: 2px 9px; border-radius: 999px; font-size: 12px; font-weight: 600; --t: var(--ui-muted);  color: var(--t); background: color-mix(in srgb, var(--t) 13%, transparent); }.ui-recordcard-accent { --t: var(--ui-accent); }.ui-recordcard-success { --t: var(--ui-success); }.ui-recordcard-warning { --t: var(--ui-warning); }.ui-recordcard-danger { --t: var(--ui-danger); }.ui-recordcard-info { --t: var(--ui-info); }.ui-recordcard-fields { flex: 1; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); align-content: start; gap: 14px 24px; margin: 0; padding: 16px 18px; }.ui-recordcard-one .ui-recordcard-fields { grid-template-columns: minmax(0, 1fr); }.ui-recordcard-field { display: grid; gap: 2px; min-width: 0; }.ui-recordcard-wide { grid-column: 1 / -1; }.ui-recordcard-field dt { color: var(--ui-muted); font-size: 12px; font-weight: 500; }.ui-recordcard-field dd { margin: 0; overflow-wrap: anywhere; }.ui-recordcard-field dd a { color: var(--ui-accent); text-decoration: none; }.ui-recordcard-field dd a:hover { text-decoration: underline; }.ui-recordcard-field dd a:focus-visible { outline: none; box-shadow: var(--ui-focus); border-radius: 3px; }.ui-recordcard-mono { font: 13px/1.45 var(--ui-mono); }.ui-recordcard-num { font-variant-numeric: tabular-nums; }.ui-recordcard-none { color: var(--ui-muted); }.ui-recordcard-foot { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; padding: 12px 18px; border-top: 1px solid var(--ui-border);  background: color-mix(in srgb, var(--ui-subtle) 50%, var(--ui-surface)); border-radius: 0 0 var(--ui-radius) var(--ui-radius); }@container (max-width: 420px) { .ui-recordcard-fields { grid-template-columns: minmax(0, 1fr); } }`fn record_card_fmt(v, format: str? = null, currency: str = "$") -> str {    if v == null || v == "" { return "—" }    if format == "bool" { return v == true ? "Yes" : "No" }    if format == "date" { return type_of(v) == "str" && date.valid(v) ? date.format(v, "MMM D, YYYY") : str(v) }    if type_of(v) != "int" && type_of(v) != "float" { return str(v) }    let sign = v < 0 ? "-" : ""    match format {        "money" => `${sign}${currency}${format_number(abs(v), "en-US", 2)}`        "percent" => `${format_number(round(v * 1000) / 10, "en-US")}%`        _ => format_number(v, "en-US")    }}fn record_card_initials(name: str) -> str {    let words = split(trim(name), " ") | filter((w) => w != "")    upper(len(words) == 0 ? "?" : (len(words) == 1 ? slice(words[0], 0, 2) : words[0][0] + words[-1][0]))}view RecordCard(title: str, subtitle: str? = null, avatar = false, status: str? = null, status_tone: str = "neutral",                fields: list = [], columns: int = 2, href: str? = null, level: int = 3, id: str? = null, class: str? = null, children) {    style global record_card_css    let pic = avatar != false && avatar != null    let heading = href != null ? <a href={href}>{title}</a> : title    <article class={ui_cls(["ui-recordcard", columns == 1 && "ui-recordcard-one", class])} id={id}>        <header.ui-recordcard-head>            {pic && <span.ui-recordcard-pic aria-hidden="true">                {record_card_initials(title)}                {type_of(avatar) == "str" && <img src={avatar} alt="" loading="lazy"/>}            </span>}            <div.ui-recordcard-titles>                {match level {                    2 => <h2.ui-recordcard-title>{heading}</h2>                    4 => <h4.ui-recordcard-title>{heading}</h4>                    _ => <h3.ui-recordcard-title>{heading}</h3>                }}                {subtitle != null && <p.ui-recordcard-sub>{subtitle}</p>}            </div>            {status != null && <span class={`ui-recordcard-status ui-recordcard-${status_tone}`}>{status}</span>}        </header>        {len(fields) > 0 && <dl.ui-recordcard-fields>            {for f in fields => {                let text = record_card_fmt(f.value, f.format, f.currency ?? "$")                let none = f.value == null || f.value == ""                let numeric = f.format == "number" || f.format == "money" || f.format == "percent"                <div class={ui_cls(["ui-recordcard-field", f.wide == true && "ui-recordcard-wide"])}>                    <dt>{f.label}</dt>                    <dd class={ui_cls([f.mono == true && "ui-recordcard-mono", numeric && "ui-recordcard-num", none && "ui-recordcard-none"])}>                        {f.href != null && !none ? <a href={f.href}>{text}</a> : text}                    </dd>                </div>            }}        </dl>}        // No children is null on newer Quillo and empty HTML on older: either means no footer.        {children != null && str(children) != "" && <footer.ui-recordcard-foot>{children}</footer>}    </article>}
Thread
74Useful? Like it, or add to its thread below.

Thread 0 posts

On RecordCard: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.