From 55767fabbcd072108fcfa3a05cd5f290d12e89e8 Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Mon, 28 Sep 2026 22:28:16 +0000 Subject: [PATCH] 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. --- plugin-designs/doxa-research-map/research-map.vue | 6 +++--- plugin-designs/research-dashboard/research-dashboard.vue | 2 +- .../src/components/dashboards/DashboardDataTable.vue | 6 +++--- .../src/components/dashboards/DashboardDetailDrawer.vue | 4 ++-- .../src/components/dashboards/DashboardMapPanel.vue | 2 +- .../src/components/dashboards/DashboardTopBar.vue | 8 ++++---- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/plugin-designs/doxa-research-map/research-map.vue b/plugin-designs/doxa-research-map/research-map.vue index 0429ed9f6..28f6cb500 100644 --- a/plugin-designs/doxa-research-map/research-map.vue +++ b/plugin-designs/doxa-research-map/research-map.vue @@ -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 */ diff --git a/plugin-designs/research-dashboard/research-dashboard.vue b/plugin-designs/research-dashboard/research-dashboard.vue index c44458833..719cddd32 100644 --- a/plugin-designs/research-dashboard/research-dashboard.vue +++ b/plugin-designs/research-dashboard/research-dashboard.vue @@ -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 diff --git a/plugin-designs/research-dashboard/src/components/dashboards/DashboardDataTable.vue b/plugin-designs/research-dashboard/src/components/dashboards/DashboardDataTable.vue index 7d48f9647..96b62b530 100644 --- a/plugin-designs/research-dashboard/src/components/dashboards/DashboardDataTable.vue +++ b/plugin-designs/research-dashboard/src/components/dashboards/DashboardDataTable.vue @@ -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 + diff --git a/plugin-designs/research-dashboard/src/components/dashboards/DashboardDetailDrawer.vue b/plugin-designs/research-dashboard/src/components/dashboards/DashboardDetailDrawer.vue index ec81261e0..69eb98d3f 100644 --- a/plugin-designs/research-dashboard/src/components/dashboards/DashboardDetailDrawer.vue +++ b/plugin-designs/research-dashboard/src/components/dashboards/DashboardDetailDrawer.vue @@ -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); } diff --git a/plugin-designs/research-dashboard/src/components/dashboards/DashboardMapPanel.vue b/plugin-designs/research-dashboard/src/components/dashboards/DashboardMapPanel.vue index c4ce96583..a8cca240d 100644 --- a/plugin-designs/research-dashboard/src/components/dashboards/DashboardMapPanel.vue +++ b/plugin-designs/research-dashboard/src/components/dashboards/DashboardMapPanel.vue @@ -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 diff --git a/plugin-designs/research-dashboard/src/components/dashboards/DashboardTopBar.vue b/plugin-designs/research-dashboard/src/components/dashboards/DashboardTopBar.vue index e9892529a..1cbe59723 100644 --- a/plugin-designs/research-dashboard/src/components/dashboards/DashboardTopBar.vue +++ b/plugin-designs/research-dashboard/src/components/dashboards/DashboardTopBar.vue @@ -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.