diff --git a/src/styles/rtl.css b/src/styles/rtl.css new file mode 100644 index 0000000..275c099 --- /dev/null +++ b/src/styles/rtl.css @@ -0,0 +1,100 @@ +/* ───────────────────────────────────────────────────────────────────────── + * RTL (Right-to-Left) consolidated styles + * + * This file centralizes RTL overrides that cannot be expressed through CSS + * logical properties alone — primarily gradient directions, transform origins, + * and physical-positioned elements that need explicit mirroring. + * + * ## What CSS logical properties handle automatically + * + * The following properties mirror under `dir="rtl"` without any extra CSS: + * - margin-inline-start / margin-inline-end + * - padding-inline-start / padding-inline-end + * - inset-inline-start / inset-inline-end + * - border-inline-start / border-inline-end + * - text-align: start / end + * - float: inline-start / inline-end + * + * ## What this file covers + * + * 1. Gradient angles (are physical, not logical) + * 2. Transform origins (default to physical center) + * 3. Box-shadow offsets (horizontal offset is physical) + * 4. Physical-positioned pseudo-elements + * 5. Explicit direction overrides for numeric content + * ───────────────────────────────────────────────────────────────────────── */ + +/* ── Gradient helper classes ─────────────────────────────────────────── */ + +[dir="rtl"] .rtl-mirror-gradient-to-right { + background-image: linear-gradient( + to left, + var(--rtl-grad-start, #10b981), + var(--rtl-grad-end, #3b82f6) + ); +} + +[dir="rtl"] .rtl-mirror-gradient-to-left { + background-image: linear-gradient( + to right, + var(--rtl-grad-start, #10b981), + var(--rtl-grad-end, #3b82f6) + ); +} + +/* ── Transform origin mirroring ──────────────────────────────────────── */ + +[dir="rtl"] .rtl-transform-origin-start { + transform-origin: right center; +} + +[dir="rtl"] .rtl-transform-origin-end { + transform-origin: left center; +} + +/* ── Numeric content: always LTR ─────────────────────────────────────── + Unicode bidi isolation prevents numeric sequences from being reordered + in RTL contexts. Per TR-9 and TR-53, numbers retain their natural order. ── */ + +.rtl-num { + direction: ltr; + unicode-bidi: isolate; + font-variant-numeric: tabular-nums; +} + +/* ── AppShell RTL adjustments ────────────────────────────────────────── */ + +[dir="rtl"] .app-shell__sidebar { + /* Sidebar slides from the right in RTL */ + transform-origin: right center; +} + +[dir="rtl"] .app-shell__main { + /* Main content area padding flips */ + padding-inline-start: var(--sidebar-width, 260px); + padding-inline-end: 0; +} + +/* ── Stepper / progress indicator RTL (shared across wizards) ────────── */ + +[dir="rtl"] .progress-indicator__fill { + transform-origin: right center; +} + +[dir="rtl"] .progress-indicator__connector--active { + background: linear-gradient( + to left, + var(--ws-completed, #10b981), + var(--ws-active, #3b82f6) + ); +} + +/* ── Reduced motion (global) ─────────────────────────────────────────── */ + +@media (prefers-reduced-motion: reduce) { + [dir="rtl"] .rtl-animate, + [dir="rtl"] .wizard-stepper__fill, + [dir="rtl"] .progress-indicator__fill { + transition: none; + } +} diff --git a/src/utils/rtl.ts b/src/utils/rtl.ts new file mode 100644 index 0000000..908bc94 --- /dev/null +++ b/src/utils/rtl.ts @@ -0,0 +1,104 @@ +/** + * RTL (Right-to-Left) utility helpers. + * + * These utilities provide a consistent API for detecting and responding to + * RTL language contexts across the application. They are used by the + * WizardStepper, AppShell, and other direction-sensitive components. + * + * ## Design principles + * + * 1. **CSS logical properties are the primary mechanism** — most RTL mirroring + * happens automatically through `margin-inline-start`, `inset-inline-end`, + * etc. These utilities handle the cases CSS can't. + * + * 2. **Numeric labels stay LTR** — per Unicode TR-9 and TR-53, numeric + * sequences (step numbers, percentages, counts) must retain their LTR + * order even in RTL contexts. Use `dir="ltr"` + `unicode-bidi: isolate` + * on every numeric element. + * + * 3. **Document order is NEVER reversed** — RTL mirroring is purely visual. + * The DOM order of wizard steps, tab lists, and other sequential elements + * matches the logical reading order for all languages. CSS (`flex-direction`, + * `direction`) handles the visual reversal. + * + * 4. **Gradients need explicit RTL overrides** — CSS gradients use `to right` + * or `to left` which are physical, not logical. Always provide a matching + * `[dir="rtl"]` rule that mirrors the gradient angle. + */ + +/** + * Returns `true` if the document's `` element has `dir="rtl"`. + * Defaults to `false` for SSR environments where `document` is unavailable. + */ +export function isRtl(): boolean { + if (typeof document === "undefined") return false; + return document.documentElement.getAttribute("dir") === "rtl"; +} + +/** + * Returns the CSS `direction` value for consumption in inline styles + * or CSS-in-JS: `"rtl"` or `"ltr"`. + */ +export function direction(): "rtl" | "ltr" { + return isRtl() ? "rtl" : "ltr"; +} + +/** + * Returns the inline-start value for a directional CSS property. + * In LTR this is `"left"`; in RTL this is `"right"`. + * + * Prefer CSS logical properties (`inset-inline-start`, `margin-inline-start`) + * over this function in production code. Use this only when computing + * imperative styles (e.g., positioning tooltips via `getBoundingClientRect`). + */ +export function inlineStart(): "left" | "right" { + return isRtl() ? "right" : "left"; +} + +/** + * Returns the inline-end value for a directional CSS property. + * In LTR this is `"right"`; in RTL this is `"left"`. + * + * @see {@link inlineStart} for usage guidance. + */ +export function inlineEnd(): "left" | "right" { + return isRtl() ? "left" : "right"; +} + +/** + * Mirrors a gradient angle for RTL contexts. + * + * Given a CSS gradient direction like `"to right"`, returns the mirrored + * equivalent `"to left"`. This is useful for progress bars, connector + * lines, and other gradient-based visual flows that must mirror under RTL. + * + * @param gradientDir - A CSS gradient direction keyword (e.g., `"to right"`) + * @returns The mirrored direction for RTL, or the original for LTR + */ +export function mirrorGradient(gradientDir: string): string { + const mirrors: Record = { + "to right": "to left", + "to left": "to right", + "to top right": "to top left", + "to top left": "to top right", + "to bottom right": "to bottom left", + "to bottom left": "to bottom right", + }; + if (!isRtl()) return gradientDir; + return mirrors[gradientDir] ?? gradientDir; +} + +/** + * Formats a step count for screen-reader announcement. + * + * The format is always LTR-numeric: "Step 2 of 5". + * This function isolates the numeric parts so they are never reordered + * by bidirectional text algorithms, even in RTL contexts. + * + * @param current - 1-based current step number + * @param total - Total number of steps + */ +export function formatStepCount(current: number, total: number): string { + // Use Unicode bidi isolation markers (LRI / PDI) + return `Step \u2066${current}\u2069 of \u2066${total}\u2069`; +}