Dashboard

SessionList

Signed-in devices with browser, system, place and time, this one marked, and Sign out for each or all the others.

#dashboard#security#settings#list#user#interactive

Demo

Preview
  • Chrome 130 on macOSThis deviceLisbon, Portugal85.244.1.2Active now
  • Safari on iOS 19Porto, PortugalOct 26, 2026, 20:03
  • Firefox 131 on Windows 11Madrid, Spain81.33.4.5Oct 14, 2026
  • Safari on iPadOS 19Sep 30, 2026, 07:45

Sign one out, or all the others.

  • Chrome 130 on macOSThis deviceLisbon, Portugal85.244.1.2Active now
  • Safari on iOS 19Porto, PortugalOct 26, 2026, 20:03

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

Example

The code of the preview above.

demos/session_list.qwe
let session_list_demo_sessions = [    { id: "s1", browser: "Chrome 130", os: "macOS", device: "desktop", place: "Lisbon, Portugal", ip: "85.244.1.2", time: "2026-10-28T09:12:00Z", current: true },    { id: "s2", browser: "Safari", os: "iOS 19", device: "mobile", place: "Porto, Portugal", time: "2026-10-26T20:03:00Z" },    { id: "s3", browser: "Firefox 131", os: "Windows 11", place: "Madrid, Spain", ip: "81.33.4.5", time: "2026-10-14" },    { id: "s4", browser: "Safari", os: "iPadOS 19", device: "tablet", time: "2026-09-30T07:45:00Z" },]component SessionListDemoLive() {    state ended = []    <div.demo-stack>        <SessionList sessions={session_list_demo_sessions} on_sign_out={(id) => { ended = ended | push(id) }}/>        <p.demo-note>{len(ended) == 0 ? "Sign one out, or all the others." : `Signed out: ${join(ended, ", ")}.`}</p>    </div>}view SessionListDemo() =    <div.demo-stack>        <SessionListDemoLive/>        <SessionList sign_out_action="#session_list" sessions={take(session_list_demo_sessions, 2)}/>    </div>

Source

