القيمة / Value:
const direction = "ltr";<script lang="ts">
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
import * as Popover from "$lib/components/ui/popover/index.js";
import * as Tabs from "$lib/components/ui/tabs/index.js";
import { Button } from "$lib/components/ui/button/index.js";
import { Slider } from "$lib/components/ui/slider/index.js";
let direction = $state<"ltr" | "rtl">("rtl");
let value = $state(35);
</script>
<div class="grid w-full max-w-lg gap-5" data-direction-demo>
<label class="flex items-center gap-3 text-sm">
Direction
<select
aria-label="Direction"
bind:value={direction}
class="rounded-md border bg-background p-2"
>
<option value="rtl">RTL — العربية</option>
<option value="ltr">LTR — English</option>
</select>
</label>
<div dir={direction} class="grid gap-5 rounded-lg border p-5">
<Tabs.Root value="account">
<Tabs.List aria-label="Direction tabs">
<Tabs.Trigger value="account">الحساب / Account</Tabs.Trigger>
<Tabs.Trigger value="security">الأمان / Security</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="account" class="text-sm">اتجاه موروث — inherited direction</Tabs.Content>
<Tabs.Content value="security" class="text-sm"
>مفاتيح معكوسة — mirrored arrow keys</Tabs.Content
>
</Tabs.Root>
<div class="grid gap-3">
<p class="text-sm">القيمة / Value: <output>{value}</output></p>
<Slider type="single" dir={direction} bind:value aria-label="Direction value" max={100} />
</div>
<div class="flex flex-wrap gap-3">
<Popover.Root>
<Popover.Trigger>
{#snippet child({ props })}<Button {...props} variant="outline">弹层 / Popover</Button
>{/snippet}
</Popover.Trigger>
<Popover.Content dir={direction} align="start" class="w-64" aria-label="Direction popover">
<p class="text-sm">محاذاة البداية / logical start</p>
<p dir="ltr" class="mt-2 text-sm text-muted-foreground">
Portal keeps context, not DOM inheritance.
</p>
</Popover.Content>
</Popover.Root>
<DropdownMenu.Root dir={direction}>
<DropdownMenu.Trigger>
{#snippet child({ props })}<Button {...props} variant="outline">菜单 / Menu</Button
>{/snippet}
</DropdownMenu.Trigger>
<DropdownMenu.Content align="start">
<DropdownMenu.Item>الحساب / Account</DropdownMenu.Item>
<DropdownMenu.Sub>
<DropdownMenu.SubTrigger>المزيد / More</DropdownMenu.SubTrigger>
<DropdownMenu.SubContent side={direction === "rtl" ? "left" : "right"}>
<DropdownMenu.Item>الإعدادات / Settings</DropdownMenu.Item>
</DropdownMenu.SubContent>
</DropdownMenu.Sub>
</DropdownMenu.Content>
</DropdownMenu.Root>
</div>
<pre dir="ltr" class="overflow-x-auto rounded-md bg-muted p-3 text-start text-xs"><code
>const direction = "ltr";</code
></pre>
</div>
</div> 不需要第二套 Provider
Valar Svelte 使用原生 dir="rtl" / dir="ltr" 与逻辑样式。Bits UI 2.20.0 没有 DirectionProvider,BitsConfig 只配置 locale 与 Portal 目标,不会为所有组件统一设置方向。上面的示例可动态切换,默认 RTL,代码块局部固定 LTR。
交互方向必须分类处理
原生 dir 能继承,但 Portal 默认把 DOM 移到 body:Svelte context 保留,原父节点的 CSS 继承不保留。局部 RTL 的弹层必须显式指定方向,或通过既有 portalProps.to 移入正确方向的容器。不要把 align 的逻辑 start/end 与 side 的物理 left/right 混为一谈。
<div dir={direction}>
<Slider dir={direction} type="single" bind:value />
<Popover.Root>
<Popover.Trigger>Open</Popover.Trigger>
<Popover.Content dir={direction} align="start">Content</Popover.Content>
</Popover.Root>
</div> 使用 ms/me、ps/pe、text-start/end 和 inset-inline-*。不是所有图标都应镜像:前进/后退/子菜单箭头应镜像,播放、商标、数字等不自动镜像。本轮修复 DropdownMenu、ContextMenu、Menubar 子菜单箭头的 ms-auto rtl:rotate-180;原有 cn-rtl-flip 标记本身并不产生镜像样式。现有逻辑弹层样式也不代表全库每个组合都已 RTL 验收。
范围与验收
本轮验收代表组合为继承方向的 Tabs、显式方向的 Slider、body Portal 的 Popover、嵌套菜单及局部 LTR 代码块,涵盖动态切换、键盘、逻辑对齐与 SSR 首屏。没有创建重复 Provider,也没有宣称现有所有业务区块、图表和第三方控件均已双向化。
固定版本源码依据、能力缺口和可重复结果见 standards/porting-records/direction-bits-ui-to-svelte.md。真实读屏、真实触摸设备及不同浏览器仍需集成项目按支持范围验证。