Social

ReplyBox

A reply box that is one line until focused, then grows and shows Post and Cancel; it posts a form or calls back.

#social#comment#forms#input#interactive

Demo

Preview

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.qwe
main.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>}
Thread
65Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.