Ana Silva
Does this work with SQLite too? Asking for a small project.
Ben OkaforAuthor
It does. The migrations run the same way.
Ana Silva
Great, thanks!
Lee Chen
Same question here, answered then.
One comment: avatar, name, role, time, text, Like and Reply, with its replies nested inside it.
Does this work with SQLite too? Asking for a small project.
It does. The migrations run the same way.
Great, thanks!
Same question here, answered then.
Yes, and Postgres: the same query runs on both.
Not liked. Reply pressed 0 times.
A comment with no actions under 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/comment.qweimport ui/theme.qwe use theme_cssuse ui/comment.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>The code of the preview above.
component CommentDemoLive() { state liked = false state likes = 3 state replies = 0 <div.demo-stack> <Comment id="comment-demo-live" name="Ben Okafor" role="Author" time="1 h" datetime="2026-10-28T10:30:00Z" text="Yes, and Postgres: the same query runs on both." likes={likes} liked={liked} on_like={(on) => { liked = on; likes = on ? likes + 1 : likes - 1 }} on_reply={() => { replies += 1 }}/> <p.demo-note>{liked ? "You liked it." : "Not liked."} Reply pressed {replies} {replies == 1 ? "time" : "times"}.</p> </div>}view CommentDemo() = <div.demo-stack> <Comment id="comment-demo-first" name="Ana Silva" time="2 h" datetime="2026-10-28T09:12:00Z" likes={12} text="Does this work with SQLite too?Asking for a small project."> <Comment name="Ben Okafor" role="Author" time="1 h" text="It does. The migrations run the same way." likes={2}> <Comment name="Ana Silva" time="40 min" text="Great, thanks!"/> </Comment> <Comment name="Lee Chen" avatar="https://i.pravatar.cc/80?img=12" time="30 min" text="Same question here, answered then."/> </Comment> <CommentDemoLive/> <Comment id="comment-demo-quiet" name="Mia Rossi" time="Yesterday" text="A comment with no actions under it." actions={false}/> </div>// Comment: one comment under a post: who wrote it (avatar, name, a role badge), when,// what they said, and Like and Reply under it. Its replies go inside it, as more// Comments, indented and joined by a line. (CommentThread folds and answers a whole// tree of them.)//// <Comment name="Ana Silva" time="2 h" datetime="2026-10-28T09:12:00Z" likes={3}// text="Does this work with SQLite too?"/>// <Comment name="Ben Okafor" role="Author" text="Yes, and Postgres." liked={true} likes={1}// on_like={(on) => { liked = on }} on_reply={() => { open = true }}>// <Comment name="Ana Silva" text="Great, thanks!"/>// </Comment>//// name: who wrote it. avatar: their picture (else their initials). role: a small badge// after the name ("Author", "Mod"). time: when, as it should read ("2 h", "Yesterday");// datetime: the same in ISO form for machines. text: what they said, plain text (line// breaks kept). likes: how many liked it; liked: whether you did. children: its// replies. actions: false hides the Like and Reply buttons. reply_label ("Reply").// id: the element's id, for a link to it ("#comment-42").// on_like(liked): inside one of your components, called with true or false when the// Like button is pressed. on_reply(): called when Reply is pressed.import ui/theme.qwe use ui_clslet comment_css = css`.ui-comment { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 4px 10px; min-width: 0; font: 14px/1.5 var(--ui-font); color: var(--ui-fg); }.ui-comment-avatar { display: grid; place-items: center; grid-row: span 3; width: 34px; height: 34px; border-radius: 50%; overflow: hidden; background: var(--ui-accent-soft); color: var(--ui-accent); font-size: 12px; font-weight: 650; user-select: none;}.ui-comment-avatar img { width: 100%; height: 100%; object-fit: cover; }.ui-comment-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 8px; min-width: 0; }.ui-comment-name { font-weight: 600; }.ui-comment-role { padding: 1px 6px; border-radius: 999px; background: var(--ui-accent-soft); color: var(--ui-accent); font-size: 11px; font-weight: 600; }.ui-comment-time { color: var(--ui-muted); font-size: 12px; }.ui-comment-text { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; }.ui-comment-actions { display: flex; gap: 4px; margin-left: -8px; }.ui-comment-btn { display: inline-flex; align-items: center; gap: 5px; height: 28px; padding: 0 8px; border: 0; border-radius: var(--ui-radius-sm); background: transparent; color: var(--ui-muted); font: 500 13px/1 var(--ui-font); cursor: pointer;}.ui-comment-btn svg { width: 15px; height: 15px; }.ui-comment-btn:hover { background: var(--ui-subtle); color: var(--ui-fg); }.ui-comment-btn:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-comment-btn[aria-pressed="true"] { color: var(--ui-danger); }.ui-comment-btn[aria-pressed="true"] svg { fill: currentColor; }.ui-comment-replies { grid-column: 2; display: grid; gap: 14px; margin-top: 8px; padding-left: 14px; border-left: 2px solid var(--ui-border); }.ui-comment-replies:empty { display: none; }`// Up to two initials of a name: "Ana Silva" is "AS".fn comment_initials(name: str) -> str = upper(join(split(trim(name), " ") | filter((w) => w != "") | map((w) => slice(w, 0, 1)) | slice(0, 2), ""))view Comment(name: str, text: str = "", avatar: str? = null, role: str? = null, time: str? = null, datetime: str? = null, likes: int = 0, liked: bool = false, actions: bool = true, reply_label: str = "Reply", id: str? = null, on_like = null, on_reply = null, class: str? = null, children) { style global comment_css <article class={ui_cls(["ui-comment", class])} id={id} aria-label={`Comment by ${name}`}> <span.ui-comment-avatar aria-hidden="true"> {avatar != null ? <img src={avatar} alt="" loading="lazy"/> : comment_initials(name)} </span> <div.ui-comment-head> <span.ui-comment-name>{name}</span> {role != null && <span.ui-comment-role>{role}</span>} {time != null && <time.ui-comment-time datetime={datetime}>{time}</time>} </div> <p.ui-comment-text>{text}</p> {actions && <div.ui-comment-actions> <button.ui-comment-btn type="button" aria-pressed={liked ? "true" : "false"} aria-label={`Like, ${likes} ${likes == 1 ? "like" : "likes"}`} @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> <span aria-hidden="true">{likes > 0 ? str(likes) : "Like"}</span> </button> <button.ui-comment-btn type="button" @click={() => { on_reply && on_reply() }}> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 17l-5-5 5-5"/><path d="M20 18v-2a4 4 0 0 0-4-4H4"/></svg> {reply_label} </button> </div>} {children != null && <div.ui-comment-replies>{children}</div>} </article>}On Comment: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.