瀏覽代碼

feat(app): add minimal RTL support (#40372)

Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
opencode-agent[bot] 1 周之前
父節點
當前提交
6c3299103c

+ 1 - 0
packages/app/src/components/file-tree-v2.tsx

@@ -215,6 +215,7 @@ 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` }}

+ 1 - 0
packages/app/src/components/terminal.tsx

@@ -742,6 +742,7 @@ export const Terminal = (props: TerminalProps) => {
     <div
       ref={container}
       data-component="terminal"
+      dir="ltr"
       data-prevent-autofocus
       tabIndex={-1}
       style={{ "background-color": terminalColors().background }}

+ 6 - 2
packages/app/src/context/language.tsx

@@ -36,6 +36,8 @@ export type Locale =
   | "fi"
   | "sv"
 
+const RTL_LOCALES: ReadonlySet<Locale> = new Set(["ar", "ur", "pa"])
+
 type RawDictionary = typeof en & typeof uiEn
 type Dictionary = i18n.Flatten<RawDictionary>
 type PluralKey =
@@ -315,8 +317,10 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont
 
     createEffect(() => {
       if (typeof document !== "object") return
-      document.documentElement.lang = locale()
-      document.cookie = cookie(locale())
+      const value = locale()
+      document.documentElement.lang = value
+      document.documentElement.dir = RTL_LOCALES.has(value) ? "rtl" : "ltr"
+      document.cookie = cookie(value)
     })
 
     return {

+ 15 - 14
packages/app/src/pages/layout.tsx

@@ -2278,7 +2278,7 @@ export default function LegacyLayout(props: ParentProps) {
               data-component="sidebar-nav-desktop"
               classList={{
                 "hidden xl:block": true,
-                "absolute inset-y-0 left-0": true,
+                "absolute inset-y-0 start-0": true,
                 "z-10": true,
               }}
               style={{ width: `${side()}px` }}
@@ -2301,7 +2301,7 @@ export default function LegacyLayout(props: ParentProps) {
             <Show when={layout.sidebar.opened()}>
               <div
                 class="hidden xl:block absolute inset-y-0 z-30 w-0 overflow-visible"
-                style={{ left: `${side()}px` }}
+                style={{ "inset-inline-start": `${side()}px` }}
                 onPointerDown={() => setState("sizing", true)}
               >
                 <ResizeHandle
@@ -2320,8 +2320,8 @@ export default function LegacyLayout(props: ParentProps) {
             </Show>
 
             <div
-              class="hidden xl:block pointer-events-none absolute top-0 right-0 z-0 border-t border-border-weaker-base"
-              style={{ left: "calc(4rem + 12px)" }}
+              class="hidden xl:block pointer-events-none absolute top-0 end-0 z-0 border-t border-border-weaker-base"
+              style={{ "inset-inline-start": "calc(4rem + 12px)" }}
             />
 
             <div class="xl:hidden">
@@ -2339,9 +2339,9 @@ export default function LegacyLayout(props: ParentProps) {
                 aria-label={language.t("sidebar.nav.projectsAndSessions")}
                 data-component="sidebar-nav-mobile"
                 classList={{
-                  "@container fixed top-10 bottom-0 left-0 z-50 w-full max-w-[400px] overflow-hidden border-r border-border-weaker-base bg-background-base transition-transform duration-200 ease-out": true,
+                  "@container fixed top-10 bottom-0 start-0 z-50 w-full max-w-[400px] overflow-hidden border-e border-border-weaker-base bg-background-base transition-transform duration-200 ease-out": true,
                   "translate-x-0": layout.mobileSidebar.opened(),
-                  "-translate-x-full": !layout.mobileSidebar.opened(),
+                  "ltr:-translate-x-full rtl:translate-x-full": !layout.mobileSidebar.opened(),
                 }}
                 onClick={(e) => e.stopPropagation()}
               >
@@ -2352,9 +2352,9 @@ export default function LegacyLayout(props: ParentProps) {
             <div
               classList={{
                 "absolute inset-0": true,
-                "xl:inset-y-0 xl:right-0 xl:left-[var(--main-left)]": true,
+                "xl:inset-y-0 xl:end-0 xl:start-[var(--main-left)]": true,
                 "z-20": true,
-                "transition-[left] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[left] motion-reduce:transition-none":
+                "transition-[inset-inline-start] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[inset-inline-start] motion-reduce:transition-none":
                   !state.sizing,
               }}
               style={{
@@ -2363,7 +2363,7 @@ export default function LegacyLayout(props: ParentProps) {
             >
               <main
                 classList={{
-                  "size-full overflow-x-hidden flex flex-col items-start contain-strict border-t border-border-weak-base bg-background-base xl:border-l xl:rounded-tl-[12px]": true,
+                  "size-full overflow-x-hidden flex flex-col items-start contain-strict border-t border-border-weak-base bg-background-base xl:border-s xl:rounded-ss-[12px]": true,
                 }}
               >
                 <Show when={!autoselecting.loading} fallback={<div class="size-full" />}>
@@ -2374,9 +2374,10 @@ export default function LegacyLayout(props: ParentProps) {
 
             <div
               classList={{
-                "hidden xl:flex absolute inset-y-0 left-16 z-30": true,
+                "hidden xl:flex absolute inset-y-0 start-16 z-30": true,
                 "opacity-100 translate-x-0 pointer-events-auto": state.peeked && !layout.sidebar.opened(),
-                "opacity-0 -translate-x-2 pointer-events-none": !state.peeked || layout.sidebar.opened(),
+                "opacity-0 ltr:-translate-x-2 rtl:translate-x-2 pointer-events-none":
+                  !state.peeked || layout.sidebar.opened(),
                 "transition-[opacity,transform] motion-reduce:transition-none": true,
                 "duration-180 ease-out": state.peeked && !layout.sidebar.opened(),
                 "duration-120 ease-in": !state.peeked || layout.sidebar.opened(),
@@ -2398,14 +2399,14 @@ export default function LegacyLayout(props: ParentProps) {
 
             <div
               classList={{
-                "hidden xl:block pointer-events-none absolute inset-y-0 right-0 z-25 overflow-hidden": true,
+                "hidden xl:block pointer-events-none absolute inset-y-0 end-0 z-25 overflow-hidden": true,
                 "opacity-100 translate-x-0": state.peeked && !layout.sidebar.opened(),
-                "opacity-0 -translate-x-2": !state.peeked || layout.sidebar.opened(),
+                "opacity-0 ltr:-translate-x-2 rtl:translate-x-2": !state.peeked || layout.sidebar.opened(),
                 "transition-[opacity,transform] motion-reduce:transition-none": true,
                 "duration-180 ease-out": state.peeked && !layout.sidebar.opened(),
                 "duration-120 ease-in": !state.peeked || layout.sidebar.opened(),
               }}
-              style={{ left: `calc(4rem + ${panel()}px)` }}
+              style={{ "inset-inline-start": `calc(4rem + ${panel()}px)` }}
             >
               <div class="h-full w-px" style={{ "box-shadow": "var(--shadow-sidebar-overlay)" }} />
             </div>

+ 1 - 1
packages/app/src/pages/session.tsx

@@ -2282,7 +2282,7 @@ export default function Page() {
             <div onPointerDown={() => size.start()}>
               <ResizeHandle
                 classList={{
-                  "-right-1": settings.general.newLayoutDesigns(),
+                  "-end-1": settings.general.newLayoutDesigns(),
                 }}
                 direction="horizontal"
                 size={sessionPanelResizedWidth()}

+ 1 - 1
packages/app/src/pages/session/timeline/message-timeline.tsx

@@ -1103,7 +1103,7 @@ export function MessageTimeline(props: {
         return (
           <TimelineRowFrame row={commentStripRow}>
             <div class="w-full px-4 md:px-5 pb-2">
-              <div class="ml-auto max-w-[82%] overflow-x-auto no-scrollbar">
+              <div class="ms-auto max-w-[82%] overflow-x-auto no-scrollbar">
                 <div class="flex w-max min-w-full justify-end gap-2">
                   <Index each={comments()}>
                     {(comment) => (

+ 1 - 0
packages/session-ui/src/components/file.tsx

@@ -700,6 +700,7 @@ function ViewerShell(props: {
     <div
       data-component="file"
       data-mode={props.mode}
+      dir="ltr"
       style={styleVariables}
       class="relative outline-none"
       classList={{

+ 19 - 11
packages/session-ui/src/components/markdown.css

@@ -98,8 +98,8 @@
   ol {
     margin-top: 8px;
     margin-bottom: 12px;
-    margin-left: 0;
-    padding-left: 32px;
+    margin-inline-start: 0;
+    padding-inline-start: 32px;
     list-style-position: outside;
   }
 
@@ -109,7 +109,7 @@
 
   ol {
     list-style-type: decimal;
-    padding-left: 2.25rem;
+    padding-inline-start: 2.25rem;
   }
 
   li {
@@ -131,7 +131,7 @@
   li > p > input[type="checkbox"] {
     position: absolute;
     top: 4px;
-    left: -20px;
+    inset-inline-start: -20px;
     margin: 0;
   }
 
@@ -149,18 +149,18 @@
   li > ol {
     margin-top: 0.25rem;
     margin-bottom: 0.25rem;
-    padding-left: 1rem; /* Minimal indent for nesting only */
+    padding-inline-start: 1rem; /* Minimal indent for nesting only */
   }
 
   li > ol {
-    padding-left: 1.75rem;
+    padding-inline-start: 1.75rem;
   }
 
   /* Blockquotes */
   blockquote {
-    border-left: 0.5px solid var(--v2-border-border-base);
+    border-inline-start: 0.5px solid var(--v2-border-border-base);
     margin: 1rem 0;
-    padding-left: 0.5rem;
+    padding-inline-start: 0.5rem;
     color: var(--v2-text-text-muted);
     font-style: normal;
   }
@@ -183,6 +183,8 @@
 
   [data-component="markdown-code"] {
     position: relative;
+    direction: ltr;
+    unicode-bidi: isolate;
   }
 
   [data-component="markdown-code"][data-code-kind="shell"] .shiki {
@@ -202,7 +204,7 @@
   [data-slot="markdown-copy-button"] {
     position: absolute;
     top: 4px;
-    right: 4px;
+    inset-inline-end: 4px;
     opacity: 0;
     transition: opacity 0.15s ease;
     z-index: 1;
@@ -237,6 +239,8 @@
   }
 
   :not(pre) > code {
+    direction: ltr;
+    unicode-bidi: isolate;
     font-family: var(--font-family-mono);
     font-feature-settings: var(--font-family-mono--font-feature-settings);
     color: var(--v2-text-text-base);
@@ -278,7 +282,7 @@
     /* Minimal borders for structure, matching TUI "lines" roughly but keeping it web-clean */
     border-bottom: 0.5px solid var(--v2-border-border-muted);
     padding: 12px;
-    text-align: left;
+    text-align: start;
     vertical-align: top;
   }
 
@@ -298,6 +302,10 @@
   }
 }
 
+[data-component="markdown"] > * {
+  unicode-bidi: plaintext;
+}
+
 body[data-new-layout] [data-component="markdown"] {
   [data-slot="markdown-copy-button"]::after {
     box-sizing: border-box;
@@ -345,7 +353,7 @@ body:not([data-new-layout]) [data-component="markdown"] {
   }
 
   blockquote {
-    border-left: 2px solid var(--border-weak-base);
+    border-inline-start: 2px solid var(--border-weak-base);
     color: var(--text-weak);
   }
 

+ 1 - 0
packages/session-ui/src/components/markdown.tsx

@@ -542,6 +542,7 @@ export function Markdown(
   return (
     <div
       data-component="markdown"
+      dir="auto"
       classList={{
         ...local.classList,
         [local.class ?? ""]: !!local.class,

+ 17 - 4
packages/session-ui/src/components/message-part.css

@@ -30,7 +30,7 @@
     gap: 8px;
     width: fit-content;
     max-width: min(82%, 64ch);
-    margin-left: auto;
+    margin-inline-start: auto;
   }
 
   [data-slot="user-message-attachment"] {
@@ -127,7 +127,7 @@
   [data-slot="user-message-body"] {
     width: fit-content;
     max-width: min(82%, 64ch);
-    margin-left: auto;
+    margin-inline-start: auto;
     display: flex;
     flex-direction: column;
     align-items: flex-end;
@@ -138,6 +138,7 @@
     white-space: pre-wrap;
     word-break: break-word;
     overflow: hidden;
+    unicode-bidi: plaintext;
     background: var(--v2-background-bg-layer-02);
     border: none;
     padding: 8px 12px;
@@ -145,10 +146,14 @@
 
     [data-highlight="file"] {
       color: var(--syntax-property);
+      direction: ltr;
+      unicode-bidi: isolate;
     }
 
     [data-highlight="agent"] {
       color: var(--syntax-type);
+      direction: ltr;
+      unicode-bidi: isolate;
     }
 
     max-width: 100%;
@@ -175,7 +180,7 @@
 
   [data-slot="user-message-meta"] {
     user-select: none;
-    text-align: right;
+    text-align: end;
     flex: 1 1 auto;
     min-width: 0;
     overflow: hidden;
@@ -197,7 +202,7 @@
     user-select: none;
     flex: 0 0 auto;
     white-space: nowrap;
-    text-align: right;
+    text-align: end;
   }
 
   &:hover [data-slot="user-message-copy-wrapper"],
@@ -364,6 +369,8 @@
 }
 
 [data-component="bash-output"] {
+  direction: ltr;
+  unicode-bidi: isolate;
   width: 100%;
   border: 0.5px solid var(--v2-border-border-base);
   border-radius: 6px;
@@ -473,6 +480,8 @@
     text-overflow: ellipsis;
     white-space: nowrap;
     font-weight: var(--font-weight-regular);
+    direction: ltr;
+    unicode-bidi: isolate;
   }
 
   [data-slot="message-part-path"] {
@@ -480,6 +489,8 @@
     flex-grow: 1;
     min-width: 0;
     font-weight: var(--font-weight-regular);
+    direction: ltr;
+    unicode-bidi: isolate;
   }
 
   [data-slot="message-part-directory"] {
@@ -1240,6 +1251,8 @@
     flex-grow: 1;
     min-width: 0;
     overflow: hidden;
+    direction: ltr;
+    unicode-bidi: isolate;
   }
 
   [data-slot="apply-patch-directory"] {

+ 7 - 3
packages/session-ui/src/components/message-part.tsx

@@ -107,7 +107,7 @@ function ShellSubmessage(props: { text: string; animate?: boolean }) {
   })
 
   return (
-    <span data-component="shell-submessage">
+    <span data-component="shell-submessage" dir="ltr">
       <span ref={widthRef} data-slot="shell-submessage-width" style={{ width: props.animate ? "0px" : undefined }}>
         <span data-slot="basic-tool-tool-subtitle">
           <span
@@ -1331,7 +1331,11 @@ export function UserMessageDisplay(props: {
         }
       >
         <div data-slot="user-message-body">
-          <div data-slot="user-message-text" data-comments={messageComments().length > 0 ? "true" : undefined}>
+          <div
+            data-slot="user-message-text"
+            dir="auto"
+            data-comments={messageComments().length > 0 ? "true" : undefined}
+          >
             <HighlightedText text={text()} references={inlineFiles()} agents={agents()} />
             <Show when={messageComments().length > 0}>
               <UserMessageComments comments={messageComments()} bounded />
@@ -2122,7 +2126,7 @@ ToolRegistry.register({
           </div>
         )}
       >
-        <div data-component="bash-output">
+        <div data-component="bash-output" dir="ltr">
           <div data-slot="bash-copy">
             <TooltipV2 value={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copy")} placement="top">
               <IconButtonV2

+ 2 - 0
packages/session-ui/src/components/session-review.css

@@ -112,6 +112,8 @@
     align-items: center;
     flex-grow: 1;
     min-width: 0;
+    direction: ltr;
+    unicode-bidi: isolate;
   }
 
   [data-slot="session-review-directory"] {

+ 2 - 0
packages/session-ui/src/components/session-turn.css

@@ -169,6 +169,8 @@
     display: flex;
     flex-grow: 1;
     min-width: 0;
+    direction: ltr;
+    unicode-bidi: isolate;
 
     font-family: var(--font-family-sans);
     font-size: var(--font-size-small);

+ 2 - 0
packages/session-ui/src/v2/components/session-review-v2.css

@@ -324,6 +324,8 @@
   gap: 6px;
   min-width: 0;
   flex: 1;
+  direction: ltr;
+  unicode-bidi: isolate;
 }
 
 [data-component="session-review-v2"] [data-slot="session-review-v2-file-diff"] {

+ 2 - 0
packages/ui/src/components/diff-changes.css

@@ -1,5 +1,7 @@
 [data-component="diff-changes"] {
   display: flex;
+  direction: ltr;
+  unicode-bidi: isolate;
   gap: 8px;
   justify-content: flex-end;
   align-items: center;

+ 2 - 0
packages/ui/src/v2/components/diff-changes-v2.css

@@ -1,5 +1,7 @@
 [data-component="diff-changes"] {
   display: flex;
+  direction: ltr;
+  unicode-bidi: isolate;
   gap: 8px;
   justify-content: flex-end;
   align-items: center;