Skip to content

fix(web): give RTL tables a concrete direction Base UI can follow

MacroscopeApp / Macroscope - UI Consistency failed Aug 19, 2026 in 6m 12s

UI Consistency: 1 issue found

apps/web/src/components/ChatMarkdown.tsx — GitHub-alert title row: dir="ltr" is set on the title <p> itself. It correctly excludes the English label from the container's dir="auto" resolution, but it also forces that flex row to lay out LTR, so in an RTL alert the icon + label pack against the left edge while the border-s-2 bar and the body text sit on the right. Suggested fix (posted inline): move dir="ltr" onto a <span> around the label text and drop it from the paragraph, so the row inherits the resolved direction while the label subtree is still skipped by the auto-direction algorithm.

No other findings: the DirectionProvider + dir on the table scroll viewport matches Base UI's direction contract, and the logical-property migration in index.css is consistent.

Details

Note

Your check run agent prompt is: .macroscope/check-run-agents/ui-consistency.md
More information about how Check Run Agents work can be found in our Help Center.

Verification performed:

  • Confirmed DirectionProvider / TextDirection are exported from @base-ui/react/direction-provider (matches @base-ui/react ^1.4.1 in apps/web/package.json), so the new import and prop typing are valid.
  • Confirmed in Base UI's source that --scroll-area-overflow-x-start/-end are logical ("inline start/end") and that RTL normalization is driven by useDirection() / DirectionProvider, not the DOM dir. This makes both the DirectionProvider wrapper in MarkdownTable and the new rtl:mask-l-from-* / rtl:mask-r-from-* swap in ui/scroll-area.tsx correct; the physical mask utilities do need the swap under dir="rtl". The override lives in the owning primitive rather than at the call site, which is the right owner.
  • Checked apps/web/src/index.css for cascade conflicts introduced by the new .chat-markdown :is(p, li, h1..h6, td, th, dt, dd) rule: the only other text-align in the .chat-markdown block (th/td) is later in the file and now also start, so no behavioral conflict. GFM column alignment still wins via inline style. Table, list, blockquote and task-list rules touched by the patch are direction-agnostic or already logical.
  • Checked the react-markdown components map for duplicate keys after the new ul handler — none.
  • The prior-run findings (alert container never resolving to RTL, and the table's direction not reaching the scroll viewport) are addressed by this revision; only the side effect of the alert-label fix remains.

🔍 Agent activity — 1 tool call

💭 Looking at the current diff, it seems prior issues like the DirectionProvider and dir="ltr" label have been addressed. I want to verify the DirectionProvider import path is correct for this base-ui package, check the scroll-area rtl mask utilities, and look into the firstStrongD logic. I'm tracing through the Unicode ranges covered by the strong-RTL regex to confirm Syriac, Thaana, NKo, Samaritan, Mandaic, and Arabic extensions are all correctly classified as RTL, and that Hebrew combining marks don't interfere since they aren't matched as letters. I also want to confirm the test case with "Next.js then עברית" correctly resolves to ltr based on the first letter. Now I should check the actual files to see if DirectionProvider is already used elsewhere and whether the export path is accurate. I'm checking whether Tailwind's rtl: variant correctly applies the mask-fade overrides for right-to-left layouts, since it relies on a dir="rtl" ancestor selector. I also need to verify that dir actually gets forwarded from the ScrollArea component down to the underlying Base UI Root element, and that tailwind-merge properly dedupes the conflicting mask-l-from-* utilities between the base and rtl variants. Right, variants come after base utilities in Tailwind's sort order, so that should work as expected. Now I want to check whether Base UI's scroll area overflow variables are logical or physical properties — I should look at the actual Base UI source or package version to confirm how --scroll-area-overflow-x-start/end are defined. I'm checking whether wrapping the ScrollArea with DirectionProvider causes issues elsewhere, since the table content might include other interactive components like tooltips or menus rendered inside cells that could be affected by the direction context. Base UI's ScrollArea.Root should forward the dir prop to its underlying div, and the DirectionProvider context is what useDirection reads from internally, so the DOM attribute and the context should stay in sync. I'm now trying to recall the exact CSS variable naming Base UI uses for overflow start/end in the viewport to confirm the RTL mask swap logic is correct. Without access to the actual source, I can't fully verify the scroll offset normalization for RTL, but the PR's approach of feeding DirectionProvider should address the concern raised in the earlier review comment. Shifting focus to the new CSS changes, I'm checking that the bidi-aware markdown styles using unicode-bidi: plaintext and text-align: start are applied consistently across elements like table headers, which looks fine since it's scoped to generated markdown content. I'm also considering whether GFM table inline styles for cell alignment would conflict with the new class-based rules — inline styles take precedence, so that should be safe. Now I'm thinking through the subtlety of applying unicode-bidi: plaintext to list items alongside dir="auto" on the parent list, and how that affects marker placement relative to each item's resolved direction. I'm also checking whether the code/pre direction-lock rule interacts properly with inline code nested inside right-to-left paragraph text, and verifying whether a .chat-markdown-codeblock class actually exists in the codebase. I want to confirm how the ul renderer spreads its props alongside the dir="auto" attribute to make sure nothing gets overridden unintentionally. I'm also checking whether adding this new ul handler duplicates an existing key in the components object, and tracing how the table handler strips out dir from props before passing a computed direction to MarkdownTable, noting that the table itself now relies on inherited direction from its scroll container rather than its own explicit attribute. I'm also considering that computing direction from the entire table's text content could cause a visible direction flip mid-stream if the header starts neutral but later content is strongly RTL — a minor edge case. I'm also double-checking the character ranges used in the strong-direction detection logic to make sure they correctly capture RTL script boundaries without misclassifying neutral characters like Arabic number signs. For the blockquote and alert components, I'm confirming that dir="auto" correctly derives direction from the first strong character while excluding nested elements that declare their own explicit direction, which matches the HTML spec behavior. I'm trying to recall the exact spec algorithm for computing auto directionality—walking descendant nodes in tree order, skipping elements that have their own dir attribute set or are bdi/script/style/textarea, and checking text nodes for the first strong directional character. Confirmed — the ancestor check means the whole subtree under a dir-attributed element is skipped, so the label's text never factors into auto directionality. That resolves the earlier concern about the <p dir="ltr"> label content being excluded correctly. Since the flex container packs items at main-start regardless of text-align, the alert label would end up on the left while the RTL body and border sit on the right — a real visual regression from my earlier fix. I'm weighing whether wrapping the label text in a <bdi> element could isolate its direction without affecting the flex layout side, since bdi defaults to dir="auto" and would resolve "Note" as LTR on its own. The cleaner approach is to leave the outer <p> without a dir attribute so it inherits the alert's resolved direction (placing the icon+label on the correct side), while wrapping just the label text in <bdi> or an inner span with dir="ltr" to keep the text itself rendering LTR. Since the icon is an SVG with no text, it won't affect direction resolution either way, so this minimal fix should work. I want to verify this against the actual alert markup in the file, and also check the blockquote-alert container's logical properties before moving on to review lines 1580-1660. I'm checking that the codeblock direction rule stays forced to left-to-right regardless of language context. Now I'm tracing through the alert label issue: since the label is excluded from the bidi detection, the container's dir="auto" resolves to rtl based on the Hebrew body text, which shifts the border and padding to the right side while the label itself stays forced ltr and packs its content to the left. This creates a real visual mismatch where the title ends up misaligned from the body text and border, and I'm fairly confident this produces a title that appears at the left edge when it shouldn't. The fix is to move the dir="ltr" attribute off the paragraph itself and onto an inner span wrapping just the label text, so the paragraph inherits the container's resolved direction while still excluding the English label from bidi detection — this works because the skip rule applies to descendant elements with their own dir attribute, and the label text is always plain ASCII so isolating just that span is sufficient. Now I'm checking the second finding: the rtl mask in scroll-area.tsx, verifying whether Tailwind v4's rtl: variant and mask-l-from utilities compose correctly with tailwind-merge, since both the bare and rtl-prefixed mask utilities need to coexist without one overriding the other incorrectly in the cascade. Base UI likely spreads the dir prop onto the rendered Root div via its render element hook, so that should work fine. I'm also checking whether ScrollArea.Root treats dir specially (it doesn't seem to), and confirming where DirectionProvider and its direction type are actually exported from in this version of the package. Now I'm checking the CSS specificity of the new combined selector against existing rules like the text-align on table headers, to make sure the new rule doesn't accidentally override intended styling further down in the stylesheet. I should also check whether .chat-markdown applies beyond markdown rendering, like diff viewers or tooltips, since the new global text-align rule could unintentionally override centered content elsewhere. Let me look at the index.css region around line 1580-1620 and grep for other text-align usages within .chat-markdown. I'm checking how remark-gfm applies table alignment through inline styles on th/td elements, confirming that inline style should override my CSS class rules, then moving on to verify the code block direction setting. For task list checkboxes, I'm confirming the margin-inline fix was applied correctly so checkboxes position properly under RTL with plaintext bidi. Now I want to scan the list CSS block around lines 1655-1760 for any remaining physical properties like left positioning or text-indent that the PR might have missed, since those could cause regressions in RTL layouts. These seem fine, and the link underline's background-position is physical but harmless since it's repeat-x anyway. The main finding remains the alert label's dir="ltr" attribute, though I should also check the chat-markdown table container CSS, sticky thead behavior, and whether dir on the ScrollArea root affects the scrollbar—though since scrollbars are hidden, that's moot. I'm also looking at how MarkdownTable's type definition omits the native dir prop to replace it with a custom one. The table component destructures out the dir prop safely, so that's fine. But I'm now wondering whether DirectionProvider wrapping the ScrollArea also affects table cell contents like file links with tooltips or menus, which could inherit rtl direction and flip their alignment—likely a minor, acceptable side effect. I'm also reconsidering whether dir="ltr" on the alert paragraph actually forces the flex container's main axis to start from the left, pushing the icon and label to begin there. Yes, confirmed: in RTL the title row ends up on the opposite side from the body text, which is a real regression introduced by the fix and worth flagging with a suggested correction—removing dir="ltr" from the paragraph itself and instead wrapping just the label text in a span with dir="ltr", since the spec excludes descendants with their own dir attribute from the parent's directional styling. That way the paragraph inherits the container's resolved direction for layout purposes while the label text itself stays LTR. I'm drafting the exact inline suggestion now, matching indentation for the comment block and the paragraph across lines 1606-1614. Now I'm also checking the ScrollArea RTL mask change — since it touches a shared primitive rather than a single consumer, that ownership placement seems right, but I need to verify whether Base UI's overflow-x-start/end variables are logical or physical before trusting the added rtl override. I search the repo for usages of these scroll-area overflow variables, but they're not defined locally since Base UI is a dependency. Based on the naming convention using "start/end" rather than "left/right," and Base UI's documented style variables, it's likely these are logical properties that already account for RTL, which supports the author's assumption — so I won't flag this as an issue. This confirms the logical inline start/end vars align with RTL handling through useDirection and DirectionProvider, matching Base UI's design approach. I should double-check that TextDirection is actually exported from the direction-provider module in that repo. The import checks out. I'm also confirming that DirectionProvider only wraps ScrollArea, and that base-ui's ScrollArea.Root doesn't set dir itself via useDirection—so the PR's explicit dir assignment on the root is consistent. Now I'm checking how the dir prop type aligns with ScrollAreaPrimitive.Root's props. Next I want to check the chat-markdown-table-container CSS, particularly the sticky header and first-column shadow rules, since those likely use physical left/right positioning that could break under RTL now that the PR introduces RTL table support. I'm checking that the table's ellipsis truncation still works fine with plaintext bidi resolution, and thinking through how an English cell would align within a Hebrew RTL table when each cell resolves its own direction independently. That confirms the spec note exists, so the alignment works out correctly in practice. I'm now checking other cases — headings, blockquotes, markdown paragraph handlers — and they all seem fine since they rely on the same plaintext heuristic. I'm also considering whether the codeblock's forced ltr direction might bleed into its header area with the language label and buttons. Looking at task-list checkboxes inside li elements with plaintext direction, I trace through how negative inline margins interact with rtl resolution for Hebrew list items, and it seems to work correctly. There's a subtlety worth noting: a ul with mixed-language items resolves its overall direction from the first strong character across all items, which could leave markers on one side while individual items align differently — but that's an acceptable, already-documented tradeoff. I also want to verify that adding a ul override doesn't conflict with an existing entry in the components object. No duplicates found, so this alert-title finding stands as a real RTL layout inconsistency in the changed lines. I'll flag it with one inline comment and mark the check as a failure, since this is a concrete regression risk rather than a clean pass. Let me just verify that the dir="ltr" attribute actually does place the content on the left as I'm assuming. Confirming the flex direction logic, I trace through how dir="ltr" combined with the flex display and text-align properties pushes the icon and label to the left edge, while the rest of the alert body stays right-aligned under RTL — a clear visual mismatch between the heading row and its border. Writing the inline suggestion comment...

1. modify_pr — One finding on the RTL alert layout; the rest of the bidi work (DirectionProvider on the table viewport, logical properties in index.css, the rtl: mask swap against Base UI's logical --scroll-area-overflow-x-* vars) looks consistent.


Agent Credits: 128 credits