Skip to content

Web interface implementation notes

This page covers how the browser interface is built, how it is styled, and what it does not do yet.

Common tasks

  • Build the Vue application from web/ when running from source.
  • Use the shared design system rather than introducing one-off component styling.
  • Check the known-gap list before implementing a browser feature that may already be intentionally out of scope.

Detailed behavior

The sections below retain the technical and behavioral detail needed for troubleshooting and development. You can stop after the task summary if you only need to use the feature.

Building it

Built into the container image already. From a source checkout:

cd web
npm install
npm run build        # writes into src/altero/web/static
npm test
npm run dev          # localhost:5173, proxying the API to :8000

Without that build the server still runs and the API is fully usable; /app/ answers 503 and says what to run.

End-to-end tests

npm run e2e drives the built interface in Chromium, at desktop and phone sizes, against a real server on a throwaway SQLite database. It fails on any Content-Security-Policy violation and on serious or critical axe findings. Build first; the first run needs npx playwright install chromium.

Design

The design follows Material 3 with a teal accent, and light and dark follow the operating system unless the user picks one. What altero does inside that frame is written down in design.md: one rule — fill groups, outline controls, hairline separates — three surface steps, one card recipe that every screen imports rather than draws itself, and a toolbar around every row of icon-only tools, because a glyph on a page says nothing about being pressable until a pointer rests on it. styles/surfaces.node.spec.ts fails when a component draws a surface of its own.

The target is WCAG 2.2 AA. What can be measured is measured in tests that fail — every contrast pair the interface puts on screen, and the ordering that keeps a hovered row from looking more chosen than a chosen one — and design.md says which criteria were decided where, including what has not been verified.

Hover is a state layer rather than a color — a translucent wash of the text color, one token, laid over whatever is underneath. One wash rather than a surface step per depth: it reads the same over the item list as over a dialog's raised surface, where a step chosen to separate a row from a white page comes to 1.03:1 and a screen at an angle shows nothing at all. styles/contrast.node.spec.ts measures what the wash comes to on each surface it lands on, and checks the one ordering that matters: a selected row is marked more strongly than the row under the pointer, or hovering would look like choosing.

It is set in IBM Plex Sans, with Plex Sans JP for Japanese, Plex Sans Arabic for Arabic, Persian, Urdu and Shahmukhi Punjabi, and Plex Sans Hebrew for Hebrew. All are served by this application: no CDN or other third party receives a font request. The Latin and Japanese faces are split by unicode-range, so a page fetches only the subsets its text needs. The Arabic and Hebrew packages provide complete faces; regular, medium and bold are included, with italic synthesized by the browser. Their stylesheets are deferred alongside Japanese, and font files are fetched only when text needs them, including mixed-language libraries. Every face uses font-display: swap, so system fonts show the text while it loads. Fingerprinted assets are cached for later visits. Package installation scripts remain disabled to prevent IBM's telemetry hook.

The RTL checks cover Arabic screenshots and axe on desktop and phone, mixed item and note directions in both UI directions, public profiles and shares, splitter keys, mouse drags and touch holds. Font checks inspect the faces that Chromium actually used to draw every script letter in the five RTL catalogues. These are Chromium checks at two viewport sizes; Safari and native-device verification remain separate. Mirroring uses composed transforms and ancestor selectors compatible with the build's browser targets. A compiled-selector check keeps scoped CSS from applying a splitter transform to the document itself.

The note editor

Notes are edited with Tiptap over a port of Zotero's own note-editor schema (web/src/notes/), so a note saved here is the HTML Zotero Desktop would save: the specs, the HTML reading and writing and the keymap are the original's, and a differential run against the original code at the revision Zotero pins found no difference. web/src/notes/corpus/ holds notes as Zotero 10.0.6's shipped editor saved them, recorded by the note-corpus desktop acceptance scenario, and the editor here is tested to save each the same way. The editor is loaded the first time a note is opened. Tiptap's injected stylesheet is switched off, because the interface's Content-Security-Policy refuses inline styles; notes/editor.css carries it.

Toolbar menus use fixed viewport positions. Apply those coordinates before moving keyboard focus into the menu: focusing the unpositioned list can scroll the document, whose scroll handler then dismisses it. Focus also disables native ancestor scrolling; only the menu list scrolls to reveal the selected option. Scroll notifications dismiss the menu when its anchor moves. A notification queued while the trigger was focused can arrive after opening and must leave the menu open when its anchor is unchanged. The browser regression opens New Note below a populated collection sidebar and checks that the menu stays open without moving the page.

Duplicate merge endpoint

The duplicates listing accepts grouped=true with scope=duplicates. It returns groups (lists of item keys), totalGroups, and the usual item envelopes and library watermark. In this mode start and limit count groups. Search, tag and collection filters find groups; every member readable by the viewer is included, oldest first. The requested sort orders groups by their first matching record. Without grouped, the existing item-based pagination is unchanged.

The export endpoint accepts the same grouped=true flag, expanding matching groups before writing them in batches. Explicit group-member selections export by key with scope=all, so the current search cannot remove a selected member.

The library store requests group pages and selects a whole group from any member. Select-all stays within the active group. Explicit individual inspection remains available, and an append at a different watermark restarts group pagination.

Groups are connected components of the existing candidate matches. A matching identifier can connect records whose titles differ, and a title match can connect another record to that group. Membership is a suggestion for review, not a claim that every pair has the same identifier or item type.

The comparison follows Zotero 10.0.6's master-and-field-choice model, reviewed in duplicatesMergePane.js and itemBox.js. The browser uses native select controls instead of desktop field popups. Group selection, individual inspection and membership changes have ordinary click/tap controls; coarse pointers retain 44-pixel targets. On a narrow screen, activating a group reveals its pane and closing it returns focus to the group.

The duplicates pane uses POST /web/libraries/{id}/items/merge, authenticated by the browser session and CSRF token. It accepts:

{
  "master": "MASTER22",
  "others": ["DUPL2345"],
  "fields": {
    "title": "Chosen title",
    "creators": [{"creatorType": "author", "name": "A collective"}]
  }
}

Send If-Unmodified-Since-Version with the library version at which the items were read. The version must match exactly, checked under the library lock. An absent header answers 428; a stale or future one answers 412. On 412, read the selected items again and review the choices before retrying.

The browser prepares a separate review snapshot with scope=all and the selected keys, in batches of at most 100. Every batch must report the same library version and include every selected key. Live list refreshes do not advance that snapshot's version. Confirmation sends it once; a failed list refresh after a committed merge does not turn success into an invitation to submit the merge again.

The items must be untrashed regular items of one type in this library, named once each. fields is optional: omitted fields and creators stay on the master, text or null replaces a bibliographic field, and a creators array replaces the whole creator list. An empty string or null clears a field; creators: [] clears creators. Structural properties, type changes and timestamps are refused. Tags, collections and relations are combined by the merge itself, including attachment deduplication and child moves, in one new library version.

The 200 response is {"items": [...]}, with the master first and each changed item in its ordinary envelope. Last-Modified-Version gives their shared new version. An optional locale query selects the interface language for PDF word ordering; it defaults to the account language, then en-US.

Not built yet

Items, notes and attachments can be created and edited in the browser, and duplicate items can be merged. Remaining browser gaps include:

  • An integrated PDF reader and annotation tools.
  • Offline library access. The installed app opens offline, but library data is not stored on the device.
  • Advanced search and a saved-search builder.
  • Adding items by DOI, ISBN or PMID.
  • A citation-style picker and a multi-item bibliography or report interface. The server already renders bibliographies and reports; the detail pane's citation uses format=bib.
  • Deleting a tag from the whole library at once.

Moving in from zotero.org brings the personal library only; a group has to be made here and its members invited.

Making an account for somebody else, resetting a password, suspending an account and the operator's view of the instance are under Administration above. administration.md says what the shell covers alone and what it shares with the browser.