Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
ea74766
Feat(covers): a banner image across the top of a note, picked from th…
adibhanna Oct 27, 2026
16722ca
Fix(phones): a PDF opens in the phone's viewer instead of a blank page
adibhanna Oct 27, 2026
4bacd56
Fix(reference): Hide Reference Pane works for a PDF pinned to one note
adibhanna Oct 27, 2026
b1fd339
Fix(pdf): the PDF card keeps its file name and stays clear of Preview…
adibhanna Oct 27, 2026
fec4965
Feat(covers): a published note and Quick Look show the cover too
adibhanna Oct 27, 2026
57513b7
Fix(tasks): ticking a task in a note saved with Windows line endings …
adibhanna Oct 27, 2026
3fc4010
Feat(calendar): core builds what the phones' home-screen Calendar wid…
adibhanna Oct 27, 2026
4680e68
Merge fix/pdf-embed-phones: a PDF on a phone opens in the phone's vie…
adibhanna Oct 27, 2026
bc5829f
Merge feat/calendar-widget: a Calendar widget on the phones' Home Scr…
adibhanna Oct 27, 2026
7ae3afc
Deps(diagrams): Pele 1.12.0, labels on author fills readable in every…
adibhanna Oct 27, 2026
5ef9884
Merge deps/pele-1.12.0: Mermaid labels stay readable on any fill in e…
adibhanna Oct 27, 2026
fa66853
Feat(diagrams): $$ math in Mermaid labels is typeset, not shown as TeX
adibhanna Oct 27, 2026
30aabd1
Merge feat/pele-label-math: $$ math in Mermaid labels is typeset with…
adibhanna Oct 27, 2026
4bda75e
Release(cli): bundle the verified 0.12.2 terminal release
adibhanna Oct 27, 2026
b0dc80f
Merge chore/cli-0.12.2-pin: the bundled zn is 0.12.2, which draws LaT…
adibhanna Oct 27, 2026
413632b
Release: align desktop and shared packages at 2.78.0
adibhanna Oct 27, 2026
6cc483a
Docs(whats-new): the 2.78.0 highlights
adibhanna Oct 27, 2026
e6bf83f
Fix(covers): a read-only cover draws only the sources it is meant to
adibhanna Oct 27, 2026
52ab86f
Fix(themes): typing in the Themes picker never applies a theme you di…
adibhanna Oct 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/desktop/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@zennotes/desktop",
"productName": "ZenNotes",
"version": "2.77.0",
"version": "2.78.0",
"description": "ZenNotes desktop shell",
"private": true,
"main": "./out/main/index.js",
Expand Down Expand Up @@ -71,7 +71,7 @@
"jsxgraph": "^1.12.2",
"katex": "^0.16.15",
"node-tikzjax": "^1.0.5",
"pele": "1.6.0",
"pele": "1.12.0",
"prettier": "^3.8.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
Expand Down
5 changes: 5 additions & 0 deletions apps/desktop/src/main/app-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,11 @@ const SCALAR_FIELDS: Partial<Record<PortablePrefKey, ScalarFieldMap>> = {
comment: 'show disclosure arrows in the sidebar'
},
contentAlign: { section: 'appearance', tomlKey: 'content_align', comment: 'center | left' },
noteCoverPlacement: {
section: 'appearance',
tomlKey: 'note_cover_placement',
comment: 'scroll | pinned | hidden: where a note cover image sits'
},
unifiedSidebar: {
section: 'appearance',
tomlKey: 'unified_sidebar',
Expand Down
21 changes: 21 additions & 0 deletions apps/desktop/src/main/tasklists.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,3 +318,24 @@ describe('setTaskFieldAtIndex', () => {
expect(setTaskFieldAtIndex(md, 0, 'Bad Key', 'x')).toBe(md)
})
})

