AppShell
The application shell, driven by **one recursive structure tree**: section nodes render as top-bar tabs (show:'dropdown' opens a menu of children, show:'sidebar' renders them in the left sidebar, show:'drawer' takes NO top-bar tab at all and appears only inside the mobile drawer); inside sections live switcher (workspace/profile picker whose selected child supplies the nav below it), search (filters the items after it), group, item (nesting freely — the chevron expands independently of the link), and divider (vertical in the top bar, horizontal in the sidebar). A switcher placed at the top level of structure is the scope switcher: a compact pill in the top bar whose selected child supplies the whole sidebar, so choosing a scope changes the entire left-hand nav — an explicit sidebar prop still overrides it. On mobile the pill moves into the drawer at full width. A section with search:true enables the global top-bar search while active (or force it with showSearchOverride). Give the search something to find with searchGroups — results appear from the first keystroke, grouped by type, matched on title + sub with accents folded; onSearch fires per keystroke for a server-backed search, and onSearchSelect reports the chosen row. Selections bind via bind:activeSection / bind:selections; the current page is marked active:true in the data; clicks fire onSelect(node). Responsive via container query — below the breakpoint everything collapses into a hamburger drawer where sections become an accordion with the active one expanded. Layout: the shell scrolls at PAGE level, not in an inner pane — the sidebar is position: sticky under the top bar and gets its own scrollbar only when its items overflow. Page-level scroll was chosen over an independently scrolling content pane because it keeps the browser's own scroll restoration, find-in-page and anchor links working, and it is the less surprising default; an inner pane would also bury the footer where only the pane's scrollbar reaches it. The footer snippet closes the CONTENT column, so with a sidebar it starts where the content starts, and with no sidebar it is full width. Full-page demos in context: the patient, doctor and admin rooms under @mocks.
Props
show:'sidebar' children when set.href, shows an unread count badge. Used when the host has a notifications page instead of inline items. Ignored if notifications has entries.onSearch and swap this array as results arrive — the component only renders what it is given.search). Use for a global search.user (e.g. sign in). Events
url).