Actions

VoteButtons

Up and down votes around a score that follows them, as on a forum, in a column or a row.

#button#social#forms#interactive

Demo

Preview
No vote.

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/vote_buttons.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/vote_buttons.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/vote_buttons.qwe
component VoteButtonsDemoLive() {    state v = 0    <div.demo-row>        <VoteButtons score={41} on_change={(x) => { v = x }}/>        <span.demo-note>{v == 1 ? "Voted up." : v == -1 ? "Voted down." : "No vote."}</span>    </div>}view VoteButtonsDemo() =    <div.demo-stack>        <VoteButtonsDemoLive/>        <div.demo-row>            <VoteButtons score={128} vote={1} name="vote" layout="horizontal"/>            <VoteButtons score={-3} vote={-1} layout="horizontal" size="sm"/>            <VoteButtons score={7} size="lg"/>            <VoteButtons score={2} disabled={true}/>        </div>    </div>

Source

ui/vote_buttons.qwe
// VoteButtons: an up and a down arrow around a score, as on a forum: one vote each// way, pressed again to take it back, the score following it. In a form it sends// `name` as 1, -1 or 0.////   <VoteButtons score={41}/>//   <VoteButtons score={41} vote={1} name="vote" layout="horizontal" on_change={(v) => { vote(post, v) }}/>//// score: the score before your vote. vote: yours to start with, 1, -1 or 0 (none).// layout: "vertical" (default: up, score, down in a column) or "horizontal".// name: in a <form>, sent as name=1, -1 or 0. label ("Votes"): the group's name for// screen readers. size: "sm", "md" (default) or "lg".// on_change(vote): inside one of your components, given 1, -1 or 0.import ui/theme.qwe use ui_clslet vote_buttons_css = css`.ui-vote-buttons { display: inline-flex; flex-direction: column; align-items: center; gap: 2px; vertical-align: middle; font: 600 14px/1 var(--ui-font); }.ui-vote-buttons-horizontal { flex-direction: row; gap: 4px; padding: 2px; border: 1px solid var(--ui-border); border-radius: 999px; }.ui-vote-buttons-btn {  display: inline-grid; place-items: center; width: 30px; height: 30px; padding: 0; border: 0; border-radius: 999px;  background: transparent; color: var(--ui-muted); cursor: pointer; transition: background .15s, color .15s;}.ui-vote-buttons-btn svg { width: 18px; height: 18px; }.ui-vote-buttons-btn:hover { background: var(--ui-subtle); color: var(--ui-fg); }.ui-vote-buttons-btn:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-vote-buttons-btn:disabled { opacity: .55; cursor: not-allowed; }.ui-vote-buttons-up[aria-pressed="true"] { color: var(--ui-accent); background: var(--ui-accent-soft); }.ui-vote-buttons-down[aria-pressed="true"] { color: var(--ui-danger); background: var(--ui-danger-soft); }.ui-vote-buttons-score { min-width: 2ch; text-align: center; font-variant-numeric: tabular-nums; color: var(--ui-fg); }.ui-vote-buttons-plus .ui-vote-buttons-score { color: var(--ui-accent); }.ui-vote-buttons-minus .ui-vote-buttons-score { color: var(--ui-danger); }.ui-vote-buttons-sm { font-size: 13px; }.ui-vote-buttons-sm .ui-vote-buttons-btn { width: 26px; height: 26px; }.ui-vote-buttons-sm .ui-vote-buttons-btn svg { width: 16px; height: 16px; }.ui-vote-buttons-lg { font-size: 16px; }.ui-vote-buttons-lg .ui-vote-buttons-btn { width: 36px; height: 36px; }.ui-vote-buttons-lg .ui-vote-buttons-btn svg { width: 22px; height: 22px; }@media (prefers-reduced-motion: reduce) { .ui-vote-buttons-btn { transition: none; } }`component VoteButtons(score: int = 0, vote: int = 0, layout: str = "vertical", name: str? = null, label: str = "Votes",                      size: str = "md", disabled: bool = false, on_change = null, class: str? = null) {    style global vote_buttons_css    state mine = vote    let shown = score - vote + mine    let cast = (v: int) => {        mine = mine == v ? 0 : v        on_change && on_change(mine)    }    <div class={ui_cls(["ui-vote-buttons", layout == "horizontal" && "ui-vote-buttons-horizontal", size != "md" && `ui-vote-buttons-${size}`,                        mine == 1 && "ui-vote-buttons-plus", mine == -1 && "ui-vote-buttons-minus", class])}         role="group" aria-label={`${label}: ${shown}`}>        <button.ui-vote-buttons-btn.ui-vote-buttons-up type="button" aria-label="Vote up" title="Vote up" aria-pressed={mine == 1 ? "true" : "false"}                disabled={disabled} @click={() => { cast(1) }}>            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"                 stroke-linejoin="round" aria-hidden="true"><path d="M12 19V5M5 12l7-7 7 7"/></svg>        </button>        <span.ui-vote-buttons-score aria-hidden="true">{shown}</span>        <button.ui-vote-buttons-btn.ui-vote-buttons-down type="button" aria-label="Vote down" title="Vote down" aria-pressed={mine == -1 ? "true" : "false"}                disabled={disabled} @click={() => { cast(-1) }}>            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"                 stroke-linejoin="round" aria-hidden="true"><path d="M12 5v14M5 12l7 7 7-7"/></svg>        </button>        {name != null && <input type="hidden" name={name} value={str(mine)}/>}    </div>}
Thread
56Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.