NotificationCenter
A bell with the unread count, opening a panel of notifications to filter and mark read, one by one or all at once.
Demo
Notifications
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/notification_center.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/notification_center.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/notification_center.qwe
component NotificationCenterDemoLive() { state did = "Nothing read yet." <div.demo-row> <NotificationCenter id="nc-demo" all_href="#notification_center" items={[ { id: 1, title: "Ana commented on Invoice #1042", text: "Looks good to me, sending it today.", time: "2 min ago" }, { id: 2, title: "Payment received", text: "€1,240.00 from Northwind.", time: "1 h ago", href: "#notification_center" }, { id: 3, title: "Ben invited you to Design", time: "Yesterday" }, { id: 4, title: "Your export is ready", time: "2 days ago", read: true }, ]} on_read={(i) => { did = `Read ${i}.` }} on_read_all={() => { did = "Read all." }}/> <span.demo-note>{did}</span> </div>}view NotificationCenterDemo() = <div.demo-stack> <NotificationCenterDemoLive/> <div.demo-row> <NotificationCenter id="nc-demo-empty" align="start" items={[{ id: 1, title: "Welcome aboard", time: "Last week", read: true }]}/> <span.demo-note>Nothing unread: no count on the bell.</span> </div> </div>Source
ui/notification_center.qwe
// NotificationCenter: a bell with the number of unread notifications on it, opening// a panel of them, newest first: unread ones marked with a dot, All and Unread to// filter, "Mark all as read", and each one marked read when opened. It is the// browser's `popover`, so Escape or a click elsewhere closes it; where the browser// can anchor it (CSS anchor positioning) it opens under the bell, else mid-screen.//// <NotificationCenter id="inbox" all_href="/notifications" items={[// { id: 1, title: "Ana commented on Invoice #1042", text: "Looks good to me.", time: "2 min ago", href: "/invoices/1042" },// { id: 2, title: "Your export is ready", time: "1 h ago", read: true },// ]} on_read={(id) => { mark_read(id) }} on_read_all={() => { mark_all_read() }}/>//// id (required): the panel's id, unique on the page. items: a list of { id, title,// text?, time? (as you want it shown), read?: true, href? }. An item with an href is// a link (opening it marks it read); one without is a button that marks it read.// label ("Notifications"): the bell's name and the panel's heading. empty ("You're all// caught up."): what the panel says when there is nothing to show. all_href: a "See// all" link at the bottom. align: "end" (default: the panel's right edge under the// bell's) or "start".// on_read(id): called when one is marked read; on_read_all(): when all are, inside// one of your components.import ui/theme.qwe use ui_clslet notification_center_css = css`.ui-notification-center-bell { position: relative; display: inline-grid; place-items: center; width: 38px; height: 38px; padding: 0; border: 1px solid transparent; border-radius: var(--ui-radius-sm); background: transparent; color: var(--ui-fg); cursor: pointer; vertical-align: middle;}.ui-notification-center-bell:hover { background: var(--ui-subtle); }.ui-notification-center-bell:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-notification-center-bell svg { width: 20px; height: 20px; }.ui-notification-center-badge { position: absolute; top: 3px; right: 2px; min-width: 18px; height: 18px; padding: 0 5px; border: 2px solid var(--ui-bg); border-radius: 999px; background: var(--ui-danger); color: #fff; font: 700 10px/14px var(--ui-font); text-align: center; box-sizing: border-box;}.ui-notification-center { box-sizing: border-box; width: min(380px, calc(100vw - 16px)); max-height: min(520px, calc(100dvh - 16px)); margin: auto; padding: 0; overflow: hidden; border: 1px solid var(--ui-border); border-radius: var(--ui-radius); background: var(--ui-surface); color: var(--ui-fg); box-shadow: var(--ui-shadow-lg); font: 14px/1.45 var(--ui-font);}.ui-notification-center:popover-open { display: flex; flex-direction: column; animation: ui-notification-center-in .12s ease-out; }@keyframes ui-notification-center-in { from { opacity: 0; transform: translateY(-4px); } }@supports (position-area: bottom) { .ui-notification-center { inset: auto; margin: 6px 0; position-area: bottom span-left; position-try-fallbacks: flip-block, flip-inline; } .ui-notification-center-start { position-area: bottom span-right; }}@supports not (position-area: bottom) { .ui-notification-center::backdrop { background: var(--ui-overlay); } }.ui-notification-center-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex: none; padding: 12px 14px 8px; }.ui-notification-center-head h2 { margin: 0; font-size: 15px; font-weight: 600; }.ui-notification-center-link { padding: 2px 4px; border: 0; border-radius: 4px; background: none; color: var(--ui-accent); font: 500 13px/1.3 var(--ui-font); text-decoration: none; cursor: pointer;}.ui-notification-center-link:hover { text-decoration: underline; }.ui-notification-center-link:disabled { color: var(--ui-muted); text-decoration: none; cursor: default; }.ui-notification-center-filter { display: flex; gap: 4px; flex: none; padding: 0 14px 8px; border-bottom: 1px solid var(--ui-border); }.ui-notification-center-tab { height: 28px; padding: 0 10px; border: 0; border-radius: 999px; background: transparent; color: var(--ui-muted); font: 500 13px/1 var(--ui-font); cursor: pointer;}.ui-notification-center-tab:hover { color: var(--ui-fg); background: var(--ui-subtle); }.ui-notification-center-tab[aria-pressed="true"] { background: var(--ui-accent-soft); color: var(--ui-accent); }.ui-notification-center-list { flex: 1; min-height: 0; margin: 0; padding: 4px; overflow: auto; list-style: none; overscroll-behavior: contain; }.ui-notification-center-item { position: relative; display: grid; gap: 2px; width: 100%; padding: 10px 12px 10px 26px; border: 0; border-radius: var(--ui-radius-sm); background: transparent; color: var(--ui-fg); font: inherit; text-align: left; text-decoration: none; cursor: pointer;}.ui-notification-center-item:hover { background: var(--ui-subtle); }.ui-notification-center-unread { background: color-mix(in srgb, var(--ui-accent-soft) 55%, transparent); }.ui-notification-center-unread::before { content: ""; position: absolute; left: 10px; top: 16px; width: 8px; height: 8px; border-radius: 50%; background: var(--ui-accent); }.ui-notification-center-title { font-weight: 500; }.ui-notification-center-read .ui-notification-center-title { font-weight: 400; color: var(--ui-muted); }.ui-notification-center-text { color: var(--ui-muted); font-size: 13px; }.ui-notification-center-time { color: var(--ui-muted); font-size: 12px; }.ui-notification-center-empty { margin: 0; padding: 32px 16px; color: var(--ui-muted); text-align: center; }.ui-notification-center-foot { flex: none; padding: 8px 14px; border-top: 1px solid var(--ui-border); text-align: center; }.ui-notification-center-tab:focus-visible, .ui-notification-center-item:focus-visible, .ui-notification-center-link:focus-visible { outline: none; box-shadow: var(--ui-focus); }@media (prefers-reduced-motion: reduce) { .ui-notification-center:popover-open { animation: none; } }`component NotificationCenter(id: str, items: list = [], label: str = "Notifications", empty: str = "You're all caught up.", all_href: str? = null, align: str = "end", on_read = null, on_read_all = null, class: str? = null) { style global notification_center_css state rows = items | map((it) => { ...it, read: get(it, "read") == true }) state only_unread = false let unread = len(rows | filter((r) => !r.read)) let shown = only_unread ? rows | filter((r) => !r.read) : rows let mark = (rid) => { if any(rows, (r) => r.id == rid && !r.read) { rows = rows | map((r) => r.id == rid ? { ...r, read: true } : r) on_read && on_read(rid) } } <span class={class} style={`anchor-name: --${id}`}> <button.ui-notification-center-bell type="button" popovertarget={id} aria-haspopup="dialog" aria-label={unread > 0 ? `${label}, ${unread} unread` : label} title={label}> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9M13.7 21a2 2 0 0 1-3.4 0"/></svg> {unread > 0 && <span.ui-notification-center-badge aria-hidden="true">{unread > 99 ? "99+" : unread}</span>} </button> <div id={id} popover="auto" class={ui_cls(["ui-notification-center", align == "start" && "ui-notification-center-start"])} role="dialog" aria-labelledby={`${id}-title`} style={`position-anchor: --${id}`}> <div.ui-notification-center-head> <h2 id={`${id}-title`}>{label}</h2> <button.ui-notification-center-link type="button" disabled={unread == 0} @click={() => { rows = rows | map((r) => { ...r, read: true }) on_read_all && on_read_all() }}>Mark all as read</button> </div> <div.ui-notification-center-filter role="group" aria-label="Show"> <button.ui-notification-center-tab type="button" aria-pressed={only_unread ? "false" : "true"} @click={() => { only_unread = false }}>All</button> <button.ui-notification-center-tab type="button" aria-pressed={only_unread ? "true" : "false"} @click={() => { only_unread = true }}> Unread{unread > 0 ? ` (${unread})` : ""} </button> </div> {len(shown) == 0 ? <p.ui-notification-center-empty>{empty}</p> : <ul.ui-notification-center-list aria-label={label}> {for r in shown { let cls = ui_cls(["ui-notification-center-item", r.read ? "ui-notification-center-read" : "ui-notification-center-unread"]) let body = [ <span.ui-notification-center-title>{r.title}{!r.read && <span.ui-sr>, unread</span>}</span>, get(r, "text") != null && <span.ui-notification-center-text>{r.text}</span>, get(r, "time") != null && <span.ui-notification-center-time>{r.time}</span>, ] <li key={r.id}>{get(r, "href") != null ? <a class={cls} href={r.href} @click={() => { mark(r.id) }}>{body}</a> : <button class={cls} type="button" @click={() => { mark(r.id) }}>{body}</button>}</li> }} </ul>} {all_href != null && <div.ui-notification-center-foot><a.ui-notification-center-link href={all_href}>See all</a></div>} </div> </span>}Related
FlyoutA panel that opens beside its button at a chosen side, keeps open while you work in it, and closes on a click outside.5 views · 10 downloadsHoverCardA card of details on hover or focus, after a short wait, that stays under the pointer; Escape hides it.5 views · 6 downloadsPopoverA small panel of anything beside a button, on the browser's popover: Escape and clicks outside close it.5 views · 5 downloadsCoachmarkA small callout pointing at an element by id, explaining it once, dismissed with Got it; several make a tour.6 views · 8 downloadsTooltipA short note shown while something is hovered or focused, in CSS alone, on any of its four sides.5 views · 10 downloadsAlertDialogA modal that only informs, with one OK button, on the browser's own <dialog> and no JavaScript.6 views · 7 downloads
Thread
Thread 0 posts
On NotificationCenter: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.