Skip to main content

Authoring Tutorials

xOpat's tutorial system has three layers:

  1. USER_INTERFACE.Tutorials.add(...) — registers a tutorial entry. Defined in src/user-interface.js; runs at any time after USER_INTERFACE is available (typically in pluginReady, or from app.ts for core entries).
  2. UI.TutorialsModal — the launcher modal that lists every registered tutorial as a card (ui/classes/components/tutorialsModal.mjs). Opened by USER_INTERFACE.Tutorials.show() and by the graduation-cap icon in the top-right AppBar.
  3. The tour engine (src/classes/app/tutorial/, reached as APPLICATION_CONTEXT.tutorials) — the actual step driver, with the visual styling tuned to DaisyUI (glass exit pill, readable label, gradient Next/Prev buttons).

Plugins/modules that want to expose a tutorial only ever touch layer 1. The launcher and the driver are wired up for you.

When to add a tutorial​

Per docs/web/development.md: provide a tutorial when your feature is one of the main UI surfaces, or when a change in core behaviour would surprise an existing user. Keep each tutorial short (4–10 steps); split into multiple entries rather than a single 20-step monster — the launcher shows cards, so several short tutorials read better than one giant one.

API reference​

USER_INTERFACE.Tutorials.add(
pluginId, // "" for core, otherwise this.id from your XOpatPlugin
name, // short card title (locale string recommended)
description, // one-line subtitle
icon, // Phosphor class like "ph-compass" (preferred), or legacy "fa-..."
steps, // ordered array of step objects (see below)
prerequisites // optional () => void run when the tour starts
);

Step shape​

Each step is an object with exactly one rule key plus optional metadata:

