Skip to content

feat(shell): render the web-app chrome on the web host (0.2) - #11

Merged
dominikhorn93 merged 15 commits into
mainfrom
feat/shell-web-chrome
Oct 19, 2026
Merged

dominikhorn93 merged 15 commits into
mainfrom
feat/shell-web-chrome

Conversation

@dominikhorn93

Copy link
Copy Markdown
Member

Closes the "web-app chrome" part of #4. @miragon/modeler-shell 0.1 covered the chrome for IDE hosts; 0.2 adds the web column of guide §5.2. A Miragon web app now gets its whole chrome from mountShell({ host: "web" }), so the Event Storming web app looks and behaves like its VS Code webview.

What 0.2 adds

  • Primary action, top right: "Share" by default. Its label collapses before the palette moves to a second row (decideTopRow).
  • Legal line, bottom left: "© year Miragon GmbH", then Impressum and Datenschutz with lang="de" (the corporate-identity URLs), then the modeler's own links.
    • Where the legal line and the zoom panel would meet, the zoom panel moves up above the line (shell rule).
    • In narrow windows the line wraps instead of being cut off, and everything above it moves up with it.
  • Menu groups: File and Appearance join Model, Export and Help, in that order.
    • Appearance only appears for dark-capable modelers.
    • commandKeyLabel gives the chips for an app's own Cmd/Ctrl keys.
  • Mode before first paint: webHeadMarkup / webModeScript (§13.3), plus the runtime shell.appearance.
  • Problems popover above the pill. Every row can be reached by keyboard; rows that point at an element select it.
  • Toasts via shell.toast().
    • Two persistent live regions announce them.
    • At most three, with eviction.
    • Hovering or focusing a toast pauses it; a toast with an action never closes by itself.
    • The stack goes into the top layer above modal dialogs.
    • It moves up only where its widest card would meet the bottom row.
  • Start screen: the empty-state card. It hides the work chrome and makes the canvas inert. Its hint offers "Open a file…". The browser keeps its page-zoom keys there.
  • Drop zone: page-wide, with the §9 over and forbidden states.
  • Conformance kit: five web cases and extended geometry, aria and appearance cases, each backed by saboteurs.

Review round (in this PR)

Six commits on top of the first implementation fix what three reviews (spec, Event Storming integration, accessibility) found:

  • Toasts
    • Status toasts could go unannounced because each card was its own live region; there are now persistent live regions.
    • At 1280px the toasts sat at bottom 40 instead of 12 because of a 0.5px gap test against the whole stack; it now tests the widest card.
  • Keyboard
    • Keyboard users couldn't read past the visible rows of a problems list made of text rows.
    • The start screen swallowed the browser's zoom keys.
    • Closing a toast on the start screen dropped the focus to <body>.
    • The start screen had no keyboard way to open a file.
  • Narrow windows: at 320px the legal line cut off Datenschutz.
  • Smaller fixes:
    • Close buttons are now told apart by their toast's text.
    • The legal links say they open a new tab.
    • The canvas is the web page's main landmark.
    • The start screen's card border in high contrast now matches the other islands.
    • The docs are aligned with the Event Storming pilot.

Testing

  • npm run lint, npm test (api 78, shell 1182), npm run build, publint.
  • playwright test: 136/136. The kit runs on the ide, web and embedded hosts plus the dark-capable variants, and all saboteurs pass.
  • Each commit typechecks and passes vitest on its own.

Notes

  • The version comes from release-please: the feat commits make 0.2.0. The Event Storming pilot (Miragon/event-storming-modeler, feat/webapp-shell) runs on a local pack of this branch until 0.2.0 is published.
  • Shell rules where the guide is silent, listed for the product owner in the implementation notes:
    • toast stack position (the bottom row and the widest card);
    • the raised zoom row;
    • the wrapping legal line;
    • the text link on the start screen;
    • the browser keeping its page zoom on the start screen;
    • "dialog" in §5.8 read as modal;
    • cards sized to their content;
    • the copyright as a link.

🤖 Generated with Claude Code

