From e0862c6d53e0f80023f06d7dabe0325cc815fb88 Mon Sep 17 00:00:00 2001 From: usamaasfar Date: Sun, 22 Feb 2026 00:59:01 +0530 Subject: [PATCH 1/6] Improve editor writing experience - Add inline mark input rules: _italic_, *bold*, ~strikethrough~, `code` - Add Cmd+Enter as hard break (alongside Shift+Enter) - Add underline mark with Cmd+U shortcut - Add --- input rule to insert horizontal rule - Restore caretPlugin (pulled from main) - Fade-in animation on editor mount - Remove first-heading top margin - Tighten typography: line-height 1.75, paragraph spacing 0.5em Co-Authored-By: Claude Sonnet 4.6 --- apps/desktop/src/editor/Editor.tsx | 63 ++++++++++++++++++++++++----- apps/desktop/src/editor/markdown.ts | 9 ++++- apps/desktop/src/styles/editor.css | 25 ++++++++++-- 3 files changed, 80 insertions(+), 17 deletions(-) diff --git a/apps/desktop/src/editor/Editor.tsx b/apps/desktop/src/editor/Editor.tsx index 2a41f18..a8ce265 100644 --- a/apps/desktop/src/editor/Editor.tsx +++ b/apps/desktop/src/editor/Editor.tsx @@ -1,7 +1,8 @@ import { baseKeymap, chainCommands, exitCode, setBlockType, toggleMark, wrapIn } from "prosemirror-commands"; import { history, redo, undo } from "prosemirror-history"; -import { inputRules, textblockTypeInputRule, wrappingInputRule } from "prosemirror-inputrules"; +import { InputRule, inputRules, textblockTypeInputRule, wrappingInputRule } from "prosemirror-inputrules"; import { keymap } from "prosemirror-keymap"; +import type { MarkType } from "prosemirror-model"; import { liftListItem, sinkListItem, splitListItem, wrapInList } from "prosemirror-schema-list"; import { EditorState } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; @@ -9,9 +10,37 @@ import { useEffect, useRef } from "react"; import { caretPlugin } from "./plugins/caret"; import { parseMarkdown, schema, serializeMarkdown } from "./markdown"; +function markRule(pattern: RegExp, markType: MarkType): InputRule { + return new InputRule(pattern, (state, match, start, end) => { + const [full, inner] = match; + const markStart = start + full.indexOf(inner); + const markEnd = markStart + inner.length; + const mark = markType.create(); + const tr = state.tr + .addMark(markStart, markEnd, mark) + .delete(markEnd, end) + .delete(start, markStart) + .removeStoredMark(mark); + return tr; + }); +} + +function insertHorizontalRule() { + return new InputRule(/^---$/, (state, _match, start, end) => { + const { horizontal_rule, paragraph } = schema.nodes; + const tr = state.tr.replaceWith(start - 1, end, [ + horizontal_rule.create(), + paragraph.create(), + ]); + return tr; + }); +} + function buildInputRules() { + const { strong, em, code, strikethrough } = schema.marks; return inputRules({ rules: [ + // block rules textblockTypeInputRule(/^(#{1,3})\s$/, schema.nodes.heading, (match) => ({ level: match[1].length })), wrappingInputRule(/^\s*>\s$/, schema.nodes.blockquote), textblockTypeInputRule(/^```$/, schema.nodes.code_block), @@ -22,12 +51,19 @@ function buildInputRules() { (match) => ({ order: +match[1] }), (match, node) => node.childCount + node.attrs.order === +match[1], ), + insertHorizontalRule(), + // inline mark rules + markRule(/_([^_]+)_$/, em), // _italic_ + markRule(/\*([^*]+)\*$/, strong), // *bold* + markRule(/~~([^~]+)~~$/, strikethrough), // ~~strikethrough~~ + markRule(/~([^~]+)~$/, strikethrough), // ~strikethrough~ + markRule(/`([^`]+)`$/, code), // `code` ], }); } function buildKeymap() { - const { strong, em, code, strikethrough } = schema.marks; + const { strong, em, code, strikethrough, underline } = schema.marks; const { heading, code_block, blockquote, bullet_list, ordered_list, list_item, hard_break, paragraph } = schema.nodes; const insertHardBreak = chainCommands(exitCode, (state, dispatch) => { @@ -40,31 +76,33 @@ function buildKeymap() { "Mod-z": undo, "Mod-Shift-z": redo, - // inline marks — standard across iA Writer, Notion, Bear + // inline marks "Mod-b": toggleMark(strong), "Mod-i": toggleMark(em), - "Mod-e": toggleMark(code), // inline code (matches Bear/Notion) + "Mod-u": toggleMark(underline), + "Mod-e": toggleMark(code), "Mod-Shift-s": toggleMark(strikethrough), - // headings — Notion / Bear standard + // headings "Mod-Alt-1": setBlockType(heading, { level: 1 }), "Mod-Alt-2": setBlockType(heading, { level: 2 }), "Mod-Alt-3": setBlockType(heading, { level: 3 }), "Mod-Alt-0": setBlockType(paragraph), // blocks - "Mod-Shift-b": wrapIn(blockquote), // blockquote + "Mod-Shift-b": wrapIn(blockquote), "Mod-Alt-c": setBlockType(code_block), - // lists — standard across editors + // lists "Mod-Shift-7": wrapInList(ordered_list), "Mod-Shift-8": wrapInList(bullet_list), Enter: splitListItem(list_item), - "Mod-[": liftListItem(list_item), // outdent - "Mod-]": sinkListItem(list_item), // indent + "Mod-[": liftListItem(list_item), + "Mod-]": sinkListItem(list_item), - // line break + // line break — both Shift-Enter and Cmd-Enter "Shift-Enter": insertHardBreak, + "Mod-Enter": insertHardBreak, }); } @@ -115,7 +153,10 @@ export function Editor({ initialMarkdown, onChange }: EditorProps) { }); syncEmptyClass(viewRef.current); - requestAnimationFrame(() => viewRef.current?.focus()); + requestAnimationFrame(() => { + viewRef.current?.focus(); + mountRef.current?.classList.add("is-mounted"); + }); return () => { viewRef.current?.destroy(); diff --git a/apps/desktop/src/editor/markdown.ts b/apps/desktop/src/editor/markdown.ts index ece55c8..0cfdd01 100644 --- a/apps/desktop/src/editor/markdown.ts +++ b/apps/desktop/src/editor/markdown.ts @@ -3,7 +3,7 @@ import type Token from "markdown-it/lib/token.mjs"; import { defaultMarkdownSerializer, MarkdownParser, MarkdownSerializer } from "prosemirror-markdown"; import { Schema } from "prosemirror-model"; -// schema: CommonMark nodes + strikethrough mark +// schema: CommonMark nodes + strikethrough + underline marks export const schema = new Schema({ nodes: { doc: { content: "block+" }, @@ -164,6 +164,10 @@ export const schema = new Schema({ toDOM: () => ["s", 0], parseDOM: [{ tag: "s" }, { tag: "del" }, { tag: "strike" }], }, + underline: { + toDOM: () => ["u", 0], + parseDOM: [{ tag: "u" }], + }, }, }); @@ -220,10 +224,11 @@ export const markdownParser = new MarkdownParser(schema, md, { s: { mark: "strikethrough" }, }); -// serializer: default + strikethrough → ~~text~~ +// serializer: default + strikethrough → ~~text~~, underline → plain text (no MD syntax) export const markdownSerializer = new MarkdownSerializer(defaultMarkdownSerializer.nodes, { ...defaultMarkdownSerializer.marks, strikethrough: { open: "~~", close: "~~", mixable: true, expelEnclosingWhitespace: true }, + underline: { open: "", close: "", mixable: true, expelEnclosingWhitespace: true }, }); export function parseMarkdown(text: string) { diff --git a/apps/desktop/src/styles/editor.css b/apps/desktop/src/styles/editor.css index 7ac3dee..f8c665f 100644 --- a/apps/desktop/src/styles/editor.css +++ b/apps/desktop/src/styles/editor.css @@ -2,6 +2,12 @@ position: relative; width: 100%; height: 100%; + opacity: 0; + transition: opacity 120ms ease; +} + +.editor-mount.is-mounted { + opacity: 1; } .ProseMirror { @@ -13,7 +19,7 @@ caret-color: transparent; font-family: var(--font); font-size: var(--font-size); - line-height: 1.7; + line-height: 1.75; letter-spacing: 0.01em; color: inherit; word-break: break-word; @@ -28,7 +34,7 @@ } .ProseMirror p { - margin: 0 0 0.75em; + margin: 0 0 0.5em; } .ProseMirror h1, @@ -39,6 +45,12 @@ margin: 1.5em 0 0.4em; } +.ProseMirror > h1:first-child, +.ProseMirror > h2:first-child, +.ProseMirror > h3:first-child { + margin-top: 0; +} + .ProseMirror h1 { font-size: 2rem; } @@ -73,12 +85,12 @@ .ProseMirror ul, .ProseMirror ol { - margin: 0 0 0.75em 1.5em; + margin: 0 0 0.5em 1.5em; padding: 0; } .ProseMirror li { - margin-bottom: 0.25em; + margin-bottom: 0.2em; } .ProseMirror li > p { @@ -116,6 +128,11 @@ opacity: 0.5; } +.ProseMirror u { + text-decoration: underline; + text-underline-offset: 3px; +} + .ProseMirror-selectednode { outline: 2px solid rgba(128, 128, 128, 0.4); border-radius: 2px; From 5bc09fd11f630629fb574a6af5ca1094d2f8d7c8 Mon Sep 17 00:00:00 2001 From: usamaasfar Date: Sun, 22 Feb 2026 01:03:06 +0530 Subject: [PATCH 2/6] Disable autocomplete and spellcheck on rename input Co-Authored-By: Claude Sonnet 4.6 --- apps/desktop/src/App.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index d9807e5..03d164f 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -129,6 +129,10 @@ function App() { value={titleValue} size={Math.max(1, titleValue.length)} readOnly={!isRenaming} + autoComplete="off" + autoCorrect="off" + autoCapitalize="off" + spellCheck={false} onDoubleClick={startRename} onChange={(e) => { if (isRenaming) setRenameValue(e.target.value); From ca47c3b921c1e62a1032906b7bf6d15ad9dc07ed Mon Sep 17 00:00:00 2001 From: usamaasfar Date: Sun, 22 Feb 2026 01:11:45 +0530 Subject: [PATCH 3/6] Hide title by default, reveal on hover and file switch Title is hidden by default and fades in (400ms) on topbar hover. Untitled stays always visible as a nudge to rename. Flashes visible for 1.2s on file switch via useTitleVisibility hook. Stays visible while renaming. Co-Authored-By: Claude Sonnet 4.6 --- apps/desktop/src/App.tsx | 4 ++- apps/desktop/src/hooks/useTitleVisibility.ts | 28 ++++++++++++++++++++ apps/desktop/src/styles/globals.css | 10 ++++++- 3 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 apps/desktop/src/hooks/useTitleVisibility.ts diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index 03d164f..53c5d5a 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -9,6 +9,7 @@ import { useAutoUpdate } from "~/hooks/useAutoUpdate"; import { useAutoSave } from "~/hooks/useAutoSave"; import { useFileSystem } from "~/hooks/useFileSystem"; import { useRename } from "~/hooks/useRename"; +import { useTitleVisibility } from "~/hooks/useTitleVisibility"; import { getFileStem } from "~/utils/file"; function App() { @@ -113,6 +114,7 @@ function App() { const activeFileName = activePath ? getFileStem(activePath) : "Untitled"; const titleValue = isRenaming ? renameValue : activeFileName; + const titleSwitching = useTitleVisibility(activePath); const handleTopbarPointerDown = useCallback((e: React.PointerEvent) => { if (e.button !== 0) return; void getCurrentWindow().startDragging(); @@ -122,7 +124,7 @@ function App() {
-
+
| null>(null); + const prevRef = useRef(activePath); + + useEffect(() => { + if (prevRef.current === activePath) return; + prevRef.current = activePath; + + if (timerRef.current) clearTimeout(timerRef.current); + setVisible(true); + timerRef.current = setTimeout(() => setVisible(false), FLASH_DURATION); + + return () => { + if (timerRef.current) clearTimeout(timerRef.current); + }; + }, [activePath]); + + return visible; +} diff --git a/apps/desktop/src/styles/globals.css b/apps/desktop/src/styles/globals.css index 9dcb86a..1a2296c 100644 --- a/apps/desktop/src/styles/globals.css +++ b/apps/desktop/src/styles/globals.css @@ -75,6 +75,15 @@ body { align-items: center; max-width: calc(100vw - 180px); pointer-events: auto; + opacity: 0; + transition: opacity 400ms ease; +} + +.app-topbar:hover .app-title-wrap, +.app-title-wrap.is-untitled, +.app-title-wrap.is-switching, +.app-title-wrap:has(.is-editing) { + opacity: 1; } .app-title-input { @@ -92,7 +101,6 @@ body { line-height: 1.1; text-align: center; padding: 0; - opacity: 0.62; letter-spacing: 0.01em; } From 1e876c4dc64e8f82487f2969c9bbd4ef19c70df8 Mon Sep 17 00:00:00 2001 From: usamaasfar Date: Sun, 22 Feb 2026 01:13:44 +0530 Subject: [PATCH 4/6] Delete .npmrc --- .npmrc | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .npmrc diff --git a/.npmrc b/.npmrc deleted file mode 100644 index e69de29..0000000 From d8d2d070c037b4832d22e8da2c405585cec0245c Mon Sep 17 00:00:00 2001 From: Codex <242516109+Codex@users.noreply.github.com> Date: Sun, 22 Feb 2026 01:22:16 +0530 Subject: [PATCH 5/6] [desktop] Fix inline markdown input rules triggering mid-paragraph (#21) * Initial plan * Fix inline markdown input rule anchors --------- Co-authored-by: openai-code-agent[bot] <242516109+Codex@users.noreply.github.com> --- apps/desktop/src/editor/Editor.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/desktop/src/editor/Editor.tsx b/apps/desktop/src/editor/Editor.tsx index a8ce265..5d0c976 100644 --- a/apps/desktop/src/editor/Editor.tsx +++ b/apps/desktop/src/editor/Editor.tsx @@ -53,11 +53,11 @@ function buildInputRules() { ), insertHorizontalRule(), // inline mark rules - markRule(/_([^_]+)_$/, em), // _italic_ - markRule(/\*([^*]+)\*$/, strong), // *bold* - markRule(/~~([^~]+)~~$/, strikethrough), // ~~strikethrough~~ - markRule(/~([^~]+)~$/, strikethrough), // ~strikethrough~ - markRule(/`([^`]+)`$/, code), // `code` + markRule(/_([^_]+)_(?=\s|$)/, em), // _italic_ + markRule(/\*([^*]+)\*(?=\s|$)/, strong), // *bold* + markRule(/~~([^~]+)~~(?=\s|$)/, strikethrough), // ~~strikethrough~~ + markRule(/~([^~]+)~(?=\s|$)/, strikethrough), // ~strikethrough~ + markRule(/`([^`]+)`(?=\s|$)/, code), // `code` ], }); } From b493183b7b31563490f2c0e6a82e387df4444ff8 Mon Sep 17 00:00:00 2001 From: usamaasfar Date: Sun, 22 Feb 2026 01:39:31 +0530 Subject: [PATCH 6/6] Refine command palette styling for a more minimal look Co-Authored-By: Claude Sonnet 4.6 --- apps/desktop/src/styles/command.css | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/apps/desktop/src/styles/command.css b/apps/desktop/src/styles/command.css index cf079bc..86ef3eb 100644 --- a/apps/desktop/src/styles/command.css +++ b/apps/desktop/src/styles/command.css @@ -13,21 +13,21 @@ width: 480px; max-width: 90vw; background: var(--bg); - border: 1px solid color-mix(in srgb, var(--fg) 18%, transparent); - border-radius: 8px; + border: 1px solid color-mix(in srgb, var(--fg) 14%, transparent); + border-radius: 10px; overflow: hidden; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.18), 0 2px 8px rgba(0, 0, 0, 0.12); + box-shadow: 0 16px 48px rgba(0, 0, 0, 0.22), 0 2px 8px rgba(0, 0, 0, 0.10); } [cmdk-input] { width: 100%; - padding: 12px 16px; + padding: 14px 16px; font-family: var(--font); font-size: var(--font-size); color: var(--fg); background: transparent; border: none; - border-bottom: 1px solid color-mix(in srgb, var(--fg) 12%, transparent); + border-bottom: 1px solid color-mix(in srgb, var(--fg) 10%, transparent); outline: none; } @@ -38,13 +38,15 @@ [cmdk-list] { max-height: 320px; overflow-y: auto; - padding: 4px 0; + padding: 6px 0; } [cmdk-item] { display: flex; align-items: center; - padding: 8px 16px; + padding: 8px 14px; + margin: 0 4px; + border-radius: 6px; font-family: var(--font); font-size: var(--font-size); color: var(--fg); @@ -52,7 +54,7 @@ } [cmdk-item][data-selected="true"] { - background: color-mix(in srgb, var(--fg) 8%, transparent); + background: color-mix(in srgb, var(--fg) 7%, transparent); } [cmdk-empty] {