From aabfe1d55d09742cb27ff0c9d4e97a82f8160b4b Mon Sep 17 00:00:00 2001
From: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w
<52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@sprout-oss.stage.blox.sqprod.co>
Date: Mon, 8 Jun 2026 16:58:00 -0700
Subject: [PATCH] fix(desktop): WYSIWYG parity between chat composer and
message timeline
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Composer styling now mirrors the rendered Markdown timeline (default
variant in shared/ui/markdown.tsx) so what the user sees while typing
matches what they see after sending — every block element (p, ul, ol,
li, blockquote, pre, hr, h1/h2/h3, inline code, links).
Key changes
- Inter-block spacing driven by an owl rule (` & > * + * `) using the
same Tailwind tokens as the timeline (mt-3, mt-3.5 for headings /
blockquote / code-block, mt-4 for hr, mt-1.5 for p+ul/ol). Removes the
empty-list height jitter: a freshly toggled ul/ol with no items adds
no margin of its own, so the editor only grows when items appear.
- Editor wrapper bumped from leading-6 to leading-7 and min-height from
1.5rem to 1.75rem to match the rendered side.
- Per-element styles converted to Tailwind utilities via @apply:
list-disc/decimal pl-6 space-y-1 marker:text-muted-foreground on
ul/ol; my-1 [&_p]:inline on li; bordered/italic blockquote with
child-margin owl rules; rounded code-block with border/bg/shadow;
inline code with rounded bg-muted; primary underlined links.
- Headings (h1/h2/h3) gain proper sizing — only reachable via paste
since heading input rules are still disabled in StarterKit, but
pasted markdown round-trips correctly now.
Behavior change
- Shift+Enter now splits into a real new paragraph (splitBlock) instead
of inserting a hard break (
). Plain Enter still submits. This
matches user intuition (Slack/iMessage style) and keeps WYSIWYG —
previously the soft-break composer rendering looked tighter than the
paragraph spacing the message would receive on send. The hardBreak
node is still enabled in StarterKit so pasted markdown containing
explicit hard breaks round-trips.
Verified: pnpm typecheck, pnpm lint, pnpm build all green.
---
.../messages/lib/useRichTextEditor.ts | 14 +-
desktop/src/shared/styles/globals.css | 141 ++++++++++++------
2 files changed, 104 insertions(+), 51 deletions(-)
diff --git a/desktop/src/features/messages/lib/useRichTextEditor.ts b/desktop/src/features/messages/lib/useRichTextEditor.ts
index dc65ccad9..69dc079eb 100644
--- a/desktop/src/features/messages/lib/useRichTextEditor.ts
+++ b/desktop/src/features/messages/lib/useRichTextEditor.ts
@@ -112,7 +112,9 @@ export function useRichTextEditor({
{
extensions: [
StarterKit.configure({
- // Use hard breaks (Shift+Enter) — Enter submits the message.
+ // Hard-break node is kept enabled so pasted markdown that contains
+ // explicit hard breaks still round-trips, but Shift+Enter is rebound
+ // below (`smartShiftEnter`) to split into a real new paragraph.
hardBreak: {
keepMarks: true,
},
@@ -258,8 +260,12 @@ export function useRichTextEditor({
// Non-empty → split the paragraph within the blockquote.
return ed.chain().splitBlock().focus().run();
}
- // Default: hard break (StarterKit handles it).
- return false;
+ // Default: split into a real new paragraph. WYSIWYG parity
+ // with the rendered timeline — two paragraphs always have
+ // margin between them. Previously inserted a hard break
+ // (
), which produced a single \n on send and rendered
+ // visibly tighter than the post-send paragraph spacing.
+ return ed.chain().splitBlock().focus().run();
},
ArrowDown: ({ editor: ed }) => {
// Empty last list item + Down → exit list to paragraph below.
@@ -320,7 +326,7 @@ export function useRichTextEditor({
editorProps: {
attributes: {
class:
- "min-h-0 resize-none overflow-y-hidden border-0 bg-transparent px-0 py-0 text-sm leading-6 md:leading-6 shadow-none focus-visible:ring-0 caret-foreground outline-hidden prose-sm max-w-none",
+ "min-h-0 resize-none overflow-y-hidden border-0 bg-transparent px-0 py-0 text-sm leading-7 md:leading-7 shadow-none focus-visible:ring-0 caret-foreground outline-hidden max-w-none",
"data-testid": "message-input",
},
// ArrowUp in an empty composer → edit your last message (Slack
diff --git a/desktop/src/shared/styles/globals.css b/desktop/src/shared/styles/globals.css
index 60bd597ec..843a62014 100644
--- a/desktop/src/shared/styles/globals.css
+++ b/desktop/src/shared/styles/globals.css
@@ -221,13 +221,64 @@
}
/* ── Tiptap rich-text composer ──────────────────────────────────────── */
+/*
+ * Composer styles mirror the rendered Markdown timeline (see
+ * desktop/src/shared/ui/markdown.tsx, default variant) so the editor is
+ * visually WYSIWYG with the sent message. The owl rules `& > * + *` drive
+ * inter-block spacing, leaving an isolated single block (e.g. an empty
+ *