mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(desktop): WYSIWYG parity between chat composer and message timeline
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 (<br>). 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.
This commit is contained in:
parent
bfafdd46b2
commit
aabfe1d55d
@@ -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
|
||||
// (<br>), 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
|
||||
|
||||
@@ -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
|
||||
* <ul> when a list is freshly toggled) with zero added margin — so the
|
||||
* composer height stays static until additional items are added.
|
||||
*/
|
||||
.rich-text-composer .tiptap {
|
||||
outline: none;
|
||||
min-height: 1.5rem; /* single line height */
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5rem;
|
||||
@apply text-sm leading-7 outline-none;
|
||||
min-height: 1.75rem; /* one line at leading-7 (28px) */
|
||||
}
|
||||
|
||||
/* Inter-block spacing — matches markdown.tsx default variant. */
|
||||
.rich-text-composer .tiptap > * + * {
|
||||
@apply mt-3;
|
||||
}
|
||||
.rich-text-composer .tiptap > *:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
.rich-text-composer .tiptap > *:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Headings push and pull. */
|
||||
.rich-text-composer .tiptap > * + h1,
|
||||
.rich-text-composer .tiptap > * + h2,
|
||||
.rich-text-composer .tiptap > * + h3 {
|
||||
@apply mt-3.5;
|
||||
}
|
||||
.rich-text-composer .tiptap > h1 + *,
|
||||
.rich-text-composer .tiptap > h2 + *,
|
||||
.rich-text-composer .tiptap > h3 + * {
|
||||
@apply mt-0.5;
|
||||
}
|
||||
|
||||
/* Blockquote, code block, table, and hr breathe more. */
|
||||
.rich-text-composer .tiptap > * + blockquote,
|
||||
.rich-text-composer .tiptap > blockquote + * {
|
||||
@apply mt-3.5;
|
||||
}
|
||||
.rich-text-composer .tiptap > * + pre,
|
||||
.rich-text-composer .tiptap > pre + * {
|
||||
@apply mt-3.5;
|
||||
}
|
||||
.rich-text-composer .tiptap > * + hr,
|
||||
.rich-text-composer .tiptap > hr + * {
|
||||
@apply mt-4;
|
||||
}
|
||||
|
||||
/* Lists immediately after a paragraph hug a little tighter. */
|
||||
.rich-text-composer .tiptap > p + ul,
|
||||
.rich-text-composer .tiptap > p + ol {
|
||||
@apply mt-1.5;
|
||||
}
|
||||
|
||||
/* Paragraph placeholder. Paragraph itself has no margin — the owl rule
|
||||
* above provides spacing between sibling blocks. */
|
||||
.rich-text-composer .tiptap p {
|
||||
margin: 0;
|
||||
}
|
||||
@@ -254,81 +305,77 @@
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
/* Inline marks. */
|
||||
.rich-text-composer .tiptap strong {
|
||||
font-weight: 600;
|
||||
@apply font-semibold;
|
||||
}
|
||||
|
||||
.rich-text-composer .tiptap em {
|
||||
font-style: italic;
|
||||
@apply italic;
|
||||
}
|
||||
|
||||
.rich-text-composer .tiptap s {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
.rich-text-composer .tiptap a {
|
||||
@apply font-medium text-primary underline underline-offset-4 cursor-pointer;
|
||||
}
|
||||
|
||||
/* Inline code. */
|
||||
.rich-text-composer .tiptap code {
|
||||
border-radius: 0.375rem;
|
||||
background: hsl(var(--muted));
|
||||
padding: 0.125rem 0.375rem;
|
||||
font-family: ui-monospace, monospace;
|
||||
font-size: 0.8125rem;
|
||||
@apply rounded-md bg-muted px-1.5 py-0.5 font-mono text-[13px] text-foreground;
|
||||
}
|
||||
|
||||
/* Code block. */
|
||||
.rich-text-composer .tiptap pre {
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid hsl(var(--border) / 0.7);
|
||||
background: hsl(var(--muted) / 0.6);
|
||||
padding: 0.375rem 0.75rem;
|
||||
overflow-x: auto;
|
||||
margin: 0.25rem 0;
|
||||
@apply max-h-[400px] overflow-x-auto overflow-y-auto rounded-xl border border-border/70 bg-muted/60 px-3 py-1.5 shadow-xs;
|
||||
}
|
||||
|
||||
.rich-text-composer .tiptap pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.5rem;
|
||||
@apply bg-transparent p-0 rounded-none text-[13px] leading-6;
|
||||
}
|
||||
|
||||
/* Blockquote. */
|
||||
.rich-text-composer .tiptap blockquote {
|
||||
border-left: 2px solid hsl(var(--border));
|
||||
padding-left: 1rem;
|
||||
font-style: italic;
|
||||
color: hsl(var(--muted-foreground));
|
||||
margin: 0.25rem 0;
|
||||
@apply border-l-2 border-border pl-4 italic text-muted-foreground;
|
||||
}
|
||||
.rich-text-composer .tiptap blockquote > *:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
.rich-text-composer .tiptap blockquote > * + * {
|
||||
@apply mt-2;
|
||||
}
|
||||
|
||||
/* Lists. The owl rule (& > * + *) above handles spacing relative to
|
||||
* surrounding blocks; per-list margin is intentionally not set, so
|
||||
* toggling an empty list does not change the editor height. */
|
||||
.rich-text-composer .tiptap ul {
|
||||
list-style-type: disc;
|
||||
padding-left: 1.5rem;
|
||||
margin: 0.25rem 0;
|
||||
@apply list-disc pl-6 space-y-1 marker:text-muted-foreground;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.rich-text-composer .tiptap ol {
|
||||
list-style-type: decimal;
|
||||
padding-left: 1.5rem;
|
||||
margin: 0.25rem 0;
|
||||
@apply list-decimal pl-6 space-y-1 marker:text-muted-foreground;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.rich-text-composer .tiptap li {
|
||||
margin: 0.125rem 0;
|
||||
@apply my-1;
|
||||
}
|
||||
|
||||
.rich-text-composer .tiptap li p {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.rich-text-composer .tiptap a {
|
||||
color: hsl(var(--primary));
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 4px;
|
||||
cursor: pointer;
|
||||
/* Headings (only reachable via paste, since heading input rules are
|
||||
* disabled in StarterKit — but pasted markdown round-trips). */
|
||||
.rich-text-composer .tiptap h1 {
|
||||
@apply text-xl font-semibold leading-8 tracking-tight;
|
||||
}
|
||||
.rich-text-composer .tiptap h2 {
|
||||
@apply text-lg font-semibold leading-7 tracking-tight;
|
||||
}
|
||||
.rich-text-composer .tiptap h3 {
|
||||
@apply text-base font-semibold leading-6 tracking-tight;
|
||||
}
|
||||
|
||||
/* Horizontal rule. */
|
||||
.rich-text-composer .tiptap hr {
|
||||
border-color: hsl(var(--border) / 0.8);
|
||||
margin: 0.5rem 0;
|
||||
@apply border-border/80;
|
||||
}
|
||||
|
||||
.rich-text-composer .tiptap .mention-highlight {
|
||||
|
||||
Reference in New Issue
Block a user