MedLib UI

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

Name
Details
Default
Control
brand
string
MedLib
brandHref
string
#
structure
NavNode[]
[]
The whole navigation: sections (top bar) with nested switchers/groups/items (sidebar).
sidebar
NavNode[]
A persistent left sidebar, independent of the top-bar sections (for cockpit scopes like MedOS/Admin that have no section tabs). Takes precedence over a section's own show:'sidebar' children when set.
activeSection
string
$bindable(structure.find((n) => n.type === 'section' && n.show !== 'drawer')?.id ?? '')
Active section id (bindable).
selections
Record<string, string>
$bindable({})
Per-switcher selection: { [switcherId]: selectedChildId } (bindable).
user
UserConfig
notifications
NotifItem[]
[]
bell
BellLink
Bell without a dropdown: navigates to href, shows an unread count badge. Used when the host has a notifications page instead of inline items. Ignored if notifications has entries.
searchPlaceholder
string
Caută
searchGroups
SearchGroup[]
[]
Searchable data, grouped by type. AppShell filters these client-side on title + sub, which is all a mock or a small dataset needs. For a server-backed search, listen to onSearch and swap this array as results arrive — the component only renders what it is given.
showSearchOverride
boolean
Whether the top-bar search shows (overrides per-section search). Use for a global search.
signIn
{ label: string; href: string }
Right-side call to action when there's no user (e.g. sign in).

Events

Name
Details
onSearch
(query: string) => void
Fires on every keystroke (trimmed). Use it to fetch.
onSearchSelect
(result: SearchResult, group: SearchGroup) => void
A result was chosen (click or Enter).
onSelect
(node: NavNode) => void
Fired when any section/item/switcher-child is clicked.
onUserMenu
(item: UserMenuItem) => void
Fired when an account-menu row is clicked (also for rows with url).

Snippets

Name
Details
children
Snippet
footer
Snippet
Optional footer below the content (e.g. the marketing footer). It spans the CONTENT column, not the shell: with a sidebar it starts where the content starts, and with no sidebar (the signed-out public shell) that is the full width anyway. A footer closes the content it belongs to, and a sidebar is not content — running it underneath the sidebar also broke the sidebar's sticky pin.

Examples

Top-bar search — results grouped by type

Scope switcher — a top-level switcher in the top bar

show:'drawer' — reachable on mobile, free on desktop