Every composer edits markdown source, on nldd-text-editor #279

Merged
robbertbos merged 1 commit from nldd-text-editor-composer into main 2026-08-13 15:57:43 +00:00
Owner

Closes #168.

One commit on top of main.

Two changes, and the second does not need the first

1. NLDD is imported per component instead of through the barrel (WDR-0005). import '@nldd/design-system' resolves to one pre-bundled chunk of all ~90 components; nothing tree-shakes out of it and Waggle renders 36. src/nlddImports.test.ts guards it by resolving each rendered tag through the package's own exports map - it caught a missing import for nldd-keyboard-shortcut when main's keyboard work merged in.

2. Every composer edits markdown source, on nldd-text-editor (WDR-0006). Tiptap is WYSIWYG over a node tree, and the markdown we send was reconstructed on the way out by tiptap-markdown. That reconstruction was where the bugs were: ~town-square came out \~town-square, task lists gained a blank line, hard breaks needed a custom node. The new editor's document is the markdown, so there is nothing to reconstruct. The syntax would stay on screen; it is hidden here except on the construct the caret is in.

Reply, new message and notes all use it. Notes stop being a second implementation of the same composer: they differ by one flag and one absent handler. Tiptap, ProseMirror and tiptap-markdown leave the tree.

What the subclass is for

<waggle-text-editor> extends the element over the view / buildExtensions() / reconfigure() seam it documents as protected, because four things are not in the public API:

why the element cannot do it
insert at the caret value = replaces the document and drops the caret to 0
our own @ and : typeaheads its own is @-only and writes [@Label](user:id) where Mattermost wants @username; a second autocompletion({override}) throws a config merge conflict
marker hiding visible syntax is a deliberate choice there
<u> / <mark> notes-only, no command for them

Plus task-list checkboxes (GFM parses - [ ], nothing styles it) and a quote bar (the editor styles a quote italic with no bar, while NLDD's rendered blockquote has one).

A rename of any protected member fails the build rather than failing silently - that is the reason to sit on those rather than on internals. The element is marked beta; that and hideMarkers.test.ts are what to watch on an NLDD bump.

Defects found and fixed

Each has a test that was first run against the broken behaviour.

  • The reply box had no accessible name - the element logged WCAG 4.1.2 on every open. Missed because vue-tsc never binds aria-* to a prop, so a call site that omits it type-checks like one that passes it. The prop is accessibleLabel now, and required.
  • A double-click stopped selecting a word. Forwarding padding clicks to the editor also caught clicks inside it, because a click in a shadow root is retargeted to the host and sailed past the guard; the second click collapsed the selection.
  • Escape peels one layer per press. A focused ProseMirror delivered the keydown with defaultPrevented already true, so the dialog's cancel never ran; CodeMirror does not. It now closes the link bar first, then leaves the field for the toolbar, then closes the window - the middle step is the way out now that Tab indents. Closing the link bar or emoji picker also used to drop focus on <body>.
  • Tab did nothing inside a code fence. The comment said it indented there; the fall-through it relied on only existed for Mod-]. Tab now indents by two spaces on a fence content line and Shift+Tab outdents, never on the ``` line itself (four spaces there stop it being a fence). Both directions are written out rather than taken from CodeMirror, whose indentLess rewrites a line's whole leading whitespace run and so converts pasted tabs to spaces - inside the one region shipped byte for byte.
  • **The was the last marker still on screen** when the caret was elsewhere. Hiding it was blocked by the surface: NLDD tints a block's content lines and deliberately leaves the fence lines clean, so hiding the backticks left two blank rows around the tint, and an empty block - whose only lines are fence lines - vanished. The surface now covers the fence rows, in NLDD's own classes rather than a copy of its private tokens, with a test that fails if those class names go. Language name kept as a label; an unclosed fence keeps its.
  • Dropping a text file attached it and pasted its contents into the message. CodeMirror reads a dropped file with a FileReader and inserts the text, and runHandlers only calls preventDefault, so the event still reached the dropzone and uploaded it too. Measured: dropping a notes.txt on a composer holding hallo gave GEHEIM BESTANDSINHOUDhallo plus an attachment.
  • Pasting a screenshot over selected text deleted the text. The built-in paste handler runs doPaste(view, '') when the clipboard holds only files, which replaces the selection with nothing. Both are guarded now by a domEventHandlers extension that claims the event before the built-in and lets it bubble on to the uploader.
  • A : or @ inside code opened the emoji or mention list, and Tab would have committed a pick into text Mattermost renders literally.
  • > markers were hidden on the interior lines of a quoted code block, so the code read as unquoted while the source still carried the quote.
  • Cmd/Ctrl+] and [ could turn a message into a code block. They come in with CodeMirror's defaultKeymap and indent any line by two spaces; two presses give four, which renders as <code>. macOS gives both keys to browser history, so they are swallowed rather than rebound. Tab nests a list item and Shift+Tab un-nests it, with toolbar buttons for both. On a line where indentation has no markdown meaning Tab keeps moving focus - one to three leading spaces are dropped by the renderer and the fourth makes a code block, so there is nothing useful for it to do.
  • The new-message window accepted typing while a message was being scheduled.
  • notesMarks was applied once and never watched, and toggleUnderline/toggleHighlight wrote a literal <u> whether or not the extension was loaded - only a v-if stood between that and a Mattermost message.
  • A click on the composer padding restored the old caret instead of placing it where you clicked.
  • List bullets and numbers rendered in the dimmed colour.
  • The typing surface was capped in rem, a unit that tracks the root font rather than the editor's line, at a size that ate the room the toolbar needed on a short window.

Prop cleanup

A design round asked whether the options belong in a config object. They do not: after deleting what is dead there are three real feature switches, and two of them are dependencies whose presence is the switch. But getting to that count fixed things - five switches had no caller, and three features needed two props each to stay coherent (a paperclip and its handler, a mention fetcher and its account, marks and the buttons that toggle them). Those are one value each now. 27 declared props across the three layers become 21.

Tests the work exposed as missing

  • FormatToolbar had none. It is mocked in one suite and mounted-but-never-driven in two others, so making the paperclip unconditional passed the whole suite.
  • Its twenty icon names were unguarded - a retired NLDD icon renders an empty box with no warning.
  • Nothing covered the toolbar's 457-576 px band. Deleting a rule that looked dead would have made link, bijlage and emoji reachable from nowhere; an adversarial pass over the plan caught it before it shipped.

Bundle

Same tree, three builds, gzip -9 over every emitted .js:

raw gzip entry chunk
main (barrel + Tiptap) 3284 kB 919 kB 747 kB
à la carte + Tiptap 1894 kB 529 kB 426 kB
this branch 1979 kB 556 kB 454 kB

The 39% is change 1. The editor swap costs 27 kB: CodeMirror and @lezer/markdown are larger than what they replace, and they share one copy with the element, which imports @codemirror/* as bare specifiers.

Verification

typecheck, vitest 1153, playwright 113 (3 skipped), build, eslint 0 errors, pre-commit. Driven by hand in the browser at each step.

Two things worth knowing for the next person: a typo in an optional prop name produces no diagnostic (strictTemplates is off, and turning it on is 142 errors of its own), and HMR does not rebuild the adopted stylesheet inside a shadow root - a CSS change there needs a full reload before it means anything.

Closes #168. One commit on top of main. ## Two changes, and the second does not need the first **1. NLDD is imported per component instead of through the barrel** (WDR-0005). `import '@nldd/design-system'` resolves to one pre-bundled chunk of all ~90 components; nothing tree-shakes out of it and Waggle renders 36. `src/nlddImports.test.ts` guards it by resolving each rendered tag through the package's own exports map - it caught a missing import for `nldd-keyboard-shortcut` when main's keyboard work merged in. **2. Every composer edits markdown source, on `nldd-text-editor`** (WDR-0006). Tiptap is WYSIWYG over a node tree, and the markdown we send was reconstructed on the way out by `tiptap-markdown`. That reconstruction was where the bugs were: `~town-square` came out `\~town-square`, task lists gained a blank line, hard breaks needed a custom node. The new editor's document *is* the markdown, so there is nothing to reconstruct. The syntax would stay on screen; it is hidden here except on the construct the caret is in. Reply, new message and notes all use it. Notes stop being a second implementation of the same composer: they differ by one flag and one absent handler. Tiptap, ProseMirror and `tiptap-markdown` leave the tree. ### What the subclass is for `<waggle-text-editor>` extends the element over the `view` / `buildExtensions()` / `reconfigure()` seam it documents as protected, because four things are not in the public API: | | why the element cannot do it | |---|---| | insert at the caret | `value =` replaces the document and drops the caret to 0 | | our own `@` and `:` typeaheads | its own is `@`-only and writes `[@Label](user:id)` where Mattermost wants `@username`; a second `autocompletion({override})` throws a config merge conflict | | marker hiding | visible syntax is a deliberate choice there | | `<u>` / `<mark>` | notes-only, no command for them | Plus task-list checkboxes (GFM parses `- [ ]`, nothing styles it) and a quote bar (the editor styles a quote italic with no bar, while NLDD's rendered blockquote has one). A rename of any protected member fails the build rather than failing silently - that is the reason to sit on those rather than on internals. The element is marked **beta**; that and `hideMarkers.test.ts` are what to watch on an NLDD bump. ## Defects found and fixed Each has a test that was first run against the broken behaviour. - **The reply box had no accessible name** - the element logged WCAG 4.1.2 on every open. Missed because vue-tsc never binds `aria-*` to a prop, so a call site that omits it type-checks like one that passes it. The prop is `accessibleLabel` now, and required. - **A double-click stopped selecting a word.** Forwarding padding clicks to the editor also caught clicks *inside* it, because a click in a shadow root is retargeted to the host and sailed past the guard; the second click collapsed the selection. - **Escape peels one layer per press.** A focused ProseMirror delivered the keydown with `defaultPrevented` already true, so the dialog's cancel never ran; CodeMirror does not. It now closes the link bar first, then leaves the field for the toolbar, then closes the window - the middle step is the way out now that Tab indents. Closing the link bar or emoji picker also used to drop focus on `<body>`. - **Tab did nothing inside a code fence.** The comment said it indented there; the fall-through it relied on only existed for `Mod-]`. Tab now indents by two spaces on a fence content line and Shift+Tab outdents, never on the ``` line itself (four spaces there stop it being a fence). Both directions are written out rather than taken from CodeMirror, whose `indentLess` rewrites a line's whole leading whitespace run and so converts pasted tabs to spaces - inside the one region shipped byte for byte. - **The ``` was the last marker still on screen** when the caret was elsewhere. Hiding it was blocked by the surface: NLDD tints a block's content lines and deliberately leaves the fence lines clean, so hiding the backticks left two blank rows around the tint, and an empty block - whose only lines are fence lines - vanished. The surface now covers the fence rows, in NLDD's own classes rather than a copy of its private tokens, with a test that fails if those class names go. Language name kept as a label; an unclosed fence keeps its ```. - **Dropping a text file attached it and pasted its contents into the message.** CodeMirror reads a dropped file with a `FileReader` and inserts the text, and `runHandlers` only calls `preventDefault`, so the event still reached the dropzone and uploaded it too. Measured: dropping a `notes.txt` on a composer holding `hallo` gave `GEHEIM BESTANDSINHOUDhallo` plus an attachment. - **Pasting a screenshot over selected text deleted the text.** The built-in paste handler runs `doPaste(view, '')` when the clipboard holds only files, which replaces the selection with nothing. Both are guarded now by a `domEventHandlers` extension that claims the event before the built-in and lets it bubble on to the uploader. - **A `:` or `@` inside code opened the emoji or mention list**, and Tab would have committed a pick into text Mattermost renders literally. - **`>` markers were hidden on the interior lines of a quoted code block**, so the code read as unquoted while the source still carried the quote. - **Cmd/Ctrl+] and [ could turn a message into a code block.** They come in with CodeMirror's `defaultKeymap` and indent any line by two spaces; two presses give four, which renders as `<code>`. macOS gives both keys to browser history, so they are swallowed rather than rebound. **Tab** nests a list item and **Shift+Tab** un-nests it, with toolbar buttons for both. On a line where indentation has no markdown meaning Tab keeps moving focus - one to three leading spaces are dropped by the renderer and the fourth makes a code block, so there is nothing useful for it to do. - **The new-message window accepted typing while a message was being scheduled.** - **`notesMarks` was applied once and never watched**, and `toggleUnderline`/`toggleHighlight` wrote a literal `<u>` whether or not the extension was loaded - only a `v-if` stood between that and a Mattermost message. - **A click on the composer padding restored the old caret** instead of placing it where you clicked. - **List bullets and numbers rendered in the dimmed colour.** - **The typing surface was capped in `rem`**, a unit that tracks the root font rather than the editor's line, at a size that ate the room the toolbar needed on a short window. ## Prop cleanup A design round asked whether the options belong in a config object. They do not: after deleting what is dead there are three real feature switches, and two of them are dependencies whose presence is the switch. But getting to that count fixed things - five switches had no caller, and three features needed two props each to stay coherent (a paperclip and its handler, a mention fetcher and its account, marks and the buttons that toggle them). Those are one value each now. 27 declared props across the three layers become 21. ## Tests the work exposed as missing - **FormatToolbar had none.** It is mocked in one suite and mounted-but-never-driven in two others, so making the paperclip unconditional passed the whole suite. - **Its twenty icon names were unguarded** - a retired NLDD icon renders an empty box with no warning. - **Nothing covered the toolbar's 457-576 px band.** Deleting a rule that looked dead would have made link, bijlage and emoji reachable from nowhere; an adversarial pass over the plan caught it before it shipped. ## Bundle Same tree, three builds, `gzip -9` over every emitted `.js`: | | raw | gzip | entry chunk | |---|---|---|---| | `main` (barrel + Tiptap) | 3284 kB | 919 kB | 747 kB | | à la carte + Tiptap | 1894 kB | 529 kB | 426 kB | | this branch | 1979 kB | 556 kB | 454 kB | The 39% is change 1. The editor swap *costs* 27 kB: CodeMirror and `@lezer/markdown` are larger than what they replace, and they share one copy with the element, which imports `@codemirror/*` as bare specifiers. ## Verification `typecheck`, `vitest` 1153, `playwright` 113 (3 skipped), `build`, `eslint` 0 errors, `pre-commit`. Driven by hand in the browser at each step. Two things worth knowing for the next person: a typo in an *optional* prop name produces no diagnostic (`strictTemplates` is off, and turning it on is 142 errors of its own), and HMR does not rebuild the adopted stylesheet inside a shadow root - a CSS change there needs a full reload before it means anything.
`import '@nldd/design-system'` resolves to dist/components/components.js: one
pre-bundled chunk holding every component, with CodeMirror inlined. Nothing
tree-shakes out of it, and Waggle renders 36 of the ~90 elements.

