{% extends layout|default('base.html', true) %} {# The viewer's tab says which project it is: the display name, else the name. #} {% block title %}{% if data.datasource %}{{ project_title or data.datasource }}{% endif %}{% endblock %} {% block style %} {% endblock %} {% block content %} {% if data.datasource %}
{# Docked by the canvas layout above the caption (top-left), so the two stack instead of overlapping when the sidebar is closed. #} {# The one place the viewer's ground colour is not the app's. A transmitted-light image IS its white background, and the dark shell behind it reads as a shadow the scanner never recorded. #}
{# The calibration pencil. Parked here and moved into OpenSeadragon's container by views/scaleCalibration.js, which then keeps it beside the scale bar -- see that file. #} {% include "_scale_calibration_float.html" %}
{# What takes the centre spinner down. The spinner is in this page's own markup and visible from first paint, so this has to be loaded before main.js builds the viewer and calls watch() on it -- deferred scripts run in document order, which is the whole reason it sits above rather than in base.html with the other services. Viewer-only: no other page has the element. #} {# What was on screen (viewport, zoom, HD mode, channels), captured and put back -- regions remember the view they were drawn under -- and magic select's client (click an artifact, get its outline; sets itself up once). Core services the ROI and QC panels share; they call only /segment/v1. #} {# Grab/grabbing on the OSD canvas. Deferred and unconditional within this branch: it installs three document-level listeners that cost nothing until a press lands inside a `.openseadragon-canvas`, which only this page has -- either viewer's. #} {# Before main.js and deferred like it, so it is defined by the time init() sees a pending mask job. Loaded here rather than in base.html because the pending state belongs to a viewer looking at the project whose mask is converting -- every other page would carry a listener with nothing to listen for. #} {# The modal-and-chip of a background job (views/jobWait.js), and its waits: the mask pyramid here, the 3-D mask's cell surfaces below. #} {# Brightfield display adjustments. Deferred like main.js and loaded unconditionally: it does nothing at all when its sliders are absent, and gating the tag on image_kind would put a second thing in this page that has to agree with the staged markup above. It runs after main.js has built the base image layer's card, which is where those sliders will be by then. #} {# Here rather than base.html for the same reason: the only things that open it are the channel list and a registered layer's channel panel, and both are the viewer's. Only has to exist by the time the user clicks one of those upload buttons, so deferred is ample. #} {# What the last sample arranged, on its way into this one. BEFORE main.js and deferred like it, so the snapshot is on hand by the time init() builds the channel slots -- the sidebar reads it while deciding which channels to turn on, which is the earliest any of it is wanted. Viewer-only: nothing else has an arrangement to carry. #} {# Previous / Next for a sample that belongs to a dataset. Deliberately independent of main.js: a sample whose image will not open has no viewer at all, and this is the way out of it. It mounts off its own fetch and asks the viewer for nothing. #} {# Why the canvas is empty, when it is. Before main.js, which calls it from the boot path the moment a load comes back without an image. #} {# Core's Rotate and Flip tools. Before main.js, which activates a `lazy` definition at boot only when `?tool=` staged its panel. #} {# Volumes: where the viewer is in a z-stack (the state), the Z strip and badge on the canvas, and core's Depth card. Before main.js, which reads the state beside /config and mounts the strip. All three do nothing for a planar image. #} {# How the 3-D view may be cut, in numbers (before the state, which repairs its `inspect` with it), and the controller every control changes it through. #} {# A channel switched on comes in rather than lands, on every surface the channel section drives (the plane, layers, section panes). Before main.js, whose boot adds the first channels. #} {# Surfaces (plexora/server/surface): the meshes drawn in the 3-D view, the service that loads and styles them, their card, Surface Studio's dock, the picked object's card and the objects table. #} {# A 3-D mask drawn in 3-D: the Cells control's Outlines and Filled as cell surfaces, and the wait while the mask is converted. #} {# The 3-D view's dock (cutting the stack) and the handles that drag its cuts. #} {# The Area tool (a square on the image, opened in 3-D), and Back (B) to the view it was opened from. #} {# Bookmarks: views kept as captures of the plugin that keeps them, and their tray. The store registers itself (Plugin.viewer_scripts, in ). #} {# An external agent driving this viewer (the live viewer control plane, /agent/v1). Deferred and BEFORE main.js: it waits for DOMContentLoaded and then for main.js's __plexoraReady, so being earlier costs nothing and means it is in place before anything main.js boots can touch the revision. #} {# The agent panel: the card that shows an agent session's phase, evidence and controls. Its orb driver is base.html's, which the import dialog draws with too. Before agentBridge.js, which routes show_evidence to the panel when one is up. #} {# Plexora AI chat (/ai/v1/conversations): a launcher chip and a non-modal panel in the viewer's lower-left corner. #} {# What a launch link asked this page to show beyond its channels -- a colour-by column, cells, regions, a viewport (page_routes.launch_from_dict). After agentBridge.js: it applies them through the bridge's own handlers. #} {% else %} {# The home page: `/` with no datasource named, whether or not anything is registered yet. One raised card that takes one image, and under it the way back to what was opened last. The card is one question asked in reading order -- a file or a folder, on which machine, which one -- and its button is Browse until there is a path in the box and Load from then on, so there is never a second control for the same act. The one gesture -- pick an image, land in the viewer -- is what this page is for, and it is the page's own control rather than a button that opens the import dialog: a modal covering an otherwise empty page is a step, not a shortcut. What it is NOT is a second importer. Load POSTs to `/import/sample`, the same route the dialog submits to, so a Xenium run dropped here brings its transcripts and its table with it instead of being refused for not being one image. See views/quickViewLanding.js. Everything that needs a decision first -- several samples in one folder, a name, a dataset -- is the dialog, one quiet line under the card. #}
{# An instruction, not a logo: the name is already in the navbar, and the page's one heading should say what the page is for. #}

Explore your tissue

Open an image from this computer or a connected server.

{# `is-local` / `is-remote`, `data-face` (browse | load | busy) and `data-check` (idle | checking | ok | bad | url) are set by the controller, and everything that changes with them -- which glyph the machine chip leads with, which face the button shows, the mark at the end of the box -- is CSS over those three. Every icon is in the markup from the start because Font Awesome swaps each for an once: a class changed on the span afterwards changes nothing anybody can see. #}
{# The quick beam, faded in over the card's own slow one while something is happening: choosing, loading, sharing. #}
{# Which shape of thing Browse opens a dialog for. Not "which format" -- the chips at the foot of the card answer that -- and not a question for a typed path, which is whatever it is: a path that is plainly one or the other moves this with it. #}
{# Which machine the image is on, as the first thing in the box it governs -- `host:/path`, the shape anybody who has used scp already reads. "Local" or the server's name; the menu under it lists the actual machines, so Remote is never a second question. #} {# Built by the controller each time it opens, from the live list of machines (services/remoteState.js). #}
{# Browse with an empty box, Load with a path in it -- one button whose job follows the box, so the typed way in and the browsed way in can never look like two routes. #}
{# What the card has to say, under the row it is about. Two live regions, each always in the DOM and empty when it has nothing to say: this page's own line, and the location service's (sharing a file on another machine, or why it cannot be). A region that appears and gains its text in the same tick is the shape screen readers miss. The wrapper opens and closes smoothly around whichever has something in it. #}
{# Two examples a side, from browsePicker.js's KIND_EXAMPLES -- the same pair every Browse button on the import form shows -- the side for the chosen tab lit, and "and more" so nobody reads four as the whole list. The `?` opens the catalogue the import dialog's own "All formats" link opens (views/importHelp.js). #}
  • and more
{# The import dialog, for a pick that needs a decision first. `id="sample-import-home"` is what binds it: views/importSample.js binds every "Import Sample..." on every surface by id, so this needs no controller of its own. A

{# The way back to what was opened last. `/` renders this card whenever no datasource is NAMED in the URL -- not only on an empty install -- so somebody with twenty samples lands here too. Three rows, from page_routes._recent_rows, rendered with the page so nothing moves when they arrive; quiet until pointed at, because the card above is what this page is for. Absent on an install with nothing to reopen. #} {% if data.recent_projects %}
{% endif %}
{% endif %} {% endblock %}