From 43186d28e7a58d9af88a420c13b5b685a599ab82 Mon Sep 17 00:00:00 2001 From: 01zulfi <85733202+01zulfi@users.noreply.github.com> Date: Wed, 5 Nov 2025 14:02:48 +0500 Subject: [PATCH 1/9] core: fix user keys re-encryption when updating password Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com> --- packages/core/src/api/user-manager.ts | 35 +++++++++++++++++++-------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/packages/core/src/api/user-manager.ts b/packages/core/src/api/user-manager.ts index 8ab3cc5c4..bd6875833 100644 --- a/packages/core/src/api/user-manager.ts +++ b/packages/core/src/api/user-manager.ts @@ -666,6 +666,15 @@ class UserManager { usesFallback: await this.usesFallbackPWHash(old_password) }); + // retrieve user keys before deriving a new encryption key + const oldUserKeys = { + attachmentsKey: await this.getAttachmentsKey(), + monographPasswordsKey: await this.getMonographPasswordsKey(), + inboxKeys: (await this.hasInboxKeys()) + ? await this.getInboxKeys() + : undefined + } as const; + await this.db.storage().deriveCryptoKey({ password: new_password, salt @@ -678,27 +687,33 @@ class UserManager { const userEncryptionKey = await this.getEncryptionKey(); if (userEncryptionKey) { const updateUserPayload: Partial = {}; - const attachmentsKey = await this.getAttachmentsKey(); - if (attachmentsKey) { + if (oldUserKeys.attachmentsKey) { user.attachmentsKey = await this.db .storage() - .encrypt(userEncryptionKey, JSON.stringify(attachmentsKey)); + .encrypt( + userEncryptionKey, + JSON.stringify(oldUserKeys.attachmentsKey) + ); updateUserPayload.attachmentsKey = user.attachmentsKey; } - const monographPasswordsKey = await this.getMonographPasswordsKey(); - if (monographPasswordsKey) { + if (oldUserKeys.monographPasswordsKey) { user.monographPasswordsKey = await this.db .storage() - .encrypt(userEncryptionKey, JSON.stringify(monographPasswordsKey)); + .encrypt( + userEncryptionKey, + JSON.stringify(oldUserKeys.monographPasswordsKey) + ); updateUserPayload.monographPasswordsKey = user.monographPasswordsKey; } - const inboxKeys = await this.getInboxKeys(); - if (inboxKeys) { + if (oldUserKeys.inboxKeys) { user.inboxKeys = { - public: inboxKeys.publicKey, + public: oldUserKeys.inboxKeys.publicKey, private: await this.db .storage() - .encrypt(userEncryptionKey, JSON.stringify(inboxKeys.privateKey)) + .encrypt( + userEncryptionKey, + JSON.stringify(oldUserKeys.inboxKeys.privateKey) + ) }; updateUserPayload.inboxKeys = user.inboxKeys; } From f3f802092949fcef3c23b07c5546202c8692c4ee Mon Sep 17 00:00:00 2001 From: 01zulfi <85733202+01zulfi@users.noreply.github.com> Date: Mon, 27 Oct 2025 14:03:34 +0500 Subject: [PATCH 2/9] editor: disable collapsible headings inside tables Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com> --- packages/editor/src/extensions/heading/heading.ts | 13 ++++++++----- packages/editor/styles/styles.css | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/packages/editor/src/extensions/heading/heading.ts b/packages/editor/src/extensions/heading/heading.ts index 465e8d471..1873f4be3 100644 --- a/packages/editor/src/extensions/heading/heading.ts +++ b/packages/editor/src/extensions/heading/heading.ts @@ -184,11 +184,14 @@ export const Heading = TiptapHeading.extend({ if (typeof pos !== "number") return; const resolvedPos = editor.state.doc.resolve(pos); - const calloutAncestor = findParentNodeClosestToPos( - resolvedPos, - (node) => node.type.name === "callout" - ); - if (calloutAncestor) return; + const forbiddenParents = ["callout", "table"]; + if ( + findParentNodeClosestToPos(resolvedPos, (node) => + forbiddenParents.includes(node.type.name) + ) + ) { + return; + } if ( isClickWithinBounds( diff --git a/packages/editor/styles/styles.css b/packages/editor/styles/styles.css index 214ce04da..301c52c0e 100644 --- a/packages/editor/styles/styles.css +++ b/packages/editor/styles/styles.css @@ -1010,6 +1010,21 @@ del.diffdel { display: none; } +.ProseMirror table h1::before, +.ProseMirror table h2::before, +.ProseMirror table h3::before, +.ProseMirror table h4::before, +.ProseMirror table h5::before, +.ProseMirror table h6::before, +.ProseMirror table h1::after, +.ProseMirror table h2::after, +.ProseMirror table h3::after, +.ProseMirror table h4::after, +.ProseMirror table h5::after, +.ProseMirror table h6::after { + display: none; +} + [data-hidden="true"] { display: none !important; } From 9ebcbb7bd8130ba3e7e6d58f8c801ff16cf821e2 Mon Sep 17 00:00:00 2001 From: 01zulfi <85733202+01zulfi@users.noreply.github.com> Date: Wed, 19 Nov 2025 10:31:22 +0500 Subject: [PATCH 3/9] web: exclude empty paragraphs in editor stats Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com> --- apps/web/src/components/editor/tiptap.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/src/components/editor/tiptap.tsx b/apps/web/src/components/editor/tiptap.tsx index 3463e4526..5e3e2712f 100644 --- a/apps/web/src/components/editor/tiptap.tsx +++ b/apps/web/src/components/editor/tiptap.tsx @@ -118,7 +118,7 @@ function countCharacters(text: string) { function countParagraphs(fragment: Fragment) { let count = 0; fragment.nodesBetween(0, fragment.size, (node) => { - if (node.type.name === "paragraph") { + if (node.type.name === "paragraph" && node.content.size > 0) { count++; } return true; @@ -737,7 +737,7 @@ function toIEditor(editor: Editor): IEditor { function getSelectedParagraphs(editor: Editor, selection: Selection): number { let count = 0; editor.state.doc.nodesBetween(selection.from, selection.to, (node) => { - if (node.type.name === "paragraph") { + if (node.type.name === "paragraph" && node.content.size > 0) { count++; } return true; From 3e4779e6bfa357439fcd833f900e868c1276abd8 Mon Sep 17 00:00:00 2001 From: 01zulfi <85733202+01zulfi@users.noreply.github.com> Date: Wed, 19 Nov 2025 16:57:22 +0500 Subject: [PATCH 4/9] editor: fade checklist when checked Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com> Update packages/editor/styles/styles.css Signed-off-by: Abdullah Atta --- packages/editor/styles/styles.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/editor/styles/styles.css b/packages/editor/styles/styles.css index 301c52c0e..59234251b 100644 --- a/packages/editor/styles/styles.css +++ b/packages/editor/styles/styles.css @@ -739,6 +739,10 @@ p > *::selection { mask-size: cover; } +.simple-checklist > li.checked p { + opacity: 0.8; +} + /* Callout */ .ProseMirror div.callout { padding: 15px; From cec05b6dfc13218d110a3bd0b6e48a3bab096472 Mon Sep 17 00:00:00 2001 From: 01zulfi <85733202+01zulfi@users.noreply.github.com> Date: Tue, 18 Nov 2025 13:58:44 +0500 Subject: [PATCH 5/9] web: allow archiving when notes drag-n-dropped on nav item Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com> --- apps/web/src/common/drop-handler.ts | 12 ++++++++++-- apps/web/src/components/navigation-menu/index.tsx | 2 ++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/apps/web/src/common/drop-handler.ts b/apps/web/src/common/drop-handler.ts index 89048061f..183c8ab0a 100644 --- a/apps/web/src/common/drop-handler.ts +++ b/apps/web/src/common/drop-handler.ts @@ -30,13 +30,18 @@ export async function handleDrop( item: | ItemReference | Context - | { type: "trash" | "notebooks" | "favorites" | undefined } + | { type: "trash" | "notebooks" | "favorites" | "archive" | undefined } ) { if (!item.type) return; const noteIds = getDragData(dataTransfer, "note"); const notebookIds = getDragData(dataTransfer, "notebook"); - const { setColor, favorite, delete: trashNotes } = useNoteStore.getState(); + const { + setColor, + favorite, + delete: trashNotes, + archive + } = useNoteStore.getState(); switch (item.type) { case "notebook": if (noteIds.length > 0) { @@ -83,5 +88,8 @@ export async function handleDrop( await useNoteStore.getState().refresh(); } break; + case "archive": + archive(true, ...noteIds); + break; } } diff --git a/apps/web/src/components/navigation-menu/index.tsx b/apps/web/src/components/navigation-menu/index.tsx index 6f8c67637..5a7476e6e 100644 --- a/apps/web/src/components/navigation-menu/index.tsx +++ b/apps/web/src/components/navigation-menu/index.tsx @@ -506,6 +506,8 @@ function RouteItem({ ? "trash" : item.path === "/favorites" ? "favorites" + : item.path == "/archive" + ? "archive" : undefined }); }} From bdd78df452567d6ebd6ab78c434a9a611609f484 Mon Sep 17 00:00:00 2001 From: 01zulfi <85733202+01zulfi@users.noreply.github.com> Date: Mon, 24 Nov 2025 10:49:43 +0500 Subject: [PATCH 6/9] editor: change heading collapse icon pos (#8953) * editor: enable heading in table, change collapse icon pos, && disable empty heading collapse Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com> * editor: migrate empty collapsed headings in parseHTML instead of plugin Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com> * editor: remove migration for empty collapsed headings Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com> * editor: fix heading collapse on mobile Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com> * editor: use ontouchend instead of ontouchstart --------- Co-authored-by: Abdullah Atta --- .../editor/src/extensions/heading/heading.ts | 59 +++--- packages/editor/styles/styles.css | 193 ++++++++---------- 2 files changed, 110 insertions(+), 142 deletions(-) diff --git a/packages/editor/src/extensions/heading/heading.ts b/packages/editor/src/extensions/heading/heading.ts index 1873f4be3..83a1cde0d 100644 --- a/packages/editor/src/extensions/heading/heading.ts +++ b/packages/editor/src/extensions/heading/heading.ts @@ -23,10 +23,8 @@ import { textblockTypeInputRule } from "@tiptap/core"; import { Heading as TiptapHeading } from "@tiptap/extension-heading"; -import { isClickWithinBounds } from "../../utils/prosemirror.js"; import { Plugin, PluginKey, Selection, Transaction } from "@tiptap/pm/state"; import { Node } from "@tiptap/pm/model"; -import { useToolbarStore } from "../../toolbar/stores/toolbar-store.js"; const COLLAPSIBLE_BLOCK_TYPES = [ "paragraph", @@ -168,6 +166,14 @@ export const Heading = TiptapHeading.extend({ addNodeView() { return ({ node, getPos, editor, HTMLAttributes }) => { const heading = document.createElement(`h${node.attrs.level}`); + const contentWrapper = document.createElement("div"); + const icon = document.createElement("span"); + + // providing a minWidth so that empty headings show the blinking cursor + contentWrapper.style.minWidth = "1px"; + + icon.className = "heading-collapse-icon"; + icon.contentEditable = "false"; for (const attr in HTMLAttributes) { heading.setAttribute(attr, HTMLAttributes[attr]); @@ -176,15 +182,16 @@ export const Heading = TiptapHeading.extend({ if (node.attrs.collapsed) heading.dataset.collapsed = "true"; else delete heading.dataset.collapsed; - function onClick(e: MouseEvent | TouchEvent) { - if (e instanceof MouseEvent && e.button !== 0) return; - if (!(e.target instanceof HTMLHeadingElement)) return; + function onIconClick(e: MouseEvent | TouchEvent) { + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); const pos = typeof getPos === "function" ? getPos() : 0; if (typeof pos !== "number") return; const resolvedPos = editor.state.doc.resolve(pos); - const forbiddenParents = ["callout", "table"]; + const forbiddenParents = ["callout"]; if ( findParentNodeClosestToPos(resolvedPos, (node) => forbiddenParents.includes(node.type.name) @@ -193,36 +200,28 @@ export const Heading = TiptapHeading.extend({ return; } - if ( - isClickWithinBounds( - e, - resolvedPos, - useToolbarStore.getState().isMobile ? "right" : "left" - ) - ) { - e.preventDefault(); - e.stopImmediatePropagation(); + editor.commands.command(({ tr }) => { + const currentNode = tr.doc.nodeAt(pos); + if (currentNode && currentNode.type.name === "heading") { + const shouldCollapse = !currentNode.attrs.collapsed; + const headingLevel = currentNode.attrs.level; - editor.commands.command(({ tr }) => { - const currentNode = tr.doc.nodeAt(pos); - if (currentNode && currentNode.type.name === "heading") { - const shouldCollapse = !currentNode.attrs.collapsed; - const headingLevel = currentNode.attrs.level; - - tr.setNodeAttribute(pos, "collapsed", shouldCollapse); - toggleNodesUnderHeading(tr, pos, headingLevel, shouldCollapse); - } - return true; - }); - } + tr.setNodeAttribute(pos, "collapsed", shouldCollapse); + toggleNodesUnderHeading(tr, pos, headingLevel, shouldCollapse); + } + return true; + }); } - heading.onmousedown = onClick; - heading.ontouchstart = onClick; + icon.onmousedown = onIconClick; + icon.ontouchend = onIconClick; + + heading.appendChild(contentWrapper); + heading.appendChild(icon); return { dom: heading, - contentDOM: heading, + contentDOM: contentWrapper, update: (updatedNode) => { if (updatedNode.type !== this.type) { return false; diff --git a/packages/editor/styles/styles.css b/packages/editor/styles/styles.css index 59234251b..1362eb640 100644 --- a/packages/editor/styles/styles.css +++ b/packages/editor/styles/styles.css @@ -870,163 +870,131 @@ del.diffdel { text-decoration: none; } -.ProseMirror h1, -.ProseMirror h2, -.ProseMirror h3, -.ProseMirror h4, -.ProseMirror h5, +.ProseMirror h1 , +.ProseMirror h2 , +.ProseMirror h3 , +.ProseMirror h4 , +.ProseMirror h5 , .ProseMirror h6 { - position: relative; + display: flex; + align-items: center; } -.ProseMirror h1::before, -.ProseMirror h2::before, -.ProseMirror h3::before, -.ProseMirror h4::before, -.ProseMirror h5::before, -.ProseMirror h6::before { - position: absolute; +.ProseMirror h1 .heading-collapse-icon, +.ProseMirror h2 .heading-collapse-icon, +.ProseMirror h3 .heading-collapse-icon, +.ProseMirror h4 .heading-collapse-icon, +.ProseMirror h5 .heading-collapse-icon, +.ProseMirror h6 .heading-collapse-icon { cursor: pointer; - content: ""; background-size: 18px; - width: 18px; - height: 18px; + margin-inline-start: 8px; background-color: var(--icon); mask: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGZpbGw9IiM4ODg4ODgiIGQ9Ik03LjQxIDguNThMMTIgMTMuMTdsNC41OS00LjU5TDE4IDEwbC02IDZsLTYtNmwxLjQxLTEuNDJaIi8+PC9zdmc+) no-repeat 50% 50%; mask-size: cover; - border: 1px solid var(--background); transform: rotate(0); transition: transform 250ms ease, opacity 200ms ease; - left: -22px; opacity: 0; + user-select: none; } -.ProseMirror h1[dir="rtl"]::before, -.ProseMirror h2[dir="rtl"]::before, -.ProseMirror h3[dir="rtl"]::before, -.ProseMirror h4[dir="rtl"]::before, -.ProseMirror h5[dir="rtl"]::before, -.ProseMirror h6[dir="rtl"]::before { - display: none; -} - -.ProseMirror h1[dir="rtl"]::after, -.ProseMirror h2[dir="rtl"]::after, -.ProseMirror h3[dir="rtl"]::after, -.ProseMirror h4[dir="rtl"]::after, -.ProseMirror h5[dir="rtl"]::after, -.ProseMirror h6[dir="rtl"]::after { - position: absolute; - cursor: pointer; - content: ""; - background-size: 18px; +.ProseMirror h1 .heading-collapse-icon { + margin-top: 3.5px; width: 18px; height: 18px; - - background-color: var(--icon); - mask: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGZpbGw9IiM4ODg4ODgiIGQ9Ik03LjQxIDguNThMMTIgMTMuMTdsNC41OS00LjU5TDE4IDEwbC02IDZsLTYtNmwxLjQxLTEuNDJaIi8+PC9zdmc+) - no-repeat 50% 50%; - mask-size: cover; - border: 1px solid var(--background); - - transform: rotate(0deg); - transition: transform 250ms ease, opacity 200ms ease; - right: -22px; - opacity: 0; } -.ProseMirror h1::before, -.ProseMirror h1::after { - top: 8px; +.ProseMirror h2 .heading-collapse-icon { + margin-top: 3px; + width: 16px; + height: 16px; } -.ProseMirror h2::before, -.ProseMirror h2::after - { - top: 3px; +.ProseMirror h3 .heading-collapse-icon { + margin-top: 2.3px; + width: 15px; + height: 15px; } -.ProseMirror h3::before, -.ProseMirror h3::after { - top: 0px; +.ProseMirror h4 .heading-collapse-icon { + margin-top: 1.8px; + width: 14px; + height: 14px; } -.ProseMirror h4::before, -.ProseMirror h4::after { - top: -1px; +.ProseMirror h5 .heading-collapse-icon { + margin-top: 1.3px; + width: 13px; + height: 13px; } -.ProseMirror h5::before, -.ProseMirror h5::after { - top: -2px; +.ProseMirror h6 .heading-collapse-icon { + margin-top: 0.3px; + width: 12px; + height: 12px; } -.ProseMirror h6::before, -.ProseMirror h6::after { - top: -4px; -} -.ProseMirror h1[data-collapsed="true"]::before, -.ProseMirror h2[data-collapsed="true"]::before, -.ProseMirror h3[data-collapsed="true"]::before, -.ProseMirror h4[data-collapsed="true"]::before, -.ProseMirror h5[data-collapsed="true"]::before, -.ProseMirror h6[data-collapsed="true"]::before { +.ProseMirror h1[data-collapsed="true"] .heading-collapse-icon, +.ProseMirror h2[data-collapsed="true"] .heading-collapse-icon, +.ProseMirror h3[data-collapsed="true"] .heading-collapse-icon, +.ProseMirror h4[data-collapsed="true"] .heading-collapse-icon, +.ProseMirror h5[data-collapsed="true"] .heading-collapse-icon, +.ProseMirror h6[data-collapsed="true"] .heading-collapse-icon { transform: rotate(-90deg); opacity: 1; } -.ProseMirror h1[data-collapsed="true"]::after, -.ProseMirror h2[data-collapsed="true"]::after, -.ProseMirror h3[data-collapsed="true"]::after, -.ProseMirror h4[data-collapsed="true"]::after, -.ProseMirror h5[data-collapsed="true"]::after, -.ProseMirror h6[data-collapsed="true"]::after { +.ProseMirror h1[data-collapsed="true"][dir="rtl"] .heading-collapse-icon, +.ProseMirror h2[data-collapsed="true"][dir="rtl"] .heading-collapse-icon, +.ProseMirror h3[data-collapsed="true"][dir="rtl"] .heading-collapse-icon, +.ProseMirror h4[data-collapsed="true"][dir="rtl"] .heading-collapse-icon, +.ProseMirror h5[data-collapsed="true"][dir="rtl"] .heading-collapse-icon, +.ProseMirror h6[data-collapsed="true"][dir="rtl"] .heading-collapse-icon { transform: rotate(90deg); opacity: 1; } - -.ProseMirror h1:hover::before, -.ProseMirror h2:hover::before, -.ProseMirror h3:hover::before, -.ProseMirror h4:hover::before, -.ProseMirror h5:hover::before, -.ProseMirror h6:hover::before, -.ProseMirror h1:hover::after, -.ProseMirror h2:hover::after, -.ProseMirror h3:hover::after, -.ProseMirror h4:hover::after, -.ProseMirror h5:hover::after, -.ProseMirror h6:hover::after { +.ProseMirror h1:hover .heading-collapse-icon, +.ProseMirror h2:hover .heading-collapse-icon, +.ProseMirror h3:hover .heading-collapse-icon, +.ProseMirror h4:hover .heading-collapse-icon, +.ProseMirror h5:hover .heading-collapse-icon, +.ProseMirror h6:hover .heading-collapse-icon { opacity: 1; } -.ProseMirror div.callout h1::before, -.ProseMirror div.callout h2::before, -.ProseMirror div.callout h3::before, -.ProseMirror div.callout h4::before, -.ProseMirror div.callout h5::before, -.ProseMirror div.callout h6::before { +.ProseMirror div.callout h1 .heading-collapse-icon, +.ProseMirror div.callout h2 .heading-collapse-icon, +.ProseMirror div.callout h3 .heading-collapse-icon, +.ProseMirror div.callout h4 .heading-collapse-icon, +.ProseMirror div.callout h5 .heading-collapse-icon, +.ProseMirror div.callout h6 .heading-collapse-icon { display: none; } -.ProseMirror table h1::before, -.ProseMirror table h2::before, -.ProseMirror table h3::before, -.ProseMirror table h4::before, -.ProseMirror table h5::before, -.ProseMirror table h6::before, -.ProseMirror table h1::after, -.ProseMirror table h2::after, -.ProseMirror table h3::after, -.ProseMirror table h4::after, -.ProseMirror table h5::after, -.ProseMirror table h6::after { - display: none; +/* hide collapse icon when heading is empty (only contains trailing break) */ +.ProseMirror h1:has(> div > br.ProseMirror-trailingBreak:only-child) .heading-collapse-icon, +.ProseMirror h2:has(> div > br.ProseMirror-trailingBreak:only-child) .heading-collapse-icon, +.ProseMirror h3:has(> div > br.ProseMirror-trailingBreak:only-child) .heading-collapse-icon, +.ProseMirror h4:has(> div > br.ProseMirror-trailingBreak:only-child) .heading-collapse-icon, +.ProseMirror h5:has(> div > br.ProseMirror-trailingBreak:only-child) .heading-collapse-icon, +.ProseMirror h6:has(> div > br.ProseMirror-trailingBreak:only-child) .heading-collapse-icon { + display: none !important; +} + +@media screen and (max-width: 768px) { + .ProseMirror h1 .heading-collapse-icon, + .ProseMirror h2 .heading-collapse-icon, + .ProseMirror h3 .heading-collapse-icon, + .ProseMirror h4 .heading-collapse-icon, + .ProseMirror h5 .heading-collapse-icon, + .ProseMirror h6 .heading-collapse-icon { + opacity: 1 !important; + } } [data-hidden="true"] { @@ -1047,3 +1015,4 @@ del.diffdel { pre[class*="language-"] { overflow: initial !important; } + From cf608977e13d8f7db7882c50776c0bd22da583fc Mon Sep 17 00:00:00 2001 From: 01zulfi <85733202+01zulfi@users.noreply.github.com> Date: Mon, 24 Nov 2025 14:18:13 +0500 Subject: [PATCH 7/9] editor: improve table cell styling (#8960) Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com> --- packages/editor/styles/styles.css | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/editor/styles/styles.css b/packages/editor/styles/styles.css index 1362eb640..85e398942 100644 --- a/packages/editor/styles/styles.css +++ b/packages/editor/styles/styles.css @@ -316,6 +316,31 @@ img.ProseMirror-separator { .ProseMirror table p { margin: 0; } + +.ProseMirror td > h1:first-child, +.ProseMirror td > h2:first-child, +.ProseMirror td > h3:first-child, +.ProseMirror td > h4:first-child, +.ProseMirror td > h5:first-child, +.ProseMirror td > h6:first-child { + margin-top: 0; +} + +.ProseMirror td > ol, +.ProseMirror td > ul { + padding-left: 20px; + margin-top: 0; +} + +.ProseMirror td > blockquote { + margin-left: 0; + margin-top: 0; +} + +.ProseMirror td > blockquote > :first-child { + margin-top: 0; +} + /* .resize-cursor { From 704ad578fa32c7cc1674db55acd4156e8a129de9 Mon Sep 17 00:00:00 2001 From: 01zulfi <85733202+01zulfi@users.noreply.github.com> Date: Mon, 24 Nov 2025 14:20:02 +0500 Subject: [PATCH 8/9] editor: unhide children when collapsed heading is changed to non-heading node (#8946) Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com> --- .../__snapshots__/heading.test.ts.snap | 10 ++++ .../heading/__tests__/heading.test.ts | 59 ++++++++++++++++++- .../editor/src/extensions/heading/heading.ts | 55 ++++++++--------- 3 files changed, 96 insertions(+), 28 deletions(-) diff --git a/packages/editor/src/extensions/heading/__tests__/__snapshots__/heading.test.ts.snap b/packages/editor/src/extensions/heading/__tests__/__snapshots__/heading.test.ts.snap index 36139c74a..36f948208 100644 --- a/packages/editor/src/extensions/heading/__tests__/__snapshots__/heading.test.ts.snap +++ b/packages/editor/src/extensions/heading/__tests__/__snapshots__/heading.test.ts.snap @@ -3,3 +3,13 @@ exports[`collapse heading > heading collapsed 1`] = `"