Replacing it with the 33 subpath imports that cover those 36 tags cuts the
built JS from 3202 kB raw / 911 kB gzip to 1845 kB / 530 kB - 42% off both,
and the entry chunk drops from 2726 kB to 1535 kB raw.

The barrel also cannot coexist with a subpath import: both call
customElements.define('nldd-icon', ...) and the second throws. That blocked
loading any single component on its own.

nlddImports.test.ts guards it. It scans the templates for nldd-* tags and
resolves each import through the package's own exports map to the tags that
module actually defines, so a rename upstream fails the test rather than
leaving a stale constant behind. Resolution is deliberately non-transitive:
./icon-button happens to pull in nldd-icon, and relying on that would let a
tag we render lose its registration when NLDD reshuffles an internal import.

Note the subpaths are per source module, not per tag: ./menu also defines
nldd-menu-item and nldd-menu-divider, ./form-field also defines
nldd-form-field-help-text.
Evaluation build for #168. A dev-only switch in the top nav swaps the composer
between Tiptap and NLDD's <nldd-text-editor>; production always gets Tiptap and
the switch does not render. Notes stay on Tiptap either way - they use the
underline and highlight marks, which the NLDD engine has no command for.

The toolbar is shared rather than rebuilt. FormatToolbar now takes a
ComposerEditorApi instead of a Tiptap Editor, with an implementation per
engine, so both get the same buttons, the same overflow behaviour and the same
active states and cannot drift. The mention and emoji dropdowns are Vue
components over the existing CSS modules, so they are pixel-identical to the
Tiptap ones.

WaggleTextEditor subclasses the element for the three things its public API
lacks: insert-at-caret (value = replaces the document and drops the caret to
0), extra CodeMirror extensions, and a caret rect to anchor a typeahead on.
NLDDCodeMirrorElement declares view and buildExtensions() protected for this,
and re-runs buildExtensions() on reconnect, so the override survives a remount.

hideMarkers.ts hides the markdown syntax markers except on the construct the
caret is in, and draws `- [ ]` as a real checkbox whose click rewrites the
source character. It is a display layer only: the value stays byte-exact.

The element's own @-typeahead is unused. It inserts `[@Label](user:id)` where
Mattermost needs `@username`, only triggers on `@` (so no `:shortcode` - a
second autocompletion source throws on the override facet), has no word
boundary so `piet@sam` opens it, and cannot render avatars. Ours does all four.

@codemirror/{view,state,language} become direct dependencies; they were only
reachable by hoisting. The ranges match NLDD's, so npm keeps one copy.

Known difference while evaluating: the NLDD engine uses NLDD's own markdown
link command rather than Waggle's LinkBar, which is built on Tiptap link marks.
All confirmed by reproduction, most in a browser against the real component.

