diff --git a/packages/app/src/components/file-tree-v2.tsx b/packages/app/src/components/file-tree-v2.tsx index ba1a7aaab4..091e489c32 100644 --- a/packages/app/src/components/file-tree-v2.tsx +++ b/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} - {local.node.name} + + {local.node.name} + {(() => { const value = kind() if (!value || local.node.type !== "file") return null @@ -116,7 +118,7 @@ const FileTreeNodeV2 = ( function GuideLines(props: { level: number }) { return ( - {(_, index) =>
} + {(_, index) =>
} ) } @@ -215,7 +217,6 @@ export default function FileTreeV2(props: {
{ if (!local.draggable) return diff --git a/packages/app/src/components/settings-v2/settings-v2.css b/packages/app/src/components/settings-v2/settings-v2.css index c4d47344a2..b73269b496 100644 --- a/packages/app/src/components/settings-v2/settings-v2.css +++ b/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%); } diff --git a/packages/app/src/components/titlebar-tab-nav.css b/packages/app/src/components/titlebar-tab-nav.css index 13dfbe04fc..93834b5b96 100644 --- a/packages/app/src/components/titlebar-tab-nav.css +++ b/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"] { diff --git a/packages/app/src/components/titlebar.tsx b/packages/app/src/components/titlebar.tsx index f24caeb51b..64df40685f 100644 --- a/packages/app/src/components/titlebar.tsx +++ b/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(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 > diff --git a/packages/session-ui/src/v2/components/session-review-v2.css b/packages/session-ui/src/v2/components/session-review-v2.css index 557ef16f6d..ac899be988 100644 --- a/packages/session-ui/src/v2/components/session-review-v2.css +++ b/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); diff --git a/packages/session-ui/src/v2/components/session-review-v2.tsx b/packages/session-ui/src/v2/components/session-review-v2.tsx index d2f3b6b8a6..cb0a6bd8cb 100644 --- a/packages/session-ui/src/v2/components/session-review-v2.tsx +++ b/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")} - + } > @@ -241,7 +245,7 @@ export function SessionReviewV2(props: SessionReviewV2Props) { value={ <> {i18n.t("ui.sessionReviewV2.nextFile")} - + } > diff --git a/packages/ui/src/components/dropdown-menu.css b/packages/ui/src/components/dropdown-menu.css index edc2eee9a4..f8a9fa60ee 100644 --- a/packages/ui/src/components/dropdown-menu.css +++ b/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%); } diff --git a/packages/ui/src/components/icon.tsx b/packages/ui/src/components/icon.tsx index b46cdc833b..a16fad0c32 100644 --- a/packages/ui/src/components/icon.tsx +++ b/packages/ui/src/components/icon.tsx @@ -154,7 +154,14 @@ export function Icon(props: IconProps) {
.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 { diff --git a/packages/ui/src/components/tabs.tsx b/packages/ui/src/components/tabs.tsx index f46e9bfb30..0dd5fb3e23 100644 --- a/packages/ui/src/components/tabs.tsx +++ b/packages/ui/src/components/tabs.tsx @@ -80,6 +80,7 @@ function TabsTrigger(props: ParentProps) { >