dominikhorn93 and others added 15 commits October 19, 2026 14:02
API.md gains the web host (guide §5.2, web column): primary action with
label collapse, legal line, File and Appearance groups, problems popover,
toasts, empty-state card, drop zone and the web app's mode (head script
and runtime choice). Exact signatures land as stubs in exclusive
directories for three implementers: src/dom/web and src/core/web-chrome
(web-chrome), src/dom/feedback and src/core/feedback (feedback),
src/testing/web-cases.ts plus demo/web.* (kit-demo).

mountShell wires every stub (web host only; toasts and the popover also
embedded), the menu renders File, Model, Export, Appearance, Help, the
fit keeps 92px for a raised zoom row, and Viewport.newDocument() lets
every new web document open like the first. The IDE host is unchanged:
VsCodeShellOptions does not accept the web options, and the 0.1 kit and
e2e suite pass as before.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The DOM-free decisions of the web column (guide §5.7, §5.10, §5.15, §9):

- decideTopRow tries the primary action with its label, then icon only,
  then moves the palette to the second row, where the label comes back
  when it keeps 16px to the menu trigger;
- zoomRowRaised raises the zoom panel above the legal line where they
  would keep less than 8px (a shell rule the guide does not settle yet);
- dragVerdict checks an open dialog, then several files, then the MIME
  types (only items of kind "file"; an empty type never forbids);
