Social

StoryRing

A row of avatars in rings, bright for stories not yet seen and grey once seen, with your own first to add one.

#social#user#media#list#no-js

Demo

Preview

Open a story to mark it seen.

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

Example

The code of the preview above.

demos/story_ring.qwe
component StoryRingDemoLive() {    state seen = []    state added = 0    let people = [{ name: "Ana Silva", src: "https://i.pravatar.cc/120?img=47", count: 3 }, { name: "Ben Okafor", src: "https://i.pravatar.cc/120?img=15" },                  { name: "Lee Chen", count: 1 }, { name: "Mia Rossi", src: "https://i.pravatar.cc/120?img=32" }, { name: "Sam Patel" },                  { name: "Zoe Park", src: "https://i.pravatar.cc/120?img=5" }]        | map((p) => { ...p, seen: contains(seen, p.name) })    <div.demo-stack>        <div id="story-ring-demo-live">            <StoryRing you={{ name: "You" }} people={people} on_open={(n) => { seen = unique(seen | push(n)) }} on_add={() => { added += 1 }}/>        </div>        <p.demo-note>{len(seen) == 0 ? "Open a story to mark it seen." : `Seen: ${join(seen, ", ")}.`}{added > 0 ? ` Add pressed ${added}.` : ""}</p>    </div>}view StoryRingDemo() =    <div.demo-stack>        <StoryRingDemoLive/>        <div id="story-ring-demo-links">            <StoryRing size="sm" label="Team stories" you={{ name: "Ana Silva", src: "https://i.pravatar.cc/120?img=47", href: "#story_ring" }}                       people={[{ name: "Ben Okafor", href: "#story_ring", seen: true }, { name: "Lee Chen", href: "#story_ring" }]}/>        </div>    </div>

Source

