You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Add RTL (right-to-left) language support across the component library
Summary
Support RTL languages (Arabic, Hebrew, Farsi, Urdu) at the component level.
Components should be direction-agnostic by default using CSS logical
properties, with a Storybook toolbar switch (alongside brand / theme / density /
language) to preview RTL.
PR series status (updated 2026-09-10, supersedes #320)
Work lands as a series of small targeted PRs. Each merge cuts a 0.7.2-dev.x
release for in-the-wild validation; full release after the series completes.
Phase 2 batch 1 — #430 (merged): Toast logical positions (physical values
kept as deprecated aliases) + FloatingWindow. The ratchet baseline is now zero — any new physical-direction class in src/components fails CI unless
annotated with // rtl-ignore -- <reason>. The scanner now prints per-offense
logical-equivalent suggestions and inline PR annotations on failure. #430 also
fixed missing Tailwind 4 --animate-* theme tokens (the Toast slide-in
animations never fired under TW4; remaining animation-token gaps are a
candidate follow-up issue).
Phase 2 batch 2 — #438 (merged): Directional navigation icons mirror in RTL
via rtl:-scale-x-100 across 16 components — pagination/breadcrumb chevrons,
back buttons (SiteHeader, PatientHeader, CaseManagementHeader,
ProviderDetailHeader, CodeLookup, ComposerModelSelector), calendar month nav
(DateRangePicker, DateInput), submenu/drill/expand chevrons (Dropdown, Sidebar,
HealthSurveillance, PermissionsEditor), and link arrows (GlossaryTooltip,
SectionSpyNav). Per Material bidirectionality guidance, checkmarks, media
controls, and up/down chevrons deliberately do not flip. Also fixed a
physical inline marginLeft in HealthSurveillance (inline styles evade the
scanner) and rewrote 11 stale story RTL notes that predated Phase 1.
Tailwind 4 — native logical utilities (ms-/me-/ps-/pe-/start-/end-/ text-start/rounded-s) and rtl:/ltr: variants are available
Storybook toolbar globals (brand/theme/density/locale) live in .storybook/preview.tsx with a shared applyGlobalTheme() — the right place
to also set dir
No dir/direction infrastructure exists anywhere yet
Guiding principle
Logical properties first, rtl: variants last. Make components
direction-agnostic so RTL "just works" from dir="rtl". Reserve rtl:
overrides for true exceptions (icon flips, transforms). Avoid doubling the
class surface with per-direction overrides.
Phase 0 — Infrastructure (one PR, do first) — ✅ shipped in #356
Add direction Storybook global (ltr / rtl / auto) next to theme/density/locale
auto derives direction from the locale global (rtl for ar, he, fa, ur)
Wire into applyGlobalTheme() to set dir on document.documentElement (works on docs-only MDX pages, same pattern as dark mode)
Add an RTL sample locale (e.g. العربية (sample)) to the locale toolbar for real RTL text
Runtime direction API: useDirection() hook + isRtlLocale/RTL_LOCALES for JS-level direction logic (keyboard arrows, popover placement, drag math). Default: read from closest dir attribute — zero setup for consumers who set ``
Enforcement: CI script (scripts/rtl-scan.mjs ratchet, like ccme-scan.mjs) that fails on new physical-direction classes in src/components — prevents regression while migration is in flight
Phase 1 — Mechanical codemod (bulk of the 137 files) — ✅ complete (#357–#429)
Scripted, reviewable find/replace over className strings, run per component
batch (small PRs — see PR series status above):
Docs: add "direction-agnostic styling" section to copilot-instructions / lessons so future components are born RTL-safe
First milestone
Phase 0 + codemod on ~10 primitives, verified with the new toolbar switch.
Proves the full pipeline (switch → logical classes → visual check → lint guard)
Add RTL (right-to-left) language support across the component library
Summary
Support RTL languages (Arabic, Hebrew, Farsi, Urdu) at the component level.
Components should be direction-agnostic by default using CSS logical
properties, with a Storybook toolbar switch (alongside brand / theme / density /
language) to preview RTL.
PR series status (updated 2026-09-10, supersedes #320)
Work lands as a series of small targeted PRs. Each merge cuts a
0.7.2-dev.xrelease for in-the-wild validation; full release after the series completes.
Phase 1 codemod series complete.
Phase 2 batch 1 — #430 (merged): Toast logical positions (physical values
kept as deprecated aliases) + FloatingWindow. The ratchet baseline is now
zero — any new physical-direction class in
src/componentsfails CI unlessannotated with
// rtl-ignore -- <reason>. The scanner now prints per-offenselogical-equivalent suggestions and inline PR annotations on failure. #430 also
fixed missing Tailwind 4
--animate-*theme tokens (the Toast slide-inanimations never fired under TW4; remaining animation-token gaps are a
candidate follow-up issue).
Phase 2 batch 2 — #438 (merged): Directional navigation icons mirror in RTL
via
rtl:-scale-x-100across 16 components — pagination/breadcrumb chevrons,back buttons (SiteHeader, PatientHeader, CaseManagementHeader,
ProviderDetailHeader, CodeLookup, ComposerModelSelector), calendar month nav
(DateRangePicker, DateInput), submenu/drill/expand chevrons (Dropdown, Sidebar,
HealthSurveillance, PermissionsEditor), and link arrows (GlossaryTooltip,
SectionSpyNav). Per Material bidirectionality guidance, checkmarks, media
controls, and up/down chevrons deliberately do not flip. Also fixed a
physical inline
marginLeftin HealthSurveillance (inline styles evade thescanner) and rewrote 11 stale story RTL notes that predated Phase 1.
Remaining Phase 2 work (keyboard nav, JS positioning remainder, LTR islands,
transforms remainder, third-party RTL flags) continues below.
Current state (measured 2026-07-21)
.tsxfiles; 137 contain physical-direction Tailwind classes(
ml-,pr-,left-,text-left,rounded-l,border-l,space-x-, …)ms-/me-/ps-/pe-/start-/end-/text-start/rounded-s) andrtl:/ltr:variants are available.storybook/preview.tsxwith a sharedapplyGlobalTheme()— the right placeto also set
dirdir/direction infrastructure exists anywhere yetGuiding principle
Logical properties first,
rtl:variants last. Make componentsdirection-agnostic so RTL "just works" from
dir="rtl". Reservertl:overrides for true exceptions (icon flips, transforms). Avoid doubling the
class surface with per-direction overrides.
Phase 0 — Infrastructure (one PR, do first) — ✅ shipped in #356
directionStorybook global (ltr/rtl/auto) next to theme/density/localeautoderives direction from thelocaleglobal (rtl forar,he,fa,ur)applyGlobalTheme()to setdirondocument.documentElement(works on docs-only MDX pages, same pattern as dark mode)العربية (sample)) to the locale toolbar for real RTL textuseDirection()hook +isRtlLocale/RTL_LOCALESfor JS-level direction logic (keyboard arrows, popover placement, drag math). Default: read from closestdirattribute — zero setup for consumers who set ``scripts/rtl-scan.mjsratchet, likeccme-scan.mjs) that fails on new physical-direction classes insrc/components— prevents regression while migration is in flightPhase 1 — Mechanical codemod (bulk of the 137 files) — ✅ complete (#357–#429)
Scripted, reviewable find/replace over
classNamestrings, run per componentbatch (small PRs — see PR series status above):
ml-*/mr-*ms-*/me-*pl-*/pr-*ps-*/pe-*left-*/right-*start-*/end-*text-left/text-righttext-start/text-endrounded-l*/rounded-r*/ cornersrounded-s*/rounded-e*/rounded-ss…border-l*/border-r*border-s*/border-e*space-x-*flex gap-*; elsertl:space-x-reversedivide-x-*rtl:divide-x-reversescripts/rtl-codemod.mjs, shipped in feat(rtl): RTL infrastructure — scan guard, codemod, useDirection, Storybook direction toolbar #356); it also flags dynamic class construction it can't safely rewrite (cond ? 'ml-2' : …,cn()calls, class maps) → produces the Phase 2 worklistPhase 2 — Manual/semantic pass (needs judgment, per component)
rtl:-scale-x-100); checkmarks, media buttons, logos, up/down chevrons do NOT flip. (Send/Reply/Undo/Redo-type action icons not yet audited — most grep hits were identifiers, not JSX.)useDirection())translate-x-*,slide-in-from-left, Switch thumb travel, Sheet/Sidebar slide directions (Toast slide-in done in feat(rtl): logical Toast positions, FloatingWindow cleanup, zero the ratchet baseline #430)top/bottom-start/center/end, physical values kept as deprecated aliases); FloatingWindow ✅ resolved in feat(rtl): logical Toast positions, FloatingWindow cleanup, zero the ratchet baseline #430 (resize/drag math is deliberately physical, exempted viartl-ignore; minimized-bar classes made logical)dir="ltr"enableRtl), DataVis/charts, RichTextEditor/Kerebron, ESheet renderer, YChartPhase 3 — Rollout order
rtl:mirrors)Phase 4 — Verification & guardrails
dirvalues (2× matrix only for direction-sensitive components)First milestone
Phase 0 + codemod on ~10 primitives, verified with the new toolbar switch.
Proves the full pipeline (switch → logical classes → visual check → lint guard)