Skip to content

Gevell Tags workspace shell (`apps/tags`)

Phase 2 workstream A (#3962). apps/tags is a Next.js static export served as Cloudflare Workers static assets at tags.gevell.com (same recipe as apps/power). This page is the contract the other Phase 2 workstreams build against.

Routes

RouteWhatFooter + workshop bar
/LandingYes
/dashboard/Document list and uploadYes
/edit/?doc=<id>Full-screen workspace (query parameter, so the page exports statically)No (Larry, 2026-10-01)
/auth/login/, /auth/callback/Sign-in (@accessible-org/auth)No

Mount points for the views

src/workspace/WorkspaceShell.tsx lays out four regions for the viewport (spec ยง1.2โ€“1.5: wide, standard, compact drawer, single pane) and renders each viewโ€™s content from the views prop:

<WorkspaceShell
documentName={doc.name}
engine="ready"
views={{ tree: <Tree />, canvas: <Canvas />, reader: <Reader />, inspector: <Inspector /> }}
appBarStatus={<SyncStatus />} // workstream G: sync and version state
appBarActions={<UndoRedoSaveExport />} // workstream G
statusItems={<PageZoomSelection />} // views: page x of N, zoom, selection, validation
/>

src/components/edit/EditWorkspace.tsx is where the views get passed in. Inside a view:

HookFromGives
useWorkspaceDocument()workspace/document-context.tsx{ document, engine }: the stored document and the EngineDocument (a DocId in the worker). The session model (DocMirror, selection) is workstream Bโ€™s (#3963) and wraps this.
useRegisterCommands(commands)workspace/commands-react.tsxRegisters commands for as long as the view is mounted. One registration puts a command in the palette (Mod+K), on its key bindings, and in keyboard help (Mod+/). Set scope: '<region>' for keys that fire only with focus in that region; return a reason string from isEnabled to show (not hide) why it is unavailable.
useAnnouncer()workspace/announcer.tsxannounce(text) (polite) / announce(text, { assertive: true }) (errors only, spec ยง13). The status bar echoes the last message.

Views must keep every control at least var(--ws-control) and tree rows var(--ws-row): 28px compact (the default) and 44px / 40px in comfortable density, set on .ws-root[data-density]. Never hard-code a smaller target (WCAG 2.5.8). Dialogs use ModalDialog (44px close button in every density).

Engine access

Only through @accessible-org/tag-engine-web (ADR ยง8). src/engine/engine.ts creates the one client: the wasm worker in production, the replay mock in the a11y sandbox build (TAGS_ENGINE=mock). useOpenDocument downloads the bytes, opens them, asks for a password when the engine says encrypted, and watches onCrash for the Recover screen. Recovery from the server op log and the local outbox (spec ยง11.3) plugs in as OpenDocumentDeps.recover (workstream G); the default reopens the downloaded bytes.

The production build needs packages/tag-engine-web/pkg/ (npm run build:wasm -w packages/tag-engine-web); scripts/check-engine.mjs fails npm run build without it.

Document API

src/lib/documents.ts calls workstream Hโ€™s /api/v1/tag-docs (#3969, sync-api.md): list, create plus a presigned PUT with exactly the signed headers, commit of the base (version 0), get, the base download through GET /:id/versions/0, and delete. A failed PUT or commit deletes the half-made document.

The two sides share one contract (#4040). workers/api/src/__tests__/routes/tag-docs.contract.test.ts drives the real routes and writes every request and response the client relies on to src/lib/__contract__/tag-docs.json. src/__tests__/lib/documents.contract.test.ts replays that file through the client. A server change that alters a recorded shape fails the server test (CI never rewrites the file); regenerating it with npx vitest run -u src/__tests__/routes/tag-docs.contract.test.ts in workers/api changes apps/tags, which re-runs the client side.

Tests

  • Unit (npm test): registry, keys, palette, keyboard help, F6, splitters, live regions, layouts, dashboard, upload, /edit states, and useOpenDocument against the replay mock (including a real worker crash and Recover).
  • a11y and mobile gate: see accessibility-ci.md.