Correctness:
- hideMarkers hid fenced-code fences. `CodeMark` is the ``` node as well as an
  inline backtick, so leaving a code block blanked both fence lines and left the
  language name floating as loose text. Each inline marker now has to sit under
  the node it is a marker of. hideMarkers.test.ts covers it; the pure range
  computation was split out of the ViewPlugin so it is testable without a view.
- A mention fetch that resolved after the list had closed reopened it, and could
  overwrite a newer query. close() now bumps the same guard refresh() does.
- The typeahead never recomputed on caret movement, so clicking or arrowing away
  left the open list anchored to its original range and Enter replaced the wrong
  text. It now listens to nldd-text-editor-state, which also fires on selection.
- A bare `:` listed a recent emoji twice (and gave Vue two rows with one key).
- The mention query could not contain a space, so `@anne schuth` closed the list
  halfway; the Tiptap composer allows it.

Accessibility:
- The combobox relationship sat on the host, where assistive tech never sees it:
  focus is on CodeMirror's contenteditable inside the shadow root. role,
  aria-expanded, aria-controls and aria-activedescendant are now set there.
- Arrow keys did not scroll the highlighted option into view.
- The task checkbox was in the tab order without an accessible name. It is a
  pointer affordance over source text that already reads as `[ ]`, so it is
  aria-hidden and not focusable.
- The checkbox ignored readOnly: a raw dispatch bypasses the facet, so a
  disabled composer still ticked boxes.

Build:
- A production build emitted the 611 kB CodeMirror chunk even though nothing can
  reach it there. The env check is now inline in MessageEditor rather than read
  off the store, so the branch folds at build time and the chunk is gone
  (verified on a clean build).
Alignment and spacing:
- The editor text sat 5px from the box border while the toolbar's button labels
  sat at 12px, so the row below read as indented. The wrapper now carries the
  same 0.5rem `.md-prose` gives the Tiptap editor, so both engines line up
  identically. On the wrapper, not the editor: NLDD drives .cm-content padding
  from private `--_` tokens.
- .composer-footer had 8px below and 4px above; it is 4px both ways now. Shared
  chrome, so this tightens the Tiptap composer and the notes editor too.

Mentions looked broken in a new message. They were not: with no channel chosen
there is nothing to scope the search to, and mentionFetch returns nothing - the
same on both engines. The difference was that the Tiptap composer still opens
the list with "Begin met typen om iemand te vinden in dit kanaal", while this
one closed silently. It now shows the same empty state.

Task lists:
- The toolbar's takenlijst button left the caret in front of the marker it had
  just inserted, so the next keystrokes landed before it and pushed `- [ ] ` to
  the end of the line. It edits only the line prefix now, and CodeMirror maps
  the caret through the change by itself.
- The checkbox never gave way to its source. Every other marker here comes back
  when the caret is on it - that is the whole point of a hybrid source editor -
  so the checkbox does too, and `- [x] ` is hand-editable again.

Blockquotes in rendered markdown take NLDD's own border token instead of a
hand-rolled 2px, and keep the normal content colour: a quoted message is a
sentence the reader reads, not a label on other text (CLAUDE.md's rule for
--semantics-content-secondary-color). The indent stays Waggle's - NLDD's
blockquote spacing is 34px, sized for a content page, not a card. Note the
editor's own quote styling was already pure NLDD (italic, larger, hanging
indent); the bar only ever existed in the rendered view.
NLDD styles a quote italic and one step larger but gives it no bar
(text-editor.styles.js:351-355), so a short quote is hard to pick out from the
line above it. The Tiptap composer has drawn one all along
(MarkdownEditor.vue:213), and that is the version to keep.

A line decoration rather than a marker style, so it survives whether or not the
`> ` itself is hidden, and it borrows NLDD's own --semantics-blockquotes-border
instead of inventing a width - the token their rendered blockquote already uses
in rich-text.css. That inconsistency inside NLDD (a bar when rendered, none in
the editor) is worth raising upstream.
The bar sat flush against the quoted text. NLDD hangs the `> ` in a gutter -
`text-indent: -x` against an equal `padding-left` - and writes both as an inline
style on its own line decoration, so a border on the line lands exactly where
that gutter puts the text, and no class-based rule can push it away.

Rather than fight the inline style, the line shifts right and the bar is painted
in the reclaimed margin by a pseudo-element. NLDD's indent stays untouched, and
the gap between bar and text measures 12px - the same as
`.md-prose blockquote` gives the Tiptap editor.
Three things from hands-on use.

A task line under the caret showed NLDD's styled bullet dot followed by a raw
`[ ]` - half rendered, half source. The rule everywhere else in this editor is
that the caret's line shows what is actually written, so the dot now gives way
to the literal `-` there, for plain bullets as well as task items. NLDD draws
the dot with `color: transparent` plus a ::before on `.cm-md-bullet`, both
class-based, so a theme rule scoped to the caret's line is enough - no reaching
past an inline style this time.

The task checkbox was the browser's default 13px, small beside 16px body text;
it is 16px now, like the Tiptap composer's.

The editor had 5px above the first line where the Tiptap one has 13px. The
wrapper's padding goes from inline-only to all four sides.
Ordered lists now match bullets: the number keeps the normal content colour
instead of NLDD's dimmed marker grey. `**` or `#` are scaffolding you look past,
but `1.` is the list's visible numbering - the thing you read - and a bullet
gets a full-strength dot in the same spot. Only the colour changes; the marker
keeps NLDD's monospace, whose fixed advance its hanging indent is measured in.

Backspace right after a list marker deletes one character instead of the whole
`- `. NLDD binds that shortcut at Prec.highest, so an appended extension cannot
outrank it and the key is intercepted before CodeMirror sees it. It contradicted
the rest of the editor: on the caret's line you are editing source, and `## `
next to it already deleted a character at a time.

The quote bar takes the divider colour at 3px, like the Tiptap composer.
NLDD's --semantics-blockquotes-border paints it in the *content* colour, as loud
as the text it marks. (A bar that stops and restarts is two blockquotes with a
blank line between them - real markdown structure, and a source editor shows
that blank line. Contiguous quote lines measure a 0px gap.)

The toolbar's overflow container moves from the footer to the toolbar's own
slot. The footer also holds the send button, so the query told the toolbar it
had ~144px more room than it had, and there was a band of widths where the
buttons "fit" and then wrapped to a second row. Breakpoints drop by that amount.
`flex: 1` makes the slot exactly the available space, with or without a send
button beside it - the notes editor has none.

Long messages no longer push the toolbar off the bottom: the text area caps at
24rem and scrolls, the toolbar stays put.
A disabled composer was still editable on the NLDD engine. Four of the
subclass's mutators dispatch straight at the view, and a raw dispatch bypasses
the readOnly facet - NLDD's own commands check it for exactly this reason - so
the toolbar, the emoji picker and both typeaheads still wrote into a composer
that was mid-send. They go through a writableView getter now. Verified in the
browser: all four are no-ops while readonly and work again after.

