Northwind Traders
Customer since 2019
- Phone
- —
- Balance
- $1,240.50
- Renews
- Jan 31, 2027
- Tax exempt
- No
- Customer id
- cus_8f2a91
- Notes
- Prefers invoices by post, to the Hamburg office.
A record's fields as labelled values in a card, with a title, status, formatted values and actions in a footer.
Customer since 2019
HP LaserJet M404
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.qweimport ui/theme.qwe use theme_cssuse ui/record_card.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>The code of the preview above.
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>// 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>}On RecordCard: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.