Skip to content

feat: add per-block RTL support - #277

Open
n00ki wants to merge 2 commits into
bholmesdev:mainfrom
n00ki:feat/rtl
Open

feat: add per-block RTL support#277
n00ki wants to merge 2 commits into
bholmesdev:mainfrom
n00ki:feat/rtl

Conversation

@n00ki

@n00ki n00ki commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Rationale

Hubble lays out editor content from left to right, which makes Hebrew, Arabic, and other RTL text awkward to write and read.

This adds automatic direction detection per block, allowing LTR and RTL content to coexist in the same document. New empty blocks initially follow the preceding block’s direction, preventing the caret from jumping when writing several lines in the same language.

Alternatives considered

  • A document-wide direction setting would not work well for mixed-language notes.
  • Storing direction in Markdown or front matter would add application-specific metadata to otherwise portable files.
  • A custom ProseMirror plugin would duplicate the application’s built-in first-strong-character detection and require extra editor state.

Summary

  • Detect text direction automatically for each rich-text block and plain-text line.
  • Keep Markdown source and code blocks left to right.
  • Align list markers, task checkboxes, and quote borders with their block’s direction.
  • Keep new empty blocks on the preceding block’s side until their first strong character establishes a direction.
  • Add coverage for rich-text, plain-text, source, and code-block direction attributes.

Testing

  • pnpm --filter @hubble.md/ui test
  • pnpm check
  • pnpm check:react-compiler
  • pnpm build:desktop
  • Verified mixed English, Hebrew, and Arabic content in the desktop application.
  • Verified same-direction caret stability and intentional switching for opposite-direction input in paragraphs and list items.

@vercel

vercel Bot commented Aug 25, 2026

Copy link
Copy Markdown

@n00ki is attempting to deploy a commit to the bholmesdev's projects Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Overview

Per-block RTL via TipTap textDirection: "auto", LTR code/source, and CSS so list/quote chrome follows block direction. Empty new blocks try to keep the previous side until the first strong character.

Approach is sound (native TipTap, no Markdown metadata). One correctness gap breaks the empty-block inheritance story after the first blank line.

Concerns

  • Empty-block CSS keys off :dir(ltr|rtl) on the previous sibling. Empty nodes keep HTML dir="auto", so they never match :dir(ltr|rtl) even after CSS sets direction. Second+ blank line after RTL loses the intended side (double Enter, blank between paragraphs).
  • Tests only assert dir attributes, not resolved direction or empty-block inheritance.
  • No CHANGELOG.md [Unreleased] entry for this user-facing change.

Verdict

Found: 0 critical, 1 important, 2 suggestions

Request changes


Reviewed by a Warp Factory agent.

Comment thread packages/ui/src/editor/EditorView.css Outdated
Comment on lines +147 to +150
[data-hubble-editor]
.ProseMirror
:is(
:dir(rtl) + p:has(> br.ProseMirror-trailingBreak:only-child),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ [IMPORTANT] :dir(ltr|rtl) + empty p only matches when the previous element’s HTML direction is ltr/rtl. TipTap keeps empty blocks at dir="auto", and CSS direction does not change :dir(). After one blank line inherits RTL from a strong block, the next blank’s previous sibling is still dir=auto, so it drops out of these rules and the caret jumps LTR (double Enter / several empty lines).

Same issue on the LTR twin above. Fix: also match a previous empty that already got temporary direction (extra selector / data attr), or set real dir on empty blocks in JS from the previous strong block until content arrives.

Comment on lines +58 to +68
const editor = getEditor();
expect(editor.getAttribute("dir")).toBe("auto");
for (const selector of ["h1", "p", "blockquote", "ul", "li"]) {
const blocks = editor.querySelectorAll(selector);
expect(blocks.length).toBeGreaterThan(0);
for (const block of blocks)
expect(block.getAttribute("dir")).toBe("auto");
}
expect(editor.querySelector(".pm-code-block")?.getAttribute("dir")).toBe(
"ltr",
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 [SUGGESTION] These asserts only check dir="auto"|"ltr" attributes, which TipTap sets globally. They miss the CSS empty-block inheritance path and never assert computed/used direction for Hebrew/Arabic content or consecutive empty paragraphs. Prefer at least one case that documents the double-Enter caret side (and would have caught the :dir() issue above).


const editor = useEditor({
editable,
textDirection: "auto",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 [SUGGESTION] User-facing RTL support needs a CHANGELOG.md bullet under ## [Unreleased]### Added (CONTRIBUTING). One line on auto per-block direction for mixed LTR/RTL notes.

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.

1 participant