Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
c950259
Fix(editor): eight more decorations redraw when a cut parse finishes
adibhanna Oct 20, 2026
abcb3ea
Perf(editor): heading arrows no longer scan each visible section
adibhanna Oct 20, 2026
bf26b7a
Fix(callouts): collapsed callouts in long notes start folded too (#853)
adibhanna Oct 20, 2026
1fdbe58
Fix(editor): a click below a short note keeps the keyboard in the edi…
adibhanna Oct 20, 2026
0305e83
Fix(editor): Ctrl+Shift+Z redoes on Windows too (#967)
adibhanna Oct 20, 2026
56faa5a
Fix(quick-capture): the panel opens on the display under the pointer …
adibhanna Oct 22, 2026
ee2fd2a
Fix(links): a link to a missing [[2026-11-03]] creates that day's dai…
adibhanna Oct 22, 2026
d06cf66
Feat(tabs): next and previous tab work with Vim mode off (#973)
adibhanna Oct 22, 2026
27771b6
Fix(calendar): hovering a day shows its note the way it reads
adibhanna Oct 22, 2026
5d1de89
Test(editor): fold and diagram tests pass whether or not a parse runs…
adibhanna Oct 22, 2026
6b23068
Fix(templates): an edited Daily Note template still fills new daily n…
adibhanna Oct 22, 2026
b24efde
Test(editor): six more editor test mounts end on a complete parse
adibhanna Oct 22, 2026
2f8c825
Fix(editor): task metadata chips redraw when a cut parse finishes
adibhanna Oct 22, 2026
ab96e79
Fix(diagrams): a diagram keeps its last good drawing through a cut parse
adibhanna Oct 22, 2026
de82278
Feat(databases): create pages for every record at once
adibhanna Oct 22, 2026
ad5e52d
Fix(databases): a record's page file already in the folder gets linke…
adibhanna Oct 22, 2026
47a1bba
Docs(whats-new): the 2.74.0 highlights
adibhanna Oct 22, 2026
37b314e
Release: align desktop and shared packages at 2.74.0
adibhanna Oct 22, 2026
fe1bec3
Release(cli): bundle the verified 0.10.0 terminal release
adibhanna Oct 22, 2026
9e2c651
Deps: @modelcontextprotocol/sdk 1.32.0 for the advisories that fail n…
adibhanna Oct 22, 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.73.0",
"version": "2.74.0",
"description": "ZenNotes desktop shell",
"private": true,
"main": "./out/main/index.js",
Expand Down Expand Up @@ -48,7 +48,7 @@
"@codemirror/state": "^6.5.0",
"@codemirror/view": "^6.35.3",
"@lezer/highlight": "^1.2.1",
"@modelcontextprotocol/sdk": "^1.31.0",
"@modelcontextprotocol/sdk": "^1.32.0",
"@myriaddreamin/typst-ts-renderer": "^0.7.0",
"@myriaddreamin/typst-ts-web-compiler": "^0.7.0",
"@myriaddreamin/typst.ts": "^0.7.0",
Expand Down
23 changes: 23 additions & 0 deletions apps/desktop/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,7 @@ import {
showTray,
} from "./tray";
import { shouldQuitWithOnlyQuickCaptureLeft } from "./last-window";
import { quickCaptureBoundsOnDisplay } from "./quick-capture-placement";
import {
checkForAppUpdates,
describeInstall,
Expand Down Expand Up @@ -5537,10 +5538,32 @@ async function showQuickCaptureWindow(): Promise<void> {
) {
inheritWindowWorkspaceSession(sourceWindow, win);
}
// A hidden panel opens on the display under the pointer (#976). One already
// on screen stays put: the user can see it, and a pinned panel sits where
// it was placed.
if (!win.isVisible()) moveQuickCaptureToPointerDisplay(win);
win.show();
win.focus();
}

/** Where the panel last sat on each display it left, by display id. */
const quickCaptureBoundsByDisplay = new Map<number, Electron.Rectangle>();

function moveQuickCaptureToPointerDisplay(win: BrowserWindow): void {
const bounds = win.getBounds();
const from = screen.getDisplayMatching(bounds);
const to = screen.getDisplayNearestPoint(screen.getCursorScreenPoint());
const next = quickCaptureBoundsOnDisplay({
bounds,
from,
to,
remembered: quickCaptureBoundsByDisplay.get(to.id),
});
if (!next) return;
quickCaptureBoundsByDisplay.set(from.id, bounds);
win.setBounds(next);
}

async function toggleQuickCaptureWindow(): Promise<void> {
const win = quickCaptureWindow;
if (win && !win.isDestroyed() && win.isVisible() && win.isFocused()) {
Expand Down
73 changes: 73 additions & 0 deletions apps/desktop/src/main/quick-capture-placement.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { describe, expect, it } from 'vitest'
import { quickCaptureBoundsOnDisplay, type DisplayArea } from './quick-capture-placement'

// Work areas of a two-display Mac like the one in #976: a main display with
// the Dock below, and a smaller built-in display to its left, lower down.
const main: DisplayArea = { id: 1, workArea: { x: 0, y: 30, width: 2560, height: 1356 } }
const side: DisplayArea = { id: 2, workArea: { x: -1728, y: 206, width: 1728, height: 1085 } }

describe('the Quick capture panel opens on the display under the pointer (#976)', () => {
it('stays exactly where it is on its own display', () => {
const bounds = { x: 140, y: 90, width: 620, height: 480 }
expect(quickCaptureBoundsOnDisplay({ bounds, from: main, to: main })).toBeNull()
})

it('opens centered on the other display when it was centered', () => {
const bounds = { x: 970, y: 468, width: 620, height: 480 }
expect(quickCaptureBoundsOnDisplay({ bounds, from: main, to: side })).toEqual({
x: -1728 + (1728 - 620) / 2,
y: Math.round(206 + (1085 - 480) / 2),
width: 620,
height: 480
})
})

it('keeps a corner the user dragged it to', () => {
// Top right of the main display, flush with its edges.
const bounds = { x: 2560 - 620, y: 30, width: 620, height: 480 }
expect(quickCaptureBoundsOnDisplay({ bounds, from: main, to: side })).toEqual({
x: -620,
y: 206,
width: 620,
height: 480
})
})

it('shrinks to fit a smaller work area and stays inside it', () => {
const small: DisplayArea = { id: 3, workArea: { x: 2560, y: 0, width: 500, height: 400 } }
const bounds = { x: 200, y: 200, width: 620, height: 480 }
expect(quickCaptureBoundsOnDisplay({ bounds, from: main, to: small })).toEqual({
x: 2560,
y: 0,
width: 500,
height: 400
})
})

it('returns exactly to where it last sat on a display', () => {
const bounds = { x: 1700, y: 90, width: 700, height: 520 }
const there = quickCaptureBoundsOnDisplay({ bounds, from: main, to: side })!
expect(
quickCaptureBoundsOnDisplay({ bounds: there, from: side, to: main, remembered: bounds })
).toEqual(bounds)
})

it('maps afresh when the remembered spot no longer fits the display', () => {
// The display got smaller since, so the old spot hangs off its edge.
const remembered = { x: 2200, y: 1200, width: 620, height: 480 }
const bounds = { x: -1174, y: 509, width: 620, height: 480 }
const next = quickCaptureBoundsOnDisplay({ bounds, from: side, to: main, remembered })!
expect(next).not.toEqual(remembered)
expect(next.x + next.width).toBeLessThanOrEqual(2560)
expect(next.y + next.height).toBeLessThanOrEqual(30 + 1356)
})

it('lands fully on the new display when it hung off the edge of the old one', () => {
const bounds = { x: 2400, y: 1300, width: 620, height: 480 }
const next = quickCaptureBoundsOnDisplay({ bounds, from: main, to: side })!
expect(next.x + next.width).toBeLessThanOrEqual(0)
expect(next.y + next.height).toBeLessThanOrEqual(206 + 1085)
expect(next.x).toBeGreaterThanOrEqual(-1728)
expect(next.y).toBeGreaterThanOrEqual(206)
})
})
65 changes: 65 additions & 0 deletions apps/desktop/src/main/quick-capture-placement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/** A rectangle in Electron's screen coordinates (DIPs, all displays share one space). */
export interface ScreenRect {
x: number
y: number
width: number
height: number
}

/** The part of a display Electron reports that windows may use. */
export interface DisplayArea {
id: number
workArea: ScreenRect
}

const clamp = (value: number, min: number, max: number): number =>
Math.min(Math.max(value, min), Math.max(min, max))

const contains = (area: ScreenRect, rect: ScreenRect): boolean =>
rect.x >= area.x &&
rect.y >= area.y &&
rect.x + rect.width <= area.x + area.width &&
rect.y + rect.height <= area.y + area.height

/**
* Where the Quick capture panel opens when the hotkey shows it again. The
* panel is created once and hidden between captures, so it kept the bounds of
* its first show and came back on that display even with the pointer and the
* focused window on another one (#976); as a macOS panel floating above other
* windows, tiling window managers leave it alone, so nothing outside the app
* could move it either.
*
* On the display it is already on, it stays exactly where the user left it
* (null). Back on a display it has been on, it returns to where it last sat
* there, as long as that still fits the display's work area. On a display it
* has not been on, it takes the same place there: its center keeps its
* relative position in the work area, so a centered panel opens centered and
* one dragged to a corner opens in that corner, shrunk to fit a smaller work
* area and kept inside it. That mapping rounds, so without the remembered
* spot each trip there and back could move the panel by a pixel.
*/
export function quickCaptureBoundsOnDisplay(input: {
bounds: ScreenRect
/** The display the panel is on now. */
from: DisplayArea
/** The display under the pointer. */
to: DisplayArea
/** Where the panel last sat on `to`, if it has been there. */
remembered?: ScreenRect
}): ScreenRect | null {
const { bounds, from, to, remembered } = input
if (from.id === to.id) return null
const source = from.workArea
const area = to.workArea
if (remembered && contains(area, remembered)) return { ...remembered }
const width = Math.min(bounds.width, area.width)
const height = Math.min(bounds.height, area.height)
const fx = source.width > 0 ? clamp((bounds.x + bounds.width / 2 - source.x) / source.width, 0, 1) : 0.5
const fy = source.height > 0 ? clamp((bounds.y + bounds.height / 2 - source.y) / source.height, 0, 1) : 0.5
return {
x: clamp(Math.round(area.x + fx * area.width - width / 2), area.x, area.x + area.width - width),
y: clamp(Math.round(area.y + fy * area.height - height / 2), area.y, area.y + area.height - height),
width,
height
}
}
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.9.1",
"commit": "c6e988604834fa65314e6f8617f2e83b4461334e",
"version": "0.10.0",
"commit": "ced7926715549e85349e545f780a9e4bdca1585f",
"artifacts": {
"darwin-arm64": {
"url": "https://github.com/ZenNotes/tui/releases/download/v0.9.1/zn_0.9.1_darwin_arm64.tar.gz",
"sha256": "6ee4b0d9ed60417453bae5aed10c1cb92cd2d1504be79afb5e6b8fb31caed99a"
"url": "https://github.com/ZenNotes/tui/releases/download/v0.10.0/zn_0.10.0_darwin_arm64.tar.gz",
"sha256": "0ab6f5596079cadb1329161dd9a6716f253355465a4ec8e527838fbb8ec97ee6"
},
"darwin-x64": {
"url": "https://github.com/ZenNotes/tui/releases/download/v0.9.1/zn_0.9.1_darwin_amd64.tar.gz",
"sha256": "58c990a91900df44684f942d11907fec79c2712ca1a513278f957328779478c6"
"url": "https://github.com/ZenNotes/tui/releases/download/v0.10.0/zn_0.10.0_darwin_amd64.tar.gz",
"sha256": "dcbe9cfe7b116b1667e282dc81fcf13e8340b41d7f9575e0919481bd75908376"
},
"linux-arm64": {
"url": "https://github.com/ZenNotes/tui/releases/download/v0.9.1/zn_0.9.1_linux_arm64.tar.gz",
"sha256": "b8a3dd4a3f160c2d5d9184a0a4ca941822d410623ef3c609aa841638dc866a54"
"url": "https://github.com/ZenNotes/tui/releases/download/v0.10.0/zn_0.10.0_linux_arm64.tar.gz",
"sha256": "b7bd5f8a1b22988240d612879b3194a07212ba8d7a51112df822ba935680d3b0"
},
"linux-x64": {
"url": "https://github.com/ZenNotes/tui/releases/download/v0.9.1/zn_0.9.1_linux_amd64.tar.gz",
"sha256": "8064c1282f63ce520ba252c09be4d867c8b8b4417d88c547cd5ea4e74a2a84da"
"url": "https://github.com/ZenNotes/tui/releases/download/v0.10.0/zn_0.10.0_linux_amd64.tar.gz",
"sha256": "d7aeca19b82c29b994cc0c2bcf6d3eff2767f3dea0842170462d21b6eda394f1"
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion 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.73.0",
"version": "2.74.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
2 changes: 1 addition & 1 deletion 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.73.0",
"version": "2.74.0",
"type": "module",
"description": "Read-only renderer for publicly shared ZenNotes, embedded by the zennotes.org website",
"homepage": "https://zennotes.org",
Expand Down
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@zennotes/web",
"private": true,
"version": "2.73.0",
"version": "2.74.0",
"type": "module",
"description": "ZenNotes web client for self-hosted and hosted deployments",
"homepage": "https://zennotes.org",
Expand Down
28 changes: 14 additions & 14 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "zennotes-monorepo",
"private": true,
"version": "2.73.0",
"version": "2.74.0",
"description": "ZenNotes monorepo for desktop, web, and self-hosted server builds",
"packageManager": "npm@10.9.2",
"engines": {
Expand Down
2 changes: 1 addition & 1 deletion packages/app-core/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@zennotes/app-core",
"private": true,
"version": "2.73.0",
"version": "2.74.0",
"type": "module",
"exports": {
"./main": "./src/main.tsx",
Expand Down
Loading
Loading