{# The application shell's side column — 0.5's first piece. A back office outgrows a header bar at about six destinations: the row runs out of width, and grouping ("Workspace" / "Admin") has nowhere to live. That is where an app without a sidebar component starts writing `fixed inset-y-0 w-64 border-r` by hand, and the closed vocabulary is gone on page one. Basecoat already ships `.sidebar`: the CSS is in the stylesheet whether or not anything renders it (CHARTER.md §7), and its JS is vendored. So this file is not a new widget but the fjkit-shaped door onto one already paid for — route names instead of hrefs, an icon by name, closed enumerations, and no class string anywhere in the signature. The markup contract belongs to Basecoat and is load-bearing: `aside.sidebar > nav > {header, section, footer}`, `[role=group] > ul > li`, and the next sibling of the aside is what gets the margin that keeps the page clear of it. `ui/shell.html` owns that sibling; an app calls these macros through the shell's `sidebar` block and never has to know. {% block sidebar %} {% set mark %}{{ brand("Acme", url_for(request, "home"), "gauge") }}{% endset %} {% call sidebar(header=mark) %} {% call sidebar_group("Workspace") %} {{ sidebar_link(request, "home", "Overview", icon_name="gauge") }} {% endcall %} {% endcall %} {% endblock %} #} {% from "ui/attrs.html" import attrs %} {% from "ui/icon.html" import icon %} {# sidebar(id, label, side, open, header, footer, **html_attrs) side: left | right `header` and `footer` take pre-rendered markup, like `card`'s `actions`: a call block runs its body once per slot, and the body here is the navigation — always present and always the biggest part. Rendering it twice to make room for a brand mark is the wrong trade. `open` is the desktop starting state only. On a narrow screen the sidebar always starts closed, because it renders there as a full-screen overlay and a page that opens covered by its own navigation is a bug, not a preference. Basecoat's script owns both states from then on: it sets `aria-hidden` and `inert` together, so a collapsed sidebar leaves the tab order rather than merely going invisible, and on a narrow screen it closes itself when a link inside it is followed. Without JavaScript, the server-rendered `aria-hidden="false"` plus Basecoat's `:not([data-sidebar-initialized])` rule leaves a wide screen fully navigable and hides the overlay on a narrow one. The links stay in the HTML either way, so a crawler and reader mode still see them. #} {% macro sidebar(id="sidebar", label="Main", side="left", open=true, header=none, footer=none) -%} {# Closed lookup, not interpolation: `data-side="{{ side }}"` would emit `data-side="middle"`, which the CSS has no rule for, and the sidebar would lose its border and its margin with nothing to say why. An unknown value falls back to the default instead. #} {%- set sides = {"left": "left", "right": "right"} -%}