ui/story_ring.qwe
// StoryRing: a row of people's avatars, each in a ring: bright while they have stories// you have not seen, grey once you have. Your own comes first with a + to add one.// Each is a link to the story, or a button that calls on_open.////   <StoryRing people={[{ name: "Ana Silva", src: "/img/ana.jpg", href: "/stories/ana" },//                       { name: "Ben Okafor", seen: true, href: "/stories/ben" }]}/>//   <StoryRing you={{ name: "Me", href: "/stories/new" }} people={stories} on_open={(name) => { show(name) }}/>//// people: a list of { name, src?, seen?, href?, count? (how many stories, for screen// readers) }. you: { name, src?, href? }, shown first as "Your story" with a + (left// out, no first item). size: "sm" (48px), "md" (64px, default) or "lg" (80px).// label: what screen readers call the row ("Stories"). names: false hides the names// under the avatars. The row scrolls sideways when it does not fit.// on_open(name): inside one of your components, called with the person's name when their// story is opened (for one without an href). on_add(): called for your own + .import ui/theme.qwe use ui_clslet story_ring_css = css`.ui-story-ring { display: flex; gap: 14px; margin: 0; padding: 4px 2px; list-style: none; overflow-x: auto; scrollbar-width: thin; font: 12px/1.3 var(--ui-font); color: var(--ui-fg); --s: 64px; }.ui-story-ring li { flex: none; width: calc(var(--s) + 12px); }.ui-story-ring-item { display: grid; justify-items: center; gap: 6px; width: 100%; padding: 0; border: 0; background: none; color: inherit; font: inherit; text-decoration: none; cursor: pointer; }.ui-story-ring-item:focus-visible { outline: none; }.ui-story-ring-item:focus-visible .ui-story-ring-circle { box-shadow: var(--ui-focus); }.ui-story-ring-circle {  position: relative; display: grid; place-items: center; width: calc(var(--s) + 8px); height: calc(var(--s) + 8px); border-radius: 50%; padding: 3px;  background: conic-gradient(from 200deg, var(--ui-accent), color-mix(in srgb, var(--ui-accent) 40%, var(--ui-danger)), var(--ui-warning), var(--ui-accent));  transition: transform .15s;}.ui-story-ring-item:hover .ui-story-ring-circle { transform: scale(1.04); }.ui-story-ring-seen .ui-story-ring-circle { background: var(--ui-strong); }.ui-story-ring-you .ui-story-ring-circle { background: transparent; border: 2px dashed var(--ui-strong); padding: 2px; }.ui-story-ring-avatar {  display: grid; place-items: center; width: 100%; height: 100%; border-radius: 50%; overflow: hidden; border: 2px solid var(--ui-bg);  background: var(--ui-accent-soft); color: var(--ui-accent); font-size: calc(var(--s) * .32); font-weight: 650; user-select: none;}.ui-story-ring-avatar img { width: 100%; height: 100%; object-fit: cover; }.ui-story-ring-plus {  position: absolute; right: 0; bottom: 0; display: grid; place-items: center; width: 22px; height: 22px; border-radius: 50%;  border: 2px solid var(--ui-bg); background: var(--ui-accent); color: var(--ui-accent-fg); font-size: 16px; font-weight: 700; line-height: 1;}.ui-story-ring-name { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }.ui-story-ring-seen .ui-story-ring-name { color: var(--ui-muted); }.ui-story-ring-sm { --s: 48px; gap: 10px; font-size: 11px; }.ui-story-ring-lg { --s: 80px; gap: 18px; font-size: 13px; }@media (prefers-reduced-motion: reduce) { .ui-story-ring-circle { transition: none; } }`// Up to two initials of a name: "Ana Silva" is "AS".fn story_ring_initials(name: str) -> str =    upper(join(split(trim(name), " ") | filter((w) => w != "") | map((w) => slice(w, 0, 1)) | slice(0, 2), ""))// What screen readers hear for a person: "Ana Silva, 3 new stories", "Ben Okafor, seen".fn story_ring_said(p: map) -> str {    let n = p.count ?? null    p.seen == true ? `${p.name}, seen` : (n != null ? `${p.name}, ${n} new ${n == 1 ? "story" : "stories"}` : `${p.name}, new story`)}view StoryRing(people: list = [], you: map? = null, size: str = "md", label: str = "Stories", names: bool = true,               on_open = null, on_add = null, class: str? = null) {    style global story_ring_css    let avatar = (p: map) => <span.ui-story-ring-avatar aria-hidden="true">        {p.src != null ? <img src={p.src} alt="" loading="lazy"/> : story_ring_initials(p.name ?? "")}    </span>    <ul class={ui_cls(["ui-story-ring", size != "md" && `ui-story-ring-${size}`, class])} aria-label={label}>        {you != null && <li.ui-story-ring-you>            {you.href != null                ? <a.ui-story-ring-item href={you.href} aria-label="Add to your story">                      <span.ui-story-ring-circle>{avatar(you)}<span.ui-story-ring-plus aria-hidden="true">+</span></span>                      {names && <span.ui-story-ring-name aria-hidden="true">Your story</span>}                  </a>                : <button.ui-story-ring-item type="button" aria-label="Add to your story" @click={() => { on_add && on_add() }}>                      <span.ui-story-ring-circle>{avatar(you)}<span.ui-story-ring-plus aria-hidden="true">+</span></span>                      {names && <span.ui-story-ring-name aria-hidden="true">Your story</span>}                  </button>}        </li>}        {for p in people => <li key={p.name} class={p.seen == true ? "ui-story-ring-seen" : null}>            {p.href != null                ? <a.ui-story-ring-item href={p.href} aria-label={story_ring_said(p)}>                      <span.ui-story-ring-circle>{avatar(p)}</span>                      {names && <span.ui-story-ring-name aria-hidden="true">{p.name}</span>}                  </a>                : <button.ui-story-ring-item type="button" aria-label={story_ring_said(p)} @click={() => { on_open && on_open(p.name) }}>                      <span.ui-story-ring-circle>{avatar(p)}</span>                      {names && <span.ui-story-ring-name aria-hidden="true">{p.name}</span>}                  </button>}        </li>}    </ul>}
Thread
66Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.