StoryRing
A row of avatars in rings, bright for stories not yet seen and grey once seen, with your own first to add one.
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/story_ring.qwemain.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>}Related
LeaderboardPeople ranked by score with medals for the top three, shared places for ties, and your own row kept in view.7 views · 7 downloadsCommentOne comment: avatar, name, role, time, text, Like and Reply, with its replies nested inside it.6 views · 8 downloadsCommentThreadNested comments with Like and Reply, a reply box that opens under the comment answered, and branches that fold.5 views · 5 downloadsHashtagA #tag that links to its page, with an optional count, and HashtagList, the trending tags ranked with their post counts.6 views · 8 downloadsPostCardA post in a feed: author, time, text, picture, like, comment and share counts, and the three buttons.5 views · 5 downloadsProfileHeaderThe top of a profile: cover, avatar, name, handle, bio, facts, counts and a Follow button that becomes Following.6 views · 5 downloads
Thread
Thread 0 posts
On StoryRing: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.