AppShell
An app's frame: a sidebar of links, a top bar and the page; the sidebar is a drawer on phones.
Demo
Overview
The page goes here. The sidebar stays put as it scrolls.
Paragraph 1 of the page, to give it some length.
Paragraph 2 of the page, to give it some length.
Paragraph 3 of the page, to give it some length.
Paragraph 4 of the page, to give it some length.
Paragraph 5 of the page, to give it some length.
When it is narrow, the sidebar becomes a drawer, opened by the menu button:
Overview
The page goes here. The sidebar stays put as it scrolls.
Paragraph 1 of the page, to give it some length.
Paragraph 2 of the page, to give it some length.
Paragraph 3 of the page, to give it some length.
Paragraph 4 of the page, to give it some length.
Paragraph 5 of the page, to give it some length.
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/app_shell.qweimport ui/theme.qwe use theme_cssuse ui/app_shell.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
let app_shell_demo_nav = [{ label: "Overview", href: "/", icon: "◎" }, { label: "Projects", href: "#app_shell", icon: "▤" }, { label: "Team", href: "#team", icon: "☺" }, { label: "Billing", href: "#billing", icon: "▭" }]view AppShellDemoPage() = <div> <h2 style="margin: 0 0 6px; font-size: 20px">Overview</h2> <p style="margin: 0; color: var(--ui-muted)">The page goes here. The sidebar stays put as it scrolls.</p> {for i in range(1, 6) => <p>Paragraph {i} of the page, to give it some length.</p>} </div>view AppShellDemo() = <div.demo-stack> <div style="--ui-shell-height: 340px; height: 340px; overflow: auto; border: 1px solid var(--ui-border); border-radius: var(--ui-radius); transform: translateZ(0)"> <AppShell id="app-shell-demo" brand="Acme" nav={app_shell_demo_nav} current="/" header={[<b>Overview</b>, <Button size="sm" variant="outline">Invite</Button>]}> <AppShellDemoPage/> </AppShell> </div> <p.demo-note>When it is narrow, the sidebar becomes a drawer, opened by the menu button:</p> <div style="--ui-shell-height: 340px; width: min(380px, 100%); height: 340px; overflow: auto; border: 1px solid var(--ui-border); border-radius: var(--ui-radius); transform: translateZ(0)"> <AppShell id="app-shell-demo-narrow" brand="Acme" nav={app_shell_demo_nav} current="/" header={[<b>Overview</b>, <Button size="sm" variant="outline">Invite</Button>]}> <AppShellDemoPage/> </AppShell> </div> </div>Source
// AppShell: the frame of an app's pages: a sidebar of links on the left, a bar// across the top and the page in the rest. When it is narrow (under 760px) the// sidebar hides and a menu button opens it as a drawer over the page, with the// browser's `popover`: Escape or a click beside it closes it, and no script runs.//// <AppShell brand="Acme" current={request.path}// nav={[{ label: "Overview", href: "/", icon: "◎" }, { label: "Billing", href: "/billing" }]}// header={<Button size="sm" variant="outline">Invite</Button>}>// <h1>Overview</h1> ...// </AppShell>//// brand, brand_href ("/"): the app's name at the top of the sidebar.// nav: a list of { label, href, icon? }; icon is text (an emoji) or HTML (an <svg>).// The one whose href is `current` is marked aria-current="page".// sidebar: HTML for the sidebar, after the nav (or instead of it).// header: HTML for the top bar (a title, a search, actions: the first at its left,// the last at its right). children: the page.// id: what the drawer is named by ("ui-shell"); give each shell on a page its own.// It fills the window's height; set --ui-shell-height to make it another.import ui/theme.qwe use ui_clslet app_shell_css = css`.ui-shell { container: ui-shell / inline-size; background: var(--ui-bg); color: var(--ui-fg); font: 15px/1.5 var(--ui-font); }.ui-shell-in { display: grid; grid-template-columns: 240px minmax(0, 1fr); min-height: var(--ui-shell-height, 100vh); }.ui-shell-side { position: sticky; top: 0; inset-inline: auto; bottom: auto; display: flex; flex-direction: column; gap: 4px; width: auto; height: var(--ui-shell-height, 100vh); margin: 0; padding: 14px 12px; overflow: auto; border: 0; border-right: 1px solid var(--ui-border); background: var(--ui-subtle); color: var(--ui-fg);}.ui-shell-brand { display: flex; align-items: center; justify-content: space-between; min-height: 36px; padding: 0 8px 10px; }.ui-shell-brand a { color: var(--ui-fg); font-weight: 650; font-size: 16px; text-decoration: none; border-radius: var(--ui-radius-sm); }.ui-shell-side ul { display: grid; gap: 2px; margin: 0; padding: 0; list-style: none; }.ui-shell-side li a { display: flex; align-items: center; gap: 10px; padding: 7px 10px; border-radius: var(--ui-radius-sm); color: var(--ui-muted); font-size: 14px; font-weight: 500; text-decoration: none;}.ui-shell-side li a:hover { color: var(--ui-fg); background: color-mix(in srgb, var(--ui-fg) 6%, transparent); }.ui-shell-side li a[aria-current="page"] { color: var(--ui-accent); background: var(--ui-accent-soft); }.ui-shell-icon { display: inline-grid; place-items: center; width: 18px; flex: none; }.ui-shell-icon svg { width: 18px; height: 18px; }.ui-shell-main { display: flex; flex-direction: column; min-width: 0; }.ui-shell-header { position: sticky; top: 0; z-index: 5; display: flex; align-items: center; gap: 10px; min-height: 56px; padding: 0 20px; border-bottom: 1px solid var(--ui-border); background: var(--ui-bg);}.ui-shell-bar { display: flex; flex: 1; align-items: center; justify-content: space-between; gap: 8px; min-width: 0; }.ui-shell-hbrand { display: none; color: var(--ui-fg); font-weight: 650; text-decoration: none; }.ui-shell-content { flex: 1; padding: 24px; }.ui-shell-btn { display: none; place-items: center; flex: none; width: 36px; height: 36px; padding: 0; border: 0; border-radius: var(--ui-radius-sm); background: transparent; color: var(--ui-fg); cursor: pointer;}.ui-shell-btn:hover { background: var(--ui-subtle); }.ui-shell a:focus-visible, .ui-shell-btn:focus-visible { outline: none; box-shadow: var(--ui-focus); }@container ui-shell (max-width: 760px) { .ui-shell-in { grid-template-columns: minmax(0, 1fr); } .ui-shell-btn, .ui-shell-hbrand { display: inline-grid; } .ui-shell-header { padding: 0 12px; } .ui-shell-content { padding: 18px 16px; } .ui-shell-side:not(:popover-open) { display: none; } .ui-shell-side:popover-open { position: fixed; inset: 0 auto 0 0; width: min(290px, 85vw); height: 100%; max-height: none; box-shadow: var(--ui-shadow-lg); animation: ui-shell-in .2s ease-out; } .ui-shell-side::backdrop { background: var(--ui-overlay); }}@keyframes ui-shell-in { from { transform: translateX(-100%); } }@media (prefers-reduced-motion: reduce) { .ui-shell-side:popover-open { animation: none; } }`view AppShell(brand: str = "", brand_href: str = "/", nav: list = [], current: str? = null, sidebar = null, header = null, id: str = "ui-shell", label: str = "Main", class: str? = null, children) { style global app_shell_css let side = `${id}-side` <div class={ui_cls(["ui-shell", class])}><div.ui-shell-in> <aside.ui-shell-side id={side} popover="auto"> <div.ui-shell-brand> {brand != "" && <a href={brand_href}>{brand}</a>} <button.ui-shell-btn type="button" popovertarget={side} popovertargetaction="hide" aria-label="Close menu"> <svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg> </button> </div> {len(nav) > 0 && <nav aria-label={label}><ul> {for n in nav => <li><a href={n.href} aria-current={current != null && n.href == current ? "page" : null}> {get(n, "icon") != null && <span.ui-shell-icon aria-hidden="true">{get(n, "icon")}</span>} <span>{n.label}</span> </a></li>} </ul></nav>} {sidebar} </aside> <div.ui-shell-main> <header.ui-shell-header> <button.ui-shell-btn type="button" popovertarget={side} aria-label="Open menu"> <svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M4 6h16M4 12h16M4 18h16"/></svg> </button> {brand != "" && <a.ui-shell-hbrand href={brand_href}>{brand}</a>} <div.ui-shell-bar>{header}</div> </header> <main.ui-shell-content>{children}</main> </div> </div></div>}Related
Thread 0 posts
On AppShell: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.