Navigation Shell
The navigation shell is the app chrome that frames every page — a fixed top navbar, a mobile-only bottom tab bar, and the page-navigation sidebar with collapsible groups and an Arr scope selector. These are reusable Svelte components under $ui/ (source: packages/praxrr-app/src/lib/client/ui/). They follow the repo’s Svelte convention of plain props and onclick/on:click handlers rather than runes.
Navbar
Section titled “Navbar”Fixed top navigation bar (app chrome). On mobile it shows a hamburger that opens the mobile nav plus a centered latest-alert toast; on desktop it shows the praxrr logo and wordmark. The right side always renders the AccentPicker and ThemeToggle.
Import:
import Navbar from '$ui/navigation/navbar/navbar.svelte';This component takes no props.
Usage:
<script> import Navbar from '$ui/navigation/navbar/navbar.svelte';</script>
<Navbar />Notes:
- The hamburger button calls
mobileNavOpen.open()(from$stores/mobileNav) to open the mobile page nav. There are no callback props. - Depends on
$stores/mobileNav,$alerts/store($alertStore),$assets/logo.svg, and the child componentsAccentPickerandThemeToggle. - Reactively derives
latestAlertfrom the last item in$alertStore. Uses Svelte 4 style ($:andon:click).
Variants:
- Responsive — hamburger plus a centered
MobileNavAlerton mobile (<md); logo plus wordmark on desktop (md+). - Fixed,
z-50(md z-80), full width on mobile andw-80on desktop.
ThemeToggle
Section titled “ThemeToggle”Icon button that toggles light/dark theme with an animated cross-fade/rotate transition between two states. It renders Lucide icons (MoonStar/Sun) or emoji (✨/💡) depending on the nav icon preference.
Import:
import ThemeToggle from '$ui/navigation/navbar/themeToggle.svelte';This component takes no props.
Usage:
<script> import ThemeToggle from '$ui/navigation/navbar/themeToggle.svelte';</script>
<ThemeToggle />Notes:
on:clickcallsthemeStore.toggle()(from$stores/theme). There are no callback props.- Reads
$themeStore(isDark = $themeStore === 'dark') and$navIconStore(useEmoji = $navIconStore === 'emoji'). Hasaria-label="Toggle theme".
Variants:
- Two icon modes driven by
$navIconStore—'emoji'(✨ dark / 💡 light) versus the default Lucide (MoonStardark /Sunlight). - Animated scale/rotate/opacity transition on toggle.
AccentPicker
Section titled “AccentPicker”Accent color picker — a swatch button that opens a Dropdown of available accent colors. Selecting one sets the global accent store; the current selection is marked with a Check icon.
Import:
import AccentPicker from '$ui/navigation/navbar/accentPicker.svelte';This component takes no props.
Usage:
<script> import AccentPicker from '$ui/navigation/navbar/accentPicker.svelte';</script>
<AccentPicker />Notes:
- The trigger button (
on:click|stopPropagation) togglesopen. Each color button callsaccentStore.set(accent)viaselect(). Asvelte:windowon:clickcloses the dropdown when clicking outside.accent-picker. There are no callback props. - Imports
accentStore,accentColors, and theAccentColortype from$stores/accent; uses$ui/dropdown/Dropdown.svelteandCheckfromlucide-svelte. currentColor = accentColors.find(c => c.value === $accentStore) ?? accentColors[0]. Internal state:open(boolean) andtriggerEl(HTMLElement, bound and passed toDropdown).
Variants:
- Open and closed states.
- Renders
Dropdownwithposition="middle",minWidth="auto",fixed={true}, anchored to the trigger element.
BottomNav
Section titled “BottomNav”Mobile-only fixed bottom tab bar. It flattens the nav shell’s grouped items into a single row, filters by the active Arr scope, sorts by mobilePriority, and renders icon-plus-short-label links with active-route highlighting.
Import:
import BottomNav from '$ui/navigation/bottomNav/BottomNav.svelte';Props:
| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
navShell |
NavShell | undefined |
undefined |
No | The resolved navigation shell (from $shared/navigation/types). Its groups[].items are flattened into bottom-nav entries. When undefined, no items render. |
Usage:
<script> import BottomNav from '$ui/navigation/bottomNav/BottomNav.svelte'; export let navShell;</script>
<BottomNav {navShell} />Notes:
- Items are plain
<a href>anchors — there are no events or callback props. - Reads
$navScopeand$navIconStoreplus$page.url.pathname. - Scope filtering via
isScopedItemVisible— scope'all'or norequiredFeaturemeans visible; otherwisesupportsFeature(scope, requiredFeature). shortLabelByHrefmaps known routes to abbreviated labels (for example/quality-profiles→'Profiles').- Items are sorted by
priorityOrder({ always: 0, medium: 1, low: 2 }) then original source order.isActiveis an exact match for'/', otherwisestartsWith(href).
Variants:
md:hidden(mobile only).- Per-item visibility is driven by
mobilePriority—'always'is always shown,'medium'is hidden until thesmbreakpoint,'low'is hidden. The active link uses the accent color; icons switch between Lucide (resolveNavIconbyiconKey) and emoji based on$navIconStore.
PageNav
Section titled “PageNav”The primary sidebar navigation shell. It renders the scope selector, collapsible nav groups with items, scope-disabled annotations, and the version footer. It slides in as an overlay on mobile and is a fixed left sidebar on desktop.
Import:
import PageNav from '$ui/navigation/pageNav/pageNav.svelte';Props:
| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
version |
string |
'' |
No | App version string forwarded to the Version footer component. |
navShell |
NavShell | undefined |
undefined |
No | The resolved navigation shell. Provides groups (each with id, label, items) and arrScopeOptions consumed by the embedded NavScopeSelector. |
Usage:
<script> import PageNav from '$ui/navigation/pageNav/pageNav.svelte'; export let navShell; export let version;</script>
<PageNav {navShell} {version} />Notes:
- Composes
Group,GroupItem,SectionHeader,NavScopeSelector, andVersion. - A
svelte:windowon:keydowncloses the mobile nav on Escape. The mobile backdrop button and close (X) button callmobileNavOpen.close(). The mobile nav also closes reactively on route change. There are no callback props. - Scope filtering (
resolveScopeEntries) — visible items pass through; unsupported leaf items are hidden; unsupported items with children render as disabled annotation cards. collapsedGroupIds = { settings, dev }andcollapsedItemIds = { 'policies.media_management' }start collapsed unless the current route matches.- Group labels prefix an emoji when
$navIconStore === 'emoji'.sectionLabelis passed only to the first item of each group. Reads$navScope,$navIconStore,$mobileNavOpen, and$page.
Variants:
- Mobile — a full-height overlay drawer (
w-90vw) that translates in/out based on$mobileNavOpen, with its own logo-plus-close header; version scrolls with content. - Desktop (
md+) — a fixedw-80sidebar below the navbar with version pinned to the bottom. - Groups render either
'visible'(interactiveGroup) or'disabled'(a dashed annotation card explaining scope unavailability).
A collapsible navigation group — it renders an optional SectionHeader, a GroupHeader (link plus chevron toggle), and a slide-animated list of child items connected by a vertical guide line.
Import:
import Group from '$ui/navigation/pageNav/group.svelte';Props:
| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
label |
string |
— |
Yes | Group header label text. |
href |
string |
— |
Yes | Destination href for the group header link. |
icon |
ComponentType | undefined |
undefined |
No | Optional Svelte icon component rendered in the header via svelte:component. |
initialOpen |
boolean |
true |
No | Initial and route-driven open state. When true the group is forced open and the chevron toggle is disabled. |
hasItems |
boolean |
false |
No | Whether the group has child items. Controls chevron rendering and whether the slotted children/guide line show. |
sectionLabel |
string | undefined |
undefined |
No | Optional section divider label rendered above the group via SectionHeader. |
Usage:
<Group label="Policies" href="/quality-profiles" icon={someIcon} sectionLabel="Configuration" initialOpen={true} hasItems={true}> <GroupItem label="Quality Profiles" href="/quality-profiles" /></Group>Notes:
- Default slot — the group’s child items (typically
GroupItemcomponents), rendered only whenisOpen && hasItems. - Internal
toggleOpen(bound toGroupHeaderonToggle) flipsisOpen, but is a no-op whileisRouteOpen(initialOpen) is true. No callback props are exposed. - Reactive —
isRouteOpenmirrorsinitialOpenand forcesisOpenopen when true (closes when false). ComposesGroupHeaderandSectionHeader; usessvelte/transitionslide.
Variants:
- Open and closed (slide transition, 200ms). When
initialOpen/isRouteOpenis true the group is locked open. - Optional leading
SectionHeader.
GroupHeader
Section titled “GroupHeader”The header row for a Group — a main navigation link (with optional icon) plus a separate chevron toggle button (only when the group has items). It handles active-state styling.
Import:
import GroupHeader from '$ui/navigation/pageNav/groupHeader.svelte';Props:
| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
label |
string |
— |
Yes | Header link text. |
href |
string |
— |
Yes | Destination href for the main link. |
icon |
ComponentType | undefined |
undefined |
No | Optional Svelte icon component rendered before the label via svelte:component. |
isOpen |
boolean |
— |
Yes | Whether the group is expanded; rotates the chevron and sets its aria-label (Collapse/Expand group). |
hasItems |
boolean |
— |
Yes | Whether to render the chevron toggle button and adjust the link’s border radius (rounded-l vs fully rounded). |
onToggle |
() => void |
— |
Yes | Callback invoked by the chevron button’s onclick to toggle the group open/closed. |
activePattern |
string | undefined |
undefined |
No | Optional active-state matcher: when set, active = pathname.includes(activePattern); otherwise exact match or pathname.startsWith(href + '/'). |
Usage:
<GroupHeader label="Settings" href="/settings" icon={settingsIcon} isOpen={open} hasItems={true} onToggle={() => (open = !open)}/>Notes:
- The chevron button uses
onclick={onToggle}(Svelte 5 styleonclickattribute). The main link is a plain<a href>.onToggleis the callback prop. isOpen,hasItems, andonToggleare required (no defaults). Reads$page.url.pathname. The chevron is an inline SVG (not a Lucide icon).- Typically rendered by
Grouprather than used directly.
Variants:
- Active versus inactive styling.
- With-items (link
rounded-lplus chevronrounded-r) versus no-items (fully rounded link, no chevron). The chevron rotates 90deg whenisOpen.
GroupItem
Section titled “GroupItem”A leaf navigation link (child of a Group) with active-state highlighting. It special-cases the logout route to clear UI preferences before navigating.
Import:
import GroupItem from '$ui/navigation/pageNav/groupItem.svelte';Props:
| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
label |
string |
— |
Yes | Link text. |
href |
string |
— |
Yes | Destination href. |
activePattern |
string | RegExp | undefined |
undefined |
No | Optional custom active-state matcher. String → pathname.includes(activePattern); RegExp → activePattern.test(pathname). When omitted, active = exact match or pathname.startsWith(href + '/'). |
Usage:
<GroupItem label="Custom Formats" href="/custom-formats" activePattern="/custom-formats"/>Notes:
on:clickhandleClick— forhref === '/auth/logout'it callspreventDefault, thenuserInterfacePreferencesStore.clearOnAuthChange(), thengoto(href). There are no callback props.- Reads
$page.url.pathnamefor active detection. Importsgotofrom$app/navigationanduserInterfacePreferencesStorefrom$stores/userInterfacePreferences.
Variants:
- Active versus inactive (background plus text color highlight).
SectionHeader
Section titled “SectionHeader”A small uppercase section-divider label with hairline rules above and below it, used to separate groups of navigation entries.
Import:
import SectionHeader from '$ui/navigation/pageNav/sectionHeader.svelte';Props:
| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
label |
string |
— |
Yes | Section label text (rendered uppercase, tracked, semibold). |
Usage:
<SectionHeader label="Configuration" />Notes:
- Presentational only; single required prop.
- Used by
Group(sectionLabel) and bypageNav’s disabled-item annotation branch.
NavScopeSelector
Section titled “NavScopeSelector”A labeled <select> that lets the user choose the active Arr scope (“Apps Scope”). Options are the ARR condition targets filtered to those the nav shell declares available; selection updates the navScope store.
Import:
import NavScopeSelector from '$ui/navigation/pageNav/navScopeSelector.svelte';Props:
| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
navShell |
NavShell | undefined |
undefined |
No | The nav shell; its arrScopeOptions determine which scope options are offered (availableScopes). |
Usage:
<NavScopeSelector {navShell} />Notes:
on:change(onScopeChange) reads the select value and callsnavScope.setScope(value as ArrType). It reactively callsnavScope.syncAvailableScopes(availableScopes). There are no callback props.- Binds the select value to
$navScope. ImportsARR_CONDITION_TARGET_OPTIONSfrom$shared/arr/capabilities, theArrTypetype from$shared/pcd/types, andnavScopefrom$stores/navScope. - Reactively syncs available scopes into the store on every
navShellchange.
Variants:
- Options are
ARR_CONDITION_TARGET_OPTIONSfiltered byavailableScopes; empty whennavShell/arrScopeOptionsare absent.
Version
Section titled “Version”Footer card showing the praxrr logo, wordmark, and build metadata line (platform · channel · optional version).
Import:
import Version from '$ui/navigation/pageNav/version.svelte';Props:
| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
version |
string |
'' |
No | App version string; only appended to the metadata line when shouldShowVersion() is true and version is non-empty. |
Usage:
<Version version="2.0.0" />Notes:
- Computes
platform = getPlatformLabel(),channel = getChannelLabel(), andshowVersion = shouldShowVersion()(all from$shared/utils/version) once at init. Uses$assets/logo.svg. - Rendered by
pageNavin both the mobile (scrolling) and desktop (pinned bottom) footer slots.
Variants:
- The metadata line always shows platform and channel; the version segment is conditionally appended.