diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 776c2f23f..c18825a6f 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@zennotes/desktop", "productName": "ZenNotes", - "version": "2.72.0", + "version": "2.73.0", "description": "ZenNotes desktop shell", "private": true, "main": "./out/main/index.js", diff --git a/apps/desktop/terminal-release.json b/apps/desktop/terminal-release.json index ccdd7c80c..4f6e7ced2 100644 --- a/apps/desktop/terminal-release.json +++ b/apps/desktop/terminal-release.json @@ -3,24 +3,24 @@ "release": { "repository": "ZenNotes/tui", "protocol": 1, - "version": "0.9.0", - "commit": "a0d335c459c04fdccb8686d81d481e10634a3948", + "version": "0.9.1", + "commit": "c6e988604834fa65314e6f8617f2e83b4461334e", "artifacts": { "darwin-arm64": { - "url": "https://github.com/ZenNotes/tui/releases/download/v0.9.0/zn_0.9.0_darwin_arm64.tar.gz", - "sha256": "1f97fcbf307ccfba03496311bdc04e9708c10e822ac2abdf03ffa7131197f92e" + "url": "https://github.com/ZenNotes/tui/releases/download/v0.9.1/zn_0.9.1_darwin_arm64.tar.gz", + "sha256": "6ee4b0d9ed60417453bae5aed10c1cb92cd2d1504be79afb5e6b8fb31caed99a" }, "darwin-x64": { - "url": "https://github.com/ZenNotes/tui/releases/download/v0.9.0/zn_0.9.0_darwin_amd64.tar.gz", - "sha256": "2f95711520fda368cff318d744b87b3afa98cccefcb34b35d86cce967c4c0bb8" + "url": "https://github.com/ZenNotes/tui/releases/download/v0.9.1/zn_0.9.1_darwin_amd64.tar.gz", + "sha256": "58c990a91900df44684f942d11907fec79c2712ca1a513278f957328779478c6" }, "linux-arm64": { - "url": "https://github.com/ZenNotes/tui/releases/download/v0.9.0/zn_0.9.0_linux_arm64.tar.gz", - "sha256": "b0a08f2575fec89e87f76fcb5dc9a7410c9b8e3327922e587605ccdd3588f2dc" + "url": "https://github.com/ZenNotes/tui/releases/download/v0.9.1/zn_0.9.1_linux_arm64.tar.gz", + "sha256": "b8a3dd4a3f160c2d5d9184a0a4ca941822d410623ef3c609aa841638dc866a54" }, "linux-x64": { - "url": "https://github.com/ZenNotes/tui/releases/download/v0.9.0/zn_0.9.0_linux_amd64.tar.gz", - "sha256": "6ca45da7e8adcbed0037888961239fe4dbb6b960ec87844a8b6fb2439484f523" + "url": "https://github.com/ZenNotes/tui/releases/download/v0.9.1/zn_0.9.1_linux_amd64.tar.gz", + "sha256": "8064c1282f63ce520ba252c09be4d867c8b8b4417d88c547cd5ea4e74a2a84da" } } } diff --git a/apps/quicklook/package.json b/apps/quicklook/package.json index 32c08ce75..3c2497873 100644 --- a/apps/quicklook/package.json +++ b/apps/quicklook/package.json @@ -1,7 +1,7 @@ { "name": "@zennotes/quicklook", "private": true, - "version": "2.72.0", + "version": "2.73.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", diff --git a/apps/share-viewer/package.json b/apps/share-viewer/package.json index 27829a49b..86402a34a 100644 --- a/apps/share-viewer/package.json +++ b/apps/share-viewer/package.json @@ -1,7 +1,7 @@ { "name": "@zennotes/share-viewer", "private": true, - "version": "2.72.0", + "version": "2.73.0", "type": "module", "description": "Read-only renderer for publicly shared ZenNotes, embedded by the zennotes.org website", "homepage": "https://zennotes.org", diff --git a/apps/web/package.json b/apps/web/package.json index dd1b07888..4c4c0baa3 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,7 +1,7 @@ { "name": "@zennotes/web", "private": true, - "version": "2.72.0", + "version": "2.73.0", "type": "module", "description": "ZenNotes web client for self-hosted and hosted deployments", "homepage": "https://zennotes.org", diff --git a/package-lock.json b/package-lock.json index d07cdae83..dba92754f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "zennotes-monorepo", - "version": "2.72.0", + "version": "2.73.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "zennotes-monorepo", - "version": "2.72.0", + "version": "2.73.0", "hasInstallScript": true, "workspaces": [ "apps/*", @@ -23,7 +23,7 @@ }, "apps/desktop": { "name": "@zennotes/desktop", - "version": "2.72.0", + "version": "2.73.0", "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.18.3", @@ -915,7 +915,7 @@ }, "apps/quicklook": { "name": "@zennotes/quicklook", - "version": "2.72.0", + "version": "2.73.0", "dependencies": { "@codemirror/autocomplete": "^6.18.3", "@codemirror/commands": "^6.7.1", @@ -989,7 +989,7 @@ }, "apps/share-viewer": { "name": "@zennotes/share-viewer", - "version": "2.72.0", + "version": "2.73.0", "dependencies": { "@codemirror/autocomplete": "^6.18.3", "@codemirror/commands": "^6.7.1", @@ -1061,7 +1061,7 @@ }, "apps/web": { "name": "@zennotes/web", - "version": "2.72.0", + "version": "2.73.0", "dependencies": { "@codemirror/autocomplete": "^6.18.3", "@codemirror/commands": "^6.7.1", @@ -16409,7 +16409,7 @@ }, "packages/app-core": { "name": "@zennotes/app-core", - "version": "2.72.0", + "version": "2.73.0", "dependencies": { "@codemirror/autocomplete": "^6.18.3", "@codemirror/commands": "^6.7.1", @@ -16498,14 +16498,14 @@ }, "packages/bridge-contract": { "name": "@zennotes/bridge-contract", - "version": "2.72.0", + "version": "2.73.0", "devDependencies": { "typescript": "^5.7.2" } }, "packages/shared-domain": { "name": "@zennotes/shared-domain", - "version": "2.72.0", + "version": "2.73.0", "dependencies": { "@scure/bip39": "2.4.0", "@zennotes/bridge-contract": "*", @@ -16519,7 +16519,7 @@ }, "packages/shared-ui": { "name": "@zennotes/shared-ui", - "version": "2.72.0" + "version": "2.73.0" } } } diff --git a/package.json b/package.json index 5355cfbdc..3fc609023 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "zennotes-monorepo", "private": true, - "version": "2.72.0", + "version": "2.73.0", "description": "ZenNotes monorepo for desktop, web, and self-hosted server builds", "packageManager": "npm@10.9.2", "engines": { diff --git a/packages/app-core/package.json b/packages/app-core/package.json index c918d7785..841eb17a1 100644 --- a/packages/app-core/package.json +++ b/packages/app-core/package.json @@ -1,7 +1,7 @@ { "name": "@zennotes/app-core", "private": true, - "version": "2.72.0", + "version": "2.73.0", "type": "module", "exports": { "./main": "./src/main.tsx", diff --git a/packages/app-core/src/App.tsx b/packages/app-core/src/App.tsx index 54e8df69d..9854f18ff 100644 --- a/packages/app-core/src/App.tsx +++ b/packages/app-core/src/App.tsx @@ -1201,10 +1201,29 @@ function App(): JSX.Element { } }, []) + // A window-drag strip belongs to the window, not the page, so a tab or note + // dragged inside the app may not be able to drop on one (the blank end of a + // tab strip with the title bar off). Nothing moves the window mid-drag, so + // the strips stand down until the drag ends. + useEffect(() => { + const root = document.documentElement + const start = (): void => root.classList.add('zn-app-dragging') + const end = (): void => root.classList.remove('zn-app-dragging') + window.addEventListener('dragstart', start, true) + window.addEventListener('dragend', end, true) + window.addEventListener('drop', end, true) + return () => { + window.removeEventListener('dragstart', start, true) + window.removeEventListener('dragend', end, true) + window.removeEventListener('drop', end, true) + end() + } + }, []) + if (!hasCompletedOnboarding) { return (
- {!zenMode && } + {!zenMode && } }> @@ -1223,7 +1242,7 @@ function App(): JSX.Element { if (!vault) { return (
- {!zenMode && } + {!zenMode && } }> diff --git a/packages/app-core/src/components/Editor.tsx b/packages/app-core/src/components/Editor.tsx index c2912347f..b30888e98 100644 --- a/packages/app-core/src/components/Editor.tsx +++ b/packages/app-core/src/components/Editor.tsx @@ -17,6 +17,7 @@ import { registerHalfPageMotion, } from "../lib/cm-vim-half-page-motion"; import { registerHeadingMotion } from "../lib/cm-vim-heading-motion"; +import { registerOpenLineContinuation } from "../lib/cm-vim-open-line"; import { registerReflowOperator } from "../lib/cm-vim-reflow"; import { harperAddWordAtCursor, @@ -88,7 +89,6 @@ import { selectActiveBuffer, } from "../lib/buffer-navigation"; import { applyVimInsertEscape } from "../lib/vim-insert-escape"; -import { listContinuationPrefix } from "../lib/list-continuation"; import { focusEditorNormalMode } from "../lib/editor-focus"; import { toVimSequence } from "../lib/vim-key-sequence"; import { registerNoteMoveExCommands } from "../lib/vim-ex-commands"; @@ -506,65 +506,7 @@ function registerVimCommands(): void { { context: "visual" }, ); - // #320: opening a line with o/O on a list item carries the marker forward — - // like pressing Enter — so bullets repeat, ordered numbers advance (the - // renumber pass fixes the exact value for both o and O), indentation is - // preserved, and a checkbox continues as a fresh unchecked box. Non-list lines - // fall back to Vim's built-in open-line. The handler runs as `actions[name]`, - // so `this` is Vim's action table — reuse its open-line + insert-mode entry. - type VimActionTable = { - newLineAndEnterInsertMode: ( - cm: unknown, - args: unknown, - vim: unknown, - ) => void; - enterInsertMode: (cm: unknown, args: unknown, vim: unknown) => void; - }; - Vim.defineAction("zenOpenLineContinuingList", function ( - this: VimActionTable, - cm: ReturnType, - actionArgs: { after?: boolean; repeat?: number }, - vim: { insertMode?: boolean }, - ) { - const view = (cm as unknown as { cm6?: EditorView }).cm6; - const prefix = view - ? listContinuationPrefix( - view.state.doc.lineAt(view.state.selection.main.head).text, - ) - : null; - if (!view || prefix == null) { - this.newLineAndEnterInsertMode(cm, actionArgs, vim); - return; - } - const line = view.state.doc.lineAt(view.state.selection.main.head); - vim.insertMode = true; - if (actionArgs.after) { - view.dispatch({ - changes: { from: line.to, insert: `\n${prefix}` }, - selection: { anchor: line.to + 1 + prefix.length }, - }); - } else { - view.dispatch({ - changes: { from: line.from, insert: `${prefix}\n` }, - selection: { anchor: line.from + prefix.length }, - }); - } - this.enterInsertMode(cm, { repeat: actionArgs.repeat }, vim); - } as unknown as Parameters[1]); - Vim.mapCommand( - "o", - "action", - "zenOpenLineContinuingList", - { after: true }, - { context: "normal", isEdit: true, interlaceInsertRepeat: true }, - ); - Vim.mapCommand( - "O", - "action", - "zenOpenLineContinuingList", - { after: false }, - { context: "normal", isEdit: true, interlaceInsertRepeat: true }, - ); + registerOpenLineContinuation(); // #290/#312: make j/k move by display line through soft-wrapped content. // Shared with the Quick Note window (QuickCaptureApp) via the same helper. diff --git a/packages/app-core/src/components/EditorPane.tsx b/packages/app-core/src/components/EditorPane.tsx index 31b1fdf09..3e8a72b65 100644 --- a/packages/app-core/src/components/EditorPane.tsx +++ b/packages/app-core/src/components/EditorPane.tsx @@ -348,6 +348,7 @@ import { import { isTabStripOverflowing } from '../lib/tab-strip-overflow' import { editorTabSize } from '../lib/editor-tab-size' import { cursorDrawSelection } from '../lib/cm-cursor-blink' +import { chromeCarriesWindowDrag, isPaneOnTopEdge } from '../lib/window-chrome' const MODE_OPTIONS: Array<{ mode: PaneMode @@ -949,6 +950,12 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element { ) const sidebarOpen = useStore((s) => s.sidebarOpen) const zenMode = useStore((s) => s.zenMode) + // With the title bar off, the top pane's chrome is what moves the window. + const runtime = window.zen.getAppInfo().runtime + const dragsWindow = useStore( + (s) => + chromeCarriesWindowDrag(s.showWindowTitleBar, runtime) && isPaneOnTopEdge(s.paneLayout, paneId) + ) const toggleSidebar = useStore((s) => s.toggleSidebar) const setFocusedPanel = useStore((s) => s.setFocusedPanel) const focusedPanel = useStore((s) => s.focusedPanel) @@ -3562,7 +3569,9 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element { data-tab-pane-id={paneId} data-tab-path={tab.path} data-tab-active={active ? 'true' : undefined} - className="relative" + // A window-drag strip takes the mouse from everything in it that is + // not marked no-drag, and a tab is dragged and right-clicked itself. + className={dragsWindow ? 'relative no-drag' : 'relative'} draggable={!isVirtual} onDragStart={(e) => { if (isVirtual) { @@ -4005,7 +4014,8 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element { // Outer header holds the back/forward nav buttons + the (flex-1) tab strip. const tabStripHeaderClass = [ 'glass-header flex shrink-0 items-stretch border-b border-paper-300/70 pl-1', - wrapTabs ? 'min-h-[var(--z-tab-height)]' : 'h-[var(--z-tab-height)]' + wrapTabs ? 'min-h-[var(--z-tab-height)]' : 'h-[var(--z-tab-height)]', + dragsWindow ? 'drag-region' : '' ].join(' ') const tabStripClass = [ 'workspace-tab-strip flex min-w-0 flex-1 items-stretch gap-0', @@ -4370,8 +4380,16 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element {
)} + {dragsWindow && !hasTabs && !content && !zenMode && ( + // No tab strip and no header: keep something to move the window by. +
+ )} {content && !zenMode && ( -
+
s.prefetchNotes) const focusedPanel = useStore((s) => s.focusedPanel) const vimMode = useStore((s) => s.vimMode) + const dragsWindow = useStore((s) => + chromeCarriesWindowDrag(s.showWindowTitleBar, window.zen.getAppInfo().runtime) + ) const noteListCursorIndex = useStore((s) => s.noteListCursorIndex) const setFocusedPanel = useStore((s) => s.setFocusedPanel) const systemFolderLabels = useStore((s) => s.systemFolderLabels) @@ -724,7 +728,11 @@ export function NoteList(): JSX.Element { onMouseDownCapture={() => setFocusedPanel('notelist')} onFocusCapture={() => setFocusedPanel('notelist')} > -
+

{heading}

diff --git a/packages/app-core/src/components/SettingsModal.test.ts b/packages/app-core/src/components/SettingsModal.test.ts index 8bea77f36..795e849b3 100644 --- a/packages/app-core/src/components/SettingsModal.test.ts +++ b/packages/app-core/src/components/SettingsModal.test.ts @@ -487,10 +487,14 @@ describe("SettingsModal date note directories", () => { } }); - it("does not offer native title bar settings in the web app", async () => { + it("offers the title bar switch in the web app, worded for a page", async () => { mocks.runtime = "web"; await act(async () => root.render(createElement(SettingsModal))); - expect(host.querySelector('[data-settings-search-id="window-title-bar"]')).toBeNull(); + const row = host.querySelector('[data-settings-search-id="window-title-bar"]'); + expect(row).not.toBeNull(); + expect(row?.textContent).toContain("Title bar"); + expect(row?.textContent).not.toContain("Window title bar"); + expect(row?.textContent).not.toContain("move the window"); }); it("rejects reserved prefixes with a visible explanation", async () => { diff --git a/packages/app-core/src/components/SettingsModal.tsx b/packages/app-core/src/components/SettingsModal.tsx index 4cfa2bd2b..6f8df80ca 100644 --- a/packages/app-core/src/components/SettingsModal.tsx +++ b/packages/app-core/src/components/SettingsModal.tsx @@ -1565,10 +1565,12 @@ export function SettingsModal(): JSX.Element { }, { id: "window-title-bar", - title: "Window title bar", - description: "Show the main window title and controls, or hide them to reclaim vertical space.", - keywords: ["decorations", "frameless", "traffic lights", "tiling", "hyprland", "linux"], - available: appInfo.runtime === "desktop", + title: appInfo.runtime === "desktop" ? "Window title bar" : "Title bar", + description: + appInfo.runtime === "desktop" + ? "Show the main window title and controls, or hide them to reclaim vertical space." + : "Show the bar with the open note's title, or hide it to reclaim vertical space.", + keywords: ["decorations", "frameless", "traffic lights", "tiling", "hyprland", "linux", "header"], }, { id: "keep-running-in-tray", @@ -1998,10 +2000,18 @@ export function SettingsModal(): JSX.Element { title="Chrome" description="Small visual adjustments that change how the shell feels." > - {appInfo.runtime === "desktop" && ( + {appInfo.runtime === "desktop" ? ( + ) : ( +
-
+
Guided tutorial
@@ -3314,10 +3324,10 @@ export function SettingsModal(): JSX.Element { : `${hidden} of ${total} recipes are hidden from the recipe gallery, behind New workflow (press x on a recipe there to hide one at a time).`; return (
-
+
Built-in recipes
@@ -3325,7 +3335,7 @@ export function SettingsModal(): JSX.Element { {copy}
-
+