rtl-layout
GitHub用于创建或修改 OpenChamber UI 的 RTL 布局,处理水平定位、偏移量、滚动计算及箭头键导航等。
Trigger Scenarios
Install
npx skills add openchamber/openchamber --skill rtl-layout -g -y
SKILL.md
Frontmatter
{
"name": "rtl-layout",
"description": "Use when creating or modifying OpenChamber UI layout, horizontal positioning, offsets or scroll math, resize or drag handles, arrow-key navigation, directional icons, or slide animations."
}
RTL Layout
Arabic and Hebrew mirror the whole interface. getLocaleDirection in packages/ui/src/lib/i18n/runtime.ts derives the direction from the UI locale; the i18n provider puts it on <html dir> and hands it to Base UI's DirectionProvider. Components read it as useI18n().direction, or as isRightToLeft(el) from @/lib/i18n in event handlers and code outside React. Do not call el.matches(':dir(rtl)'): it throws on WebKit before 16.4. Adding a right-to-left locale follows locale-ui-patterns and also puts it in RTL_LOCALES there.
Every layout change works in both directions. The test: English stays pixel-identical, and Arabic is its mirror image.
Logical by default
Write sides as start and end of the line: ms-/me-, ps-/pe-, inset-s-/inset-e-, text-start/text-end, rounded-s/rounded-e/rounded-ss, border-s/border-e; in CSS and inline styles margin-inline-start, padding-inline-start, inset-inline-start. Flex and grid already follow dir, so DOM order stays semantic.
What CSS cannot express flips with the rtl: variant: translate-x-* (data-[checked]:translate-x-4 rtl:data-[checked]:-translate-x-4), bg-gradient-to-r rtl:bg-gradient-to-l, origin-top-left rtl:origin-top-right, and inline transforms built from a sign. A fade toward the end of a line uses linear-gradient(var(--oc-to-inline-end), …).
Physical on purpose
Some sides belong to the screen, not the line. The lint rule openchamber/no-physical-sides (tools/oxlint/openchamber, enabled in oxlint.config.ts) flags physical classes in packages/ui class lists, so a deliberate one carries // oxlint-disable-next-line openchamber/no-physical-sides -- <why it is physical>:
- OS window controls. macOS traffic lights stay top left in every language; frameless Windows/Linux controls stay on the side the user picked. The titlebar publishes
--oc-titlebar-start-insetand maps controls throughgetInlineStartSide(direction)(seeHeader.tsx,TitlebarLeftControls.tsx). - Geometry from measurement. An element moved by
offsetLeft,getBoundingClientRect().leftorclientXanchors withleft-0, because the numbers are physical (the active-tab pill insortable-tabs-strip.tsx). - Content that reads left to right: code, diffs, terminal output and its key strip, file paths and path fields, URLs, IDs, charts along a time axis. The container gets
dir="ltr", so its own logical classes resolve as physical inside it. - Keyboard keys and labels that name a physical key ("close tabs to the left" means the screen's left).
Direction-aware behaviour
- Resize and drag. A handle on a panel's edge changes width by the pointer delta times a sign:
(clientX - startX) * (rtl ? -1 : 1)for a start-side panel. Arrow keys on the handle take the same sign. - Horizontal scroll. In RTL
scrollLeftis 0 at the start and negative toward the end. Distance from the start isMath.abs(scrollLeft); distance from the physical left isscrollWidth - clientWidth + scrollLeft. Wheel-to-horizontal mapping flips the delta. - Previous/next keys. ArrowLeft and ArrowRight swap meaning when they step through a horizontal sequence (tabs, image viewer, thinking levels).
- Sorting.
horizontalListSortingStrategyassumes later items sit further right; passrectSortingStrategyin RTL (seedrag-to-reorder). - Slide-ins. A drawer, pushed page or row swipe moves toward the side its trigger sits on; compute the offset sign from
directionand mirror edge swipes with it (MobileApp.tsx,MobileSessionSwipe.tsx).
Mixed-direction text
User content (session titles, run names, file names in lists) gets dir="auto" so it shows its own start when truncated. Text whose direction differs from the UI aligns with the interface through oc-ui-start-align, so it lines up with the rows around it. Values made of numbers and units ("~41 tok/s", "6.6K ↑ · 242 ↓") also take dir="auto". Markdown already resolves direction per block; see components/chat/message/parts/DOCUMENTATION.md before touching it.
Icons
DIRECTIONAL_ICONS in components/icon/Icon.tsx lists the icons that mirror in RTL (back/forward, chevrons, panel sides, send); one CSS rule flips them through :dir(rtl), so an icon inside a dir="ltr" block stays as drawn. A new icon whose meaning points somewhere joins the set. The flip happens before the <svg>'s own rotation, so a chevron that turns a quarter turn pairs it: rotate-90 rtl:-rotate-90.
Verify
- Take English screenshots of the affected screens before the change and after it; they match pixel for pixel.
- Switch to Arabic (
localStorage['openchamber.i18n.v1'] = '{"locale":"ar"}') and check the same screens, then drag, scroll and use the arrow keys on whatever changed. - For phone layouts, check the drawer and swipe direction at a 390px touch viewport.
Version History
- bf977d2 Current 2026-10-19 13:05


