PayeeList
Saved payees with bank, masked account and what was last paid when, a Pay button each, and a search.
Demo
Payees
Add payee- Ana OkaforHarbor Bank · account ending in 0345Last paid $120.00 on
- City Power & LightFernway Credit Union · account ending in 9917Ref. CPL-0049-2217Last paid $88.15 on
- Linden Court LettingsFernway Credit Union · account ending in 1204Last paid $1,450.00 on
- Tomás ReyesPinecrest Savings · account ending in 6193Not paid yet
Press Pay on one.
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/payee_list.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/payee_list.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/payee_list.qwe
let payee_list_demo_payees = [ { id: "p-ana", name: "Ana Okafor", bank: "Harbor Bank", number: "88120345", last_amount: 120m, last_date: "2026-10-03" }, { id: "p-power", name: "City Power & Light", bank: "Fernway Credit Union", number: "40029917", reference: "CPL-0049-2217", last_amount: 88.15m, last_date: "2026-10-28" }, { id: "p-rent", name: "Linden Court Lettings", bank: "Fernway Credit Union", number: "40031204", last_amount: 1450m, last_date: "2026-10-01" }, { id: "p-tomas", name: "Tomás Reyes", bank: "Pinecrest Savings", number: "7712 0061 93" },]component PayeeListDemoLive() { state paying = "" <div.demo-stack> <PayeeList id="payee-list-demo" payees={payee_list_demo_payees} add_href="#add-payee" on_pay={(id) => { paying = id }}/> <p.demo-note>{paying == "" ? "Press Pay on one." : `on_pay: ${paying}`}</p> </div>}view PayeeListDemo() = <div style="max-width: 520px"><PayeeListDemoLive/></div>Source
ui/payee_list.qwe
// PayeeList: the people and companies you pay: each one's name, bank and masked// account number, what you last paid them and when, and a Pay button. A search// box filters them by name, bank or the account's last four as it is typed.//// <PayeeList currency="USD" add_href="/payees/new" payees={[// { id: "p1", name: "Ana Okafor", bank: "Harbor Bank", number: "88120345", last_amount: 120m, last_date: "2026-10-03", href: "/pay/p1" },// { id: "p2", name: "City Power & Light", bank: "Fernway Credit Union", number: "40029917" }]}/>// <PayeeList payees={payees} on_pay={(id) => { paying = id }}/>//// payees: a list of { id, name, bank?, number?, reference?, last_amount?,// last_date?, href? }: number shows as its last four ("•••• 0345"); reference, a// line of its own (a customer number); last_amount (a number or decimal) and// last_date (an ISO date) what was paid last ("Not paid yet" without them); href,// where its Pay link goes. on_pay(id): inside one of your components, called by a// Pay button for a payee without href. pay_label: the button's text ("Pay").// label: the heading ("Payees"; null for none). search: the search box (true).// add_href: an "Add payee" link by the heading. currency ("USD") and locale// ("en-US"): how amounts and dates are written. empty: what shows when nothing// matches. id: the element's id.import ui/theme.qwe use ui_clslet payee_list_css = css`.ui-payees { display: grid; gap: 12px; min-width: 0; font: 14px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-payees-top { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; }.ui-payees-title { flex: 1; margin: 0; font-size: 16px; font-weight: 600; }.ui-payees-add { display: inline-flex; align-items: center; gap: 4px; color: var(--ui-accent); font-size: 13px; font-weight: 500; text-decoration: none; }.ui-payees-add:hover { text-decoration: underline; }.ui-payees-add:focus-visible { outline: none; box-shadow: var(--ui-focus); border-radius: 3px; }.ui-payees-search { position: relative; flex-basis: 100%; }.ui-payees-search svg { position: absolute; left: 10px; top: 50%; width: 14px; height: 14px; transform: translateY(-50%); color: var(--ui-muted); pointer-events: none; }.ui-payees-search input { width: 100%; height: 34px; padding: 0 10px 0 30px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface); color: var(--ui-fg); font: 14px var(--ui-font); outline: none; transition: border-color .15s, box-shadow .15s; }.ui-payees-search input:hover { border-color: var(--ui-strong); }.ui-payees-search input:focus { border-color: var(--ui-accent); box-shadow: var(--ui-focus); }.ui-payees-list { margin: 0; padding: 0; list-style: none; border: 1px solid var(--ui-border); border-radius: var(--ui-radius); background: var(--ui-surface); overflow: hidden; }.ui-payees-item { display: flex; align-items: center; gap: 12px; padding: 12px 14px; }.ui-payees-item + .ui-payees-item { border-top: 1px solid var(--ui-border); }.ui-payees-initials { flex: none; display: grid; place-items: center; width: 38px; height: 38px; border-radius: 50%; background: var(--ui-accent-soft); color: var(--ui-accent); font-size: 13px; font-weight: 650; letter-spacing: .02em; }.ui-payees-who { display: grid; flex: 1; gap: 1px; min-width: 0; }.ui-payees-name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }.ui-payees-bank, .ui-payees-last { color: var(--ui-muted); font-size: 12px; }.ui-payees-num { font-family: var(--ui-mono); letter-spacing: .03em; }.ui-payees-last b { color: var(--ui-fg); font-weight: 600; font-variant-numeric: tabular-nums; }.ui-payees-pay { flex: none; display: inline-flex; align-items: center; height: 32px; padding: 0 14px; border: 1px solid var(--ui-accent); border-radius: 999px; background: transparent; color: var(--ui-accent); font: 500 13px var(--ui-font); text-decoration: none; cursor: pointer; transition: background .15s, color .15s; }.ui-payees-pay:hover { background: var(--ui-accent); color: var(--ui-accent-fg); }.ui-payees-pay:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-payees-empty { margin: 0; padding: 24px 14px; border: 1px dashed var(--ui-border); border-radius: var(--ui-radius); color: var(--ui-muted); text-align: center; }@media (prefers-reduced-motion: reduce) { .ui-payees-pay, .ui-payees-search input { transition: none; } }`// A payee's initials: "Ana Okafor" gives "AO", "City Power & Light" "CP".fn payee_list_initials(name: str) -> str = split(trim(name), " ") | filter((w) => re.test(w, "^[A-Za-z0-9]")) | take(2) | map((w) => upper(slice(w, 0, 1))) | join("")// The last four letters or digits of an account number.fn payee_list_last4(number) -> str = slice(re.replace(str(number ?? ""), "[^0-9A-Za-z]", ""), -4)// An amount as text in its currency.fn payee_list_money(amount, currency: str, locale: str) -> str { let m = money.of(dec(amount), currency, "half_up") is_err(m) ? str(amount) : money.format(m, locale)}component PayeeList(payees: list = [], currency: str = "USD", locale: str = "en-US", on_pay = null, pay_label: str = "Pay", label: str? = "Payees", search: bool = true, add_href: str? = null, empty: str = "No payees match.", id: str? = null, class: str? = null) { style global payee_list_css state q = "" let code = upper(currency) let needle = lower(trim(q)) let shown = needle == "" ? payees : filter(payees, (p) => contains(lower(p.name), needle) || contains(lower(p.bank ?? ""), needle) || (len(needle) >= 2 && contains(payee_list_last4(p.number), needle))) <section class={ui_cls(["ui-payees", class])} id={id} aria-label={label ?? "Payees"}> {(label != null || add_href != null || search) && <div.ui-payees-top> {label != null && <h3.ui-payees-title>{label}</h3>} {add_href != null && <a.ui-payees-add href={add_href}><span aria-hidden="true">+</span> Add payee</a>} {search && <div.ui-payees-search role="search"> <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><circle cx="7" cy="7" r="5"/><path d="m11 11 3.5 3.5"/></svg> <input type="search" bind:value={q} placeholder="Search payees" aria-label="Search payees" autocomplete="off"/> </div>} </div>} <p.ui-sr role="status">{needle == "" ? "" : `${len(shown)} ${len(shown) == 1 ? "payee" : "payees"} found`}</p> {len(shown) == 0 ? <p.ui-payees-empty>{len(payees) == 0 ? "No payees yet." : empty}</p> : <ul.ui-payees-list> {for p in shown { let last4 = payee_list_last4(p.number) let pid = str(p.id) <li.ui-payees-item key={pid} data-payee={pid}> <span.ui-payees-initials aria-hidden="true">{payee_list_initials(p.name)}</span> <div.ui-payees-who> <span.ui-payees-name>{p.name}</span> {(p.bank != null || last4 != "") && <span.ui-payees-bank> {p.bank}{p.bank != null && last4 != "" && " · "} {last4 != "" && [<span.ui-payees-num aria-hidden="true">•••• {last4}</span>, <span.ui-sr>account ending in {last4}</span>]} </span>} {p.reference != null && <span.ui-payees-bank>Ref. {p.reference}</span>} <span.ui-payees-last>{p.last_amount != null ? [<span>Last paid </span>, <b>{payee_list_money(p.last_amount, code, locale)}</b>, p.last_date != null && <span> on <time datetime={p.last_date}>{date.format(p.last_date, { locale, style: "medium" })}</time></span>] : "Not paid yet"}</span> </div> {p.href != null ? <a.ui-payees-pay href={p.href} aria-label={`${pay_label} ${p.name}`}>{pay_label}</a> : <button.ui-payees-pay type="button" aria-label={`${pay_label} ${p.name}`} @click={() => { on_pay && on_pay(p.id) }}>{pay_label}</button>} </li> }} </ul>} </section>}Related
TransactionListTransactions grouped by day with category, money in and out, the running balance, pending ones marked, and a search that filters as you type.7 views · 6 downloadsBillListUpcoming bills soonest first, overdue and due-soon ones marked, autopay on or off, with the total to pay and a Pay button each.7 views · 5 downloadsAccountCardA bank account at a glance: type, nickname, masked number, current and available balance, what is on hold, and quick actions.8 views · 7 downloadsAccountDetailsThe details to share to get paid: account name and number, IBAN in groups of four (checked), BIC, sort code or routing number, each with a copy button.6 views · 6 downloadsAccountPickerChoose the account to pay from as radio cards with each one's masked number and available balance, those short of the amount marked.7 views · 7 downloadsBankStatementA monthly statement that prints: the period, opening and closing balance, totals paid in and out, and each transaction with the balance after.7 views · 6 downloads
Thread
Thread 0 posts
On PayeeList: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.