Ana Silva
Shipped the new tables today 🚀 Sorting, filtering and 100,000 rows without a hiccup.
128 likes12 comments2.5K shares
A post in a feed: author, time, text, picture, like, comment and share counts, and the three buttons.
Shipped the new tables today 🚀 Sorting, filtering and 100,000 rows without a hiccup.
Anyone up for a run along the river on Saturday?
Not liked.
v2.4.0 is out.
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/post_card.qweimport ui/theme.qwe use theme_cssuse ui/post_card.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>The code of the preview above.
component PostCardDemoLive() { state liked = false state likes = 1199 state events = [] <div.demo-stack> <PostCard id="post-card-demo-live" name="Ben Okafor" handle="@ben" time="4 h" datetime="2026-10-28T07:00:00Z" text="Anyone up for a run along the river on Saturday?" likes={likes} comments={3} liked={liked} on_like={(on) => { liked = on; likes = on ? likes + 1 : likes - 1 }} on_comment={() => { events = events | push("comment") }} on_share={() => { events = events | push("share") }}/> <p.demo-note>{liked ? "Liked." : "Not liked."} {len(events) == 0 ? "" : `Pressed: ${join(events, ", ")}.`}</p> </div>}view PostCardDemo() = <div.demo-stack.demo-narrow style="max-width: 520px"> <PostCard id="post-card-demo-first" name="Ana Silva" handle="@ana" verified={true} avatar="https://i.pravatar.cc/80?img=47" time="2 h" datetime="2026-10-28T09:12:00Z" href="#post_card" text="Shipped the new tables today 🚀Sorting, filtering and 100,000 rows without a hiccup." image="https://picsum.photos/seed/tables/900/420" image_alt="A table of orders on a laptop screen" likes={128} comments={12} shares={2500} comment_href="#post_card"/> <PostCardDemoLive/> <PostCard id="post-card-demo-quiet" name="Release bot" text="v2.4.0 is out." time="Yesterday" actions={false}/> </div>// PostCard: a post in a feed: who wrote it, when, what they said, a picture, and how// many liked, commented and shared it, with Like, Comment and Share under it.//// <PostCard name="Ana Silva" handle="@ana" time="2 h" datetime="2026-10-28T09:12:00Z" likes={128} comments={12}// text="Shipped the new tables today 🚀" image="/img/tables.png"/>// <PostCard name="Ben Okafor" text="Anyone up for a run?" liked={liked} likes={likes}// on_like={(on) => { liked = on }} on_comment={() => { open = true }}/>//// name: who wrote it; handle: their @name; avatar: their picture (else their initials);// verified: a tick after the name. time: when, as it should read; datetime: the same in// ISO form; href: a link to the post, on the time. text: what they said, plain text// (line breaks kept); children: anything more (a quoted post, a link card).// image: a picture under the text (image_alt says what it shows). likes, comments,// shares: the counts (1200 shows as 1.2K); liked: whether you liked it.// actions: false hides the buttons. comment_href: a link for the Comment button.// on_like(liked): inside one of your components, called with true or false.// on_comment(), on_share(): called when those are pressed.import ui/theme.qwe use ui_clslet post_card_css = css`.ui-post-card { display: grid; gap: 10px; min-width: 0; padding: 14px 16px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius); background: var(--ui-surface); font: 14px/1.5 var(--ui-font); color: var(--ui-fg);}.ui-post-card-head { display: flex; align-items: center; gap: 10px; min-width: 0; }.ui-post-card-avatar { display: grid; place-items: center; flex: none; width: 40px; height: 40px; border-radius: 50%; overflow: hidden; background: var(--ui-accent-soft); color: var(--ui-accent); font-size: 14px; font-weight: 650; user-select: none;}.ui-post-card-avatar img { width: 100%; height: 100%; object-fit: cover; }.ui-post-card-who { display: grid; min-width: 0; line-height: 1.3; }.ui-post-card-name { display: flex; align-items: center; gap: 4px; font-weight: 600; }.ui-post-card-name svg { width: 15px; height: 15px; color: var(--ui-info); }.ui-post-card-meta { display: flex; gap: 6px; color: var(--ui-muted); font-size: 13px; }.ui-post-card-meta a { color: inherit; text-decoration: none; }.ui-post-card-meta a:hover { text-decoration: underline; }.ui-post-card-text { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; font-size: 15px; }.ui-post-card-image { display: block; width: 100%; max-height: 420px; border-radius: var(--ui-radius-sm); object-fit: cover; background: var(--ui-subtle); }.ui-post-card-counts { display: flex; gap: 14px; color: var(--ui-muted); font-size: 13px; }.ui-post-card-actions { display: flex; gap: 4px; margin: 0 -8px -4px; padding-top: 6px; border-top: 1px solid var(--ui-border); }.ui-post-card-btn { display: inline-flex; flex: 1; align-items: center; justify-content: center; gap: 6px; height: 34px; padding: 0 10px; border: 0; border-radius: var(--ui-radius-sm); background: transparent; color: var(--ui-muted); font: 500 13px/1 var(--ui-font); text-decoration: none; cursor: pointer;}.ui-post-card-btn svg { width: 17px; height: 17px; }.ui-post-card-btn:hover { background: var(--ui-subtle); color: var(--ui-fg); }.ui-post-card-btn:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-post-card-btn[aria-pressed="true"] { color: var(--ui-danger); }.ui-post-card-btn[aria-pressed="true"] svg { fill: currentColor; }`// Up to two initials of a name: "Ana Silva" is "AS".fn post_card_initials(name: str) -> str = upper(join(split(trim(name), " ") | filter((w) => w != "") | map((w) => slice(w, 0, 1)) | slice(0, 2), ""))// A count as it reads in a feed: 950 is "950", 1200 is "1.2K", 2500000 is "2.5M".fn post_card_count(n: int) -> str { if n < 1000 { return str(n) } let unit = n < 1000000 ? 1000 : 1000000 let tenths = floor(n * 10 / unit) `${floor(tenths / 10)}${tenths % 10 == 0 ? "" : `.${tenths % 10}`}${unit == 1000 ? "K" : "M"}`}view PostCard(name: str, text: str = "", handle: str? = null, avatar: str? = null, verified: bool = false, time: str? = null, datetime: str? = null, href: str? = null, image: str? = null, image_alt: str = "", likes: int = 0, comments: int = 0, shares: int = 0, liked: bool = false, actions: bool = true, comment_href: str? = null, id: str? = null, on_like = null, on_comment = null, on_share = null, class: str? = null, children) { style global post_card_css let when = time != null && <time datetime={datetime}>{time}</time> <article class={ui_cls(["ui-post-card", class])} id={id} aria-label={`Post by ${name}`}> <div.ui-post-card-head> <span.ui-post-card-avatar aria-hidden="true"> {avatar != null ? <img src={avatar} alt="" loading="lazy"/> : post_card_initials(name)} </span> <div.ui-post-card-who> <span.ui-post-card-name> {name} {verified && <svg viewBox="0 0 24 24" fill="currentColor" role="img" aria-label="Verified"><path d="M12 2l2.4 2.1 3.2-.4 1 3 2.9 1.5-.6 3.2L22 14l-2.1 2.4.4 3.2-3 1L15.8 23.5l-3.2-.6L9.6 23.5l-1.5-2.9-3-1 .4-3.2L3.4 14l1.1-2.6-.6-3.2L6.8 6.7l1-3 3.2.4z"/><path d="M8.5 12.5l2.5 2.5 5-5" fill="none" stroke="var(--ui-bg)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>} </span> <span.ui-post-card-meta> {handle != null && <span>{handle}</span>} {handle != null && time != null && <span aria-hidden="true">·</span>} {href != null ? <a href={href}>{when}</a> : when} </span> </div> </div> {text != "" && <p.ui-post-card-text>{text}</p>} {children} {image != null && <img.ui-post-card-image src={image} alt={image_alt} loading="lazy"/>} {(likes > 0 || comments > 0 || shares > 0) && <div.ui-post-card-counts> {likes > 0 && <span>{post_card_count(likes)} {likes == 1 ? "like" : "likes"}</span>} {comments > 0 && <span>{post_card_count(comments)} {comments == 1 ? "comment" : "comments"}</span>} {shares > 0 && <span>{post_card_count(shares)} {shares == 1 ? "share" : "shares"}</span>} </div>} {actions && <div.ui-post-card-actions> <button.ui-post-card-btn type="button" aria-pressed={liked ? "true" : "false"} @click={() => { on_like && on_like(!liked) }}> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20.8 4.6a5.5 5.5 0 0 0-7.8 0L12 5.6l-1-1a5.5 5.5 0 0 0-7.8 7.8l1 1L12 21l7.8-7.6 1-1a5.5 5.5 0 0 0 0-7.8z"/></svg> Like </button> {comment_href != null ? <a.ui-post-card-btn href={comment_href}> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12a8 8 0 0 1-8 8H5l-2 2V12a8 8 0 0 1 8-8h2a8 8 0 0 1 8 8z"/></svg> Comment </a> : <button.ui-post-card-btn type="button" @click={() => { on_comment && on_comment() }}> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12a8 8 0 0 1-8 8H5l-2 2V12a8 8 0 0 1 8-8h2a8 8 0 0 1 8 8z"/></svg> Comment </button>} <button.ui-post-card-btn type="button" @click={() => { on_share && on_share() }}> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 12v7a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-7"/><path d="M16 6l-4-4-4 4"/><path d="M12 2v13"/></svg> Share </button> </div>} </article>}On PostCard: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.