Skip to content

Commit c596a78

Browse files
yzn-hclaude
andcommitted
fix(web): address review on right-to-left direction
- Raw HTML blocks such as `<li>a</li><li>b</li>` add whitespace when a message's words are counted, so neighbouring items no longer merge into one word. - File chips in messages and the composer stay left to right. A chip that replaces inline code dropped its isolate, so `./foo.ts` inside an Arabic line could render as `foo.ts./`. - Sidebar thread titles align to their start. The row's `text-left` kept short Arabic titles on the left. Co-Authored-By: Claude Opus 5.5 <[email protected]>
1 parent 2967192 commit c596a78

5 files changed

Lines changed: 70 additions & 2 deletions

File tree

‎apps/web/src/components/ComposerPromptEditorTiptap.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,7 @@ function ComposerMentionNodeView({ node }: NodeViewProps) {
265265
const chip = (
266266
<ContextChip
267267
kind="mention"
268+
dir="ltr"
268269
render={<button type="button" />}
269270
onClick={() => actions.openMention(path)}
270271
aria-label={`Preview ${path}`}

‎apps/web/src/components/chat/MarkdownFileLink.tsx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,7 @@ export const MarkdownFileLink = memo(function MarkdownFileLink({
328328
hasPrimaryAction ? (
329329
<ContextChip
330330
kind="mention"
331+
dir="ltr"
331332
render={<a href={href} />}
332333
className={MARKDOWN_FILE_LINK_CLASS_NAME}
333334
data-markdown-copy={copyMarkdown}
@@ -356,6 +357,7 @@ export const MarkdownFileLink = memo(function MarkdownFileLink({
356357
) : (
357358
<ContextChip
358359
kind="mention"
360+
dir="ltr"
359361
render={<button type="button" />}
360362
aria-label={`File options for ${label}`}
361363
aria-haspopup="menu"
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { describe, expect, it } from "vite-plus/test";
2+
3+
import { rehypeTextDirection } from "./markdownTextDirection";
4+
5+
const text = (value: string) => ({ type: "text", value });
6+
const element = (tagName: string, children: object[]) => ({
7+
type: "element",
8+
tagName,
9+
properties: {} as Record<string, unknown>,
10+
children,
11+
});
12+
13+
function directed(...blocks: ReturnType<typeof element>[]) {
14+
rehypeTextDirection()({ type: "root", children: blocks });
15+
return blocks;
16+
}
17+
18+
describe("rehypeTextDirection", () => {
19+
it("keeps raw HTML list items apart when counting words", () => {
20+
const [list] = directed(
21+
element("ul", [element("li", [text("مرحبا")]), element("li", [text("hello world")])]),
22+
);
23+
expect(list!.properties.dir).toBe("ltr");
24+
});
25+
26+
it("leaves inline code out of the count and isolates it", () => {
27+
const code = element("code", [text("useEffect useLayoutEffect React")]);
28+
const [paragraph] = directed(element("p", [text("استخدم "), code, text(" هنا")]));
29+
expect(paragraph!.properties.dir).toBe("rtl");
30+
expect(code.properties.dir).toBe("ltr");
31+
});
32+
33+
it("leaves code blocks to their own left-to-right frame", () => {
34+
const code = element("code", [text("// احسب المجموع هنا")]);
35+
const [pre] = directed(element("pre", [code]));
36+
expect(pre!.properties.dir).toBeUndefined();
37+
expect(code.properties.dir).toBeUndefined();
38+
});
39+
});

‎apps/web/src/components/chat/markdownTextDirection.ts‎

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,36 @@ type HastNode = {
88
children?: HastNode[];
99
};
1010

11+
// Raw HTML such as `<li>a</li><li>b</li>` has no whitespace between its
12+
// blocks, so they add their own to keep the words apart.
13+
const BLOCK_TAGS = new Set([
14+
"blockquote",
15+
"br",
16+
"details",
17+
"div",
18+
"h1",
19+
"h2",
20+
"h3",
21+
"h4",
22+
"h5",
23+
"h6",
24+
"hr",
25+
"li",
26+
"ol",
27+
"p",
28+
"summary",
29+
"table",
30+
"td",
31+
"th",
32+
"tr",
33+
"ul",
34+
]);
35+
1136
function proseText(node: HastNode): string {
1237
if (node.type === "text") return node.value ?? "";
1338
if (node.tagName === "code") return " ";
14-
return (node.children ?? []).map(proseText).join("");
39+
const text = (node.children ?? []).map(proseText).join("");
40+
return node.tagName && BLOCK_TAGS.has(node.tagName) ? ` ${text} ` : text;
1541
}
1642

1743
function isolateInlineCode(node: HastNode): void {

‎apps/web/src/components/sidebar/SidebarThreadRow.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -955,7 +955,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThreadRowP
955955
<span
956956
aria-hidden
957957
className={cn(
958-
"min-w-0 flex-1 text-sm transition-opacity motion-reduce:transition-none",
958+
"min-w-0 flex-1 text-start text-sm transition-opacity motion-reduce:transition-none",
959959
shouldRecede ? "font-normal" : "font-medium",
960960
variant === "card"
961961
? cn(

0 commit comments

Comments
 (0)