Menu
A vertical list of links and actions in titled sections, with icons, shortcuts, counts, disabled items and the active one marked.
Demo
Picked: nothing yet
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/menu.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/menu.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/menu.qwe
component MenuDemoLive() { state did = "nothing yet" <div.demo-stack> <Menu id="menu-demo-live" label="Actions" bordered={true} size="sm" items={[ { label: "Rename", shortcut: "F2", icon: "M4 20h4L18.5 9.5a2.1 2.1 0 0 0-3-3L5 17z" }, { label: "Duplicate", shortcut: "⌘D", icon: "M8 8h12v12H8zM4 16V4h12" }, { label: "Archive", disabled: true }, { divider: true }, { label: "Delete", shortcut: "⌫", tone: "danger", icon: "M4 7h16M9 7V4h6v3M6 7l1 13h10l1-13" } ]} on_select={(it) => { did = it.label }}/> <p.demo-note>Picked: {did}</p> </div>}view MenuDemo() = <div.demo-row style="align-items: flex-start; gap: 32px"> <div style="width: 240px"> <Menu id="menu-demo" label="Settings" active="#menu" items={[ { section: "Account" }, { label: "Profile", href: "#profile", icon: "M16 19v-1a4 4 0 0 0-8 0v1M12 11a3 3 0 1 0 0-6 3 3 0 0 0 0 6" }, { label: "Notifications", href: "#menu", icon: "M6 16V11a6 6 0 0 1 12 0v5l2 2H4zM10 21h4", badge: 3 }, { label: "Billing", href: "#billing", icon: "M3 7h18v10H3zM3 11h18" }, { section: "Workspace" }, { label: "Members", href: "#members", icon: "👥", badge: 12 }, { label: "Integrations", href: "#integrations", icon: "🔌", disabled: true }, { divider: true }, { label: "Sign out", href: "#signout", tone: "danger", icon: "M10 17l5-5-5-5M15 12H3M21 4v16" } ]}/> </div> <div style="width: 240px"><MenuDemoLive/></div> </div>Source
ui/menu.qwe
// Menu: a vertical list of links and actions, in titled sections, each with an icon,// a shortcut or a count where it has one; items can be disabled, and the active one is// marked. For a settings page's side list, an account menu, a list of commands.//// <Menu label="Settings" active="/settings/profile" items={[// { section: "Account" },// { label: "Profile", href: "/settings/profile", icon: "👤" },// { label: "Billing", href: "/settings/billing", icon: "M3 7h18v10H3zM3 11h18", badge: 2 },// { divider: true },// { label: "Sign out", shortcut: "⇧⌘Q", tone: "danger" }// ]}/>//// items: links { label, href, icon?, shortcut?, badge?, disabled?, tone?: "danger" }; an// item without an href is a button that, inside one of your components, calls// on_select(item). { section: "Title" } starts a titled section of the items after it;// { divider: true } draws a line. icon: a character or an emoji, or an SVG path (`d`,// 24x24, stroked) when it starts with "M". shortcut: shown at the right ("⌘S"), and// told to screen readers as aria-keyshortcuts. badge: a count or a short text.// active: the href (or, for a button, the label) of the item marked current.// label: what screen readers call it ("Menu"). size: "sm" or "md" (default).// bordered: a box around it. on_select(item): see items.import ui/theme.qwe use ui_clslet menu_css = css`.ui-menu { display: grid; gap: 4px; min-width: 0; padding: 4px; font: 14px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-menu-bordered { border: 1px solid var(--ui-border); border-radius: var(--ui-radius); background: var(--ui-surface); }.ui-menu ul { display: grid; gap: 1px; margin: 0; padding: 0; list-style: none; }.ui-menu-section + .ui-menu-section { margin-top: 6px; }.ui-menu-title { margin: 0; padding: 6px 10px 4px; color: var(--ui-muted); font-size: 12px; font-weight: 600; letter-spacing: .05em; text-transform: uppercase;}.ui-menu-item { display: flex; align-items: center; gap: 10px; width: 100%; min-height: 34px; padding: 6px 10px; border: 0; border-radius: var(--ui-radius-sm); background: transparent; color: var(--ui-fg); font: inherit; font-weight: 500; text-align: left; text-decoration: none; cursor: pointer; transition: background .12s, color .12s;}.ui-menu-item:hover { background: var(--ui-subtle); }.ui-menu-item:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-menu-item[aria-current] { color: var(--ui-accent); background: var(--ui-accent-soft); }.ui-menu-item[aria-disabled="true"], .ui-menu-item:disabled { opacity: .5; cursor: not-allowed; pointer-events: none; }.ui-menu-danger { color: var(--ui-danger); }.ui-menu-danger:hover { background: var(--ui-danger-soft); }.ui-menu-icon { display: inline-grid; place-items: center; flex: none; width: 20px; height: 20px; font-size: 16px; line-height: 1; }.ui-menu-icon svg { width: 18px; height: 18px; }.ui-menu-text { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }.ui-menu-key { flex: none; color: var(--ui-muted); font-size: 12px; letter-spacing: .02em; white-space: nowrap; }.ui-menu-item[aria-current] .ui-menu-key { color: inherit; opacity: .8; }.ui-menu-badge { flex: none; min-width: 20px; padding: 1px 7px; border-radius: 999px; border: 1px solid var(--ui-border); background: var(--ui-surface); color: var(--ui-fg); font-size: 12px; font-weight: 600; text-align: center; font-variant-numeric: tabular-nums;}.ui-menu-item[aria-current] .ui-menu-badge { border-color: transparent; background: var(--ui-accent); color: var(--ui-accent-fg); }.ui-menu-sep { height: 1px; margin: 5px 6px; background: var(--ui-border); }.ui-menu-sm .ui-menu-item { min-height: 30px; padding: 4px 8px; font-size: 13px; }.ui-menu-sm .ui-menu-title { font-size: 11px; }@media (prefers-reduced-motion: reduce) { .ui-menu-item { transition: none; } }`// The items cut into sections at each { section }: [{ title, items }].fn menu_sections(items: list) -> list = reduce(items, [], (acc, it) => get(it, "section") != null ? acc | push({ title: it.section, items: [] }) : len(acc) == 0 ? [{ title: null, items: [it] }] : slice(acc, 0, len(acc) - 1) | push({ title: acc[-1].title, items: acc[-1].items | push(it) }))fn menu_icon(icon: str?) = icon != null && <span.ui-menu-icon aria-hidden="true">{ starts_with(icon, "M") && len(icon) > 2 ? <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d={icon}/></svg> : icon}</span>fn menu_body(it) = [ menu_icon(get(it, "icon")), <span.ui-menu-text>{it.label}</span>, get(it, "badge") != null && <span.ui-menu-badge>{it.badge}</span>, get(it, "shortcut") != null && <span.ui-menu-key aria-hidden="true">{it.shortcut}</span>,]fn menu_item(it, active: str?, on_select) { let cls = ui_cls(["ui-menu-item", get(it, "tone") == "danger" && "ui-menu-danger"]) let off = get(it, "disabled") == true let keys = get(it, "shortcut") != null ? replace(it.shortcut, " ", "+") : null get(it, "href") != null ? <a class={cls} href={it.href} aria-current={active != null && it.href == active ? "page" : null} aria-disabled={off ? "true" : null} tabindex={off ? "-1" : null} aria-keyshortcuts={keys}>{menu_body(it)}</a> : <button class={cls} type="button" disabled={off} aria-keyshortcuts={keys} aria-current={active != null && it.label == active ? "true" : null} @click={() => { on_select && on_select(it) }}>{menu_body(it)}</button>}view Menu(items: list = [], active: str? = null, label: str = "Menu", size: str = "md", bordered: bool = false, id: str? = null, on_select = null, class: str? = null) { style global menu_css <nav class={ui_cls(["ui-menu", size == "sm" && "ui-menu-sm", bordered && "ui-menu-bordered", class])} id={id} aria-label={label}> {for [i, s] in enumerate(menu_sections(items)) => <div.ui-menu-section> {s.title != null && <p.ui-menu-title id={id != null ? `${id}-s${i}` : null}>{s.title}</p>} <ul aria-label={s.title}> {for it in s.items => get(it, "divider") == true ? <li.ui-menu-sep role="separator"></li> : <li>{menu_item(it, active, on_select)}</li>} </ul> </div>} </nav>}Related
AlphabetIndexA to Z links into a list sorted by name; letters with nothing under them stay in place, disabled.4 views · 6 downloadsCollapsibleNavSide navigation in groups that fold under their titles, the browser remembering which are open.6 views · 7 downloadsDirectoryNavA long list of names grouped under sticky letter headers, with a row of letters at the top that jumps to each group.5 views · 5 downloadsLinkListA plain list of links, each with a line saying where it leads, an icon and a short note at the right.5 views · 6 downloadsMegaMenuA top bar whose items open wide panels of grouped links under the whole bar, on hover and from the keyboard, with no script.5 views · 7 downloadsSitemapListA site's pages as a nested list of links, as deep as the site goes, flowing into columns on wide screens.4 views · 5 downloads
Thread
Thread 0 posts
On Menu: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.