Skip to content

feat(TextEditor): prose-v3 typography and code block improvements#620

Merged
netchampfaris merged 7 commits intomainfrom
better-prose-styles
Apr 20, 2026
Merged

feat(TextEditor): prose-v3 typography and code block improvements#620
netchampfaris merged 7 commits intomainfrom
better-prose-styles

Conversation

@netchampfaris
Copy link
Copy Markdown
Contributor

@netchampfaris netchampfaris commented Apr 13, 2026

Summary

This PR adds a new prose-v3 typography modifier for TextEditor content and improves code block presentation in light and dark themes.

It does not make prose-v3 the default yet. TextEditor still defaults to prose-sm.

What changed

  • added typography.v3 in tailwind/plugin.js
  • added prose-p-spacing as a compatibility modifier for legacy paragraph spacing
  • updated CodeBlockComponent.vue styling and language UI
  • added targeted style.css overrides for readonly empty paragraphs and blockquote border specificity

Design decisions

  • Paragraph spacing in prose-v3 is explicit: paragraphs have no default margins.
  • Heading, list, block, and rule spacing follow a tighter, consistent rhythm.
  • All prose-v3 styling is opt-in and scoped; existing typography defaults stay unchanged.
  • prose-p-spacing provides a transition path for older content.
  • Code blocks use a GitHub-inspired light/dark treatment with clearer language affordances.

Notes

  • Removed the in-repo PROSE_V3.md note and moved the rationale here.
  • Switching TextEditor to prose-v3 by default is a follow-up decision.

…uage label

- Add prose-v3 typography modifier (zero paragraph margins, 8px grid spacing)
- Default editorClass changed to prose-v3
- Remove prose-v2 CSS (replaced by plugin-based approach)
- Code block: GitHub light/dark themes based on data-theme
- Code block: 12px font, 1.7 line-height, border, border-radius
- Code block: language selector always visible in edit mode
- Code block: static language label in readonly mode (reactive via editor.on('update'))
- Blockquote: 2px border-inline-start via style.css override
- Empty paragraph height: 20px in prose-v3
- Add PROSE_V3.md documenting design decisions and migration strategy
netchampfaris added a commit to frappe/gameplan that referenced this pull request Apr 13, 2026
… branch

