Repository navigation
Conversation
…e keyboard
A note can now carry a cover: a wide image across its top, the way Notion
draws one, that scrolls away with the text. Space l c, Add Cover… in the
palette or :cover opens the cover picker, one search field over eight
gradients, six public-domain photographs (Hokusai, Van Gogh, Bierstadt and
Cole from The Met's CC0 collection, Earthrise and the Blue Marble from
NASA), the images already in the vault, and an https link pasted into the
field. Upload image… takes a file from disk, and an image pasted or
dropped anywhere in the picker becomes the cover. The arrows (and
Ctrl+N/P, Ctrl+J/K) move a row at a time through the tiles; Enter picks.
The cover is frontmatter, in the keys Obsidian's banner plugins read, so
a vault shared with one of them shows the same banner:
banner: "![[assets/lisbon.jpg]]"
banner_y: 0.35
banner_y is the focal point from 0 (top) to 1 (bottom); Banners
Reloaded's percentages and Pixel Banner's 0..100 read the same, and a
note that uses cover: shows its image too, with edits keeping its key.
The value is an embed rather than a plain [[link]] on purpose: every
host's note index (desktop extractAssetEmbeds, the Go server's port, the
phones' vault-core) counts only ![[...]] as an asset a note uses. A plain
link left the Assets view calling a cover image unused and offering to
delete it, kept the image out of a published note's uploads, and could
leave the note out of an asset rename. As an embed it gets all three with
no scanner changed anywhere; reading accepts both forms. note-cover.ts in
shared-domain edits the keys through updateFrontmatterFields, so every
other frontmatter line stays byte-identical, and removing a cover drops a
frontmatter block it left empty.
Everything a pick brings lands in assets/ through importPastedImage, the
import every host already has, so covers work in a local vault, a remote
workspace over the Go server's asset endpoints, the web client and Cloud
sync, encrypted vaults included. Gradients are written as small SVG files
and gallery photos as WebP; a second pick reuses the file, so no
ZenNotes-only value ever lands in a note. The photographs ship as inline
chunks loaded on pick, because the packaged app's file:// CSP refuses a
fetch of a bundled file. A cover edit goes through the focused editor as
a transaction when the note is on screen, so u and Mod+Z take it back
like any edit, and the note is saved right after.
In the editor the cover is a CodeMirror block widget above the first line
(cm-note-cover.ts): a slot the pane portals the same NoteCover component
into that the Read view draws, so the two cannot drift. It bleeds to the
full pane width through measured CSS variables and keeps its gap as
padding, since CodeMirror measures a block widget by its border box.
Hovering shows Change cover, Reposition and Remove, and an image dropped
on the cover replaces it. Reposition is a mode: the cover becomes a
self-keyed surface, the picture follows a drag or the arrows (j/k too
with Vim on, Shift for 1% steps, Home/End), Enter writes banner_y and
Escape puts the old position back. Reposition Cover, Paste Cover from
Clipboard and Remove Cover are in the palette, and :cover takes paste,
reposition, a percentage, ! or remove, an https URL, a vault image or a
built-in name.
Settings, Typography, Note covers chooses Scroll with note (the default),
Pinned above the editor and Read view, or Hidden, which only stops
drawing. It is a portable preference, note_cover_placement under
[appearance].
Two fixes outside the feature's own files made it work. The command
palette refocused the editor after any command that did not open an
overlay it knew, so Change Cover… typed into the note behind the picker;
COMMAND_OVERLAY_FIELDS now names the picker and the reposition mode next
to the boolean flags. And leaving the Read view at its top landed the
editor on the first rendered block, below the cover and the frontmatter,
which scrolled the cover away; for a note that shows a cover the landing
now keeps the editor at its top. The same may be right for every note
with frontmatter; that is left alone here.
Not yet: the share viewer, Quick Look, PDF and DOCX export, the pinned
reference pane, floating windows and hover previews do not draw covers,
and the phones get them with the next core release.
Verified in the built app over CDP with isolated userData and config dir
(27 checks, Vim on and off): Space l c opens the picker; Dusk writes the
embed and its SVG; the cover spans the pane at its top; u and Ctrl+R;
:cover earthrise; the Assets view counts the cover as used; renaming the
image rewrites the banner and the cover follows; keyboard and drag
reposition save banner_y and Escape writes nothing; split and Read draw
it at full height; leaving Read keeps it in sight; a pasted and a dropped
image become the cover without embedding into the text; a cover: note
shows at its 30%; Pinned and Hidden, saved to config.toml; :cover!
restores the note byte for byte; with Vim off j does nothing in
reposition mode, the arrows do, and the palette opens the picker with its
field focused. Against a Go server built from znserver (6 checks):
:cover lagoon and a dropped image land in the server's vault, the cover
loads through zen-asset://remote, and :cover! restores the note.
Tests: the reader (every value shape, both keys, all three scales,
quoting, comments), the editor (block creation, key choice, rounding,
removal and the emptied block, a round trip), a cover following an asset
rename, and the palette's refocus rule. shared-domain 2,158, app-core
3,543, desktop 1,194 and quicklook 15 tests pass; typecheck clean.
Dave (Discord, 2026-10-27) embedded a PDF that showed fine on his Linux
desktop, but on his Android phone "Open as reference" and "Preview" both
opened an empty sheet. Android's WebView has no PDF viewer: an iframe
pointed at a PDF stays about:blank however right its URL is. On an
emulator the frame's URL, fetched from inside the WebView, came back 200
with application/pdf and the right bytes, and navigator.pdfViewerEnabled
was false. Every PDF surface in core is such a frame: the editor's inline
embed, the reference pane, the reading view's embed and an asset tab.
Where the engine says it has no viewer (canShowPdfInline), core no
longer draws the frame:
- The editor's PDF card always shows, even with PDFs in edit mode set to
full. Its badge says "open", it has no Preview, and a tap hands the
file to another app through openAssetExternally. That wins over a pin,
so a PDF pinned on an older build opens there too.
- The reading view's standalone PDF embed becomes the same card.
- The reference pane and an asset tab show PdfOpenElsewhere: the name,
why, and Open in another app. A pin made before this, or from the
asset menu, lands there instead of on a blank sheet.
- openVaultAssetExternally takes { confirm: false }. A PDF viewer runs
nothing, so a PDF goes straight to it without the "Open attachment?"
question other attachments get.
An engine that gives no answer keeps the frame, and desktop Chromium,
Safari and Firefox answer true, so nothing changes there. Checked over
CDP on the desktop build (card, Preview frame and reference pane as
before) and on an iPhone 17 Pro simulator, whose WebView draws the PDF in
the pane. The Android shell had no openAssetExternally; zennotesandroid
fix/pdf-embed adds one.
The absolute path in Dave's embed
(/home/dave/pCloudrive/zennotes/assets/Tourtickets.pdf) played no part:
on the phone it resolves to assets/Tourtickets.pdf through the
unique-basename reading, and the vault-relative form behaved the same.
Tests: cm-live-preview-pdf-inline (the card for the absolute and the
relative path, full mode, an old pin, and the unchanged desktop card and
frame), local-assets-pdf-inline (the reading view both ways), pdf-inline
and PdfOpenElsewhere. app-core's suite and typecheck pass.
A PDF card pins its file to the note it sits in
(pinAssetReferenceForNote), but the Reference commands checked only the
global pin. With a per-note pin showing, the palette offered neither
Hide Reference Pane nor Focus Reference Pane, and
runAppCommand('ref.toggle') refused to run. The phones close their
full-screen reference sheet through that command, so Done and Back could
not close a PDF there (Dave, Discord, 2026-10-27; the shells' half is
fix/pdf-embed in zennotesandroid and zennotesiphone).
activeReferencePath reads the pin the pane shows, the open note's own
pin, else the global one, the way App.tsx already did inline. App,
ref.toggle and ref.focus now share it. Unpin Reference still means the
global pin; the pane's own x unpins a per-note one.
Tests: pinned-reference, and commands (Hide and Focus offered for a
per-note pin, Hide hides it, neither without a pin). On the Android
emulator the palette lists Show Reference Pane for Dave's pin.
… at phone width At 360 px with Android's text size at 1.3x, the compact PDF card's "open as reference" badge spilled out of its button and drew under the Preview button beside it (Dave, Discord, 2026-10-27), and the file name was squeezed to nothing; at 1.0x the name was already gone. The badge was flex: 0 0 auto inside a button that may shrink (min-width: 0) and does not clip, so once the row ran out of room the button shrank under its badge. The button now wraps: when the name and the badge do not fit on one line the badge moves under the name, and the button clips anything that still would not fit. The badge may shrink to an ellipsis as a last resort. The expanded PDF header wraps its actions below the title the same way instead of running past the card's clipped edge. At desktop widths both stay on one line as before. Checked on the Android emulator at 360 px and 1.3x (the whole name, the badge under it, nothing past the card), on an iPhone 17 Pro simulator beside Preview, and on the desktop build, unchanged.
A cover drew in the editor and the Read view only. The public page of a
published note and the macOS Quick Look preview now draw it as well,
read-only, at the note's focal point: ReadOnlyNoteCover is the app's
frame without the buttons, the drop target or the picker imports, which
would have carried the cover gallery into both bundles.
Publishing a note with a gradient cover failed outright ("The
attachment assets/cover-gradient-dusk.svg is not a supported image"):
the service refuses SVG uploads on purpose, since a served SVG can run
script. The file does not need to travel. Its name names the gradient,
the specs moved to shared-domain (note-cover-gradients.ts), and the
share viewer redraws the same SVG from them, so the publisher now
leaves built-in gradient files out of the upload. An SVG of the
author's own is still refused, with the same message.
Quick Look uses the file its embed scan already found for a ![[...]]
cover. Any other form readNoteCover accepts (a plain [[link]], a bare
path, cover:) the page asks for by reference over a new zenql://
cover route, which resolves it with the extension's AssetResolver, so
the frontmatter is parsed in one place and the extension keeps its
limits (inside the vault, media files only). A cover from the web
stays out, as the preview never goes online, and Hidden in Note
covers hides it there too.
How to test locally: build the share viewer, serve dist with a page
carrying a #zen-share-data payload whose markdown has
banner: "![[assets/x.webp]]" and an assets entry for it; then one with
banner: "![[assets/cover-gradient-dusk.svg]]" and no assets. For Quick
Look, follow apps/quicklook/README.md's test-copy steps and press Space
on notes with each cover form.
…works, on the right task The task-index walks in shared-domain's tasklists.ts (editTaskAtIndex, behind every set*AtIndex mutator; taskLineNumbers, behind moveTaskLine; and takeTaskLineAtIndex) matched TASK_LINE_RE against raw lines. A line saved on Windows ends in "\r", which the pattern's "$" does not match, while parseTasksFromBody turns "\r\n" into "\n" before it counts. So the scanner listed a CRLF note's tasks and every action on them did nothing, and in a note mixing endings the walk counted fewer tasks than the parser and acted on the wrong one: toggling index 1 of "a\n b\r\n c\n" ticked c. Found while building the phones' Calendar widget, whose ticks go through toggleTaskFromList: on the iPhone, a widget tick on such a note was dropped. The walks now match the line with its "\r" taken off and put the ending back on whatever they write, as extractOpenTaskBlocks already does for rollover. A line without "\r" matches exactly as before. The MCP server's own task-line copy (apps/desktop/src/mcp/vault-ops.ts) is one of the synced copies and is left alone here; it likely has the same gap. Four CRLF cases in apps/desktop/src/main/tasklists.test.ts: a toggle keeps the ending, mixed endings tick the right task, take and move. desktop 1,198 and shared-domain 2,145 tests pass; typecheck clean.
…get shows Dave (Discord, 2026-10-27) asked for the phone Calendar sheet on his Home Screen: the month with week numbers and marks, and the week's tasks below with boxes to tick. A widget cannot run this app, so the phone shells draw it natively from a snapshot; core supplies what it shows and takes the ticks back. lib/calendar-data.ts holds the panel's grid, ISO week, word-count dots and note measure, moved out of CalendarPanel unchanged, so the widget and the panel share one copy of those rules. getCalendarWidgetData (@zennotes/app-core/calendar) builds the grid marks and the week agenda for the previous, current and next month with the panel's own rules: daily-note tasks take the note's date, only open tasks are listed, and a trashed note's leftover tasks are skipped. Each inline task carries its line number, the line as it is on disk, and the line after core's own toggleTaskAtIndex, so a shell that writes a tick outside the app swaps one known line for the other and never parses a task. There is no new task-line parser copy. setTaskChecked (@zennotes/app-core/tasks) applies a tick the widget logged. It looks the task up in the body toggleTaskFromList will edit (the open buffer, else the file), by id or by the same line when lines above it moved, and reads the write back. It never ticks a cancelled or forwarded task. It answers 'retry' instead of guessing when a clean open buffer is behind the file on disk (editing that buffer would save it over the newer file) or when the writer declined. openDateNote, and a day plus a focused "Add a task for ..." field on DateCalendar and CalendarPanel, serve the widget's links. The settings snapshot adds weekly notes, week start and week numbers, which the widget draws. Desktop behavior does not change: with no initialDate or focusAdd the panel orients, measures and draws as before. On the built desktop app over CDP, a seeded vault shows the same dots, task marks, week label and agenda. A rehearsal core from this tree ran in both phone shells on an Android API 37 emulator and an iOS 27 simulator. app-core 3,561 tests pass (new: calendar-widget-data, tasks-set-checked and three CalendarPanel cases); typecheck clean.
…wer, Done and Back close the reference sheet, and the PDF card fits at phone width
…een, and ticks that land in Windows-saved notes Core's half of the Calendar widget Dave asked for (Discord, 2026-10-27): getCalendarWidgetData builds the grid marks and week agenda the phone widgets draw, and setTaskChecked applies the ticks they log, matched in the body the toggle edits and read back. openDateNote and a day plus a focused add field on DateCalendar serve the widget's links. The panel's rules moved to lib/calendar-data.ts unchanged; desktop behavior is the same. Also the task-index walks in shared-domain's tasklists.ts now match a line without its "\r", so ticking or editing a task in a note saved with Windows line endings works everywhere, on the right task. The phone shells' halves (Android e1d17d5, iPhone 264aaa5 on their feat/calendar-widget branches) need a published core containing this merge.
… theme Mermaid fences have drawn with Pele since 2.71.0, pinned at 1.6.0. Pele shipped six releases since (1.7.0 to 1.12.0) with no breaking change to the API this app calls (`render`, `renderAsync`, `supports`, `PeleError` from `pele/lazy`), no new diagram types, and fixes that show up in real notes: - Labels on a fill the author chose (`classDef`, `style`, a sequence `rect` or `box`) pick near-black or white text, whichever contrasts with the fill. On 1.6.0 they painted in the theme's text colour, so a dark node was unreadable in a light theme and a light one in a dark theme. Author fills on subgraphs, composite states and the like now draw at full opacity. - Labels format `<u>`, `<sub>`, `<sup>`, `<code>` and `<kbd>`, and text that only looks like a tag stays: `List<T>` used to come out as `List`. - Chinese and Japanese labels wrap between characters instead of stretching the diagram; C4 relation labels stop covering the shapes they join; sequence self-messages, timelines, sankey, treemap and block diagrams get layout fixes. The two new colour variables, `--pele-fg-on-light` and `--pele-fg-on-dark`, stay at Pele's defaults on purpose: they must contrast with the author's fill, and no theme token is dark (or light) in every theme. The `.pele` rule's comment says so. The help text claimed other HTML in a label shows as written. Even 1.6.0 dropped unknown tags and kept their text; it now lists the tags that format. Not taken up here: 1.8.0's `math` option (typeset `$$...$$` in labels through a resolver the host supplies) and 1.11.0's `monospace` option. Label math still shows as written. The lockfile moves one hoisted entry and the five workspace specs (app-core, desktop, web, share-viewer, quicklook); nothing else changes. The phone shells get the new version through the core artifact. Verified on the built desktop app over CDP, in apple light, apple dark and gruvbox dark-hard: 27 fences across 22 diagram types render in Preview and the editor's live preview, with no console errors; the pele-runtime chunk is still absent at boot and loads with the first diagram. Mermaid's own docs (550 examples) render the same set on both versions: 534 draw, the 16 ZenUML examples do not, none newly fail.
…very theme, with sub/superscript, underline and code in labels (Pele 1.12.0)
A Mermaid label like `A["$$\frac{a}{b}$$"]` showed its TeX as written.
Mermaid itself typesets label math with KaTeX, so a diagram written for
mermaid.live, GitHub or Obsidian read differently here, and the manual had
to list it as a known gap. Pele 1.8.0 added a `math` render option for
exactly this: the host typesets each formula and returns its markup and
size, and Pele lays the label out around it.
lib/mermaid-label-math.ts is that typesetter, passed through
renderOptions in mermaid-render, so Preview, the editor's live preview,
the expanded view, Quick Look, shared pages and the phones all get it.
What it chooses:
- KaTeX, always. Label math is LaTeX because the syntax is Mermaid's, so a
note whose math renderer is Typst still gets KaTeX in its diagrams.
- MathML, not KaTeX's HTML. Pele puts the markup in a foreignObject, and
WebKit (the iPhone app, Quick Look, Safari) positions KaTeX's HTML wrongly
inside SVG.
- Measured, not estimated. Labels sized for something other than what
paints in them were the clipped-label bugs of mermaid.js's HTML labels
(#529, #911). Each formula is measured as the exact element Pele
receives, at the font size Pele asks for, in an off-screen probe with the
line height and wrapping of the box Pele wraps it in, then rounded up to
whole pixels. Successful measurements are cached by size and source, so
typing next to a diagram does not force a layout per formula per key.
- Lazy. The module is reached only through pele-runtime, so it adds
nothing before a diagram is drawn; KaTeX itself already ships in
vendor-markdown with the editor.
What it does not do: class members and diagram titles keep `$$` as written,
as in Mermaid; TeX KaTeX rejects shows as written, the way a broken formula
reads in a note; the Math size setting does not scale diagram math, which
follows the diagram's font size; formulas use the browser's math font, so
they can look slightly different from the note's KaTeX formulas. KaTeX's
default refuses \href and \url, so label math never becomes a link.
The in-app manual and the website docs describe it.
Verified on the built desktop app over CDP, in apple light, apple dark,
gruvbox dark-hard, and with Typst as the math renderer: 14 formulas across
flowchart nodes and edges, sequence messages and notes, state transitions,
a class relation, an ER relation, author fills and a mindmap typeset in
Preview and in the live preview, each inside its box with no overflow and
no console errors; text on a dark author fill is white and on a light one
near-black; broken TeX shows as written. The same diagrams in a WKWebView
(WebKit) place every formula inside its box.
… KaTeX instead of shown as TeX
The bundled CLI is the floor a desktop build guarantees, and 0.12.2 is the newest release (ZenNotes/tui#20). It brings LaTeX to the terminal preview: with [terminal] math = "latex", zn draws display math and TikZ through the TeX installed on the user's computer (ZenNotes/tui#17), says plainly that zn does not include TeX and prints the install commands in zn doctor, and fits preview pictures to the pane as it grows or shrinks. The pin is the release's terminal-release.json copied byte for byte after it verified against packaging/release-signing/zn-release-1.pub in ZenNotes/tui (source commit 3f1f638a, integration protocol 1, all four archives it names matching checksums.txt). Staged through `npm run terminal:stage`; the native darwin-arm64 binary reports zn v0.12.2. The in-app manual's CLI card says 0.12.2 and what 0.12.0 to 0.12.2 change.
…eX math in the terminal preview with your own TeX
What's New opens on the first launch after the update with four highlights. First, a note can carry a cover: a wide image across its top, picked from the palette or Space l c, repositioned or removed on hover. Second, a cover is plain banner frontmatter that Obsidian's banner plugins read, with an ordinary image file in assets, and published notes and Quick Look show it. Third, Mermaid labels typeset $$ math and keep text readable on a node's own fill. Fourth, the bundled zn 0.12.2 draws LaTeX in its terminal preview. The phones' Calendar widget and PDF fixes stay out on purpose: the window is desktop only, and the phones announce through their stores.
CodeQL flagged the release PR (alert 136, js/xss-through-dom, high): ReadOnlyNoteCover set an <img> src from a URL that starts as page text, the share page's #zen-share-data payload. It was not an attack path: an <img> runs no script whatever its source, readNoteCover keeps only http(s) links as web covers, lookupShareAsset returns only http(s) uploads, and the rest are this app's own gradient data URLs and Quick Look's zenql:// route. The component now says so where the URL meets the element. readOnlyCoverSource keeps http(s), zenql:// and data:image/ and draws no cover for anything else, so a later caller cannot point a share page or a Quick Look preview at another kind of URL. Every source the two surfaces hand over today passes unchanged. Tests: ReadOnlyNoteCover (the allowed sources, including a built-in gradient; javascript:, a text/html data URL, file: and a bare path refused; the img drawn at its focal point; no frame for a refused source). app-core 3,599 and quicklook 18 pass; typecheck clean.
…d not pick Found while recording the 2.78.0 diagram clip: from Gruvbox Hard dark, Cmd+Shift+P, Themes, then typing l switched the app to Nord Light with no row highlighted, and the next keystroke kept it there. Each new filter runs two effects in one commit: one re-syncs the highlight onto the current theme, which lands on the next render, and the preview one read `active` in the same commit, still an index into the previous list. It previewed whatever sat at that index in the new list, and the next re-sync found that theme current and locked onto it. Choosing Themes from the recent commands, with an empty query, could do the same with an index from the command list. The preview now remembers the mode and the list it last saw and skips the commit in which either changed (and the first render), so it only follows an `active` that was set against the list it indexes. Arrows, the pointer and Enter preview and keep exactly as before, and Escape still reverts. The same take showed the theme search could not find "apple light" or "gruvbox dark hard": rankItems scores each field alone and the query spans two. rankThemes (lib/themes.ts) adds the row read as one phrase, family, mode, flavor, label, below the label's weight, so typing a label still ranks it first. Tests: rankThemes (family plus mode or flavor, a label first, every theme for an empty query). In the built app over CDP with isolated stores, from Gruvbox Hard dark: typing "light" leaves the theme alone, ArrowDown previews Apple Light and Escape reverts, "apple light" lists Apple Light first, and Themes from the recents keeps the theme; six runs passed. One earlier run read the theme 500 ms after ArrowDown before it repainted, with the machine at a load average of about 38.
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
ZenNotes 2.78.0, the release PR: everything on
v2.78.0, with What's New (6cc483ac) and the version bump (413632bd). The phone apps pick up the core changes once core 2.78.0 is published.Features
ea74766b): a wide banner image across the top of a note, picked from the keyboard (Space l c, Add Cover in the palette,:cover): gradients, six public-domain photographs, vault images, an https link, upload, paste or drop; hover for Change cover, Reposition, Remove. Stored asbanner: "![[assets/...]]"+banner_y(the keys Obsidian's banner plugins read;cover:is read too), imported throughimportPastedImage, so local, remote, web and Cloud (encrypted included) vaults all work. New portable pref[appearance] note_cover_placement.fec49657):ReadOnlyNoteCover; built-in gradients are redrawn by the share viewer fromnote-cover-gradients.tsinstead of uploading their SVG; Quick Look resolves other cover forms overzenql://cover.7ae3afcf): labels on author fills pick near-black or white text,<u>/<sub>/<sup>/<code>/<kbd>in labels,List<T>kept, CJK wrapping, layout fixes. No API change.$$math in Mermaid labels (fa668530):lib/mermaid-label-math.ts, KaTeX to MathML, measured and cached, loaded only withpele-runtime.3fc40107):getCalendarWidgetData,setTaskChecked,lib/calendar-data.ts; the shells are onfeat/calendar-widgetin their own repos.4bda75e8).Fixes
16722caf), Hide/Focus Reference Pane for a PDF pinned to one note (4bacd562), the PDF card fits at phone width (b1fd339d).57513b7e). The other task-line copies (desktop MCP, zn, server) already count CRLF notes correctly.Verification, release gates on
413632bdin the v2.78.0 worktree with its own node_modules: audit (high) clean; typecheck 8/8 and test:run uncached (shared-domain 2,164, app-core 3,593, desktop 1,198, quicklook 18); fresh build:prod; Developer ID signednpm run packand the packaged launch check (page target, rendered, 1.6 s, 2.78.0); vim-editor, sidebar-vim and editor-improvements smoke suites pass.6cc483ac(What's New data only): app-core typecheck and whats-new tests pass. Each merged branch was verified in its own session: covers over CDP (27 checks, Vim on and off, plus a Go server vault), Pele and label math over CDP in three themes and with Typst, the PDF and widget work on an Android emulator and an iOS simulator.ci-sweepsince v2.77.0: no failed jobs; hazard scan: no errors.Outside this PR: the website docs and release page (branch
website/docs-2.78), the share viewer artifact for covers on published pages, and the phone releases (release/1.34.0in zennotesios, ZenNotes/zennotesios#79;release/1.1.48in zennotesandroid, ZenNotes/zennotesandroid#136; each withfix/pdf-embedandfeat/calendar-widget).