Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions src/styles/rtl.css
Original file line number Diff line number Diff line change
@@ -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;
}
}
104 changes: 104 additions & 0 deletions src/utils/rtl.ts
Original file line number Diff line number Diff line change
@@ -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 `<html>` 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<string, string> = {
"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`;
}