ADR 0010: The Angular shell owns the top bar and controls the canvas through its element
Context
The board page is to get a top bar like ariadne's: board identity and sync status on the left, actions in the middle, theme toggle and Library on the right, replacing Excalidraw's hamburger menu. The canvas is a React tree inside <elysion-canvas> (ADR 0004), the page around it is Angular. Where the bar lives decides what has to cross that boundary.
Boards live on the server: the list, creating a board, opening one by URL and (later) the user session are Angular concerns (router, HttpClient, auth). What needs the canvas is small: the sync status, the theme, and later the Library sidebar, export and import.
Decision
- The top bar is an Angular component in the shell. It sits in the board page next to
<elysion-canvas>and uses the router and the BFF client directly. - The canvas is controlled through its element, in both directions.
- In, as attributes:
board-id,yjs-server-url,theme(existing), anduser-nameanduser-color(#110: how this user appears to others; optional, a generated guest identity otherwise).readonly(a boolean attribute, #324): the canvas is a viewer's, with Excalidraw's view mode, a toolbar that only zooms and no import; the realtime service refuses a viewer's changes anyway, this is what the viewer sees instead of tools that would silently do nothing. - Out, as events:
readyanderror(existing),status({ status: 'connecting' | 'connected' | 'disconnected' }, the Yjs connection),themechange({ theme }, only when the user switches the theme inside the canvas, not when the host sets it),presence({ users: [{ id, name, color }] }, the other people on the board, only when that list changes, #110).error({ message }) is sent when the connection to the board server fails, once per outage; the canvas keeps retrying. - Provided by the host, as a property:
tokenProvider, an async function the canvas calls before every connection to the board server, the first and each reconnect, for the board-scoped WS token it puts on the/yjsURL (#312). It resolves with the token, ornullwhen the host does not want a connection (it has shown why, e.g. the user lost access: the canvas then stays disconnected and does not retry); a rejection is retried with a growing delay (1x, 2x, 4x the reconnect delay, at most 30 s) and reported once aserror. A property and not an attribute because it is a function, and because a token lives for about a minute: a token given once would be useless at the next reconnect. Without a provider the canvas connects without a token (a gateway that does not check one, which the realtime service no longer is). - Commands, as methods on the element, added when a feature needs one (
toggleLibrary(),exportBoard(format),importFile(file),insertFile(file)come with their PBIs; the shared timer (ADR 0020) addsstartTimer(durationMs),pauseTimer(),resumeTimer(),extendTimer(ms)andstopTimer(), with the eventtimerand the attributeuser-id; dot voting (ADR 0020) addsstartVoting({ name, votesPerPerson }),endVoting(),clearVotingResults()andscrollToElement(id), with the eventvoting;exportBoardtakespng,svg,pdf(ADR 0013) orexcalidraw). Methods rather than attributes, because they are actions, not state.
- In, as attributes:
- The theme is app-wide state in the shell (
ThemeService: a stored explicit choice, otherwise the system preference), written todata-themeon<html>and passed to the canvas as thethemeattribute. A switch inside the canvas arrives asthemechangeand becomes the explicit choice. - The design tokens are one package,
@elysion/design-tokens, used by the shell and the canvas (and kept in step with ariadne'sstyles.scss), instead of a copy per app. - The canvas fills the space it is given. Its root is
position: absolute; inset: 0inside the element, which the host sizes; the shell lays out top bar and canvas as a column. The dev entry gives#rootthe full viewport itself.
Consequences
- No board logic in React: the canvas stays reusable and testable without a router or HTTP client.
- Every new canvas capability is a small contract change (an event or a method) that has to be documented in the frontend spec and tested on both sides; there is no shared state object.
- Excalidraw's own hamburger menu stayed until the top bar covered what it offered (export and import, #211) and was replaced by a menu in the canvas's own toolbar (#535: Help and Clear canvas); the theme can still be switched in two places, which
themechangekeeps in step. - A host that does not size
<elysion-canvas>gets a canvas that fills its nearest positioned ancestor, which is the documented behavior, not an accident. - The alternative (a bar rendered inside the canvas) would be simpler for Library and export but would have to call back into Angular for board navigation and the session.