fix(client): three more places follow the reading direction in Arabic

On a phone in Arabic every system notice, the release notes included,
opened as an empty sheet: its three-slot strip is shifted to show the
middle slot, and in a right-to-left row that shift moved all three out
of view. The shifts point the other way in a right-to-left page now,
which the swipe handling already assumed.

The budget's total amount kept its parts in order but sat on the
opposite edge from its label, because the whole row was made left to
right. Only the amount itself is now. The phone chat's bubble tails
were still physical corners while the desktop ones follow the row; both
use logical corners now.

lint:rtl takes comments from the TypeScript parser instead of scanning
lines, so a "/*" inside a string (accept="image/*") no longer hides the
lines after it.
This commit is contained in:
Maurice
2026-10-08 00:01:43 +00:00
parent 0126ef3401
commit d4eccb39a3
7 changed files with 68 additions and 46 deletions
@@ -57,11 +57,14 @@ export default function BudgetSummary({ theme, currency, locale, grandTotal, has
const sep = (0.1).toLocaleString(locale).replace(/\d/g, '')
const [integerPart, decimalPart] = decimals > 0 ? full.split(sep) : [full, '']
return (
// dir=ltr: the amount is split into spans, which a right-to-left flex row would reorder.
<div dir="ltr" style={{ display: 'flex', alignItems: 'baseline', gap: 4, letterSpacing: '-0.03em', lineHeight: 1 }}>
<span style={{ fontSize: 'calc(38px * var(--fs-scale-title, 1))', fontWeight: 700 }}>{integerPart}</span>
{decimalPart && <span style={{ fontSize: 'calc(22px * var(--fs-scale-title, 1))', fontWeight: 500, color: theme.sub }}>{sep}{decimalPart}</span>}
<span style={{ fontSize: 'calc(22px * var(--fs-scale-title, 1))', fontWeight: 500, color: theme.sub, marginInlineStart: 2 }}>{SYMBOLS[currency] || currency}</span>
<div style={{ display: 'flex', letterSpacing: '-0.03em', lineHeight: 1 }}>
{/* One left-to-right box at the row's start: the amount is split into
spans, which a right-to-left flex row would reorder. */}
<span dir="ltr" style={{ display: 'inline-flex', alignItems: 'baseline', gap: 4 }}>
<span style={{ fontSize: 'calc(38px * var(--fs-scale-title, 1))', fontWeight: 700 }}>{integerPart}</span>
{decimalPart && <span style={{ fontSize: 'calc(22px * var(--fs-scale-title, 1))', fontWeight: 500, color: theme.sub }}>{sep}{decimalPart}</span>}
<span style={{ fontSize: 'calc(22px * var(--fs-scale-title, 1))', fontWeight: 500, color: theme.sub, marginInlineStart: 2 }}>{SYMBOLS[currency] || currency}</span>
</span>
</div>
)
})()}
@@ -1199,10 +1199,13 @@ describe('ModalRenderer', () => {
await flushGraceDelay();
const { sheet, strip } = sheetParts();
// The slots run right to left, so the strip sits shifted the other way to
// show the current one; the left-to-right shift left the sheet empty.
expect(strip.style.transform).toBe('translateX(33.333%)');
await act(async () => {
swipeHorizontally(sheet, 120, 250);
});
expect(strip.style.transform).toBe('translateX(-66.666%)');
expect(strip.style.transform).toBe('translateX(66.666%)');
await act(async () => {
fireEvent.transitionEnd(strip);
@@ -668,6 +668,11 @@ function MobileNoticeSheet(S: NoticeState) {
const prevNotice = notices[idx - 1] ?? null;
const nextNotice = notices[idx + 1] ?? null;
// Where the 300% strip has to sit to show slot 0 (prev), 1 (current) or 2
// (next). In a right-to-left page the slots run the other way and the strip
// overflows to the left, so every shift points the other way; with the
// left-to-right shifts the sheet showed nothing at all in Arabic.
const stripShift = (slot: 0 | 1 | 2): number => (isRtlLanguage(language) ? 1 : -1) * slot * 33.333;
return (
<div className="fixed inset-0 z-[var(--z-notice)]" role="presentation">
@@ -718,7 +723,7 @@ function MobileNoticeSheet(S: NoticeState) {
if (!strip) return;
strip.style.transition = 'none';
// Strip base = -33.333% (center slot visible); dx offsets from there
strip.style.transform = `translateX(calc(-33.333% + ${dx}px))`;
strip.style.transform = `translateX(calc(${stripShift(1)}% + ${dx}px))`;
} else if (dragLockRef.current === 'v' && notice.dismissible) {
// Only intercept downward drag for dismiss when the sheet is scrolled to the top.
// If scrolled into content, let native pan-y scroll it back up.
@@ -751,7 +756,7 @@ function MobileNoticeSheet(S: NoticeState) {
if ((goNext && canGoNext) || (goPrev && canGoPrev)) {
// Animate strip to the adjacent slot (-66.666% = next, 0% = prev)
strip.style.transition = 'transform 200ms ease-out';
strip.style.transform = goNext ? 'translateX(-66.666%)' : 'translateX(0%)';
strip.style.transform = `translateX(${stripShift(goNext ? 2 : 0)}%)`;
strip.addEventListener('transitionend', function onDone() {
strip.removeEventListener('transitionend', onDone);
strip.style.transition = 'none';
@@ -767,16 +772,16 @@ function MobileNoticeSheet(S: NoticeState) {
prevSlotRef.current?.scrollTo({ top: 0 });
contentWrapperRef.current?.scrollTo({ top: 0 });
nextSlotRef.current?.scrollTo({ top: 0 });
strip.style.transform = 'translateX(-33.333%)';
strip.style.transform = `translateX(${stripShift(1)}%)`;
}, { once: true });
} else {
// Spring back to center
strip.style.transition = 'transform 300ms cubic-bezier(0.34,1.56,0.64,1)';
strip.style.transform = 'translateX(-33.333%)';
strip.style.transform = `translateX(${stripShift(1)}%)`;
strip.addEventListener('transitionend', function onSnap() {
strip.removeEventListener('transitionend', onSnap);
strip.style.transition = '';
strip.style.transform = 'translateX(-33.333%)';
strip.style.transform = `translateX(${stripShift(1)}%)`;
}, { once: true });
}
return;
@@ -809,7 +814,7 @@ function MobileNoticeSheet(S: NoticeState) {
{/* 3-slot strip: [prev][current][next] — starts at -33.333% to show current */}
<div
ref={stripRef}
style={{ display: 'flex', width: '300%', height: '100%', alignItems: 'stretch', transform: 'translateX(-33.333%)' }}
style={{ display: 'flex', width: '300%', height: '100%', alignItems: 'stretch', transform: `translateX(${stripShift(1)}%)` }}
>
{/* The side slots are drag previews only: their pager and CTA are a
second copy of the centre ones, so they stay out of the a11y tree