ui/session_list.qwe
// SessionList: where someone is signed in: each device with its browser and// system, where and when, this one marked, and a Sign out on each of the// others, plus one for all of them at once.////   <SessionList sign_out_action="/settings/sessions/sign-out" sessions={[//       { id: "s1", browser: "Chrome 130", os: "macOS", device: "desktop", place: "Lisbon, Portugal", ip: "85.244.1.2", time: "2026-10-28T09:12:00Z", current: true },//       { id: "s2", browser: "Safari", os: "iOS 19", device: "mobile", place: "Porto, Portugal", time: "2026-10-26T20:03:00Z" },//   ]}/>//   <SessionList sessions={sessions} on_sign_out={(id) => { end(id) }}/>//// sessions: a list of { id, browser, os?, device?, place?, ip?, time?, current? }.// device: "desktop" (default), "mobile" or "tablet", for the icon. time: an ISO// date-time of the last activity, shown as "Oct 28, 2026, 09:12" (other text as// it is); the current session says "This device" instead. Signing out:// sign_out_action makes the list a <form method="post"> to that address; each// Sign out sends `sign_out` = the session's id, and "Sign out all other devices"// sends `sign_out` = "others". Inside one of your components, on_sign_out(id)// is called instead ("others" for all of them), and the rows go. label: the// list's name ("Sessions").import ui/theme.qwe use ui_clslet session_list_css = css`.ui-sessions { display: grid; gap: 12px; margin: 0; min-width: 0; font: 14px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-sessions-list { margin: 0; padding: 0; list-style: none; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface); }.ui-sessions-row { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 2px 14px; align-items: center; padding: 12px 16px; }.ui-sessions-row + .ui-sessions-row { border-top: 1px solid var(--ui-border); }.ui-sessions-icon { display: grid; place-items: center; width: 40px; height: 40px; border-radius: var(--ui-radius-sm); background: var(--ui-subtle); color: var(--ui-muted); }.ui-sessions-icon svg { width: 20px; height: 20px; }.ui-sessions-current .ui-sessions-icon { background: var(--ui-success-soft); color: var(--ui-success); }.ui-sessions-what { display: grid; gap: 1px; min-width: 0; }.ui-sessions-name { font-weight: 600; }.ui-sessions-this { display: inline-block; margin-left: 8px; padding: 1px 7px; border-radius: 999px; background: var(--ui-success-soft); color: var(--ui-success); font-size: 11px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; }.ui-sessions-meta { color: var(--ui-muted); font-size: 13px; }.ui-sessions-meta span + span::before { content: " · "; }.ui-sessions-ip { font-family: var(--ui-mono); font-size: 12px; }.ui-sessions-btn { height: 30px; padding: 0 11px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface);  color: var(--ui-fg); font: 500 13px/1 var(--ui-font); cursor: pointer; white-space: nowrap; }.ui-sessions-btn:hover { border-color: var(--ui-danger); background: var(--ui-danger-soft); color: var(--ui-danger); }.ui-sessions-btn:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-sessions-all { justify-self: start; }.ui-sessions-empty { padding: 24px 16px; border: 1px dashed var(--ui-border); border-radius: var(--ui-radius-sm); color: var(--ui-muted); text-align: center; }@media (max-width: 480px) { .ui-sessions-row { grid-template-columns: auto minmax(0, 1fr); } .ui-sessions-row .ui-sessions-btn { grid-column: 2; justify-self: start; margin-top: 6px; } }`let session_list_months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]// An ISO date-time as "Oct 28, 2026, 09:12" (a date alone without the time); other text as it is.fn session_list_time(v) -> str {    let s = str(v)    let p = split(slice(s, 0, 10), "-")    if len(p) != 3 || len(p[0]) != 4 { return s }    let day = `${session_list_months[(int(p[1]) ?? 1) - 1]} ${int(p[2]) ?? p[2]}, ${p[0]}`    len(s) >= 16 && (s[10] == "T" || s[10] == " ") ? `${day}, ${slice(s, 11, 16)}` : day}// The device's icon: a laptop, a phone or a tablet.fn session_list_icon(device: str) {    let body = match device {        "mobile" => [<rect x="7" y="2" width="10" height="20" rx="2"/>, <path d="M11 18h2"/>]        "tablet" => [<rect x="4" y="2" width="16" height="20" rx="2"/>, <path d="M11 18h2"/>]        _ => [<rect x="3" y="4" width="18" height="12" rx="2"/>, <path d="M2 20h20"/>]    }    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">{body}</svg>}component SessionList(sessions: list = [], sign_out_action: str? = null, on_sign_out = null, label: str = "Sessions", class: str? = null) {    style global session_list_css    state gone = []    let rows = sessions | filter((s) => !contains(gone, s.id))    let others = rows | filter((s) => s.current != true)    let end = (id) => {        gone = id == "others" ? gone + (others | map((s) => s.id)) : gone | push(id)        on_sign_out(id)    }    let button = (id: str, text: str, cls: str) => on_sign_out != null        ? <button class={cls} type="button" @click={() => { end(id) }}>{text}</button>        : <button class={cls} type="submit" name="sign_out" value={id}>{text}</button>    let body = [        len(rows) == 0            ? <p.ui-sessions-empty>No sessions.</p>            : <ul.ui-sessions-list aria-label={label}>                {for s in rows => <li class={ui_cls(["ui-sessions-row", s.current == true && "ui-sessions-current"])} key={s.id}>                    <span.ui-sessions-icon>{session_list_icon(s.device ?? "desktop")}</span>                    <span.ui-sessions-what>                        <span.ui-sessions-name>                            {s.browser}{s.os != null && ` on ${s.os}`}                            {s.current == true && <span.ui-sessions-this>This device</span>}                        </span>                        <span.ui-sessions-meta>                            {s.place != null && <span>{s.place}</span>}                            {s.ip != null && <span.ui-sessions-ip>{s.ip}</span>}                            {s.current == true ? <span>Active now</span> : (s.time != null && <span>{session_list_time(s.time)}</span>)}                        </span>                    </span>                    {s.current != true && button(str(s.id), "Sign out", "ui-sessions-btn")}                </li>}            </ul>,        len(others) > 0 && button("others", `Sign out all other devices (${len(others)})`, "ui-sessions-btn ui-sessions-all"),    ]    sign_out_action != null && on_sign_out == null        ? <form class={ui_cls(["ui-sessions", class])} method="post" action={sign_out_action}>{body}</form>        : <div class={ui_cls(["ui-sessions", class])}>{body}</div>}
Thread
65Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.