Data

ContactCard

A person's contact details as tel:, mailto: and map links, with a vCard to download in one tap.

#card#user#profile#record#no-js

Demo

Preview

Grace Hopper

Rear admiral · US Navy

Ada Lovelace

Analytical engineer

Plato

The Academy

Get it

It needs theme.qwe, as every component does, and avatar.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/avatar.qwe$ curl -O https://quillolang.org/components/ui/contact_card.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/contact_card.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/contact_card.qwe
view ContactCardDemo() =    <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 16px; align-items: start">        <ContactCard id="contact-card-demo-grace" name="Grace Hopper" title="Rear admiral" company="US Navy"                     email="grace@example.com" phone="+1 (555) 010-0100" website="https://example.com"                     address="1 Navy Way, Arlington, VA 22202"/>        <ContactCard name="Ada Lovelace" title="Analytical engineer" src="https://i.pravatar.cc/112?img=32"                     email="ada@example.com" phone="+44 20 7946 0100" map={false} address="Ockham Park, Surrey">            <Button size="sm" variant="soft">Message</Button>        </ContactCard>        <ContactCard id="contact-card-demo-plain" name="Plato" company="The Academy" email="plato@example.com" vcard={false}/>    </div>

Source

ui/contact_card.qwe
// ContactCard: a person's contact details, each a link that does something: the// phone dials (tel:), the email writes (mailto:), the address opens a map; and a// vCard made from the same details, so one tap saves them to a phone's contacts.////   <ContactCard name="Grace Hopper" title="Rear admiral" company="US Navy" src="/img/grace.jpg"//                email="grace@example.com" phone="+1 555 010 0100" website="https://example.com"//                address="1 Navy Way, Arlington, VA 22202"/>//// name, title, company: who they are; src: their picture (initials without one).// email, phone, website, address: each shown with an icon and linked; left out, not// shown. The tel: link keeps only the digits and + of the phone. The address links// to a map search (map_href names another address to open; map={false} leaves it// plain). vcard: a "Save contact" link that downloads a .vcf made of the details// (true by default). children: more actions beside it. id: the card's id.// Needs avatar.qwe (Avatar draws the picture).import ui/avatar.qwe use Avatarimport ui/theme.qwe use ui_clslet contact_card_css = css`.ui-contact-card { display: grid; gap: 14px; min-width: 0; padding: 18px 20px; border: 1px solid var(--ui-border);  border-radius: var(--ui-radius); background: var(--ui-surface); color: var(--ui-fg); font: 14px/1.5 var(--ui-font); font-style: normal; }.ui-contact-card-who { display: flex; align-items: center; gap: 14px; min-width: 0; }.ui-contact-card-name { margin: 0; font-size: 16px; font-weight: 650; line-height: 1.3; }.ui-contact-card-role { color: var(--ui-muted); font-size: 13px; }.ui-contact-card-rows { display: grid; gap: 6px; margin: 0; padding: 0; list-style: none; }.ui-contact-card-row { display: flex; align-items: flex-start; gap: 10px; min-width: 0; }.ui-contact-card-row svg { flex: none; width: 16px; height: 16px; margin-top: 3px; color: var(--ui-muted); }.ui-contact-card-row a, .ui-contact-card-row span { min-width: 0; overflow-wrap: anywhere; }.ui-contact-card-row a { color: var(--ui-fg); text-decoration: none; }.ui-contact-card-row a:hover { color: var(--ui-accent); text-decoration: underline; }.ui-contact-card-row a:focus-visible { outline: none; border-radius: 4px; box-shadow: var(--ui-focus); }.ui-contact-card-acts:empty { display: none; }.ui-contact-card-acts { display: flex; flex-wrap: wrap; gap: 8px; }.ui-contact-card-save { display: inline-flex; align-items: center; gap: 6px; height: 32px; padding: 0 12px; border: 1px solid var(--ui-border);  border-radius: var(--ui-radius-sm); color: var(--ui-accent); font: 500 13px/1 var(--ui-font); text-decoration: none; }.ui-contact-card-save:hover { border-color: var(--ui-accent); background: var(--ui-accent-soft); }.ui-contact-card-save:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-contact-card-save svg { width: 14px; height: 14px; }`// A value as a vCard writes it: commas, semicolons and line breaks escaped.fn contact_card_esc(s: str) -> str = replace(replace(replace(replace(s, "\\", "\\\\"), ";", "\\;"), ",", "\\,"), "\n", "\\n")// The phone as a tel: link wants it: digits and a leading +.fn contact_card_tel(phone: str) -> str = re.replace(phone, "[^0-9+]", "")// A vCard 3.0 of the details, as text.fn contact_card_vcard(name: str, title, company, email, phone, website, address) -> str {    let words = split(trim(name), " ") | filter((w) => w != "")    let family = len(words) > 1 ? words[-1] : ""    let given = len(words) > 1 ? join(slice(words, 0, len(words) - 1), " ") : name    let lines = [        "BEGIN:VCARD", "VERSION:3.0",        `N:${contact_card_esc(family)};${contact_card_esc(given)};;;`,        `FN:${contact_card_esc(name)}`,        company != null && `ORG:${contact_card_esc(company)}`,        title != null && `TITLE:${contact_card_esc(title)}`,        phone != null && `TEL;TYPE=WORK,VOICE:${phone}`,        email != null && `EMAIL;TYPE=INTERNET:${email}`,        website != null && `URL:${website}`,        address != null && `ADR;TYPE=WORK:;;${contact_card_esc(address)};;;;`,        "END:VCARD",    ]    join(lines | filter((l) => l != false), "\r\n")}fn contact_card_icon(kind: str) =    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">        {match kind {            "email" => [<rect x="3" y="5" width="18" height="14" rx="2"/>, <path d="m3 8 9 6 9-6"/>]            "phone" => <path d="M5 3h4l2 5-2.5 1.5a11 11 0 0 0 6 6L16 13l5 2v4a2 2 0 0 1-2 2A17 17 0 0 1 3 5a2 2 0 0 1 2-2Z"/>            "web" => [<circle cx="12" cy="12" r="9"/>, <path d="M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18"/>]            _ => [<path d="M12 21s6-5.5 6-11a6 6 0 0 0-12 0c0 5.5 6 11 6 11Z"/>, <circle cx="12" cy="10" r="2.2"/>]        }}    </svg>view ContactCard(name: str, title: str? = null, company: str? = null, src: str? = null, email: str? = null,                 phone: str? = null, website: str? = null, address: str? = null, map: bool = true,                 map_href: str? = null, vcard: bool = true, id: str? = null, class: str? = null, children) {    style global contact_card_css    let role = join([title, company] | filter((x) => x != null), " · ")    let site = website != null ? replace(replace(website, "https://", ""), "http://", "") : null    let map_to = map_href ?? (map && address != null ? `https://www.google.com/maps/search/?api=1&query=${url_encode(address)}` : null)    let card = `data:text/vcard;charset=utf-8,${url_encode(contact_card_vcard(name, title, company, email, phone, website, address))}`    <address class={ui_cls(["ui-contact-card", class])} id={id}>        <div.ui-contact-card-who>            <Avatar name={name} src={src} size="lg"/>            <div>                <p.ui-contact-card-name>{name}</p>                {role != "" && <div.ui-contact-card-role>{role}</div>}            </div>        </div>        <ul.ui-contact-card-rows>            {email != null && <li.ui-contact-card-row>{contact_card_icon("email")}<a href={`mailto:${email}`}>{email}</a></li>}            {phone != null && <li.ui-contact-card-row>{contact_card_icon("phone")}<a href={`tel:${contact_card_tel(phone)}`}>{phone}</a></li>}            {website != null && <li.ui-contact-card-row>{contact_card_icon("web")}<a href={website} target="_blank" rel="noopener">{site}</a></li>}            {address != null && <li.ui-contact-card-row>{contact_card_icon("map")}                {map_to != null ? <a href={map_to} target="_blank" rel="noopener">{address}</a> : <span>{address}</span>}</li>}        </ul>        <div.ui-contact-card-acts>            {vcard && <a.ui-contact-card-save href={card} download={`${name}.vcf`}>                <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"                     stroke-linejoin="round" aria-hidden="true"><path d="M8 2.5v8M4.5 7L8 10.5 11.5 7M3 13.5h10"/></svg>                Save contact            </a>}            {children}        </div>    </address>}
Thread
57Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.