{ "<action> <css-selector>": "<HTML text shown next to the element>", runIf?: () => boolean }
  • action — next (advance via the tour's NEXT button) or click (advance only when the user actually clicks the selector — useful for opening a collapsed panel as part of the walk).
  • css-selector — any CSS selector (resolved with document.querySelector). See the cookbook below.
  • runIf — optional function evaluated when the tour starts; if it returns falsy, the step is silently skipped. Use it for visualization-only steps, multi-viewer-only steps, etc.

Step text is HTML-capable: small formatting tags (<b>, <i>, <u>, <br>, <code>, <sub>, <sup>) render as expected. In xOpat builds that sanitise plugin-supplied input (see plugins/extra-tutorials/README.md → "Allowed HTML in text fields"), unsafe tags and attributes are stripped at registration time.

prerequisites​

Runs once when the user clicks the card, before the tour takes over. Use it to put the UI into the known starting state your steps assume — e.g. close floating windows, scroll a list to the top. Don't put per-step set-up here; that belongs in runIf (or in an earlier click step).

Selector cookbook​

What you want to targetStable selectorSource
The main viewer area#viewer-containersrc/app.ts (MainLayout)
Active OSD canvas (first viewer)#osd-0per-viewer cell id, src/loader.ts
Hide-all-UI button#fullscreen-buttonui/services/appBar.mjs:152
View / Edit / Plugins AppBar tab#visual-menu-b-view, #visual-menu-b-edit, #visual-menu-b-pluginstab id pattern ${parent.id}-b-${tab.id}
AppBar right-side icons (settings, tutorials, share, user)#top-user-buttons-menu-b-{settings,tutorial,share,user}same pattern, parent top-user-buttons-menu
Right-side viewer menu — Navigator tab toggle[id$="-right-menu-menu-b-opened-navigator"]per-viewer, ui/classes/components/multiPanelMenuTab.mjs:113
Right-side viewer menu — Layers (shaders) tab toggle[id$="-right-menu-menu-b-opened-shaders"]same
Right-side viewer menu — open Layers panel body[id$="-right-menu-menu-opendiv-shaders"]same
Visualisation picker <select> (when Layers is open)[id$="-right-menu-menu-opendiv-shaders"] select[name="shaders"]ui/classes/components/shaderSideMenu.mjs:148
The whole shader-cards container[id$="-panel-shaders"]ui/classes/components/shaderSideMenu.mjs:244
A specific shader card (by shader id)#${shaderId}-shaderglobal, keyed by name
Snapshot / cache button[id$="-cache-snapshot"]ui/classes/components/shaderSideMenu.mjs:176
Right-side viewer menu — Annotations tab toggle[id$="-right-menu-menu-b-opened-gui_annotations"]per-viewer; tab id is the annotations plugin id (gui_annotations)
Annotations enable/disable toggle[id$="-annotations-enable-toggle"]plugins/annotations/methods/viewerMenu.mjs
Annotations save button[id$="-annotations-save"]same

For plugin-registered right-side tabs in general, the toggle id is [id$="-right-menu-menu-b-opened-${pluginId}"] — the ${pluginId} is whatever id was passed to the menu-tab registration (usually the plugin's this.id, i.e. the value from include.json's id field).

Why [id$="-…"] for per-viewer panels?​

xOpat supports multiple viewers in a single page. Per-viewer DOM elements get the viewer's position id as a prefix — e.g. osd-0-right-menu-menu-b-opened-shaders in a single-viewer session, osd-0-… and osd-1-… in a two-viewer session. Hard-coding #osd-0-… would walk the wrong viewer in some setups; the suffix selector ([id$="-right-menu-menu-b-opened-shaders"]) resolves through document.querySelector to the first matching element, which is the focused viewer in the layouts core ships today. (jQuery is not loaded in xOpat — $ is the i18n namespace.)

If your tutorial step genuinely needs to point at a specific viewer (e.g. a multi-viewer LINK/sync walkthrough that explicitly compares left vs. right), use the explicit #osd-0-… / #osd-1-… ids and mention the layout assumption in the step text. Gate with runIf if necessary.

The tour can't highlight collapsed elements​

Layers and Navigator tabs start collapsed on first load. The next highlight needs the target visible; if you point at something inside a closed panel, the tour will look broken.

Two ways to handle this:

  • Open via a click step before the next step that points inside the panel (preferred). The user opens the panel themselves; the step advances automatically.
  • Open programmatically in prerequisites (rarely needed, and it bypasses the chance to teach the user that the panel exists).

Example (the core basic tutorial uses this pattern):

{ 'click [id$="-right-menu-menu-b-opened-shaders"]': $.t('tutorials.basic.openLayers'), runIf: withLayers },
{ 'next [id$="-right-menu-menu-opendiv-shaders"] select[name="shaders"]': $.t('tutorials.basic.visualizationPicker'), runIf: withLayers },

Locale conventions​

  • Tutorial strings live under tutorials.<area>.* in src/locales/en.json (core) or <pluginId>.tutorial.* in the plugin's own locale file (e.g. plugins/annotations/locales/en.json).
  • Keep step text under ~200 characters; longer text is fine when needed but wraps awkwardly in the step label.
  • Use $.t(...) rather than hard-coded English. The basic tutorial in src/app.ts:603-621 is the canonical example.

Multi-viewer caveat​

When designing a tutorial that walks per-viewer elements:

  1. Prefer the [id$="-…"] selector form so the walk works on 1, 2, or N viewers without code changes.
  2. If the focused viewer is genuinely interchangeable (the user can have either pane focused), state that explicitly in the first step's text: "In multi-viewer sessions every panel below exists per viewer; the tour walks the focused one."
  3. For genuinely multi-viewer-only steps (e.g. linking two viewers via the scalebar LINK button), gate with runIf: () => APPLICATION_CONTEXT.config.background.length > 1 so single-viewer sessions skip them.

Testing your tutorial​

  1. Load a session that exercises the configuration variants your tutorial cares about (with / without visualizations, 1 vs. 2 viewers, plugin loaded vs. not).
  2. Open the launcher via the top-right graduation-cap icon, or in DevTools: USER_INTERFACE.Tutorials.show().
  3. Walk through every step. On each step, confirm the highlighted element is the right one and that no console.error fires.
  4. Repeat for at least one negative config — a session that should skip steps via runIf. Confirm the skip is silent (no leftover empty highlight, no jumps to wrong elements).
  5. Hit the glass X in the button row mid-tour — confirm the close cleans up without leaving residual overlays.

See also​

  • plugins/extra-tutorials/README.md — embedder-supplied tutorials via session config, with an optional glass welcome modal (gradient, illustration, accent palette) before auto-start.
  • src/EVENTS.md — viewer lifecycle events you might pin tutorial registration against (plugin-loaded, before-app-init, etc.).
  • src/assets/custom.css (.xo-tour-*) — visual styling of the in-step label and buttons. DaisyUI supplies the button shape; the palette is pinned there because the overlay is dark in every theme.