Repository navigation
fix(editor): keep numbered list start number in markdown and html export - #15726
Conversation
A numbered list that doesn't start at 1 (for example one that continues after a code block) stores its number in the `order` prop. The markdown and html importers read the start number into `order`, but the exporters ignored it, so such a list was always exported starting from 1. Set `start` on the exported list from the first item's `order` when it is greater than 1. Fixes toeverything#14098 Co-Authored-By: Claude Opus 5.5 <[email protected]>
Walkthrough
Merge Risk:
|
There was a problem hiding this comment.
Actionable comments posted: 2
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Organization UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
4ce0abc0-2638-43d1-98b6-43b79beca8e8
📒 Files selected for processing: 4
blocksuite/affine/all/src/__tests__/adapters/html.unit.spec.tsblocksuite/affine/all/src/__tests__/adapters/markdown.unit.spec.tsblocksuite/affine/blocks/list/src/adapters/html.tsblocksuite/affine/blocks/list/src/adapters/markdown.ts
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## canary #15726 +/- ##
==========================================
- Coverage 55.10% 55.08% -0.02%
==========================================
Files 4032 4032
Lines 193887 193893 +6
Branches 31462 31460 -2
==========================================
- Hits 106847 106814 -33
- Misses 83022 83067 +45
+ Partials 4018 4012 -6
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
… for 3.5.0 (#664) * feat(edgeless): build a mind map with no editor, text measurement injected buildMindmap on @labre/affine-gfx-mindmap/interchange (core: gfx/mindmap/interchange) returns the elements the toolbar's mind map writes: one styled shape per node, sized as fitContent sizes it from the caller's MindmapTextMeasure, placed by the view's layout, then the mindmap element naming them by local id. materializeInterchangeImport rewrites a mind map's children and their parent onto the minted ids. layout.ts is typed against a structural LayoutNode; style.ts loads no lodash (applyNodeStyle moves to apply-style.ts) and joins model ./headless; the headless guard allows @toeverything/theme (data only). ADR 0045, amendment of 2026-11-10. Refs #662. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(edgeless): bound a built mind map's text and wrap long words in linear time A server builds what a model sent: a node holds at most 2,000 characters, a word wider than the line is cut searching forward (a line costs its own width), the widest line is a reduce rather than a spread, and the materializer passes a mind map entry that is not a record through instead of throwing after the nodes are written. Refs #662. Co-Authored-By: Claude Opus 5.5 <[email protected]> * chore(edgeless): regenerate the architecture map for the mind map builder Refs #662. Co-Authored-By: Claude Opus 5.5 <[email protected]> * test(edgeless): read node boxes through the surface in the mind map parity spec Refs #662. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(edgeless): draw the canvas viewer under a strict content security policy Labre's site serves the viewer with `style-src 'self'` and no `'wasm-unsafe-eval'`. The 3.5.0-rc.0 viewer mounted there and drew nothing. Reproduced in a browser against that policy (203 reports), then fixed: - every light-DOM component appended its styles as a `<style>` in `<head>`: `adoptShadowlessStyles()` (std) moves the registered ones, and every later one, to `document.adoptedStyleSheets`, which a CSP does not block; the viewer calls it, the editor keeps `<style>`; - the viewer's own rule was a `<style>`: it is an adopted sheet now, and it carries the two declarations `@blocksuite/icons` binds as a `style` attribute; - the code block highlighter compiled Oniguruma to WebAssembly at mount: `CodeBlockConfig.engine` picks shiki's JavaScript engine, which the viewer asks for (the editor keeps Oniguruma); - emotion's one `<style>` (tables, databases) has a null sheet under the policy: its registered and later rules go to an adopted sheet too; - the paragraph block rendered a `<style>` in its template: the rule moved to its static styles, a higher specificity than the heading icon's own rule, so the editor's cascade is unchanged; - the file drop is not mounted by the viewer (its indicator in `<body>`). Two reports remain and change nothing on screen: emotion's `<style>`, created at import, and the icons' `style` attribute. Found on the way, also fixed: a click on a note's content never reached its link in a browser, because the note's mask covers it; the link is now resolved from the elements under the pointer. And `docTitles` names the other pages, which page references showed as deleted. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(edgeless): settle synced docs of named pages and follow only the clicked block Found by a fresh review of the strict-CSP round: - a page listed in `docTitles` became an empty doc with no root, so a synced-doc card of it waited forever for content; it now gets an empty page with its title and settles; - the click read every element under the pointer, so a card hidden under another note could be followed; the lookup stops at the first block under the pointer, and reads the container's root node, so it also works inside a shadow root; - in a shadow root, a class leaving removed every copy of a shared sheet; it now removes one occurrence per sheet. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(blocks): keep emotion's rules out of a style element a strict policy blocks Emotion (the table and database blocks) created its `<style>` at import, before any canvas viewer existed: one report under the published site's `style-src`, whose criterion is none. Every `css` now comes from `@labre/affine-shared/styles`, the library's one emotion instance, whose rules wait for the next microtask before reaching emotion's `<style>`. The editor gets them there as before, before its first render. The canvas viewer calls `adoptEmotionStyles()` (with `adoptShadowlessStyles()`) at import, while the modules are still evaluating, so the rules go to an adopted sheet and no `<style>` is ever created. The replay from `cache.registered` in the viewer is gone. `@emotion/css` moves from the table and database packages to `@labre/affine-shared`, the one place that imports it now. Checked in a browser under the viewer pages' policy (`script-src 'self' 'wasm-unsafe-eval'; style-src 'self'; style-src-attr 'unsafe-inline'`): no report, no `<style>` in `<head>`, the table styled. Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(edgeless): make the canvas viewer the editor itself, derived from the registry The PO's principle for the viewer: a mirror of the interactive editor, generated by the library so that changing the editor changes it, with no maintenance of its own (ADR 0051, superseding in part ADR 0050). `mountCanvasViewer` now mounts the `edgeless` scope of the view registry (every flag on) plus the host's providers (`viewExtensions`: each framework bundle, which the published core leaves to the bundles), minus `READ_ONLY_EXCLUDED_VIEW_EXTENSIONS` and `READ_ONLY_EXCLUDED_EXTENSIONS`, the one list of tooling, kept beside the registry in `extensions/view.ts` with `getReadOnlyViewProviders`. On the read-only store the editor takes the pan tool, so the viewer's own pan, wheel and pinch handlers are gone, and with them the shared wheel module ADR 0050 asked for: the edgeless root is back to the default branch, unchanged. The viewer keeps only the host's integration (links resolved under a note's mask, the frozen view of a frame, framing, the light theme) and the strict-policy measures. `read-only-view.unit.spec.ts` compares what the editor and the viewer register in a real DI container: no excluded entry may paint content (a block view, a canvas element's view or renderer), nothing may be missing unless excluded, no exclusion may be stale. Excluding the paragraph provider makes it fail on `BlockView|affine:paragraph`. Checked in a browser under the viewer pages' policy: the editor root mounts, no report, the drag pans, a click on a page reference reaches `onOpenDoc`. Co-Authored-By: Claude Opus 5.5 <[email protected]> * test(edgeless): paint the canvas viewer in a browser under a strict policy The 3.5.0-rc.0 viewer mounted on Labre's site and drew nothing under the page's Content-Security-Policy. This spec mounts the viewer in a frame whose policy is stricter than the site's (`script-src 'self'`, no `'wasm-unsafe-eval'`; `style-src 'self'`; `style-src-attr 'unsafe-inline'`), on a page whose frame holds a filled shape, and counts the shape's pixels on the canvases: a whole canvas and one frame frozen in a page. It also fails on any `<style>` element or any policy report whose source is the library rather than Vite's own dev CSS injection. With `adoptShadowlessStyles()` switched off, it fails as the site did: 0 pixels on the whole canvas. A shim module defines `__vitest_browser_runner__.wrapDynamicImport`, which Vitest's browser mode writes into every dynamic `import()` it serves and which only its own frame defines. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(edgeless): viewer guard reads the mounted list, keys and clicks after a drag - getReadOnlyViewExtensions is the one list mountCanvasViewer mounts; the guard reads it instead of recomposing it, and counts AffineInlineSpec as content - a key typed while the pointer rests on an interactive canvas reaches the page: the editor's dispatcher pauses for that key, then resumes - a keyboard click (detail 0) never counts as the end of a drag - the spec names its constants and no longer leaves an unused variable Co-Authored-By: Claude Opus 5.5 <[email protected]> * chore(edgeless): regenerate the architecture map after the merge Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(database): read a malformed cell as its property default DatabaseBlockDataSource.getCellValue returned undefined when the stored value failed the property's rawValue.schema (a type change, an import, a peer on another version). It now returns rawValue.default(), at the read boundary only: the stored value is kept as is and nothing is written. Port of toeverything/AFFiNE#15809 (1433ac7). Closes #656. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(blocks): keep a fractional index key before its next neighbour When the subkeys of a and b share a prefix, generateFractionalIndexingKeyBetween(a, b) appended '0' and a random postfix to generateKeyBetween(a, b). If b itself continues with that key plus '0', the postfix decided the order and the new key could sort after b: inserting a table row or column right after the first one reordered it within about twenty inserts. The key now moves towards b until b no longer continues with key + '0' (upstream toeverything/AFFiNE#15709). Only newly generated keys change; stored keys are read and compared as before. Closes #655 Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(blocks): open the icon picker outside an editor from a public entry `@labre/affine/components/icon-picker` re-exports the icon picker so a host importing only public entries can reuse it (issue 636: the Labre shell's workspace icon). `openIconPicker(anchor, options)` opens `affine-icon-picker` in a portal anchored to the element and resolves with the picked `IconData` (plain JSON), `null` for Remove, or `undefined` on Escape, a click outside, the host's `signal`, the anchor leaving or a second opening; the popup closes in every case and Escape stops before a host dialog. The three elements take an optional `translation` (the host's `TranslationService`) so the picker reads in the host's language without `std`, English otherwise. `effects()` is now idempotent; nothing is defined at import. The playground debug bar gains an "Icon Picker (host demo)" button that goes through the public entry only. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(blocks): keep the canvas selection when a table selects or clears The table selection controller wrote its selection with `set` and cleared it with `clear()`, both of which replace every selection group. In edgeless that dropped the `gfx` group: the note being edited left edit mode when the caret entered a cell, and selecting then clearing a column wiped an unrelated canvas selection. `setSelected` now writes only the `TableSelection` group through `setGroup` (issue #659, upstream toeverything/AFFiNE#15791). Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(blocks): keep the start number of a numbered list on export A numbered list that does not start at 1 keeps its start number in the order prop of its first list block, and the Markdown and HTML imports already read it from the list start. Both exports dropped it, so the exported list restarted at 1 and an export then import round trip lost the numbering. The markdown and html list adapters now write start on a new numbered list when the first item's order is an integer above 1 (upstream toeverything/AFFiNE#15726). A list at 1 or without an order exports unchanged. The clipboard's text/html copy uses the same adapter. Closes #658 Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(edgeless): wrap a built mind map's text exactly as the editor does The builder's own wrap dropped the trailing space wrapText keeps on a line broken before a word, ignored hyphens and cut long words one character later, so a node long enough to break was stored 4 to 9 px narrower than the editor lays it out. wrapMindmapText is now wrapText line for line with the injected width, held to the real function on a corpus by mindmap-headless.spec.ts. A measure answering NaN, a negative or an infinite size throws MindmapMeasureError instead of looping. The parity spec now covers the four styles and three layouts, a wrapping node, and a toolbar-made map read prop for prop; the unit spec tests both caps at their bound, a refusal that quotes no text, the measure as the only source of size, and the child index against generateKeyBetween up to the node cap. Co-Authored-By: Claude Opus 5.5 <[email protected]> * test(edgeless): guard the mind map style colour past the palette Dropping lodash last from the style getters must leave a node deeper than style two's palette on its last colour. Checked once against the getters before the change on 12 000 paths of the four styles; this keeps the branch that changed. Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(playground): build a mind map with no editor from the debug menu Build Mind Map (headless) calls buildMindmap with the browser's measure, writes the result with the materializer at the top-left of the view, and reports how many nodes the editor's own layout moved on landing (zero is the expected answer). Each click takes the next of the four styles. Co-Authored-By: Claude Opus 5.5 <[email protected]> * chore(playground): regenerate the architecture map for the mind map recette Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(edgeless): show a focused frame's title badge in the canvas viewer A frozen frame was framed edge to edge, so the badge the editor draws above the frame's top edge was clipped. The view now keeps a strip of the badge's height (28px) above a titled frame: its aspect-ratio adds that strip for the current width and is read again on each resize, and fit() frames the frame under it. The viewer's adopted sheet also carries vertical-align: middle for a page reference's icon, which @blocksuite/icons asks for in a style attribute that a strict policy refuses. CommunityCanvasTextFonts is re-exported from the canvas-viewer entry for a public page that serves the faces itself. Guards: the read-only view spec names the frame-title widget among the viewer's and the editing toolbars outside it (excluding the widget made the four other cases pass and this one fail); the viewer spec asserts that mount, zoom and a click leave the shown Y.Doc's state vector unchanged. Co-Authored-By: Claude Opus 5.5 <[email protected]> * test(edgeless): paint the canvas viewer under the site's own policy The spec ran with style-src-attr 'unsafe-inline', so it could not see an attribute report. It now runs under default-src 'none'; script-src 'self'; style-src 'self'; font-src 'self', waits until the shape is painted instead of a fixed delay (180s deadline, cold dev-server imports took over 60s here), and its page holds a linked-doc card, a formatted note and a page-only note. Both cases assert painted pixels, no style element, the frame badge shown inside the view and no editing chrome. The two known reports are named and let through: one style-src-attr per icon whose declarations the viewer's sheet carries, and the data: drag image pragmatic-drag-and-drop preloads. Any other report fails. Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs: tell a host what a strict policy needs from the canvas viewer The static HTML guide says what importing the viewer already calls (adoptShadowlessStyles, adoptEmotionStyles, the JavaScript regex engine), what the policy still needs from the host, which reports remain and why the viewer belongs on a page without the editor. ADR 0051 gains an amendment with the measurements under the site's policy and the frame badge. Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(playground): open the document in the canvas viewer The debug menu's "Open in Canvas Viewer" serializes the document and posts it to examples/canvas-viewer, a page of its own (importing the viewer moves the page's styles to adopted sheets). A selected frame or group opens frozen; a click expands it. The strict variant loads under Labre Sites' policy. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(blocks): flip the code toolbar wrap and line-number switches once In the code block's "more" menu, a click on the Wrap or Line number switch flipped the switch and bubbled up to its `editor-menu-action` row, which flipped the setting a second time; the switch was bound through the `checked` attribute, so its drawn state drifted from the model (issue #660, upstream toeverything/AFFiNE#15782). `ToggleSwitch` now stops the click at its label, reads the new value from the checkbox and binds `.checked` as a property. Each code toolbar row hands its switch an `onChange` that shares the row's write, so a click on the switch or on the row flips the setting once, and both refuse a readonly store before writing. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(database): draw a blank select option as untitled A select or multi-select option whose name is empty or blank drew an empty pill: invisible in cells and in the tag menu, and as a kanban group title a zero-height strip that could not be clicked to rename it. The pill, the cell tag, the mobile picker and the group title now draw TAG_UNTITLED (com.labre.data-view.tag.untitled, "Untitled") through one tagLabel helper, the tag menu finds blank options by that label, the rename fields take it as placeholder, and the group title keeps a min-height. Display only: the stored option name is never rewritten. The manifest's chrome dedupe keeps the word apart from the page's DOC_UNTITLED (a name, not a title). Port of toeverything/AFFiNE#15807 (a2d1584). Closes #657. Co-Authored-By: Claude Opus 5.5 <[email protected]> * test(edgeless): report the stage a framed canvas viewer stalls at A shell edit had left the deadline's Error without its message, and the stage it reads unused (TS6133 in yarn build). Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(playground): keep the canvas viewer page's status off window.status The page is a script with no top-level import, so a top-level status clashed with the global (TS2451 in yarn build). Co-Authored-By: Claude Opus 5.5 <[email protected]> * chore: refresh the architecture map for the canvas viewer The viewer imports the frame-title widget's style constants; the playground gains its examples/canvas-viewer page. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(database): open a document row from a read-only desktop view On a read-only page (a public share, a reader, the trash) the desktop table hid its open button with the title column's editing and a desktop kanban card attached no click, so a reader could open no row, whereas the phone table always drew its open chip. The database block now sets HostOpensRowsKey on its data source when a DatabaseOpenRowExtension opener is registered (read with getOptional, null reads as none). opensRowsWhenReadonly (that key and rowsAreDocuments) keeps the table's open button and gives a read-only kanban card a click that opens the row through openDetailPanel. Editing chrome stays off and opening writes nothing; without an opener or over block rows, a read-only view is unchanged, and the writable paths are untouched. Co-Authored-By: Claude Opus 5.5 <[email protected]> * chore(database): refresh the architecture map for the tag label Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(edgeless): ignore a pinch step whose fingers started on one point The helper ADR 0051 removed guarded the zero distance; the restored inline handler did not, and divided by it, sending the zoom to Infinity. The read-only canvas viewer runs this same handler. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(edgeless): name a frozen canvas view and repaint it when page fonts load A frozen view is a button; with no title it had no accessible name. It is now named by com.labre.canvas-viewer.open-titled ("Open canvas: {{title}}") or com.labre.canvas-viewer.open ("Open canvas"), English when the host has no translation. The viewer loads no face: a public page declares its own, which may land after the canvas painted its text. The viewer now repaints the surface on each document.fonts loadingdone, as the editor does once FontLoaderService's faces are in, and stops listening when disposed. Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs: say the canvas viewer repaints when the page's fonts load Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(blocks): keep a native modal dialog open when escape closes the icon picker The picker's Escape listener stopped propagation but left the default action, and a native `dialog` opened with `showModal()` closes on that default action, not through a listener. Opening `openIconPicker` with a `container` inside such a dialog and pressing Escape therefore closed both. The listener now also calls `preventDefault()`; the spec asserts the dispatched event is `defaultPrevented` (happy-dom never closes a dialog on Escape, so a dialog left open there would prove nothing). Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(blocks): flip a menu toggle row once per click A `menu.toggleSwitch` row handed `onChange` the value it was built with, so a click on the row changed nothing, or undid a flip made on the switch. A click on the switch also reached the row: in a browser the label click and the checkbox click it sends on both bubbled to the row, whose `select` wrote twice beside the switch's own `onChange`. The row's `select` now writes `!on`, and the switch's host stops its click so only the switch's `onChange` answers it. Independent of the `ToggleSwitch` change of the code toolbar fix: either can land first. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(blocks): bound the steps moving a fractional index key towards b The loop that moves the key towards b ran once per repetition of a forged b shaped as a10 followed by 10 repeated N times, quadratic in its length: a 100 KB order key from a peer or a pasted or imported snapshot froze the tab for tens of seconds on a table row or column insert. The loop now stops after MAX_PREFIX_STEPS (16) steps, legitimate keys need at most 3, and returns the bare middle key, still strictly between a and b; only its uniqueness under concurrent edits is lost, on a hostile document. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(blocks): write no list start past nine digits The numbered-list typing shortcut accepts any number of digits, so a list typed as 1234567890 would export as 1234567890. x, which CommonMark does not read as a list (a start is nine digits at most) and which re-imports as a paragraph; before the start was written, the same list came back as a list. listStartNumber now writes a start only up to 999999999, in Markdown and HTML alike. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(database): open a read-only kanban card from its cells Every kanban cell stops the click it receives, so the read-only card's bubbling listener only heard its margin: a reader clicking the title or a value opened nothing. The card now listens in the capture phase, and leaves a click on a link (an a[href] in the composed path) to the link. The writable path is unchanged. The spec now clicks the title at its centre, a property value and a URL cell's link, as a reader does. Co-Authored-By: Claude Opus 5.5 <[email protected]> * test(blocks): leave the column select-and-clear case to the unit spec The integration spec's column case replayed the unit spec's `setGroup` path on the same `StoreSelectionExtension`, and its page variant could not fail on the old code. The spec keeps the case that goes through the real cell focus and real typing. Co-Authored-By: Claude Opus 5.5 <[email protected]> * test(blocks): drop the words path from the code toggle unit spec The row's click handler does not look at its target, so a click on the words crossed no branch the row path did not; the real click on the words stays held in Chromium by the code toolbar toggles spec. Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs(edgeless): say why the export options menu rebuilds its rows The reason given in the export SVG options menu for `menu.dynamic` no longer held once a toggle row flips on click: with a static `on`, a second click on the row writes the same value again and the switch is not redrawn. Co-Authored-By: Claude Opus 5.5 <[email protected]> * refactor(edgeless): carry the palette values in the library's theme The editor-free entries now load zod only. The model's theme read @toeverything/theme/v2 at run time for getColorByKey, pureBlack and pureWhite, which put the package in every headless graph once the mind map styles joined @labre/affine-model/headless. The product owner chose not to allow it. themes/palette-values.ts holds the 33 palette entries the theme reads, valued as getColorByKey valued them, and imports types only. palette-values.unit.spec.ts holds each entry to the package and the key list to default.ts, so a theme bump that would move a stored colour fails there. DefaultTheme and Themes are deep-equal before and after: no stored colour changes. The interchange-headless guard drops the package from its allow-list and refuses it by name. ADR 0045: the 2026-11-10 amendment no longer widens the list; a 2026-11-12 amendment records the copy (red zone, stored format unchanged). Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(blocks): keep the random postfix past the fractional index step bound The bound added on the loop moving a key towards b returned the bare middle key, the same on every peer. Legitimate keys reach that bound: inserting again and again right after the first table row needs 32 steps by the 150th insert, so two peers inserting at the same place got equal keys, and a later insert between them threw "a should be smaller than b". Past the bound the key now aims at a, generateKeyBetween(a, middle) plus '0' and the random postfix: that key is never a prefix of middle, so it stays below b, and it stays unique. The bare middle remains only as a guard that the comparison keeps. Co-Authored-By: Claude Opus 5.5 <[email protected]> * test(edgeless): drop a duplicate type check and a repeated comment The first builder test already asserts the exact list of element types; the allow-list's reason is written next to the @toeverything assertion, in palette-values.ts and in headless.ts. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(blocks): give a menu toggle row's focus back after a switch click The switch of a `menu.toggleSwitch` row keeps its click from the row, so the menu container that gives the focus back to the hidden search field on a click never saw it: the label's mousedown had taken that focus, and the arrows and Enter, heard only on that field, went dead. The switch's `onChange` now hands the focus back through `menu.focusTo`, wherever the click was stopped. Also: a `ponytail:` line on `stopSwitchClick` (redundant once `ToggleSwitch` stops its own click), the table header menu docblock no longer describes the old row behaviour, and the integration spec takes `deepQueryAll` from a shared `utils/deep-query.ts`. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(blocks): give the library an emotion instance of its own The first version patched the sheet of @emotion/css's default instance at import and deferred every rule to a microtask, in every editor: a host whose install deduplicates @emotion/css had its own rules deferred too, broke on a measure taken right after css(), and could see its cascade order flip. The library now creates its own instance (createEmotion, key labre: classes read labre-...). Its rules are inserted synchronously, as before, until adoptEmotionStyles is called; the default instance is never touched. A spec holds both. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(edgeless): deny the pages a canvas viewer is not given, free its keys A page docTitles leaves out may be unshared, private or deleted: the viewer now names it Access denied (com.labre.canvas-viewer.access-denied, and its .description for the card sentence) wherever the editor would say deleted, by wrapping the host's translation, and a click on it calls nothing. onOpenDoc keeps its untrusted-input note. A host's link preview service and image proxy reach the blocks through a provider of viewExtensions; a spec mounts a bookmark with no metadata and sees the host's service called, no fetch, and the host's proxy in the icon. The key pause of the dispatcher also ends on the next task: a listener that stopped a key left the wheel and the drag dead. The viewer adopts the library's styles from a module it imports first, before any block module evaluates, now that emotion inserts synchronously. Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs: canvas viewer pages, host seams and emotion instance Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(database): decide a kanban card's read-only open at the click The capture listener was attached in connectedCallback from readonly$ read once, but readonly$ flips without remounting the cards: a card mounted read-only kept opening rows once the page turned writable (a click meant to edit navigated away), and one mounted writable never opened once read-only. The listener is now always attached and returns at once unless the view is read-only and the host opens its rows. The spec's link guard now listens in the capture phase (the cell stops the click before it bubbles) and the link points at example.invalid, so the suite never reaches the network. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(blocks): bound the last list number, not the start, on export remark-stringify numbers a list from its start, so a two-item list at 999999999 exported as 999999999. then 1000000000., and the second line folded into the first item on import. listStartNumber now takes the walker node, counts the items the exported list merges (the following siblings of the same list type) and writes start only when the last number stays within nine digits. One rule for Markdown and HTML, though an ol start has no limit, so both exports of a document number it alike. Co-Authored-By: Claude Opus 5.5 <[email protected]> * chore: refresh the architecture map for the viewer's styles module Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs(blocks): say why the fractional index fallback guard is required The comment claimed generateKeyBetween(a, middle) is never a prefix of middle. The midpoint b.slice(0, 1) branch can return one, and that prefix plus the postfix may sort after b: the a < key < b check is what keeps the order, not a free extra. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(database): keep a read-only kanban card from opening a row twice A card mounted writable keeps its editing listeners when the view turns read-only without a remount. With a card selection left from editing, a click on its margin opened the row through the read-only capture listener, then again through the editing one (two host navigations, two tabs on Ctrl/Cmd); a right click still popped the editing menu. Both editing listeners now return at once while the view is read-only. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(edgeless): read the host's translation in the canvas viewer's own container The viewer read the host's translation from a second extension manager built on the host's providers alone, whose get('edgeless') throws when none of them registers in that scope (a provider that registers for the page view only, the ext-loader README's pattern): the mount threw, before the workspace was freed, and every host extension was set up twice. The wrapper is now an extension set up after every other one of the viewer's, the host's included: it reads the translation registered so far in the same container and overrides it. The extensions are built inside the try that frees the workspace. The static HTML guide notes that setImageProxyURL also sets a module-wide URL. Co-Authored-By: Claude Opus 5.5 <[email protected]> * chore: refresh the architecture map after the viewer translation fix Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(playground): show the interactive canvas viewer across the whole window The recette page kept every view in a 960 px column, so the whole canvas looked like a bug of the viewer. A frozen frame stays in that column, as on a published page; the interactive view takes the window. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(page): store the outline type-icon switch's own value The settings menu of the outline panel wrote `!showPreviewIcon`, the negation of the value read at its last render, instead of the value the `toggle-switch` hands its `onChange`. Two flips before the menu re-rendered both wrote the same negation, leaving the switch on and the stored setting off. The handler now writes the value it receives. Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs: record the unpushed host-session work and the browser-start false red Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(database): read a malformed data-view cell as its column default BlockQueryDataSource.cellValueGet returned the stored cells[rowId][columnId] raw, with no schema: a value its column rejects (a type change, an import, a peer on another version) reached the views, where a malformed date or multi-select threw in stringValue$ and a malformed checkbox rendered ticked. It now reads as the column's rawValue.default(), at the read boundary only: the stored value is kept and nothing is written. An absent cell still reads as undefined, and a column type without a meta passes through unchanged. Same rule as the affine:database source (#656). Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(blocks): refuse an oversized fractional index key with a named error A forged neighbour key such as `a0` followed by twenty thousand `z` made the midpoint of the fractional-indexing package recurse once per character and overflow the stack with a RangeError. Keys longer than 4096 characters are now refused before the package is called, with a BlockSuiteError whose message does not quote the key. The bound sits under the measured cold-stack overflow (about 5700 characters on V8) and above every legitimate series measured. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(blocks): keep a shadow-held portal open on a click inside it The click-away listener of `createLitPortal` read `e.target` on the document, where a click inside a portal held by a shadow tree is retargeted to a shadow host outside the portal. The zoom bar's vertical menu, appended inside its toggle button's shadow root, closed after every "Zoom in". The listener now reads `e.composedPath()`; a click that is really outside still closes the portal for every caller. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(blocks): count only list items when importing an html start number An indented `<ol start="3">` has a whitespace text node between two items, and the index added to the start counted it: the items came in as 4, 6, 8 and the list block shows a stored integer order as is. The index now counts the `li` element children only. An `<ol>` without a start keeps a null order, which the list block numbers from 1. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(edgeless): cache a canvas character's width by the whole character `charWidth` indexed its cache by `charCodeAt(0)`. An emoji is two UTF-16 units and the emojis from U+1F300 to U+1F64F share the first one, so the first measured in a font lent its width to the others and `wrapText` cut a word of emojis in the wrong place. A character beyond one unit is now cached under the whole string; a single-unit character keeps the indexed array, so a text without emoji measures and wraps as before. `getCache` still returns that array. Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs(database): record the virtual table's missing row open button The virtual table (enable_table_virtual_scroll) has no row open button, writable or read-only; a row opens only from the context menu, which a read-only view turns off. A ponytail line on DatabaseCellContainer names the ceiling and the upgrade: the pc table's row-ops with opensRowsWhenReadonly the day the flag turns on. Co-Authored-By: Claude Opus 5.5 <[email protected]> * test(blocks): take every deep shadow query from the shared helper Five integration specs kept their own copy of `deepQueryAll`, identical to the one `utils/deep-query.ts` now exports (the paste spec's as an arrow function with another parameter name). They import it instead. The selection pane spec keeps its local `deepQuery`, a first-match variant the helper does not offer. Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(blocks): unify the emoji picker on the callout's emoji-mart picker The library had two neighbouring pickers: the callout's `affine-emoji-menu` (emoji-mart) and `affine-icon-picker` (its own emoji grid plus the icon set), which `openIconPicker` opens for hosts. The PO kept the callout's. `affine-emoji-picker-panel` is now emoji-mart's picker, handed the bundled `@emoji-mart/data` set and an `i18n` object read through `com.labre.*` wordings, so it never downloads its dataset or a locale from the CDN (11 new keys under `com.labre.emoji-picker.*`, the categories reuse `com.labre.emoji-group.*`). `affine-icon-picker` keeps its Icons tab, gains `tabs`, and draws neither tab bar nor frame when it offers one tab and no Remove. The callout opens it with the Emoji tab alone, since `props.emoji` stores an emoji and nothing else, and writes the pick to the same field. `openIconPicker` keeps its options, result and Escape handling. Removed with what they drew: the callout's `EmojiMenu` and its teardown spec, `emoji-data.ts` (`getEmojiGroups`, `filterEmojiGroups`, `emojiUnicode`, `SKIN_TONES`, `RECENT_GROUP_NAME`), `RECENT_EMOJIS_KEY`, the emoji footer styles, and their literal-ratchet entries. `emoji-mart` moves from the callout's manifest to the components'. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(blocks): write list markers in plain text The plain-text export and the text/plain copy wrote a list as bare lines. A list item now writes the marker the Markdown export writes (a dash for a bullet or a toggle, the number from the list's start for a numbered item, a checkbox for a to-do), and a sub-list is indented by its parent's marker width, as Markdown indents it. Both paths share listPlainTextPrefix in affine-shared; other blocks are unchanged. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(blocks): keep frequently used for every emoji picker after one without recents emoji-mart keeps its categories at module level, shared by every picker of the page, and its init splices an empty `frequent` category out of that shared list. An opening with `rememberRecent: false` (`maxFrequentRows: 0`) emptied it, so a host's `openIconPicker` without recents took the "Frequently used" row away from every later picker, the callout's included, until the page reloaded. The panel now always names emoji-mart's `categories` (with `frequent` first, or without it when recents are off): given that list, emoji-mart filters a copy and never splices the shared one. This uses emoji-mart's public option rather than patching its `Data` from outside. The literal ratchet's comment no longer cites the deleted `emoji-data.ts`. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(database): give a duplicated column its own option ids DatabaseBlockDataSource.propertyDuplicate copied data.options with the same option ids and the cells with the same values, so a select or multi-select column and its copy shared one option identity. The copy now gets fresh option ids and its duplicated cells are remapped to them; labels, colours and the source column are untouched. The column and its cells are written in one transaction (one undo step), and a read-only store writes nothing (it used to return an id). copyCellsByProperty gains an optional mapValue argument, identity by default. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(edgeless): follow a middle click in the canvas viewer through its link checks The viewer intercepted click, dblclick and keys, but not auxclick: a middle click on an anchor the canvas renders opened it in a new tab past isSafeLinkUrl, onLinkClick and docTitles, frozen view included. auxclick is now captured: a frozen view follows nothing, an interactive one resolves the link as a click does (a page of docTitles to onOpenDoc, an external URL normalised and checked, then onLinkClick or a noopener new tab), and any other button follows nothing. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(blocks): open no emoji picker on a read-only callout A click on a callout's icon opened the picker on a read-only document, and a pick wrote `props.emoji` into Yjs, which then synced: the gesture never checked `store.readonly` (before the picker was unified too). The icon now opens nothing when the store is read-only, and a pick that lands after the document turned read-only is dropped. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(database): keep a rich-text cell stored as a string in a data-view The read gate of bbb78fc defaulted every value its column's schema refuses. The rich-text schema takes a Text only, but a plain string is a stored shape its readers still read: RichTextCell.beforeEnterEditMode turns it into a Text keeping its content, toString and the rich-text conversions read it too. Read as undefined, the first edit wrote an empty Text over it and a type change converted nothing: the text was lost. storedShapeIsReadable (database block, properties/stored-shapes.ts) lists the shapes a reader handles by code written for them; a refused value is read as stored when listed, as the default otherwise. Rich-text strings are the only entry: coercing readers (number, checkbox, date) do not count. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(database): keep a rich-text cell stored as a string The rich-text schema takes a Text only, so getCellValue read a cell stored as a plain string as the default (undefined, as the code before #656 did). Its readers still read that shape: RichTextCell.beforeEnterEditMode turns it into a Text keeping its content, toString and the rich-text conversions read it too. Read as undefined, the first edit wrote an empty Text over it and a type change converted nothing: the text was lost. storedShapeIsReadable (properties/stored-shapes.ts, the same file as on the data-view branch) lists the shapes a reader handles by code written for them; a refused value is read as stored when listed. The rich-text toJson reads a string as its own text instead of walking deltas it lacks. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(blocks): name the fractional index stack overflow instead of bounding key length A length bound refused legitimate keys: inserting again and again right before the same row grows them past 4000 characters after 20000 inserts, before the package would actually overflow. The package is now called inside a try: only its RangeError becomes the named BlockSuiteError (message without the key, the RangeError as cause); any other error goes up unchanged, and no key is refused for its length. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(blocks): separate copied blocks by a newline in plain text The clipboard writes text/plain from a slice holding one snapshot per copied top-level block, and MixTextAdapter.fromSliceSnapshot joined those snapshots with nothing: copying two paragraphs pasted them as one line elsewhere. They are now separated as the adapter already separates two blocks of one snapshot, a newline before every block but the first. A single copied block is unchanged. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(database): drag-to-fill a rich-text cell stored as a string 1367140 reads a rich-text cell stored as a plain string as stored, and its message said the shape's readers read it. Not all of them did: fillSelectionWithFocusCellData, in the table and the virtual table, took every rich-text value for a Text. A string source threw on toDelta and filled nothing; a string target threw on clear and stopped the fill halfway, the rows after it left as they were. The source's delta now comes from a Text or a Y.Text, and from toString() otherwise. A target is cleared and refilled only when it is a Text; any other value is replaced by a new Y.Text, as an empty cell already was. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(blocks): name the fractional index overflow on firefox too Firefox reports a stack overflow as InternalError (too much recursion), not RangeError, so the named error now covers both. The spec drops an assertion that could never fail. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(edgeless): drop the canvas viewer's auxclick branch for other buttons Only the middle button follows a link on auxclick: the right one goes through contextmenu, back and forward navigate before auxclick. The branch cancelling the other buttons had no effect, nor its assertion. Co-Authored-By: Claude Opus 5.5 <[email protected]> * refactor(blocks): move the list start number rule to affine-shared The plain-text list markers need the same rule and affine-shared cannot import the list package, so listStartNumber moves to affine-shared/adapters/list-start-number.ts, exported from the adapters entry, and the Markdown and HTML list adapters import it from there. The docblock now states the rule itself; the parameter only asks for the node, its parent and its index, which is all it reads. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(blocks): copy the text alone from inside one list item A selection inside one list item copies that item alone, without children, and the text/plain copy wrote its marker with the word, so a word pasted into a field or a terminal came out as "- word". A copy of one childless list item now writes its text alone; several blocks, or an item with its sub-items, keep their markers, and the plain-text export is unchanged. The start number rule moves to affine-shared/adapters/list-start-number.ts, byte for byte the module the Markdown and HTML list exports use, instead of a private copy in list-marker.ts. Co-Authored-By: Claude Opus 5.5 <[email protected]> * chore: refresh the architecture map for the recette merge * chore(blocks): drop the switch click stop the toggle fix made redundant Both toggle fixes are in this branch: <toggle-switch> stops its own click at its label, so the menu row's extra stop on the switch's host is redundant, as its ponytail line said. The icon picker's comment no longer says the portal reads event.target. Land-Verified: 2026-11-12T22:30:00+01:00 rounds=4 Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: r <r@l> Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: Mathieu Jolly <[email protected]>
Description
Closes #14098
When a numbered list doesn't start at 1 (for example it picks up at 3 after a paragraph or a code block), AFFiNE keeps that number in the list block's
orderprop. Importing markdown/html already reads the list's start number intoorder, but the markdown and html exporters never wrote it back, so the exported list always restarted at 1.This sets
starton the exported markdown list /<ol>from the first item'sorderwhen it's above 1. Lists that start at 1, or older blocks withoutorder, export exactly as before. I added a test for each exporter; the markdown one is the reverse of the existing "non consecutive numbered list" import test. PDF export already usedorder, so it wasn't affected.I used Claude Code to write this change.
Checklist
license/clacheck must be green (how it works)canarybranch and its title follows Conventional Commitsyarn lintandyarn typecheckpass locally