Skip to content

Commit de1bd36

Browse files
authored
fix(app): correct RTL layout interactions (#40410)
1 parent 6c97a46 commit de1bd36

21 files changed

Lines changed: 210 additions & 84 deletions

‎packages/app/src/components/file-tree-v2.tsx‎

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,14 @@ export type { Kind } from "@/components/file-tree"
2929

3030
const INDENT_STEP = 16
3131

32-
function rowPaddingLeft(level: number, type: FileNode["type"]) {
32+
function rowPaddingStart(level: number, type: FileNode["type"]) {
3333
if (type === "directory") return 8 + level * INDENT_STEP
3434
if (level === 0) return 8
3535
return 8 + level * INDENT_STEP - INDENT_STEP
3636
}
3737

38-
function guideLineLeft(level: number) {
39-
return rowPaddingLeft(level, "directory") + 8
38+
function guideLineStart(level: number) {
39+
return rowPaddingStart(level, "directory") + 8
4040
}
4141

4242
export const kindLabel = (kind: Kind) => {
@@ -87,7 +87,7 @@ const FileTreeNodeV2 = (
8787
...local.classList,
8888
[local.class ?? ""]: !!local.class,
8989
}}
90-
style={`padding-left: ${rowPaddingLeft(local.level, local.node.type)}px`}
90+
style={`padding-inline-start: ${rowPaddingStart(local.level, local.node.type)}px`}
9191
draggable={local.draggable}
9292
onDragStart={(event: DragEvent) => {
9393
if (!local.draggable) return
@@ -99,7 +99,9 @@ const FileTreeNodeV2 = (
9999
{...rest}
100100
>
101101
{local.children}
102-
<span class="flex-1 min-w-0 text-12-medium whitespace-nowrap truncate">{local.node.name}</span>
102+
<span class="flex-1 min-w-0 text-start text-12-medium whitespace-nowrap truncate">
103+
<bdi dir="auto">{local.node.name}</bdi>
104+
</span>
103105
{(() => {
104106
const value = kind()
105107
if (!value || local.node.type !== "file") return null
@@ -116,7 +118,7 @@ const FileTreeNodeV2 = (
116118
function GuideLines(props: { level: number }) {
117119
return (
118120
<For each={Array.from({ length: props.level })}>
119-
{(_, index) => <div data-slot="file-tree-v2-guide" style={`left: ${guideLineLeft(index())}px`} />}
121+
{(_, index) => <div data-slot="file-tree-v2-guide" style={`inset-inline-start: ${guideLineStart(index())}px`} />}
120122
</For>
121123
)
122124
}
@@ -215,7 +217,6 @@ export default function FileTreeV2(props: {
215217
<div
216218
ref={setRoot}
217219
data-component="file-tree-v2"
218-
dir="ltr"
219220
data-total-rows={live() ? rows().length : model()!.total}
220221
class="group/file-tree-v2"
221222
style={{ position: "relative", height: `${virtualizer.getTotalSize()}px` }}
@@ -228,7 +229,7 @@ export default function FileTreeV2(props: {
228229
style={{
229230
position: "absolute",
230231
top: "0",
231-
left: "0",
232+
"inset-inline-start": "0",
232233
width: "100%",
233234
height: `${item().size}px`,
234235
transform: `translateY(${item().start}px)`,

‎packages/app/src/components/file-tree.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,13 +146,13 @@ const FileTreeNode = (
146146
<Dynamic
147147
component={local.as ?? "div"}
148148
classList={{
149-
"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,
149+
"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,
150150
"bg-surface-base-active": local.node.path === local.active,
151151
...local.classList,
152152
[local.class ?? ""]: !!local.class,
153153
[local.nodeClass ?? ""]: !!local.nodeClass,
154154
}}
155-
style={`padding-left: ${Math.max(0, 8 + local.level * 12 - (local.node.type === "file" ? 24 : 4))}px`}
155+
style={`padding-inline-start: ${Math.max(0, 8 + local.level * 12 - (local.node.type === "file" ? 24 : 4))}px`}
156156
draggable={local.draggable}
157157
onDragStart={(event: DragEvent) => {
158158
if (!local.draggable) return

‎packages/app/src/components/settings-v2/settings-v2.css‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@
287287
}
288288

289289
.settings-v2-provider-env-hint {
290-
padding-right: 12px;
290+
padding-inline-end: 12px;
291291
font-size: 13px;
292292
font-weight: 440;
293293
line-height: 1;
@@ -311,7 +311,7 @@
311311
line-height: 1;
312312
color: var(--v2-text-text-accent);
313313
cursor: pointer;
314-
text-align: left;
314+
text-align: start;
315315
}
316316

317317
.settings-v2-providers-view-all:hover {
@@ -361,13 +361,13 @@
361361
}
362362

363363
.settings-v2-tab-search [data-slot="text-input-v2-input"] {
364-
padding-right: 28px;
364+
padding-inline-end: 28px;
365365
}
366366

367367
.settings-v2-tab-search-clear {
368368
position: absolute;
369369
top: 50%;
370-
right: 6px;
370+
inset-inline-end: 6px;
371371
z-index: 1;
372372
transform: translateY(-50%);
373373
}

‎packages/app/src/components/titlebar-tab-nav.css‎

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[data-titlebar-tab] [data-slot="tab-close"] {
22
position: absolute;
33
top: 4px;
4-
right: 4px;
4+
inset-inline-end: 4px;
55
display: flex;
66
height: 20px;
77
width: 20px;
@@ -52,7 +52,7 @@
5252
content: "";
5353
position: absolute;
5454
top: 8px;
55-
left: -3.75px;
55+
inset-inline-start: -3.75px;
5656
width: 1.5px;
5757
height: 12px;
5858
border-radius: 9999px;
@@ -86,14 +86,31 @@
8686
);
8787
}
8888

89+
[data-titlebar-tab][data-title-overflow="true"]:not([data-editing="true"]):dir(rtl) [data-slot="tab-link"] {
90+
-webkit-mask-image: linear-gradient(
91+
to left,
92+
black 0,
93+
black calc(100% - var(--tab-title-fade-offset) - 16px),
94+
transparent calc(100% - var(--tab-title-fade-offset)),
95+
transparent 100%
96+
);
97+
mask-image: linear-gradient(
98+
to left,
99+
black 0,
100+
black calc(100% - var(--tab-title-fade-offset) - 16px),
101+
transparent calc(100% - var(--tab-title-fade-offset)),
102+
transparent 100%
103+
);
104+
}
105+
89106
[data-titlebar-tab][data-title-overflow="true"]:is(:hover, [data-active="true"]):not([data-editing="true"])
90107
[data-slot="tab-link"] {
91108
--tab-title-fade-offset: 24px;
92109
}
93110

94111
[data-titlebar-tab][data-title-overflow="true"]:not(:hover):not([data-active="true"]):not([data-editing="true"])
95112
[data-slot="tab-link"] {
96-
padding-right: 0;
113+
padding-inline-end: 0;
97114
}
98115

99116
[data-titlebar-tab] [data-slot="tab-title"] {

‎packages/app/src/components/titlebar.tsx‎

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,15 @@
1-
import { createEffect, createMemo, createResource, createSignal, Match, onMount, Show, Switch, untrack } from "solid-js"
1+
import {
2+
createEffect,
3+
createMemo,
4+
createResource,
5+
createSignal,
6+
Match,
7+
on,
8+
onMount,
9+
Show,
10+
Switch,
11+
untrack,
12+
} from "solid-js"
213
import { createStore } from "solid-js/store"
314
import { useLocation, useNavigate, useParams } from "@solidjs/router"
415
import { IconButton } from "@opencode-ai/ui/icon-button"
@@ -42,8 +53,11 @@ export type TitlebarUpdate = {
4253
}
4354

4455
export function useTitlebarRightMount() {
56+
const language = useLanguage()
4557
const [mount, setMount] = createSignal<HTMLElement | null>(null)
46-
onMount(() => setMount(document.getElementById("opencode-titlebar-right")))
58+
const sync = () => setMount(document.getElementById("opencode-titlebar-right"))
59+
onMount(sync)
60+
createEffect(on(language.direction, sync, { defer: true }))
4761
return mount
4862
}
4963

@@ -169,7 +183,8 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
169183
"padding-left": macTrafficLights() ? `${macTrafficLightsBaseWidth / zoom()}px` : 0,
170184
width: windows() ? `env(titlebar-area-width, calc(100vw - ${windowsControlsWidth()}))` : undefined,
171185
"max-width": windows() ? `env(titlebar-area-width, calc(100vw - ${windowsControlsWidth()}))` : undefined,
172-
"align-self": windows() ? "flex-start" : undefined,
186+
// Native Windows caption controls remain on the physical right in both writing directions.
187+
"margin-right": windows() ? "auto" : undefined,
173188
}}
174189
data-tauri-drag-region
175190
>

‎packages/session-ui/src/v2/components/session-review-v2.css‎

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,19 +30,19 @@
3030
flex-shrink: 0;
3131
min-height: 0;
3232
overflow: hidden;
33-
border-right: 1px solid var(--border-weaker-base, var(--v2-border-border-weak));
33+
border-inline-end: 1px solid var(--border-weaker-base, var(--v2-border-border-weak));
3434
background: var(--v2-background-bg-base);
3535
}
3636

3737
[data-component="session-review-v2-sidebar-root"] [data-slot="session-review-v2-sidebar"][aria-hidden="true"] {
38-
border-right-width: 0;
38+
border-inline-end-width: 0;
3939
}
4040

4141
[data-component="session-review-v2-sidebar-root"]
4242
[data-slot="session-review-v2-sidebar"][data-transition]:not([data-resizing]) {
4343
transition:
4444
width 200ms cubic-bezier(0.22, 1, 0.36, 1),
45-
border-right-width 200ms cubic-bezier(0.22, 1, 0.36, 1);
45+
border-inline-end-width 200ms cubic-bezier(0.22, 1, 0.36, 1);
4646
}
4747

4848
[data-component="session-review-v2-sidebar-root"] [data-slot="session-review-v2-sidebar-resize"] {
@@ -63,7 +63,8 @@
6363
align-items: center;
6464
justify-content: space-between;
6565
gap: 8px;
66-
padding: 12px 16px 12px 8px;
66+
padding-block: 12px;
67+
padding-inline: 8px 16px;
6768
flex-shrink: 0;
6869
}
6970

@@ -79,7 +80,7 @@
7980

8081
[data-component="session-review-v2-sidebar-root"]
8182
[data-slot="session-review-v2-sidebar-title"]:not(:has([data-component="select-v2-root"])) {
82-
margin-left: 8px;
83+
margin-inline-start: 8px;
8384
}
8485

8586
[data-component="session-review-v2-sidebar-root"]
@@ -211,7 +212,7 @@
211212
justify-content: space-between;
212213
gap: 8px;
213214
padding: 10px 12px;
214-
padding-left: 8px;
215+
padding-inline-start: 8px;
215216
flex-shrink: 0;
216217
border-bottom: 1px solid var(--border-weaker-base, var(--v2-border-border-weak));
217218
}
@@ -231,7 +232,7 @@
231232
align-items: center;
232233
gap: 8px;
233234
min-width: 0;
234-
padding-right: 8px;
235+
padding-inline-end: 8px;
235236
}
236237

237238
[data-component="session-review-v2"] [data-slot="session-review-v2-toolbar-title"] {
@@ -331,25 +332,30 @@
331332
[data-component="session-review-v2"] [data-slot="session-review-v2-file-diff"] {
332333
position: relative;
333334
flex-shrink: 0;
334-
margin-left: auto;
335+
margin-inline-start: auto;
335336
background-color: var(--v2-background-bg-base);
336337

337338
&::before {
338339
content: "";
339340
position: absolute;
340341
top: -8px;
341342
bottom: -8px;
342-
right: 100%;
343+
inset-inline-end: 100%;
343344
width: 16px;
344345
z-index: 1;
345346
pointer-events: none;
346347
background: linear-gradient(90deg, transparent, var(--v2-background-bg-base));
347348
}
348349

350+
&:dir(rtl)::before {
351+
background: linear-gradient(270deg, transparent, var(--v2-background-bg-base));
352+
}
353+
349354
&::after {
350355
content: "";
351356
position: absolute;
352-
inset: -8px -16px -8px 0;
357+
inset-block: -8px;
358+
inset-inline: 0 -16px;
353359
z-index: 0;
354360
pointer-events: none;
355361
background-color: var(--v2-background-bg-base);

‎packages/session-ui/src/v2/components/session-review-v2.tsx‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
99
import type { SessionReviewDiffStyle } from "../../components/session-review"
1010
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
1111
import { ScrollView } from "@opencode-ai/ui/scroll-view"
12+
import { useLocale } from "@kobalte/core/i18n"
1213
import { makeEventListener } from "@solid-primitives/event-listener"
1314
import { Show, createEffect, createMemo, createSignal, type JSX } from "solid-js"
1415
import { getWorkerPool } from "../../pierre/worker"
@@ -149,6 +150,7 @@ export function SessionReviewV2Sidebar(props: SessionReviewV2SidebarProps) {
149150

150151
export function SessionReviewV2(props: SessionReviewV2Props) {
151152
const i18n = useI18n()
153+
const locale = useLocale()
152154

153155
createEffect(() => {
154156
getWorkerPool(props.diffStyle)
@@ -175,6 +177,8 @@ export function SessionReviewV2(props: SessionReviewV2Props) {
175177
}
176178

177179
const canCycle = () => props.files.length > 0
180+
const previousKey = () => (locale.direction() === "rtl" ? "ArrowRight" : "ArrowLeft")
181+
const nextKey = () => (locale.direction() === "rtl" ? "ArrowLeft" : "ArrowRight")
178182
const showCollapsedMeta = () => props.sidebarOpen === false
179183
// Memoize slot getters so Show conditions do not instantiate throwaway elements.
180184
const title = createMemo(() => props.title)
@@ -189,11 +193,11 @@ export function SessionReviewV2(props: SessionReviewV2Props) {
189193
// pane is mounted, but never while typing in an input or comment editor.
190194
makeEventListener(document, "keydown", (event) => {
191195
if (event.defaultPrevented || event.ctrlKey || event.metaKey || event.altKey) return
192-
if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return
196+
if (event.key !== previousKey() && event.key !== nextKey()) return
193197
const target = event.target
194198
if (target instanceof HTMLElement && (target.isContentEditable || target.closest("input, textarea, select"))) return
195199
if (!props.hasDiffs || !canCycle()) return
196-
const file = event.key === "ArrowLeft" ? prev() : next()
200+
const file = event.key === previousKey() ? prev() : next()
197201
if (!file) return
198202
event.preventDefault()
199203
cycle(file)
@@ -221,7 +225,7 @@ export function SessionReviewV2(props: SessionReviewV2Props) {
221225
value={
222226
<>
223227
{i18n.t("ui.sessionReviewV2.previousFile")}
224-
<KeybindV2 keys={["←"]} variant="neutral" />
228+
<KeybindV2 keys={[locale.direction() === "rtl" ? "→" : "←"]} variant="neutral" />
225229
</>
226230
}
227231
>
@@ -241,7 +245,7 @@ export function SessionReviewV2(props: SessionReviewV2Props) {
241245
value={
242246
<>
243247
{i18n.t("ui.sessionReviewV2.nextFile")}
244-
<KeybindV2 keys={["→"]} variant="neutral" />
248+
<KeybindV2 keys={[locale.direction() === "rtl" ? "←" : "→"]} variant="neutral" />
245249
</>
246250
}
247251
>

‎packages/ui/src/components/dropdown-menu.css‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060

6161
[data-slot="dropdown-menu-checkbox-item"],
6262
[data-slot="dropdown-menu-radio-item"] {
63-
padding-right: 28px;
63+
padding-inline-end: 28px;
6464
}
6565

6666
[data-slot="dropdown-menu-sub-trigger"] {
@@ -76,7 +76,7 @@
7676
width: 16px;
7777
height: 16px;
7878
position: absolute;
79-
right: 8px;
79+
inset-inline-end: 8px;
8080
top: 50%;
8181
transform: translateY(-50%);
8282
}

‎packages/ui/src/components/icon.tsx‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,14 @@ export function Icon(props: IconProps) {
154154
<div
155155
data-component="icon"
156156
data-size={local.size || "normal"}
157-
data-directional={local.name === "chevron-left" || local.name === "chevron-right" ? true : undefined}
157+
data-directional={
158+
local.name === "arrow-left" ||
159+
local.name === "arrow-right" ||
160+
local.name === "chevron-left" ||
161+
local.name === "chevron-right"
162+
? true
163+
: undefined
164+
}
158165
>
159166
<svg
160167
data-slot="icon-svg"

0 commit comments

Comments
 (0)