Avatar
A picture, or initials on a colour of the person's own, with a status dot; and overlapping groups with +N.
Demo
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/avatar.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/avatar.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/avatar.qwe
let avatar_demo_team = [ { name: "Ada Lovelace", status: "online" }, { name: "Grace Hopper", src: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 40 40'%3E%3Crect width='40' height='40' fill='%23c9b8f5'/%3E%3Ccircle cx='20' cy='16' r='7' fill='%236e56cf'/%3E%3Cpath d='M6 40c2-9 8-13 14-13s12 4 14 13z' fill='%236e56cf'/%3E%3C/svg%3E" }, { name: "Alan Turing" }, { name: "Katherine Johnson" }, { name: "Linus Torvalds" }, { name: "Margaret Hamilton" }, { name: "Dennis Ritchie" },]view AvatarDemo() = <div.demo-stack> <div.demo-row> <Avatar name="Ada Lovelace" size="xs"/> <Avatar name="Grace Hopper" size="sm"/> <Avatar name="Alan Turing"/> <Avatar name="Katherine Johnson" size="lg"/> <Avatar name="Plato" size="xl"/> <Avatar name="Margaret Hamilton" shape="square" size="lg"/> </div> <div.demo-row> <Avatar name="Ada Lovelace" status="online"/> <Avatar name="Linus Torvalds" status="away"/> <Avatar name="Dennis Ritchie" status="busy"/> <Avatar name="Barbara Liskov" status="offline"/> <Avatar name="Grace Hopper" src={avatar_demo_team[1].src} size="lg" status="online"/> <Avatar name="Broken Picture" src="data:image/png;base64,bm90IGEgcGljdHVyZQ==" size="lg"/> </div> <div.demo-row> <AvatarGroup people={avatar_demo_team} max={4} label="Project members"/> <AvatarGroup people={take(avatar_demo_team, 3)} size="sm"/> </div> </div>Source
ui/avatar.qwe
// Avatar: a person's picture, or their initials on a colour of their own.//// <Avatar name="Ada Lovelace" src="/img/ada.jpg" status="online"/>// <Avatar name="Grace Hopper" size="lg"/> shows "GH"// <AvatarGroup people={team} max={4}/> four, then "+3"//// name: whose it is; it gives the initials (the first and last words' first// letters, or a single word's first two) and the colour, the same one every time// for a name. src: a picture; the initials show while it loads, and if it fails.// size: "xs" (24px), "sm" (32px), "md" (40px, default), "lg" (56px) or "xl"// (72px). status: "online", "away", "busy" or "offline", a dot on its corner,// also said to screen readers. shape: "circle" (default) or "square".// AvatarGroup: people is a list of { name, src? }, overlapped; past max// the rest are counted in a "+N". label names the group for screen readers.import ui/theme.qwe use ui_clslet avatar_css = css`.ui-avatar { position: relative; display: inline-grid; place-items: center; flex: none; vertical-align: middle; width: var(--s); height: var(--s); border-radius: 50%; --s: 40px; --h: #6e56cf; background: color-mix(in srgb, var(--h) 24%, var(--ui-bg)); color: color-mix(in srgb, var(--h) 70%, var(--ui-fg)); font: 600 calc(var(--s) * .38)/1 var(--ui-font); letter-spacing: .02em; user-select: none;}.ui-avatar-square { border-radius: calc(var(--s) * .22); }.ui-avatar img { position: absolute; inset: 0; width: 100%; height: 100%; border-radius: inherit; object-fit: cover; color: transparent; font-size: 0; }.ui-avatar img::after { content: attr(data-initials); position: absolute; inset: 0; display: grid; place-items: center; border-radius: inherit; background: color-mix(in srgb, var(--h) 24%, var(--ui-bg)); color: color-mix(in srgb, var(--h) 70%, var(--ui-fg)); font: 600 calc(var(--s) * .38)/1 var(--ui-font); }.ui-avatar-xs { --s: 24px; }.ui-avatar-sm { --s: 32px; }.ui-avatar-lg { --s: 56px; }.ui-avatar-xl { --s: 72px; }.ui-avatar-c0 { --h: #e5484d; } .ui-avatar-c1 { --h: #f76b15; } .ui-avatar-c2 { --h: #ad7f00; }.ui-avatar-c3 { --h: #30a46c; } .ui-avatar-c4 { --h: #12a594; } .ui-avatar-c5 { --h: #0090ff; }.ui-avatar-c6 { --h: #6e56cf; } .ui-avatar-c7 { --h: #d6409f; }.ui-avatar-status { position: absolute; right: 0; bottom: 0; width: max(8px, calc(var(--s) * .27)); height: max(8px, calc(var(--s) * .27)); border-radius: 50%; border: 2px solid var(--ui-bg); background: var(--ui-strong); }.ui-avatar-online { background: var(--ui-success); }.ui-avatar-away { background: var(--ui-warning); }.ui-avatar-busy { background: var(--ui-danger); }.ui-avatar-group { display: inline-flex; align-items: center; }.ui-avatar-group > .ui-avatar { box-shadow: 0 0 0 2px var(--ui-bg); }.ui-avatar-group > .ui-avatar + .ui-avatar { margin-left: calc(var(--s) * -.28); }.ui-avatar-more { background: var(--ui-subtle); color: var(--ui-muted); font-size: calc(var(--s) * .32); }`// The initials of a name: "Ada Lovelace" is "AL", "Plato" is "PL", "" is "?".fn avatar_initials(name: str) -> str { let words = split(trim(name), " ") | filter((w) => w != "") let letters = len(words) == 0 ? "?" : (len(words) == 1 ? slice(words[0], 0, 2) : words[0][0] + words[-1][0]) upper(letters)}// One of the eight colours, chosen from the name's letters, so a name always gets the same.fn avatar_hue(name: str) -> int { let chars = "abcdefghijklmnopqrstuvwxyz0123456789" let codes = split(lower(name), "") | map((c) => (index_of(chars, c) ?? 36) + 1) floor(reduce(codes, 5381, (h, c) => (h * 33 + c) % 1000003) / 13) % 8}view Avatar(name: str = "", src: str? = null, size: str = "md", status: str? = null, shape: str = "circle", class: str? = null) { style global avatar_css <span class={ui_cls(["ui-avatar", `ui-avatar-c${avatar_hue(name)}`, size != "md" && `ui-avatar-${size}`, shape == "square" && "ui-avatar-square", class])} role="img" aria-label={status != null ? `${name} (${status})` : name}> <span aria-hidden="true">{avatar_initials(name)}</span> {src != null && <img src={src} alt="" loading="lazy" data-initials={avatar_initials(name)}/>} {status != null && <span class={`ui-avatar-status ui-avatar-${status}`}></span>} </span>}view AvatarGroup(people: list, max: int = 4, size: str = "md", label: str? = null, class: str? = null) { style global avatar_css let shown = len(people) > max ? take(people, max) : people let rest = len(people) - len(shown) <span class={ui_cls(["ui-avatar-group", class])} role="group" aria-label={label}> {for p in shown => <Avatar name={p.name ?? ""} src={p.src} size={size}/>} {rest > 0 && <span class={ui_cls(["ui-avatar", "ui-avatar-more", size != "md" && `ui-avatar-${size}`])} role="img" aria-label={`and ${rest} more`}>+{rest}</span>} </span>}Related
AvatarStackOverlapping avatars with names in tooltips, links to each person, and a +N that names the rest.5 views · 9 downloadsContactCardA person's contact details as tel:, mailto: and map links, with a vCard to download in one tap.5 views · 7 downloadsProfileCardA 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 downloadsAvatarUploadA round picture with Change and Remove, showing the picture picked before it is sent, on a real file input.6 views · 6 downloadsProfileFormAvatar, name, username, bio and website in the usual layout: a plain form posting to your route, shown again with errors.5 views · 8 downloads
Thread
Thread 0 posts
On Avatar: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.