Arabic mirrored the layout but not these sixteen rules

An RTL audit of research-map, research-dashboard and its four dashboard
components. Translation was already sound -- 101 keys, all present in all eleven
locales, no hardcoded English anywhere user-visible -- so what was left was
direction: rules that pin a physical side while the reader's text flows the other
way.

Three an Arabic reader would notice immediately. Both search icons sit at
left:8px and left:10px while the input around them mirrors, so the icon ends up
on the far side of the field it belongs to. The detail drawer's border-left
stays on the geometric left, which in Arabic is the drawer's trailing edge, so
the panel appears unattached on the side it hinges from.

The rest are quieter but the same mistake: a badge, a provider chip, the two
edge affordances, a table's numeric columns and its indicator, a result row's
spacer, the projection toggle.

Every change is LTR-NEUTRAL, which is what makes this safe to land on its own.
In left-to-right, inset-inline-start resolves to left, text-align:end resolves
to right, margin-inline-start resolves to margin-left. The English view cannot
move; only the Arabic one can. Fourteen insertions against fourteen deletions,
one property each, nothing else touched.

The pattern is not new here -- .rd-legend-map already uses inset-inline-end,
with a comment recording that a physical right pinned it to the wrong edge in
Arabic. These sixteen are the rules that were missed when that lesson was
applied.

What this does NOT establish: that Arabic renders correctly. Every RTL gate in
the repo needs a browser, so this was read rather than run. The three visible
ones want eyes on ar before anyone calls RTL done.
This commit is contained in:
Doxa Maps 2026-09-28 22:28:16 +00:00
parent f7e9ed1ea7
commit 55767fabbc
6 changed files with 14 additions and 14 deletions

View file