@lezer/markdown was a phantom dependency, imported by two new tests and declared
nowhere. Same class of problem as the @codemirror/* declarations earlier in this
branch; it is a devDependency now and dedupes to the copy lang-markdown pulls.

Blockquotes drifted apart inside this branch: the editor drew the Tiptap bar
(3px, divider colour) while rendered markdown had taken NLDD's token, which
paints in the content colour. Both are the Tiptap bar now, so a quote looks the
same while you write it and after it is sent. The quoteBar comment claimed to
use the NLDD token it had stopped using.
The typeahead advertised a combobox it could not wire. `aria-controls` and
`aria-activedescendant` are IDREFs and do not cross a shadow boundary, so
pointing them from CodeMirror's contenteditable at a light-DOM listbox
referenced nothing - and stamping role="combobox" on top dropped the textbox
role the editor actually has. Both are gone. The list is announced through a
polite live region instead: less precise than a combobox, but true. Doing it
properly needs an API on the component, which goes on the upstream list.

The task checkbox was aria-hidden on the argument that the source `[ ]` is what
gets read. That was wrong: the widget *replaces* that text, so assistive tech
got no task state at all. It keeps its checked state and a name now, and stays
out of the tab order - the line is edited as text and the caret already reveals
the raw `- [x] `.

MessageEditor kept both engines' handles after a switch, so focus() and the
emoji picker went on driving a detached element. Each ready-handler now clears
the other.

Also: `waggle-text-editor` was missing from isCustomElement in both vite and
vitest config, so Vue warned on every mount; and a z-index referenced
`--z-overlay`, which exists nowhere in this repo (a dead var() is silent).
Close the last review findings
Some checks failed
CI / pre-commit (pull_request) Successful in 3m33s
CI / release-scripts (pull_request) Successful in 10s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 57s
CI / frontend-test (pull_request) Failing after 5m5s
security-scan / Python SAST (bandit) (pull_request) Successful in 38s
security-scan / JS SCA (npm audit) (pull_request) Successful in 37s
security-scan / SBOM (trivy) (pull_request) Successful in 18s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 40s
test-build / build (backend) (pull_request) Successful in 2m35s
test-build / build (frontend) (pull_request) Successful in 2m42s
test-build / build (pull_request) Successful in 0s
CI / e2e (pull_request) Failing after 10m29s
CI / backend-test (pull_request) Successful in 13m12s
2f97b162b1
The typeahead's listbox had no accessible name and its empty state was an
invalid owned element - a listbox may only own options, and that row is a
message. Both lists are named now and the message is role="presentation", the
same shape the Tiptap dropdown uses. The live region repeats that message
verbatim instead of flattening it to "Geen resultaten"; on a new message with no
channel chosen, "Begin met typen om iemand te vinden in dit kanaal" is the part
that actually helps.

nlddEditorApi typed NLDD's active-format payload as a widened index signature,
so a state key renamed upstream would read undefined and leave a toolbar button
silently stuck off. It takes the element's own return type now, so that becomes
a typecheck failure - the kind of silent-token-rename this repo has been bitten
by before.
The dev switch has served its purpose: the NLDD engine is the one we keep, so
the second engine and everything that existed to keep the two interchangeable
goes with it. Tiptap, ProseMirror and tiptap-markdown leave the dependency tree.

- Notes move onto MessageEditor. NotesEditor was a 297-line second
  implementation of the same composer and had already drifted from it; what is
  actually different is two props (notesMarks for <u>/<mark>, showAttach off
  because notes have no upload target) plus its own debounced save.
- composerEditorApi, the adapter that let one toolbar drive either engine, is
  gone; FormatToolbar, LinkBar and MessageEditor talk to WaggleTextEditor.
- minHeight becomes rows. The CSS route ran through a .ProseMirror rule that no
  longer matches anything, so the new-message window and notes had silently
  fallen back to the reply composer's two lines; the element sizes its own
  typing surface from rows.
- Tests that mocked MarkdownEditor now mock nldd/NlddMarkdownEditor. LinkBar's
  suite drives a real CodeMirror view through the production link commands, so
  it still asserts on the markdown a user would end up with. Two e2e specs
  looked for Tiptap's rendered <h2>/<a>/<blockquote>; they now read the source
  the editor holds and the decorations it draws.
- NotesEditor gains a suite for the debounce: every one of the five ways it can
  be broken is caught by at least one test.
Record the editor decision and renumber the colliding WDR
Some checks failed
CI / release-scripts (pull_request) Successful in 5s
security-scan / SBOM (trivy) (pull_request) Successful in 9s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 15s
security-scan / JS SCA (npm audit) (pull_request) Successful in 16s
security-scan / Python SAST (bandit) (pull_request) Successful in 16s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 38s
CI / pre-commit (pull_request) Successful in 48s
test-build / build (frontend) (pull_request) Successful in 55s
CI / frontend-test (pull_request) Successful in 1m3s
CI / backend-test (pull_request) Successful in 2m3s
CI / e2e (pull_request) Successful in 2m33s
test-build / build (pull_request) Has been cancelled
test-build / build (backend) (pull_request) Has been cancelled
e32d135d24
WDR-0006 covers why the composer edits markdown source: the serialiser between
the node tree and the sent message was where the escaping bugs came from, and
there is no serialiser left. It also names what the choice costs - a component
the design system marks beta, and marker hiding that is ours rather than theirs.

The a-la-carte record was numbered 0004, which main already used for the
generated API types. It becomes 0005, and the index lists all three (main's 0004
was never added to it).
robbertbos force-pushed nldd-text-editor-composer from e32d135d24
Some checks failed
CI / release-scripts (pull_request) Successful in 5s
security-scan / SBOM (trivy) (pull_request) Successful in 9s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 15s
security-scan / JS SCA (npm audit) (pull_request) Successful in 16s
security-scan / Python SAST (bandit) (pull_request) Successful in 16s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 38s
CI / pre-commit (pull_request) Successful in 48s
test-build / build (frontend) (pull_request) Successful in 55s
CI / frontend-test (pull_request) Successful in 1m3s
CI / backend-test (pull_request) Successful in 2m3s
CI / e2e (pull_request) Successful in 2m33s
test-build / build (pull_request) Has been cancelled
test-build / build (backend) (pull_request) Has been cancelled
to f7d46c4ab6
All checks were successful
CI / release-scripts (pull_request) Successful in 6s
security-scan / SBOM (trivy) (pull_request) Successful in 9s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 14s
security-scan / JS SCA (npm audit) (pull_request) Successful in 22s
security-scan / Python SAST (bandit) (pull_request) Successful in 23s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 44s
CI / pre-commit (pull_request) Successful in 1m5s
CI / frontend-test (pull_request) Successful in 1m11s
CI / backend-test (pull_request) Successful in 2m21s
CI / e2e (pull_request) Successful in 2m23s
test-build / build (frontend) (pull_request) Successful in 5m58s
test-build / build (backend) (pull_request) Successful in 6m0s
test-build / build (pull_request) Successful in 0s
2026-08-10 15:36:01 +00:00
Compare
robbertbos changed title from A second composer engine on nldd-text-editor, and NLDD imported per component to Every composer edits markdown source, on nldd-text-editor 2026-08-10 15:36:27 +00:00
Merge remote-tracking branch 'origin/main' into nldd-text-editor-composer
All checks were successful
CI / release-scripts (pull_request) Successful in 6s
security-scan / SBOM (trivy) (pull_request) Successful in 10s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 13s
security-scan / JS SCA (npm audit) (pull_request) Successful in 15s
security-scan / Python SAST (bandit) (pull_request) Successful in 16s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 36s
CI / pre-commit (pull_request) Successful in 47s
test-build / build (frontend) (pull_request) Successful in 54s
test-build / build (backend) (pull_request) Successful in 58s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 1m1s
CI / backend-test (pull_request) Successful in 2m3s
CI / e2e (pull_request) Successful in 2m4s
024369fbd8
Five of them, all reachable only through a default. `tasks`/`showTodo` is
`true` at all three call sites, `showInsert` never had a caller, `hideMarkers`
is a compartment plus an accessor pair no code writes, `FormatItem.groupStart`
is written and never read, and NlddMarkdownEditor's `defineExpose` is
unreachable because MessageEditor holds the element from `onReady` and calls it
directly.

The `showInsert` removal has a trap in it. `.noInsert` is never applied, so
`.toolbar:not(.noInsert) .moreWrap` always matches - which makes it the only
rule that reveals the "Meer" trigger between 457 and 576 px, where the inline
insert group is already hidden. Dropping the class and the rule together, which
is what "delete the dead branch" looks like, leaves link, bijlage and emoji
reachable from nowhere. Only the qualifier goes.

Nothing covered that band: the sole narrow-viewport e2e sits at 500 px and
asserts something else. The new spec drives the toolbar's own container width
and asserts the invariant behind the breakpoints - the insert actions are
reachable at every width, inline or one click away. Verified against the
deletion above, where it fails with "at 576px neither the inline link button nor
Meer is visible".
The reply composer passed no label at all, so its editor was an unnamed textbox
and the element said so on every open: "No accessible-label or input-id provided
... (WCAG SC 4.1.2)". Measured in the browser, not inferred.

It went unnoticed because `aria-label` cannot be checked. vue-tsc lists `aria-*`
under htmlAttributes and exempts it from camelization, so an `aria-label`
attribute is emitted raw and absorbed by the trailing index signature - it never
binds to the `ariaLabel` prop as far as the type-checker is concerned, and a
call site that omits it type-checks exactly like one that passes it. At runtime
Vue does camelize, which is why the other two composers worked and this one was
simply forgotten.

So the prop takes NLDD's own name, `accessibleLabel`, and is required. Removing
it from a call site is now TS2345, "Property 'accessibleLabel' is missing" -
verified by doing it.

Unrelated but worth knowing for the next person: Vue's dev warnings do not
surface in this vitest setup at all. A probe component with a missing required
prop produces no "Missing required prop" line, so a green test run is no
evidence a required prop is being passed. vue-tsc is.
`showAttach` and the `attach` emit were two halves of one decision, and nothing
tied them together: a surface could show a paperclip nothing handled, or handle
an event no button could raise. They become one optional `onAttach` callback -
present means both, absent means neither.

`@attach="fn"` at a call site still binds to it, because a declared prop wins
over an emit listener, but the two composers now write `:on-attach="fn"` so the
pairing is visible where it is decided. Notes drop `:show-attach="false"` and
say nothing at all, which is the same statement with less to keep in sync.

FormatToolbar gets its first test. It is mocked away in MessageEditor.test.ts
and mounted-but-never-driven by the two composer suites, so no test asserted its
own rules: making the paperclip unconditional passed the whole suite. Five tests
now cover which buttons it decides to render, including that a button and its
twin in the "Meer" menu appear together - collapse only one of the two and the
action is unreachable at one width.
`mentionFetch` and `accountId` were separate and independently optional, but
neither is useful alone: the avatar proxy needs the account and answers 422
without it, so a fetcher on its own turns every candidate row into initials
without saying anything. They become one `MentionSource { fetch, accountId }`,
which the compiler will not let a caller half-fill - a literal missing
`accountId` is TS2741 where two optional props were silently fine.

`accountId` existed on MessageEditor only to reach the mention dropdown, so it
leaves the prop list entirely rather than moving.

Both composers build it in a computed. An inline object literal would mint a new
identity on every render, which the wrapper's watcher would read as a changed
source.
`showUnderline` and `showHighlight` were separate toolbar props that MessageEditor
set from the same `notesMarks` value it gave the editor - three names for one
decision, and nothing stopping a caller from enabling the buttons on an editor
whose compartment is off. The toolbar takes `notesMarks` directly.

Two holes behind them:

- The wrapper applied `notesMarks` once in `onMounted` and never watched it,
  while `value` and `readOnly` both have watchers. The setter reconfigures a live
  compartment, so a later change was silently dropped. No consumer varies it
  today, so nothing tests this - it is here because the prop should behave like
  the other two, not because something broke.
- `toggleUnderline`/`toggleHighlight` wrote a literal `<u>`/`<mark>` whether or
  not the extension was loaded; the only guard was the `v-if` on the button.
  Mattermost renders neither, so that is a tag reaching a real message. Both
  commands now check the compartment.

The e2e for the second one asserts the promise rather than the mechanism: a
Mattermost composer's editor refuses the command outright, notes wrap the
selection. Verified against the ungated version, where the reply half comes back
"belangrijk<u></u>".
The send button knows it is busy (`createScheduled.isPending`), the editor next
to it did not, so during the mutation you could keep typing into a draft that
resetLocal() throws away on success. The reply composer already passed its own
sending state; this is the same line, missing.

Only the schedule path: sending closes the overlay optimistically, so there is
no window there to guard.

The toolbar deliberately stays enabled. `disabled` on an `nldd-icon-button`
drops it from the tab order - its own docs point at `loading` for "block
activation without that" - and the send path holds the busy state for the whole
10-second undo window, so a focused toolbar button would send focus to <body>
and leave it there. The editor already refuses the writes through
`writableView`, so the buttons are inert either way.
The composer box behaves as one input, so a click on its padding is forwarded to
the editor. It called `focus()`, which restores the previous selection: click in
the empty space below the last line of a long draft and the caret jumps back to
wherever you were, usually off-screen.

NLDD ships `focusFromPoint(x, y)` for exactly this - its docs describe a
wrapping composition forwarding clicks from its own padding - and uses it
internally for the same purpose. Measured in the browser on a three-line draft
with the caret at 0: `focus()` leaves it at 0, `focusFromPoint` moves it to the
end of the line clicked beside.

The e2e asserts the caret's own line after clicking the padding, and fails with
an empty `textBefore` against the old call.
`iconNames.test.ts` scans templates for literal `icon="…"` attributes and reads
six named maps. The toolbar names its icons in neither: FORMAT_ITEMS and
INSERT_ITEMS are object literals, and the heading icons are built as
`heading-${l}`. Twenty names, none of them guarded, in the one component where
`paperclip` now decides whether an attachment button exists at all - and a
retired NLDD icon name renders an empty box with no warning anywhere.

INSERT_ITEMS moves to composerCommands.ts, next to FORMAT_ITEMS, so the guard
can import it - the module is already where the toolbar's data lives.

Verified by renaming `paperclip` to `paper-clip`: "TOOLBAR_ICONS has names NLDD
does not know".
Note the composer prop cleanup and its three fixes
All checks were successful
CI / release-scripts (pull_request) Successful in 5s
security-scan / SBOM (trivy) (pull_request) Successful in 10s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 14s
security-scan / JS SCA (npm audit) (pull_request) Successful in 16s
security-scan / Python SAST (bandit) (pull_request) Successful in 16s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 37s
CI / pre-commit (pull_request) Successful in 48s
test-build / build (frontend) (pull_request) Successful in 55s
test-build / build (backend) (pull_request) Successful in 59s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 1m8s
CI / e2e (pull_request) Successful in 2m34s
CI / backend-test (pull_request) Successful in 2m43s
2ff4dc72b3
Write down the three prop-checking traps this branch measured
All checks were successful
CI / release-scripts (pull_request) Successful in 5s
security-scan / SBOM (trivy) (pull_request) Successful in 10s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 14s
security-scan / JS SCA (npm audit) (pull_request) Successful in 15s
security-scan / Python SAST (bandit) (pull_request) Successful in 18s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 37s
CI / pre-commit (pull_request) Successful in 48s
test-build / build (frontend) (pull_request) Successful in 56s
test-build / build (backend) (pull_request) Successful in 57s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 1m3s
CI / backend-test (pull_request) Successful in 1m58s
CI / e2e (pull_request) Successful in 2m6s
60b2968c88
All three cost real time here, none is guessable from the code, and each has a
measurement behind it: an optional prop's *name* is unchecked while its *value*
is checked, `aria-*` never reaches a prop as far as vue-tsc is concerned, and
Vue's dev warnings are invisible in this vitest setup. The second one is why the
reply composer shipped without an accessible name.
The ceiling was `24rem`, a number with nothing behind it. Two things were wrong
with it beyond the arbitrariness:

`rem` follows the root font (16px) while the editor's text is 18px/27px, so the
cap measured in a different unit from the thing it capped and landed at 14.2
lines - clipping a line rather than sitting on one. The floor already speaks in
lines: NLDD derives it from our `rows` as `rows * 1lh`.

And a fixed cap ignores the room available. `nldd-window` limits itself to
`100dvh - inset * 2` and Modal.vue makes its body the only scroll region, so on
a short window a 384px composer simply consumes the space the toolbar needed.
Measured at a 560px-tall viewport with a 40-line draft: the toolbar 168px below
the fold in the new-message window. At 480px the reply composer goes too - a
451px composer inside the 388px its dialog can show.

So: `min(14lh, 32dvh)`. Fourteen lines where there is room (378px, exactly on a
line boundary), a third of the window where there is not. The floor still wins
where they cross, which is the right precedence.

The e2e asserts the invariant - text and toolbar visible together, i.e. the
composer fits the dialog's visible body - at 480px, chosen because at 560px the
card modal still just fits and the test would not catch a regression. It failed
its first version for exactly that reason.
Note the composer height cap in the changelog
All checks were successful
CI / release-scripts (pull_request) Successful in 5s
security-scan / SBOM (trivy) (pull_request) Successful in 10s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 14s
security-scan / JS SCA (npm audit) (pull_request) Successful in 16s
security-scan / Python SAST (bandit) (pull_request) Successful in 19s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 38s
CI / pre-commit (pull_request) Successful in 46s
test-build / build (frontend) (pull_request) Successful in 55s
test-build / build (backend) (pull_request) Successful in 57s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 1m4s
CI / backend-test (pull_request) Successful in 1m59s
CI / e2e (pull_request) Successful in 2m5s
52cca5bdeb
The editor already offers the open affordance. With the caret in a link NLDD
renders `a.cm-link-badge` beside it - measured live while the bar was open: same
href, same `target="_blank" rel="noopener noreferrer"`, and a better accessible
name ("Open link in nieuw tabblad: <url>") than our unlabelled "Openen". Two
buttons for one action, one of which says less.

The height mismatch was a symptom, not a detail. The field measured 42px against
32px buttons because (a) its own `padding: 0.25rem 0.5rem` was dead - base.css
styles every native `input[type=text]` and that selector outranks a bare class,
which is why it silently lost - and (b) the bar was not using the compact
single-line font the project reserves for exactly this kind of floating panel.

It now takes `--semantics-controls-sm-min-size`, the token `nldd-button
size="sm"` uses, as a `height` with no block padding. Not `min-height`: the
border token is 2px, so line-height plus block padding plus border comes to 34
and pushes straight past a 32px floor. A native input centres its single line in
whatever height it is given, so this needs no arithmetic over tokens and holds
if either token moves. Measured: field and all three buttons 32px, tops aligned.
Give the composer more room, and stop explaining the cap wrongly
Some checks failed
CI / pre-commit (pull_request) Successful in 1m55s
CI / release-scripts (pull_request) Successful in 15s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 1m0s
security-scan / Python SAST (bandit) (pull_request) Successful in 38s
CI / frontend-test (pull_request) Failing after 4m13s
security-scan / JS SCA (npm audit) (pull_request) Successful in 40s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 27s
security-scan / SBOM (trivy) (pull_request) Successful in 18s
test-build / build (backend) (pull_request) Successful in 2m39s
test-build / build (pull_request) Successful in 0s
test-build / build (frontend) (pull_request) Successful in 2m34s
CI / e2e (pull_request) Failing after 10m8s
CI / backend-test (pull_request) Successful in 12m17s
3e763ab0c7
Twenty lines or 45% of the window, up from fourteen and 32%. The old numbers
were picked to survive a 480px-tall window and every larger screen paid for it;
at 1200px tall the box now reaches 20 lines instead of 14, and the 480px case
still fits with about 115px to spare.

The comment claimed the `rows` floor wins where the two limits cross, "because
min-height beats max-height". That is wrong, and measured wrong: at a 400px
viewport the ceiling computes to 128px, the floor is 135px, and the box is 128.
They are not on the same element - NLDD puts the floor on `.cm-scroller` inside
its shadow root and this is a `max-height` on the wrapper outside it - so no
precedence rule applies. The outer box clamps and scrolls, which is the
behaviour we wanted anyway; only the explanation was invented.

Sticky was the alternative considered, and rejected on measurement rather than
taste. It works: with the cap removed and `position: sticky; bottom: 0` on the
footer, the toolbar pins to the dialog body in both surfaces. But the footer is
only the last quarter of the composer's apron - the link bar, the emoji anchor,
the attachment chips and the status and error regions all sit above it and would
scroll away with the text - and the caret then hides behind the pinned bar,
because CodeMirror does its own scroll-into-view and `scroll-padding` appears
nowhere in its source. That needs `EditorView.scrollMargins`, an opaque
background, a fade, and re-anchoring two overlays. More machinery than the cap,
not less.

regelrecht does pin its toolbar, through `nldd-page`'s `sticky-footer`, where
the editor is the page: one editor, one scroll region, one footer. Waggle's card
modal has two editors, a thread and the card actions in a single scroll region,
so there is no page footer to put a toolbar in.

The e2e now also asserts that the cap actually engages. Without that it would
pass on a cap so generous it never binds, which is how a "the toolbar is safe"
test stops meaning anything - checked against both `max-height: none` and an
absurd cap.
Give the composer spec's import waits room for a busy runner
Some checks failed
CI / pre-commit (pull_request) Successful in 1m56s
CI / release-scripts (pull_request) Successful in 11s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 57s
security-scan / Python SAST (bandit) (pull_request) Successful in 38s
CI / frontend-test (pull_request) Failing after 4m7s
security-scan / JS SCA (npm audit) (pull_request) Successful in 41s
security-scan / SBOM (trivy) (pull_request) Successful in 19s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 29s
test-build / build (backend) (pull_request) Successful in 2m35s
test-build / build (frontend) (pull_request) Successful in 2m36s
test-build / build (pull_request) Successful in 0s
CI / e2e (pull_request) Failing after 10m14s
CI / backend-test (pull_request) Successful in 12m20s
2755c3d292
Four tests in this file each run the full import wizard, because the e2e backend
has no Mattermost account and a card cannot get in any other way. The two waits
around it were 10s, which is generous when the step takes under a second and a
coin flip when the runner is loaded.

It is loaded often enough to matter. One push takes 13 jobs, and two pushes
overlap regularly: measured across recent runs, e2e sits at ~130s and
frontend-test at ~64s when the runner is quiet, and on a contended push they
went to 608s and 253s while backend-test went from 129s to 737s and still
passed. At that ratio these waits expire on a step that has nothing to do with
what the test asserts.

30s costs nothing when things are fast.
Merge origin/main (v2026.8.11) into nldd-text-editor-composer
Some checks failed
CI / pre-commit (pull_request) Successful in 3m3s
CI / release-scripts (pull_request) Successful in 9s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 58s
security-scan / Python SAST (bandit) (pull_request) Successful in 40s
security-scan / JS SCA (npm audit) (pull_request) Successful in 42s
CI / frontend-test (pull_request) Successful in 5m40s
security-scan / SBOM (trivy) (pull_request) Successful in 18s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 30s
test-build / build (backend) (pull_request) Successful in 2m40s
test-build / build (frontend) (pull_request) Successful in 2m44s
test-build / build (pull_request) Successful in 0s
CI / e2e (pull_request) Failing after 14m22s
CI / backend-test (pull_request) Successful in 17m5s
a18d603645
A merge rather than the rebase that was asked for. The branch already carries
one merge of main, and a rebase replays all 31 commits from the beginning: it
stopped at commit 2 of 30, re-resolving conflicts against intermediate states
that no longer exist (the dev switch this branch later deleted). The merge is
nine conflicted files in one pass, all of them files this branch owns.

Where main's work and this branch's touched the same thing, main's semantics
win and this branch's implementation carries them:

- NotesEditor. Main fixed two real bugs there that the rewrite would have
  dropped: `onSave` now names its card (`onSave(cardId, markdown)`, read from
  the component's own frozen prop) so a debounced save cannot land on the card
  the user just stepped to, and unmount *flushes* the pending save instead of
  dropping it, because the timer dies with the instance and losing typed text is
  worse. This branch's version did the opposite on unmount, with a test pinning
  it. Both are now the MessageEditor-based component's behaviour, and the test
  is inverted.
- Main's `NotesEditor.stepping.test.ts` drove Tiptap directly; it now drives the
  same guarantees through the mocked MessageEditor. Verified by making unmount
  drop again - both flush tests fail.
- MessageEditor gains main's `schedule` emit (Ctrl/Cmd+Shift+Enter, checked
  before plain Cmd+Enter) and its deferred `focus()`: Modal's `open` fires
  before the editor exists, so focus() waits for it once rather than guessing a
  delay. Here that is a watcher on the element from `onReady` instead of on the
  Tiptap instance.
- `MessageEditor.focus.integration.test.ts` is deleted rather than ported. It
  mounts a real Tiptap to prove focus lands on the editable and not the wrapper;
  `<waggle-text-editor>` does not boot in happy-dom, so there is nothing to
  mount. The guarantee is asserted for real in tests/e2e/shortcuts.spec.ts ("r
  opens the card ready to reply, with real keyboard focus in the editor"), which
  passes against this engine.
- Main's new `ReplyComposer.stepping.test.ts` imported the composer without
  mocking the editor, so the real element re-registered `nldd-icon`. Mocked at
  the same seam the sibling suites use.

Two things the merge surfaced that are not conflicts:

- `nldd-keyboard-shortcut` had no a-la-carte import. Main renders it and gets
  away with it because main still loads the NLDD barrel; on this branch the tag
  would have rendered an empty box. `src/nlddImports.test.ts` caught it, which
  is what it exists for.
- The CHANGELOG was rebuilt from main's, which restructured everything into
  `## [v2026.8.11]`. Only this branch's own unreleased entries were put back;
  the rest is released now, reworded by that restructure.

`types/api.ts` takes main's typed `source_data` (so `Card = CardRead`) and keeps
this branch's `MentionSource`. `iconNames.test.ts` keeps both new maps.

Gates: typecheck, 1066 vitest, 96 e2e (3 skipped), build, eslint 0 errors,
pre-commit.
Merge origin/main (menu behind a labeled button, page titles)
All checks were successful
CI / release-scripts (pull_request) Successful in 5s
security-scan / SBOM (trivy) (pull_request) Successful in 9s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 11s
security-scan / JS SCA (npm audit) (pull_request) Successful in 12s
security-scan / Python SAST (bandit) (pull_request) Successful in 15s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 35s
test-build / build (frontend) (pull_request) Successful in 45s
test-build / build (backend) (pull_request) Successful in 47s
test-build / build (pull_request) Successful in 0s
CI / pre-commit (pull_request) Successful in 1m6s
CI / frontend-test (pull_request) Successful in 1m23s
CI / backend-test (pull_request) Successful in 2m24s
CI / e2e (pull_request) Successful in 3m18s
e91224fe74
One commit behind, and the PR had gone unmergeable on it. The only conflict is
CHANGELOG.md, resolved the same way as the previous merge: main's file is
authoritative for everything released, this branch keeps only its own
Unreleased section.

Nothing else collided - main's TopNav rewrite does not touch the composer, and
the DevEditorSwitcher this branch removed had already gone from main.

Gates: typecheck, 1084 vitest, 99 e2e (3 skipped), build, eslint 0 errors.
Hand focus back to the editor when a composer popover closes
Some checks failed
CI / release-scripts (pull_request) Successful in 7s
security-scan / SBOM (trivy) (pull_request) Failing after 7s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 13s
security-scan / JS SCA (npm audit) (pull_request) Successful in 14s
security-scan / Python SAST (bandit) (pull_request) Successful in 17s
CI / backend-test (pull_request) Failing after 31s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 35s
test-build / build (frontend) (pull_request) Successful in 46s
test-build / build (backend) (pull_request) Successful in 51s
test-build / build (pull_request) Successful in 0s
CI / pre-commit (pull_request) Successful in 1m20s
CI / frontend-test (pull_request) Successful in 1m20s
CI / e2e (pull_request) Successful in 3m23s
763737f49d
Both popovers focus something inside themselves - the link bar its URL field,
the emoji picker its search box - and neither gave focus back on the way out.
Measured: open the link bar with Ctrl/Cmd+K, press Escape, and
document.activeElement is <body>. The caret is gone, the next Escape has nothing
to act on, and a keyboard user is returned to the top of the document
(WCAG 2.4.3).

Applying a link already refocused the editor, but from inside the command, so
only the path that changed something was covered. Escape, Annuleren,
click-outside and an apply that changed nothing all fell through.

The e2e walks the layers Escape is supposed to peel - link bar, then the card -
and asserts the caret is back in the editor in between. It fails with "closing
the link bar dropped focus out of the editor" against the old behaviour.

Worth recording while it is fresh: Escape from inside the editor does close the
card on this branch. The note in tests/e2e/shortcuts.spec.ts says a focused
contenteditable delivers the keydown with defaultPrevented already true, so the
dialog's cancel algorithm never runs - that was ProseMirror. CodeMirror does not
prevent the default, measured at both capture and bubble on the document, and
the dialog closes.
robbertbos force-pushed nldd-text-editor-composer from 763737f49d
Some checks failed
CI / release-scripts (pull_request) Successful in 7s
security-scan / SBOM (trivy) (pull_request) Failing after 7s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 13s
security-scan / JS SCA (npm audit) (pull_request) Successful in 14s
security-scan / Python SAST (bandit) (pull_request) Successful in 17s
CI / backend-test (pull_request) Failing after 31s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 35s
test-build / build (frontend) (pull_request) Successful in 46s
test-build / build (backend) (pull_request) Successful in 51s
test-build / build (pull_request) Successful in 0s
CI / pre-commit (pull_request) Successful in 1m20s
CI / frontend-test (pull_request) Successful in 1m20s
CI / e2e (pull_request) Successful in 3m23s
to 5b9f9bccff
Some checks failed
CI / release-scripts (pull_request) Successful in 8s
security-scan / SBOM (trivy) (pull_request) Successful in 11s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 15s
security-scan / JS SCA (npm audit) (pull_request) Successful in 17s
security-scan / Python SAST (bandit) (pull_request) Successful in 19s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 38s
test-build / build (frontend) (pull_request) Successful in 58s
test-build / build (backend) (pull_request) Successful in 1m3s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Failing after 1m7s
CI / pre-commit (pull_request) Successful in 1m16s
CI / backend-test (pull_request) Successful in 3m2s
CI / e2e (pull_request) Successful in 3m54s
2026-08-12 21:09:56 +00:00
Compare
robbertbos force-pushed nldd-text-editor-composer from 5b9f9bccff
Some checks failed
CI / release-scripts (pull_request) Successful in 8s
security-scan / SBOM (trivy) (pull_request) Successful in 11s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 15s
security-scan / JS SCA (npm audit) (pull_request) Successful in 17s
security-scan / Python SAST (bandit) (pull_request) Successful in 19s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 38s
test-build / build (frontend) (pull_request) Successful in 58s
test-build / build (backend) (pull_request) Successful in 1m3s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Failing after 1m7s
CI / pre-commit (pull_request) Successful in 1m16s
CI / backend-test (pull_request) Successful in 3m2s
CI / e2e (pull_request) Successful in 3m54s
to c5ea3cc7e0
All checks were successful
CI / release-scripts (pull_request) Successful in 5s
security-scan / SBOM (trivy) (pull_request) Successful in 9s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 11s
security-scan / JS SCA (npm audit) (pull_request) Successful in 12s
security-scan / Python SAST (bandit) (pull_request) Successful in 15s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 35s
test-build / build (frontend) (pull_request) Successful in 47s
CI / pre-commit (pull_request) Successful in 48s
test-build / build (backend) (pull_request) Successful in 49s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 53s
CI / backend-test (pull_request) Successful in 3m21s
CI / e2e (pull_request) Successful in 4m11s
2026-08-12 21:43:40 +00:00
Compare
robbertbos force-pushed nldd-text-editor-composer from c5ea3cc7e0
All checks were successful
CI / release-scripts (pull_request) Successful in 5s
security-scan / SBOM (trivy) (pull_request) Successful in 9s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 11s
security-scan / JS SCA (npm audit) (pull_request) Successful in 12s
security-scan / Python SAST (bandit) (pull_request) Successful in 15s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 35s
test-build / build (frontend) (pull_request) Successful in 47s
CI / pre-commit (pull_request) Successful in 48s
test-build / build (backend) (pull_request) Successful in 49s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 53s
CI / backend-test (pull_request) Successful in 3m21s
CI / e2e (pull_request) Successful in 4m11s
to d52c7e4cb2
All checks were successful
CI / release-scripts (pull_request) Successful in 4s
security-scan / SBOM (trivy) (pull_request) Successful in 10s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 12s
security-scan / JS SCA (npm audit) (pull_request) Successful in 13s
security-scan / Python SAST (bandit) (pull_request) Successful in 17s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 36s
test-build / build (frontend) (pull_request) Successful in 48s
CI / pre-commit (pull_request) Successful in 49s
test-build / build (backend) (pull_request) Successful in 50s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 55s
CI / backend-test (pull_request) Successful in 2m2s
CI / e2e (pull_request) Successful in 2m56s
2026-08-12 22:03:59 +00:00
Compare
robbertbos force-pushed nldd-text-editor-composer from d52c7e4cb2
All checks were successful
CI / release-scripts (pull_request) Successful in 4s
security-scan / SBOM (trivy) (pull_request) Successful in 10s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 12s
security-scan / JS SCA (npm audit) (pull_request) Successful in 13s
security-scan / Python SAST (bandit) (pull_request) Successful in 17s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 36s
test-build / build (frontend) (pull_request) Successful in 48s
CI / pre-commit (pull_request) Successful in 49s
test-build / build (backend) (pull_request) Successful in 50s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 55s
CI / backend-test (pull_request) Successful in 2m2s
CI / e2e (pull_request) Successful in 2m56s
to 6b88a4774d
All checks were successful
CI / release-scripts (pull_request) Successful in 5s
security-scan / SBOM (trivy) (pull_request) Successful in 9s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 12s
security-scan / JS SCA (npm audit) (pull_request) Successful in 13s
security-scan / Python SAST (bandit) (pull_request) Successful in 16s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 37s
CI / pre-commit (pull_request) Successful in 47s
test-build / build (frontend) (pull_request) Successful in 48s
test-build / build (backend) (pull_request) Successful in 51s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 53s
CI / backend-test (pull_request) Successful in 2m5s
CI / e2e (pull_request) Successful in 2m56s
2026-08-12 22:17:46 +00:00
Compare
robbertbos force-pushed nldd-text-editor-composer from 6b88a4774d
All checks were successful
CI / release-scripts (pull_request) Successful in 5s
security-scan / SBOM (trivy) (pull_request) Successful in 9s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 12s
security-scan / JS SCA (npm audit) (pull_request) Successful in 13s
security-scan / Python SAST (bandit) (pull_request) Successful in 16s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 37s
CI / pre-commit (pull_request) Successful in 47s
test-build / build (frontend) (pull_request) Successful in 48s
test-build / build (backend) (pull_request) Successful in 51s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 53s
CI / backend-test (pull_request) Successful in 2m5s
CI / e2e (pull_request) Successful in 2m56s
to 2c33ac17bf
All checks were successful
CI / release-scripts (pull_request) Successful in 4s
security-scan / SBOM (trivy) (pull_request) Successful in 9s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 11s
security-scan / JS SCA (npm audit) (pull_request) Successful in 12s
security-scan / Python SAST (bandit) (pull_request) Successful in 14s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 33s
test-build / build (frontend) (pull_request) Successful in 46s
CI / pre-commit (pull_request) Successful in 48s
test-build / build (backend) (pull_request) Successful in 49s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 53s
CI / backend-test (pull_request) Successful in 2m3s
CI / e2e (pull_request) Successful in 2m54s
2026-08-13 06:41:03 +00:00
Compare
robbertbos force-pushed nldd-text-editor-composer from 2c33ac17bf
All checks were successful
CI / release-scripts (pull_request) Successful in 4s
security-scan / SBOM (trivy) (pull_request) Successful in 9s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 11s
security-scan / JS SCA (npm audit) (pull_request) Successful in 12s
security-scan / Python SAST (bandit) (pull_request) Successful in 14s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 33s
test-build / build (frontend) (pull_request) Successful in 46s
CI / pre-commit (pull_request) Successful in 48s
test-build / build (backend) (pull_request) Successful in 49s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 53s
CI / backend-test (pull_request) Successful in 2m3s
CI / e2e (pull_request) Successful in 2m54s
to d437b0f576
All checks were successful
CI / release-scripts (pull_request) Successful in 5s
security-scan / SBOM (trivy) (pull_request) Successful in 9s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 12s
security-scan / JS SCA (npm audit) (pull_request) Successful in 12s
security-scan / Python SAST (bandit) (pull_request) Successful in 14s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 33s
test-build / build (frontend) (pull_request) Successful in 48s
CI / pre-commit (pull_request) Successful in 50s
test-build / build (backend) (pull_request) Successful in 50s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 56s
CI / backend-test (pull_request) Successful in 2m5s
CI / e2e (pull_request) Successful in 3m1s
2026-08-13 07:25:14 +00:00
Compare
robbertbos force-pushed nldd-text-editor-composer from d437b0f576
All checks were successful
CI / release-scripts (pull_request) Successful in 5s
security-scan / SBOM (trivy) (pull_request) Successful in 9s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 12s
security-scan / JS SCA (npm audit) (pull_request) Successful in 12s
security-scan / Python SAST (bandit) (pull_request) Successful in 14s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 33s
test-build / build (frontend) (pull_request) Successful in 48s
CI / pre-commit (pull_request) Successful in 50s
test-build / build (backend) (pull_request) Successful in 50s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 56s
CI / backend-test (pull_request) Successful in 2m5s
CI / e2e (pull_request) Successful in 3m1s
to 21b3a8a842
Some checks failed
CI / release-scripts (pull_request) Successful in 7s
security-scan / Filesystem scan (trivy fs) (pull_request) Failing after 8s
security-scan / SBOM (trivy) (pull_request) Successful in 17s
security-scan / JS SCA (npm audit) (pull_request) Successful in 23s
security-scan / Python SAST (bandit) (pull_request) Successful in 24s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 42s
test-build / build (backend) (pull_request) Successful in 1m25s
CI / pre-commit (pull_request) Successful in 1m26s
test-build / build (frontend) (pull_request) Successful in 1m20s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 1m31s
CI / backend-test (pull_request) Successful in 5m58s
CI / e2e (pull_request) Successful in 6m48s
2026-08-13 07:59:29 +00:00
Compare
robbertbos force-pushed nldd-text-editor-composer from 21b3a8a842
Some checks failed
CI / release-scripts (pull_request) Successful in 7s
security-scan / Filesystem scan (trivy fs) (pull_request) Failing after 8s
security-scan / SBOM (trivy) (pull_request) Successful in 17s
security-scan / JS SCA (npm audit) (pull_request) Successful in 23s
security-scan / Python SAST (bandit) (pull_request) Successful in 24s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 42s
test-build / build (backend) (pull_request) Successful in 1m25s
CI / pre-commit (pull_request) Successful in 1m26s
test-build / build (frontend) (pull_request) Successful in 1m20s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 1m31s
CI / backend-test (pull_request) Successful in 5m58s
CI / e2e (pull_request) Successful in 6m48s
to ce0b36c254
All checks were successful
CI / release-scripts (pull_request) Successful in 7s
security-scan / Python SAST (bandit) (pull_request) Successful in 24s
security-scan / JS SCA (npm audit) (pull_request) Successful in 21s
security-scan / SBOM (trivy) (pull_request) Successful in 20s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 26s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 43s
CI / pre-commit (pull_request) Successful in 1m23s
CI / frontend-test (pull_request) Successful in 1m30s
test-build / build (frontend) (pull_request) Successful in 1m10s
test-build / build (backend) (pull_request) Successful in 1m21s
test-build / build (pull_request) Successful in 0s
CI / backend-test (pull_request) Successful in 3m3s
CI / e2e (pull_request) Successful in 3m47s
2026-08-13 08:09:24 +00:00
Compare
robbertbos force-pushed nldd-text-editor-composer from ce0b36c254
All checks were successful
CI / release-scripts (pull_request) Successful in 7s
security-scan / Python SAST (bandit) (pull_request) Successful in 24s
security-scan / JS SCA (npm audit) (pull_request) Successful in 21s
security-scan / SBOM (trivy) (pull_request) Successful in 20s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 26s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 43s
CI / pre-commit (pull_request) Successful in 1m23s
CI / frontend-test (pull_request) Successful in 1m30s
test-build / build (frontend) (pull_request) Successful in 1m10s
test-build / build (backend) (pull_request) Successful in 1m21s
test-build / build (pull_request) Successful in 0s
CI / backend-test (pull_request) Successful in 3m3s
CI / e2e (pull_request) Successful in 3m47s
to f2ec9eec39
All checks were successful
CI / release-scripts (pull_request) Successful in 5s
security-scan / SBOM (trivy) (pull_request) Successful in 8s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 11s
security-scan / JS SCA (npm audit) (pull_request) Successful in 12s
security-scan / Python SAST (bandit) (pull_request) Successful in 15s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 35s
test-build / build (frontend) (pull_request) Successful in 56s
CI / pre-commit (pull_request) Successful in 1m1s
CI / frontend-test (pull_request) Successful in 1m3s
test-build / build (backend) (pull_request) Successful in 1m6s
test-build / build (pull_request) Successful in 0s
CI / backend-test (pull_request) Successful in 3m17s
CI / e2e (pull_request) Successful in 4m33s
2026-08-13 10:12:33 +00:00
Compare
robbertbos force-pushed nldd-text-editor-composer from f2ec9eec39
All checks were successful
CI / release-scripts (pull_request) Successful in 5s
security-scan / SBOM (trivy) (pull_request) Successful in 8s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 11s
security-scan / JS SCA (npm audit) (pull_request) Successful in 12s
security-scan / Python SAST (bandit) (pull_request) Successful in 15s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 35s
test-build / build (frontend) (pull_request) Successful in 56s
CI / pre-commit (pull_request) Successful in 1m1s
CI / frontend-test (pull_request) Successful in 1m3s
test-build / build (backend) (pull_request) Successful in 1m6s
test-build / build (pull_request) Successful in 0s
CI / backend-test (pull_request) Successful in 3m17s
CI / e2e (pull_request) Successful in 4m33s
to e74c2169cf
All checks were successful
CI / release-scripts (pull_request) Successful in 4s
security-scan / SBOM (trivy) (pull_request) Successful in 9s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 11s
security-scan / JS SCA (npm audit) (pull_request) Successful in 13s
security-scan / Python SAST (bandit) (pull_request) Successful in 15s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 36s
test-build / build (frontend) (pull_request) Successful in 47s
CI / pre-commit (pull_request) Successful in 49s
test-build / build (backend) (pull_request) Successful in 52s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 54s
CI / backend-test (pull_request) Successful in 2m3s
CI / e2e (pull_request) Successful in 3m1s
2026-08-13 11:45:32 +00:00
Compare
robbertbos force-pushed nldd-text-editor-composer from e74c2169cf
All checks were successful
CI / release-scripts (pull_request) Successful in 4s
security-scan / SBOM (trivy) (pull_request) Successful in 9s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 11s
security-scan / JS SCA (npm audit) (pull_request) Successful in 13s
security-scan / Python SAST (bandit) (pull_request) Successful in 15s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 36s
test-build / build (frontend) (pull_request) Successful in 47s
CI / pre-commit (pull_request) Successful in 49s
test-build / build (backend) (pull_request) Successful in 52s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 54s
CI / backend-test (pull_request) Successful in 2m3s
CI / e2e (pull_request) Successful in 3m1s
to 2d4c3c4f84
All checks were successful
CI / release-scripts (pull_request) Successful in 4s
security-scan / SBOM (trivy) (pull_request) Successful in 9s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 12s
security-scan / JS SCA (npm audit) (pull_request) Successful in 13s
security-scan / Python SAST (bandit) (pull_request) Successful in 16s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 34s
test-build / build (frontend) (pull_request) Successful in 46s
CI / pre-commit (pull_request) Successful in 48s
test-build / build (backend) (pull_request) Successful in 53s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 54s
CI / backend-test (pull_request) Successful in 2m1s
CI / e2e (pull_request) Successful in 3m2s
2026-08-13 13:05:22 +00:00
Compare
robbertbos force-pushed nldd-text-editor-composer from 2d4c3c4f84
All checks were successful
CI / release-scripts (pull_request) Successful in 4s
security-scan / SBOM (trivy) (pull_request) Successful in 9s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 12s
security-scan / JS SCA (npm audit) (pull_request) Successful in 13s
security-scan / Python SAST (bandit) (pull_request) Successful in 16s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 34s
test-build / build (frontend) (pull_request) Successful in 46s
CI / pre-commit (pull_request) Successful in 48s
test-build / build (backend) (pull_request) Successful in 53s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 54s
CI / backend-test (pull_request) Successful in 2m1s
CI / e2e (pull_request) Successful in 3m2s
to be3f5d94ad
All checks were successful
CI / release-scripts (pull_request) Successful in 4s
security-scan / SBOM (trivy) (pull_request) Successful in 8s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 12s
security-scan / JS SCA (npm audit) (pull_request) Successful in 13s
security-scan / Python SAST (bandit) (pull_request) Successful in 16s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 34s
test-build / build (frontend) (pull_request) Successful in 49s
CI / pre-commit (pull_request) Successful in 49s
test-build / build (backend) (pull_request) Successful in 52s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 54s
CI / backend-test (pull_request) Successful in 2m2s
CI / e2e (pull_request) Successful in 2m59s
2026-08-13 13:51:08 +00:00
Compare
robbertbos force-pushed nldd-text-editor-composer from be3f5d94ad
All checks were successful
CI / release-scripts (pull_request) Successful in 4s
security-scan / SBOM (trivy) (pull_request) Successful in 8s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 12s
security-scan / JS SCA (npm audit) (pull_request) Successful in 13s
security-scan / Python SAST (bandit) (pull_request) Successful in 16s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 34s
test-build / build (frontend) (pull_request) Successful in 49s
CI / pre-commit (pull_request) Successful in 49s
test-build / build (backend) (pull_request) Successful in 52s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 54s
CI / backend-test (pull_request) Successful in 2m2s
CI / e2e (pull_request) Successful in 2m59s
to de93ed721e
All checks were successful
CI / release-scripts (pull_request) Successful in 5s
security-scan / SBOM (trivy) (pull_request) Successful in 9s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 12s
security-scan / JS SCA (npm audit) (pull_request) Successful in 13s
security-scan / Python SAST (bandit) (pull_request) Successful in 17s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 34s
test-build / build (frontend) (pull_request) Successful in 49s
CI / pre-commit (pull_request) Successful in 50s
test-build / build (backend) (pull_request) Successful in 52s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 55s
CI / backend-test (pull_request) Successful in 2m2s
CI / e2e (pull_request) Successful in 2m58s
2026-08-13 14:45:12 +00:00
Compare
robbertbos force-pushed nldd-text-editor-composer from de93ed721e
All checks were successful
CI / release-scripts (pull_request) Successful in 5s
security-scan / SBOM (trivy) (pull_request) Successful in 9s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 12s
security-scan / JS SCA (npm audit) (pull_request) Successful in 13s
security-scan / Python SAST (bandit) (pull_request) Successful in 17s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 34s
test-build / build (frontend) (pull_request) Successful in 49s
CI / pre-commit (pull_request) Successful in 50s
test-build / build (backend) (pull_request) Successful in 52s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 55s
CI / backend-test (pull_request) Successful in 2m2s
CI / e2e (pull_request) Successful in 2m58s
to 5adca295a2
All checks were successful
CI / release-scripts (pull_request) Successful in 7s
security-scan / SBOM (trivy) (pull_request) Successful in 9s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 14s
security-scan / Python SAST (bandit) (pull_request) Successful in 15s
security-scan / JS SCA (npm audit) (pull_request) Successful in 15s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 33s
test-build / build (frontend) (pull_request) Successful in 48s
CI / pre-commit (pull_request) Successful in 51s
test-build / build (backend) (pull_request) Successful in 53s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 54s
CI / backend-test (pull_request) Successful in 2m3s
CI / e2e (pull_request) Successful in 3m4s
2026-08-13 14:56:51 +00:00
Compare
robbertbos force-pushed nldd-text-editor-composer from 5adca295a2
All checks were successful
CI / release-scripts (pull_request) Successful in 7s
security-scan / SBOM (trivy) (pull_request) Successful in 9s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 14s
security-scan / Python SAST (bandit) (pull_request) Successful in 15s
security-scan / JS SCA (npm audit) (pull_request) Successful in 15s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 33s
test-build / build (frontend) (pull_request) Successful in 48s
CI / pre-commit (pull_request) Successful in 51s
test-build / build (backend) (pull_request) Successful in 53s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 54s
CI / backend-test (pull_request) Successful in 2m3s
CI / e2e (pull_request) Successful in 3m4s
to 766a7f9f3d
Some checks failed
CI / release-scripts (pull_request) Successful in 5s
security-scan / SBOM (trivy) (pull_request) Successful in 9s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 11s
security-scan / JS SCA (npm audit) (pull_request) Successful in 12s
security-scan / Python SAST (bandit) (pull_request) Successful in 15s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 33s
CI / frontend-test (pull_request) Failing after 41s
test-build / build (frontend) (pull_request) Successful in 48s
CI / pre-commit (pull_request) Successful in 49s
test-build / build (backend) (pull_request) Successful in 52s
test-build / build (pull_request) Successful in 0s
CI / backend-test (pull_request) Successful in 2m0s
CI / e2e (pull_request) Successful in 3m18s
2026-08-13 15:12:55 +00:00
Compare
robbertbos force-pushed nldd-text-editor-composer from 766a7f9f3d
Some checks failed
CI / release-scripts (pull_request) Successful in 5s
security-scan / SBOM (trivy) (pull_request) Successful in 9s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 11s
security-scan / JS SCA (npm audit) (pull_request) Successful in 12s
security-scan / Python SAST (bandit) (pull_request) Successful in 15s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 33s
CI / frontend-test (pull_request) Failing after 41s
test-build / build (frontend) (pull_request) Successful in 48s
CI / pre-commit (pull_request) Successful in 49s
test-build / build (backend) (pull_request) Successful in 52s
test-build / build (pull_request) Successful in 0s
CI / backend-test (pull_request) Successful in 2m0s
CI / e2e (pull_request) Successful in 3m18s
to 9557e63ea4
All checks were successful
CI / release-scripts (pull_request) Successful in 5s
security-scan / SBOM (trivy) (pull_request) Successful in 9s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 13s
security-scan / JS SCA (npm audit) (pull_request) Successful in 14s
security-scan / Python SAST (bandit) (pull_request) Successful in 15s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 38s
test-build / build (frontend) (pull_request) Successful in 50s
CI / pre-commit (pull_request) Successful in 50s
test-build / build (backend) (pull_request) Successful in 54s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 56s
CI / backend-test (pull_request) Successful in 2m5s
CI / e2e (pull_request) Successful in 3m6s
2026-08-13 15:20:42 +00:00
Compare
robbertbos force-pushed nldd-text-editor-composer from 9557e63ea4
All checks were successful
CI / release-scripts (pull_request) Successful in 5s
security-scan / SBOM (trivy) (pull_request) Successful in 9s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 13s
security-scan / JS SCA (npm audit) (pull_request) Successful in 14s
security-scan / Python SAST (bandit) (pull_request) Successful in 15s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 38s
test-build / build (frontend) (pull_request) Successful in 50s
CI / pre-commit (pull_request) Successful in 50s
test-build / build (backend) (pull_request) Successful in 54s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 56s
CI / backend-test (pull_request) Successful in 2m5s
CI / e2e (pull_request) Successful in 3m6s
to bf91e11643
All checks were successful
CI / release-scripts (pull_request) Successful in 5s
security-scan / SBOM (trivy) (pull_request) Successful in 9s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 13s
security-scan / JS SCA (npm audit) (pull_request) Successful in 14s
security-scan / Python SAST (bandit) (pull_request) Successful in 15s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 36s
test-build / build (frontend) (pull_request) Successful in 48s
CI / pre-commit (pull_request) Successful in 51s
test-build / build (backend) (pull_request) Successful in 53s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 56s
CI / backend-test (pull_request) Successful in 2m2s
CI / e2e (pull_request) Successful in 3m3s
2026-08-13 15:41:06 +00:00
Compare
robbertbos force-pushed nldd-text-editor-composer from bf91e11643
All checks were successful
CI / release-scripts (pull_request) Successful in 5s
security-scan / SBOM (trivy) (pull_request) Successful in 9s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 13s
security-scan / JS SCA (npm audit) (pull_request) Successful in 14s
security-scan / Python SAST (bandit) (pull_request) Successful in 15s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 36s
test-build / build (frontend) (pull_request) Successful in 48s
CI / pre-commit (pull_request) Successful in 51s
test-build / build (backend) (pull_request) Successful in 53s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 56s
CI / backend-test (pull_request) Successful in 2m2s
CI / e2e (pull_request) Successful in 3m3s
to 2451259a01
All checks were successful
CI / release-scripts (pull_request) Successful in 6s
security-scan / SBOM (trivy) (pull_request) Successful in 9s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 13s
security-scan / JS SCA (npm audit) (pull_request) Successful in 14s
security-scan / Python SAST (bandit) (pull_request) Successful in 15s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 37s
test-build / build (frontend) (pull_request) Successful in 49s
CI / pre-commit (pull_request) Successful in 51s
test-build / build (backend) (pull_request) Successful in 53s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 56s
CI / backend-test (pull_request) Successful in 2m1s
CI / e2e (pull_request) Successful in 3m7s
CI / release-scripts (push) Successful in 5s
security-scan / SBOM (trivy) (push) Successful in 8s
security-scan / Filesystem scan (trivy fs) (push) Successful in 12s
security-scan / JS SCA (npm audit) (push) Successful in 13s
security-scan / Python SAST (bandit) (push) Successful in 14s
security-scan / Python SCA (pip-audit) (push) Successful in 33s
CI / pre-commit (push) Successful in 48s
publish-main / build (frontend) (push) Successful in 49s
CI / frontend-test (push) Successful in 55s
publish-main / build (backend) (push) Successful in 56s
publish-main / build (push) Successful in 0s
CI / backend-test (push) Successful in 2m3s
CI / e2e (push) Successful in 3m30s
2026-08-13 15:52:17 +00:00
Compare
robbertbos deleted branch nldd-text-editor-composer 2026-08-13 15:57:43 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
robbertbos/waggle!279
No description provided.