UI Services
Unlike classes, UI services have no common UI base class, and unlike components that are meant to be re-used, these services represent a single core UI concept that is used in a single place and must not be instanced multiple times. The API of these services is often used by other plugins, modules, and parts of the viewer to add and control different menus and UI elements.
Core Services
-
AppBar(Top menu) Manages the top navigation bar. It exposes APIs to add custom actions, submenus, and buttons. It hosts core viewer controls like View, Edit, and Plugins menus. Plugins often useAppBar.EditorAppBar.Viewto insert their controls into the main application.AppBar.Viewgroups its checkable rows into categories declared inView.structure:appearance(visual state drawn over the slide — the analysis capture markers, the scalebar),sideViewerMenu,toolbarMenuandglobalMenuTabs(the window/menu groups). Register into one withView.registerViewComponent(category, {id, icon, title, visibilityManager}); specs sharing anidacross viewports collapse into a single row that fans a click out to all of them, so theidmust be the component kind, not a per-viewer id.View.append(...)is the un-categorised path and is for plugin windows only — a preference registered through it renders as a loose row above every group. Owners that flip a registered component from outside the registry (e.g. the Settings panel) should callView.refresh()so the checkmark follows.AppBar.Chromeis an opt-in registry that backs the "hide UI" button in the top-right. Components register aVisibilityManager(or any{ is, on, off }/{ is, set }duck) withAppBar.Chrome.register(id, vm); everything already routed throughAppBar.View.append()orAppBar.View.registerViewComponent()is auto-enrolled. The button callsAppBar.Chrome.toggle(), which snapshots each registeredvm.is()and callsvm.off()directly (bypassingVisibilityManager.set()so the user's persisted visibility choices inAppCacheare not overwritten); the next press restores only the entries that were visible before. Not related toFullscreenMenusbelow.AppBar.Actionsis a read-only, live catalogue of invocable actions aggregated from pluggable providers (ui/services/appBarActions.mjs). It exists so a renderer can enumerate "everything the user can trigger" without every registrant opting into a second registry. Built-in providers:tools(AppBar.Toolsentries),view(AppBar.Viewvisibility toggles — stateful, so their descriptors carryselected),shortcut(APPLICATION_CONTEXT.shortcutsspecs that opted in withquickAction: true) andcustom. Every entry is normalized to an immutableActionDescriptor{key: "<providerId>:<rawId>", label, icon, hint, kbd, group, disabled, selected?, pinnable, invoke}— plain values only, so the renderer diffs snapshots instead of polling. Surface:registerProvider(p) → off,register(rawId, {label, icon, invoke})/unregister(rawId)(the escape hatch for functionality in no registry),list(),get(key),invoke(key, ev),onChange(cb) → off(rAF-coalesced). Providers notify; nothing polls.Tools.list()/onChange()andView.list()/onChange()/toggleRow()exist to back this.AppBar.QuickActionsrenders the pinned subset as icon-only buttons inside#top-side-left(alreadyChrome-enrolled, and unlike a new#top-menussibling it does not compete with theflex-1toolbar embed slot). Configuration is a two-tier trust split (AGENTS.md §7):ENV core.setup.quickActionsis operator-trusted and may carry{id, icon, label}presentation overrides, while the per-user list resolved throughgetOptioncan come from a URL param or an imported peer session and is therefore reduced to id strings only — otherwise a hostile bundle could relabeltools:core.sync.resetas "Save" (andcomponentIconNodeaccepts image URLs, soiconwould double as a beacon).core.setup.quickActionsUserEditable: falsefreezes the bar and hides the Settings → Quick actions card.quickActionsMaxVisible(default 5) caps the buttons; the remainder spills into one trailing overflow menu. That cap is deliberately static — collapsing reactively onToolbarSlot.onRoomwould widen the slot, which re-expands the bar, which re-collapses it. BelowmaxMobileWidthPxthe whole strip hides;MobileBottomBaris the right host there and can reuse the same catalogue. -
MobileBottomBarA separate navigation bar service used exclusively on narrow/mobile viewports to provide touch-friendly counterparts to the AppBar items. -
FloatingManagerHandles z-index ordering and focus state for floating panels (e.g.FloatingWindowcomponents). It ensures that multiple floating UI windows properly overlap and the newly focused window always comes to the front. -
FullscreenMenusManages full-screen overlay menus (e.g., settings, data panels). Ensures that only one fullscreen menu is visible at a time and provides API to toggle or switch them.Tab body layout helpers. Plugin tab bodies look out of place when they ship their own ad-hoc DOM, so two thin van.js helpers on the service render the same DaisyUI cards core Settings uses:
const fs = USER_INTERFACE.FullscreenMenu;return fs.layout(fs.card("Export options", checkbox1, select1),fs.card("Display", checkbox2),);fs.layout(...sections)returns the outer flex shell + 2-column responsive grid;fs.layout(title, ...sections)adds a 2xl in-body header above the grid (matches the look of core Settings).fs.card(title, ...children)is a single titled card. All are optional — use them for visual parity with core, drop down to raw van.js when you need custom chrome. Passnull/""as the title to either helper to render the chromeless variant.Tab bodies are resolved eagerly.
register()calls a functionbodyimmediately (_normalizeBody), so the tab's DOM is built at registration — for core Settings that is insideFullscreenMenu.init()(src/app.ts:252), before plugins load and beforesrc/app.tsregisters its ownAppBar.Toolsentries. The body is not rebuilt when the panel opens. Any settings UI that must reflect runtime state therefore has to keep a stable container and re-render it from a change signal — see_quickActionsCard(), which repopulates fromAppBar.Actions.onChangeandAppBar.QuickActions.onPinsChange.Sidebar grouping.
FullscreenMenus.register(item, ns)places the tab under the namespacens(defaultNAMESPACE.PLUGINS). The sidebar rendersSYSTEMandPLUGINSgroups with a divider + uppercase label between them —Menu.NAMESPACE.SYSTEM(order 10) vsMenu.NAMESPACE.PLUGINS(order 20). Explicititem.namespaceoverrides thensargument.Plain chrome.
AppBar.Plugins.setMenu(...)andFullscreenMenus.setMenu(...)accept a finaloptsargument; pass{ chrome: "plain" }to skip the default rounded outer card around the plugin tab body. Use this when the tab renders its own cards viafs.card(...)and you'd otherwise see nested borders. Defaultchrome: "card"preserves the existing look for plugins that aren't migrating. -
FloatingWindowexternal mode Detached browser windows are now handled directly byFloatingWindowwithexternal: true, including inherited UI assets/theme support and Monaco-backed editor windows. -
GlobalTooltipA singleton tooltip service that allows components to display contextual help text without needing localized tooltip DOM structures everywhere. -
Tutorials (
USER_INTERFACE.Tutorials) The tutorial launcher.Tutorials.show()opens theUI.TutorialsModal(anIllustratedModal-backed two-pane modal); each card kicks off a walk through the registered steps, driven byAPPLICATION_CONTEXT.tutorials(src/classes/app/tutorial/). Author new tutorials viaUSER_INTERFACE.Tutorials.add(pluginId, name, description, icon, steps, prerequisites?)and consultsrc/TUTORIALS.mdfor the selector cookbook and step grammar (next/clickactions,runIfguards, viewer-agnostic[id$="-…"]selectors).