@ -268,7 +268,7 @@ useShadowStyles(`
.dg-main { font-weight:500;font-size:13px;color:#222;line-height:1.3;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:flex;align-items:center;gap:6px; }
.dg-main strong { font-weight:700;overflow:hidden;text-overflow:ellipsis; }
.dg-meta { margin-top:2px;display:flex;flex-wrap:wrap;gap:4px 10px;font-size:11px;color:#555;line-height:1.35; }
.dg-field strong { font-weight:600;color:#333;margin-right:2px; }
.dg-field strong { font-weight:600;color:#333;margin-inline-end:2px; }
/* Round 16 — labeled category tag replacing the old emoji prefixes. */
.dg-tag { flex:none;font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:0.4px;color:#3b463d;background:rgba(59,70,61,0.10);border-radius:4px;padding:1px 5px;line-height:1.5;white-space:nowrap; }
.dg-agg-label { overflow:hidden;text-overflow:ellipsis; }
@ -424,10 +424,10 @@ useShadowStyles(`
map did manage to render stays visible around it. A spinner that outlives the
failure reads as "slow" and the user just waits; this says the map is gone. */
.rm-error { position:absolute;top:50%;left:12px;right:12px;z-index:7;transform:translateY(-50%);text-align:center;font:13px/1.5 -apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;color:#fecaca;background:rgba(15,18,22,.95);border:1px solid rgba(254,202,202,.28);border-radius:10px;padding:12px 14px;box-shadow:0 2px 10px rgba(0,0,0,.4); }
.rm-badge { position:absolute;top:10px;right:10px;background:rgba(124,140,248,0.12);color:#7c8cf8;border:1px solid rgba(124,140,248,0.3);padding:3px 9px;border-radius:4px;font-size:11px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;pointer-events:none;z-index:50; }
.rm-badge { position:absolute;top:10px;inset-inline-end:10px;background:rgba(124,140,248,0.12);color:#7c8cf8;border:1px solid rgba(124,140,248,0.3);padding:3px 9px;border-radius:4px;font-size:11px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;pointer-events:none;z-index:50; }
/* Engine badge — bottom-left, unobtrusive. Colour differs per engine so a
side-by-side comparison is readable at a glance. */
.rm-provider-badge { position:absolute;bottom:8px;left:8px;z-index:50;pointer-events:none;padding:2px 8px;border-radius:999px;font:600 10px/1.6 -apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;letter-spacing:.04em;color:#fff;background:rgba(30,41,59,0.72);border:1px solid rgba(255,255,255,0.18);backdrop-filter:blur(2px); }
.rm-provider-badge { position:absolute;bottom:8px;inset-inline-start:8px;z-index:50;pointer-events:none;padding:2px 8px;border-radius:999px;font:600 10px/1.6 -apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;letter-spacing:.04em;color:#fff;background:rgba(30,41,59,0.72);border:1px solid rgba(255,255,255,0.18);backdrop-filter:blur(2px); }
.rm-provider-badge[data-provider="mapbox"] { background:rgba(37,99,235,0.78);border-color:rgba(147,197,253,0.5); }
.rm-provider-badge[data-provider="maplibre"] { background:rgba(5,150,105,0.80);border-color:rgba(110,231,183,0.5); }
/* Mobile-only legend slot — desktop hides via @media */

View file

@ -423,7 +423,7 @@ useShadowStyles(`
border-radius:5px; font-size:11px;
line-height:1; box-shadow:0 1px 4px rgba(0,0,0,.14); }
.rd-edge-caret:hover{ background:var(--dx-hover,rgba(59,70,61,.06)); }
.rd-edge-facet{ left:5px; } .rd-edge-rail{ right:5px; }
.rd-edge-facet{ inset-inline-start:5px; } .rd-edge-rail{ inset-inline-end:5px; }
/* (the floating .rd-table-restore pill is retired — the table's own always-docked header bar
carries the chevron, the live count and Export CSV while collapsed) */
/* Medium-width niceties. These keep the SAME rail balance as the defaults above (tree wider

View file

@ -713,10 +713,10 @@ useShadowStyles(`
so nothing is clipped in practice, and hidden keeps a pathological config (a 40-character
label with an explicit narrow minW) from painting over the next column. */
.rd-th{ flex:1 1 0; min-width:0; padding:4px 4px; font-weight:700; color:var(--dx-text, #3b463d); cursor:pointer; user-select:none; white-space:nowrap; overflow:hidden; }
.rd-th.num{ text-align:right; } .rd-th.chk{ flex:0 0 30px; cursor:default; text-align:center; padding:6px 0; }
.rd-th.num{ text-align:end; } .rd-th.chk{ flex:0 0 30px; cursor:default; text-align:center; padding:6px 0; }
/* sort indicator — the glyph carries the meaning; colour only needs to read as ACTIVE
against the muted header row, so it takes primary ink rather than a hue of its own. */
.rd-th .ind{ font-size:9px; color:var(--dx-text, #3b463d); margin-left:3px; }
.rd-th .ind{ font-size:9px; color:var(--dx-text, #3b463d); margin-inline-start:3px; }
/* ★ THE ROW WIDTH IS SHARED, NEVER INTRINSIC — and getting this wrong is invisible in the
numbers. Two facts had to be reconciled:
(1) min-width ALONE DOES NOT CREATE OVERFLOW. A block-level flex container inside the
@ -736,7 +736,7 @@ useShadowStyles(`
.rd-row{ display:flex; width:100%; height:26px; line-height:16px; border-bottom:1px solid var(--dx-surface, #f3f3f1); align-items:center; }
.rd-row:hover{ background:var(--dx-surface, #f3f3f1); } .rd-row.sel{ background:#fff7ec; box-shadow:inset 3px 0 0 #e67e22; }
.rd-td{ flex:1 1 0; min-width:0; padding:0 4px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; color:var(--dx-text, #3b463d); }
.rd-td.num{ text-align:right; font-variant-numeric:tabular-nums; } .rd-td.chk{ flex:0 0 30px; text-align:center; padding:0; }
.rd-td.num{ text-align:end; font-variant-numeric:tabular-nums; } .rd-td.chk{ flex:0 0 30px; text-align:center; padding:0; }
/* ★ THE FOOTER IS A SEPARATE SCROLLPORT and has to be told to follow. .rd-foot is a SIBLING of
.rd-grid, not a child, so scrolling the grid right does not move it — the Σ total would slide
out of alignment with the Population column the moment the table scrolled. overflow:hidden +

View file

@ -114,7 +114,7 @@ function clearDetail() {
}
useShadowStyles(`
.rd-detail{ box-sizing:border-box; background:var(--rd-panel,#fff); border-left:1px solid var(--rd-line, #d1d4cd);
.rd-detail{ box-sizing:border-box; background:var(--rd-panel,#fff); border-inline-start:1px solid var(--rd-line, #d1d4cd);
border-bottom:1px solid var(--rd-line, #d1d4cd); padding:var(--s3,12px);
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif; color:var(--rd-ink, #3b463d); flex:0 0 auto; }
.rd-detail .rd-dh{ display:flex; align-items:center; justify-content:space-between; gap:8px;
@ -137,7 +137,7 @@ useShadowStyles(`
padding:2px 0; border-bottom:1px solid var(--rd-line, #d1d4cd); }
.rd-detail .rd-row:last-child{ border-bottom:0; }
.rd-detail .rd-row .k{ color:var(--rd-muted, #6d766e); flex:0 0 auto; min-width:118px; }
.rd-detail .rd-row .v{ color:var(--rd-ink, #3b463d); margin-left:auto; text-align:right; font-variant-numeric:tabular-nums; }
.rd-detail .rd-row .v{ color:var(--rd-ink, #3b463d); margin-inline-start:auto; text-align:end; font-variant-numeric:tabular-nums; }
.rd-detail .rd-row .v .fam{ color:var(--rd-muted, #6d766e); }
.rd-detail .rd-exp{ margin-top:var(--s3,12px); width:100%; border:0; border-radius:var(--r-md,6px); cursor:pointer;
background:var(--rd-accent,#e67e22); color:#fff; font-weight:700; font-size:var(--fs-md,12px); padding:var(--s2,8px) var(--s3,12px); }

View file

@ -1599,7 +1599,7 @@ useShadowStyles(`
shows on engines without fog support (MapLibre), where black is the honest fallback. */
.rd-mapwrap[data-projection="globe"]{ background:#000; }
.rd-mapwrap .rd-mapc{ position:absolute; inset:0; }
.rd-proj{ position:absolute; right:8px; top:8px; z-index:3; font-size:11px; font-weight:700; cursor:pointer; user-select:none;
.rd-proj{ position:absolute; inset-inline-end:8px; top:8px; z-index:3; font-size:11px; font-weight:700; cursor:pointer; user-select:none;
background:#1a1a2ecc; color:#fff; border:1px solid #ffffff33; border-radius:6px; padding:4px 9px; backdrop-filter:blur(4px); }
.rd-proj:hover{ background:#1a1a2e; }
/* ★ TOUCH TARGET: 24px, STATED, AT EVERY WIDTH. Measured on a phone (390x844) this rendered

View file

@ -260,11 +260,11 @@ useShadowStyles(`
.rd-topbar .rd-filter-search .search-box input{ height:32px; padding:0 10px 0 34px; font-size:var(--fs-md,12px);
border-radius:16px; border-color:#ffffff3d; }
.rd-topbar .rd-filter-search .search-box input:focus{ border-color:var(--rd-accent,#e67e22); }
.rd-topbar .rd-filter-search .search-icon{ left:10px; width:15px; height:15px; }
.rd-topbar .rd-filter-search .search-icon{ inset-inline-start:10px; width:15px; height:15px; }
/* narrow: return the search to normal flow (drop the absolute centring) and let it take the row
width; the align button is already hidden here, so a centred search has no right-hand anchor. */
.rd-shell.rd-narrow .rd-topbar .rd-filter-search{ position:static; transform:none;
left:auto; top:auto; flex:1 1 auto; width:auto; max-width:none; }
inset-inline-start:auto; top:auto; flex:1 1 auto; width:auto; max-width:none; }
/* search — translucent light field so typed text stays legible on the green bar */
.rd-search{ position:relative; flex:0 1 280px; min-width:140px; }
.rd-search input{ width:100%; box-sizing:border-box; height:30px; padding:0 10px 0 28px; font-size:var(--fs-md,12px);
@ -272,14 +272,14 @@ useShadowStyles(`
border-radius:var(--r-md,6px); outline:none; }
.rd-search input:focus{ border-color:var(--rd-accent,#e67e22); background:#ffffff33; }
.rd-search input::placeholder{ color:var(--rd-header-muted,#cbc5b9); }
.rd-search .ic{ position:absolute; left:8px; top:50%; transform:translateY(-50%); font-size:12px; color:var(--rd-header-muted,#cbc5b9); pointer-events:none; }
.rd-search .ic{ position:absolute; inset-inline-start:8px; top:50%; transform:translateY(-50%); font-size:12px; color:var(--rd-header-muted,#cbc5b9); pointer-events:none; }
.rd-results{ position:absolute; left:0; right:0; top:34px; z-index:60; background:#fff; border:1px solid var(--rd-line, #d1d4cd);
border-radius:var(--r-md,6px); box-shadow:0 8px 24px #1f273322; overflow:hidden; max-height:320px; overflow-y:auto;
scrollbar-width:thin; scrollbar-color:var(--dx-scroll-thumb, #c4c8c0) transparent; }
.rd-results .rd-res{ display:flex; align-items:baseline; gap:8px; padding:6px 10px; cursor:pointer; font-size:var(--fs-md,12px); }
.rd-results .rd-res:hover, .rd-results .rd-res.on{ background:var(--rd-bg, #f3f3f1); }
.rd-results .rd-res .nm{ font-weight:700; color:var(--rd-ink, #3b463d); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.rd-results .rd-res .sub{ margin-left:auto; color:var(--rd-muted, #6d766e); font-size:var(--fs-sm,11px); white-space:nowrap; flex:0 0 auto; }
.rd-results .rd-res .sub{ margin-inline-start:auto; color:var(--rd-muted, #6d766e); font-size:var(--fs-sm,11px); white-space:nowrap; flex:0 0 auto; }
.rd-results .rd-empty{ padding:8px 10px; color:var(--rd-muted, #6d766e); font-size:var(--fs-sm,11px); }
/* (csv button + toast styles moved to DashboardDataTable's header bar) */
/* Align Dashboard — subtle ghost button on the green bar; snaps the dragged layout to defaults.