ReplyBox
A reply box that is one line until focused, then grows and shows Post and Cancel; it posts a form or calls back.
Demo
Nothing posted yet.
Click in the box: it grows and shows the buttons. Ctrl+Enter posts, Escape cancels. Below, one that posts a form:
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/reply_box.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/reply_box.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/reply_box.qwe
component ReplyBoxDemoLive() { state posted = [] state cancelled = 0 <div.demo-stack> <ReplyBox id="reply-box-demo-live" you="Ana Silva" on_post={(t) => { posted = posted | push(t) }} on_cancel={() => { cancelled += 1 }}/> <p.demo-note>{len(posted) == 0 ? "Nothing posted yet." : `Posted: ${join(posted, " · ")}.`}{cancelled > 0 ? ` Cancelled ${cancelled}.` : ""}</p> </div>}view ReplyBoxDemo() = <div.demo-stack> <ReplyBoxDemoLive/> <p.demo-note>Click in the box: it grows and shows the buttons. Ctrl+Enter posts, Escape cancels. Below, one that posts a form:</p> <ReplyBox id="reply-box-demo-form" name="text" method="get" action="#reply_box" you="Ben Okafor" avatar="https://i.pravatar.cc/80?img=15" placeholder="Reply to Ana…"> <input type="hidden" name="post" value="42"/> </ReplyBox> </div>Source
ui/reply_box.qwe
// ReplyBox: a box to write a reply in. Folded, it is one line; focused, it grows// and shows Post and Cancel. Cancel folds it and empties it; Post gives the text to// on_post and folds it, or, in a page, sends the form. Ctrl+Enter (⌘+Enter) posts,// Escape cancels. It is a real <textarea> named `name`, so a plain <form> sends it.//// <ReplyBox action="/posts/42/replies" name="text" you="Ana Silva">// <input type="hidden" name="post" value="42"/>// </ReplyBox>// <ReplyBox you="Ana Silva" on_post={(text) => { replies = replies | push(text) }}/>//// you: who is writing, for the avatar beside the box (initials, or `avatar`, an image).// placeholder ("Write a reply…"), post_label ("Post"), cancel_label ("Cancel").// open: true starts it unfolded (with the focus in it, when it is inside one of your// components). value: text to start with. rows: lines shown when open (3).// In a page, it is a <form method={method} action={action}> (post, and the page's own// address by default) sending `name` ("reply") = the text, with children inside it// (hidden inputs). Inside one of your components, on_post(text) is called with the// text instead, and the box is emptied and folded; on_cancel() when Cancel is pressed.// id: the text box's id ("ui-reply-box"); disabled.import ui/theme.qwe use ui_clslet reply_box_css = css`.ui-reply-box { display: flex; align-items: flex-start; gap: 10px; min-width: 0; margin: 0; font: 14px/1.45 var(--ui-font); color: var(--ui-fg); }.ui-reply-box-avatar { display: grid; place-items: center; flex: none; 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-reply-box-avatar img { width: 100%; height: 100%; object-fit: cover; }.ui-reply-box-main { display: grid; flex: 1; gap: 8px; min-width: 0; }.ui-reply-box-input { display: block; width: 100%; min-height: 38px; margin: 0; padding: 8px 12px; resize: none; field-sizing: content; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface); color: var(--ui-fg); font: 14px/1.45 var(--ui-font); transition: border-color .15s, box-shadow .15s;}.ui-reply-box-input::placeholder { color: var(--ui-muted); opacity: 1; }.ui-reply-box-input:hover { border-color: var(--ui-strong); }.ui-reply-box-input:focus { outline: none; border-color: var(--ui-accent); box-shadow: var(--ui-focus); }.ui-reply-box-input:disabled { opacity: .55; cursor: not-allowed; }.ui-reply-box-open .ui-reply-box-input { min-height: calc(var(--ui-reply-box-rows, 3) * 1.45em + 18px); }.ui-reply-box-actions { display: flex; justify-content: flex-end; gap: 8px; }.ui-reply-box-actions[hidden] { display: none; }.ui-reply-box-btn { display: inline-flex; align-items: center; height: 32px; padding: 0 14px; border: 1px solid transparent; border-radius: var(--ui-radius-sm); background: var(--ui-accent); color: var(--ui-accent-fg); font: 500 13px/1 var(--ui-font); cursor: pointer;}.ui-reply-box-btn:hover { background: color-mix(in srgb, var(--ui-accent) 88%, var(--ui-fg)); }.ui-reply-box-btn:disabled { opacity: .55; cursor: not-allowed; }.ui-reply-box-btn:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-reply-box-cancel { background: transparent; border-color: var(--ui-border); color: var(--ui-fg); }.ui-reply-box-cancel:hover { background: var(--ui-subtle); }@media (prefers-reduced-motion: reduce) { .ui-reply-box-input { transition: none; } }`// Up to two initials of a name: "Ana Silva" is "AS".fn reply_box_initials(name: str) -> str = upper(join(split(trim(name), " ") | filter((w) => w != "") | map((w) => slice(w, 0, 1)) | slice(0, 2), ""))component ReplyBox(name: str = "reply", you: str? = null, avatar: str? = null, placeholder: str = "Write a reply…", post_label: str = "Post", cancel_label: str = "Cancel", open: bool = false, value: str = "", rows: int = 3, action: str? = null, method: str = "post", disabled: bool = false, id: str = "ui-reply-box", on_post = null, on_cancel = null, class: str? = null, children) { style global reply_box_css state shown = open state draft = value state box = null let ready = trim(draft) != "" let post = () => { if ready && on_post != null { on_post(trim(draft)) draft = "" box.value = "" shown = false box.blur() } } let cancel = () => { draft = "" box.value = "" shown = false box.blur() on_cancel && on_cancel() } <form class={ui_cls(["ui-reply-box", shown && "ui-reply-box-open", class])} method={method} action={action} style={`--ui-reply-box-rows: ${rows}`} @submit={on_post != null ? post : null}> {children} {you != null && <span.ui-reply-box-avatar aria-hidden="true"> {avatar != null ? <img src={avatar} alt="" loading="lazy"/> : reply_box_initials(you)} </span>} <div.ui-reply-box-main> <label.ui-sr for={id}>{placeholder}</label> <textarea.ui-reply-box-input id={id} name={name} rows="1" placeholder={placeholder} required={true} disabled={disabled} aria-expanded={shown ? "true" : "false"} @mount={(el) => { box = el if open && on_post != null { el.focus() } }} @focus={() => { shown = true }} @input={(e) => { draft = e.value }} @keydown={(e) => { if e.key == "Enter" && (e.ctrlKey || e.metaKey) { e.preventDefault() if trim(e.target.value) != "" { if on_post != null { post() } else { e.target.form.requestSubmit() } } } if e.key == "Escape" { e.preventDefault(); cancel() } }}>{value}</textarea> <div.ui-reply-box-actions hidden={!shown}> <button.ui-reply-box-btn.ui-reply-box-cancel type="button" @click={cancel}>{cancel_label}</button> <button.ui-reply-box-btn type="submit" disabled={disabled || !ready}>{post_label}</button> </div> </div> </form>}Related
MentionInputA textarea that offers people when @ is typed, puts @handle in, and sends the handles mentioned beside the text.5 views · 6 downloadsChatInputA chat message box that grows as you type, sends on Enter and breaks the line on Shift+Enter; a plain form in a page.6 views · 6 downloadsPostComposerWrite a post with a character ring and count, attach files, and post with a button or Ctrl+Enter; a plain form in a page.6 views · 6 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 downloadsReactionsEmoji reaction chips with counts, yours marked, and an add button that opens a row of emoji to pick from.6 views · 5 downloads
Thread
Thread 0 posts
On ReplyBox: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.