Răsfoiți Sursa

fix(app): correct RTL layout interactions (#40410)

Luke Parker 5 zile în urmă
părinte
comite
de1bd36a47

+ 9 - 8
packages/app/src/components/file-tree-v2.tsx

@@ -29,14 +29,14 @@ export type { Kind } from "@/components/file-tree"
 
 const INDENT_STEP = 16
 
-function rowPaddingLeft(level: number, type: FileNode["type"]) {
+function rowPaddingStart(level: number, type: FileNode["type"]) {
   if (type === "directory") return 8 + level * INDENT_STEP
   if (level === 0) return 8
   return 8 + level * INDENT_STEP - INDENT_STEP
 }
 
-function guideLineLeft(level: number) {
-  return rowPaddingLeft(level, "directory") + 8
+function guideLineStart(level: number) {
+  return rowPaddingStart(level, "directory") + 8
 }
 
 export const kindLabel = (kind: Kind) => {
@@ -87,7 +87,7 @@ const FileTreeNodeV2 = (
         ...local.classList,
         [local.class ?? ""]: !!local.class,
       }}
-      style={`padding-left: ${rowPaddingLeft(local.level, local.node.type)}px`}
+      style={`padding-inline-start: ${rowPaddingStart(local.level, local.node.type)}px`}
       draggable={local.draggable}
       onDragStart={(event: DragEvent) => {
         if (!local.draggable) return
@@ -99,7 +99,9 @@ const FileTreeNodeV2 = (
       {...rest}
     >
       {local.children}
-      <span class="flex-1 min-w-0 text-12-medium whitespace-nowrap truncate">{local.node.name}</span>
+      <span class="flex-1 min-w-0 text-start text-12-medium whitespace-nowrap truncate">
+        <bdi dir="auto">{local.node.name}</bdi>
+      </span>
       {(() => {
         const value = kind()
         if (!value || local.node.type !== "file") return null
@@ -116,7 +118,7 @@ const FileTreeNodeV2 = (
 function GuideLines(props: { level: number }) {
   return (
     <For each={Array.from({ length: props.level })}>
-      {(_, index) => <div data-slot="file-tree-v2-guide" style={`left: ${guideLineLeft(index())}px`} />}
+      {(_, index) => <div data-slot="file-tree-v2-guide" style={`inset-inline-start: ${guideLineStart(index())}px`} />}
     </For>
   )
 }
@@ -215,7 +217,6 @@ export default function FileTreeV2(props: {
     <div
       ref={setRoot}
       data-component="file-tree-v2"
-      dir="ltr"
       data-total-rows={live() ? rows().length : model()!.total}
       class="group/file-tree-v2"
       style={{ position: "relative", height: `${virtualizer.getTotalSize()}px` }}
@@ -228,7 +229,7 @@ export default function FileTreeV2(props: {
                 style={{
                   position: "absolute",
                   top: "0",
-                  left: "0",
+                  "inset-inline-start": "0",
                   width: "100%",
                   height: `${item().size}px`,
                   transform: `translateY(${item().start}px)`,

+ 2 - 2
packages/app/src/components/file-tree.tsx

@@ -146,13 +146,13 @@ const FileTreeNode = (
     <Dynamic
       component={local.as ?? "div"}
       classList={{
-        "w-full min-w-0 h-6 flex items-center justify-start gap-x-1.5 rounded-md px-1.5 py-0 text-left hover:bg-surface-raised-base-hover active:bg-surface-base-active transition-colors cursor-pointer": true,
+        "w-full min-w-0 h-6 flex items-center justify-start gap-x-1.5 rounded-md px-1.5 py-0 text-start hover:bg-surface-raised-base-hover active:bg-surface-base-active transition-colors cursor-pointer": true,
         "bg-surface-base-active": local.node.path === local.active,
         ...local.classList,
         [local.class ?? ""]: !!local.class,
         [local.nodeClass ?? ""]: !!local.nodeClass,
       }}
-      style={`padding-left: ${Math.max(0, 8 + local.level * 12 - (local.node.type === "file" ? 24 : 4))}px`}
+      style={`padding-inline-start: ${Math.max(0, 8 + local.level * 12 - (local.node.type === "file" ? 24 : 4))}px`}
       draggable={local.draggable}
       onDragStart={(event: DragEvent) => {
         if (!local.draggable) return

+ 4 - 4
packages/app/src/components/settings-v2/settings-v2.css

@@ -287,7 +287,7 @@
 }
 
 .settings-v2-provider-env-hint {
-  padding-right: 12px;
+  padding-inline-end: 12px;
   font-size: 13px;
   font-weight: 440;
   line-height: 1;
@@ -311,7 +311,7 @@
   line-height: 1;
   color: var(--v2-text-text-accent);
   cursor: pointer;
-  text-align: left;
+  text-align: start;
 }
 
 .settings-v2-providers-view-all:hover {
@@ -361,13 +361,13 @@
 }
 
 .settings-v2-tab-search [data-slot="text-input-v2-input"] {
-  padding-right: 28px;
+  padding-inline-end: 28px;
 }
 
 .settings-v2-tab-search-clear {
   position: absolute;
   top: 50%;
-  right: 6px;
+  inset-inline-end: 6px;
   z-index: 1;
   transform: translateY(-50%);
 }

+ 20 - 3
packages/app/src/components/titlebar-tab-nav.css

@@ -1,7 +1,7 @@
 [data-titlebar-tab] [data-slot="tab-close"] {
   position: absolute;
   top: 4px;
-  right: 4px;
+  inset-inline-end: 4px;
   display: flex;
   height: 20px;
   width: 20px;
@@ -52,7 +52,7 @@
   content: "";
   position: absolute;
   top: 8px;
-  left: -3.75px;
+  inset-inline-start: -3.75px;
   width: 1.5px;
   height: 12px;
   border-radius: 9999px;
@@ -86,6 +86,23 @@
   );
 }
 
+[data-titlebar-tab][data-title-overflow="true"]:not([data-editing="true"]):dir(rtl) [data-slot="tab-link"] {
+  -webkit-mask-image: linear-gradient(
+    to left,
+    black 0,
+    black calc(100% - var(--tab-title-fade-offset) - 16px),
+    transparent calc(100% - var(--tab-title-fade-offset)),
+    transparent 100%
+  );
+  mask-image: linear-gradient(
+    to left,
+    black 0,
+    black calc(100% - var(--tab-title-fade-offset) - 16px),
+    transparent calc(100% - var(--tab-title-fade-offset)),
+    transparent 100%
+  );
+}
+
 [data-titlebar-tab][data-title-overflow="true"]:is(:hover, [data-active="true"]):not([data-editing="true"])
   [data-slot="tab-link"] {
   --tab-title-fade-offset: 24px;
@@ -93,7 +110,7 @@
 
 [data-titlebar-tab][data-title-overflow="true"]:not(:hover):not([data-active="true"]):not([data-editing="true"])
   [data-slot="tab-link"] {
-  padding-right: 0;
+  padding-inline-end: 0;
 }
 
 [data-titlebar-tab] [data-slot="tab-title"] {

+ 18 - 3
packages/app/src/components/titlebar.tsx

@@ -1,4 +1,15 @@
-import { createEffect, createMemo, createResource, createSignal, Match, onMount, Show, Switch, untrack } from "solid-js"
+import {
+  createEffect,
+  createMemo,
+  createResource,
+  createSignal,
+  Match,
+  on,
+  onMount,
+  Show,
+  Switch,
+  untrack,
+} from "solid-js"
 import { createStore } from "solid-js/store"
 import { useLocation, useNavigate, useParams } from "@solidjs/router"
 import { IconButton } from "@opencode-ai/ui/icon-button"
@@ -42,8 +53,11 @@ export type TitlebarUpdate = {
 }
 
 export function useTitlebarRightMount() {
+  const language = useLanguage()
   const [mount, setMount] = createSignal<HTMLElement | null>(null)
-  onMount(() => setMount(document.getElementById("opencode-titlebar-right")))
+  const sync = () => setMount(document.getElementById("opencode-titlebar-right"))
+  onMount(sync)
+  createEffect(on(language.direction, sync, { defer: true }))
   return mount
 }
 
@@ -169,7 +183,8 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
         "padding-left": macTrafficLights() ? `${macTrafficLightsBaseWidth / zoom()}px` : 0,
         width: windows() ? `env(titlebar-area-width, calc(100vw - ${windowsControlsWidth()}))` : undefined,
         "max-width": windows() ? `env(titlebar-area-width, calc(100vw - ${windowsControlsWidth()}))` : undefined,
-        "align-self": windows() ? "flex-start" : undefined,
+        // Native Windows caption controls remain on the physical right in both writing directions.
+        "margin-right": windows() ? "auto" : undefined,
       }}
       data-tauri-drag-region
     >

+ 16 - 10
packages/session-ui/src/v2/components/session-review-v2.css

@@ -30,19 +30,19 @@
   flex-shrink: 0;
   min-height: 0;
   overflow: hidden;
-  border-right: 1px solid var(--border-weaker-base, var(--v2-border-border-weak));
+  border-inline-end: 1px solid var(--border-weaker-base, var(--v2-border-border-weak));
   background: var(--v2-background-bg-base);
 }
 
 [data-component="session-review-v2-sidebar-root"] [data-slot="session-review-v2-sidebar"][aria-hidden="true"] {
-  border-right-width: 0;
+  border-inline-end-width: 0;
 }
 
 [data-component="session-review-v2-sidebar-root"]
   [data-slot="session-review-v2-sidebar"][data-transition]:not([data-resizing]) {
   transition:
     width 200ms cubic-bezier(0.22, 1, 0.36, 1),
-    border-right-width 200ms cubic-bezier(0.22, 1, 0.36, 1);
+    border-inline-end-width 200ms cubic-bezier(0.22, 1, 0.36, 1);
 }
 
 [data-component="session-review-v2-sidebar-root"] [data-slot="session-review-v2-sidebar-resize"] {
@@ -63,7 +63,8 @@
   align-items: center;
   justify-content: space-between;
   gap: 8px;
-  padding: 12px 16px 12px 8px;
+  padding-block: 12px;
+  padding-inline: 8px 16px;
   flex-shrink: 0;
 }
 
@@ -79,7 +80,7 @@
 
 [data-component="session-review-v2-sidebar-root"]
   [data-slot="session-review-v2-sidebar-title"]:not(:has([data-component="select-v2-root"])) {
-  margin-left: 8px;
+  margin-inline-start: 8px;
 }
 
 [data-component="session-review-v2-sidebar-root"]
@@ -211,7 +212,7 @@
   justify-content: space-between;
   gap: 8px;
   padding: 10px 12px;
-  padding-left: 8px;
+  padding-inline-start: 8px;
   flex-shrink: 0;
   border-bottom: 1px solid var(--border-weaker-base, var(--v2-border-border-weak));
 }
@@ -231,7 +232,7 @@
   align-items: center;
   gap: 8px;
   min-width: 0;
-  padding-right: 8px;
+  padding-inline-end: 8px;
 }
 
 [data-component="session-review-v2"] [data-slot="session-review-v2-toolbar-title"] {
@@ -331,7 +332,7 @@
 [data-component="session-review-v2"] [data-slot="session-review-v2-file-diff"] {
   position: relative;
   flex-shrink: 0;
-  margin-left: auto;
+  margin-inline-start: auto;
   background-color: var(--v2-background-bg-base);
 
   &::before {
@@ -339,17 +340,22 @@
     position: absolute;
     top: -8px;
     bottom: -8px;
-    right: 100%;
+    inset-inline-end: 100%;
     width: 16px;
     z-index: 1;
     pointer-events: none;
     background: linear-gradient(90deg, transparent, var(--v2-background-bg-base));
   }
 
+  &:dir(rtl)::before {
+    background: linear-gradient(270deg, transparent, var(--v2-background-bg-base));
+  }
+
   &::after {
     content: "";
     position: absolute;
-    inset: -8px -16px -8px 0;
+    inset-block: -8px;
+    inset-inline: 0 -16px;
     z-index: 0;
     pointer-events: none;
     background-color: var(--v2-background-bg-base);

+ 8 - 4
packages/session-ui/src/v2/components/session-review-v2.tsx

@@ -9,6 +9,7 @@ import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
 import type { SessionReviewDiffStyle } from "../../components/session-review"
 import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
 import { ScrollView } from "@opencode-ai/ui/scroll-view"
+import { useLocale } from "@kobalte/core/i18n"
 import { makeEventListener } from "@solid-primitives/event-listener"
 import { Show, createEffect, createMemo, createSignal, type JSX } from "solid-js"
 import { getWorkerPool } from "../../pierre/worker"
@@ -149,6 +150,7 @@ export function SessionReviewV2Sidebar(props: SessionReviewV2SidebarProps) {
 
 export function SessionReviewV2(props: SessionReviewV2Props) {
   const i18n = useI18n()
+  const locale = useLocale()
 
   createEffect(() => {
     getWorkerPool(props.diffStyle)
@@ -175,6 +177,8 @@ export function SessionReviewV2(props: SessionReviewV2Props) {
   }
 
   const canCycle = () => props.files.length > 0
+  const previousKey = () => (locale.direction() === "rtl" ? "ArrowRight" : "ArrowLeft")
+  const nextKey = () => (locale.direction() === "rtl" ? "ArrowLeft" : "ArrowRight")
   const showCollapsedMeta = () => props.sidebarOpen === false
   // Memoize slot getters so Show conditions do not instantiate throwaway elements.
   const title = createMemo(() => props.title)
@@ -189,11 +193,11 @@ export function SessionReviewV2(props: SessionReviewV2Props) {
   // pane is mounted, but never while typing in an input or comment editor.
   makeEventListener(document, "keydown", (event) => {
     if (event.defaultPrevented || event.ctrlKey || event.metaKey || event.altKey) return
-    if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return
+    if (event.key !== previousKey() && event.key !== nextKey()) return
     const target = event.target
     if (target instanceof HTMLElement && (target.isContentEditable || target.closest("input, textarea, select"))) return
     if (!props.hasDiffs || !canCycle()) return
-    const file = event.key === "ArrowLeft" ? prev() : next()
+    const file = event.key === previousKey() ? prev() : next()
     if (!file) return
     event.preventDefault()
     cycle(file)
@@ -221,7 +225,7 @@ export function SessionReviewV2(props: SessionReviewV2Props) {
           value={
             <>
               {i18n.t("ui.sessionReviewV2.previousFile")}
-              <KeybindV2 keys={["←"]} variant="neutral" />
+              <KeybindV2 keys={[locale.direction() === "rtl" ? "→" : "←"]} variant="neutral" />
             </>
           }
         >
@@ -241,7 +245,7 @@ export function SessionReviewV2(props: SessionReviewV2Props) {
           value={
             <>
               {i18n.t("ui.sessionReviewV2.nextFile")}
-              <KeybindV2 keys={["→"]} variant="neutral" />
+              <KeybindV2 keys={[locale.direction() === "rtl" ? "←" : "→"]} variant="neutral" />
             </>
           }
         >

+ 2 - 2
packages/ui/src/components/dropdown-menu.css

@@ -60,7 +60,7 @@
 
   [data-slot="dropdown-menu-checkbox-item"],
   [data-slot="dropdown-menu-radio-item"] {
-    padding-right: 28px;
+    padding-inline-end: 28px;
   }
 
   [data-slot="dropdown-menu-sub-trigger"] {
@@ -76,7 +76,7 @@
     width: 16px;
     height: 16px;
     position: absolute;
-    right: 8px;
+    inset-inline-end: 8px;
     top: 50%;
     transform: translateY(-50%);
   }

+ 8 - 1
packages/ui/src/components/icon.tsx

@@ -154,7 +154,14 @@ export function Icon(props: IconProps) {
     <div
       data-component="icon"
       data-size={local.size || "normal"}
-      data-directional={local.name === "chevron-left" || local.name === "chevron-right" ? true : undefined}
+      data-directional={
+        local.name === "arrow-left" ||
+        local.name === "arrow-right" ||
+        local.name === "chevron-left" ||
+        local.name === "chevron-right"
+          ? true
+          : undefined
+      }
     >
       <svg
         data-slot="icon-svg"

+ 14 - 2
packages/ui/src/components/resize-handle.css

@@ -14,7 +14,7 @@
     opacity: 1;
   }
 
-  &[data-direction="horizontal"] {
+  &:where([data-direction="horizontal"]) {
     inset-block: 0;
     inset-inline-end: 0;
     width: 8px;
@@ -33,9 +33,21 @@
       inset-inline-start: 50%;
       transform: translateX(-50%);
     }
+
+    &:dir(rtl) {
+      transform: translateX(-50%);
+
+      &[data-edge="start"] {
+        transform: translateX(50%);
+      }
+
+      &::after {
+        transform: translateX(50%);
+      }
+    }
   }
 
-  &[data-direction="vertical"] {
+  &:where([data-direction="vertical"]) {
     inset-inline: 0;
     inset-block-start: 0;
     height: 8px;

+ 5 - 1
packages/ui/src/components/resize-handle.tsx

@@ -33,6 +33,10 @@ export function ResizeHandle(props: ResizeHandleProps) {
     e.preventDefault()
     const edge = local.edge ?? (local.direction === "vertical" ? "start" : "end")
     const start = local.direction === "horizontal" ? e.clientX : e.clientY
+    const rtl =
+      local.direction === "horizontal" &&
+      e.currentTarget instanceof Element &&
+      getComputedStyle(e.currentTarget).direction === "rtl"
     const startSize = local.size
     const min = local.min
     const max = local.max
@@ -53,7 +57,7 @@ export function ResizeHandle(props: ResizeHandleProps) {
           ? edge === "end"
             ? pos - start
             : start - pos
-          : edge === "start"
+          : (edge === "start") !== rtl
             ? start - pos
             : pos - start
       current = startSize + delta

+ 1 - 1
packages/ui/src/components/scroll-view.css

@@ -21,7 +21,7 @@
 
 .scroll-view__thumb {
   position: absolute;
-  right: 0;
+  inset-inline-end: 0;
   top: 0;
   width: 12px;
   transition: opacity 200ms ease;

+ 5 - 3
packages/ui/src/components/select.css

@@ -1,6 +1,7 @@
 [data-component="select"] {
   [data-slot="select-select-trigger"] {
-    padding: 0 4px 0 8px;
+    padding-block: 0;
+    padding-inline: 8px 4px;
     box-shadow: none;
 
     [data-slot="select-select-trigger-value"] {
@@ -46,7 +47,8 @@
 
   &[data-trigger-style="settings"] {
     [data-slot="select-select-trigger"] {
-      padding: 6px 6px 6px 12px;
+      padding-block: 6px;
+      padding-inline: 12px 6px;
       box-shadow: none;
       border-radius: 6px;
       min-width: 160px;
@@ -154,7 +156,7 @@
       display: flex;
       align-items: center;
       justify-content: center;
-      margin-left: auto;
+      margin-inline-start: auto;
       width: 16px;
       height: 16px;
     }

+ 4 - 0
packages/ui/src/components/switch.css

@@ -103,6 +103,10 @@
     background-color: var(--icon-invert-base);
   }
 
+  &:dir(rtl)[data-checked] [data-slot="switch-thumb"] {
+    transform: translateX(-12px);
+  }
+
   &[data-checked]:hover:not([data-disabled], [data-readonly]) [data-slot="switch-control"] {
     border-color: var(--border-hover);
     background-color: var(--surface-hover);

+ 54 - 26
packages/ui/src/components/tabs.css

@@ -61,7 +61,7 @@
     flex-shrink: 0;
     max-width: 280px;
     border-bottom: 1px solid var(--border-weak-base);
-    border-right: 1px solid var(--border-weak-base);
+    border-inline-end: 1px solid var(--border-weak-base);
     background-color: var(--background-base);
 
     [data-slot="tabs-trigger"] {
@@ -111,10 +111,10 @@
       color: var(--text-strong);
     }
     &:has([data-slot="tabs-trigger-close-button"]) {
-      padding-right: 12px;
+      padding-inline-end: 12px;
 
       [data-slot="tabs-trigger"] {
-        padding-right: 0;
+        padding-inline-end: 0;
       }
     }
   }
@@ -141,8 +141,8 @@
 
     [data-slot="tabs-list"] {
       height: var(--tabs-bar-height);
-      padding-left: 12px;
-      padding-right: 0;
+      padding-inline-start: 12px;
+      padding-inline-end: 0;
       --tabs-review-gap: 16px;
       --tabs-review-fade: 16px;
       gap: var(--tabs-review-gap);
@@ -162,11 +162,15 @@
           position: absolute;
           top: 0;
           bottom: 0;
-          left: calc(var(--tabs-review-fade) * -1);
+          inset-inline-end: 100%;
           width: var(--tabs-review-fade);
           pointer-events: none;
           background: linear-gradient(90deg, transparent, var(--background-stronger));
         }
+
+        &:dir(rtl)::before {
+          background: linear-gradient(270deg, transparent, var(--background-stronger));
+        }
       }
     }
 
@@ -213,9 +217,9 @@
       }
 
       &:has([data-slot="tabs-trigger-close-button"]) {
-        padding-right: 5px;
+        padding-inline-end: 5px;
         [data-slot="tabs-trigger"] {
-          padding-right: 0 !important;
+          padding-inline-end: 0 !important;
         }
       }
 
@@ -271,7 +275,7 @@
   #terminal-panel &[data-variant="normal"][data-orientation="horizontal"] {
     [data-slot="tabs-list"] {
       height: 52px;
-      padding-right: 12px;
+      padding-inline-end: 12px;
       gap: 8px;
     }
 
@@ -291,7 +295,7 @@
       }
 
       &:has([data-slot="tabs-trigger-close-button"]) {
-        padding-right: 8px;
+        padding-inline-end: 8px;
       }
 
       &:has([data-selected]) {
@@ -378,9 +382,9 @@
       }
 
       &:has([data-slot="tabs-trigger-close-button"]) {
-        padding-right: 0;
+        padding-inline-end: 0;
         [data-slot="tabs-trigger"] {
-          padding-right: 0;
+          padding-inline-end: 0;
         }
       }
     }
@@ -490,7 +494,7 @@
       padding: 8px;
       gap: 4px;
       background-color: var(--background-base);
-      border-right: 1px solid var(--border-weak-base);
+      border-inline-end: 1px solid var(--border-weak-base);
 
       &::after {
         display: none;
@@ -572,7 +576,7 @@
         padding: 12px;
         gap: 0;
         background-color: var(--background-base);
-        border-right: 1px solid var(--border-weak-base);
+        border-inline-end: 1px solid var(--border-weak-base);
 
         &::after {
           display: none;
@@ -581,7 +585,8 @@
 
       [data-slot="tabs-section-title"] {
         width: 100%;
-        padding: 0 0 0 4px;
+        padding: 0;
+        padding-inline-start: 4px;
         font-family: var(--font-family-sans);
         font-size: var(--font-size-small);
         font-weight: var(--font-weight-medium);
@@ -695,6 +700,10 @@ body[data-new-layout] #terminal-panel [data-component="tabs"][data-variant="norm
       &::before {
         background: linear-gradient(90deg, transparent, var(--v2-background-bg-base));
       }
+
+      &:dir(rtl)::before {
+        background: linear-gradient(270deg, transparent, var(--v2-background-bg-base));
+      }
     }
   }
 }
@@ -702,7 +711,7 @@ body[data-new-layout] #terminal-panel [data-component="tabs"][data-variant="norm
 body[data-new-layout] #review-panel [data-component="tabs"][data-variant="normal"][data-orientation="horizontal"] {
   [data-slot="tabs-list"] {
     height: 52px;
-    padding-right: 12px;
+    padding-inline-end: 12px;
     gap: 8px;
     --tabs-review-fade: 8px;
   }
@@ -723,10 +732,10 @@ body[data-new-layout] #review-panel [data-component="tabs"][data-variant="normal
     }
 
     &:has([data-slot="tabs-trigger-close-button"]) {
-      padding-right: 8px;
+      padding-inline-end: 8px;
 
       [data-slot="tabs-trigger-close-button"] {
-        margin-left: 2px;
+        margin-inline-start: 2px;
       }
     }
 
@@ -754,7 +763,7 @@ body[data-new-layout] #review-panel [data-component="tabs"][data-variant="normal
     .tab-fileicon-color,
     .tab-fileicon-mono,
     [data-slot="icon-svg"] {
-      margin-left: -2px;
+      margin-inline-start: -2px;
     }
   }
 
@@ -792,7 +801,7 @@ body[data-new-layout]
   .session-review-v2-tabs-bar
   [data-slot="tabs-list"]
   > .sticky {
-  padding-right: 0;
+  padding-inline-end: 0;
 
   [data-component="icon-button-v2"] {
     position: relative;
@@ -800,7 +809,7 @@ body[data-new-layout]
     &::after {
       content: "";
       position: absolute;
-      left: 100%;
+      inset-inline-start: 100%;
       top: 50%;
       transform: translateY(-50%);
       width: 20px;
@@ -811,21 +820,35 @@ body[data-new-layout]
   }
 }
 
+body[data-new-layout]
+  #review-panel
+  [data-component="tabs"]
+  .session-review-v2-tabs-bar
+  [data-slot="tabs-list"]
+  > .sticky:not(.session-review-v2-sidebar-toggle-slot) {
+  left: auto;
+  right: auto;
+  inset-inline-end: 0;
+}
+
 body[data-new-layout]
   #review-panel
   [data-component="tabs"]
   .session-review-v2-tabs-bar
   [data-slot="tabs-list"]
   > .session-review-v2-sidebar-toggle-slot.sticky {
-  padding-right: 0;
+  left: auto;
+  right: auto;
+  inset-inline-start: 0;
+  padding-inline-end: 0;
 
   &::before {
     content: "";
     position: absolute;
     top: 50%;
     bottom: auto;
-    left: auto;
-    right: 100%;
+    inset-inline-start: auto;
+    inset-inline-end: 100%;
     transform: translateY(-50%);
     width: 12px;
     height: 28px;
@@ -838,7 +861,8 @@ body[data-new-layout]
     content: "";
     position: absolute;
     top: 50%;
-    left: 100%;
+    inset-inline-start: 100%;
+    inset-inline-end: auto;
     transform: translateY(-50%);
     width: 8px;
     height: 28px;
@@ -846,6 +870,10 @@ body[data-new-layout]
     background: linear-gradient(90deg, var(--v2-background-bg-base), transparent);
   }
 
+  &:dir(rtl)::after {
+    background: linear-gradient(270deg, var(--v2-background-bg-base), transparent);
+  }
+
   [data-component="icon-button-v2"]::after {
     display: none;
   }
@@ -853,7 +881,7 @@ body[data-new-layout]
 
 body[data-new-layout] #review-panel [data-component="tabs"] .session-review-v2-open-in-app-slot {
   flex-shrink: 0;
-  margin-left: auto;
+  margin-inline-start: auto;
 }
 
 body[data-new-layout] #review-panel [data-component="tabs"] .session-review-v2-open-in-app {

+ 1 - 0
packages/ui/src/components/tabs.tsx

@@ -80,6 +80,7 @@ function TabsTrigger(props: ParentProps<TabsTriggerProps>) {
     >
       <Kobalte.Trigger
         {...rest}
+        dir={rest.dir ?? "auto"}
         data-slot="tabs-trigger"
         data-value={props.value}
         classList={{ [split.classes?.button ?? ""]: split.classes?.button }}

+ 2 - 2
packages/ui/src/components/text-field.css

@@ -45,7 +45,7 @@
       align-items: start;
       justify-content: space-between;
       width: 100%;
-      padding-right: 4px;
+      padding-inline-end: 4px;
 
       border-radius: var(--radius-md);
       border: 1px solid var(--border-weak-base);
@@ -68,7 +68,7 @@
       }
 
       &:not(:has([data-slot="input-copy-button"])) {
-        padding-right: 0;
+        padding-inline-end: 0;
       }
     }
 

+ 6 - 2
packages/ui/src/v2/components/file-tree-v2.css

@@ -13,13 +13,13 @@
   align-items: center;
   justify-content: flex-start;
   gap: 6px;
-  padding-right: 8px;
+  padding-inline-end: 8px;
   overflow: visible;
   border: none;
   border-radius: 6px;
   background-color: transparent;
   color: var(--v2-text-text-muted);
-  text-align: left;
+  text-align: start;
   cursor: pointer;
   scroll-margin-block: 8px;
   transition:
@@ -87,6 +87,10 @@
   transform: rotate(-90deg);
 }
 
+[data-component="file-tree-v2"]:dir(rtl) [data-slot="file-tree-v2-chevron"]:not([data-expanded]) svg {
+  transform: rotate(90deg);
+}
+
 [data-component="file-tree-v2"] [data-slot="file-tree-v2-row"][data-selected] [data-slot="file-tree-v2-chevron"] {
   color: var(--v2-text-text-base);
 }

+ 16 - 5
packages/ui/src/v2/components/select-v2.css

@@ -52,7 +52,8 @@
   display: flex;
   flex-direction: row;
   align-items: stretch;
-  padding: 0 6px 0 0;
+  padding-block: 0;
+  padding-inline: 0 6px;
   gap: 8px;
   width: 280px;
   height: 28px;
@@ -74,7 +75,8 @@
 
 [data-component="select-v2"][data-appearance="large"] {
   height: 32px;
-  padding: 0 8px 0 0;
+  padding-block: 0;
+  padding-inline: 0 8px;
 }
 
 [data-component="select-v2"]:where(:hover):not([data-disabled], [data-invalid]):not(:focus-within):not(
@@ -126,12 +128,13 @@
   overflow: hidden;
   text-overflow: ellipsis;
   white-space: nowrap;
-  padding: 0 0 0 8px;
+  padding-block: 0;
+  padding-inline: 8px 0;
   margin: 0;
   border: 0;
   background: transparent;
   outline: none;
-  text-align: left;
+  text-align: start;
   font-style: normal;
   font-weight: 440;
   font-size: 13px;
@@ -188,7 +191,8 @@
   width: fit-content;
   max-width: 100%;
   height: 24px;
-  padding: 4px 4px 4px 8px;
+  padding-block: 4px;
+  padding-inline: 8px 4px;
   gap: 4px;
   border-radius: 4px;
   background: transparent;
@@ -278,6 +282,13 @@
   color: var(--menu-v2-accent);
 }
 
+[data-slot="select-v2-listbox"]
+  [data-component="menu-v2-item"][data-selected]
+  [data-slot="menu-v2-item-indicator"]
+  svg {
+  visibility: visible;
+}
+
 [data-slot="select-v2-listbox"]
   [data-component="menu-v2-item"]:not([data-selected])
   [data-slot="menu-v2-item-indicator"]

+ 8 - 0
packages/ui/src/v2/components/switch-v2.css

@@ -121,6 +121,10 @@
       var(--v2-grey-300);
   }
 
+  &:dir(rtl)[data-checked] [data-slot="switch-thumb"] {
+    transform: translateX(-8px);
+  }
+
   &[data-checked]:hover:not([data-disabled], [data-readonly]) [data-slot="switch-control"] {
     background:
       linear-gradient(
@@ -135,6 +139,10 @@
     transform: translateX(7px);
   }
 
+  &:dir(rtl)[data-checked]:hover:not([data-disabled], [data-readonly]) [data-slot="switch-thumb"] {
+    transform: translateX(-7px);
+  }
+
   &[data-disabled] {
     cursor: not-allowed;
   }

+ 7 - 5
packages/ui/src/v2/components/text-input-v2.css

@@ -3,7 +3,8 @@
   display: flex;
   flex-direction: row;
   align-items: stretch;
-  padding: 0 8px 0 0;
+  padding-block: 0;
+  padding-inline: 0 8px;
   gap: 8px;
   width: 280px;
   height: 28px;
@@ -64,7 +65,7 @@
   flex: none;
   align-items: center;
   justify-content: center;
-  padding-left: 8px;
+  padding-inline-start: 8px;
   color: var(--v2-icon-icon-muted);
 }
 
@@ -74,7 +75,7 @@
 }
 
 [data-component="text-input-v2"][data-leading-icon] [data-slot="text-input-v2-input"] {
-  padding-left: 0;
+  padding-inline-start: 0;
 }
 
 [data-component="text-input-v2"] [data-slot="text-input-v2-input"] {
@@ -82,7 +83,8 @@
   width: 100%;
   min-width: 0;
   height: 100%;
-  padding: 0 0 0 8px;
+  padding-block: 0;
+  padding-inline: 8px 0;
   margin: 0;
   border: 0;
   background: transparent;
@@ -165,7 +167,7 @@
   height: 28px;
   padding: 0;
   border-radius: 6px;
-  margin-right: -8px;
+  margin-inline-end: -8px;
 }
 
 [data-component="text-input-v2"][data-invalid]:not([data-disabled]) [data-slot="text-input-v2-input"] {