ContactCard
A person's contact details as tel:, mailto: and map links, with a vCard to download in one tap.
Demo
Grace Hopper
Rear admiral · US Navy
Ada Lovelace
Analytical engineer
- ada@example.com
- +44 20 7946 0100
- Ockham Park, Surrey
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.qwemain.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>}Related
ProfileCardA person at a glance: picture, name, title, bio, a row of stats and action buttons, stacked or in a row.7 views · 4 downloadsTeamCardA person in a team grid: square photo or initials, name, role, a line, and icon links to email, GitHub, LinkedIn, X or a site.6 views · 8 downloadsAvatarA picture, or initials on a colour of the person's own, with a status dot; and overlapping groups with +N.6 views · 5 downloadsEmailPreviewAn email as a client shows it: subject, labels and star, sender with picture, to and cc, the date, the message and attachments to download.6 views · 7 downloadsFeedItemA social-feed item: avatar, name, handle, time, text, a picture or any media, and like, comment and share buttons with counts.6 views · 7 downloadsIdCardAn ID badge with a lanyard slot: photo or initials, name, role, organisation, number, issued and expiry dates, an Expired stamp, and a barcode slot.5 views · 6 downloads
Thread
Thread 0 posts
On ContactCard: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.