Main Heading

paragraph.

Subheading

subheading paragraph

Main heading 2

paragraph another

"`; exports[`collapse heading > heading uncollapsed 1`] = `"

Main Heading

paragraph.

Subheading

subheading paragraph

Main heading 2

paragraph another

"`; + +exports[`replacing collapsed heading with another heading level should not unhide content 1`] = `"

A collapsed heading

Hidden paragraph

"`; + +exports[`replacing collapsed heading with another node (blockquote) should unhide content 1`] = `"

A collpased heading

Hidden paragraph

"`; + +exports[`replacing collapsed heading with another node (bulletList) should unhide content 1`] = `"
  • A collpased heading

Hidden paragraph

"`; + +exports[`replacing collapsed heading with another node (codeBlock) should unhide content 1`] = `"
A collpased heading

Hidden paragraph

"`; + +exports[`replacing collapsed heading with another node (paragraph) should unhide content 1`] = `"

A collpased heading

Hidden paragraph

"`; diff --git a/packages/editor/src/extensions/heading/__tests__/heading.test.ts b/packages/editor/src/extensions/heading/__tests__/heading.test.ts index 9fda63e91..8f6a8a46d 100644 --- a/packages/editor/src/extensions/heading/__tests__/heading.test.ts +++ b/packages/editor/src/extensions/heading/__tests__/heading.test.ts @@ -18,8 +18,9 @@ along with this program. If not, see . */ import { test, expect } from "vitest"; -import { createEditor } from "../../../../test-utils/index.js"; +import { createEditor, h } from "../../../../test-utils/index.js"; import { Heading } from "../heading.js"; +import { Editor } from "@tiptap/core"; test("collapse heading", () => { const { editor } = createEditor({ @@ -52,3 +53,59 @@ test("collapse heading", () => { expect(editor.getHTML()).toMatchSnapshot("heading uncollapsed"); }); + +test("replacing collapsed heading with another heading level should not unhide content", () => { + const el = h("div", [ + h("h1", ["A collapsed heading"], { "data-collapsed": "true" }), + h("p", ["Hidden paragraph"], { "data-hidden": "true" }) + ]); + const { editor } = createEditor({ + extensions: { + heading: Heading.configure({ levels: [1, 2, 3, 4, 5, 6] }) + }, + initialContent: el.outerHTML + }); + + editor.commands.setTextSelection(0); + editor.commands.setHeading({ level: 2 }); + + expect(editor.getHTML()).toMatchSnapshot(); +}); + +const nodes: { name: string; setNode: (editor: Editor) => void }[] = [ + { + name: "paragraph", + setNode: (editor) => editor.commands.setParagraph() + }, + { + name: "codeBlock", + setNode: (editor) => editor.commands.setCodeBlock() + }, + { + name: "bulletList", + setNode: (editor) => editor.commands.toggleList("bulletList", "listItem") + }, + { + name: "blockquote", + setNode: (editor) => editor.commands.toggleBlockquote() + } +]; +for (const { name, setNode } of nodes) { + test(`replacing collapsed heading with another node (${name}) should unhide content`, () => { + const el = h("div", [ + h("h1", ["A collpased heading"], { "data-collapsed": "true" }), + h("p", ["Hidden paragraph"], { "data-hidden": "true" }) + ]); + const { editor } = createEditor({ + extensions: { + heading: Heading.configure({ levels: [1, 2, 3, 4, 5, 6] }) + }, + initialContent: el.outerHTML + }); + + editor.commands.setTextSelection(0); + setNode(editor); + + expect(editor.getHTML()).toMatchSnapshot(); + }); +} diff --git a/packages/editor/src/extensions/heading/heading.ts b/packages/editor/src/extensions/heading/heading.ts index 83a1cde0d..45ac83846 100644 --- a/packages/editor/src/extensions/heading/heading.ts +++ b/packages/editor/src/extensions/heading/heading.ts @@ -207,7 +207,7 @@ export const Heading = TiptapHeading.extend({ const headingLevel = currentNode.attrs.level; tr.setNodeAttribute(pos, "collapsed", shouldCollapse); - toggleNodesUnderHeading(tr, pos, headingLevel, shouldCollapse); + toggleNodesUnderPos(tr, pos, headingLevel, shouldCollapse); } return true; }); @@ -255,17 +255,17 @@ export const Heading = TiptapHeading.extend({ } }); -function toggleNodesUnderHeading( +function toggleNodesUnderPos( tr: Transaction, - headingPos: number, + pos: number, headingLevel: number, isCollapsing: boolean ) { const { doc } = tr; - const headingNode = doc.nodeAt(headingPos); - if (!headingNode || headingNode.type.name !== "heading") return; + const node = doc.nodeAt(pos); + if (!node) return; - let nextPos = headingPos + headingNode.nodeSize; + let nextPos = pos + node.nodeSize; const cursorPos = tr.selection.from; let shouldMoveCursor = false; let insideCollapsedHeading = false; @@ -320,8 +320,8 @@ function toggleNodesUnderHeading( } if (shouldMoveCursor) { - const headingEndPos = headingPos + headingNode.nodeSize - 1; - tr.setSelection(Selection.near(tr.doc.resolve(headingEndPos))); + const endPos = pos + node.nodeSize - 1; + tr.setSelection(Selection.near(tr.doc.resolve(endPos))); } } @@ -366,26 +366,27 @@ const headingUpdatePlugin = new Plugin({ let modified = false; newDoc.descendants((newNode, pos) => { - if (newNode.type.name === "heading") { - if (pos >= oldDoc.content.size) return; + if (pos >= oldDoc.content.size) return; - const oldNode = oldDoc.nodeAt(pos); - if ( - oldNode && - oldNode.type.name === "heading" && - oldNode.attrs.level !== newNode.attrs.level - ) { - /** - * if the level of a collapsed heading is changed, - * we need to reset visibility of all the nodes under it as there - * might be a heading of same or higher level previously - * hidden under this heading - */ - if (newNode.attrs.collapsed) { - toggleNodesUnderHeading(tr, pos, oldNode.attrs.level, false); - toggleNodesUnderHeading(tr, pos, newNode.attrs.level, true); - modified = true; - } + const oldNode = oldDoc.nodeAt(pos); + if ( + oldNode && + oldNode.type.name === "heading" && + oldNode.attrs.level !== newNode.attrs.level + ) { + /** + * if the level of a collapsed heading is changed, + * we need to reset visibility of all the nodes under it as there + * might be a heading of same or higher level previously + * hidden under this heading + */ + if (newNode.type.name === "heading" && newNode.attrs.collapsed) { + toggleNodesUnderPos(tr, pos, oldNode.attrs.level, false); + toggleNodesUnderPos(tr, pos, newNode.attrs.level, true); + modified = true; + } else if (newNode.type.name !== "heading" && oldNode.attrs.collapsed) { + toggleNodesUnderPos(tr, pos, oldNode.attrs.level, false); + modified = true; } } }); From 9f88eaae77b8b4b280ac168e1a0059be7166b136 Mon Sep 17 00:00:00 2001 From: Abdullah Atta Date: Mon, 24 Nov 2025 14:21:04 +0500 Subject: [PATCH 9/9] web: bump version to 3.3.6-beta.0 --- apps/desktop/package-lock.json | 4 ++-- apps/desktop/package.json | 2 +- apps/web/package-lock.json | 4 ++-- apps/web/package.json | 2 +- apps/web/src/dialogs/feature-dialog.tsx | 13 +------------ 5 files changed, 7 insertions(+), 18 deletions(-) diff --git a/apps/desktop/package-lock.json b/apps/desktop/package-lock.json index 7a7348aba..4974979f6 100644 --- a/apps/desktop/package-lock.json +++ b/apps/desktop/package-lock.json @@ -1,12 +1,12 @@ { "name": "@notesnook/desktop", - "version": "3.3.5", + "version": "3.3.6-beta.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@notesnook/desktop", - "version": "3.3.5", + "version": "3.3.6-beta.0", "hasInstallScript": true, "license": "GPL-3.0-or-later", "dependencies": { diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 194205643..f3c1d3163 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -2,7 +2,7 @@ "name": "@notesnook/desktop", "productName": "Notesnook", "description": "Your private note taking space", - "version": "3.3.5", + "version": "3.3.6-beta.0", "appAppleId": "1544027013", "private": true, "main": "./dist/cjs/index.js", diff --git a/apps/web/package-lock.json b/apps/web/package-lock.json index c9130d6c9..e4fac6277 100644 --- a/apps/web/package-lock.json +++ b/apps/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "@notesnook/web", - "version": "3.3.5", + "version": "3.3.6-beta.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@notesnook/web", - "version": "3.3.5", + "version": "3.3.6-beta.0", "hasInstallScript": true, "license": "GPL-3.0-or-later", "dependencies": { diff --git a/apps/web/package.json b/apps/web/package.json index 28a451344..7533b7516 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,7 +1,7 @@ { "name": "@notesnook/web", "description": "Your private note taking space", - "version": "3.3.5", + "version": "3.3.6-beta.0", "private": true, "main": "./src/app.js", "homepage": "https://notesnook.com/", diff --git a/apps/web/src/dialogs/feature-dialog.tsx b/apps/web/src/dialogs/feature-dialog.tsx index 41164fd43..67117c72b 100644 --- a/apps/web/src/dialogs/feature-dialog.tsx +++ b/apps/web/src/dialogs/feature-dialog.tsx @@ -91,18 +91,7 @@ const features: Record = { ) } ] - : [ - { - title: "Notesnook Circle", - subtitle: - "Notesnook Circle brings together trusted partners who share our commitment to privacy, transparency, and user freedom. As a member you get exclusive discounts and offers from our partners. Check it out in Settings > Notesnook Circle." - }, - { - title: "Collapsible headings", - subtitle: - "You can now collapse and expand headings in your notes. This makes it easier to manage large notes and focus on specific sections." - } - ], + : [], cta: { title: strings.gotIt(), icon: Checkmark,