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

Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
This commit is contained in:
opencode-agent[bot]
2026-08-04 05:08:51 +00:00
committed by GitHub
parent 5535f7d6d1
commit 5f29a82190
16 changed files with 80 additions and 36 deletions
@@ -215,6 +215,7 @@ export default function FileTreeV2(props: {
<div <div
ref={setRoot} ref={setRoot}
data-component="file-tree-v2" data-component="file-tree-v2"
dir="ltr"
data-total-rows={live() ? rows().length : model()!.total} data-total-rows={live() ? rows().length : model()!.total}
class="group/file-tree-v2" class="group/file-tree-v2"
style={{ position: "relative", height: `${virtualizer.getTotalSize()}px` }} style={{ position: "relative", height: `${virtualizer.getTotalSize()}px` }}
+1
View File
@@ -742,6 +742,7 @@ export const Terminal = (props: TerminalProps) => {
<div <div
ref={container} ref={container}
data-component="terminal" data-component="terminal"
dir="ltr"
data-prevent-autofocus data-prevent-autofocus
tabIndex={-1} tabIndex={-1}
style={{ "background-color": terminalColors().background }} style={{ "background-color": terminalColors().background }}
+6 -2
View File
@@ -36,6 +36,8 @@ export type Locale =
| "fi" | "fi"
| "sv" | "sv"
const RTL_LOCALES: ReadonlySet<Locale> = new Set(["ar", "ur", "pa"])
type RawDictionary = typeof en & typeof uiEn type RawDictionary = typeof en & typeof uiEn
type Dictionary = i18n.Flatten<RawDictionary> type Dictionary = i18n.Flatten<RawDictionary>
type PluralKey = type PluralKey =
@@ -315,8 +317,10 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont
createEffect(() => { createEffect(() => {
if (typeof document !== "object") return if (typeof document !== "object") return
document.documentElement.lang = locale() const value = locale()
document.cookie = cookie(locale()) document.documentElement.lang = value
document.documentElement.dir = RTL_LOCALES.has(value) ? "rtl" : "ltr"
document.cookie = cookie(value)
}) })
return { return {
+15 -14
View File
@@ -2278,7 +2278,7 @@ export default function LegacyLayout(props: ParentProps) {
data-component="sidebar-nav-desktop" data-component="sidebar-nav-desktop"
classList={{ classList={{
"hidden xl:block": true, "hidden xl:block": true,
"absolute inset-y-0 left-0": true, "absolute inset-y-0 start-0": true,
"z-10": true, "z-10": true,
}} }}
style={{ width: `${side()}px` }} style={{ width: `${side()}px` }}
@@ -2301,7 +2301,7 @@ export default function LegacyLayout(props: ParentProps) {
<Show when={layout.sidebar.opened()}> <Show when={layout.sidebar.opened()}>
<div <div
class="hidden xl:block absolute inset-y-0 z-30 w-0 overflow-visible" 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)} onPointerDown={() => setState("sizing", true)}
> >
<ResizeHandle <ResizeHandle
@@ -2320,8 +2320,8 @@ export default function LegacyLayout(props: ParentProps) {
</Show> </Show>
<div <div
class="hidden xl:block pointer-events-none absolute top-0 right-0 z-0 border-t border-border-weaker-base" class="hidden xl:block pointer-events-none absolute top-0 end-0 z-0 border-t border-border-weaker-base"
style={{ left: "calc(4rem + 12px)" }} style={{ "inset-inline-start": "calc(4rem + 12px)" }}
/> />
<div class="xl:hidden"> <div class="xl:hidden">
@@ -2339,9 +2339,9 @@ export default function LegacyLayout(props: ParentProps) {
aria-label={language.t("sidebar.nav.projectsAndSessions")} aria-label={language.t("sidebar.nav.projectsAndSessions")}
data-component="sidebar-nav-mobile" data-component="sidebar-nav-mobile"
classList={{ 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-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()} onClick={(e) => e.stopPropagation()}
> >
@@ -2352,9 +2352,9 @@ export default function LegacyLayout(props: ParentProps) {
<div <div
classList={{ classList={{
"absolute inset-0": true, "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, "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, !state.sizing,
}} }}
style={{ style={{
@@ -2363,7 +2363,7 @@ export default function LegacyLayout(props: ParentProps) {
> >
<main <main
classList={{ 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" />}> <Show when={!autoselecting.loading} fallback={<div class="size-full" />}>
@@ -2374,9 +2374,10 @@ export default function LegacyLayout(props: ParentProps) {
<div <div
classList={{ 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-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, "transition-[opacity,transform] motion-reduce:transition-none": true,
"duration-180 ease-out": state.peeked && !layout.sidebar.opened(), "duration-180 ease-out": state.peeked && !layout.sidebar.opened(),
"duration-120 ease-in": !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 <div
classList={{ 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-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, "transition-[opacity,transform] motion-reduce:transition-none": true,
"duration-180 ease-out": state.peeked && !layout.sidebar.opened(), "duration-180 ease-out": state.peeked && !layout.sidebar.opened(),
"duration-120 ease-in": !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 class="h-full w-px" style={{ "box-shadow": "var(--shadow-sidebar-overlay)" }} />
</div> </div>
+1 -1
View File
@@ -2282,7 +2282,7 @@ export default function Page() {
<div onPointerDown={() => size.start()}> <div onPointerDown={() => size.start()}>
<ResizeHandle <ResizeHandle
classList={{ classList={{
"-right-1": settings.general.newLayoutDesigns(), "-end-1": settings.general.newLayoutDesigns(),
}} }}
direction="horizontal" direction="horizontal"
size={sessionPanelResizedWidth()} size={sessionPanelResizedWidth()}
@@ -1103,7 +1103,7 @@ export function MessageTimeline(props: {
return ( return (
<TimelineRowFrame row={commentStripRow}> <TimelineRowFrame row={commentStripRow}>
<div class="w-full px-4 md:px-5 pb-2"> <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"> <div class="flex w-max min-w-full justify-end gap-2">
<Index each={comments()}> <Index each={comments()}>
{(comment) => ( {(comment) => (
@@ -700,6 +700,7 @@ function ViewerShell(props: {
<div <div
data-component="file" data-component="file"
data-mode={props.mode} data-mode={props.mode}
dir="ltr"
style={styleVariables} style={styleVariables}
class="relative outline-none" class="relative outline-none"
classList={{ classList={{
+19 -11
View File
@@ -98,8 +98,8 @@
ol { ol {
margin-top: 8px; margin-top: 8px;
margin-bottom: 12px; margin-bottom: 12px;
margin-left: 0; margin-inline-start: 0;
padding-left: 32px; padding-inline-start: 32px;
list-style-position: outside; list-style-position: outside;
} }
@@ -109,7 +109,7 @@
ol { ol {
list-style-type: decimal; list-style-type: decimal;
padding-left: 2.25rem; padding-inline-start: 2.25rem;
} }
li { li {
@@ -131,7 +131,7 @@
li > p > input[type="checkbox"] { li > p > input[type="checkbox"] {
position: absolute; position: absolute;
top: 4px; top: 4px;
left: -20px; inset-inline-start: -20px;
margin: 0; margin: 0;
} }
@@ -149,18 +149,18 @@
li > ol { li > ol {
margin-top: 0.25rem; margin-top: 0.25rem;
margin-bottom: 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 { li > ol {
padding-left: 1.75rem; padding-inline-start: 1.75rem;
} }
/* Blockquotes */ /* Blockquotes */
blockquote { 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; margin: 1rem 0;
padding-left: 0.5rem; padding-inline-start: 0.5rem;
color: var(--v2-text-text-muted); color: var(--v2-text-text-muted);
font-style: normal; font-style: normal;
} }
@@ -183,6 +183,8 @@
[data-component="markdown-code"] { [data-component="markdown-code"] {
position: relative; position: relative;
direction: ltr;
unicode-bidi: isolate;
} }
[data-component="markdown-code"][data-code-kind="shell"] .shiki { [data-component="markdown-code"][data-code-kind="shell"] .shiki {
@@ -202,7 +204,7 @@
[data-slot="markdown-copy-button"] { [data-slot="markdown-copy-button"] {
position: absolute; position: absolute;
top: 4px; top: 4px;
right: 4px; inset-inline-end: 4px;
opacity: 0; opacity: 0;
transition: opacity 0.15s ease; transition: opacity 0.15s ease;
z-index: 1; z-index: 1;
@@ -237,6 +239,8 @@
} }
:not(pre) > code { :not(pre) > code {
direction: ltr;
unicode-bidi: isolate;
font-family: var(--font-family-mono); font-family: var(--font-family-mono);
font-feature-settings: var(--font-family-mono--font-feature-settings); font-feature-settings: var(--font-family-mono--font-feature-settings);
color: var(--v2-text-text-base); color: var(--v2-text-text-base);
@@ -278,7 +282,7 @@
/* Minimal borders for structure, matching TUI "lines" roughly but keeping it web-clean */ /* Minimal borders for structure, matching TUI "lines" roughly but keeping it web-clean */
border-bottom: 0.5px solid var(--v2-border-border-muted); border-bottom: 0.5px solid var(--v2-border-border-muted);
padding: 12px; padding: 12px;
text-align: left; text-align: start;
vertical-align: top; vertical-align: top;
} }
@@ -298,6 +302,10 @@
} }
} }
[data-component="markdown"] > * {
unicode-bidi: plaintext;
}
body[data-new-layout] [data-component="markdown"] { body[data-new-layout] [data-component="markdown"] {
[data-slot="markdown-copy-button"]::after { [data-slot="markdown-copy-button"]::after {
box-sizing: border-box; box-sizing: border-box;
@@ -345,7 +353,7 @@ body:not([data-new-layout]) [data-component="markdown"] {
} }
blockquote { blockquote {
border-left: 2px solid var(--border-weak-base); border-inline-start: 2px solid var(--border-weak-base);
color: var(--text-weak); color: var(--text-weak);
} }
@@ -542,6 +542,7 @@ export function Markdown(
return ( return (
<div <div
data-component="markdown" data-component="markdown"
dir="auto"
classList={{ classList={{
...local.classList, ...local.classList,
[local.class ?? ""]: !!local.class, [local.class ?? ""]: !!local.class,
@@ -30,7 +30,7 @@
gap: 8px; gap: 8px;
width: fit-content; width: fit-content;
max-width: min(82%, 64ch); max-width: min(82%, 64ch);
margin-left: auto; margin-inline-start: auto;
} }
[data-slot="user-message-attachment"] { [data-slot="user-message-attachment"] {
@@ -127,7 +127,7 @@
[data-slot="user-message-body"] { [data-slot="user-message-body"] {
width: fit-content; width: fit-content;
max-width: min(82%, 64ch); max-width: min(82%, 64ch);
margin-left: auto; margin-inline-start: auto;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-end; align-items: flex-end;
@@ -138,6 +138,7 @@
white-space: pre-wrap; white-space: pre-wrap;
word-break: break-word; word-break: break-word;
overflow: hidden; overflow: hidden;
unicode-bidi: plaintext;
background: var(--v2-background-bg-layer-02); background: var(--v2-background-bg-layer-02);
border: none; border: none;
padding: 8px 12px; padding: 8px 12px;
@@ -145,10 +146,14 @@
[data-highlight="file"] { [data-highlight="file"] {
color: var(--syntax-property); color: var(--syntax-property);
direction: ltr;
unicode-bidi: isolate;
} }
[data-highlight="agent"] { [data-highlight="agent"] {
color: var(--syntax-type); color: var(--syntax-type);
direction: ltr;
unicode-bidi: isolate;
} }
max-width: 100%; max-width: 100%;
@@ -175,7 +180,7 @@
[data-slot="user-message-meta"] { [data-slot="user-message-meta"] {
user-select: none; user-select: none;
text-align: right; text-align: end;
flex: 1 1 auto; flex: 1 1 auto;
min-width: 0; min-width: 0;
overflow: hidden; overflow: hidden;
@@ -197,7 +202,7 @@
user-select: none; user-select: none;
flex: 0 0 auto; flex: 0 0 auto;
white-space: nowrap; white-space: nowrap;
text-align: right; text-align: end;
} }
&:hover [data-slot="user-message-copy-wrapper"], &:hover [data-slot="user-message-copy-wrapper"],
@@ -364,6 +369,8 @@
} }
[data-component="bash-output"] { [data-component="bash-output"] {
direction: ltr;
unicode-bidi: isolate;
width: 100%; width: 100%;
border: 0.5px solid var(--v2-border-border-base); border: 0.5px solid var(--v2-border-border-base);
border-radius: 6px; border-radius: 6px;
@@ -473,6 +480,8 @@
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
font-weight: var(--font-weight-regular); font-weight: var(--font-weight-regular);
direction: ltr;
unicode-bidi: isolate;
} }
[data-slot="message-part-path"] { [data-slot="message-part-path"] {
@@ -480,6 +489,8 @@
flex-grow: 1; flex-grow: 1;
min-width: 0; min-width: 0;
font-weight: var(--font-weight-regular); font-weight: var(--font-weight-regular);
direction: ltr;
unicode-bidi: isolate;
} }
[data-slot="message-part-directory"] { [data-slot="message-part-directory"] {
@@ -1240,6 +1251,8 @@
flex-grow: 1; flex-grow: 1;
min-width: 0; min-width: 0;
overflow: hidden; overflow: hidden;
direction: ltr;
unicode-bidi: isolate;
} }
[data-slot="apply-patch-directory"] { [data-slot="apply-patch-directory"] {
@@ -107,7 +107,7 @@ function ShellSubmessage(props: { text: string; animate?: boolean }) {
}) })
return ( 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 ref={widthRef} data-slot="shell-submessage-width" style={{ width: props.animate ? "0px" : undefined }}>
<span data-slot="basic-tool-tool-subtitle"> <span data-slot="basic-tool-tool-subtitle">
<span <span
@@ -1331,7 +1331,11 @@ export function UserMessageDisplay(props: {
} }
> >
<div data-slot="user-message-body"> <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()} /> <HighlightedText text={text()} references={inlineFiles()} agents={agents()} />
<Show when={messageComments().length > 0}> <Show when={messageComments().length > 0}>
<UserMessageComments comments={messageComments()} bounded /> <UserMessageComments comments={messageComments()} bounded />
@@ -2122,7 +2126,7 @@ ToolRegistry.register({
</div> </div>
)} )}
> >
<div data-component="bash-output"> <div data-component="bash-output" dir="ltr">
<div data-slot="bash-copy"> <div data-slot="bash-copy">
<TooltipV2 value={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copy")} placement="top"> <TooltipV2 value={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copy")} placement="top">
<IconButtonV2 <IconButtonV2
@@ -112,6 +112,8 @@
align-items: center; align-items: center;
flex-grow: 1; flex-grow: 1;
min-width: 0; min-width: 0;
direction: ltr;
unicode-bidi: isolate;
} }
[data-slot="session-review-directory"] { [data-slot="session-review-directory"] {
@@ -169,6 +169,8 @@
display: flex; display: flex;
flex-grow: 1; flex-grow: 1;
min-width: 0; min-width: 0;
direction: ltr;
unicode-bidi: isolate;
font-family: var(--font-family-sans); font-family: var(--font-family-sans);
font-size: var(--font-size-small); font-size: var(--font-size-small);
@@ -324,6 +324,8 @@
gap: 6px; gap: 6px;
min-width: 0; min-width: 0;
flex: 1; flex: 1;
direction: ltr;
unicode-bidi: isolate;
} }
[data-component="session-review-v2"] [data-slot="session-review-v2-file-diff"] { [data-component="session-review-v2"] [data-slot="session-review-v2-file-diff"] {
@@ -1,5 +1,7 @@
[data-component="diff-changes"] { [data-component="diff-changes"] {
display: flex; display: flex;
direction: ltr;
unicode-bidi: isolate;
gap: 8px; gap: 8px;
justify-content: flex-end; justify-content: flex-end;
align-items: center; align-items: center;
@@ -1,5 +1,7 @@
[data-component="diff-changes"] { [data-component="diff-changes"] {
display: flex; display: flex;
direction: ltr;
unicode-bidi: isolate;
gap: 8px; gap: 8px;
justify-content: flex-end; justify-content: flex-end;
align-items: center; align-items: center;