- Switch all editor instances from prose-sm to prose-v3
- Point frappe-ui to frappe/frappe-ui#better-prose-styles
  (frappe-ui PR: frappe/frappe-ui#620)
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 13, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This pull request introduces a new prose-v3 typography system for the TextEditor component. The change includes: (1) comprehensive documentation describing the prose-v3 paragraph-spacing model, typography parameters, element-specific styling rules, code block implementation, and data migration strategy for existing content; (2) updated CodeBlockComponent with conditional language selector visibility based on editor editability state and GitHub light theme styling with dark mode overrides; (3) additional CSS rules for empty paragraph line-height and blockquote border styling; and (4) a new Tailwind typography variant plugin with complete prose CSS configuration, custom color variables, margin/padding rules, and nested element styling.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main changes: introducing prose-v3 typography and improving code blocks, which are the two primary components of this changeset.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/TextEditor/components/CodeBlockComponent.vue`:
- Around line 42-46: The mounted() hook attaches an anonymous listener via
this.editor.on('update', ...) but never removes it, causing a memory leak;
refactor to store the callback in a stable reference (e.g.
this.handleEditorUpdate) and use that same function to unsubscribe in
beforeUnmount() by calling this.editor.off('update', this.handleEditorUpdate),
ensuring you update this.isEditable inside the handler (keep the reference name
consistent with the component methods/fields such as mounted, beforeUnmount, and
isEditable).

In `@src/components/TextEditor/PROSE_V3.md`:
- Around line 24-36: Update the PROSE_V3.md docs to match the implemented
values: change the documented line-height from 1.6 to 1.7 to match plugin.js,
update "Empty paragraph" from ≈24px to 20px to match the height set in
style.css, and change the documented li spacing from 2px to 4px to match the li
margin set in plugin.js; reference the specific keys "line-height" in plugin.js,
the empty paragraph "height" in style.css, and the "li" margin in plugin.js when
making these edits.
- Around line 22-25: Update the fenced code block in PROSE_V3.md that contains
"Base unit: 8px / Empty paragraph: 14px × 1.6..." to include a language
identifier (e.g., use ```text or ```plaintext) on the opening fence so the
linter stops flagging it; only modify the opening fence to ```text (or
```plaintext) and leave the block contents unchanged.

In `@tailwind/plugin.js`:
- Around line 393-401: The comment for the v3 prose settings is inconsistent
with the implementation: PROSE_V3.md and the inline comment state a 1.6
line-height (14px × 1.6 ≈ 22.4px) but the actual config sets lineHeight: '1.7'
under the v3 css block (fontSize: '14px', fontWeight: 420, lineHeight: '1.7').
Fix by either updating the comment/PROSE_V3.md to reflect 1.7 (and the new
calculated px) or change the lineHeight value to '1.6' so the code matches the
comment and documentation; locate the v3 css object (v3 -> css -> { fontSize,
fontWeight, lineHeight }) to make the edit.
- Around line 444-533: The file defines blockquote twice so the second
declaration overrides the first and drops border/color settings; merge the two
by removing the first blockquote object and copying its borderInlineStartWidth,
borderInlineStartStyle, borderInlineStartColor, color and any fontStyle into the
later blockquote block that contains margin/padding/quotes, then move the
adjacent selectors 'blockquote p:first-of-type::before' and 'blockquote
p:last-of-type::after' next to the merged blockquote to keep the no-quote rules
together; ensure the resulting single blockquote contains all properties
(borderInlineStartWidth, borderInlineStartColor, borderInlineStartStyle,
fontStyle, color, marginTop, marginBottom, paddingInlineStart, quotes).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 371869f5-0ecd-47d1-877b-65e18b366438

📥 Commits

Reviewing files that changed from the base of the PR and between 404a2a2 and 2fde537.

📒 Files selected for processing (5)
  • src/components/TextEditor/PROSE_V3.md
  • src/components/TextEditor/TextEditor.vue
  • src/components/TextEditor/components/CodeBlockComponent.vue
  • src/components/TextEditor/style.css
  • tailwind/plugin.js

Comment on lines +42 to +46
mounted() {
this.editor.on('update', () => {
this.isEditable = this.editor.isEditable
})
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Does TipTap editor.on() return an unbind function?

💡 Result:

No, TipTap's editor.on does not return an unbind function. It follows a standard EventEmitter pattern where editor.on(event, callback) adds the listener but returns the editor instance (this) for chaining. To unbind, use editor.off(event, callback) with the exact same callback reference. This is shown in the official documentation with examples like: const onUpdate = => { ... }; editor.on('update', onUpdate); editor.off('update', onUpdate);

Citations:


Missing event listener cleanup — potential memory leak.

The editor.on('update', ...) listener is registered in mounted() but never removed. This will leak memory if the component is mounted/unmounted multiple times. TipTap uses a standard EventEmitter pattern: to unbind, call editor.off(event, callback) with the same callback reference in beforeUnmount().

🐛 Proposed fix: store callback and call editor.off() on unmount
   data() {
     return {
       isEditable: this.editor.isEditable,
+      onUpdate: null,
     }
   },
   mounted() {
-    this.editor.on('update', () => {
+    this.onUpdate = () => {
       this.isEditable = this.editor.isEditable
-    })
+    }
+    this.editor.on('update', this.onUpdate)
   },
+  beforeUnmount() {
+    if (this.onUpdate) {
+      this.editor.off('update', this.onUpdate)
+    }
+  },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/TextEditor/components/CodeBlockComponent.vue` around lines 42
- 46, The mounted() hook attaches an anonymous listener via
this.editor.on('update', ...) but never removes it, causing a memory leak;
refactor to store the callback in a stable reference (e.g.
this.handleEditorUpdate) and use that same function to unsubscribe in
beforeUnmount() by calling this.editor.off('update', this.handleEditorUpdate),
ensuring you update this.isEditable inside the handler (keep the reference name
consistent with the component methods/fields such as mounted, beforeUnmount, and
isEditable).

Comment thread src/components/TextEditor/PROSE_V3.md Outdated
Comment thread src/components/TextEditor/PROSE_V3.md Outdated
Comment thread tailwind/plugin.js
Comment thread tailwind/plugin.js
Comment on lines +444 to +533
// blockquote: left border, receded color, no italic, no quote marks
blockquote: {
'border-inline-start-width': '2px',
borderInlineStartColor: 'var(--ink-gray-3)',
borderInlineStartStyle: 'solid',
fontStyle: 'normal',
color: 'var(--ink-gray-6)',
},
'blockquote p:first-of-type::before': { content: 'none' },
'blockquote p:last-of-type::after': { content: 'none' },

// paragraphs: zero margin — user controls spacing with empty paragraphs
p: {
marginTop: '0',
marginBottom: '0',
},

// headings: marginTop creates section break (32/24px),
// marginBottom keeps heading close to its content (8px, proximity)
// h1/h2: full weight + darkest; h3-h5: softer weight + stepped-back color
h1: {
fontSize: em(20, 14),
marginTop: '32px',
marginBottom: '8px',
lineHeight: '1.3',
},
h2: {
fontSize: em(18, 14),
marginTop: '32px',
marginBottom: '8px',
lineHeight: '1.35',
},
h3: {
fontSize: em(16, 14),
marginTop: '24px',
marginBottom: '8px',
lineHeight: '1.4',
},
h4: {
fontSize: em(14, 14),
marginTop: '24px',
marginBottom: '8px',
lineHeight: '1.45',
},
h5: {
fontSize: em(13, 14),
marginTop: '24px',
marginBottom: '8px',
lineHeight: '1.45',
},

// element after heading gets no extra top margin
'h1 + *': { marginTop: '0' },
'h2 + *': { marginTop: '0' },
'h3 + *': { marginTop: '0' },
'h4 + *': { marginTop: '0' },
'h5 + *': { marginTop: '0' },

// lists: small outer margin (4px), tight internal spacing
ul: {
marginTop: '4px',
marginBottom: '4px',
paddingInlineStart: '1.5em',
},
ol: {
marginTop: '4px',
marginBottom: '4px',
paddingInlineStart: '1.5em',
},
li: {
marginTop: '4px',
marginBottom: '4px',
},
'li p': {
marginTop: '4px',
marginBottom: '4px',
},
'ul ul, ul ol, ol ul, ol ol': {
marginTop: '4px',
marginBottom: '4px',
},

// blockquote: breathing room (16px), no italic, no quotes
blockquote: {
marginTop: '16px',
marginBottom: '16px',
paddingInlineStart: '1em',
fontStyle: 'normal',
quotes: 'none',
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Duplicate blockquote property — second declaration overrides the first.

The static analyzer correctly identified that blockquote is defined twice (lines 445-451 and 527-533). The second declaration will completely override the first, losing the borderInlineStartColor and color properties.

🐛 Proposed fix: merge blockquote declarations

Remove the first blockquote block (lines 445-451) and merge its properties into the second one:

-                // blockquote: left border, receded color, no italic, no quote marks
-                blockquote: {
-                  'border-inline-start-width': '2px',
-                  borderInlineStartColor: 'var(--ink-gray-3)',
-                  borderInlineStartStyle: 'solid',
-                  fontStyle: 'normal',
-                  color: 'var(--ink-gray-6)',
-                },
-                'blockquote p:first-of-type::before': { content: 'none' },
-                'blockquote p:last-of-type::after': { content: 'none' },
-
                 // paragraphs: zero margin — user controls spacing with empty paragraphs

Then update the second blockquote block to include all properties:

                 // blockquote: breathing room (16px), no italic, no quotes
                 blockquote: {
                   marginTop: '16px',
                   marginBottom: '16px',
                   paddingInlineStart: '1em',
                   fontStyle: 'normal',
                   quotes: 'none',
+                  'border-inline-start-width': '2px',
+                  borderInlineStartColor: 'var(--ink-gray-3)',
+                  borderInlineStartStyle: 'solid',
+                  color: 'var(--ink-gray-6)',
                 },

Also move the ::before/::after selectors to be adjacent to the merged blockquote block.

🧰 Tools
🪛 Biome (2.4.10)

[error] 445-451: This property is later overwritten by an object member with the same name.

(lint/suspicious/noDuplicateObjectKeys)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tailwind/plugin.js` around lines 444 - 533, The file defines blockquote twice
so the second declaration overrides the first and drops border/color settings;
merge the two by removing the first blockquote object and copying its
borderInlineStartWidth, borderInlineStartStyle, borderInlineStartColor, color
and any fontStyle into the later blockquote block that contains
margin/padding/quotes, then move the adjacent selectors 'blockquote
p:first-of-type::before' and 'blockquote p:last-of-type::after' next to the
merged blockquote to keep the no-quote rules together; ensure the resulting
single blockquote contains all properties (borderInlineStartWidth,
borderInlineStartColor, borderInlineStartStyle, fontStyle, color, marginTop,
marginBottom, paddingInlineStart, quotes).

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
src/components/TextEditor/PROSE_V3.md (1)

155-159: ⚠️ Potential issue | 🟡 Minor

Add language identifier to code block.

The linter flags this code block as missing a language identifier. Since it describes a deployment sequence in plain text, consider using text or plaintext.

✏️ Suggested fix
-```
+```text
 1. Deploy code (prose-v3 default)   ← can still revert code-only, zero data risk
 2. Run forward migration             ← insert marked empty paragraphs
 3. Monitor                           ← if issues, run rollback + revert code
 ```
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/TextEditor/PROSE_V3.md` around lines 155 - 159, Update the
Markdown code block that lists the deployment steps so it includes a language
identifier (use "text" or "plaintext"); locate the fenced code block starting
with "1. Deploy code (prose-v3 default)" in PROSE_V3.md and change the opening
fence from ``` to ```text so the linter recognizes it as plain text.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/components/TextEditor/PROSE_V3.md`:
- Around line 155-159: Update the Markdown code block that lists the deployment
steps so it includes a language identifier (use "text" or "plaintext"); locate
the fenced code block starting with "1. Deploy code (prose-v3 default)" in
PROSE_V3.md and change the opening fence from ``` to ```text so the linter
recognizes it as plain text.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5a0cd704-650c-41a5-b2fb-caf7d4713104

📥 Commits

Reviewing files that changed from the base of the PR and between 2fde537 and a71191d.

📒 Files selected for processing (1)
  • src/components/TextEditor/PROSE_V3.md

@safwansamsudeen
Copy link
Copy Markdown
Contributor

Some observations:

  • Italics can look better
  • Numerals feel worse in this modified font IMO

… non-breaking

- Revert default editorClass back to prose-sm (non-breaking for existing users)
- Restore prose-v2 CSS that was removed (restores backward compatibility)
- prose-v3 remains as an opt-in modifier, not enforced by default
@netchampfaris netchampfaris merged commit 7fcfbb6 into main Apr 20, 2026
4 checks passed
@netchampfaris netchampfaris deleted the better-prose-styles branch April 20, 2026 23:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants