diff --git a/.changeset/logical-rtl-classes.md b/.changeset/logical-rtl-classes.md new file mode 100644 index 00000000..53a6a8e8 --- /dev/null +++ b/.changeset/logical-rtl-classes.md @@ -0,0 +1,5 @@ +--- +"streamdown": patch +--- + +fix: use logical Tailwind classes (`ps-*`, `border-s-*`, `text-start`, `start-*`, `end-*`, `ms-*`) instead of physical left/right classes so layout mirrors in RTL text. `cn` now treats logical and physical utilities on the same axis as conflicting, so a consumer `className="pl-8"` still overrides the built-in padding. The code block keeps physical classes because it is pinned `dir="ltr"`. diff --git a/packages/streamdown/__tests__/components.test.tsx b/packages/streamdown/__tests__/components.test.tsx index 03ba1b07..52f98946 100644 --- a/packages/streamdown/__tests__/components.test.tsx +++ b/packages/streamdown/__tests__/components.test.tsx @@ -236,10 +236,27 @@ describe("Markdown Components", () => { const blockquote = container.querySelector("blockquote"); expect(blockquote).toBeTruthy(); expect(blockquote?.className).toContain("my-4"); - expect(blockquote?.className).toContain("border-l-4"); - expect(blockquote?.className).toContain("pl-4"); + expect(blockquote?.className).toContain("border-s-4"); + expect(blockquote?.className).toContain("ps-4"); expect(blockquote?.className).toContain("italic"); }); + + it("lets a physical className override the logical blockquote padding", () => { + const Blockquote = components.blockquote; + if (!Blockquote) { + throw new Error("Blockquote component not found"); + } + const { container } = render( +
+ Quote text ++ ); + const classes = container.querySelector("blockquote")?.classList; + expect(classes?.contains("pl-8")).toBe(true); + expect(classes?.contains("ps-4")).toBe(false); + expect(classes?.contains("border-l-2")).toBe(true); + expect(classes?.contains("border-s-4")).toBe(false); + }); }); describe("Code Components", () => { @@ -525,7 +542,7 @@ describe("Markdown Components", () => { expect(th?.className).toContain("whitespace-nowrap"); expect(th?.className).toContain("px-4"); expect(th?.className).toContain("py-2"); - expect(th?.className).toContain("text-left"); + expect(th?.className).toContain("text-start"); expect(th?.className).toContain("font-semibold"); expect(th?.className).toContain("text-sm"); }); diff --git a/packages/streamdown/__tests__/pan-zoom-interaction.test.tsx b/packages/streamdown/__tests__/pan-zoom-interaction.test.tsx index 54239915..5828bc66 100644 --- a/packages/streamdown/__tests__/pan-zoom-interaction.test.tsx +++ b/packages/streamdown/__tests__/pan-zoom-interaction.test.tsx @@ -233,13 +233,13 @@ describe("PanZoom pointer interactions", () => { ); - // Fullscreen controls should use bottom-4 left-4 + // Fullscreen controls should use bottom-4 start-4 const controls = container.querySelector(".absolute.z-10"); expect(controls?.className).toContain("bottom-4"); - expect(controls?.className).toContain("left-4"); + expect(controls?.className).toContain("start-4"); }); - it("should position controls at bottom-2 left-2 when not fullscreen", () => { + it("should position controls at bottom-2 start-2 when not fullscreen", () => { const { container } = render(