- acceptsFile matches extensions case-insensitively and MIME types
  exactly or as type/*; a list without a usable entry accepts any file.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- webModeScript and webHeadMarkup write the guide's §13.3 head for the
  web app: a light-only modeler gets one theme-color and the contrast
  line only, a dark-capable one the color-scheme meta, both
  theme-colors and the whole script. The script is ES5, reads storage
  inside try/catch and JSON-escapes every literal (with <, U+2028 and
  U+2029), so no value ends the <script> element.
- parseAppearanceChoice and documentMode give the mode for a choice.
- toastLifetime, toastToEvict, toastStackWidth and toastStackBottom
  hold the stack's rules (§8): 2.8s, 5s for warnings and errors, never
  with an action, at most three, 8px above a bottom-row island the
  centred column would touch.
- problemSummary counts the problems list's items; the new message
  problemSeverity names a row's severity for screen readers.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…drop zone

The web column's islands (guide §5.7, §5.10, §8, §9, §2):

- primary action: a blue button at the top right whose label collapses
  before the palette moves; collapsed it is 40 × 40 with the chrome
  tooltip and an unchanged aria-label; aria-disabled stays focusable;
- legal line: copyright, Impressum and Datenschutz (lang="de") with the
  docs-site kit's URLs, then the modeler's links; up to 440px it scrolls
  sideways with the focus ring inside;
- File and Appearance groups: New diagram, Open…, Show example,
  Import… with the shared terms and icons, and the radio group System,
  Light, Dark for a dark-capable modeler only;
- empty-state card: Komet, title, help, New diagram before Show example
  and the drop hint; either button leaves the card first;
- drop zone: page-wide file drags only, 12px inset, over and forbidden
  states with the chip, accepted files to onDrop, the rest to onReject
  or a danger toast per file.

A dialog menu entry now shows its shortcut chip and aria-keyshortcuts
like an action ("Open…" as an in-page dialog keeps its ⌘O), and no
web button lifts on hover under reduced motion.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- toasts (guide §8, §9): role by status, a 4px bar and a dot in the
  status colour, an optional ghost action and Close; paused while
  hovered or focused, at most three, a repeat restarts the shown toast
  with the newer action. In the web host the stack is a manual popover
  shown again with every toast, so it stays above a showModal() dialog;
  embedded it stays inside the root.
- problems popover (§5.10): the pill opens a non-modal dialog titled
  "Problems" above it; rows with an element are buttons that close it,
  focus the canvas and call onSelectItem; the icon names the severity
  for screen readers; Escape and Tab return to the pill.
- connectWebAppearance (§13.3): the Appearance choice in localStorage
  ("miragon.darstellung"), data-cd-theme, color-scheme and both
  theme-color metas in one task with one frame without transitions,
  storage events of other tabs, and prefers-contrast mirrored on <html>.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- The toast stack's bottom is recomputed when the inspector opens or
  closes: up to 760px its bottom sheet hides zoom panel and pill.
- The top row decides again when the primary action resizes, so a label
  measured before Geist loaded does not keep a wrong decision.
- A focus the start screen hides moves to its New diagram button; one
  in the problems list moves to the canvas when the last problem goes,
  instead of falling to <body>.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The 0.1 cases learn the web host and five cases join them after cleanup;
each runs in every host and asserts outside its host that nothing of it
shows.

- geometry: primary action (40 wide while collapsed, 16px to the
  palette), legal line, the raised zoom row with its tooltip and fit,
  the popover above the pill, the toast stack at toastStackBottom, the
  centred start-screen card and the drop zone's inset;
- aria: the primary action in the tab order, the legal nav with lang="de"
  and rel="noopener noreferrer", File first and Appearance between
  Export and Help, the pill as the popover's trigger;
- appearance: web pages under emulateMedia, light-only never dark, the
  dark-capable choice stored, applied and removed in one frame;
- webChrome, emptyState, toasts, problemsList and dropZone with the new
  show hooks emptyState and toast and the harness's drop fixtures
  (DropFixtures.multiple for a modeler that takes several files).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
demo/web.html mounts every web-host option the way a Miragon web app
passes them, with the head markup of webHeadMarkup through the Vite
config and window.__webDemo for the harness. The e2e suite runs the kit
for the web host (light-only and dark-capable) against it, adds eight
saboteurs for the new cases, a style spec for the web chrome's colours,
radii and states, and a spec for the demo's own handlers. The 0.1 demo
passes its theme colours in the web host, which a dark-capable modeler
now needs.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
README: a complete web-app example (head markup, mountShell with the web
column, new documents, the start screen), a web harness for the kit and
the web demo. API.md: dialog entries with shortcut chips, the
problemSeverity message, the measured primary label, the legal line's
narrow scroll, the in-memory appearance choice, the popover's keys and
focus, repeated toasts and cards, the start screen's focus, accept
normalisation and the drop after a type verdict, DropFixtures.multiple,
and a shorter out-of-scope list. Implementation notes: a Web app host
section with the mechanics behind each island.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A web app binds Cmd/Ctrl+O and Cmd/Ctrl+S itself and shows them as
chips on its File entries. ./core labelled only the chrome keys, so every
app wrote the platform logic again ("⌘O" against "Ctrl+O", "Strg+O" in
German). commandKeyLabel(key, platform, keyNames) gives the chip, and
ariaKeyShortcutsFromLabel reads it back as aria-keyshortcuts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…dest card

Screen readers: each toast was its own role="status" or role="alert",
inserted with its text into a stack that is display: none and a closed
popover while empty, and raise() hid and re-showed the stack for every
toast. A live region created together with its content is often not
announced (NVDA with Chrome, VoiceOver with Safari), a repeated toast was
never announced again, and re-showing rebuilt every shown toast in the
accessibility tree. Two live regions now live in the root from mount on
(.cd-shell-live: role="status" polite, role="alert"); a toast's text
arrives there 100ms after the region was emptied, so a repeat speaks
again, and leaves it when the toast closes. The cards carry no live
role. The stack is re-shown only while a modal dialog is open. Each Close
button is described by its toast's text, so three of them are told
apart.

Bottom row: toastStackBottom tested the whole 480px stack against the
bottom row, so at 1280px with a GitHub link (legal line ends 7.5px before
the stack) every toast stood at 40 instead of 12, although "Link copied"
is 146px wide in the middle. ToastStackInput.cardWidth takes the widest
card's measured width; the shell passes it and recomputes whenever a
toast comes or goes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Problems list: rows without an element were plain text that never took
  the focus, and the popover consumed the arrow keys, so a keyboard could
  not read past the visible rows of a list of line findings (WCAG 2.1.1).
  Every row now takes the focus (text rows tabindex="-1"), ↑/↓/Home/End
  move through all rows and scroll the focused one into the list (only
  the list scrolls, never the root), and the popover itself shows an
  inset focus ring when it holds the focus.
- Start screen keys: Cmd/Ctrl + `+`/`−`/`0` and Shift+1 were prevented
  and did nothing, so a low-vision user could neither zoom the page nor
  reset a stored zoom. With no diagram to zoom they now reach the browser
  (KeyContext.startScreen); the diagram-js guard returns false for them
  meanwhile, so diagram-js zooms nothing either.
- Start screen focus: closing a focused toast fell back to the inert
  canvas and the hidden menu trigger, both focus() calls failed and the
  focus landed on <body>. It now goes to "New diagram", and the fallback
  checks that a target really took the focus.
- Start screen Open: the menu is hidden and a drop needs a pointer. With
  file.open the hint ends in a text link "Open a file…" with the
  shortcut chip and aria-keyshortcuts (messages.openFileLink); it is part
  of the hint, not a third action, and leaves the card up until the file
  loaded.
- The web host's canvas slot is the page's main landmark.
- High contrast: the start-screen card keeps --cd-ui-linie like every
  other island (the role already turns it into kontur, guide §5.3).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…it off

Up to 440px the legal line scrolled sideways without a visible
scrollbar: at 320 CSS px (1280px at 400 % zoom, WCAG 1.4.10) Datenschutz
was cut mid-word and GitHub could not be seen, and Datenschutz has to be
reachable for legal reasons as well. The line is now at most
100cqw − 24px wide and wraps between its entries, each separator kept
with the link after it (.cd-shell-legal-item), growing upwards from
bottom 12. The shell measures its height in whole lines
(legalLineHeight, LEGAL_LINE_HEIGHT) into --modeler-shell-legal-height
on the root, and the pill, the problems popover, the legend, the raised
zoom panel and the fit's bottom (FitLayout.legalHeight, 112 on two
lines) stand on it. Every legal link is described as opening a new tab
(messages.opensInNewTab).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…nce kit

- toasts: a role="status" and a role="alert" live region in the root
  before the first toast, the toast's text arriving in the one of its
  status, no live role on the card. Cards are found by [data-status].
- geometry: toasts at toastStackBottom with the widest card
  (ChromeMeasurement.toastCardWidth); the legal line 20 high per line and
  at most 100cqw − 24px wide, pill, legend, raised zoom panel, tooltip
  row and fit standing on its measured lines.
- webChrome: at 320px every legal link lies whole inside the line.
- problemsList: Enter on the pill, ↓ to the last row, Home and End reach
  every row, text rows included, each scrolled into the list
  (PopoverFacts.focusedRow). absent: ["problemSelection"] declares a
  modeler whose problems never point at an element, so no row selects
  and nothing is skipped.
- emptyState: the chrome keys reach the browser unprevented without
  zooming the diagram; with show.toast, closing a focused toast puts the
  focus on the card; a text link in the hint is no third action.

Four saboteurs prove the new rules (live regions that come with the
toast, a text row that cannot take the focus, a start screen that
swallows the zoom keys, a legal line that scrolls), and the style spec
covers the hint's Open link.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
API.md: the toasts' live regions, Close descriptions and the widest-card
bottom row; the problems list's focusable text rows; the start screen's
keys, focus and Open link; the wrapping legal line with its measured
height, FitLayout.legalHeight, LEGAL_LINE_HEIGHT and legalLineHeight;
commandKeyLabel; the main landmark; the new messages; the kit's new
checks, ChromeMeasurement.toastCardWidth and absent "problemSelection";
that a renderer's export takes the canvas container's own <svg> and
strips the live canvas's ARIA. The Event Storming usage now follows the
real app (copy inside the click, platform-aware chips, the counted
warning toast, the withdrawn undo offer).

README: commandKeyLabel in the web example, the Vite 8 setting that keeps
the licence comment. Implementation notes: why each of these changed,
and that "dialog" in §5.8 is read as a modal dialog.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@dominikhorn93
dominikhorn93 merged commit e08d63a into main Oct 19, 2026
4 checks passed
@dominikhorn93
dominikhorn93 deleted the feat/shell-web-chrome branch October 19, 2026 16:30
dominikhorn93 pushed a commit that referenced this pull request Oct 19, 2026
🤖 I have created a release *beep* *boop*
---


<details><summary>modeler-shell: 0.2.0</summary>

##
[0.2.0](modeler-shell-v0.1.0...modeler-shell-v0.2.0)
(2026-10-19)


### Features

* **shell:** render the web-app chrome on the web host (0.2)
([#11](#11))
([e08d63a](e08d63a))
</details>

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: miragon-release-please[bot] <292359359+miragon-release-please[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant