Tag Studio β Interaction Design Spec
Epic: #3866 Β· Product: Gevell Tags at tags.gevell.com (add it to
packages/accessible-org-ui/src/products.ts when it ships) Β· Status: draft for review.
This spec covers how the editor behaves: layout, selection, keyboard, task modes, validation, bulk
operations, AI review, undo, save and export. It does not cover the Rust/WASM engine internals
(phase 1), except for the operation vocabulary that the UI emits. The design starts from scratch. It
does not inherit the interaction model of apps/webβs tag views or apps/tag-editor.
Standards targets: the output is validated for PDF/UA-1 (ISO 14289-1, Matterhorn Protocol 1.1) and WCAG 2.1 AA content requirements. The tool itself must meet WCAG 2.2 AA and must be fully usable with a keyboard only and with NVDA, JAWS and VoiceOver.
0. Principles
- One structure model, many views. The canvas, tree, reader preview and inspector all render the same in-memory structure. No view keeps its own copy, so views cannot drift out of sync.
- Every edit is an operation. Each change is a typed, validated, reversible op with an inverse and an origin (user, fix-all, select-similar, AI, template). Nothing mutates the model any other way. Undo is reliable because it is part of the model, not added afterwards.
- Invalid ops are refused before they apply. Each op checks its preconditions (for example βLI may only be a child of Lβ). A refused op leaves the document unchanged and gives a plain-language reason that says what to do instead.
- Guided first, raw one toggle away. Task modes are the default workspace. Raw tree is a peer mode, not a hidden βadvancedβ screen.
- No false green. Items that need human judgment (alt text quality, reading order meaning, heading semantics) are never shown as passing. They show as Needs review until a person marks them reviewed, and that attestation is recorded.
- Never alter what the page looks like. Saving is gated on a pixel-identity check. Tagging changes structure only, never appearance.
- Non-modal by default. Properties are edited in the inspector, with no blocking dialogs. The only modal surfaces are unsaved-changes and destructive-confirm prompts, and the save-gate failure sheet.
- Every pointer action has a keyboard path and a non-drag path (WCAG 2.5.7). Drag is a shortcut, never the only way.
- Say what happened. Every op result is announced in a live region in the words the history panel uses (βRetyped P to H2 Β· 1 itemβ).
- The AI proposes and the human decides. AI output arrives as proposed ops with a confidence level and a one-line reason. By default it changes nothing until someone accepts it. Users can opt in to auto-apply above a confidence threshold (Β§9.4). Auto-applied changes are marked as AI, can be undone, and never count as human review.
- The server holds the record. Documents, versions and the op log sync to Gevellβs servers (AWS). The browser keeps a local outbox so work continues offline and nothing is lost (Β§11.3).
Terms
| Term | Meaning |
|---|---|
| Node | A structure element (StructElem) such as H2, P, Figure or TD |
| Content item | A leaf of page content: marked-content sequence (MCR/MCID), annotation (OBJR), or untagged run |
| Orphan | Page content that is neither tagged nor artifacted. Every orphan is a PDF/UA failure |
| Op | An atomic, reversible edit (see Β§3) |
| Group | One or more ops applied as one undo step (bulk, fix-all, AI accept) |
| Primary | The most recently selected item in a multi-selection. The inspector shows the primary |
| Mode | A guided task workspace (Reading order, Headings, β¦) or Raw tree |
| Proposal | An op (or group) suggested by AI or a detector, not yet applied |
1. Layout
1.1 Regions (all breakpoints)
Each region is a landmark: <header>, <nav aria-label="Modes">, <main> containing the view
regions, each a <section aria-labelledby> with a visible heading. Screen reader users can move
between views with landmark and heading navigation as well as with F6.
| Region | Contents |
|---|---|
| App bar | File name, sync status (βSyncedβ / βSyncing 3β¦β / βOffline Β· 12 changes on this deviceβ / βSync problemβ), version state (βv7 + 14 changesβ / βv8 Β· verifyingβ¦β), undo/redo, validation summary chip, Save, Export, command palette button, help, theme |
| Mode rail | Vertical tablist of task modes with per-mode issue counts |
| Task panel | Mode-specific work queue: outline, figure queue, table picker, artifact groups, and so on |
| Structure tree | Virtualized treegrid. In guided modes it shares a column with the task panel as a split pane. In Raw tree mode it fills the column |
| Canvas | Rendered page(s) with overlays, reading-order path and orphan highlighting |
| Reader | Screen reader preview: linear announcements, plus a reflow tab |
| Inspector | Non-modal properties of the primary selection |
| Dock tabs | Issues Β· History Β· Suggestions (AI) Β· Similar |
| Status bar | Page x of N, zoom, selection count, validation progress, engine status, and a live-region echo of the last announcement |
1.2 Wide desktop (β₯ 1920 CSS px)
All four views are visible at once, and the dock sits under the right column.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β§ Tag Studio annual-report-2025.pdf β Synced Β· v7 + 14 changes βΆ β· [ β 12 fail Β· ? 31 review ] βK [Save] [ExportβΎ]βββββββ¬βββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββ¬βββββββββββββββββββββ¬βββββββββββββββββ€β β‘ β READING ORDER Β· page 3 β β p3/48 βΆ 100% βΎ Overlays βΎ β READER Reflow β INSPECTOR ββ RO β ββββββββββββββββββββββββββββ β ββββββββββββββββββββββββββββββββββββ β heading level 1, β H2 Β· page 3 ββ 3 β β 1 H1 Annual Report 2025 β β β β βββββββββββββββββββββββββ[H1] β β Annual Report β Type [H2 βΎ] ββ Hd β β 2 P The fiscal yearβ¦ β β β ββ‘ββββββββββββββββββββββ[P] β β The fiscal yearβ¦ β Lang [inherit]ββ 1 β β 3 H2 Budget overview β β β β β β’ββββββββββββ[H2] βsel β ββΆheading level 2, β Title [ ]ββ Fg β β 4 β¦ Table 1 (4Γ3) β β β β β£β¦β¦β¦β¦β¦β¦β¦β¦β¦β¦β¦[Table] β β Budget overview β ActualText ββ 7 β β 5 P Revenue grewβ¦ β β β β β€ββββββββββββ[P] β β table with 4 rows β [ ]ββ Tb β ββββββββββββββββββββββββββββ β β βββ untagged βββ β β β and 3 columns β BBox 72,540 β¦ ββ 2 ββββββββββββββββββββββββββββββββ€ β ββββββββββββββββββββββββββββββββββββ β β¦ β [Attributes βΈ] ββ Ar β STRUCTURE / filter β β β ββββββββββββββββββ€β Ls β βΎ Document β β β β Issues History ββ Lk β βΎ Sect β β β β Suggest Similarββ Fm β H1 Annual Report 2025 β β β β β Figure has noββ Nt β P The fiscal yearβ¦ β β β β alt (p3)[Fix]ββ La β βΈ H2 Budget overview β1 β β β β ? Reading orderββ TC β Table 4Γ3 β β β β p3 [Review] ββ {} β β β β β βββββββ΄βββββββββββββββββββββββββββββββ΄βββββββββββββββββββββββββββββββββββββββ΄βββββββββββββββββββββ΄βββββββββββββββββ€β Page 3 of 48 Β· 100% Β· 1 selected (H2) Β· Validatingβ¦ 62% ββββββ Β· Engine ready β Retyped P to H2 Β· 1 item βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββColumn widths: rail 56 Β· task/tree 360 Β· canvas flex (min 640) Β· reader 340 Β· inspector/dock 380.
Every splitter can be resized with the pointer and with the keyboard. Each splitter is a focusable
role="separator" with aria-valuenow; the arrow keys move it 16px and Home/End collapse or restore
it. Layout is saved per user in localStorage (a convenience only).
1.3 Standard desktop (1280β1919 CSS px)
The reader moves into the right-hand dock as a tab. The inspector stays pinned at the top of the right column. The dock tabs are Reader Β· Issues Β· History Β· Suggestions Β· Similar.
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β§ annual-report-2025.pdf β Synced Β· v7+14 βΆ β· [β12 ?31] βK [Save] [ExportβΎ] βββββββ¬ββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββ€β RO β READING ORDER Β· p3 β β p3/48 βΆ 100% βΎ Overlays βΎ β INSPECTOR H2 Β· p3 ββ Hd β 1 H1 Annual Report 2025 β ββββββββββββββββββββββββββββββββ β Type [H2 βΎ] Lang[inh] ββ Fg β 2 P The fiscal yearβ¦ β β β ββββββββββββββββββ[H1] β β ActualText [ ] ββ Tb β 3 H2 Budget overview β β β ββ‘ββββββββββββββ[P] β β [Attributes βΈ] ββ Ar βββββββββββββββββββββββββββββ€ β β β’ββββββββ[H2] β βββββββββββββββββββββββββ€β Ls β STRUCTURE / filter β β β β£β¦β¦β¦β¦β¦β¦β¦[Table] β β Reader Issues History ββ Lk β βΎ Document β β ββ untagged ββ β β β Suggest Similar ββ Fm β H1 Annual Report 2025 β β β β heading level 2, ββ Nt β βΈ H2 Budget overview β1 β ββββββββββββββββββββββββββββββββ β Budget overview ββ La β Table 4Γ3 β β table with 4 rows β¦ ββ TC β β β ββ {} β β β βββββββ΄ββββββββββββββββββββββββββββ΄βββββββββββββββββββββββββββββββββββ΄ββββββββββββββββββββββββ€β p3/48 Β· 100% Β· 1 selected Β· Validating 62% ββββ β Retyped P to H2 Β· 1 item ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββColumn widths: rail 56 Β· task/tree 300 Β· canvas flex (min 520) Β· right 360.
1.4 Compact desktop (1024β1279 CSS px): minimum for the multi-pane workspace
- 1024 CSS px is the minimum width for the synchronized multi-pane workspace. This equals a 1280px screen at 125% browser zoom, which is a common setup for remediators.
- The mode rail shrinks to icons with visible short labels (2β3 letters, as in the wireframes) and a tooltip. The tooltip is not the only label: each tabβs accessible name is the full mode name.
- The right column becomes a drawer that overlays the canvas. It is toggled with the Inspector
button or
Mod+I(see Β§5, with its conflict note). The drawer is non-modal: focus is not trapped, and Esc closes it and returns focus to where it came from. - The task panel and tree stack vertically in the left column, with a draggable splitter.
1.5 Below 1024 CSS px: single-pane mode (required, not optimized)
This is a desktop pro tool, but WCAG 1.4.10 Reflow still applies. A remediator with low vision may run 1280px at 400% zoom, which gives a 320px viewport. The tool must remain fully functional there, without being optimized for it.
- One view at a time. A view switcher (tablist: Task Β· Tree Β· Page Β· Reader Β· Inspector Β· Issues Β· History Β· Suggestions) sits under the app bar. Selection is shared, so choosing an item in Tree and switching to Page shows it selected and scrolled into view.
- The app bar collapses to: file name (truncated, with the full name in
titleand the accessible name), save state icon + text, and an overflow menu holding Undo, Redo, Save, Export and Palette. - The canvas is a documented 1.4.10 exception (two-dimensional content). It pans in both directions inside its own scroll container, and the page itself never scrolls horizontally.
- The table grid editor is also a 2D exception. It scrolls inside its container and the header row and header column stay sticky.
- Below 1024px a dismissible notice appears once per session: βTag Studio works best at 1024 pixels wide or more. All features remain available one view at a time.β
- There is no βunsupportedβ wall at any width.
1.6 Zoom, text spacing and motion
- Browser zoom (
Mod+=,Mod+-,Mod+0) is never overridden. Canvas zoom has its own keys (Β§5). - All UI text respects WCAG 1.4.12 text-spacing overrides. Truncation uses ellipsis plus a
full-text accessible name, and never clips with
overflow:hiddenalone. prefers-reduced-motion: no animated scrolling on sync, no path-drawing animation, and the proposal βghostβ pulse is replaced by a static dashed style.- Minimum UI font size 12px. Tree rows are 28px tall by default, with a comfortable density of 36px. Hit targets are β₯ 24Γ24 CSS px (WCAG 2.2 AA 2.5.8). Primary buttons are 44px.
2. Selection model and sync rules
2.1 What can be selected
A single shared selection holds either:
- Structure selection: a set of node IDs, or
- Content selection: a set of content-item refs (MCRs, OBJRs, orphan runs, artifact runs).
The two kinds never mix in one selection. If you shift-click an orphan while nodes are selected, the selection becomes content-only and the status bar says βSelection changed to content (3 items)β. This rule removes a whole class of ambiguous ops, such as βretype this orphan runβ.
The selection also carries:
- a primary item, which is the last one added and is what the inspector shows;
- an anchor for Shift-range extension;
- a scope (page range) for select-similar and mode queues.
Selection lives in the session store, not in any view.
2.2 Focus is separate from selection
- Each view has its own focus (DOM focus or
aria-activedescendant). Only the view the user is working in holds DOM focus. - In the tree and canvas, selection follows focus by default: arrow keys move focus and set a
single selection. With
Ctrl(Windows/Linux) orCmd(macOS) held, arrow keys move focus without changing selection, andSpacetoggles the focused item (APG multi-select convention). - Other views reflect a selection change visually. They never take DOM focus and never announce it. Announcing mirror updates would double-speak every keystroke.
2.3 Sync rules
| Source β Target | Rule |
|---|---|
| Any β Canvas | Highlights the bounding boxes of all content under each selected node. If the primaryβs first content item is off-screen, scroll it into view (instant with reduced motion, otherwise 150ms). If any part of the primary is already visible, do not scroll. Multi-page selections show a page-strip badge β+ on pages 4, 7, 12β with clickable links |
| Any β Tree | Expands ancestors of the primary and scrolls it into view (virtualized). Other selected rows are marked but not expanded to. If a selected node is hidden by the current filter, a non-modal bar above the tree says: β1 selected item is hidden by the filter. [Show it] [Clear filter]β |
| Any β Reader | Scrolls to the first utterance generated by the primary and outlines every utterance from selected nodes |
| Any β Inspector | Shows the primary. For a multi-selection it shows the shared fields; fields that differ show βMixed (3 values)β. Editing a mixed field applies to all selected items as one group |
| Canvas click | Selects the innermost node that owns the clicked content. Clicking the same point again (within 600ms, or with Alt+click) walks up one ancestor per click, cycling back to the innermost after the root. A click on orphan content selects that orphan run (content selection). A click on artifact content selects the artifact run. Empty space clears the selection |
| Hover (pointer only) | A βpeekβ highlight in the other views, without changing selection and without any announcement. Keyboard users get the same peek on the focused row |
| After an op | Every op returns a result selection: wrap selects the new parent, unwrap selects the former children, retype keeps the node, artifact selects nothing and moves focus to the next item in the mode queue. Undo and redo restore the selection that was current before or after that op |
| Mode switch | The selection is kept if the new mode can show it. For example, a Figure selection carries into Figures mode and opens that figure in the queue. Otherwise it is kept but the mode shows βYour selection (H2) isnβt part of Figures. [Go to Headings]β |
| Page change | Paging the canvas does not clear the selection. The page strip shows where selected items live |
2.4 Selection tools (pointer and keyboard parity)
| Action | Pointer | Keyboard / non-drag alternative |
|---|---|---|
| Select one | Click | Arrow keys in tree or canvas |
| Add/remove | Mod+click | Mod+arrows then Space |
| Range | Shift+click (tree: row range; canvas: reading-order range) | Shift+arrows |
| Box select | Drag a marquee on empty canvas | Mark mode Alt+B: arrows move a crosshair, Enter sets the first corner, arrows (with Shift for Γ10) size the box, Enter finishes. Each corner is announced as a percentage of the page (βCorner at 12% across, 30% downβ) |
| Lasso | Shift+drag on empty canvas | Mark mode + L switches to a polygon: Enter adds a vertex, Mod+Enter closes it |
| Select text range | Drag across text | In the Reader, Shift+arrows extends across utterances, then βSelect contentβ (Alt+Enter) |
| Select all on page | β | Mod+A in canvas (the page), or in tree (siblings of focus; press again for the whole visible tree) |
| Clear | Click empty space | Esc (first press closes any open popover, second press clears selection) |
3. Operation vocabulary (what the UI emits)
The UI calls the engine only through these ops. Each has preconditions, an inverse, and a
human-readable label template (i18n key ts.op.<name>.label). Ops are batched into groups when
they come from one gesture.
| Op | Args | Inverse | Notes |
|---|---|---|---|
retype | node, newType | retype to old | Refused if the parent/child rules break (for example TD outside TR) |
wrap | nodes[] (contiguous siblings), newType, attrs? | unwrap | Creates a parent |
unwrap | node | wrap | Removes the node and promotes its children in place |
move | node(s), newParent, index | move back | Used by drag, cut/paste and Alt+arrows |
reorder | parent, permutation | inverse permutation | Reading-order edits within one parent |
mergeNodes | a, b (same type, adjacent) | splitNode | For example a P split across a page break |
splitNode | node, childIndex | mergeNodes | |
tagContent | contentRefs[], parent, index, type | untagContent | Orphan β new node |
untagContent | contentRefs[] | tagContent | Leaves content orphaned (flagged) |
splitContent | contentRef, glyphOffset | joinContent | Splits a marked-content sequence, for example a bullet glyph from list text. Engine phase 1 |
artifact | contentRefs[] | node, kind (Pagination/Layout/Page/Background), subtype? (Header/Footer/Watermark/PageNum) | unartifact | Artifacting a node artifacts all of its content and removes the node |
unartifact | contentRefs[] | artifact | Content becomes orphan, then is tagged by a follow-up op in the same group |
setAttr | node, key, value | setAttr old | Alt, ActualText, E, Lang, ID, Title; Table: Scope, Headers, RowSpan, ColSpan, Summary; List: ListNumbering; Layout: BBox, Placement; any owner/key in Raw mode |
setAnnot | annot, key, value | old | Link /Contents, widget /TU (tooltip) |
linkAnnot | annot, node | unlinkAnnot | Adds an OBJR; also sets /StructParent |
setTabOrder | page, S|R|C | old | Forms and Links modes offer S (structure order), which PDF/UA requires |
setDoc | key, value | old | Lang, Title, DisplayDocTitle, PDF/UA identifier (XMP), Suspects |
setRoleMap / setClassMap | name, value | null | old | Raw mode |
attest | issueKey, reviewer, note? | unattest | Records βreviewed by a humanβ. This changes the report, not the PDF. It is still in the op log so it can be undone and replayed |
Every op is undoable, with no exceptions. Acrobatβs Content and Reading Order panels have no
undo, and some axesPDF operations cannot be undone. In Tag Studio, an operation without a tested
inverse cannot ship: the engineβs op registry rejects any op type that lacks one, and CI round-trips
op β inverse on the corpus. This guarantee appears in the product copy and the help site.
Empty tags: βRemove empty tagsβ is a document-wide command (palette + Issues fix-all). It emits one
group of unwrap ops on nodes with no content and no children (keeping TD/TH cells that hold a
table shape, and Form nodes with widgets).
Group envelope (as seen in the history panel): { groupId, label, origin: user|fixAll|similar|ai|template|mode, ops[], selectionBefore, selectionAfter, validationDelta: {fixed, introduced}, at }.
4. Views
4.1 Page canvas
Rendering: a PDF page raster tiled at the current zoom, with a vector overlay layer (SVG) on top. In
both themes the page is always drawn as printed; dark mode never inverts the page. Only the
surrounding canvas background changes (bone #F7F5F1 in light, ink #0E1116 in dark).
Overlays (each can be toggled from the βOverlaysβ menu or the command palette; defaults are set per mode):
| Overlay | Rendering | Default on in |
|---|---|---|
| Tag boxes | Stroke in the tag-family color and stroke pattern (Β§14), label chip at top-left (βH2β, βPβ, βFigβ) | All modes |
| Reading-order path | Numbered ink badges (white number on #0E1116, with a white 2px ring) at each blockβs start, joined by a 2px ink polyline with arrowheads. Page-to-page continuation shows ββ p4β | Reading order, Raw |
| Orphans | Diagonal hatch fill in orphan color at 25% opacity, dashed stroke, and a β!β glyph chip labelled βUntaggedβ | Always on; cannot be turned off while orphans exist (a toggle would hide a failure) |
| Artifacts | Dotted gray stroke, 40% white wash over the content, chip βArtβ | Artifacts mode, Raw |
| Proposals | Dashed stroke in the target typeβs color, chip βSuggested H2 Β· 92%β | Whenever the Suggestions queue has items for this page |
| Tab order | Numbered squares (as opposed to the round reading-order badges) | Forms, Links |
| Issues | Small severity glyph at the nodeβs top-right (β fail, ? review) | All modes |
Filters: βShow only: [Headings] [Figures] [Tables] [Lists] [Links] [Forms] [Artifacts] [Untagged] [Issues]β (checkbox menu), and βDim everything elseβ (on by default in guided modes).
Selection style: a 2px ink (#0E1116) stroke with a 2px white outer halo, plus 8Γ8 corner
handles. This double stroke passes 3:1 against any page background. Keyboard focus on a canvas
item uses the brand focus ring (3px solid #046307, 3px offset; #5CCB60 in dark mode) drawn outside
the selection halo, so focus and selection are never confused. The palette excludes green (Β§14), so the
focus ring is never mistaken for a tag color.
Canvas ARIA: role="application" with aria-roledescription="page canvas" and
aria-label="Page 3 of 48". It is a single tab stop. Its content items are exposed through
aria-activedescendant pointing into an offscreen role="listbox" mirror, in which each optionβs
name is the node announcement (Β§4.2.3). The listbox mirror lets screen readers announce the focused
item without the SVG overlay needing an accessibility tree. The application role is justified because
the canvas has custom 2D navigation. Its keys are listed in the help dialog (?), and aria-describedby
gives a short key hint: βArrow keys move between items in reading order; Alt plus arrows moves
spatially; press question mark for all keys.β
4.2 Structure tree
4.2.1 Rows and columns
Each row shows: disclosure Β· tag chip (family color + pattern + text label) Β· content preview Β· issue badges Β· page number(s).
Content preview rules. β<MCID 12>β is never shown as a nodeβs preview.
| Node | Preview |
|---|---|
| Text container | First 80 characters of its actual extracted text, whitespace collapsed; ActualText wins if set (shown with an βATβ marker) |
Figure/Formula | 32px thumbnail + alt text in quotes, or βNo alt textβ in the error style (text, not color only) |
Table | βTable Β· 4 rows Γ 3 cols Β· 1 header rowβ |
L | βList Β· 5 items Β· numberedβ |
Link | Link text + ββ example.gov/budgetβ |
| Form widget | Field name + βTooltip: β¦β or βNo tooltipβ |
| Content item (Raw mode only) | βText: The fiscal yeβ¦β / βImage 640Γ480β / βAnnot: Linkβ and, in muted mono, MCID 12 Β· p3 |
4.2.2 ARIA pattern
- The container is
role="treegrid"witharia-label="Structure tree",aria-multiselectable="true",aria-rowcountset to the total number of visible (expanded) rows, andaria-colcount="4". - Each row is
role="row"witharia-level,aria-setsize,aria-posinset,aria-expanded(only on rows that have children),aria-selected, andaria-rowindex, which is required because rows are virtualized. - The cells are
role="gridcell"for Tag, Content, Issues and Page. There is one tab stop, using rovingtabindexon rows. Focus is on the row by default (APG treegrid βrow-firstβ behavior). Right arrow on an expanded row moves into the cells, where Left and Right move between cells and Left from the first cell returns to the row. - A rowβs accessible name is built explicitly (
aria-labelon the row). The implementation does not rely on concatenated cells, because verbosity differs across screen readers. - Virtualization renders the viewport Β± 50 rows. Focus is never placed on a row that isnβt mounted, and the virtualizer mounts the target row before moving focus. This holds for 10k+ nodes.
- Drag and drop is pointer-only sugar. Every drop has a keyboard op (Β§5.3).
4.2.3 Row announcement text
Template (i18n ts.tree.row.name, ICU):
{tagSpoken}, {preview}{issues, select, 0 {} other {, {issues, plural, one {# issue} other {# issues}}: {firstIssue}}}{pages}The level, set size and position come from the ARIA attributes, so the screen reader adds βlevel 3,
2 of 5β. The template must not repeat them. tagSpoken uses a spoken form, not the raw tag:
βHeading 2β, βParagraphβ, βFigureβ, βTableβ, βList itemβ, βArtifactβ. In Raw mode it becomes
βH2 (Heading 2)β, so experts hear the literal tag.
Examples (NVDA-style rendering; the ARIA suffix is added by the AT):
- βHeading 2, Budget overview, 1 issue: heading level skipped from 1, page 3, level 3, 2 of 5, collapsedβ
- βFigure, No alt text, 1 issue: missing alternate text, page 4, level 4, 1 of 3β
- βParagraph, The fiscal year closed withβ¦, page 3, level 3, 4 of 5, selectedβ
- βUntagged content, 3 runs, page 3β (orphan group pseudo-row, only in Raw mode and Reading order)
Rules for drop indicators:
- A 2px ink insertion line with a left circle marks βinsert betweenβ (sibling). A full-row outline with a 12% tint marks βinsert as last childβ.
- An illegal target shows a struck-through line and a tooltip giving the reason (βLI can only go inside Lβ).
- The drop position is also written as text in the status bar: βMove βBudget overviewβ after βTable 1β, inside Sectβ.
4.2.4 Search and filter
/ focuses the filter field above the tree. It accepts free text (matching previews) and facets,
which are chips with autocomplete:
type:Figure,has:alt,no:alt,issue:any,issue:<ruleId>,page:3-7,lang:fr,origin:ai,changed:session- Saved presets: βFigures with no altβ, βHeadings out of sequenceβ, βEmpty tagsβ, βTables without headersβ, βLinks without textβ, βUntagged contentβ, βChanged this sessionβ.
Filtering shows matching rows with their ancestor path as muted context rows (aria-level intact).
A live count (β23 matchesβ) appears in a polite live region, debounced to 500ms.
4.3 Screen reader preview (Reader)
The Reader is a linear, virtualized list of utterances, one per announceable unit in structure
order, rendered with the grammar in Β§4.3.2. It is role="list" inside a <section>, so it can be read
with browse-mode or virtual-cursor navigation. It is deliberately not an application widget, so
screen reader users can read it with their normal reading keys. Each utterance is a list item holding a
button, βEdit β¦β, that jumps selection to the node and focuses the inspector field that produced the
text.
Tabs: Reader (announcements) Β· Reflow (the document rendered as semantic HTML at the reading width, which shows what reflow and Liquid Mode would produce).
Verbosity profile selector: Generic (default) Β· NVDA-like Β· JAWS-like Β· VoiceOver-like. The profiles only change role words and ordering, for example βheading level 2β versus βheading 2β and whether the role comes before or after the name. A persistent caption reads: βPreview approximates what assistive technology announces. Always spot-check with a real screen reader.β We do not claim the preview is equivalent to a real screen reader.
4.3.1 Inline editing
Pressing Enter on an utteranceβs Edit button opens an inline editor for the governing text:
- a figure edits Alt;
- a heading offers a level picker and ActualText;
- a link edits the annotationβs Contents;
- a text node with ActualText edits ActualText.
Mod+Enter commits the edit (as a setAttr op) and Esc cancels it. After a commit, the utterance
re-renders and the new text is announced politely.
4.3.2 Announcement grammar (Generic profile)
Notation: {x} is a value, [β¦] is optional, | separates alternatives. Strings are ICU templates in
ts.reader.*.
| Structure | Utterance |
|---|---|
H1βH6 | heading level {n}, {text} |
H (unnumbered) | heading, {text} + flag βUnnumbered heading, level unknown to most ATβ |
P, Span, Quote, BlockQuote | {text} (BlockQuote: block quote, {text} β¦ out of block quote) |
L | list, {count, plural, one {# item} other {# items}} β¦ out of list |
LI | {Lbl} {LBody text}; nested list: list, {count} items, nesting level {depth} |
Table | table with {rows} rows and {cols} columns [, {caption}] β¦ out of table |
TD (first visit to a row) | row {r}, {rowHeaders}, column {c}, {colHeaders}, {text}; later cells in the same row omit the row part |
TH | {text}, column header | row header (from Scope) |
Figure | graphic, {Alt}; missing Alt: graphic + flag βNo alternate text; AT may read the file name or skip itβ |
Figure + Caption | graphic, {Alt} then {caption text} |
Formula | math, {Alt} |
Link | link, {text}; if annotation Contents differ: link, {text} + hint (description: {Contents}) |
Note | footnote, {text} (from a Note following a Reference) |
Reference | {text}, link to note {n} when an ID/Ref pair resolves |
TOC / TOCI | table of contents, list, {n} items / {text} |
| Form widget | {role: edit text|check box|radio button|combo box|button}, {TU}[, required][, {state}]; missing TU: {role}, unlabeled + flag |
Lang change | {text} with a leading chip (French). Generic mode inserts the phrase βswitches to Frenchβ once |
E (expansion) | Speaks the expansion instead of the text, with a chip abbr |
ActualText | Speaks ActualText instead of the content; chip AT |
| Artifact | Not announced. With the βShow skippedβ toggle, a muted row reads (skipped: running header "Annual Report 2025") |
| Orphan | Not reliably announced. Shown as an error row β(untagged: may be read out of order or not at all) {text}β |
| Page boundary | A thin divider βpage 4β (visual only; not an utterance) |
Flags are rendered as text chips and linked to the Issues entry. They are never color-only.
4.4 Inspector
The inspector is a non-modal panel that always shows the primary selection. Sections are disclosure
widgets (<details>-style buttons with aria-expanded):
- Identity: Type (combobox of allowed types for this parent, with a type-ahead filter and invalid types shown disabled with the reason), page(s), node path breadcrumb (each crumb is a button that selects that ancestor).
- Text alternatives: Alt, ActualText, E. There is a character counter, and lint hints appear inline below the field (βStarts with βimage ofβ; AT already says βgraphicββ).
- Language: Lang (combobox of BCP 47 tags with names such as βFrench (fr)β; βinheritβ shows the effective value βinherits en-US from Documentβ).
- Type-specific: Table (scope, headers, IDs, spans, summary) Β· List (ListNumbering) Β· Link (Contents, target URL read-only) Β· Form (TU, field name read-only) Β· Note (ID).
- Layout: BBox (x, y, w, h with units; a βRecompute from contentβ button), Placement.
- Attributes (Raw mode, or when expanded): an owner/key/value table, plus
+ Add attribute. - Issues on this node: a mini-list linked to the Issues panel.
Commit rules: text fields commit on blur, Enter (single-line) or Mod+Enter (multi-line). Each
commit is one setAttr op. Typing does not create an op per keystroke, and native text undo works
inside the field until it is committed. Esc reverts the field to its committed value. A pending
uncommitted edit is shown by a left border and the text βNot appliedβ.
Multi-selection: shared fields are editable and apply to all as one group (βSet Lang = fr on 12 nodesβ). Fields that donβt apply to every selected type are hidden and a note lists them: βAlt applies to 3 of 12 selected; select Figures only to edit.β
Document (nothing selected, or root selected): Title, DisplayDocTitle, document Lang, PDF/UA identifier, Tab order summary, RoleMap and ClassMap counts (links to Raw mode).
5. Keyboard map
5.1 Conventions and conflict policy
ModmeansCtrlon Windows/Linux andCmdon macOS. When a binding saysCtrlliterally it is Ctrl on every platform.- No shortcut ever uses the NVDA key (Insert/CapsLock), the JAWS key (Insert/CapsLock), or the
VoiceOver key (
Ctrl+Option). No shortcut usesCtrl+Alt+Arrows, which NVDA and JAWS use for table navigation, orCtrl+Alt+<letter>/Ctrl+Alt+<digit>, which are AltGr combinations on European layouts (for example AltGr+Q = @ on German keyboards). - No
Alt+<letter>for the letters that open browser menus on Windows: D, E, F, H, S, T, V, B (Chrome: Alt+F/E, Alt+D address bar; Firefox: Alt+F/E/V/S/B/T/H; Edge: Alt+F). - Browser zoom and tab keys are never captured:
Mod+=,Mod+-,Mod+0,Mod+1..9,Mod+T/W/N,Mod+Tab,Mod+L,Mod+Shift+T,Mod+F(Find in page is left to the browser; our search is/). Alt+<letter>accelerators only fire when focus is in a non-text widget (tree, canvas, grid, queues). In text fields they are passed through, because macOS Option+letter types characters.- Single unmodified letters are used only inside our own composite widgets, which screen readers switch into focus/forms mode automatically. They are never bound in the Reader, because that is a browse-mode document.
- The keymap can be customized (Settings β Keyboard). The editor refuses bindings from the reserved lists above and names the conflict: βCtrl+Alt+Down is used by NVDA and JAWS to move down a table column.β
?(in a widget) orMod+/(anywhere) opens the keyboard help dialog, filtered to the current view and mode.
5.2 Global
| Keys | Action | Notes |
|---|---|---|
F6 / Shift+F6 | Next / previous region (Mode rail β Task β Tree β Canvas β Reader β Inspector β Dock β App bar) | Browsers usually let pages capture F6; verify in usability testing (Β§16, scenario U14). Fallback: Mod+K βGo to β¦β and landmark navigation |
Mod+K | Command palette (every command, mode and panel, with shortcut hints) | Palette is a combobox + listbox dialog |
Mod+Z | Undo (group) | In a text field with uncommitted edits, native text undo runs instead |
Mod+Shift+Z, Ctrl+Y (Win) | Redo | |
Mod+S | Save a version (runs the gate, Β§11). Edits sync continuously anyway; Save makes a verified PDF version | preventDefault of the browserβs Save Page |
Mod+Shift+S | Save as new version with a note | |
Mod+Enter | Commit / Accept (context: inspector field, proposal, fix preview, mark-mode) | |
Esc | Close popover β cancel inline edit β clear selection (one per press) | NVDA may also switch from focus to browse mode on Esc; focus stays put (see Β§5.6) |
Mod+/ | Keyboard help | |
Mod+I | Toggle inspector drawer (compact layouts); focus inspector (wide) | Firefox Ctrl+I = Page Info; captured only when focus is inside the app. Palette alternative βGo to Inspectorβ |
Mod+Shift+. / Mod+Shift+, | Next / previous mode | Ordering follows the rail |
Alt+PageDown / Alt+PageUp | Next / previous issue in the whole document (selects the node, announces the issue) | |
Mod+Shift+J | Jump to pageβ¦ (dialog with number field) | Chrome Ctrl+Shift+J = DevTools console on Windows. Conflict, see Β§5.6; palette βGo to pageβ is the primary route |
5.3 Structure tree (focus in treegrid)
| Keys | Action | Op |
|---|---|---|
β / β | Previous / next row (selection follows) | β |
β | Expand; if expanded, move to first child; with cell focus, next cell | β |
β | Collapse; if collapsed, move to parent | β |
Home / End | First / last visible row | β |
PageUp / PageDown | Β± one viewport | β |
* | Expand all siblings (APG) | β |
Mod+β/β | Move focus without changing selection | β |
Space | Toggle selection of the focused row | β |
Shift+β/β, Shift+Home/End | Extend selection | β |
Mod+A | Select siblings; press again to select all visible | β |
| Printable character | Opens the inline Retype combobox seeded with that character (like typing into a spreadsheet cell). Enter commits, Esc cancels. Only types allowed under the parent are listed | retype |
F2 / Enter | Open Retype with the current type | retype |
Alt+β / Alt+β | Move node up / down among siblings | move |
Alt+β | Promote (outdent: become next sibling of parent) | move |
Alt+β | Demote (indent: become last child of previous sibling) | move |
Ctrl+[ / Ctrl+] | Promote / demote (alternative for Alt+β/β, see conflict C1) | move |
Mod+X then Mod+V | Cut then paste after the focused row; Mod+Shift+V pastes as last child. Cut rows are dimmed until pasted or Esc. Nothing is removed until the paste | move |
Alt+W | Wrap the selection in a new parent (type prompt) | wrap |
Delete | Unwrap (remove the tag and keep its children and content) | unwrap |
Shift+Delete | Mark as artifact (kind prompt with a sensible default) | artifact |
Alt+M / Alt+P | Merge with next sibling / split at focused child | mergeNodes / splitNode |
Alt+L | Select similar (Β§8) | β |
Alt+I | Focus the issue badge of this row / open its issue | β |
/ | Focus filter field | β |
Alt+R | Find on page: reveal on canvas (scroll only; ignores the βdonβt scroll if visibleβ rule) | β |
5.4 Canvas (focus in page canvas)
| Keys | Action | Op |
|---|---|---|
β/β or β/β | Previous / next item in reading order (the path numbers) | β |
Alt+arrows | Spatial move to the nearest item in that direction | β |
Alt+Shift+β | Select parent (walk up); Alt+Shift+β back to child | β |
Space, Shift+arrows, Mod+A | Toggle, extend, select all on page | β |
PageDown / PageUp | Next / previous page | β |
+ / - / 0 / 1 | Canvas zoom in / out / fit page / 100% (unmodified; browser zoom untouched) | β |
Alt+B | Mark mode (keyboard box select, Β§2.4); L inside switches to lasso | β |
| Printable character (with a content or box selection) | Tag asβ¦ type-ahead combobox (βh2β, βfigβ, βpβ). Enter applies | tagContent / retype / wrap |
Enter | Open Tag as⦠with the suggested type | same |
Shift+Delete | Artifact | artifact |
Delete | Untag (content becomes orphan). Asks for confirmation the first time per session, because it creates failures | untagContent |
Alt+L | Select similar | β |
O | Cycle overlay preset (All Β· Mode default Β· Issues only Β· None but orphans) | β |
N | Toggle reading-order numbers | β |
Alt+R | Find in tree: reveal the focused itemβs node in the tree (expands ancestors, clears a filter that hides it after asking) | β |
Find works in both directions (page β tree and tree β page) and is also on each viewβs context menu, so neither view ever loses track of the other. This is the Acrobat problem where the reading-order view desyncs from the tree.
5.5 Mode-specific keys
These apply only while focus is in that modeβs task panel or grid. Every one is also a visible button.
| Mode | Keys |
|---|---|
| Reading order | Alt+β/β move block; Alt+C βclick-sequenceβ tool (then Enter on canvas items in the new order; Mod+Enter finish); Alt+1..3 apply 1/2/3-column heuristic (proposal) |
| Headings | 1β6 set level of focused outline row; Alt+β/β promote/demote level; 0 demote heading to paragraph (P) |
| Figures | Mod+Enter save alt & next; Alt+N next without saving; Alt+A mark decorative (artifact); Alt+G request AI suggestion |
| Tables (grid) | Arrows move cell; Shift+arrows range; Alt+C toggle column header; Alt+R toggle row header; Alt+M merge cells; Alt+P split cell; Alt+Y cycle Scope (Column/Row/Both); Alt+J edit Headers/IDs for cell |
| Artifacts | Space toggle group; Alt+A artifact selected groups; Alt+U un-artifact; Alt+X exclude page from group |
| Lists | Alt+W make list from selection; Alt+β/β nest/unnest item; Alt+N cycle numbering |
| Links | Mod+Enter save & next; Alt+G AI description |
| Forms | Alt+β/β move in tab order; Mod+Enter save tooltip & next |
| Notes | Alt+K link reference β note |
| Language | Alt+G detect languages (proposal) |
| TOC | Alt+G generate from headings (proposal) |
| Suggestions dock | Mod+Enter accept; Alt+X reject; Alt+E edit then accept; β/β next/prev proposal |
| Issues dock | Enter go to node; Alt+F apply first fix; Alt+Shift+F fix all like this (preview) |
Alt+F is on the Windows reserved list. Exception: in the Issues dock it is captured only while
focus is on an issue row (a non-text widget), where Chrome/Firefox menu activation is suppressed by
preventDefault. It is listed as conflict C3 in case testing disproves this.
5.6 Screen reader and browser conflicts
| # | Binding | Conflicts with | Resolution | Status |
|---|---|---|---|---|
| C1 | Alt+β / Alt+β (tree promote/demote) | Windows browser Back/Forward. macOS Option+β/β = word navigation (only in text fields, which we skip) | Captured only in the treegrid with preventDefault. A beforeunload guard protects unsaved work if capture fails. Alternative keys Ctrl+[ / Ctrl+] are always available | Resolved with alternative; verify Firefox capture |
| C2 | F6 region cycle | Chrome/Edge/Firefox use F6 to focus the address bar | Pages can intercept F6 keydown in current Chromium and Gecko; if not, users fall back to Mod+K βGo to β¦β and landmarks | Unverified β test U14 |
| C3 | Alt+F in Issues dock | Chrome/Edge/Firefox File/app menu | Captured only on issue rows. The button βFixβ is always adjacent | Unverified β may need rebinding to Alt+Enter |
| C4 | Mod+Shift+J jump to page | Chrome/Edge DevTools console (Windows/Linux) | Primary route is palette βGo to pageβ; binding is user-rebindable | Unresolved β no universally free combo; kept for macOS only (Cmd+Shift+J free in Safari/Chrome mac), off by default on Windows |
| C5 | Mod+I | Firefox Page Info (Ctrl+I) | Captured only when focus is in the app; palette alternative | Resolved |
| C6 | Printable-character retype / tag-as | NVDA and JAWS browse-mode single-letter navigation (H, T, L, F, K, B β¦); VoiceOver Quick Nav single-key mode | The tree and canvas are composite widgets (treegrid, application), so NVDA/JAWS switch to focus/forms mode automatically. VoiceOver users with single-key Quick Nav on must turn it off (VO+Q / Quick Nav toggle); the treeβs aria-describedby hint says so on first focus per session. Accidental retype is harmless: nothing commits without Enter | Resolved with a hint; verify in U10 |
| C7 | Arrow keys in tree/canvas | VoiceOver arrow Quick Nav (Left+Right toggles) intercepts arrows | Same hint as C6. With Quick Nav off, VO passes arrows to the widget | Documented |
| C8 | Esc | NVDA: Esc in auto focus mode can drop to browse mode | Our handler still runs (NVDA passes the key on). Focus stays on the same element, so Space/Enter or NVDA+Space returns to focus mode. No action is lost | Documented; test U10 |
| C9 | Mod+Shift+. / , mode cycling | None known (Chrome/Firefox free; macOS Cmd+Shift+. = show hidden files, only in file dialogs) | β | Resolved |
| C10 | Alt+number (reading-order columns) | Firefox on Linux: Alt+1..9 switches tabs | Only in the Reading order task panel; buttons adjacent; rebindable | Partially unresolved on Linux Firefox |
| C11 | Ctrl+Y redo (Windows) | Firefox Windows: none; Chrome: History on some builds (Ctrl+H, not Y) | β | Resolved |
| C12 | * expand all | JAWS: none in forms mode; NVDA: none in focus mode | β | Resolved |
6. Task modes
Every mode has the same frame:
- Task panel (left): a queue or outline, with a progress meter (β14 of 23 figures reviewedβ) in
text and a
<progress>element. - Canvas (center): the modeβs overlay preset, with items outside the mode dimmed.
- Inspector (right): trimmed to the fields the mode needs, with a βMore propertiesβ disclosure.
- Done criteria: the modeβs rail badge turns to a check with text (βFigures: doneβ) when there are 0 failures in that category. It never shows a check while Needs-review items remain unattested. It shows βFigures: 3 to reviewβ instead.
Mode switching never loses the selection or the undo history. Each modeβs first visit shows a dismissible 2-line explainer (not a tour).
6.1 Reading order
Goal: the top-level structure order matches the logical reading sequence on every page, and there are no orphans.
ββββββ¬ββββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββΆRO β READING ORDER page 3 of 48β β p3 βΆ Path β Numbers β β INSPECTOR ββ Hd β β Possible column interleave β ββββββββββββββββββββββββββββββββββββββ β P Β· page 3 ββ Fg β [Apply 2-column order] β β β ββββββββββββββββββββββββββββββ β β Order: 4 of 9 ββ Tb β βββββββββββββββββββββββββββββ β β βββββββββββββββ βββββββββββββββ β β Continues on p4? [ ] ββ Ar β β β‘ 1 H1 Annual Report 2025 β β β ββ‘ col A β ββ£ col B β β β ββ Ls β β β‘ 2 P The fiscal yearβ¦ β β β β β β β β β β β [Merge with next βΈ] ββ Lk β β β‘ 3 P Our revenueβ¦ β β β β ββ’ β ββ€ β β β ββ Fm β β β‘ 4 P Expenses fellβ¦ β β β βββββββββββββββ βββββββββββββββ β β ββ Nt β β β‘ 5 Figure "Bar chartβ¦" β β β ββ page footer "3" (untagged) ββ β β β ββ La β βββββββββββββββββββββββββββββ β ββββββββββββββββββββββββββββββββββββββ β ββ TC β Untagged on this page: 1 [βΈ] β [Click-sequence β] [Tag asβ¦ ] [Artifact]β ββ {} β [β Prev page] [Next page βΆ] β β βββββββ΄ββββββββββββββββββββββββββββββββ΄βββββββββββββββββββββββββββββββββββββββββ΄βββββββββββββββββββββββSteps:
- Enter the mode. The queue opens on the first page with an order issue, or on page 1. A detector flags likely problems: column interleave, a z-order-driven sequence (a caption read before its figure), a top-to-bottom sweep across columns, and orphans. These appear as banners with actions.
- Review the path. The numbered path is drawn on the canvas. The task panel lists the pageβs top-level blocks in order, and each row has a drag handle (β‘) and a position number.
- Fix order, using any of these:
- Drag rows in the list, or blocks on the canvas β one
reorder(parent, perm). Alt+β/βon a row βmove(one position), announced as βMoved to position 3 of 9β.- Click-sequence tool (
Alt+C): click (or pressEnteron) blocks in the intended order. The badges renumber live as you go.Mod+Entercommits β onereordergroup;Escabandons with no op. Blocks you didnβt click keep their relative order after the sequenced ones, and a notice says so. - Tag as List: when blocks in the sequence (or a canvas box selection) look like list items, βTag asβ¦β offers List, which runs the Lists-mode βMake listβ group (Β§6.6), including nesting by indent. Acrobatβs reading-order tool has no List type.
- Apply N-column order banner β a proposal group of
reorderops, previewed as a ghost path until accepted.
- Drag rows in the list, or blocks on the canvas β one
- Resolve orphans: βUntagged on this pageβ expands to a list of orphan runs. For each run, choose
βTag asβ¦β β
tagContent(refs, parent, index, type)at the position implied by its location, or βArtifactβ βartifact(refs, kind). - Cross-page continuity: if a block continues on the next page, the βContinues on p4β checkbox
β
mergeNodes(a, b)(a paragraph split by a page break becomes one P). - Tab order (end of mode): if any page has annotations and
/Tabs β S, a banner offers βUse structure order for Tab key on all pagesβ β group ofsetTabOrder(page, S). - Review attestation: reading-order meaning cannot be machine-verified. Each page shows
βReading order on this page: Needs review Β· [Mark reviewed]β β
attest(ro:page3). The page queue shows reviewed pages with a check and the reviewerβs initials.
Ops emitted: reorder, move, mergeNodes, tagContent, artifact, setTabOrder, attest.
6.2 Headings
Goal: a logical heading hierarchy with no skipped levels, where every visually-heading text is tagged as a heading and nothing non-heading is.
ββββββ¬βββββββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββ¬ββββββββββββββββββββββ RO β HEADINGS OUTLINE β β p3 βΆ Headings only β β INSPECTOR βββΆHd β Level Text Page β ββββββββββββββββββββββββββββββββ β H3 Β· page 3 ββ Fg β [1] Annual Report 2025 1 β β ββ[H1]ββββββββββββββββββββ β β Level β β‘ ββ’ β£ β€ β₯ ββ Tb β [2] Message from the CEO 2 β β ββ[H2]ββββββββββ β β β Skips level 2 ββ Ar β [3] β Budget overview 3β β β ββ[H3]βββββ β β β Font: Archivo 18 B ββ Ls β [2] Operations 5 β β βββ Revenue (P, looks like β β Similar looking: 6 ββ Lk β [ ] ? "Revenue" (P) looks 3 β β a heading) [Make H3] β β [Select similar] ββ Fm β like a heading [H3βΎ] β ββββββββββββββββββββββββββββββββ β ββ Nt β Doc title: [Annual Report 2025] β β ββ La β Level map: 24ptβH1 18ptβH2 14βH3 β β βββββββ΄βββββββββββββββββββββββββββββββββββ΄βββββββββββββββββββββββββββββββββββ΄βββββββββββββββββββββSteps:
- The outline lists every H/H1βH6 in document order, indented by level, with a level chip (number + βHβ + indentation, never color alone). Skipped levels show a β and the text βSkips level 2β.
- Candidate headings: a detector clusters text by visual signature (font, size, weight, spacing before) and lists P nodes that look like headings as β?β rows with a suggested level.
- Set levels by pressing
1β6on an outline row, using the inspectorβs level radio group, or usingAlt+β/βfor relative moves βretype(node, Hn).0βretype(node, P). - Level map (optional): βApply level mapβ assigns levels by font-size cluster across the whole
document β a proposal group of
retypeops for review (see Β§9 for how proposals are shown). - Select similar from any heading applies the same level to everything that looks like it β
one
retypegroup. - Document title: prefilled from the first H1. Commit β
setDoc(Title)+setDoc(DisplayDocTitle, true). - Optional sectioning: βWrap sections under headingsβ β proposal group of
wrap(nodes, Sect). Off by default, because PDF/UA does not require it. - Attest: βHeading text describes its section: Needs reviewβ β
attest(headings:doc).
Ops: retype, setDoc, wrap, attest.
6.3 Figures
Goal: every Figure has meaningful Alt or is artifacted as decorative, with a caption association and a BBox.
ββββββ¬βββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββ¬ββββββββββββββββββββββ RO β FIGURES 14 of 23 reviewed β ββββββββββββββββββββββββββββββββ β FIGURE 15 Β· p12 ββ Hd β βββββββββββββββ β β β β ( ) Informative βββΆFg β Filter: [Needs alt βΎ] β β [ large figure crop ] β β ( ) Decorative β ββ Tb β ββββββββββββββββββββββββββββ β β β β artifact ββ Ar β β β£ 13 Logo (decorative) β β β ββββββββββββββββββββββββββββββββ β ( ) Text in image βββ Ls β β β£ 14 "Bar chart ofβ¦" β β β Context before: "β¦grew 12% as shown β ActualText ββ Lk β β β£ 15 No alt text β β β β below." Caption: "Figure 3: Revenue β Alt text ββ Fm β β β£ 16 No alt text β β β by quarter" β [ ]ββ Nt β β β£ 17 "image1.png" β β β β [ ]ββ La β ββββββββββββββββββββββββββββ β β¦ Suggestion (High, 91%): "Bar chartβ 0/250 Β· lint: β ββ TC β β of revenue by quarter; Q4 highest atβ Caption: [Linked β]ββ {} β β $4.2M." Reason: chart with legible β [Save & next ββ΅] ββ β β axis labels. [Use] [Edit] [Dismiss] β [Skip] βββββββ΄βββββββββββββββββββββββββββββββ΄βββββββββββββββββββββββββββββββββββββββ΄βββββββββββββββββββββSteps:
- The queue holds every Figure/Formula node plus image content that is not a Figure (orphan images, and images tagged P), grouped by status: Needs alt Β· Suspicious alt (file name, βimageβ, single word, over 250 characters, or identical to nearby text) Β· Has alt (needs review) Β· Reviewed Β· Decorative. The default filter is βNeeds attentionβ.
- The focused figure shows a large crop plus context (the text before and after in reading order, and the detected caption).
- Choose its role (radio group):
- Informative β
setAttr(Alt)(+retype(node, Figure)if it wasnβt one; +tagContentif it was orphan) - Decorative β
artifact(node, Layout) - Text in image β
setAttr(ActualText)(retypeto P/Span where appropriate) - Math β
retype(Formula)+setAttr(Alt)
- Informative β
- Alt text field: live lint for βimage ofβ, βpicture ofβ, file names, duplicated caption text (hint: βCaption already read; alt can be shorterβ), or length > 250 (hint to use a long description in adjacent text).
- Caption: if a Caption node is detected nearby, βLinked ββ means it is a child of the Figure, or
Figure and Caption are wrapped together. βLink captionβ β
move(caption, figure, 0). - BBox is computed automatically on save of Alt if missing β
setAttr(BBox)in the same group. - AI assist:
Alt+Gor βSuggestβ β proposal inline (Β§9). βUseβ puts the text into the field unapplied;Mod+Entercommits it as originai-accepted(orai-editedif changed). - List view (bulk, the Grackle-style alt-text panel): a toggle above the queue switches to a
gridwith the columns thumbnail Β· page Β· role Β· Alt (editable) Β· lint Β· AI band. Arrow keys move between cells,Enteredits, andMod+Entercommits a cell. Each commit is its ownsetAttrgroup, so undo stays granular. Sorting by page or lint lets a remediator sweep 200 figures without opening each one. Mod+Enter= save + next. Saving counts as a human attestation of this figureβs alt (the person read the image and wrote or accepted the text), so no separateattestis needed. The history records who saved it.
Ops: setAttr(Alt|ActualText|BBox), retype, tagContent, artifact, move.
6.4 Tables
Goal: a correct Table/TR/TH/TD structure, header cells with Scope (simple tables) or Headers/IDs (irregular tables), correct spans, and optional THead/TBody/TFoot.
ββββββ¬ββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββββββββββββββ¬ββββββββββββββββββββ RO β TABLES 2 of 6 done β TABLE GRID Β· Table 3 Β· page 7 [Grid β | Canvas] β CELL B1 ββ Hd β β Table 1 p3 4Γ3 β ββββββββββββ¬βββββββββββ¬βββββββββββ¬ββββββββββ β Type (β)TH ( )TDββ Fg β β Table 2 p5 6Γ4 β β A β B β C β D β β Scope [Column βΎ] βββΆTb β β Table 3 p7 5Γ4 β β ββββββΌβββββββββββΌβββββββββββΌβββββββββββΌββββββββββ€ β ID [t3-b1 ] ββ Ar β ? Table 4 p9 12Γ6 β β 1 β€β Region β€ β[Q1 β€] ββ Q2 β€ β Q3 β€ β β Spans β1 β1 ββ Ls β β Table 5 p9 3Γ2 β ββββββΌβββββββββββΌβββββββββββΌβββββββββββΌββββββββββ€ β Headers (auto) ββ Lk β ? Table 6 p14 2Γ2 β β 2 β North β₯ β 4.1 β 4.3 β 4.8 β β ββ Fm β β β 3 β South β₯ β 3.2 β 3.0 β 3.9 β β Announces as: ββ Nt β Structure: THead β β β 4 β East β₯ β 2.7 βmergedβ 2.9 β 3.1 β β "row 2, North, ββ La β Header rows: [1] β ββββββ΄βββββββββββ΄βββββββββββ΄βββββββββββ΄ββββββββββ β column Q1, 4.1" ββ TC β Header cols: [1] β β€ column header β₯ row header β span β ββ {} β Pattern: Simple (Scope) β [Header row] [Header col] [Merge] [Split] [Headers/IDsβ¦] β [Preview row βΆ]βββββββ΄ββββββββββββββββββββββββββ΄βββββββββββββββββββββββββββββββββββββββββββββββββββββ΄βββββββββββββββββββSteps:
- Pick a table from the queue. Detection covers Table nodes and table-like orphan or P regions,
which get βMake tableβ β a proposal that builds
wrap(Table),wrap(TR)andtagContent(TD)groups from the detected grid. - The grid editor replaces per-cell properties dialogs. Every header, scope, span and Headers/ID
edit happens in place, and bulk selection of rows, columns or ranges applies TH/TD and Scope in one
group. It is an ARIA
gridbuilt from the structure (rows = TR, cells = TH/TD with spans resolved). It can toggle between an abstract grid and βCanvasβ, which draws the grid over the page. Header cells are marked by a glyph (β€ column header, β₯ row header) and a bold label, in addition to the fill. - Header rows/columns: set the count on the task panel spinbutton or with
Alt+C/Alt+Ron a cell βretype(TDβTH)andsetAttr(Scope)for each cell as one group. If the table has no THead, βPut header rows in THeadβ (default on) βwrap(rows, THead)+wrap(rest, TBody). - Spans: select a range and press
Alt+M(Merge) βsetAttr(ColSpan/RowSpan)on the surviving cell +movecontent from the absorbed cells +unwrapof the empty cells. This is refused if the absorbed cells hold content that would change reading order, and the refusal explains why.Alt+P(Split) reverses it. - Pattern detection: the task panel shows βSimple (Scope is enough)β or βIrregular: needs
Headers/IDsβ. Irregular tables have multi-level headers, spanning headers, or headers mid-table.
For irregular tables, βGenerate Headers/IDsβ β group of
setAttr(ID)on all TH plussetAttr(Headers)on all TD, computed from the grid. The per-cell editor (Alt+J) is a checklist of all TH cells in the table, where ticking one adds its ID. - Split/merge tables: a table that continues across a page break β βMerge with Table 4 (p8)β
(
mergeNodes, thenunwrapof the duplicated header rows, which become artifacts viaartifactwhen they repeat). βSplit table hereβ βsplitNode. - Announcement preview: the inspectorβs βAnnounces asβ shows the Reader grammar for the focused
cell. βPreview row βΆβ plays the whole row into the live region. Screen reader users hear the same
string by pressing
Alt+Q. - Flatten layout table: βThis is a layout tableβ β
unwrapof Table/TR/TD down to their content (proposal preview first), because PDF/UA has no layout-table role. - Attest: βHeader associations are correct: Needs reviewβ β
attest(table:t3).
Ops: retype, setAttr(Scope|ID|Headers|RowSpan|ColSpan|Summary), wrap, unwrap, move,
mergeNodes, splitNode, tagContent, artifact, attest.
6.5 Artifacts
Goal: running headers and footers, page numbers, decorative rules, backgrounds and watermarks are artifacted, and real content is not.
ββββββ¬βββββββββββββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββ¬ββββββββββββββββ RO β REPEATING CONTENT (detected) β Page strip: 1 2 3 4 5 β¦ 48 β GROUP ββ Hd β [β] Header "Annual Report 2025" β ββββββββββββββββββββββββββββ β Header ββ Fg β top band Β· 46 of 48 pages β β ββββββββββββββββββββββ β β¦ β Kind [PaginβΎ]ββ Tb β β’β’β’β’β’β’β’β’β’β’ thumbnails β β ββ ββ ββ p4 β β Sub [HeaderβΎβββΆAr β [β] Page number "{n}" bottom-right β β β ββ β ββ β ββexcl.β β 46 pages ββ Ls β 48 of 48 pages β ββββββββββββββββββββββββββββ β Excluded: p4 ββ Lk β [ ] Logo image (same XObject) β β header band β page number β (title page) ββ Fm β top-left Β· 47 of 48 β β [Include p4] ββ Nt β [ ] Horizontal rule Β· 48 pages β β ββ La β ββ Already artifacts: 212 runs [βΈ] β β ββ TC β ββ Tagged but looks like artifact: 3 β β ββ {} β [Artifact 2 selected groups] β β βββββββ΄βββββββββββββββββββββββββββββββββββββββ΄ββββββββββββββββββββββββββββββββββββ΄βββββββββββββββSteps:
- Detection runs on entry (with progress if > 2s). Content is grouped by signature: position
band (top/bottom 12%, margins), normalized text (digits β
{n}, so βPage 3 of 48β matches), image XObject identity, and vector path hash. Groups list a sample, coverage (β46 of 48 pagesβ) and thumbnails. - Review a group: selecting it highlights every instance on the page strip and canvas. Pages
where the item is missing or differs are listed. Individual pages can be excluded (
Alt+X), for example a title page where βAnnual Report 2025β is real content. - Apply: βArtifact N selected groupsβ β one group of
artifact(refs, Pagination, Header|Footer| PageNum)(or Layout/Background for rules and images). This includesuntagContent/unwrapfor instances that were wrongly tagged (for examplePβ3β per page), all in the same group. - Reverse check: βTagged but looks like artifactβ and βArtifacted but looks like contentβ (text
in the body band, over 20 words, or a heading-sized font inside an artifact) are listed for review β
artifact/unartifact+tagContent. - The Already artifacts browser can
unartifactanything.
Ops: artifact, unartifact, untagContent, unwrap, tagContent.
6.6 Lists
ββββββ¬βββββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββ¬ββββββββββββββββββββΆLs β LISTS β βββββββββββββββββββββββββββββββββββ β LIST Β· p6 ββ β β List 1 p2 Β· 4 items Β· β’ β β [L]βββββββββββββββββββββββββββ β β Numbering ββ β β Looks like a list p6 β β β β[Lbl]β’ [LBody]Reduce costβ β β [Disc βΎ] ββ β 5 paragraphs start with β’ β β β[Lbl]β’ [LBody]Grow sales β β β Items 5 ββ β [Make list] β β β [Lbl]β nested item β β β Nested lists 1 ββ β ? List 3 p9 Β· Lbl missing β β βββββββββββββββββββββββββββ β β Announces: "listββ β [Split labels] β βββββββββββββββββββββββββββββββββββ β 5 items" βββββββ΄βββββββββββββββββββββββββββββββ΄ββββββββββββββββββββββββββββββββββββββ΄ββββββββββββββββββSteps:
- The queue holds existing L nodes with issues (LI outside L, missing Lbl/LBody, flat lists that look nested) plus candidates: runs of sibling P that start with a bullet glyph, numbering pattern or hanging indent.
- Make list: select the items (the candidate pre-selects them) β one group:
wrap(items, L), awrap(item, LI)for each item,splitContentat the label boundary,tagContent(Lbl)+tagContent(LBody), andsetAttr(ListNumbering)inferred from the glyph (β’βDisc, 1.βDecimal, a.βLowerAlpha). - Nest:
Alt+βon an item βmoveinto a new or existing sub-Linside the previous LIβs LBody.Alt+βunnests. - Split labels for lists that lack Lbl β
splitContent+tagContentfor each item. If the label cannot be split without changing the content stream (a glyph shared in one text show operator), the op is refused and βKeep label inside LBodyβ is offered, which is valid under PDF/UA. - Numbering:
Alt+Ncycles βsetAttr(ListNumbering).
Ops: wrap, splitContent, tagContent, setAttr(ListNumbering), move, unwrap.
6.7 Links
ββββββ¬βββββββββββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββ¬βββββββββββββββββββββββΆLk β LINKS 31 Β· 4 need attention β canvas: link boxes β LINK Β· p11 ββ β β "click here" β gevell.com/β¦ p11β β numbered in tab order β Link text (tagged) ββ β β (no tagged text) annot only p12 β β "click here" ββ β ? "https://example.gov/β¦" (raw URL) β β β Not descriptive ββ β β "Budget portal" p3 β β Description (Cont.)ββ β Tab order: 3 pages not structure [Fix]β β [Budget portal ]ββ β β β [Save & next ββ΅] βββββββ΄βββββββββββββββββββββββββββββββββββββββ΄βββββββββββββββββββββββββββ΄βββββββββββββββββββββSteps:
- The queue lists every Link annotation, joined with its tagged text. Issues: annotation with no Link node (the OBJR is missing), a Link node with no annotation, a Link without text, generic text (βclick hereβ, βmoreβ, βlinkβ), a raw URL, and the same text pointing to different targets.
- Missing structure: βTag linkβ β
wrap(textRuns, Link)+linkAnnot(annot, node). The text runs are found under the annotation rectangle, and you can adjust them with Mark mode. - Description: edit the annotationβs Contents β
setAnnot(Contents). This is recommended when the visible text is not descriptive (visible text canβt be changed without altering the page). - Tab order: the banner β
setTabOrder(page, S)group.
Ops: wrap, linkAnnot, unlinkAnnot, setAnnot(Contents), setTabOrder.
6.8 Forms
ββββββ¬ββββββββββββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββΆFm β FORM FIELDS 18 Β· tab order βΎ β βββββββββββββββββββββββββ β FIELD "applicant_nm" ββ β β‘ 1 Name β "Full name" β β [1]Name [____] β β Type: text ββ β β‘ 2 DOB β no tooltip β β β [2]DOB [____] β β β Tooltip (TU) ββ β β‘ 3 Email β "Email address" β β [3]Email [____] β β [Date of birth, MM/DDββ β β‘ 4 Agree [x] ? "Check"(vague) β β [4][x] I agree β β Required: yes (read) ββ β Tab order = structure order β β βββββββββββββββββββββββββ β Announces: "edit ββ β β β = tab stop number β text, Date of birth, ββ β β β required" βββββββ΄ββββββββββββββββββββββββββββββββββββββ΄ββββββββββββββββββββββββββββ΄βββββββββββββββββββββββSteps:
- List every widget in the current tab order (numbered squares on the canvas). Issues: no TU, a TU that duplicates the field name (βText1β), a widget missing a Form struct element/OBJR, and tab order β structure order.
- Tooltip: the field is prefilled with the nearest visible label text (suggestion, not applied).
Commit β
setAnnot(TU). - Structure: βTag fieldβ β
tagContent/wrap(Form)+linkAnnot(widget, formNode)at the position of its label in reading order. - Tab order: drag or
Alt+β/βreorders the structure position of Form nodes (move), andsetTabOrder(page, S)makes Tab follow it. The list mirrors the result immediately.
Ops: setAnnot(TU), wrap, tagContent, linkAnnot, move, setTabOrder.
6.9 Notes
- Detect superscript reference markers and note bodies (small font, bottom band or end section).
- For each pair:
retype(body, Note)+setAttr(ID)(unique, required by PDF/UA) + optionallywrap(marker, Reference), thenAlt+Klinks them (stored as a structureRef/ID association). If the author made the marker a link,linkAnnotis used. - Placement: offer βMove note after its referenceβ β
move(off by default; the reading-order trade-off is explained inline).
Ops: retype, setAttr(ID), wrap, move, linkAnnot.
6.10 Language
- Document language: required. It is prefilled from metadata or detection and committed with
setDoc(Lang). - Passages (
Alt+G): a detector proposes spans whose language differs from the documentβs, with a confidence level. Accept βsetAttr(Lang)on the node, orwrap(runs, Span)+setAttr(Lang)for inline phrases. - Filter
lang:*in the tree shows everything with explicit Lang.
Ops: setDoc(Lang), setAttr(Lang), wrap.
6.11 TOC
- Detect an existing TOC page (dot leaders, page numbers, links).
- βStructure TOCβ β
wrap(TOC), awrap(TOCI)for each entry, andlinkAnnotwhen entries are links. Nesting follows heading levels. - βCheck against headingsβ compares TOC entries with H nodes (missing entries or mismatched text) and lists the differences for review. There is no auto-fix.
- Bookmarks (Outlines) from headings: βGenerate bookmarksβ β
setDoc(Outlines)(a document-level op, shown in the change report).
Ops: wrap, linkAnnot, setDoc(Outlines).
6.12 Raw tree (expert)
ββββββ¬βββββββββββββββββββββββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββ¬ββββββββββββββββββββββββ {} β RAW TREE / type:Span lang:fr [Contents]β canvas β INSPECTOR (full) ββ β βΎ StructTreeRoot β β Span Β· p8 ββ β βΎ Document β β Type [Span βΎ] std ββ β βΎ Part β β Lang [fr ] ββ β βΎ Art β β Attributes ββ β P Bonjour Γ tousβ¦ β β Owner Key Value ββ β βΎ Span (lang fr) β β Layout BBox [β¦] ββ β MCR MCID 4 Β· p8 "Bonjour" β β [+ Add attribute] ββ β OBJR Link annot Β· p8 β β ClassMap: β ββ β RoleMap (3) ClassMap (0) ParentTree β IDTree ββ β Raw: /S /Span /P 41 0βββββββ΄βββββββββββββββββββββββββββββββββββββββββββββββββββ΄βββββββββββββββββββββββ΄βββββββββββββββββββββββ- Every standard PDF 1.7 structure type is available in Retype, plus RoleMapped custom types (shown as βCustom β maps to Pβ).
- Content items (MCR/OBJR) are visible as leaf rows, and the
Contentstoggle hides or shows them. - RoleMap editor: a table of custom β standard with validation (no cycles, and no mapping of
standard types, which PDF/UA forbids) β
setRoleMap. ClassMap editor βsetClassMap. - Attribute editor: any owner and key. Unknown keys are allowed with a warning.
- Read-only diagnostics: ParentTree and IDTree consistency, and the object number/generation for cross-reference with other tools.
- Raw mode still uses ops. There is no direct object editing.
7. Validation panel and fix-all
7.1 Model
- The engine validates incrementally. Each op returns a
validationDelta, and a full pass runs in the background after open and after bulk groups. While results are stale, the summary chip reads βCheckingβ¦β, and it never shows old numbers as current. - Each result has:
ruleId(Matterhorn checkpoint, for example13-004, and/or WCAG SC, for example1.1.1), severity, node/content refs, message, and zero or more fixes.
Status vocabulary (text + icon shape + color; never color alone):
| Status | Icon | Meaning |
|---|---|---|
| Fail | β in octagon | Machine-verified failure |
| Needs review | ? in triangle | A human must judge. Never shown as passing |
| Reviewed | β with a person glyph, βReviewed by LA Β· 09-28β | A human attested (an attest op) |
| Pass | β in circle | Machine-verified checkpoint only |
| Not applicable | β | For example, no tables in the document |
| Could not check | β | The check errored or the input is unsupported. Counted with failures in the summary (it is not a phantom pass) |
7.2 Panel layout
ββββββββββββββββββββββββββββββββββββββββββββββββββ ISSUES β 12 fail Β· ? 31 review Β· β 0 ββ Group by: [Rule βΎ] Show: [Fail β][Review β] ββ Scope: [Whole document βΎ] ββ βΎ β Figure without alternate text (13-004, ββ WCAG 1.1.1) Β· 7 ββ [Fix all like thisβ¦ (AI suggest)] ββ β Figure p4 "chart" region [Go][FixβΎ]ββ β Figure p12 [Go][FixβΎ]ββ βΎ β Content not tagged (01-005) Β· 3 ββ [Fix all like thisβ¦] ββ βΎ ? Reading order logical (09-001) Β· 48 pages ββ [Review in Reading order mode] ββ βΈ β Document title set (07-001) ββββββββββββββββββββββββββββββββββββββββββββββββββ- Grouping: by Rule (default), Page, Mode, or Severity. Each group heading is an
h3, and each issue row is a list item with a Go button (selects the node and moves focus to the canvas or tree, whichever was last used) and a Fix menu button. - Selecting any node filters the panel to βIssues on selectionβ with a βShow allβ toggle.
- The summary chip in the app bar is a button that opens the panel.
- Rules that need a human link to the mode that supports the review, rather than offering a fix.
7.3 Fix and fix-all flow
- Fix (one issue): the menu lists the fixes the engine offers for that rule, with the exact op described (βMark as artifact (Layout)β, βTag as P under Sectβ). Choosing one applies it as a group and announces βFixed: Content not tagged, page 3. 11 failures remain.β
- Fix all like thisβ¦: shown only when the fix is deterministic (no judgment needed), for
example artifacting page numbers, setting Scope on first-row THs,
setTabOrder(S), adding a missing BBox, or retypingHβHnwhere the level is unambiguous. It opens a preview sheet inside the panel. The sheet is non-modal, and focus moves to its heading:- βApply βMark as artifact (Pagination/PageNum)β to 47 issues on 47 pagesβ
- a checklist of every instance (virtualized) with thumbnails; each can be unticked
- a βShow on canvasβ toggle that steps through instances (
β/β) - a before/after validation delta: βFixes 47 Β· introduces 0β
- buttons: Apply to 47 (
Mod+Enter) Β· Cancel
- Apply β one group, origin
fixAll, labelled βFix all: artifact page numbers (47)β. One undo reverts all of them. - Judgment fixes (alt text, heading level meaning) have no Fix all. The group instead offers βSuggest for all (AI)β¦β, which routes to the Suggestions queue (Β§9) as proposals, never applied directly.
- If some ops in a fix-all are refused (their preconditions fail), the others still apply, and the result says βApplied 45 of 47. 2 skipped: [view]β with each reason. Skipped items stay in the list.
8. Select similar
- Start from any selection:
Alt+L, the Similar dock tab, or the context menu β Similar tab opens, seeded with the primaryβs signature. - Signature chips (toggle buttons with
aria-pressed). The defaults on are marked with β:- Visual: β font family Β· β size (Β±0.5pt) Β· β weight/style Β· color Β· position band (top/bottom/left margin, column)
- Structural: β current tag type Β· parent type Β· depth Β· has alt / no alt
- Content: text pattern (starts with bullet / number / βFigure N:β / all caps / same text), same image XObject, size Β±10%
- Scope: this page Β· page range Β· whole document (default)
- The live result shows β43 matches on 12 pagesβ (polite announcement, debounced), a list of matches with page and preview, and canvas highlights with the βDim everything elseβ style. Each match has an exclude checkbox.
- Select all matches (
Mod+Enter) β the shared selection becomes the match set. The inspector shows mixed values, and any op now applies to all of them as one group, originsimilar, labelled βRetype 43 similar items to H3β. - Shortcut actions in the tab: βRetype all toβ¦β, βArtifact allβ, βSet Langβ¦β, which create the same groups directly.
- βSave as ruleβ (phase 5): stores the signature plus the op as a replayable template for later documents.
A result set larger than 500 shows a warning chip (βLarge selection. Review a sample before applying.β) and a βReview 10 random samplesβ stepper.
9. AI suggestion review
9.1 What a suggestion is
A suggestion is a proposal: { id, ops[] (or group), target refs, confidence (0β1), band (High β₯ 0.85, Medium 0.6β0.85, Low < 0.6), reason (one line), model, cost, createdAgainst (document state hash) }. Proposals never change the document until accepted. The feature is branded
βGevell AIβ. For audit purposes, the history panel and the change report show the model identifier
recorded by the AI service for every AI-originated group, whether accepted or auto-applied.
9.2 Requesting suggestions
- Per item (
Alt+Gin Figures, Links, Language, TOC) or in bulk from a mode or issue group (βSuggest alt for 23 figuresβ). - Before a bulk request, a confirmation shows the estimated credits/cost and count, because every AI call is cost-tracked per the platform rules. Requests run in the background with a determinate progress bar in the Suggestions tab and status bar (βGenerating 23 suggestionsβ¦ 9 doneβ). You can keep working meanwhile.
9.3 Review queue
ββββββββββββββββββββββββββββββββββββββββββββββββββ SUGGESTIONS 23 pending Β· 4 accepted Β· 1 rej. ββ Group: [Figures alt βΎ] Sort: [Confidence βΎ] ββ βββββββββββββββββββββββββββββββββββββββββββββ ββ β β¦ Figure p12 Β· High (91%) β β ββ β Set Alt: "Bar chart of revenue by quarter;β ββ β Q4 highest at $4.2M." β ββ β Why: legible axis labels and caption. β ββ β [Accept ββ΅] [Edit] [Reject] [Show] β ββ βββββββββββββββββββββββββββββββββββββββββββββ€ ββ β β¦ Figure p4 Β· Low (48%) β ββ β Set Alt: "Decorative swirl" β artifact? β ββ β Why: no text, repeated on 3 pages. β ββ βββββββββββββββββββββββββββββββββββββββββββββ ββ [Accept all High (12)β¦] ββββββββββββββββββββββββββββββββββββββββββββββββββ- The confidence is always shown as a word + number (βHigh, 91%β), never as a color bar alone.
- Ghost preview: the focused proposal is drawn on the canvas as a dashed overlay with a βSuggestedβ chip, and as an italic ghost row in the tree labelled β(suggested)β. The Reader shows the would-be utterance with a βSuggestedβ chip.
- Accept β applies the ops as a group, origin
ai, labelled βAI: set alt on Figure p12β. The proposal moves to Accepted. - Edit β opens the inspector with the proposed values prefilled but not applied. Committing
applies with origin
ai-edited. - Reject β no document change. It is recorded in the history as a non-document entry (for quality metrics), and Restore in the Rejected list brings it back.
- Accept all High (N)β¦ β the same preview-sheet pattern as fix-all (Β§7.3), with a sample stepper.
One group, origin
ai. Low-confidence items are never included in bulk accept. They must be handled one at a time. - Staleness: if a proposalβs targets change after it was generated (an op touches them), it is marked βOutdated β the item changed since this was suggested. [Regenerate] [Dismiss]β and cannot be accepted as-is.
- Auto-apply is off by default. When a user turns it on, proposals at or above their threshold are applied on arrival (Β§9.4). Proposals below the threshold, Outdated proposals, and proposals for types the user excluded stay in this queue as normal.
9.4 Auto-apply (user setting)
Settings β Gevell AI β Auto-apply. This is a per-user setting. It is also shown, and can be changed, from the Suggestions tab header (βAuto-apply: on, 90% and above Β· [Change]β) so its state is never hidden.
ββββββββββββββββββββ Settings Β· Gevell AI βββββββββββββββββββββ Apply suggestions automatically ββ ( ) Off β every suggestion waits for review β default ββ (β) On, when confidence is at least [ 90 ]% (80β99) ββ ββ Apply automatically for: ββ [β] Alt text for figures [β] Heading levels ββ [β] Passage language [β] List detection ββ [ ] Mark as decorative (artifact) [ ] Table headers ββ ββ Auto-applied changes are marked "AI (auto)", can be undone ββ one at a time or as a batch, and still need human review ββ where a check requires it. ββ [Save settings] βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ-
The controls are a radio group (Off / On) and a labelled number input (
type="number", min 80, max 99, step 1,inputmode="numeric") with a visible range hint. The 80% floor is fixed (agreed 2026-09-28) so that Medium-band suggestions can never auto-apply. The type checklist defaults to the four types shown as ticked. Artifacting and table headers are unticked by default because a wrong change removes or reshapes content that screen reader users rely on. -
Organization lock: in the organization admin settings (Admin β Gevell AI), admins get the option βAllow members to auto-apply AI suggestionsβ. It is on by default. When an admin turns it off:
- Auto-apply is off for every member, whatever each member had set. Membersβ own choices are kept and come back if the lock is lifted.
- In Settings, the section stays visible and readable. The radio group, threshold and type checklist
are read-only (
aria-readonly), with a lock icon and the text βTurned off by your organizationβs admin. Contact your admin to change it.β This text is linked to the group througharia-describedby, so the reason isnβt color- or icon-only. The controls stay focusable, so screen reader users can find and hear the explanation. - The Suggestions tab header reads βAuto-apply: off (set by your organization)β.
- Taking effect is immediate: a suggestion batch already running finishes as normal review items, and nothing more is auto-applied. Changes that were already auto-applied stay in history as they are.
- The admin change is recorded in the organization audit log (who, when). The change report shows the auto-apply state that was in effect for each AI batch.
-
When it runs: whenever a suggestion batch completes (Β§9.2). Auto-apply never triggers a new AI request or cost by itself. The request and cost confirmation are unchanged.
-
How it applies: each qualifying proposal becomes its own group (origin
ai-auto). The groups from one suggestion batch are wrapped in a batch entry. The batch goes through the same precondition checks as any op, and refused ops stay in the queue with their reason. -
History: the batch entry reads βAI auto-applied 18 suggestions (90% and above)β. It shows an βAI (auto)β origin chip and expands to one row per change. Each row shows its label, the model identifier, the confidence (βHigh, 94%β), and the one-line reason.
β 14:40 AI auto-applied 18 suggestions (β₯ 90%) AI (auto) [Undo batch]β β Set alt on Figure p12 Β· model gv-vision-2026-09 Β· High 94% Β· "chart with legible axes" [Revert]β β Retype P to H3 p14 Β· model gv-structure-2026-09 Β· High 91% Β· "matches 18pt bold cluster" [Revert]β β β¦ 16 more -
Undo:
Mod+Zright after the batch undoes the whole batch as one step. Undo batch in history does the same at any time, subject to the dependency check in Β§10.1. Revert on a row undoes that single change as a new forward group. Undone auto-applied proposals return to the queue as Pending, marked βAuto-apply undoneβ, and they are never auto-applied again. That prevents an undo from being silently re-applied. -
Review status is unchanged by auto-apply. An auto-applied change never produces an
attest. A check that needs human review (alt text quality, heading meaning, language of passage) stays Needs review with an extra βAI (auto)β chip. In Figures mode those items appear under the filter βAuto-applied β needs reviewβ. The reviewer confirms withMod+Enter(which records the attestation) or edits. Tree rows and canvas chips carry a text βAIβ marker until the item is reviewed. -
Announcement (polite, once per batch): βApplied 18 AI suggestions automatically. They still need review. Undo with Control Z.β
-
Reports: the change report lists auto-applied changes in their own section (model, confidence, reviewed yes/no). The conformance report treats unreviewed auto-applied items as Needs review.
10. Undo and history
10.1 Semantics
- The unit of undo is the group: one keystroke, one fix-all (47 ops), one AI bulk accept, one
click-sequence commit.
Mod+Zundoes the most recent group. Announcement: βUndid: Fix all: artifact page numbers (47). Redo with Control Shift Z.β - Linear stack. A new group after undo discards the redo stack. The discarded groups stay in the history panel, collapsed as β3 undone changes (discarded)β, with Re-apply available if they still apply cleanly (validated as new ops). This covers accidentally undoing and then typing.
- Selective revert from the history panel (βRevert this changeβ) makes a new forward group of the inverse ops. The engine first checks that later groups donβt depend on the same nodes. If they do, the panel lists the dependent changes and offers βUndo back to hereβ (a linear undo of everything after it) instead. Nothing is reverted silently out of order.
- Undo of AI-accepted ops reverts the document and returns the proposals to Pending in the Suggestions queue (not lost, no re-charge). Undo of an AI bulk accept returns all N.
- Undo of fix-all restores all instances, and the issues reappear in the panel.
- Undo of
attestremoves the reviewed mark, and the item returns to Needs review. - Undo of auto-applied AI changes: one at a time or as a batch (Β§9.4).
- Undo survives save, reload and device changes. Every group is written to the local outbox (IndexedDB) immediately and synced to the server op log (Β§11.3). The server log is the record of truth. Saving creates a version marker in history (βSaved v7β), and undo can go past it, which puts the document ahead of its last version again. Opening the document on any device restores its history from the server. The op log is kept for the life of the document and is never trimmed. Deleting the document is the only thing that removes it.
- Undo is per session and linear across the synced log. If another session appended groups
(Β§11.3 take-over),
Mod+Zonly undoes this sessionβs own groups when they donβt depend on later ones. Otherwise the history panelβs dependency flow applies. - Undo restores selection and view: the selection, active mode and canvas page from before the group.
- Inline text fields use native text undo until committed. After commit,
Mod+Zundoes thesetAttrgroup.
10.2 History panel
ββββββββββββββββββββββββββββββββββββββββββββββββββ HISTORY Filter: [All origins βΎ] π ββ β Now ββ β 14:32 AI: set alt on Figure p12 ai βΆ ββ β 14:30 Fix all: artifact page numbers (47) βΆ ββ β fixAll Β· fixed 47 Β· introduced 0 ββ β 14:21 Retype 43 similar items to H3 similar ββ β 14:05 ββ Saved v7 ββ ββ β 13:58 Reorder page 3 (click-sequence) ββ βΈ 3 undone changes (discarded) [Re-apply] ββ β 13:40 Opened annual-report-2025.pdf (v6) ββββββββββββββββββββββββββββββββββββββββββββββββββrole="list". Each entry shows its label, time, origin chip, validation delta, and actions (Show affected, Revert this, Undo back to here). Expanding an entry lists its ops in plain language.- The filters (origin, page, node) and a search field make an audit trail usable in 3,000-op sessions.
- The entry labels come from the same
ts.op.*templates as the live announcements.
11. Save and export
11.1 Save (Mod+S)
- Pre-flight (β€ 1s): make sure no inline edit is uncommitted (offer to commit it or discard it).
- Serialize in the engine (WASM worker): incremental write where possible.
- Pixel-identity gate: render every page of the original and the candidate at 150 dpi (with a 300 dpi recheck of any page that differs) and compare exactly. Separately, verify that the content streams differ only in marked-content operators (BDC/BMC/EMC) and property-list references. Progress: a determinate bar βChecking page 18 of 48β¦β. Pages already proven unchanged by stream analysis skip rendering.
- Pass β upload the new version to the server, show βSaved v8β in the app bar, and announce βSaved version 8. Pages verified identical.β Validation summary numbers are stored with the version. A veraPDF PDF/UA-1 recheck then runs on the server in the background (Β§11.1.2).
- Fail β by default nothing is saved. A modal Save blocked sheet (this is one of the few modals) appears:
βββββββββββββββββββββββββ Save blocked: page appearance changed βββββββββββββββββββββββββββ 2 of 48 pages would look different after saving. Tagging must never change ββ what a page looks like, so this version was not saved. ββ ββ Page 7 ββββ 0.4% of pixels differ [View diff] ββ Page 12 ββ 0.1% of pixels differ [View diff] ββ ββ Likely cause: "Split cell B4 (Table 3)" at 14:12 β content split inside a text run. ββ [Undo that change] [Show all changes to page 7 and 12] ββ ββ [Keep editing] [Save with pixel changesβ¦] [Report a problem] βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ- View diff opens a diff viewer: before / after / difference (a magenta+ink overlay with an outline around changed regions, and a text summary βChanged region at 72,540, 110Γ18 ptβ), with keyboard stepping between regions.
- Likely cause: the engine bisects the unsaved groups (replaying subsets through the gate on the affected pages only) to name the responsible change(s). A progress indicator shows while this runs.
- Save with pixel changes⦠is the deliberate override (§11.1.1). Blocking stays the default. The
button is never the primary (filled) button, and
Enterin the sheet activates Keep editing. - The engine error is logged with context (structured logger), and an unexpected engine failure (as opposed to a legitimate diff) goes to error alerting.
11.1.1 Save with pixel changes: confirm step
Choosing Save with pixel changesβ¦ replaces the sheetβs contents with a confirm step in the same modal. Focus moves to its heading.
βββββββββββββββββββββ Save version 8 with pixel changes? ββββββββββββββββββββββ These pages will look different from version 7: ββ Page 7 0.4% of pixels Β· 1 region [View diff] ββ Page 12 0.1% of pixels Β· 1 region [View diff] ββ ββ Reason (required, kept in the change report) ββ [ Kerning shift from splitting the Q4 cell; not visible at 100%. ] ββ ββ [ ] I have viewed the differences and accept them on pages 7 and 12 ββ ββ After saving, Gevell re-renders every page and runs a veraPDF check. ββ Results appear on the version and in the change report. ββ ββ [Back] [Save version 8 with changes] βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ- Required: a reason (textarea, labelled, at least 10 characters) and the acknowledgement
checkbox. The primary button is not disabled, because disabled buttons canβt be focused and give
no reason. If either is missing, activating it shows inline errors linked with
aria-describedby, and focus moves to the first invalid field: βEnter a reason for accepting the page changes.β / βConfirm you have viewed the differences.β - The pages listed are the ones found by the save gate. A βView diffβ was not required, but the acknowledgement says so explicitly.
- Saving writes version 8 flagged βPixel changes: pages 7, 12β. It is a normal version (not a side copy) and becomes the current version. Version 7 remains restorable.
- History adds βSaved v8 with pixel changes (pages 7, 12) Β· reason: β¦β, attributed to the user.
- The announcement (polite) reads: βSaved version 8 with pixel changes on 2 pages. Verification running.β
11.1.2 Post-save verification and how results are shown
After every save the server runs a verification job on the stored file. Pixel-change saves get the full treatment:
| Check | Runs after | What it does |
|---|---|---|
| veraPDF recheck (PDF/UA-1 profile) | Every save | Independent validation of the stored file. Results are merged into the versionβs conformance data (they donβt replace our checks); disagreements are flagged |
| Per-page render-diff report | Saves with pixel changes | Server renders every page of the new version and of the original source file (not only the previous version) at 150 dpi, re-checks differing pages at 300 dpi, and records per page: % pixels changed, changed-region boxes, diff image, and the op groups that touched the page |
Where the results appear:
- Version chip in the app bar: βv8 Β· pixel changes Β· verifyingβ¦β (with a progress indicator) β βv8 Β· 2 pages changed Β· veraPDF 0 failuresβ β or, if problems are found, βv8 Β· verification found problemsβ in the error style (text + icon).
- Versions panel (History tab β Versions): each version lists its verification status, the render-diff report (an accessible HTML page with per-page before / after / diff images, alt text describing each changed region, and a data table of the numbers) and the veraPDF report.
- Announcements: a polite message when verification finishes (βVerification finished for version 8: 2 pages changed as accepted; veraPDF found no failures.β). It is assertive only if verification finds problems.
- Problems are: pages changed that the save gate did not report, a larger change than accepted (more than 2Γ the accepted % or new regions), or new veraPDF failures. In that case a non-modal banner appears on the workspace: βVersion 8 changed more than you accepted: page 9 also differs. [View report] [Restore version 7] [Keep version 8]β. Restore creates version 9 from version 7βs content plus the op log after it, re-run through the gate. Nothing is deleted.
- If the verification job fails (the job itself errors), the status is βCould not verifyβ, which is treated like a failure in reports, never as a pass. It retries automatically, and the error goes to alerting.
- Change report (Β§11.2): a βPages with visual changesβ section at the top listing every changed page with a diff thumbnail, % pixels changed, region count, the accepting user, the reason, and the responsible changes. The conformance report states βPage appearance differs from the source on pages 7, 12 (accepted by LA, 2026-09-28)β and includes the veraPDF result.
- Each later save carries the flag forward (βdiffers from source on pages 7, 12β) until a version is saved that is identical to the source on those pages again.
11.2 Export (Export βΎ)
| Output | Contents |
|---|---|
| Tagged PDF | The saved version (the export is disabled while unsaved; the button says βSave firstβ) |
| Change report (accessible HTML + tagged PDF) | βPages with visual changesβ first when any exist (Β§11.1.2); summary counts by op and origin; before/after validation; every group in plain language with page links; AI-accepted, AI-edited and AI auto-applied items (model, confidence, reviewed yes/no) listed separately |
| Conformance report (accessible HTML + tagged PDF) | veraPDF result for the version, PDF/UA-1 Matterhorn checkpoint table (Pass / Fail / Needs review / Reviewed by / N/A / Could not check) and a WCAG 2.1 AA SC table for document content. Needs-review items are listed as not yet verified, and Reviewed items name the reviewer and date. There is no single βcompliantβ badge unless every applicable item is Pass or Reviewed |
| Op log (JSON) | A replayable group/op list for audit and templates |
The export runs with a progress indicator. Report generation is cost-free (no AI). If the versionβs verification is still running, the export dialog says so and offers βExport when verification finishesβ (the default) or βExport nowβ. Export now puts βVerification pendingβ on the report.
11.3 Sync, autosave, conflicts and offline
What lives where: the source PDF, every saved version, verification reports and the op log live on Gevellβs servers (AWS: files in S3, op log and version metadata in the database). The browser runs the engine (WASM) against a local copy, and keeps an outbox in IndexedDB.
Autosave = op-log sync. There is no βunsaved workβ in the sense of losable edits:
- Every group is written to the outbox immediately, then sent to the server in order (batched, within about 2 seconds). Each group carries the server sequence number it was based on.
- Saving (
Mod+S) creates a verified PDF version, which is a separate step. Closing the tab with changes since the last version does not lose them. On reopen: βYou have 14 changes since version 7. [Keep working] [Save a version]β. beforeunloadwarns only when the outbox has not finished syncing, not merely when there is no new version.
Sync status indicator (app bar; text + icon, never icon or color alone; a button that opens a details popover):
| State | Text | Details popover |
|---|---|---|
| Synced | βSyncedβ | Last sync time |
| Syncing | βSyncing 3β¦β | Count pending |
| Offline | βOffline Β· 12 changes on this device" | "Your changes are stored on this device and will sync when youβre back online.β |
| Sync problem | βSync problem Β· retryingβ | Error in plain language, next retry time, [Retry now], [Download a backup of my changes] (op log JSON) |
| Read-only | βRead-only Β· open elsewhereβ | See the conflict flow below |
Announcements only fire on transitions to Offline, back to Synced after being offline (βBack online. 12 changes synced.β), and to Sync problem (assertive). Routine syncing is silent.
Same document open twice. Editing needs a document lease, held by one session and renewed by heartbeat every 15 seconds:
- If you open a document whose lease is held (another tab of yours, detected at once through BroadcastChannel, or another user or device, detected through the server), a non-modal choice appears before the workspace becomes editable: βThis document is being edited in another tab / by Laura Hopkins on another device (since 14:02).β [Open read-only] Β· [Take over editing] Β· [Open a copy].
- Read-only follows the live op log: new groups appear, the history updates, and edit commands are disabled with the reason βRead-only while this document is edited elsewhere.β
- Take over asks the other session to flush its outbox, then moves the lease. The other session switches to read-only and shows βEditing moved to another tab / to Larry Anglin at 14:31. Your changes up to then are synced.β (assertive, since it interrupts work).
- If the previous holder is unreachable (crashed or offline), take-over waits for the lease to expire (up to 45 seconds, with a countdown and progress) and then proceeds.
- Open a copy creates a new document from the current version plus synced ops.
Offline:
- Editing, validation (WASM), select-similar, undo/redo and the pixel-identity gate all keep working.
- AI requests, the veraPDF recheck and version upload need the network. Those buttons stay focusable and explain why: βNeeds a connection. Your request will not be queued, to avoid unexpected costs.β
- Save while offline runs the local gate and then queues the version upload: βVersion 8 will be uploaded when youβre back online.β A Download PDF now option gives a local copy marked βnot yet uploadedβ in the file name.
- Reconnecting after someone else took over (your lease expired while offline): your outbox groups are rebased onto the server head. Each group is re-validated against the current state. Groups that still apply are appended, labelled βSynced after reconnectingβ. Groups that no longer apply go to a βCouldnβt syncβ list in the History tab. Each one has its reason, [Re-apply] (after you edit the conflict) and [Discard]. [Save all as a copy] creates a separate document from your offline state. Nothing is silently dropped.
Recovery: after an engine crash or browser restart, the workspace rebuilds from the latest version plus the server op log plus any unsynced outbox groups, in that order.
12. Loading, empty and error states
12.1 Progress rule
Anything that may exceed 2 seconds shows a progress indicator. It is determinate when the total is
known, and it has text, a <progress> element, and a polite announcement at start, at 25% steps (at
most every 5s) and at the end. The UI stays interactive during background work unless the work
blocks editing (save gate).
| Operation | Indicator |
|---|---|
| Upload | Determinate bytes (β12.4 of 30 MBβ) |
| Parse / open | Determinate by objects. Progressive: page 1 and the top tree levels become interactive before the whole document is parsed; the status bar shows βLoading structureβ¦ 40%β |
| Page render | Per-page skeleton (gray page with the page number) then tiles. No spinner for < 300 ms |
| Full validation | Status bar bar + the summary chip reads βCheckingβ¦β |
| Detection (artifacts, lists, headings) | Task-panel skeleton rows + βScanning 48 pagesβ¦ 12β |
| Select similar on large docs | Count rises live; βStill searching (page 30 of 480)β |
| AI suggestions | Per-request determinate count; per-item skeleton in the queue |
| Save gate / bisect / export | Determinate by page; can be cancelled (except the final write) |
| Version upload | Determinate bytes |
| Post-save verification (render diff + veraPDF) | Version chip βverifyingβ¦β + determinate page count in the Versions panel; runs on the server and does not block editing |
| Lease take-over wait | Countdown + progress (up to 45 s) |
12.2 Empty states
| Situation | What the user sees |
|---|---|
| No file open | A drop zone with a file picker button, recent files (with version and date), and a short note that the file is uploaded to Gevell and edited in your browser. The keyboard path is the button itself |
| Untagged PDF (no StructTreeRoot) | βThis PDF has no tags. Screen readers will read it poorly or not at all.β Options: Start in Reading order mode (build structure page by page) Β· Auto-tag first (proposal-based, reviewed like AI suggestions) Β· Tag manually in Raw tree. No option is preselected |
| No text layer (scanned) | βThis PDF is images of text, so it has nothing to tag yet. It needs OCR first.β A link to the OCR path (see Q2). Figures mode still works for true images |
| Mode has nothing to do | βNo tables found in this document.β + βTag a region as a tableβ (Mark mode) + a check in the rail with the text βTables: noneβ |
| Filter with no matches | βNo nodes match type:Figure no:alt.β + Clear filter |
| All issues resolved | βNo failures. 6 items still need review.β (Never βAll clearβ while any review items remain) |
12.3 Errors
- Op refused (precondition): nothing changes. An assertive announcement plus a toast (error
toasts use
role="alert"; everything else is polite) states the reason and the alternative: βCanβt move LI here. List items must be inside a List (L). Wrap it in a list first with Alt+W.β - Password-protected: a password field (labelled,
autocomplete="off"). A wrong password says βThat password didnβt open the file. Check caps lock and try again.β - Encrypted with permissions that forbid modification: open read-only with a banner explaining that saving needs the owner password.
- Corrupt / unsupported (for example XFA forms, PDF 2.0 in phase 1): a specific message with the detected reason, plus βOpen read-only to inspectβ when parsing got far enough.
- Engine crash / out of memory (WASM): the workspace shows βThe editor stopped unexpectedly. Your changes up to 14:32 are safe.β and Recover, which reloads the engine and rebuilds from the server op log and the local outbox (Β§11.3). The error is logged and alerted.
- Network lost: the work continues offline (Β§11.3). Nothing is lost.
- Same document open elsewhere: the lease flow in Β§11.3. Real-time co-editing is phase 6.
- Large documents (over 2,000 pages or 200k nodes): a warning on open with the expected memory use, and a page-range open option.
13. Live-region announcements
Two always-mounted regions: polite (role="status") and assertive (role="alert", errors
only). The status-bar echo mirrors the last message visually (WCAG 4.1.3). Messages are short, lead
with the result, and follow the i18n templates ts.op.<name>.done.
| Event | Text (en) |
|---|---|
| Retype | βRetyped Paragraph to Heading 2.β (multi: βRetyped 43 items to Heading 3.β) |
| Move | βMoved Budget overview to position 3 of 9 in Section.β |
| Promote/demote | βPromoted to level 2.β / βDemoted into Table 1.β |
| Wrap / unwrap | βWrapped 5 items in List.β / βRemoved List tag; 5 items kept.β |
| Artifact | βMarked 47 page numbers as artifacts.β |
| Fix | βFixed: Figure without alternate text, page 12. 11 failures remain.β |
| Undo / redo | βUndid: Retype 43 items to Heading 3.β / βRedid: β¦β |
| AI ready | β12 suggestions ready for Figures.β (once per batch) |
| Accept | βAccepted suggestion: alt text on Figure, page 12.β |
| Validation done | βCheck complete: 12 failures, 31 need review.β |
| Save | βSaved version 8. Pages verified identical.β |
| Save with pixel changes | βSaved version 8 with pixel changes on 2 pages. Verification running.β |
| Verification done | βVerification finished for version 8: β¦β (assertive if problems found) |
| Auto-apply | βApplied 18 AI suggestions automatically. They still need review. Undo with Control Z.β |
| Sync transitions | βOffline. Changes are stored on this device.β / βBack online. 12 changes synced.β / βSync problemβ¦β (assertive) |
| Lease moved | βEditing moved to another tab at 14:31. This window is now read-only.β (assertive) |
| Save blocked (assertive) | βSave blocked. Two pages would change appearance. Details are open.β |
| Refused op (assertive) | as in Β§12.3 |
Selection sync, hover and background scrolling are never announced.
14. Overlay color palette
14.1 Design rules
- Nine tag families. Each is encoded three ways: color, stroke pattern, and a text label chip. Color is never the only signal (WCAG 1.4.1). With labels on (the default), every overlay can be identified without color vision.
- No green in the palette, so the brand focus ring (
#046307/#5CCB60) is unique on the canvas. This also avoids blueβgreen adjacency with the brand blue. - Light variant colors (used for canvas strokes in both themes, because the page is always drawn as printed, and for UI chips in light mode) are β₯ 4.5:1 on white. This means white chip text passes AA, and strokes exceed the 3:1 non-text minimum.
- Dark variant colors (UI chips, tree tags and legend in dark mode) are β₯ 4.5:1 against the dark
surfaces and carry ink (
#0E1116) text. - Every canvas stroke is drawn with a 1px white outer keyline, so it stays β₯ 3:1 against dark or colorful page backgrounds (flyers, reversed text).
14.2 Palette
| Family (tags) | Light (canvas + light UI) | on white | on bone | Dark UI | on #161B23 | on #0E1116 | on #1D2330 | Stroke pattern | Chip |
|---|---|---|---|---|---|---|---|---|---|
| Heading (H, H1βH6) | #B34700 | 5.50 | 5.05 | #F0862E | 6.71 | 7.34 | 6.11 | Solid 3px | H2 |
| Text (P, Span, Quote, BlockQuote, Code, Caption) | #4A5464 | 7.65 | 7.03 | #B9BEC8 | 9.27 | 10.14 | 8.43 | Solid 1px | P |
| Figure (Figure, Formula) | #A3238F | 6.57 | 6.04 | #F08BDF | 7.79 | 8.52 | 7.09 | Solid 2px + corner ticks | Fig |
| Table (Table, TR, TH, TD, THead, TBody, TFoot) | #0B4FC7 | 7.10 | 6.52 | #6FA3F5 | 6.78 | 7.41 | 6.17 | Solid 2px + inner cell grid | Tbl/TH/TD |
| List (L, LI, Lbl, LBody) | #735A00 | 6.58 | 6.04 | #E6C34A | 10.10 | 11.05 | 9.19 | Dashed 6-3, 2px | L/LI |
| Link / Note (Link, Annot, Reference, Note) | #6B2FA8 | 8.09 | 7.43 | #C9A6FF | 8.54 | 9.35 | 7.77 | Dotted 2px + underline | Lnk |
| Form (Form) | #0E1116 (ink) | 18.91 | 17.37 | #F7F5F1 (bone) | 15.87 | 17.37 | 14.44 | Double 1px-2-1px | Frm |
| Artifact | #6B7686 | 4.60 | 4.23 β | #8A94A6 | 5.65 | 6.18 | 5.14 | Dotted 1px + 40% white wash | Art |
| Untagged (orphan) | #C4001A | 6.25 | 5.74 | #FF8A80 | 7.57 | 8.28 | 6.89 | Dashed 4-2, 2px + diagonal hatch fill | ! Untagged |
| Containers (Document, Part, Sect, Div, Art, TOC, TOCIβ¦) | #4A5464 | 7.65 | 7.03 | #B9BEC8 | 9.27 | 10.14 | 8.43 | Long-dash 10-4, 1px, no fill | Sect |
β Artifact on bone is 4.23: that passes 3:1 for strokes, but artifact chips must sit on white or on
the chipβs own fill, not on bone. Chips in light mode always use white text on the family fill
(ratios = the βon whiteβ column, all β₯ 4.5). Chips in dark mode use ink text on the dark-variant fill
(ratios = the βon #0E1116β column, all β₯ 6.1).
UI state colors (not tag families):
| Element | Light | Ratio | Dark | Ratio |
|---|---|---|---|---|
| Focus ring | #046307 3px, 3px offset | 7.52 on white, 6.91 on bone | #5CCB60 | 8.37 on #161B23, 9.16 on ink |
| Selection | 2px ink #0E1116 + 2px white halo | β₯ 3:1 on any background (by construction) | same (the page is drawn as printed) | same |
| Reading-order badge | White on ink circle with white ring | 18.91 | same | 18.91 |
| Error text/chips | #B3261E on #FBE4E2 | 5.38 | #F28B84 on #33110F | 7.17 |
| Review/warning | #A24A00 on #FBEADC | 5.09 | #F0862E on #2C1B0C | 6.43 |
14.3 Color-vision check (simulated, CIELAB ΞE between family pairs)
Simulated with Machado et al. (2009) matrices at full severity. Pairs under ΞE 12 are βhard to tell apart by colorβ and are deliberately separated by stroke pattern:
| Vision | Closest pairs (ΞE) | Disambiguated by |
|---|---|---|
| Typical | Text/Artifact 14 Β· Table/Link 25 | Solid vs dotted+wash; solid+grid vs dotted+underline |
| Deuteranopia | Heading/Orphan 4 Β· List/Orphan 7 Β· Heading/List 11 | Solid 3px vs dashed+hatch fill; dashed vs hatch; solid vs dashed |
| Protanopia | Heading/List 0 Β· Table/Link 10 Β· Text/Artifact 14 | Solid 3px vs dashed; solid+grid vs dotted; solid vs dotted+wash |
| Tritanopia | Text/Artifact 14 Β· Table/Artifact 20 | Stroke patterns + chips |
With nine families, perfect separation by hue alone is impossible under protanopia with AA contrast
(all colors must sit in the dark half of the luminance range), so hue collisions are expected. The
stroke pattern and label make each family unique. Usability scenario U12 checks this with
participants who have color-vision deficiencies. A βPatterns onlyβ overlay setting renders all
families in ink with their patterns and chips, for users who prefer no color. A high-contrast mode
follows forced-colors: active: strokes use CanvasText, selection uses Highlight, and focus uses
system focus colors.
15. i18n
All UI strings are externalized from day one: flat JSON in locales/{lang}.json with one level of
dotted keys, ICU MessageFormat for plurals and select, and Intl for numbers, dates and percentages.
Tag names in Raw mode (H2, LBody) are not translated. Their spoken forms are.
| Namespace | Holds | Example key |
|---|---|---|
ts.shell.* | App bar, status bar, landmarks, region headings | ts.shell.saveState.unsaved |
ts.mode.<mode>.* | Mode names, explainers, task panel labels, done criteria (readingOrder, headings, figures, tables, artifacts, lists, links, forms, notes, language, toc, raw) | ts.mode.figures.queue.needsAlt |
ts.tree.* | Treegrid labels, row name template, filters, presets | ts.tree.row.name |
ts.canvas.* | Overlay menu, mark-mode announcements, canvas description | ts.canvas.mark.corner |
ts.reader.* | Announcement grammar templates and profiles (Β§4.3.2) | ts.reader.heading = heading level {level}, {text} |
ts.inspector.* | Field labels, hints, lint messages | ts.inspector.alt.lint.imageOf |
ts.tag.<Type>.* | name, spoken, description for each structure type | ts.tag.LBody.spoken |
ts.op.<op>.* | label (history), done (announcement), refused.<reason> | ts.op.move.refused.liOutsideL |
ts.rule.<ruleId>.* | Validation message, fix labels, help link text (ruleId uses mp-13-004, wcag-1-1-1) | ts.rule.mp-13-004.title |
ts.issues.* | Panel chrome, statuses, fix-all sheet | ts.issues.status.needsReview |
ts.similar.* | Signature chip labels, result counts | ts.similar.result.count |
ts.ai.* | Suggestion queue, confidence bands, reasons scaffolding, cost confirm | ts.ai.band.high |
ts.history.* | History panel chrome, origin chips, versions panel | ts.history.origin.aiAuto |
ts.sync.* | Sync status, lease/conflict flow, offline messages | ts.sync.state.offline |
ts.verify.* | Post-save verification, render-diff report, veraPDF summary | ts.verify.chip.problems |
ts.settings.* | Settings, including AI auto-apply and the org-locked state | ts.settings.ai.autoApply.lockedByOrg |
ts.save.* / ts.export.* | Gate progress, blocked sheet, report headings | ts.save.blocked.title |
ts.keys.* | Key names and shortcut descriptions, platform variants (.mac, .win) | ts.keys.undo.mac |
ts.state.* | Empty, loading, error states | ts.state.untagged.title |
ts.progress.* | Progress texts for >2s operations | ts.progress.validating |
Reports (change and conformance) use the same catalog under ts.export.report.*, so they can be
generated in the userβs locale. The Readerβs grammar is in the userβs UI locale; the spoken
document content stays in the documentβs language (and is marked up with lang in the Reader HTML).
16. Usability-test scenarios
Participants: at least 5 professional remediators (Acrobat Pro users), 3 of them screen reader users (NVDA, JAWS, VoiceOver on macOS), and 2 with color-vision deficiency. Test files come from the tag-tree golden corpus (#3856). Success = task complete without moderator help, within the time target, with no unintended ops in the history.
| # | Scenario | Measures / pass bar |
|---|---|---|
| U1 | Open an untagged 12-page two-column newsletter and produce a correct reading order with no orphans | β€ 15 min; zero orphans; path correct; used click-sequence or column proposal |
| U2 | Fix a skipped heading level and apply the level map document-wide, then undo the level map | β€ 3 min; one undo restores all; participant can say what was undone |
| U3 | Write alt text for 10 figures, mark 2 as decorative, link 1 caption | β€ 10 min; no suspicious-alt lint left |
| U4 | Repair a 3-level-header irregular table with a spanning header (Headers/IDs), confirming via announcement preview | β€ 8 min; announcement preview matches expected for 5 sampled cells |
| U5 | Artifact running headers, footers and page numbers on a 48-page report while keeping the title-page header as content | β€ 4 min; exclusion used; fix count correct |
| U6 | Convert bullet paragraphs to a nested list with labels | β€ 5 min; Lbl/LBody present; nesting correct |
| U7 | Fix 3 βclick hereβ links and set tab order to structure | β€ 4 min |
| U8 | Add tooltips to 8 form fields and reorder tab order to match the visual layout | β€ 6 min |
| U9 | Use select-similar to retype 40 mis-tagged captions (P β Caption), excluding two false matches | β€ 3 min; exclusions correct |
| U10 | Screen reader only: move a paragraph under a different heading, promote a heading, retype a node and read the result in the Reader, with no pointer | Completes on NVDA, JAWS and VoiceOver; every op announced; no lost focus; notes on C6βC8 |
| U11 | Review 20 AI alt-text suggestions: accept High, edit 2, reject 1, then undo the bulk accept and confirm the proposals return to Pending | Understands confidence; no Low items bulk-accepted; undo understood |
| U12 | Color-vision participants: identify the tag family of 10 highlighted regions with labels on, then with βPatterns onlyβ | β₯ 9/10 correct in both |
| U13 | Save after an op that changes page appearance (seeded), then find and undo the cause from the Save blocked sheet | Finds cause β€ 2 min; understands nothing was saved |
| U13b | Same file: choose Save with pixel changes, complete the confirm step, then find the verification result and the changed-pages list in the change report | Completes confirm without help; can state which pages changed and whether verification passed |
| U14 | Keyboard only: cycle regions with F6 across Chrome, Edge, Firefox, Safari; fall back to Mod+K where F6 fails | Records which browsers pass C2 |
| U15 | Work at 400% zoom (320px) in single-pane mode: fix one figure and one heading | Completes; no horizontal page scroll outside the canvas |
| U16 | Use fix-all for 47 page-number issues, untick 2, apply, and read the change report | Report lists 45 fixes and origin fixAll |
| U17 | Compare against Acrobat Pro: same file, same 5 tasks, timed (benchmark from epic phase 0) | Tag Studio β₯ 30% faster median; fewer errors |
| U18 | Recover after a forced engine crash mid-session | No lost ops; participant trusts the recovery message |
| U19 | Open the same document in two tabs, take over editing in the second, then go offline in the first and reconnect | Understands read-only state; no lost or silently dropped changes; can explain the βCouldnβt syncβ list if seeded |
| U20 | Turn on AI auto-apply at 90%, run suggestions on 30 figures, then find the auto-applied items, revert one, undo the rest as a batch, and review two | Finds them via history and the βAuto-applied β needs reviewβ filter; knows they are not yet reviewed |
17. Competitor pain points designed out, and parity
| Seen in (Acrobat Pro / Grackle / Siteimprove / our older editors) | Tag Studio answer |
|---|---|
| Tags, Content and Order are separate panels that each hold their own model and drift | One model, synchronized views (Β§2) |
| Modal Properties dialogs that hide the page | Non-modal inspector (Β§4.4) |
| Unreliable undo and βundo not availableβ after Reading Order tool actions | Every edit is an op with an inverse, and undo is group-level and persisted (Β§10) |
Nodes shown as β<MCID 12>β or βContainerβ | Real content previews (Β§4.2.1) |
| Drag-only reordering; tiny drop zones | Keyboard ops, cut/paste move, explicit drop indicators with text (Β§4.2.3, Β§5.3) |
| Checkers that mark βPassed manuallyβ by default or collapse review items into passes | Needs review is never a pass; attestation is explicit and reported (Β§7) |
| Auto-tag that silently rewrites structure | Proposals with preview and confidence (Β§9) |
| Saving that silently re-renders or re-encodes content | Pixel-identity gate (Β§11) |
| Acrobat Content / Reading Order panels have no undo; some axesPDF ops canβt be undone | Every op has a tested inverse, and none ship without one (Β§3) |
| Acrobatβs reading-order view desyncs from the tag tree | Both are renderings of one model; bidirectional Find on page / Find in tree (Β§5.4) |
| Acrobatβs reading-order tool has no List type | Tag as List from the sequence or canvas, including nesting (Β§6.1, Β§6.6) |
| Per-cell table properties dialogs (Acrobat, CommonLook) | In-place grid editor with bulk TH/TD, scope, spans, Headers/IDs (Β§6.4) |
| AI alt text without confidence (several tools) | Every proposal shows its confidence band, number and reason (Β§9) |
| No competitor documents how its editor works with a screen reader | This spec does: ARIA patterns, announcements and conflicts (Β§4.2, Β§5.6, Β§13), and U10 tests them |
Parity features we must match (from the phase-0 competitor teardown): wrap in a new container,
move up a level, merge, and delete empty tags (axesPDF, CommonLook) β wrap, promote, mergeNodes,
Remove empty tags. Tag Similar by font style (CommonLook) β Select similar (Β§8). List building from a
selection, including nested lists (CommonLook) β Β§6.6. Table grid with bulk TH/TD, scope, headers,
spans, merge/split and flatten layout table β Β§6.4. One-click document-wide fixes (axesPDF) β Fix
all (Β§7.3). A color-coded structure preview and screen reader preview (PAC, axesPDF) β overlays and
Reader (Β§4.1, Β§4.3). Bulk alt-text and links panels (Grackle) β Figures list view and Links mode (Β§6.3,
Β§6.7). An AI first pass with a review queue β Β§9 and the βAuto-tag firstβ empty state (Β§12.2).
18. Open questions for Larry
- Q1: Does saving Alt text in Figures mode count as human attestation (as specified), or should there be a separate explicit βReviewedβ step for audit-heavy customers?
- Q2: For scanned PDFs, should Tag Studio hand off to Gevell PDF for OCR, or refuse outright?
- Q3: Should
Mod+Shift+J(Jump to page) be dropped on Windows (conflict C4), keeping only the palette? - Q4: Should verbosity profiles name the products (βNVDA-likeβ) or stay generic (βProfile A/B/Cβ) to avoid implying equivalence? Resolved 2026-09-28: pixel-diff saves are allowed as a confirmed override (Β§11.1.1); documents, versions and the op log sync to the server (Β§11.3); AI auto-apply is an opt-in user setting with a fixed 80% floor that org admins can lock off (Β§9.4); the op log is kept for the life of the document (Β§10.1).