describe('task index walks on Windows line endings', () => {
it('toggles a CRLF task and keeps its ending', () => {
const md = '# Saved on Windows\r\n\r\n- [ ] one\r\n- [ ] two\r\n'
expect(toggleTaskAtIndex(md, 1, true)).toBe('# Saved on Windows\r\n\r\n- [ ] one\r\n- [x] two\r\n')
})

it('counts tasks like the parser when a note mixes endings', () => {
const md = '- [ ] a\n- [ ] b\r\n- [ ] c\n'
expect(toggleTaskAtIndex(md, 1, true)).toBe('- [ ] a\n- [x] b\r\n- [ ] c\n')
})

it('takes the right line out of a CRLF note', () => {
const md = '- [ ] a\r\n- [ ] b\r\n'
expect(takeTaskLineAtIndex(md, 1)).toEqual({ line: '- [ ] b\r', body: '- [ ] a\r\n' })
})

it('moves a CRLF task to the right place', () => {
expect(moveTaskLine('- [ ] a\r\n- [ ] b\r\n- [ ] c', 2, 0, 'before')).toBe('- [ ] c\n- [ ] a\r\n- [ ] b\r')
})
})
20 changes: 10 additions & 10 deletions apps/desktop/terminal-release.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,24 @@
"release": {
"repository": "ZenNotes/tui",
"protocol": 1,
"version": "0.11.0",
"commit": "881057116fa131212bf55c2d26f5eae42ef8d72d",
"version": "0.12.2",
"commit": "3f1f638a555450891d378b4b67ab7d9eae2f5226",
"artifacts": {
"darwin-arm64": {
"url": "https://github.com/ZenNotes/tui/releases/download/v0.11.0/zn_0.11.0_darwin_arm64.tar.gz",
"sha256": "4b3548577ece83dc311bb67d72d4389a0a48a24c3bd56b9ba28aaa705303d33f"
"url": "https://github.com/ZenNotes/tui/releases/download/v0.12.2/zn_0.12.2_darwin_arm64.tar.gz",
"sha256": "7e7b3be038dfa63dc4a79bc6068cb459462cf81e809e5b070d73bfbe3e7bd5ac"
},
"darwin-x64": {
"url": "https://github.com/ZenNotes/tui/releases/download/v0.11.0/zn_0.11.0_darwin_amd64.tar.gz",
"sha256": "9dd6b9e19c3448ff947c7dd20eec511a6ffb4975616f0a78014c0e6b33047ddb"
"url": "https://github.com/ZenNotes/tui/releases/download/v0.12.2/zn_0.12.2_darwin_amd64.tar.gz",
"sha256": "091f8b2a2f5ceb8f03acfd90f4337b085551b36058b7326b9ee64d4b1a330a12"
},
"linux-arm64": {
"url": "https://github.com/ZenNotes/tui/releases/download/v0.11.0/zn_0.11.0_linux_arm64.tar.gz",
"sha256": "132eb7755693cea9911783c2097b6a18964e18658a95d99d93353bc1e56ee6e8"
"url": "https://github.com/ZenNotes/tui/releases/download/v0.12.2/zn_0.12.2_linux_arm64.tar.gz",
"sha256": "882ec3fb8f988069c2208412e3ce72bfe8be8dfff461c0ec503f5734c7417786"
},
"linux-x64": {
"url": "https://github.com/ZenNotes/tui/releases/download/v0.11.0/zn_0.11.0_linux_amd64.tar.gz",
"sha256": "44fd68b60dd82946a210dc715bea4d8d28363870ef7fcc335522e0a3e586d2bd"
"url": "https://github.com/ZenNotes/tui/releases/download/v0.12.2/zn_0.12.2_linux_amd64.tar.gz",
"sha256": "2617004481aee2e4f9677cc362d76ed9c0c414e6ec7858fa34db3d09156a33fe"
}
}
}
Expand Down
8 changes: 4 additions & 4 deletions apps/quicklook/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ Open in ZenNotes button. Three pieces ship inside `ZenNotes.app/Contents/PlugIns
and fonts from `~/.config/zennotes/config.toml`, read-only. Typst math and the
plot renderers stay out (KaTeX typesets math), as do the Excalidraw fonts.
- `macos/Extension/`: `ZenNotesQuickLook.appex`, the sandboxed Quick Look
extension. It serves the page and the files the note embeds over its own
`zenql://` scheme, behind a Content-Security-Policy that allows nothing else,
so a preview never goes online. WebKit's helper processes still need the
network-client entitlement to start at all.
extension. It serves the page and the files the note embeds (its cover image
included) over its own `zenql://` scheme, behind a Content-Security-Policy
that allows nothing else, so a preview never goes online. WebKit's helper
processes still need the network-client entitlement to start at all.
- `macos/Opener/`: `ZenNotesQuickLookOpener.xpc`, inside the extension. Quick
Look's sandbox lets a preview launch nothing, so this unsandboxed service opens
the note in the ZenNotes that contains it (the same route as a Finder
Expand Down
16 changes: 16 additions & 0 deletions apps/quicklook/macos/Extension/NotePreview.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ struct NotePreview {
private let viewer: URL
private let page: Data
private let assets: [URL]
private let noteDirectory: URL
private let vaultRoot: URL?

init(fileURL: URL, viewer: URL) throws {
self.fileURL = fileURL
Expand All @@ -37,6 +39,8 @@ struct NotePreview {

let noteDirectory = fileURL.deletingLastPathComponent().standardizedFileURL
let vaultRoot = Self.vaultRoot(above: noteDirectory)
self.noteDirectory = noteDirectory
self.vaultRoot = vaultRoot
var resolver = AssetResolver(noteDirectory: noteDirectory, vaultRoot: vaultRoot)
var files: [URL] = []
var urls: [String: String] = [:]
Expand Down Expand Up @@ -89,6 +93,18 @@ struct NotePreview {
}
return try Self.serve(assets[index], for: url, range: range)
}
if path.hasPrefix("/cover/") {
// A cover the embed scan did not find (a plain [[link]] or a bare
// path in the frontmatter): the page names it, having read the
// frontmatter with the app's own parser, and it resolves here like
// any embed, inside the vault and media files only.
let reference = String(path.dropFirst("/cover/".count))
var resolver = AssetResolver(noteDirectory: noteDirectory, vaultRoot: vaultRoot)
guard let file = resolver.resolve(Reference(written: reference, decoded: reference, wikilink: true)) else {
throw URLError(.fileDoesNotExist)
}
return try Self.serve(file, for: url, range: nil)
}
let file = viewer.appendingPathComponent(String(path.dropFirst())).standardizedFileURL
guard file.path.hasPrefix(viewer.path + "/") else { throw URLError(.noPermissionsToReadFile) }
return try Self.serve(file, for: url, range: nil)
Expand Down
4 changes: 2 additions & 2 deletions apps/quicklook/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@zennotes/quicklook",
"private": true,
"version": "2.77.0",
"version": "2.78.0",
"type": "module",
"description": "The macOS Quick Look preview for Markdown files: the page it renders and the Swift extension that serves it",
"homepage": "https://zennotes.org",
Expand Down Expand Up @@ -33,7 +33,7 @@
"highlight.js": "^11.10.0",
"jsxgraph": "^1.12.2",
"katex": "^0.16.15",
"pele": "1.6.0",
"pele": "1.12.0",
"prettier": "^3.8.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
Expand Down
34 changes: 34 additions & 0 deletions apps/quicklook/src/cover.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { describe, expect, it } from 'vitest'
import { quickLookCover } from './cover'

const shown = { noteCoverPlacement: 'scroll' } as const

describe('quickLookCover', () => {
it('draws a cover the extension already found as an embed, at its focal point', () => {
const payload = {
markdown: '---\nbanner: "![[assets/lisbon.jpg]]"\nbanner_y: 0.3\n---\n# Lisbon\n',
assets: { 'assets/lisbon.jpg': 'zenql://app/asset/0/lisbon.jpg' }
}
expect(quickLookCover(payload, shown)).toEqual({ url: 'zenql://app/asset/0/lisbon.jpg', y: 0.3 })
})

it('asks the extension for any other form of cover by its decoded reference', () => {
expect(quickLookCover({ markdown: '---\nbanner: "[[pics/a b.png]]"\n---\n', assets: {} }, shown)).toEqual({
url: 'zenql://app/cover/pics%2Fa%20b.png',
y: 0.5
})
expect(quickLookCover({ markdown: '---\ncover: "![x](pics/a%20b.png)"\n---\n', assets: {} }, shown)?.url).toBe(
'zenql://app/cover/pics%2Fa%20b.png'
)
})

it('leaves out a cover from the web, the hidden setting, and a note without one', () => {
expect(quickLookCover({ markdown: '---\nbanner: https://example.com/a.jpg\n---\n', assets: {} }, shown)).toBeNull()
const payload = {
markdown: '---\nbanner: "![[a.png]]"\n---\n',
assets: { 'a.png': 'zenql://app/asset/0/a.png' }
}
expect(quickLookCover(payload, { noteCoverPlacement: 'hidden' })).toBeNull()
expect(quickLookCover({ markdown: '# Plain note\n\n![[a.png]]\n', assets: payload.assets }, shown)).toBeNull()
})
})
39 changes: 39 additions & 0 deletions apps/quicklook/src/cover.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { readNoteCover } from '@shared/note-cover'
import { readOnlyNoteCoverUrl } from '@shared/note-cover-gradients'
import type { QuickLookPayload } from './payload'
import type { QuickLookPrefs } from './prefs'
import { decodeRef, lookupQuickLookAsset } from './shim'

/** The extension serves a cover it was asked for by reference from here. */
export const QUICKLOOK_COVER_PREFIX = 'zenql://app/cover/'

export interface QuickLookCover {
url: string
y: number
}

/**
* The note's cover as the preview draws it, or null for none.
*
* The extension finds a `![[...]]` cover while it scans the note for embeds,
* which is how ZenNotes and Obsidian's Banners plugin write one. Every other
* form `readNoteCover` accepts (a plain `[[link]]`, a bare path, `cover:`) is
* asked for by reference instead, so the frontmatter is read here, by the
* app's own parser, and the extension only resolves the file, beside the note
* or in the vault, with the limits it puts on any embed. An https cover is
* left out: the preview never goes online. Pinned or scrolling makes no
* difference in a preview, which is one page that scrolls.
*/
export function quickLookCover(
payload: Pick<QuickLookPayload, 'markdown' | 'assets'>,
prefs: Pick<QuickLookPrefs, 'noteCoverPlacement'>
): QuickLookCover | null {
if (prefs.noteCoverPlacement === 'hidden') return null
const cover = readNoteCover(payload.markdown)
const url = readOnlyNoteCoverUrl(cover, {
resolveVault: (href) =>
lookupQuickLookAsset(payload, href) ?? `${QUICKLOOK_COVER_PREFIX}${encodeURIComponent(decodeRef(href))}`,
remote: false
})
return cover && url ? { url, y: cover.y } : null
}
7 changes: 6 additions & 1 deletion apps/quicklook/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import ReactDOM from 'react-dom/client'
import type { AssetMeta, ImportedAssetKind, NoteContent, VaultInfo } from '@shared/ipc'
import { applyTypographyVariables } from '@renderer/lib/typography-variables'
import { Button } from '@renderer/components/ui/Button'
import { ReadOnlyNoteCover } from '@renderer/components/ReadOnlyNoteCover'
// The full app stylesheet (prose, themes, KaTeX, highlight, diagram chrome),
// as the share viewer and the PDF export window ship it.
import '@renderer/styles/index.css'
Expand All @@ -11,6 +12,7 @@ import { installQuickLookBridge } from './shim'
import { quickLookTheme, readQuickLookPrefs, type QuickLookPrefs } from './prefs'
import { createQuickLookKeys, type QuickLookAction } from './keys'
import { isOutsideLink, postToHost } from './host'
import { quickLookCover, type QuickLookCover } from './cover'

const payload = readQuickLookPayload()
if (payload) {
Expand Down Expand Up @@ -78,7 +80,7 @@ async function boot(data: QuickLookPayload): Promise<void> {

ReactDOM.createRoot(root).render(
<React.StrictMode>
<QuickLookPage title={data.title} vimMode={prefs.vimMode}>
<QuickLookPage title={data.title} vimMode={prefs.vimMode} cover={quickLookCover(data, prefs)}>
<LazyPreview markdown={note.body} notePath={note.path} onRendered={onRendered} />
</QuickLookPage>
</React.StrictMode>
Expand All @@ -88,10 +90,12 @@ async function boot(data: QuickLookPayload): Promise<void> {
function QuickLookPage({
title,
vimMode,
cover,
children
}: {
title: string
vimMode: boolean
cover: QuickLookCover | null
children: React.ReactNode
}): React.ReactElement {
return (
Expand All @@ -104,6 +108,7 @@ function QuickLookPage({
{vimMode ? <KeyHint label="o" /> : null}
</Button>
</header>
{cover ? <ReadOnlyNoteCover url={cover.url} y={cover.y} /> : null}
<article className="zen-quicklook-note">{children}</article>
</>
)
Expand Down
3 changes: 3 additions & 0 deletions apps/quicklook/src/prefs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ theme_family = "catppuccin"
theme_mode = "light"
theme_id = "catppuccin-latte"
content_align = "left"
note_cover_placement = "hidden"

[typography]
text_font = "Iowan Old Style"
Expand All @@ -33,6 +34,7 @@ mono_font = ""
themeMode: 'light',
themeId: 'catppuccin-latte',
contentAlign: 'left',
noteCoverPlacement: 'hidden',
textFont: 'Iowan Old Style',
monoFont: null,
interfaceFont: null
Expand All @@ -46,6 +48,7 @@ mono_font = ""
expect(prefs.editorFontSize).toBe(defaultQuickLookPrefs().editorFontSize)
expect(prefs.themeMode).toBe(defaultQuickLookPrefs().themeMode)
expect(prefs.vimMode).toBe(true)
expect(readQuickLookPrefs('[appearance]\nnote_cover_placement = "floating"').noteCoverPlacement).toBe('scroll')
})
})

Expand Down
6 changes: 6 additions & 0 deletions apps/quicklook/src/prefs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export interface QuickLookPrefs {
previewMaxWidth: number
contentAlign: 'center' | 'left'
completedTaskStyle: string
/** Hidden keeps a note's cover out of the preview, as in the app. */
noteCoverPlacement: 'scroll' | 'pinned' | 'hidden'
vimMode: boolean
}

Expand All @@ -26,6 +28,7 @@ const TOML_KEYS: Record<keyof QuickLookPrefs, [section: string, key: string]> =
themeMode: ['appearance', 'theme_mode'],
themeId: ['appearance', 'theme_id'],
contentAlign: ['appearance', 'content_align'],
noteCoverPlacement: ['appearance', 'note_cover_placement'],
interfaceFont: ['typography', 'interface_font'],
textFont: ['typography', 'text_font'],
monoFont: ['typography', 'mono_font'],
Expand Down Expand Up @@ -75,6 +78,9 @@ export function readQuickLookPrefs(configText: string): QuickLookPrefs {
prefs.themeMode = defaultQuickLookPrefs().themeMode
}
if (prefs.contentAlign !== 'left') prefs.contentAlign = 'center'
if (!['scroll', 'pinned', 'hidden'].includes(prefs.noteCoverPlacement)) {
prefs.noteCoverPlacement = defaultQuickLookPrefs().noteCoverPlacement
}
for (const font of ['interfaceFont', 'textFont', 'monoFont'] as const) {
if (prefs[font] === '') prefs[font] = null
}
Expand Down
3 changes: 2 additions & 1 deletion apps/quicklook/src/shim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ const QUICKLOOK_APP_INFO: ZenAppInfo = {
runtime: 'web'
}

function decodeRef(href: string): string {
/** A reference's file path: no query or fragment, percent-decoded. */
export function decodeRef(href: string): string {
const cleaned = href.split('#')[0]?.split('?')[0] ?? href
try {
return decodeURIComponent(cleaned)
Expand Down
4 changes: 2 additions & 2 deletions apps/share-viewer/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@zennotes/share-viewer",
"private": true,
"version": "2.77.0",
"version": "2.78.0",
"type": "module",
"description": "Read-only renderer for publicly shared ZenNotes, embedded by the zennotes.org website",
"homepage": "https://zennotes.org",
Expand Down Expand Up @@ -34,7 +34,7 @@
"highlight.js": "^11.10.0",
"jsxgraph": "^1.12.2",
"katex": "^0.16.15",
"pele": "1.6.0",
"pele": "1.12.0",
"prettier": "^3.8.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
Expand Down
14 changes: 13 additions & 1 deletion apps/share-viewer/src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import type { AssetMeta, ImportedAssetKind, NoteContent, VaultInfo } from '@shared/ipc'
import { readNoteCover } from '@shared/note-cover'
import { readOnlyNoteCoverUrl } from '@shared/note-cover-gradients'
import { readSharePagePayload, type SharePagePayload } from './payload'
import { THEMES } from '@renderer/lib/themes'
import { installShareViewerBridge } from './shim'
import { ReadOnlyNoteCover } from '@renderer/components/ReadOnlyNoteCover'
import { installShareViewerBridge, lookupShareAsset } from './shim'
// The full app stylesheet (prose, themes, KaTeX, highlight, diagram
// chrome); the same file the PDF export window ships wholesale.
import '@renderer/styles/index.css'
Expand Down Expand Up @@ -93,8 +96,17 @@ async function boot(data: SharePagePayload): Promise<void> {
root.dataset.viewerReady = 'true'
}

// The cover image uploaded with the note (its `![[...]]` counts as an
// embed), an https cover as is, and a built-in gradient redrawn here.
const cover = readNoteCover(note.body)
const coverUrl = readOnlyNoteCoverUrl(cover, {
resolveVault: (href) => lookupShareAsset(data, href),
remote: true
})

ReactDOM.createRoot(rendered).render(
<React.StrictMode>
{cover && coverUrl ? <ReadOnlyNoteCover url={coverUrl} y={cover.y} /> : null}
<article className="zen-share-note">
<LazyPreview markdown={note.body} notePath={note.path} onRendered={onRendered} />
</article>
Expand Down
Loading
Loading